$ sudo teach IT

Зачем нужен менеджер пакетов

Пока код умещается в одном файле 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 вопроса