Swift Package Manager

aazmjr5f_19lcu43febz4buwhcu.jpeg
Вместе с релизом в open source языка Swift 3 декабря 2015 года Apple представила децентрализованный менеджер зависимостей Swift Package Manager.

К публичной версии приложили руку небезызвестные Max Howell, создатель Homebrew, и Matt Thompson, написавший AFNetworking. SwiftPM призван автоматизировать процесс установки зависимостей, а также дальнейшее тестирование и сборку проекта на языке Swift на всех доступных операционных системах, однако пока его поддерживают только macOS и Linux. Если интересно, идите под кат.

Минимальные требования — Swift 3.0. Чтобы открыть файл проекта потребуется Xcode 8.0 или выше. SwiftPM позволяет работать с проектами без xcodeproj-файла, поэтому Xcode на OS X не обязателен, а на Linux его и так нет.

Стоит развеять сомнения — проект еще в активной разработке. Использование UIKit, AppKit и других фреймворков iOS и OS X SDK как зависимостей недоступно, так как SwiftPM подключает зависимости в виде исходного кода, который потом собирает. Таким образом, использование SwiftPM на iOS, watchOS и tvOS возможно, но только с использованием Foundation и зависимостей сторонних библиотек из открытого доступа. Один единственный import UIKit делает вашу библиотеку непригодной для распространения через SwiftPM.

Все примеры в статье написаны с использованием версии 4.0.0-dev, свою версию можете проверить с помощью команды в терминале

swift package —version


Идеология Swift Package Manager


Для работы над проектом больше не нужен файл *.xcodproj — теперь его можно использовать как вспомогательный инструмент. Какие файлы участвуют в сборке модуля, зависит от их расположения на диске — для SwiftPM важны имена директорий и их иерархия внутри проекта. Первоначальная структура директории проекта выглядит следующим образом:

  • Sources — исходные файлы для сборки пакета, разбитые внутри по директориям продуктов — для каждого продукта отдельная папка.
  • Tests — тесты для разрабатываемого продукта, разбивка на папки аналогично папке Sources.
  • Package.swift — файл с описанием пакета.
  • README.md — файл документации к пакету.


Внутри папок Sources и Tests SwiftPM рекурсивно ищет все *.swift-файлы и ассоциирует их с корневой папкой. Чуть позже мы создадим подпапки с файлами.

lq8g_ooqntka_j29x1fd4olxtd0.png

Основные компоненты


Теперь давайте разберемся с основными компонентами в SwiftPM:

  • Модуль (Module) — набор *.swift–файлов, выполняющий определенную задачу. Один модуль может использовать функционал другого модуля, который он подключает как зависимость. Проект может быть собран на основании единственного модуля. Разделение исходного кода на модули позволяет выделить в отдельный модуль функцию, которую можно будет использовать повторно при сборке другого проекта. Например, модуль сетевых запросов или модуль работы с базой данных. Модуль использует порог инкапсуляции уровня internal и представляет собой библиотеку (library), которая может быть подключена к проекту. Модуль может быть подключен как из того же самого пакета (представлен в виде другого таргета), так и из другого пакета (представлен в виде другого продукта).
  • Продукт (Product) — результат сборки таргета (target) проекта. Это может быть библиотека (library) или исполняемый файл (executable). Продукт включает себя исходный код, который относится непосредственно к этому продукту, а также исходный код модулей, от которых он зависит.
  • Пакет (Package) — набор *.swift–файлов и manifest-файла Package.swift, который определяет имя пакета и набор исходных файлов. Пакет содержит один или несколько модулей.
  • Зависимость (Dependency) — модуль, необходимый для исходного кода в пакете. У зависимости должен быть путь (относительный локальный или удаленный на git-репозиторий), версия, перечень зависимостей. SwiftPM должен иметь доступ к исходному коду зависимости для их компиляции и подключения к основному модулю. В качестве зависимости таргета может выступать таргет из того же пакета или из пакета-зависимости.


ny-u9an2ijnklns0apbpjzmcbvm.jpeg

Получаем, что зависимости выстраиваются в граф — у каждой зависимости могут быть свои собственные и так далее. Разрешение графа зависимостей — основная задача менеджера зависимостей.

