В некоторых компаниях в целях безопасности на раннерах для continuous integration отсутствует выход в интернет. А для успешной выполнения команды pod install нужен доступ к GitHub репозиторию с подспеками для их скачивания в локальную директорию. Но для IOS разработки проекта, использующего в качестве менеджера зависимости Cocoapods, можно не использовать команду pod install на CI, если вся папка Pods полностью трекается гитом и Поды интегрированы во все проекты.
Однако при использовании в проекте Tuist (быстро набирающего нынче популярности очень полезного инструмента для уменьшения мердж конфликтов) файлы xcodeproj будут каждый раз генерироваться заново на CI, естественно, без интегрированных в них Подов. В таком случае необходимо вызывать команду pod install после tuist генерации с целью интеграции Подов в xcodeproj проекты. Но даже несмотря на то, что все сорсы Подов на месте, команда pod install будет выдавать ошибку без интернета, так и не внедрив Поды в наши проекты.
Такой проблеме есть решение: команде pod install не понадобится интернет, если будет находится валидный и непросроченный trunk specs repo со всеми подспеками используемых библиотек в проекте из репозитория GitHub в директории ~/.cocoapods/repos/trunk. О реализации такого решения и пойдет речь в этой статье.
Стадии команды pod install
Команда pod install выполняет несколько последовательных стадий для установки Подов:
Analyzing dependencies - самая долгая стадия, имеет много подэтапов. Здесь анализируются зависимости для таргетов по Podfile, а также скачиваются подспеки используемых фреймворков из централизованного репозитория Specs из Github в подэтапе Resolving dependencies of `Podfile`. Важно, что если все нужные подспеки уже имеются в локальной директории ~/.cocoapods/repos/trunk, то скачивания не происходит.
Downloading dependencies - скачиваются сорсы Подов, если каких-либо не хватает согласно предыдущему анализу. Если все хватает, то ничего скачиваться не будет.
Generating Pods project - генерируется проект подов Pods.xcodeproj и служебные файлы в папке Pods/Target Support Files, такие как xcconfig файлы, нужные для линковки подов с нашими таргетами.
Integrating client projects - интеграция Подов в таргеты наших проектов xcodeproj.
![Этапы команды pod install Этапы команды pod install](https://habrastorage.org/getpro/habr/upload_files/2db/1d5/d00/2db1d5d0075a9ae5facfc7a36a1ca528.png)
Существуют кейсы, когда нам необходимо выполнение последних двух этапов этой команды. Например, 4-ый этап интеграции Подов в таргеты нужен при использовании Tuist в проекте. Но при отсутствие интернета, команда фейлится с ошибкой CDN на первом же этапе (Analyzing dependencies) на CI. Эта ошибка символизирует невозможность как найти нужные файлы локально, так и скачать по url https://cdn.cocoapods.org/. В зависимости от того, отсутствует ли папка trunk полностью, либо в ней не хватает какого-либо файла или подспеки для текущей конфигурации Podfile, формулировка ошибки будет отличаться, но причина ее появления одна.
![Ошибки команды pod install при отсутствии интернета Ошибки команды pod install при отсутствии интернета](https://habrastorage.org/getpro/habr/upload_files/e8e/88a/daa/e8e88adaa2bbaaf2dd288feeba2b0c53.png)
Есть множество случаев необходимости или желания успешного исполнения команды pod install на CI без сети, даже в случае трекинга всех сорсов Подов в Pods директории:
Выполнение 3-ей стадии команды может быть полезна тем, что Cocoapods во время нее генерирует Под проект и много служебных файлов в папке Pods/Target Support Files, количество которых может достигать нескольких тысяч в зависимости от размера проекта, количества таргетов и конфигураций. А это значит можно закинуть в .gitignore файлы проекта Pods.xcodeproj и всю папку Pods/Target Support Files, чтобы заново генерились на CI. Такой подход поможет избежать мердж конфликтов в этих файлах и многочисленных изменений в них при каждом обновлении Подов.
Во время 4-ой стадии происходит интеграция Подов в наши проекты, и это необходимо при использовании tuist после их генерации. Интеграция Подов в проекты подразумевает: прицепление к каждому таргету сгенерированных Cocoapods конфиг файлов xcconfig, добавление Pods framework для таргетов, добавление build скриптов в build phases тагретов, таких как "Embed Pods Frameworks" и "Check Pods Manifest.lock".
Использование некоторых полезных плагинов для Cocoapods возможно только при успешном выполнении pod install. Так, например, такие плагины, как cocoapods-binary либо cocoapods-xcremotecache, связанные с кешированием, исполняются в pre-/post-install скрипте во время команды pod install. Очередь таких скриптов наступает только после 2-ой или 3-ей стадии команды. Поэтому из-за фейла команды в самом начале подобные плагины так и не выполнятся.
Решение
Как было сказано выше, если все нужные файлы и подспеки уже имеются в локальной директории ~/.cocoapods/repos/trunk, то их скачивания не происходит во время 1-ой стадии команды pod install, и поэтому выход в интернет для нее не понадобится.
Таким образом, мы можем просто скопировать наш локальную папку актуального trunk из ~/.cocoapods/repos/trunk прямо в проект, а на CI перед командой pod install переместить эту папку в директорию раннера без сети, т.е. в ~/.cocoapods/repos/.
Локально, у себя: Находясь в корневой директории проекта, после успешного выполнения pod install копируем папку trunk куда-нибудь в проект, например, в корень. Архивируем ее, чтобы был один архивный файл trunk.zip, а не папка с тысячами файлов и подспеков.
cp -r ~/.cocoapods/repos/trunk $PWD
zip -rm trunk.zip trunk >/dev/null
На CI без интернета: Перед pod install на всякий случай удаляем уже существующую папку trunk, если такая существует. Затем разархивируем нашу папку trunk.zip и перемещаем ее в директорию ~/.cocoapods/repos/ на раннере. Далее вызывается pod install и успешно отрабатывает без обращения к сети, так как теперь все podspecs имеются локально в trunk.
build_job:
script:
- pod repo remove trunk || echo trunk removed already
- unzip trunk.zip -d ~/.cocoapods/repos >/dev/null
- tuist generate -n # only if you use tuist
- pod install
- xcodebuild ...... # build project
При таком подходе архив папки trunk.zip будет устаревать, и потребуется его обновление только при обновлении версии некоторого Пода либо при добавлении нового Пода, которого нету в этом trunk. То есть обновление старого trunk.zip на новый будет нужно в случаях, когда в trunk добавляются новые podspecs, которых нет в старом trunk.
Преимущество этого подхода заключается в децентрализованности хранения trunk с подспеками для разных веток, так как trunk.zip хранится не в одном месте для всех веток, а прямо в проекте для конкретной конфигурации Podfile. Таким образом, на одной ветке может хранится trunk.zip с подспеками одних Подов, нужных для данной ветке, а на другой ветке - trunk.zip с подспеками совсем других Подов, пригодных уже для этой ветке. И для них обеих будет успешно выполнятся pod install на CI.
Усовершенствование данного подхода
Так или иначе обновление trunk.zip потребуется только после очередного pod install или pod update с целью обновить Поды. Поэтому можно автоматизировать копирование нового trunk.zip при последующих обновлениях Подов, поместив локальные команды в предоставляемые Cocoapods pre-/post-hooks в Podfile, например, в функцию post_integrate:
post_integrate do
if ENV['UPDATE_TRUNK'] == "true"
puts "Copying and zipping trunk from ~/.cocoapods/repos"
system("cp -r ~/.cocoapods/repos/trunk $PWD")
system("zip -rm trunk.zip trunk >/dev/null")
end
end
При желании можно дополнить команды условием, при котором обновление trunk.zip будет выполнятся только, если переменная UPDATE_TRUNK == true. Такая проверка нужна, чтобы обновление trunk.zip не происходило на CI после pod install, а также лишний раз локально, когда этого не нужно. В таком виде для обновления trunk.zip нужно всего лишь задать переменную UPDATE_TRUNK перед очередным pod install/update:
export UPDATE_TRUNK=true && pod install
or
export UPDATE_TRUNK=true && pod update
or
export UPDATE_TRUNK=true; pod install --repo-update
Cocoapods создает папку repo trunk по дефолту в рассмотренной директории для накопления в ней скаченных подспеков. Причем подспеки библиотек, используемых в других проектах, скачиваются в эту же папку trunk. И в принципе, для этого подхода ничего страшно, что при копировании trunk в нем будут лишние подспеки, главное, чтобы в нем присутствовали подспеки используемые в текущем проекте. Но при желании можно удалить текущий trunk перед очередным pod install, и новый чистый trunk создастся во время исполнения команды. Это возможно уменьшит размер trunk.zip, так как в ней перед копированием в проект не будут находиться лишние подспеки.
pod repo remove trunk
pod install
cp -r ~/.cocoapods/repos/trunk $PWD
zip -rm trunk.zip trunk >/dev/null
Также, если не хочется удалять текущий локальный trunk, можно создать такую же repo папку для скачивания в нее подспеков только используемых в текущем проекте библиотек. Для начала переименовываем текущий trunk. Затем создаем CDN trunk с помощью команды pod repo add-cdn, передавая в нее название trunk и url. И после скачивания в новый trunk подспеков во время pod install и архивирования его в проект удаляем новый trunk и переименовываем обратно свой родной trunk:
mv ~/.cocoapods/repos/trunk ~/.cocoapods/repos/trunk_backup
pod repo add-cdn trunk https://cdn.cocoapods.org/
pod install
cp -r ~/.cocoapods/repos/trunk $PWD
zip -rm trunk.zip trunk >/dev/null
pod repo remove trunk
mv ~/.cocoapods/repos/trunk_backup ~/.cocoapods/repos/trunk
Результат
Как говорилось ранее, во время команды pod install на CI в подстадии Resolving dependencies of `Podfile` вместо скачивания подспеков из репы Cocoapods возвращает и обрабатывает локальные файлы, уже находящиеся в trunk. Это можно подробнее наблюдать, вызвав команду pod install --verbose.
![Cocoapods находит все нужные файлы локально в trunk, поэтому скачивание не происходит Cocoapods находит все нужные файлы локально в trunk, поэтому скачивание не происходит](https://habrastorage.org/getpro/habr/upload_files/1c6/9f1/420/1c69f142028bcf6d1db78315b997b2c2.png)
По скрину видно, что перед скачиванием каждой подспеки .podspec.json сначала скачивается текстовый файл c префиксом all_pods_versions и с тремя символами в конце. Это обусловлено механизмом CDN, используемым Cocoapods для оптимизации. Cocoapods сперва берет название Подов, прописанных в Podfile, высчитывает из названий хеши SHA, используя md5, и берет из этих хешей первые три символа. Например, для Пода InputMask хеш по md5 - 7c16b23066345d6d2feded8e232ce756. Так, Cocoapods возьмет префикс 7c1 из хеша и скачает файл all_pods_versions_7_с_1.txt из CDN репы. В этом файле прописаны все версии всех Подов, у которых первые 3 символа хеша от названия будут совпадать с нашим InputMask, то есть тоже 7c1. Cocoapods скачает подспеки для всех версий нашего Пода, делая https запросы по типу - https://cdn.cocoapods.org/Specs/7/c/1/InputMask/1.0.0/InputMask.podspec.json. Как видно, в репозитории InputMask библиотека хранится по пути Specs/7/с/1/InputMask. И по такому же пути будут хранится все подспеки для нее в нашей локальной директории в trunk после скачивания. Такой механизм используется Cocoapods c 2019, чтобы значительно ускорить выполнение команды pod install, так как больше не нужно делать полное клонирование огромного репозитория Specs. Вместо этого загружаются только спецификации Podspecs, необходимые для всего нашего дерева зависимостей.
![Подспеки для InputMask распологаются в ~/.cocoapods/repos/trunk/Specs/7/c/1/InputMask/ Подспеки для InputMask распологаются в ~/.cocoapods/repos/trunk/Specs/7/c/1/InputMask/](https://habrastorage.org/getpro/habr/upload_files/147/8fd/8f5/1478fd8f5d24fb86f13ac2a4cfaf69f0.png)
Вывод
Мы рассмотрели метод возобновление успешного выполнения команды pod install на раннерах без выхода в интернет, но с имеющимися Подами в проекте. Был предложен способ, при котором нужно единожды скопировать валидный и актуальный trunk на локальной машине разработчика с интернетом прямо в проект в виде trunk.zip, а затем на раннере без интернета скопировать trunk из проекта в папку ~/.cocoapods/repos. Обсудили возможные дополнительные, остававшиеся в тени случаи, когда вызов команды pod install нужен на CI, такие как необходимость в интеграции Подов в проекты или желание генерировать каждый раз служебные Pods файлы заново, поместив их в gitignore. Детально разобрали этапы выполнения команды pod install, выделив для себя полезные нам стадии.
Bardakan
podfile же уже имеет средства для того, чтобы указать свои пути к репозиториям и даже к своему CDN. Чем ваш способ отличается?
artemVorkhlik Автор
Действительно в Podfile можно указать пути к своему репозиторию со specs и sources, например:
При отсутствии интернета можно создать в своем закрытом окружении такой репозиторий со всеми подспеками и обращаться к нему кажды раз на CI. Но при добавлении новых подов в проект нужно будет обновлять такой репозитория новыми подспеками, держать его всегда актуальным.
Мой подход предлагает хранить все нужные подспеки в проекте, благодаря чему pod install не нужно будет никуда обращаться для скачивания, и не нужно создавать свои репозитории