Зачем нужен менеджер пакетов
Пока код умещается в одном файле main.swift, никакой организации не нужно: написали, запустили. Но как только проект растёт, появляются две проблемы. Во-первых, хочется разложить код по смысловым частям — например, отдельно логику подсчёта и отдельно код, который эту логику использует, — и не дать одной части случайно залезть во внутренности другой. Во-вторых, почти всегда нужен чужой код: кто-то уже написал удобную работу с датами или сетью, и переписывать это с нуля бессмысленно.
Обе задачи решает Swift Package Manager (сокращённо SPM) — официальный инструмент от Apple, встроенный прямо в swift. Он умеет собирать проект из нескольких частей, скачивать чужие библиотеки нужной версии и запускать тесты — без Xcode, из командной строки, на macOS и Linux одинаково.
Пакет и манифест
Единица, с которой работает SPM, называется пакетом (package). Пакет — это каталог с файлом Package.swift внутри. Такой файл называют манифестом: это обычный Swift-код, который описывает, из чего состоит пакет.
В манифесте встречаются три термина, которые стоит различать сразу:
- target (цель) — папка с исходным кодом, которая компилируется в отдельный модуль. У каждого таргета своё пространство имён: код одного таргета не видит внутренности другого без явного
import. - product (продукт) — то, что пакет отдаёт наружу: исполняемый файл (executable) или библиотека (library), собранная из одного или нескольких таргетов.
- dependency (зависимость) — другой пакет, код которого нужен вашему.
Структура каталогов
SPM ожидает конкретное расположение файлов. Исходный код лежит в Sources/, причём имя подпапки внутри неё становится именем таргета. Тесты аналогично лежат в Tests/. Простейший исполняемый пакет выглядит так:
WeatherApp/
├── Package.swift
├── Sources/
│ └── WeatherApp/
│ └── main.swift
└── Tests/
└── WeatherAppTests/
└── WeatherAppTests.swift
Имя папки внутри Sources (WeatherApp) должно совпадать с именем таргета в манифесте — по нему SPM находит, какой код к какой цели относится.
Исполняемый пакет: минимальный манифест
// swift-tools-version:6.0
import PackageDescription
let package = Package(
name: "WeatherApp",
targets: [
.executableTarget(
name: "WeatherApp",
path: "Sources/WeatherApp"
)
]
)
Разберём построчно. Первая строка — не комментарий для человека, а инструкция для SPM: какой версией инструментов собирать пакет. Без неё манифест не соберётся. Дальше идёт import PackageDescription — специальный модуль, который знает только сам SPM, и в нём объявлен тип Package. Внутри Package(...) задаётся имя пакета и список таргетов. .executableTarget означает: из этой папки собрать запускаемую программу, точка входа — файл main.swift внутри неё.
Библиотека и зависимость с версией
Если пакет не запускается сам, а даёт код для использования другими, ему нужен продукт-библиотека и, часто, зависимость от чужого пакета:
// swift-tools-version:6.0
import PackageDescription
let package = Package(
name: "TextTools",
products: [
.library(name: "TextTools", targets: ["TextTools"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-algorithms.git", from: "1.2.0")
],
targets: [
.target(
name: "TextTools",
dependencies: [
.product(name: "Algorithms", package: "swift-algorithms")
]
),
.testTarget(
name: "TextToolsTests",
dependencies: ["TextTools"]
)
]
)
Здесь появилось несколько новых вещей. В products явно перечислено, что пакет отдаёт наружу: библиотеку TextTools, собранную из одноимённого таргета. В dependencies указан внешний пакет по адресу репозитория и требование к версии: from: "1.2.0" значит «версия не ниже 1.2.0, но ниже следующей главной версии» — это стандартное семантическое версионирование (semver), где обновления внутри одной главной версии не должны ломать код. Наконец, в таргете TextTools зависимость подключается ещё раз, уже на уровне кода: .product(name: "Algorithms", package: "swift-algorithms") говорит, какой именно продукт чужого пакета нужен этому таргету. Тестовый таргет TextToolsTests зависит от TextTools, чтобы проверять именно его код.
Свой модуль: import и роль public
Самое частое применение SPM для одного проекта — разделить исполняемый код и переиспользуемую логику на два таргета. Пусть в пакете есть библиотечный таргет CalculatorKit и исполняемый CalculatorApp, который его использует:
// swift-tools-version:6.0
import PackageDescription
let package = Package(
name: "Calculator",
targets: [
.target(name: "CalculatorKit"),
.executableTarget(
name: "CalculatorApp",
dependencies: ["CalculatorKit"]
)
]
)
Код в Sources/CalculatorKit/Calculator.swift:
public struct Calculator {
public init() {}
public func add(_ a: Int, _ b: Int) -> Int {
a + b
}
}
И код в Sources/CalculatorApp/main.swift:
import CalculatorKit
let calculator = Calculator()
print(calculator.add(2, 3))
Обратите внимание: struct Calculator, её инициализатор и метод add помечены public. Это не украшение, а необходимость. По умолчанию доступ у объявлений — internal, то есть «видно внутри своего модуля». Таргет CalculatorKit — отдельный модуль, и без public ни сам тип, ни его инициализатор, ни метод не были бы видны из CalculatorApp после import CalculatorKit: компилятор сообщил бы, что имя недоступно вне модуля. Модификатор public — это осознанно объявленная «дверь наружу»: вы решаете, что именно другие модули имеют право использовать, а что остаётся деталью реализации.
Команды: build, run, test
Три команды покрывают почти всю повседневную работу с пакетом, их выполняют из каталога, где лежит Package.swift:
swift build— собирает все таргеты пакета, ничего не запуская. Полезно, чтобы просто проверить, что всё компилируется.swift run— сначала при необходимости пересобирает, затем запускает исполняемый продукт. Если в пакете несколько исполняемых таргетов, имя нужно указать явно:swift run CalculatorApp.swift test— собирает и запускает все тестовые таргеты (папки внутриTests/), печатает отчёт о пройденных и упавших проверках.
Если после предыдущей сборки исходный код не менялся, повторный swift build или swift run ничего не пересобирает и сразу переходит к запуску — SPM отслеживает, какие файлы изменились.
Частые ошибки
- Имя папки не совпадает с именем таргета. Если в манифесте таргет называется
CalculatorKit, а папка вSources—Calculator, SPM не найдёт исходники и сообщит об ошибке. - Забыли
public. Код компилируется внутри своего таргета, но при использовании из другого таргета компилятор ругается на недоступность типа или метода — это самая частая причина недоумения «почему не видно то, что я только что написал». - Путают зависимость пакета и зависимость таргета. Указать пакет в
dependenciesсамогоPackageнедостаточно — нужно ещё явно перечислить нужный продукт вdependenciesконкретного таргета, иначе этот таргет его просто не увидит. - Не тот тип таргета.
.targetсобирает библиотечный код,.executableTarget— код с точкой входа. Если в папке лежитmain.swift, а таргет объявлен как.target, сборка завершится ошибкой об отсутствующей точке входа.
Резюме
- Пакет — это каталог с манифестом
Package.swift, который описывает таргеты, продукты и зависимости. - Таргет — это модуль: папка с кодом внутри
Sources/, имя которой должно совпадать с именем таргета. - Исполняемый таргет объявляется как
.executableTarget, библиотечный — как.targetплюс запись вproducts. - Зависимость указывается дважды: адрес и версия в
dependenciesпакета, конкретный продукт — вdependenciesнужного таргета. - Между своими таргетами код подключается через обычный
import ИмяТаргета, а видимым снаружи модуля код делает только модификаторpublic. swift buildсобирает,swift runсобирает и запускает,swift testсобирает и запускает тесты.
Проверьте себя
3 вопроса