Замечу, что все исходные файлы должны быть написаны на языке Swift, возможности использовать язык Objective-C — нет.

Каждый пакет должен быть самодостаточным и изолированным. Его отладка производится не посредством запуска (run), а с помощью логических тестов (test).

Далее рассмотрим простой пример с подключением к проекту зависимости Alamofire.

Разработка тестового проекта


Перейдем через терминал в папку, где будет лежать наш проект, создадим для него директорию и перейдем в нее.

mkdir IPInfoExample
cd IPInfoExample/


Далее инициализируем пакет с помощью команды

swift package init


В результате создается следующая иерархия исходных файлов


├── Package.swift
├── README.md
├── Sources
│   └── IPInfoExample
│       └── main.swift
└── Tests
     └── IPInfoExampleTests
         ├ LinuxMain.swift
         └── IPInfoExampleTests
             └── IPInfoExampleTests.swift


В условиях отсутствия индекса файла проекта *.xcodeproj менеджеру зависимостей нужно знать, какие исходные файлы должны участвовать в процессе сборки и в какие таргеты их включать. Поэтому SwiftPM определяет строгую иерархию папок и перечень файлов:

  • Package-файл;
  • README-файл;
  • Папка Sources с исходными файлами — отдельная папка для каждого таргета;
  • Папка Tests — отдельная папка для каждого тестового таргета.


Уже сейчас можем выполнить команды


swift build
swift test


для сборки пакета или для запуска теста Hello, world!

Добавление исходных файлов


Создадим файл Application.swift и положим его в папку IPInfoExample.

public struct Application {}

Выполняем swift build и видим, что в модуле уже компилируется 2 файла.

Compile Swift Module 'IPInfoExample' (2 sources)


Создадим директорию Model в папке IPInfoExample, создадим файл IPInfo.swift, а файл IPInfoExample.swift удалим за ненадобностью.


//Используем протокол Codable для маппинга JSON в объект
public struct IPInfo: Codable { 
    let ip: String

    let city: String

    let region: String

    let country: String
}


После этого выполним команду swift build для проверки.

Добавление зависимостей


Откроем файл Package.swift, содержание полно описывает ваш пакет: имя пакета, зависимости, таргет. Добавим зависимость Alamofire.

// swift-tools-version:4.0
import PackageDescription // Модуль, в котором находится описание пакета

let package = Package(
    name: "IPInfoExample", // Имя нашего пакета
    products: [
        .library(
            name: "IPInfoExample",
            targets: ["IPInfoExample"]),
    ],
    dependencies: [
        // подключаем зависимость-пакет Alamofire, указываем ссылку на GitHub
        .package(url: "https://github.com/Alamofire/Alamofire.git", from: "4.0.0") 
    ],
    targets: [
        .target(
            name: "IPInfoExample",
            // указываем целевой продукт – библиотеку, которая зависима 
            // от библиотеки Alamofire
            dependencies: ["Alamofire"]), 
        .testTarget(
            name: "IPInfoExampleTests",
            dependencies: ["IPInfoExample"]),
    ]
)


Далее снова swift build, и наши зависимости скачиваются, создается файл Package.resolved c описанием установленной зависимости (аналогично Podfile.lock).

В случае если в вашем пакете только один продукт, можно использовать одинаковые имена для имени пакета, продукта и таргета. У нас это IPInfoExample. Таким образом, описание пакета можно сократить, опустив параметр products. Если заглянуть в описание пакета Alamofire, увидим, что там не описаны таргеты. По умолчанию создаются один таргет с именем пакета и файлами исходного кода из папки Sources и один таргет с файлом-описанием пакета (PackageDescription). Тестовый таргет при использовании SwiftPM не задействуется, поэтому папка с тестами исключается.


import PackageDescription

let package = Package(name: "Alamofire", dependencies : [], exclude: ["Tests"])


Чтобы удостовериться в правильности создания модулей, таргетов, продукта, можем выполнить команду

swift package describe


В результате для Alamofire получим следующий лог:


