JavaRush /Курсы /Swift SELF /Targets и products в SwiftPM: executable vs library

Targets и products в SwiftPM: executable vs library

Swift SELF
48 уровень , 3 лекция
Открыта

1. Targets и products

Если вы когда-нибудь собирали IKEA-шкаф, вы знаете: есть список деталей (доски, винты, ключик-шестигранник), а есть то, что вы в итоге гордо ставите в комнату и говорите «готово». В SwiftPM примерно та же логика: targets — это «детали и узлы, которые компилируются», а products — это «то, что пакет отдаёт наружу» в виде исполняемого файла или библиотеки.

Проблема новичка обычно такая: он видит Package.swift, где есть и targets, и products, и думает «они же почти одинаковые, зачем два раза одно и то же?». А потом появляется первая ошибка No such module ... или внезапно выясняется, что «я сделал библиотеку, но её никто не может нормально подключить». Чтобы таких сюрпризов было меньше, мы сейчас выстроим очень чёткую модель: что компилируется, что получается на выходе, и как это связано с модульностью.

Ключевая мысль лекции заранее: targets/products описывают сборку и границы модулей, но не управляют видимостью типов и функций. За видимость отвечает access control: public/internal/private и друзья.

2. Targets: что компилируется и во что это превращается

Target в SwiftPM — это то, что SwiftPM компилирует как единое целое. Практически это означает, что файлы внутри Sources/<TargetName>/... (или в указанной директории) собираются вместе и образуют модуль. И если модуль образовался, его (при наличии зависимости) можно импортировать через import <TargetName> в другом таргете.

Именно поэтому target часто воспринимают как «модуль проекта» — и для базового понимания это правда.

Target как единица компиляции

Давайте на минимальном примере. Вот пример Package.swift, где объявлен один target:

// Package.swift
import PackageDescription

let package = Package(
    name: "LibraryCLI",
    targets: [
        .target(name: "Core")
    ]
)

Что это означает на практике: SwiftPM знает, что существует цель компиляции Core. Но пока мы не сказали ничего про продукты, мы как будто объявили «у нас есть деталь», но не сказали «что из неё делаем».

Чтобы привязать target к папке с исходниками, обычно структура такая:

Sources/
  Core/
    Book.swift

И важная бытовая мысль: target — это не «файл», не «папка вообще» и не «проект в IDE». Это именно единица компиляции, которая превращается в модуль.

Виды targets в SwiftPM

Когда вы читаете Package.swift, полезно понимать, что target’ы бывают разных типов. Мы не будем превращать лекцию в справочник, но минимальный набор понятий сегодня обязателен: обычный target (как библиотечный модуль), исполняемый target (тот, где есть точка входа), и тестовый target (который компилируется для запуска тестов).

Важное следствие: один пакет может содержать сразу несколько targets, и это нормально — более того, это главный способ «технически закрепить» границы модулей.

Вот пример Package.swift, который показывает все три типа target:

// Package.swift
import PackageDescription

let package = Package(
    name: "LibraryCLI",
    targets: [
        .target(name: "Core"),
        .executableTarget(name: "LibraryCLI", dependencies: ["Core"]),
        .testTarget(name: "CoreTests", dependencies: ["Core"])
    ]
)

Здесь уже видно важное: исполняемый target LibraryCLI зависит от Core, а тестовый CoreTests зависит от Core. Но пока мы не описали products, мы всё ещё не сказали SwiftPM, какие артефакты мы хотим «выдать наружу».

Ещё один нюанс, который часто удивляет: тестовые цели компилируются и запускаются, но обычно не являются продуктами пакета. Эта идея явно зафиксирована в дизайне SwiftPM: тесты — это «внутренний артефакт разработки», а не то, что пакет «продаёт» внешнему миру.

3. Products: что пакет «выдаёт наружу»

Product в SwiftPM — это описание того, что получится на выходе, чтобы этим мог пользоваться кто-то ещё (или вы сами, но уже как «потребитель пакета»). В базовой модели продуктов два: executable и library.

Если target отвечает на вопрос «что компилируем и из каких исходников?», то product отвечает на вопрос «что считаем результатом пакета и на что могут ссылаться клиенты?».

Пример: library product

Вот пример с library-продуктом:

// Package.swift
import PackageDescription

let package = Package(
    name: "LibraryCLI",
    products: [
        .library(name: "Core", targets: ["Core"])
    ],
    targets: [
        .target(name: "Core")
    ]
)

Пример: executable product

И вот пример с executable-продуктом:

