Статья о том как починить инкрементальную компиляцию в Xcode для Swift проектов и ускорить build phases для Cocoapods и Carthage, ничего не поломав.
Небольшой спойлер: на трех разных проектах получилось сократить время инкрементальной сборки в 9 раз!
Туториал несет сугубо практический характер с минимумом воды. Обязательно к прочтению для действующих iOS разработчиков.
Проблема
У нас в компании ведется несколько параллельных проектов на Swift и, практически, везде была сложность с последовательной сборкой. На каждый повторный 'Run' уходило от 30 до 50 секунд. Минуту пишется код, пол минуты ждешь пока соберется проект. Успеваешь сходить за чаем, покурить, посчитать налоги и иногда вздремнуть.
Однозначно надо было что-то менять. В предыдущих моих статьях, где описываются актуальные проблемы компилятора и способы их решения, мы смогли добиться определенных успехов. Тем не менее, это не устранило трудности с инкрементальной компиляцией, которая съедала большую часть времени, постоянно выбивая своей медлительностью из рабочего процесса. Уверен, каждый с этим сталкивается регулярно, приняв как часть рабочего процесса.
Но мы не стали с этим мириться, решив расследовать все обстоятельства дела и в итоге получив неплохой результат. О чем и буду рад рассказать.
P.S. Картинка в шапке не означает, что мы 'вертели' Xcode. Это его якобы так ускоряем.
Инкрементальная компиляция
Если кто не знаком с термином — это способ сборки только изменившихся мест кода без тотальной рекомпиляции всего проекта. А еще это то, что нормально не работает в Xcode.
В прошлом посте товарищ Gxost подсказал идею, которая на некоторое время решила проблему. Мы уже начали открывать шампанское всей командой и вешать портрет императора избавителя на стену, но, к сожалению, в этот момент проблема возобновилась.
Судя по всему, мы не одни такие. О рецидивах пишут и на StackOverflow под ответом, а так же в исходной теме на форумах Apple.
Это ни в коем случае не палка в огород. Даже наоборот — благодарность, все это подтолкнуло продолжить копать. Если хоть и временно, но проблема была решена, то значит истина где-то рядом.
Что вообще такое этот header map для которого мы ставим флаг по рекомендации Apple?
Немного веб-археологии по документации яблока:
Header maps (also known as “header maps”) are files Xcode uses to compile the locations of the headers used in a target.
Своими словами, header maps — это индексный файл Xcode с местоположениями хидеров проекта. Файл о всех хидерах, которые мы используем.
Так как я обещал не погружаться слишком глубоко в теорию, то вот отличная статья на эту тему. Легко читается, крайне рекомендую.
Главное, что нечто в этих header maps(или то, что их использует) провоцирует недобросовестную работу инкрементальной компиляции. Если мы нашли такую техническую опухоль, то давайте поступим без полумер и просто их отключим.
Заходим в build settings и убираем лишние детали выставляем существующий флаг USE_HEADER_MAPS в NO:
Теперь нам надо компенсировать потерянный функционал и вручную добавить расположение всех хидеров проекта.
Никуда не уходим из настроек и руками прописываем пути до папок с заголовками в поле 'user header search paths':
В Swift проекте их должно быть мало, только ваши кастомные Obj-C вещи.
Опять делаем полный clean и пробуем завести проект. Если влетели в следующую ошибку,
то значит вы просто забыли упомянуть путь до одного или нескольких заголовочных файлов. После их исправления все должно собраться без осложнений, так как кроме этого мы никаких изменений не вносили.
В качестве бонуса отметим, что инкрементальная компиляция работает наилучшим образом при выключенном whole-module-optimization(WMO) о котором мы говорили в прошлый раз. Это не значит, что для полномодульной оптимизациии решение не работает, просто без нее все проходит на несколько секунд быстрее. Пускай при этом сборки с чистого листа и тянутся целую вечность.
Здесь уже каждый сам для себя решает, что ему удобнее, быстрее и лучше подходит. Если вы решили отказаться от WMO, то достаточно убрать флаг SWIFT_WHOLE_MODULE_OPTIMIZATION из настроек проекта, каких-либо сложностей возникнуть точно не должно.
Результат: инкрементальную компиляцию мы успешно починили. Отныне должно тратиться не более нескольких секунд на сам факт сборки Swift, не считая codesign, линковки и различных build scripts(о них мы поговорим ниже). Внутри компании мы проверили этот метод на трех разных проектах, нескольких версия OSX и вообще множестве конфигурациях в целом, что в том числе относится и к Xcode. И вот уже продолжительное время рецидивов не наблюдаем.
Разгон Cocoapods и Carthage
Наверное, многие замечали, что кроме самой компиляции еще много времени уходит на разные 'shell scripts':
При использовании cocoapods и/или carthage их даже несколько. На их выполнение уходит от 3 до 10 секунд каждый раз в зависимости от скорости вашего диска, процессора и положения звезд на небе. Cocoapods прописывает себя туда автоматически, а для Carthage приходится это делать вручную.
Немного изучив контент, мы выяснили, что эти скрипты занимаются ничем другим как копированием своих ресурсов в сборочную директорию проекта. При этом они не заботятся о том были уже скопированы этих файлы или нет. Хотя, гораздо логичнее было бы проверять наличие нужных ресурсов перед их повторным копированием.
Что мы и сделаем.
Начнем с Carthage.
Вдохновением для этого решения послужил недооцененный ответ на StackOverflow, где приняли и покрыли лайками одноразовый костыль. Кстати, если вы вдруг решите воспользоваться одобренным решением, то поймаете дикие и горячие ошибки в Runtime стоит вам сделать полный clean.
Мы так поступать не будем, поэтому пойдем по правильному пути и видоизменим наш Copy Carthage Frameworks build phase:
FILE="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Carthage-Installation-Flag"
if [ -f "$FILE" ]; then
exit 0
fi
touch "$FILE"
/usr/local/bin/carthage copy-frameworks
Этот скрипт дополнительно копирует вместе с ресурсами еще один пустой файлик, который служит маяком, что операция прошла успешно. Он не занимает места, не дает побочных эффектов и разрешен детям с 3-х лет. В следующий раз, когда скрипт будет запущен, он сначала проверит наличие этого файла, и если файл есть, то операция автоматически завершится без лишних телодвижений.
Для уверенности, скриншот результата, который у нас должен получиться:
Важно! Если вы решите добавить или удалить зависимость из Carthage, то перед сборкой проекта обязательно нужно провести полный clean. Иначе скрипт так и будет считать, что он уже все давно установил. А вы будете в поте лица пытаться найти объяснение происходящему.
Кстати, найти функцию clean можно в меню по пути Product -> Clean.
Если еще зажать option(alt), то можно сделать особый clean, который к тому же изгоняет демонов из проекта удаляет различные локальные настройки, кеш и часто дающую сбой прочую ерунду.
Теперь Cocoapods
Для бобов принцип тот же самый, но так как скрипты генерируются автоматически, придется добавить немного магии в Podfile. Чтобы это сделать, докинем следующие строки в самый конец файла:
post_install do |installer|
Dir.glob(installer.sandbox.target_support_files_root + "Pods-*/*.sh").each do |script|
flag_name = File.basename(script, ".sh") + "-Installation-Flag"
folder = "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
file = File.join(folder, flag_name)
content = File.read(script)
content.gsub!(/set -e/, "set -enKG_FILE="#{file}"nif [ -f "$KG_FILE" ]; then exit 0; finmkdir -p "#{folder}"ntouch "$KG_FILE"")
File.write(script, content)
end
end
Принцип работы этого скрипта такой же: он добавляет в скрипты копирования ресурсов проверку на файл-флаг. При этом не меняет build phases, в отличие от Carthage, а сразу изменяет сам скрипт cocoapods.
Кстати, если у вас уже был задействован блок post_install, то не надо создавать еще один, достаточно поместить скрипт внутрь уже имеющегося. Последние версии Cocoapods(1.1.1) выводят предупреждение, если вы напортачите, а вот более ранние просто молча проглотят ошибку, обеспечив вам веселую отладку.
Теперь можно сделать 'pod install', чтобы изменения вступили в силу. Как и в случае с Carthage, при редактировании Podfile, тоже нужен полный clean проекта перед запуском.
P.S. Ruby не мой родной язык, попридержите тапки.
Заключение
Таким образом, у нас получилось сократить время сборки проекта с 45-и секунд до 5-и.
Чуть не забыл пруфы. Время сборки до:
Скриншот взят из видео к прошлой статье.
Время сборки после:
На мой взгляд, крайне значимый результат. Подход с сокращением издержек должен обязательно стоять на вооружении каждой компании и разработчика.
Уверен, что среди вас найдутся люди с опытом оптимизации Xcode, готовые дополнить и поделиться мыслями. Да и вообще, хотелось бы в комментариях видеть вопросы, чтобы в следующих своих статьях упоминал вас, ваш опыт, опыт ваших коллег и освещал тематику, т.к. для меня это хобби, жизнь и работа.
Напоследок поделюсь утилитой, которой я пользовался для измерения времени компиляции методов: https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode
Показывает индивидуально каждую функцию и суммарное время затраченное на её сборку.
Автор: Mehdzor