Name: Alamofire
Path: /Users/ivanvavilov/Documents/Xcode/Alamofire
Modules:
    Name: Alamofire
    C99name: Alamofire
    Type: library
    Module type: SwiftTarget
    Path: /Users/ivanvavilov/Documents/Xcode/Alamofire/Source
    Sources: AFError.swift, Alamofire.swift, DispatchQueue+Alamofire.swift, MultipartFormData.swift, NetworkReachabilityManager.swift, Notifications.swift, ParameterEncoding.swift, Request.swift, Response.swift, ResponseSerialization.swift, Result.swift, ServerTrustPolicy.swift, SessionDelegate.swift, SessionManager.swift, TaskDelegate.swift, Timeline.swift, Validation.swift


Если у пакета несколько продуктов, то в качестве зависимости мы указываем пакет зависимости, а уже в зависимости таргета указываем зависимость от модуля пакета. Например, так подключен SourceKitten в нашей библиотеке Synopsis.

import PackageDescription
let package = Package(
    name: "Synopsis",
    products: [
        Product.library(
            name: "Synopsis",
            targets: ["Synopsis"]
        ),
    ],
    dependencies: [
        Package.Dependency.package(
            // зависимость от пакета SourceKitten
            url: "https://github.com/jpsim/SourceKitten", 
            from: "0.18.0"
        ),
    ],
    targets: [
        Target.target(
            name: "Synopsis",
            // зависимость от библиотеки SourceKittenFramework
            dependencies: ["SourceKittenFramework"] 
        ),
        Target.testTarget(
            name: "SynopsisTests",
            dependencies: ["Synopsis"]
        ),
    ]
)


Так выглядит описание пакета SourceKitten. В пакете описаны 2 продукта


.executable(name: "sourcekitten", targets: ["sourcekitten"]),
.library(name: "SourceKittenFramework", targets: ["SourceKittenFramework"])


Synopsis использует продукт-библиотеку SourceKittenFramework.

Создание файла проекта


Мы можем создать файл проекта для своего удобства, выполнив команду

swift package generate-xcodeproj


и в результате получим в корневой папке проекта файл IPInfoExample.xcodeproj.
Открываем его, видим все исходники в папке Sources, в том числе с подпапкой Model, и исходники зависимостей в папке Dependencies.

Важно отметить, что данный шаг является опциональным при разработке продукта и не влияет на механизм работы SwiftPM. Заметьте, что все исходные файлы располагаются так же, как и на диске.

eka0fe0nrmy_hn7dcluu6opovvo.png

Проверка подключенной зависимости


Проверим, корректно ли подключилась зависимость. В примере делаем асинхронный запрос к сервису ipinfo для получения данных о текущем ip-адресе. JSON ответа декодируем в модельный объект — структуру IPInfo. Для простоты примера не будем обрабатывать ошибку маппинга JSON или ошибку сервера.


// импортируем библиотеку так же, как при использовании cocoapods или carthage 
import Alamofire 
import Foundation

public typealias IPInfoCompletion = (IPInfo?) -> Void

public struct Application {
    
    public static func obtainIPInfo(completion: @escaping IPInfoCompletion) {
        Alamofire
            .request("https://ipinfo.io/json")
            .responseData { result in
                var info: IPInfo?
                if let data = result.data {
                    // Маппинг JSON в модельный объект
                    info = try? JSONDecoder().decode(IPInfo.self, from: data)
                }
                completion(info)
        }
    }
    
}


Далее можем воспользоваться командой build в Xcode, а можем выполнить команду swift build в терминале.

Проект с исполняемым файлом


Выше описан пример для инициализации проекта библиотеки. SwiftPM позволяет работать с проектом исполняемого файла. Для этого при инициализации используем команду

swift package init —type executable.


Привести текущий проект к такому виду также можно, создав файл main.swift в директории Sources/IPInfoExample. При запуске исполняемого файла main.swift является точкой входа.
Напишем в него одну строчку

