Skip to content

Гист с вариацией имплементации build cache`a для SPM проектов

Notifications You must be signed in to change notification settings

oiuhr/WarmerGist

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Зачем нужен этот репозиторий?

Это — репозиторий с примером имплементации схемы build-cache для проектов использующих Swift Package Manager, которая была обсуждена на докладе 'SwiftPM — фреймворки вместо кофе' на 'Mobius 2024 Spring'. Используйте его как беклинк на некоторые главы с доклада, если вам интересна конкретная глава.

🌟 Глава (commit) 1:

Изначальный коммит

Это — реконструкция нашего проекта до внедрения похожего решения. Проект генерируется туистом, а сама разработка ведется в фичевых SPM пакетах + паре пакетов нижнего уровня (Core, UI), которые утилизируются фичевыми.

📦 Глава 2:

Пакет с пакетами

Делаем наш пакет с пакетами, зависимости которого мы в будущем и будем кешировать.

Почему так? Для того, чтобы SPM пакет сожрал бинарный артефакт, его нужно указать как .binaryTarget и, собственно, вкинуть его в зависимости того таргета, с которым мы работаем. В нашем случае фичевых пакетов не один и не два, как и зависимостей, поэтому проще всего будет создать один новый пакет, который не будет содержать абсолютно никакого кода и будет чисто заглушкой для указания всех наших .binaryTarget-ов. Если подключить такой пакет в один самый низкоуровневый модуль, например, кору приложения — то SPM в фичевых пакетах автоматом зарезолвит оттуда все нужные бинари. Альтернативный вариант — генерируемые бинарные артефакт прибить гвоздями в нужных местах, но тогда генерация уходит на второй план и при апдейте версий бинари придется переподключать.

Создаем пустой пакет и добавляем его в родительский конфиг генерации проекта, чтобы он был виден в сурсах. В него переносим все зависимости из Core и UI пакетов и подключаем его к ним же в обратном порядке. Если запустить проект то будет видно, что все заводится и зависимости в фичевых модулях транзитивно резолвятся.

🔨 Глава 3:

Обвязка Tuist`a – собираем фреймворки