// Package.swift
import PackageDescription

let package = Package(
    name: "LibraryCLI",
    products: [
        .executable(name: "LibraryCLI", targets: ["LibraryCLI"])
    ],
    targets: [
        .executableTarget(name: "LibraryCLI")
    ]
)

Почему продукт вообще нужен, если target уже существует? Потому что «target существует» — это правда только внутри вашего пакета. А «product существует» — это то, что может быть потреблено извне: другой пакет может зависеть от вашего продукта.

Частая путаница: executable target и executable product

На этом месте мозг новичка обычно говорит: «Подождите. У меня есть .executableTarget, и ещё .executable в products. Это же одно и то же, просто написанное дважды?»

Почти, но нет. Это как путать «кухню» и «блюдо»: кухня — место, где готовят; блюдо — то, что подают.

Executable target — это модуль, который компилируется как исполняемый, то есть SwiftPM ожидает там entry point (top-level main.swift или @main). А executable product — это «упаковка результата», то есть решение «вот этот исполняемый артефакт является продуктом пакета».

Практически в маленьких учебных пакетах они часто совпадают по имени, и это нормально. Но концептуально они разные сущности: один отвечает за компиляцию, другой — за «выдачу результата».

Чтобы закрепить, давайте посмотрим на небольшую схему:

