В некоторых компаниях в целях безопасности на раннерах для 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 выполняет несколько последовательных стадий для установки Подов:

  1. Analyzing dependencies - самая долгая стадия, имеет много подэтапов. Здесь анализируются зависимости для таргетов по Podfile, а также скачиваются подспеки используемых фреймворков из централизованного репозитория Specs из Github в подэтапе Resolving dependencies of `Podfile`. Важно, что если все нужные подспеки уже имеются в локальной директории ~/.cocoapods/repos/trunk, то скачивания не происходит.

  2. Downloading dependencies - скачиваются сорсы Подов, если каких-либо не хватает согласно предыдущему анализу. Если все хватает, то ничего скачиваться не будет.

  3. Generating Pods project - генерируется проект подов Pods.xcodeproj и служебные файлы в папке Pods/Target Support Files, такие как xcconfig файлы, нужные для линковки подов с нашими таргетами.

  4. Integrating client projects - интеграция Подов в таргеты наших проектов xcodeproj.

Этапы команды pod install
Этапы команды pod install

Существуют кейсы, когда нам необходимо выполнение последних двух этапов этой команды. Например, 4-ый этап интеграции Подов в таргеты нужен при использовании Tuist в проекте. Но при отсутствие интернета, команда фейлится с ошибкой CDN на первом же этапе (Analyzing dependencies) на CI. Эта ошибка символизирует невозможность как найти нужные файлы локально, так и скачать по url https://cdn.cocoapods.org/. В зависимости от того, отсутствует ли папка trunk полностью, либо в ней не хватает какого-либо файла или подспеки для текущей конфигурации Podfile, формулировка ошибки будет отличаться, но причина ее появления одна.

Ошибки команды pod install при отсутствии интернета
Ошибки команды pod install при отсутствии интернета

Есть множество случаев необходимости или желания успешного исполнения команды 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, поэтому скачивание не происходит

По скрину видно, что перед скачиванием каждой подспеки .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/

Вывод

Мы рассмотрели метод возобновление успешного выполнения команды pod install на раннерах без выхода в интернет, но с имеющимися Подами в проекте. Был предложен способ, при котором нужно единожды скопировать валидный и актуальный trunk на локальной машине разработчика с интернетом прямо в проект в виде trunk.zip, а затем на раннере без интернета скопировать trunk из проекта в папку ~/.cocoapods/repos. Обсудили возможные дополнительные, остававшиеся в тени случаи, когда вызов команды pod install нужен на CI, такие как необходимость в интеграции Подов в проекты или желание генерировать каждый раз служебные Pods файлы заново, поместив их в gitignore. Детально разобрали этапы выполнения команды pod install, выделив для себя полезные нам стадии.

Комментарии (2)


  1. Bardakan
    16.03.2024 14:53
    +1

    podfile же уже имеет средства для того, чтобы указать свои пути к репозиториям и даже к своему CDN. Чем ваш способ отличается?


    1. artemVorkhlik Автор
      16.03.2024 14:53

      Действительно в Podfile можно указать пути к своему репозиторию со specs и sources, например:

      source 'https://github.com/artsy/Specs.git' 
      source 'https://cdn.cocoapods.org/'

      При отсутствии интернета можно создать в своем закрытом окружении такой репозиторий со всеми подспеками и обращаться к нему кажды раз на CI. Но при добавлении новых подов в проект нужно будет обновлять такой репозитория новыми подспеками, держать его всегда актуальным.

      Мой подход предлагает хранить все нужные подспеки в проекте, благодаря чему pod install не нужно будет никуда обращаться для скачивания, и не нужно создавать свои репозитории