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