print("Hello, world!”)


А затем выполним команду swift run, в консоль выведется заветное предложение.

Синтаксис описания пакета


Описание пакета в общем виде выглядит следующим образом:


Package(
    name: String,
    pkgConfig: String? = nil,
    providers: [SystemPackageProvider]? = nil,
    products: [Product] = [],
    dependencies: [Dependency] = [],
    targets: [Target] = [],
    swiftLanguageVersions: [Int]? = nil
)


  • name — имя пакета. Единственный обязательный аргумент для пакета.
  • pkgConfig — используется для пакетов модулей, установленных в системе (System Module Packages), определяет имя pkg-config-файла.
  • providers — используется для пакетов системных модулей, описывает подсказки для установки недостающих зависимостей через сторонние менеджеры зависимостей — brew, apt и т.д.



import PackageDescription
let package = Package(
    name: "CGtk3",
    pkgConfig: "gtk+-3.0",
    providers: [
        .brew(["gtk+3"]),
        .apt(["gtk3"])
    ]
)


  • products — описание результата сборки таргета проекта — исполняемый файл или библиотека (статическая или динамическая).



let package = Package(
    name: "Paper",
    products: [
        .executable(name: "tool", targets: ["tool"]),
        .library(name: "Paper", targets: ["Paper"]),
        .library(name: "PaperStatic", type: .static, targets: ["Paper"]),
        .library(name: "PaperDynamic", type: .dynamic, targets: ["Paper"])
    ],
    targets: [
        .target(name: "tool")
        .target(name: "Paper")
    ]
)


Выше в пакете описано 4 продукта: исполняемый файл из таргета tool, библиотека Paper (SwiftPM выберет тип автоматически), статическая библиотека PaperStatic, динамическая PaperDynamic из одного таргета Paper.

  • Dependencies — описание зависимостей. Необходимо указать путь (локальный или удаленный) и версию.

    Управление версиями в SwiftPM происходит через git-тэги. Само версионирование можно настроить достаточно гибко: зафиксировать версию языка, git-ветки, минимальную мажорную, минорную версию пакета или хэш коммита. Опционально к тэгам добавляется суффикс вида @swift-3, таким образом можно поддерживать старые версии. Например, с версиями вида 1.0@swift-3, 2.0, 2.1 для SwiftPM версии 3 будет доступна только версия 1.0, для последней версии 4 — 2.0 и 2.1.
    Также есть возможность указать поддержку версии SwiftPM для manifest-файла, указав суффикс в имени package@swift-3.swift. Указание версии можно заменить на ветку или хэш коммита.



// 1.0.0 ..< 2.0.0
.package(url: "/SwiftyJSON", from: "1.0.0"),
// 1.2.0 ..< 2.0.0
.package(url: "/SwiftyJSON", from: "1.2.0"),
// 1.5.8 ..< 2.0.0
.package(url: "/SwiftyJSON", from: "1.5.8"),
// 1.5.8 ..< 2.0.0
.package(url: "/SwiftyJSON", .upToNextMajor(from: "1.5.8")),
// 1.5.8 ..< 1.6.0
.package(url: "/SwiftyJSON", .upToNextMinor(from: "1.5.8")),
// 1.5.8
.package(url: "/SwiftyJSON", .exact("1.5.8")),
// Ограничение версии интервалом.
.package(url: "/SwiftyJSON", "1.2.3"..<"1.2.6"),
// Ветка или хэш коммита.
.package(url: "/SwiftyJSON", .branch("develop")),
.package(url: "/SwiftyJSON", .revision("e74b07278b926c9ec6f9643455ea00d1ce04a021"))


  • targets — описание таргетов. В примере объявляем 2 таргета, второй — для тестов первого, в зависимостях указываем тестируемый.



let package = Package(
    name: "FooBar",
    targets: [
        .target(name: "Foo", dependencies: []),
        .testTarget(name: "Bar", dependencies: ["Foo"])
    ]
)


  • swiftLanguageVersions — описание поддерживаемой версии языка. Если установлена версия [3], компиляторы swift 3 и 4 выберут версию 3, если версия [3, 4] компилятор swift 3 выберет третью версию, компилятор swift 4 — четвертую.


Индекс команд


swift package init //инициализация проекта библиотеки
swift package init --type executable //инициализация проекта исполняемого файла
swift package --version //текущая версия SwiftPM
swift package update //обновить зависимости
swift package show-dependencies //вывод графа зависимостей
swift package describe // вывод описания пакета


Ресурсы


  • Пример Тестового проекта
  • Swift.org — Package Manager
  • Swift Package Manager — Usage

© Habrahabr.ru