Бинарные артефакты нужно чем то собрать. Заниматься этим будет тот же бинарник Tuist`a, который используется для генерации проекта — правда, в другом месте и с другим конфигом, чтобы разделять сладкое от соленого.

Создаем Project.swift файл внутри скрытой папки нашего пакета с пакетами и копируем в него манифест зависимостей — их Tuist и будет чекаутить и собирать. Отдельный проект нам нужен для того, чтобы указать в нем наши сторонние зависимости из Package.swift манифеста и Tuist начал их прогревать — это “теневой клон” нашего пакета с пакетами.

Если сейчас вызвать на этот под-проект команды

    tuist install
    tuist generate —no-binary-cache

то увидим там наш граф зависимостей, который и будет билдиться — с этим проектом мы играемся когда Tuist по какой то причине не может закешировать тот или иной фреймворк. Сама CLI не особо любит конкретизировать ошибки, с которыми падают ее команды, но это обычный Xcode проект и он спокойно скажет, что конкретно упало в резолве того или иного пакета.

Ну и вызовом tuist cache уже в целом можно получить какие то фреймворки. По дефолту они ложатся в <ваш-юзер-нейм>/.cache/tuist-cloud/BinaryCache и в целом можно даже попробовать копирнуть путь до нее и указать как .binaryTarget в пакете, который сейчас висит в проекте — но пакеты не смогут зарезолвиться, так как локальному бинарному таргету нужно находится в саб-директории от пакета, который его пытается подключить.

🛠️ Глава 4:

Скрипты генерации манифеста

Давайте фиксить резолв! Надо запаковать логику как прогрева, так и записи куда нибудь в одно место — делать это руками в какой то саб-директории просто больно, да и задокументировать такой флоу довольно проблематично.

Создаем два файла — один для генерации бинарных артефактов и второй для генерации манифеста с бинарными таргетами, который мы будем подключать в сам проект и его фичевые пакеты.

В файле warm.py вызываем команды на генерацию бинарных артефактов tuist install & tuist generate —no-binary-cache — больше в нем особо ничего не происходит.

В файле write.py проходимся grep-ом по папке с нашими бинарями <ваш-юзер-нейм>/.cache/tuist-cloud/BinaryCache, копируем их в саб-директорию от пакета чтобы он смог зарезолвиться и генерируем манифест, который их подключает. Его мы вытаскиваем наружу и используем как манифест того пакета, который подключается в самом приложении.

В самом init.py в корне проекта зашиваем вызов этих двух файлов. В целом, если сейчас вызвать init.py в корне проекта, то все должно собраться — с зависимостями в виде бинарных артефактов. Но это до первой попытки обновить зависимости — если попробовать поменять версию, например, Alamofire, то проект перестанет собираться.

Если открыть папку с кешами и походить по ней, то можно будет заметить 2 версии Alamofire.xcframework — Tuist себе ее сбилдил при ее обновлении. Внутри команды кеширования он компьютит версию зависимости, чтобы понять, нужно ли ему перебилдить тот или иной фреймворк,и если нужно (то есть если он не находит фреймворк под нужным хешом) — кладет новую версию прям рядом со старой. Дальше фреймворк из под нужного хеша он сможет прилинковать в таргет генерируемого проекта.

Однако мы так не можем — по крайней мере в подходе, где мы просто grep-аем все что в папке.

Так как мы берем ВСЕ, не разбираясь в этих всех хешах, версиях и дубликатах — при попытке пройтись по папке и сгенерировать со всех вреймворков в ней какой либо манифест разумеется вылезли криты билда по дубликатам фреймворков, и даже если их обойти — непонятно нужной ли версии бинарь был взят — в одной папке могут жить под разными хешами как минорная 1.2, так и мажорная 2.0.

♻️ Глава 5:

Хэширование версий зависимостей

Снаружи пытаться считать хэши зависимостей сложнее — хотя и возможно — так что мы возьмем вариант попроще: будем считать хеш сумму нашего манифеста с зависимостями и класть все что варит туист в папку с названием в эту сумму. Так у нас будут актуальные фреймворки в едином экземпляре под конкретный манифест, по которым мы точно так же сможем бегать grep-ом.

Tuist все свои кеши кидает в папку XDG_CACHE_HOME — это по дефолту та самая <ваш-юзер-нейм>/.cache — и если ее подменить в окружении, в котором выполняется вызов команд, то использоваться будет ваша папка. В коммите правим папку на нашу посчитанную и греем в нее все, что нужно при необходимости.

Теперь при попытке обновления какой-либо сторонней зависимости дубликатам будет взяться неоткуда, так как генерироваться все фреймворки будут заново и в другом месте. Этот вариант решения не без объективных (и довольно больших) минусов — на любой чих в виде даже комментария в Package.swift файле все будет перегенерированно, что отнимает не только время на прогонах локально/на CI, но и банально место на тачке — в случае нашего проекта одна чистая папка фреймворков занимает ~4 gb. Но он простой, работает даже в таком формате и в целом может быть подменен на решение с использованием tuist graph & tuist cache --print-hashes.

📚 Глава 6:

Бандлы зависимостей

Не забудем про бандлы — если раньше все ресурсы из подтягиваемых пакетов отруливались SPM-ом, то в рамках генерации статического фреймворка (которыми все spm пакеты и являются по дефолту в туисте) он создает отдельный таргет с бандлом, который по итогу будет сбилжен и подцеплен в ваш таргет. Но есть нюанс — туистом для генерации мы не пользуемся, и подключить бандл тупо некуда.

Еще есть момент, что эти самые бандлы он будет билдить не при кешировании — а при билде ваших таргетов. При запуске команды принта хешей она даже высветит название банда среди близящихся таргетов (на скрине — таргет Лотти, и таргет для ее бандла — с андерскором), и даже сбилдит его — но после попросту удалит его. Для проверки вызовите tuist cache —print-hashes.

Как вариант — так как командой Tuist'a подразумевается, что бандл мы сбилдим при билде таргета, который он использует, то можно просто сделать “теневую” схему, которая будет билдить этот самый бандл и вызывать ее билд при генерации бинарных артефактов. В пост-билд скрипт закидываем пару строк кода, которые пробегутся по билд-папке (DerivedData) сбилженной схемы и копирнут оттуда все бандлы, которые нам нужны, а дальше этот бандл можно подключить любым удобным способом в проект — я просто копирую его при генерации манифеста, а позже подключаю в Project.swift файле, который генерирует основной проект, как обычный ресурс.

Для проверки работы очистите папку кешей на вашей машинке, если вы запускали комиты выше, и вызовите инит файл в корне проекта.

⛲️ Глава 7:

Генерация проекта исходниками

В своей документации по использованию кэша бинарных артефактов ребята-разработчики Tuist рекомендуют использовать данный подход только для ускорения процессов на CI, а сборку на архивацию (проще — релизную) собирать исключительно из исходников. (Источник: https://docs.tuist.io/cloud/binary-caching.html#using-the-cache-binaries)

В дополнению к рекомендациям из документации могу добавить, что вести разработку с использованием пакетов сурсами в некоторых моментах может быть просто удобнее — в бинарном артефакте особо брейкпоинтами не полазаешь& Плюсом к этому — если используемое стороннее решение в виде скриптов, которые прогревают бинари, решает по то или иной причине перестать работать — наличие возможности пропустить эти скрипты и собраться из исходников при любом чихе позволит не терять рабочие часы в случае необходимости что-то срочно чинить/релизить.

Схема работы в нашем случае довольно простая — ловим аргумент в файлах генерации, который при наличии будет все пропускать и копировать внутренний манифест зависимостей наружу. Таким образом получим в виде подключаемого пакета в самом проекте обычный Package.swift файл, который был сделан во втором коммите.

About

Гист с вариацией имплементации build cache`a для SPM проектов

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published