flowchart TD
    A[Sources/LibraryCLI/*.swift] --> B[Target: LibraryCLI]
    B --> C[Executable artifact: LibraryCLI]
    C --> D["Product: executable 'LibraryCLI'"]

Смысл такой: исходники компилируются в target, target даёт артефакт, артефакт объявляется продуктом (или не объявляется — и тогда он не считается «публичным выходом пакета» в смысле SwiftPM-модели).

Library target и library product: модуль и «витрина» для клиентов

С библиотеками похожая история, но там появляется ещё один важный смысловой слой: библиотека — это не только «собрали код», но и «какой интерфейс мы показываем клиентам».

В дизайне SwiftPM есть понятие root targets продукта: продукт перечисляет targets, которые считаются корнем библиотеки. Их зависимости тоже будут собраны и прилинкованы, но «идеологически» наружу выдаётся интерфейс корневых targets.

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

В терминах нашего курса это звучит так: product помогает сказать «вот библиотека, которую можно подключить», а targets помогают сказать «вот как она устроена внутри». И это идеально ложится на модульность: вы можете иметь внутренние targets, которые вообще не экспортируются как продукты, и это часто хорошая архитектурная гигиена.

4. Практика: выносим Core в отдельный target и подключаем из CLI

Сейчас мы аккуратно сделаем шаг в сторону модульности, но без ухода в будущие лекции про разбиение по слоям. Нам важно почувствовать разницу «target как модуль» и «product как выдаваемый результат».

Представим, что LibraryCLI — это CLI для работы с мини-библиотекой книг. Мы хотим вынести модель Book в отдельный модуль Core, чтобы её можно было использовать из LibraryCLI, не смешивая всё в одном месте.

Структура папок:

Sources/
  Core/
    Book.swift
  LibraryCLI/
    main.swift

Теперь код. пример Sources/Core/Book.swift:

// Sources/Core/Book.swift
import Foundation

struct Book {
    let title: String
    let author: String
}

И пример Sources/LibraryCLI/main.swift:

// Sources/LibraryCLI/main.swift
import Foundation
import Core

let book = Book(title: "Swift для людей", author: "Кто-то смелый")
print("Книга: \(book.title) — \(book.author)")

Если вы попробуете это собрать, вы почти наверняка получите ошибку доступа (или очень похожую). И это не «Swift вредничает», а важнейшая демонстрация сегодняшней темы: targets/products описывают сборку и границы модулей, но не делают типы автоматически доступными.

Почему так? Потому что Book объявлен без модификатора доступа, а значит он internal, то есть виден только внутри модуля Core. А LibraryCLI — другой модуль. Следовательно, Book «спрятан» от потребителя.

Исправим это минимально, не превращая лекцию в лекцию про access control. пример:

// Sources/Core/Book.swift
import Foundation

public struct Book {
    public let title: String
    public let author: String

    public init(title: String, author: String) {
        self.title = title
        self.author = author
    }
}

Обратите внимание на три момента. Во‑первых, public нужен не потому, что мы сделали продукт, а потому что мы пересекли границу модуля. Во‑вторых, пришлось добавить public init, потому что иначе инициализатор будет недоступен извне (это классическая «почему я не могу создать struct?»-ловушка). В‑третьих, это демонстрирует главную мысль лекции: границы модулей (targets) и видимость API (public/internal) — разные механизмы.

Почему product не делает код публичным

Сейчас важно очень чётко проговорить то, что будет спасать вас от головной боли: product — это не магическая кнопка «сделай мне публичное API». Product отвечает за то, «что является артефактом пакета», а модификаторы доступа отвечают за то, «какие символы видны снаружи модуля».

Когда вводили продукты в SwiftPM, отдельно подчёркивали: product перечисляет корневые targets и задаёт «интерфейс, который выдаётся клиентам», но при этом зависимости корневых targets тоже включаются в сборку, просто они не должны становиться «видимыми» как часть интерфейса. Это про архитектурное намерение. Но на уровне языка Swift видимость всё равно контролируется public/internal/private, иначе получится ситуация «я случайно экспортировал половину проекта».

Поэтому правило звучит почти как техника безопасности: создал target — получил новый модуль и новую границу видимости. Создал product — описал, какой артефакт «официально» выдаётся пакетом. И ни то, ни другое не отменяет необходимость проектировать публичный API через модификаторы доступа.

Если хочется короткой аналогии: target — это «стены здания», а access control — это «двери и замки на комнатах». Можно построить стену, но если дверь открыта настежь, любой зайдёт. И наоборот: можно поставить супер-замок, но если стен нет, всё равно все ходят где хотят в одном модуле.

Минимальный Package.swift для Core + CLI

Теперь соберём минимально «правильный» манифест для нашего учебного LibraryCLI, где есть отдельный модуль Core и исполняемый модуль LibraryCLI. Это пример, который отражает сегодняшнюю модель:

// Package.swift
import PackageDescription

let package = Package(
    name: "LibraryCLI",
    products: [
        .executable(name: "LibraryCLI", targets: ["LibraryCLI"])
    ],
    targets: [
        .target(name: "Core"),
        .executableTarget(name: "LibraryCLI", dependencies: ["Core"])
    ]
)

Заметьте, что мы не обязаны объявлять Core как .library(...) продукт, чтобы он работал внутри пакета. Нам достаточно target + dependency.

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

5. Типичные ошибки

Ошибка №1: «Я создал target, значит это и есть продукт».
Такое мышление обычно появляется, когда всё маленькое и в проекте один модуль. Но как только targets становится два, путаница начинает мешать: target — это то, что компилируется, а product — это то, что «выдаётся» как артефакт (исполняемый файл или библиотека). Если сомневаетесь, полезно буквально задать себе два вопроса: «что компилируем?» (targets) и «что считаем результатом?» (products).

Ошибка №2: «Я добавил product, значит все типы автоматически доступны».
Это одна из самых обидных ловушек: всё красиво описано в Package.swift, но компилятор ругается, что тип «не виден». Причина почти всегда в том, что тип или инициализатор остался internal. Product не меняет модификаторы доступа. Если вы вынесли код в отдельный target (модуль), то для использования из другого модуля вам понадобится public там, где это действительно часть API.

Ошибка №3: «Сделаю всё public, и проблемы исчезнут».
Проблемы действительно «исчезнут» вместе с инкапсуляцией. Новички часто реагируют на ошибки видимости так же, как на ошибку компиляции «не тот тип» — «ну поставлю что-нибудь, чтобы заработало». Это опасно: вы превращаете внутренние детали в обещание внешнему миру. Гораздо лучше привыкать к мысли, что публичным должно становиться только то, что вы готовы поддерживать как контракт.

Ошибка №4: «Тесты — это тоже продукт, значит надо добавить их в products».
Тесты компилируются и запускаются, но они не являются продуктом пакета в нормальной модели SwiftPM: это внутренний артефакт разработки. Если вы пытаетесь «экспортировать тесты», вы почти наверняка решаете не ту задачу, и дальше станет только страннее.

Ошибка №5: «Я назову executable target App, а продукт LibraryCLI, чтобы было красиво».
Так можно, но для учебного проекта это почти гарантированно создаст путаницу: что запускать, что импортировать, где искать main.swift. На старте лучше держать имена консистентными и простыми, а красоту наводить тогда, когда вы уже уверенно различаете target и product и понимаете, зачем вам разные имена.

1
Задача
Swift SELF, 48 уровень, 3 лекция
Недоступна
Внутренний модуль
Внутренний модуль
1
Задача
Swift SELF, 48 уровень, 3 лекция
Недоступна
Корневые таргеты
Корневые таргеты
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