Хабрахабр

Swift Package Manager


Вместе с релизом в 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 // вывод описания пакета

Ресурсы

Показать больше

Похожие публикации

Кнопка «Наверх»