Если при сборке Xcode на удалённом облачном Mac внезапно появляется ошибка Cycle inside ... или Multiple commands produce ..., первое и самое распространённое неверное действие — сразу удалить DerivedData. Это может временно изменить порядок сообщений об ошибках, но одновременно уничтожит полезные данные для диагностики. Надёжнее зафиксировать версию кода, Scheme, конфигурацию и путь к Xcode, сохранить полный журнал, а затем определить, связана ли проблема с циклом зависимостей Target или с тем, что несколько Build Phase претендуют на один и тот же выходной путь.
Сначала добейтесь стабильного воспроизведения ошибки
Перед началом диагностики приостановите автоматическое получение изменений, обновление зависимостей и параллельные сборки, чтобы рабочая область не менялась между экспериментами. Зафиксируйте текущий коммит, версию Xcode и фактически выбранный каталог разработчика:
set -o pipefail
git rev-parse HEAD
xcodebuild -version
xcode-select -p
mkdir -p .diagnostics
xcodebuild \
-project App.xcodeproj \
-scheme App \
-configuration Debug \
-destination 'generic/platform=iOS Simulator' \
-showBuildTimingSummary \
build 2>&1 | tee .diagnostics/build.log
Если проект использует Workspace, замените -project App.xcodeproj на -workspace App.xcworkspace. Не передавайте оба параметра одновременно. Затем извлеките ключевые строки, сохранив исходный журнал для проверки контекста:
grep -nE \
'Cycle inside|cycle in dependencies|Multiple commands produce|will be run during every build' \
.diagnostics/build.log
Последняя команда в журнале обычно лишь указывает место, где был обнаружен конфликт, и не обязательно является началом цикла. Проследите выведенную Xcode цепочку зависимостей вверх до первого повторяющегося Target, скрипта или пути к результату.
Различайте два типа похожих ошибок
Цикл зависимостей означает, что A должен дождаться B, а B прямо или косвенно должен дождаться A. Дублирование результатов означает, что две команды объявили себя ответственными за один и тот же файл. Эти ошибки могут возникать одновременно, но устраняются по-разному.
| Признак в журнале | Что проверить в первую очередь | Типичная причина |
|---|---|---|
Cycle inside |
Target Dependencies, неявные зависимости | App и Framework зависят друг от друга |
Multiple commands produce |
Copy Files, Compile Sources, Run Script | Один файл копируется или создаётся дважды |
| Скрипт запускается при каждой сборке | Входы и выходы Run Script | Зависимости не объявлены или их анализ отключён |
| Ошибка возникает только при Archive | Этап Embed, скрипты архивирования | Этапы для Debug и Release настроены по-разному |
Сначала с помощью команд проверьте, какие Target и конфигурация действительно используются в этой сборке, вместо того чтобы ориентироваться на текущие параметры в интерфейсе Xcode:
xcodebuild -list -json -project App.xcodeproj \
> .diagnostics/project-list.json
xcodebuild -showBuildSettings \
-project App.xcodeproj \
-scheme App \
-configuration Debug \
> .diagnostics/build-settings.txt
Проследите цепочку зависимостей через Target и Build Phase
В разделе Target Dependencies Xcode начните проверку с Target, находящихся в начале и конце цепочки из сообщения об ошибке. App может зависеть от Framework, но Framework не должен зависеть от App ради доступа к определённым в нём типам. Общий код следует вынести в отдельный модуль либо устранить обратную зависимость с помощью протоколов и внедрения зависимостей.
Затем последовательно проверьте все Build Phases, уделяя особое внимание следующим связям:
Проверьте дублирование владельцев
Один исходный файл не должен одновременно находиться в двух элементах Compile Sources, создающих одинаковый объектный файл. Один Framework также не должен сначала обрабатываться автоматически созданным этапом встраивания, а затем повторно копироваться пользовательским этапом Copy Files. Для генераторов кода необходимо явно задать каталог результатов и не записывать выходные файлы обратно в каталог исходного кода, который уже компилируется другим Target.
Проверьте неявные зависимости
Если скрипт по фиксированному пути читает результат сборки другого Target, но зависимость не объявлена явно, порядок сборки может меняться в зависимости от степени параллелизма. Не маскируйте проблему отключением параллельной сборки: это лишь снижает вероятность ошибки, но не исправляет структуру графа.
Явно укажите входы и выходы Run Script
Run Script часто становится источником циклов. Если скрипт читает файл конфигурации, создаёт файл Swift или копирует ресурсы, укажите в соответствующей фазе Input Files и Output Files либо используйте списки файлов. Кроме того, каждый результат должен создаваться только одной фазой.
Например, скрипт генерации файла версии может сначала проверить обязательные переменные, а затем использовать временный файл и атомарную замену:
set -euo pipefail
input="${SRCROOT}/Config/version.txt"
output="${DERIVED_FILE_DIR}/GeneratedVersion.swift"
tmp="${output}.tmp"
test -f "$input"
mkdir -p "$(dirname "$output")"
version="$(tr -d '
' < "$input")"
printf 'enum GeneratedVersion { static let value = "%s" }
' \
"$version" > "$tmp"
if test -f "$output" && cmp -s "$tmp" "$output"; then
rm "$tmp"
else
mv "$tmp" "$output"
fi
В соответствующей фазе объявите $(SRCROOT)/Config/version.txt входным файлом, а $(DERIVED_FILE_DIR)/GeneratedVersion.swift — выходным. Размещение сгенерированных файлов в производном каталоге вместо непосредственного изменения файлов репозитория упрощает определение владельца и снижает риск того, что параллельные задачи перезапишут результаты друг друга.
Проверьте исправление минимальными изменениями
За один раз меняйте только одну зависимость, одну фазу или один выходной путь, а затем сохраняйте новый журнал. Одновременное удаление нескольких ссылок может позволить сборке завершиться, но затруднит определение истинной причины и способно скрыть аналогичную проблему в конфигурации архивирования.
После исправления выполните проверку в следующем порядке:
- Выполните одну чистую сборку и убедитесь, что полный граф успешно строится с нуля.
- Дважды подряд запустите сборку без очистки и убедитесь, что скрипты корректно пропускают неизменившиеся входные данные.
- Отдельно проверьте Debug и Release; для выпускаемого проекта также выполните Archive.
- Сравните два журнала и убедитесь, что не появилось новых дублирующихся результатов и предупреждений о безусловном запуске скриптов.
- Опишите изменения зависимостей Target, входы и выходы фаз, а также причины исправления в комментарии к проверке кода.
Очистку DerivedData можно использовать как последний изолирующий эксперимент, но не как окончательное исправление. В действительно стабильном проекте Xcode порядок зависимостей должен оставаться одинаковым при разной степени параллелизма, чистой и инкрементальной сборке. Только в этом случае интерактивная разработка и автоматические задачи на облачном Mac не будут случайно завершаться с ошибкой из-за изменений в планировании.
Часто задаваемые вопросы
Устраняет ли удаление DerivedData цикл сборки Xcode?
Обычно нет. Старые промежуточные файлы исчезнут, но ошибочные зависимости targets или одинаковые пути вывода снова создадут проблему.
Почему фаза Run Script может создать цикл графа сборки?
Если входы и выходы не объявлены либо скрипт пишет туда же, куда другая фаза, Xcode не может определить однозначный порядок выполнения.
Как проверить исправление?
Выполните одну чистую и две последовательные сборки без очистки. В журналах не должно быть циклов, дублирующихся выходов и безусловно запускаемых скриптов.
Запустите следующую задачу на удалённом Mac на выделенном узле
Выберите одну из двух доступных конфигураций, пять узлов и период оплаты: день, неделя, месяц или квартал. Перед заказом можно полностью проверить конфигурацию и сумму в USD.