Проект iOS может содержать совсем немного кода на Swift, но при этом включать десятки единиц компиляции Objective-C, C или C++. Когда задание на облачном Mac запускается в новом рабочем каталоге, для всех этих нативных зависимостей снова вызывается Clang, поэтому преимущества инкрементальной сборки теряются. ccache устраняет эту повторную работу: он формирует ключ на основе результатов препроцессинга, параметров компиляции и идентификатора компилятора, а при совпадении сразу использует сохранённый объектный файл.
Однако ccache — не универсальный переключатель для ускорения Xcode. Перед внедрением необходимо убедиться, что основное время действительно тратится на Clang, а не на фронтенд Swift, компоновку, компиляцию ресурсов или выполнение тестов.
Сначала определите, что именно нужно кешировать
Сохраните полный журнал сборки и найдите в нём CompileC, CompileC normal, а также файлы .m, .mm, .c, .cc и .cpp. Внедрение ccache оправдано только в том случае, если эти этапы занимают заметную долю времени. В проекте, целиком написанном на Swift, выигрыш может быть близок к нулю даже при правильной настройке.
Сначала зафиксируйте три базовых показателя:
- длительность полной сборки после очистки;
- длительность повторной сборки без изменений в коде;
- количество нативных единиц компиляции и несколько самых медленных целей.
Не пытайтесь сразу кешировать весь каталог DerivedData. В нём находятся индексы, кеши модулей, база данных сборки и промежуточные артефакты, тесно связанные с версией Xcode, SDK, абсолютными путями и графом проекта. Каталог ccache лучше использовать как отдельную и наблюдаемую единицу кеширования.
Сначала докажите, что задание повторно вызывает Clang, и только потом оценивайте долю попаданий в кеш. Иначе замедление компоновки, проверку типов Swift или длительное выполнение скриптов легко принять за неработающий кеш.
Передайте управление Clang через обёртки
После установки ccache в среде сборки не заменяйте файлы системного компилятора. Вместо этого создайте в рабочем каталоге проекта две небольшие обёртки и передайте их через настройки сборки Xcode. Для отката будет достаточно удалить соответствующие параметры, а другие задания на том же узле не будут затронуты.
set -eu
export CCACHE_BIN="$(command -v ccache)"
export CCACHE_DIR="$HOME/.cache/pingmini-ccache"
export CCACHE_BASEDIR="$PWD"
mkdir -p .ci/bin "$CCACHE_DIR"
cat > .ci/bin/clang <<'SH'
#!/bin/sh
exec "${CCACHE_BIN:?CCACHE_BIN is required}" /usr/bin/clang "$@"
SH
cat > .ci/bin/clang++ <<'SH'
#!/bin/sh
exec "${CCACHE_BIN:?CCACHE_BIN is required}" /usr/bin/clang++ "$@"
SH
chmod +x .ci/bin/clang .ci/bin/clang++
ccache --set-config="cache_dir=$CCACHE_DIR"
ccache --set-config="base_dir=$PWD"
ccache --set-config="compiler_check=content"
ccache --set-config="compression=true"
ccache --set-config="max_size=20G"
ccache --zero-stats
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration Release \
-derivedDataPath "$DERIVED_DATA" \
build \
CC="$PWD/.ci/bin/clang" \
CPLUSPLUS="$PWD/.ci/bin/clang++"
ccache --show-stats
CC используется для C и Objective-C, а CPLUSPLUS — для C++ и Objective-C++. Не оборачивайте компоновщик и не выдавайте обёртку за компилятор Swift. После запуска проверьте по подробному журналу, что фактически выполняется .ci/bin/clang: одного лишь наличия установленного ccache недостаточно, чтобы подтвердить его использование Xcode.
Обеспечьте стабильность ключа кеша между заданиями
В CI один и тот же коммит часто извлекается в каталоги, имена которых содержат номер задания. Исходный код не меняется, но абсолютные пути оказываются разными, из-за чего могут измениться отладочная информация, пути к заголовочным файлам и параметры компиляции. CCACHE_BASEDIR и base_dir преобразуют пути внутри рабочего каталога в относительные — это первый шаг к повторному использованию кеша между заданиями.
Кроме того, необходимо зафиксировать следующие входные данные:
Зафиксируйте набор инструментов и параметры сборки
Ключ кеша должен учитывать основную версию Xcode, SDK, целевую архитектуру, имя конфигурации и хеш файлов блокировки зависимостей. При обновлении набора инструментов лучше явно переключить пространство имён кеша: это проще контролировать, чем смешивать старые и новые объекты в одном каталоге.
Один и тот же кеш не должен одновременно обслуживать Debug и Release. Также не следует заставлять разные архитектуры использовать искусственно упрощённый общий ключ. ccache распознаёт различия в параметрах, однако слишком широкий внешний ключ сохраняемого кеша приводит к многочисленным бесполезным загрузкам и вытеснениям.
Контролируйте генерируемые файлы
Если генерируемый заголовочный файл при каждом запуске получает текущее время, случайный идентификатор или временный абсолютный путь, кеш всех зависящих от него единиц компиляции становится недействительным. Скрипт генерации должен перезаписывать файл только при изменении содержимого, сравнивая его с временным файлом и выполняя атомарную замену:
generate_config > "$TMPDIR/build-config.new"
if ! cmp -s "$TMPDIR/build-config.new" Generated/build-config.h; then
mv "$TMPDIR/build-config.new" Generated/build-config.h
else
rm "$TMPDIR/build-config.new"
fi
Не используйте в исходном коде макросы времени, значение которых меняется при каждой компиляции. Если приложению действительно необходимо время сборки, вынесите его в отдельный файл, чтобы сократить область повторной компиляции.
Проверьте попадания в кеш двумя сборками
При первом внедрении проведите изолированную проверку в два этапа. Перед первой сборкой обнулите статистику и оставьте кеш пустым, а затем выполните полную сборку. Перед второй удалите DerivedData, но сохраните ccache, после чего повторите сборку с точно теми же инструментами и параметрами. Только прямые попадания во время второй сборки подтверждают, что повторное использование объектов не зависит от старого каталога сборки.
Проверяйте cacheable calls, hits, misses и uncacheable calls, а не только общий процент попаданий. При первой сборке ожидаемо преобладают промахи. Если и во второй сборке все обращения заканчиваются промахами, выполните следующие проверки по порядку:
- Изучите журнал сборки и убедитесь, что обёртки действительно вызываются;
- Сравните рабочий каталог, SDK, архитектуру и параметры компиляции в обеих сборках;
- Проверьте, меняются ли генерируемые заголовочные файлы при каждом запуске;
- Найдите макросы времени, случайные значения и абсолютные пути;
- Убедитесь, что каталог кеша действительно сохраняется после завершения задания.
Если значение uncacheable calls велико, включите отладочный журнал ccache и воспроизведите компиляцию только одной единицы. Так вы не создадите огромный объём диагностических файлов для всей сборки. После обнаружения причины отключите отладку, чтобы журналы не заполнили диск.
Установите границы очистки и отката
Максимальный размер кеша следует выбирать с учётом масштаба нативных целей, а не разрешать ему расти без ограничений. Регулярно выполняйте ccache --cleanup и отслеживайте размер каталога, количество вытеснений и ошибки записи. Несколько параллельных заданий могут использовать общий каталог, но он должен находиться в надёжной локальной файловой системе. Нестабильное удалённое подключение может свести всю экономию времени компиляции на нет из-за задержек при работе с метаданными.
При подозрении на ошибочное повторное использование сначала сохраните журналы и статистику кеша, а затем повторите ту же команду с CCACHE_DISABLE=1. Если проблема исчезнет, сузьте область кеша по набору инструментов, цели и конфигурации. Не рассматривайте отключение проверок, смягчение правил сравнения временных меток или игнорирование изменений заголовочных файлов как долгосрочное решение.
Итоговый критерий успешного внедрения — не высокий процент попаданий сам по себе, а одновременное выполнение трёх условий: артефакты совпадают со сборкой без кеша, во второй сборке объём нативной компиляции заметно уменьшается, а каждое аннулирование кеша объясняется изменением набора инструментов или входных данных. Только после этого ccache можно считать поддерживаемым уровнем инфраструктуры сборки.
Часто задаваемые вопросы
Может ли ccache напрямую ускорить компиляцию Swift?
Нет. В этой схеме ccache оборачивает Clang и помогает при сборке Objective-C, Objective-C++, C и C++. Для Swift важны стабильный DerivedData, разумный параллелизм и инкрементальная сборка Xcode.
Почему повторная сборка не дает попаданий в кеш?
Сначала убедитесь, что Xcode действительно запускает обертку. Затем сравните путь рабочей копии, аргументы компилятора, создаваемые заголовки и меняющиеся макросы.
Что сохранять в CI: DerivedData или каталог ccache?
Сначала сохраняйте ccache как отдельный кеш. DerivedData сильнее зависит от версии Xcode, абсолютного пути, SDK и текущего состояния графа проекта.
Выберите выделенный физический Mac для следующего проекта
Выберите облачный Mac с одной из трёх фиксированных конфигураций для разработки, непрерывной интеграции или тестирования локальных моделей и настройте его на нужный срок.