1. Чому важливі версії залежностей
Коли ви тільки починаєте програмувати, легко піддатися приємній ілюзії: «код — це текст, отже він або правильний, або ні». Потім зʼявляються залежності, і зʼясовується, що код — це ще й контракт із зовнішнім світом. Причому зовнішній світ уміє оновлюватися без вашої згоди. Сьогодні ми спокійно розберемося, чому «плаваючі» версії створюють хаос і як SwiftPM допомагає цей хаос приручити.
Уявіть, що ваш LibraryCLI залежить від пакета SomeLib. У Package.swift ви написали: «візьми версію від 1.2.3 і вище, сумісну за правилами SemVer». Ви запустили swift build — усе зібралося. Завтра ваш колега запускає swift build — і раптом у нього інша версія SomeLib у межах дозволеного діапазону, а в ній автор трохи змінив поведінку. І ось у вас «все гаразд», а в колеги «все червоне», і починається давній ритуал під назвою «у мене працює».
SwiftPM розвʼязує цю проблему двома шарами:
- Package.swift задає правило: які версії дозволені.
- Package.resolved фіксує факт: які версії вибрані саме зараз, щоб збірка була відтворюваною. Swift.org прямо описує Package.resolved як знімок точних версій залежностей, які використовуються локально.
2. SemVer людською мовою: MAJOR.MINOR.PATCH
SemVer (Semantic Versioning) звучить як «щось для дорослих», але на практиці це просто акуратний спосіб сказати: «наскільки сильно я можу вас зламати цим релізом». Ми розберемося, як читати номери версій очима розробника і чому пакетний менеджер узагалі може робити висновки про сумісність за трьома числами. Це той рідкісний випадок, коли математика з трьох чисел справді економить години життя.
У SemVer версія зазвичай виглядає так:
MAJOR.MINOR.PATCH
наприклад: 2.5.1
Суть така:
| Частина версії | Приклад | Інтуїтивний зміст | Що це означає для клієнта (вас) |
|---|---|---|---|
|
|
«можу ламати API» | можливо, доведеться змінювати код |
|
|
«додав нові можливості без ламання API» | ваш код має й надалі працювати |
|
|
«виправив баги» | зазвичай безпечне оновлення |
Є ще pre-release (наприклад 1.2.0-beta.1) і build metadata (наприклад 1.2.0+abc123), але сьогодні нам достатньо розуміти саме основну трійку.
Важливо памʼятати одну тонкість: SemVer — це обіцянка автора пакета, а не закон природи. Якщо автор пакета помилився і зламав API в MINOR, ви все одно отримаєте проблему. Саме тому нам і потрібен другий шар — Package.resolved: він допомагає зафіксувати робочий набір версій і не наштовхуватися на сюрпризи лише тому, що хтось випустив новий реліз.
3. Правило і факт: Package.swift vs Package.resolved
Тепер поєднаємо обидві ідеї в одну картину: SemVer дає нам мову, а SwiftPM — механізм. Найчастіша помилка новачка тут — думати, що запис у Package.swift на кшталт from: "1.2.3" означає «рівно 1.2.3». Насправді це означає «дозволений діапазон», і SwiftPM має право вибрати новішу сумісну версію. Тому ми й розділяємо «правило» та «фіксацію».
У SwiftPM ви задаєте версію залежності в Package.swift приблизно так:
dependencies: [
.package(url: "https://example.com/some-package.git", from: "1.2.3")
]
Ключове слово тут — from. Це не «закріпити назавжди», а «мінімальна версія, від якої відштовхуємося». Яку саме версію SwiftPM візьме, залежить від того, які версії доступні, які обмеження накладають транзитивні залежності і що вже зафіксовано в Package.resolved.
Щоб у вас у голові не було каші, корисно тримати ось таку невелику таблицю:
| Де | Що зберігається | Це «правило» чи «факт»? | Хто пише |
|---|---|---|---|
|
вимоги до версій (діапазони, точні значення, гілки) | правило | ви |
|
конкретно вибрані версії (і часто ще commit) | факт | SwiftPM |
І ось тепер головний висновок: SemVer живе в «правилі», а відтворюваність — у «факті».
4. .exact(...): коли фіксувати версію
Іноді хочеться простоти: «не хочу думати, хочу зафіксувати одну версію і все». SwiftPM дозволяє так зробити через .exact("1.2.3"). Але тут важливо розуміти ціну. Жорстка фіксація версії підвищує ризик конфліктів у графі залежностей. Ви ніби говорите: «мені підходить тільки цей конкретний гвинтик, і жоден інший», а потім дивуєтеся, що два набори меблів IKEA не збираються в одну шафу.
Приклад жорсткої фіксації:
dependencies: [
.package(url: "https://example.com/some-package.git", .exact("1.2.3"))
]
Коли .exact доречний? Уявіть, що ви спіймали критичний баг у версії 1.2.4, а 1.2.5 ще не вийшла, а реліз вашого LibraryCLI треба зібрати вже сьогодні. Тоді .exact("1.2.3") може бути тимчасовим «замороженням», щоб команда не стикалася з різною поведінкою на різних машинах.
Чому .exact може бути небезпечним як звичка? Тому що SwiftPM (як і компілятор Swift) не любить ситуацій, коли потрібно одночасно підтягнути дві різні версії однієї й тієї самої бібліотеки. У матеріалах про pinning в екосистемі Swift окремо наголошується, що така ситуація — це «dependency hell» для Swift, бо одночасне використання кількох версій однієї залежності в одному артефакті — не те, на що SwiftPM розраховує.
Тому практична мораль, без героїзму: .exact — це інструмент, але його краще сприймати як «аварійний молоток». Він висить під склом не тому, що ним зручно відкривати пляшки, а тому, що іноді справді потрібен.
5. Як SwiftPM розвʼязує залежності
Слова «dependency resolution» звучать так, ніби зараз вийде професор і почне малювати графи на дошці. Але нам не потрібен професор — нам потрібна зрозуміла модель. Розвʼязання залежностей у SwiftPM можна уявити як спробу зібрати пазл: у кожного пакета є свої вимоги, і SwiftPM шукає набір версій, який задовольняє всі вимоги одночасно. Якщо такий набір існує — чудово. Якщо ні — збірка не відбудеться, і це насправді краще, ніж зібрати «щось», що потім вибухне під час виконання.
Уявімо мініграф:
- LibraryCLI залежить від PackageA і PackageB
- PackageA вимагає LibX версії >= 1.2.0 (і сумісної далі)
- PackageB вимагає >= 1.3.0 (і сумісної далі)
Якщо діапазони перетинаються, SwiftPM знайде версію LibX, яка підходить обом. Якщо ні — або якщо їх зафіксовано через .exact — SwiftPM чесно скаже: «друзі, у вас конфлікт».
Давайте закріпимо модель схемою:
flowchart TD
A[Package.swift: вимоги до версій] --> B[резолвер SwiftPM]
B --> C{Є сумісний набір версій?}
C -- ні --> D[Помилка розв’язання залежностей]
C -- так --> E[Вибрані версії]
E --> F[Package.resolved: фіксація результату]
F --> G[swift build/run використовують зафіксовані версії]
Важлива деталь: Package.resolved зʼявляється не тому, що SwiftPM любить плодити файли (хоча іноді здається, що любить), а тому, що команді потрібно розділяти намір і результат. І це не лише наша думка: в описі механізму pinning окремо зазначається, що мета — уникати ситуацій «у мене працює» і стандартизувати командний робочий процес, коли всім важливо збиратися на однакових версіях залежностей.
6. Package.resolved на практиці
Що всередині і чому не варто правити вручну
Перейдемо до практики: що це за файл, де він лежить і що в ньому можна побачити. Важливо: вам не потрібно запамʼятовувати точний JSON-формат. Достатньо розуміти сенс: це знімок (snapshot) того, що SwiftPM справді вибрав. Swift.org формулює це дуже прямо: під час додавання залежностей зʼявляється Package.resolved, і це знімок точних версій залежностей, які використовуються локально.
Зазвичай Package.resolved — це JSON, у якому для кожної залежності зафіксовано:
- ідентифікатор/імʼя залежності (як SwiftPM її ідентифікує),
- джерело (зазвичай URL репозиторію),
- версію (якщо залежність вибиралася за тегами SemVer),
- іноді revision (хеш коміту) — особливо важливо, якщо залежність підключено не за тегом, а за гілкою чи комітом.
Спрощено це можна уявити так:
{
"pins": [
{
"identity": "some-package",
"state": {
"version": "1.2.7",
"revision": "a1b2c3d4..."
}
}
]
}
Чому не варто редагувати Package.resolved вручну? З тієї самої причини, з якої не варто вручну виправляти індикатор помилки двигуна в машині маркером. Файл відображає результат роботи резолвера, і якщо ви внесете туди «красиву» версію, яка не узгоджується з вимогами з Package.swift або з реально доступними версіями, ви отримаєте дивні помилки, які важко налагодити.
Нормальний спосіб працювати з цим файлом такий: ви змінюєте правило в Package.swift, запускаєте збірку, SwiftPM перераховує залежності й оновлює факт у Package.resolved.
Мінісценарій на LibraryCLI: як зʼявляється Package.resolved
Щоб не лишати тему розмитою, подивімося на практичний приклад «додали залежність → зʼявився Package.resolved». У документації Swift.org для CLI-прикладу відбувається саме це: після додавання залежності й запуску збірки створюється Package.resolved. Ми адаптуємо ідею під наш LibraryCLI, не змінюючи загальної логіки курсу.
Уявіть, що ми хочемо використовувати зовнішній модуль, наприклад, для красивого ASCII-банера під час запуску застосунку, щоб LibraryCLI виглядав трохи менш як «сувора термінальна цеглина». У Package.swift це виглядає так:
import PackageDescription
let package = Package(
name: "LibraryCLI",
dependencies: [
.package(url: "https://github.com/apple/example-package-figlet", branch: "main")
],
targets: [
.executableTarget(
name: "LibraryCLI",
dependencies: [
.product(name: "Figlet", package: "example-package-figlet")
]
)
]
)
Цей приклад важливий не конкретною бібліотекою, а механікою: залежність оголошено в dependencies, а потім target підключає конкретний product як модуль для import.
Після першого swift build SwiftPM завантажить залежність і створить або оновить Package.resolved. І ось тут магія закінчується: завтра на іншій машині — або в CI — SwiftPM зможе взяти рівно ті версії, а іноді й конкретні ревізії, які зафіксовано в Package.resolved.
А в коді LibraryCLI (у точці входу) це перетворюється на звичайний імпорт модуля:
import Figlet
print(Figlet.say("LibraryCLI")) // (вивід буде ASCII-арт, залежить від бібліотеки)
У документації Swift.org для CLI-прикладу показано той самий принцип: підключили продукт, імпортували модуль і використовуєте його в коді.
Ще раз: ми не «вчимося малювати ASCII-арт». Ми вчимося бачити звʼязок:
Package.swift (правило) → swift build → Package.resolved (факт) → відтворювана збірка.
7. Команда та CI: чому Package.resolved допомагає
Коли ви працюєте самі, у вас багато свободи й мало свідків. Коли ви працюєте в команді або маєте CI, вам потрібна дисципліна: «один і той самий commit має збиратися однаково». Package.resolved якраз і виконує цю роль: він допомагає проєкту бути більш детермінованим, зменшує ймовірність того, що збірка раптом зламається сама, і робить розслідування багів менш містичним.
У поясненні pinning для SwiftPM окремо наголошується на командному сценарії: зручно, коли в усіх учасників команди й у CI зафіксовано однакові версії залежностей, щоб уникати ситуацій «у мене працює». І це дуже практична вигода: ви менше сперечаєтеся з колегами і більше розвʼязуєте задачі.
При цьому важливо не переплутати ролі. Package.resolved не має перетворюватися на «другий маніфест», де ви вручну прописуєте «правильні версії». Він має залишатися наслідком. Якщо ви хочете змінити версії — змінюйте вимоги в Package.swift (або свідомо запускайте оновлення залежностей), а Package.resolved нехай буде чесним протоколом того, що вийшло.
8. Типові помилки
Помилка №1: плутати from: із «саме цією версією».
Запис .package(..., from: "1.2.3") — це діапазон, а не фіксація. Якщо вам здається, що SwiftPM самовільно взяв іншу версію, часто це не свавілля, а нормальна робота резолвера в межах дозволених версій. Фіксація робиться не цим, а комбінацією вимог і файлу Package.resolved, який зберігає фактично вибрані версії.
Помилка №2: використовувати .exact(...) як «стандартний стиль», щоб не замислюватися.
Жорсткі фіксації підвищують ризик конфліктів транзитивних залежностей. У Swift це особливо неприємно, бо система не розрахована на «давайте підтягнемо дві версії однієї бібліотеки і якось із цим упораємося». Якщо хочеться стабільності, зазвичай правильніше тримати Package.resolved у репозиторії, а не перетворювати Package.swift на бетон.
Помилка №3: редагувати Package.resolved вручну.
Це майже завжди призводить до дивних неузгодженостей: «маніфест вимагає одне», «resolved каже інше», «SwiftPM свариться так, ніби ви його образили». У нормальному потоці ви керуєте правилами в Package.swift, а Package.resolved оновлюється SwiftPM.
Помилка №4: ігнорувати Package.resolved у командній розробці й дивуватися, чому в колеги не збирається.
Якщо ви робите CLI-застосунок, а саме ним є LibraryCLI, і хочете відтворюваних збірок, Package.resolved — один із головних способів зменшити випадковість. SwiftPM прямо створює цей файл як знімок вибраних версій під час додавання залежностей і збірки.
Помилка №5: вважати, що SemVer — гарантія, а не угода.
SemVer допомагає, але не скасовує людський фактор. Іноді автор пакета може випадково зламати сумісність навіть у PATCH або MINOR. Саме тому корисно мати зафіксований «робочий» набір залежностей і оновлювати його свідомо, а не «бо сьогодні понеділок».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