Заметки по разработке

Диагностика сдвига времени файлов в Cloud Mac CI

Диагностика сдвига времени файлов в Cloud Mac CI

Код не менялся, но при второй сборке Xcode на облачном Mac по-прежнему компилируется множество целей. При этом тот же коммит в новом рабочем каталоге снова собирается нормально. Причина подобных проблем не обязательно связана с DerivedData: неверные метки времени могут присутствовать в исходном коде, сгенерированных файлах или артефактах кэша. Очистка кэша временно устраняет симптомы, но без изменения процессов синхронизации и восстановления временной сдвиг снова попадёт в рабочий каталог.

Сначала убедитесь, что проблема связана со сдвигом времени

Сначала зафиксируйте коммит, Scheme, путь к Xcode и параметры сборки, а затем дважды подряд выполните одну и ту же сборку. Во время первого запуска разрешите создать полный набор промежуточных артефактов, а во время второго проверьте, какие задачи выполняются повторно. Если снова запускаются одни и те же этапы компиляции Swift, обработки ресурсов или выполнения скриптов, сравните результаты в чистом и повторно используемом рабочих каталогах.

При диагностике не ограничивайтесь общей продолжительностью сборки. В первую очередь фиксируйте пути к входным файлам повторно выполняемых задач, выходные данные скриптов и каталоги генерации. Также проверяйте, не перезаписывает ли какой-либо инструмент файлы перед началом сборки. Типичные признаки:

Инкрементальная сборка зависит не только от содержимого файлов. Если временная последовательность входных и выходных данных нарушена, система сборки может постоянно считать уже завершённые задачи требующими повторного выполнения.

Поиск файлов из будущего и проблемных каталогов

Сначала проверьте системное время и часовой пояс, затем найдите файлы, время которых опережает текущее более чем на пять минут. Допуск в пять минут позволяет исключить небольшие погрешности при сборе данных, но при этом выявляет заметный сдвиг.

#!/bin/zsh
set -euo pipefail

root="${1:-$PWD}"
limit=$(( $(date +%s) + 300 ))

find "$root" -type f -print0 |
while IFS= read -r -d '' file; do
  modified=$(stat -f '%m' "$file")
  if (( modified > limit )); then
    printf '%s\t%s\n' \
      "$(date -r "$modified" '+%Y-%m-%d %H:%M:%S %z')" \
      "$file"
  fi
done

Область сканирования должна как минимум включать исходный код, файлы проекта, скрипты, ресурсы, каталоги генерации кода и восстановленный кэш. Не сканируйте сразу весь пользовательский каталог: кэши менеджеров пакетов, журналы и системные файлы создадут слишком много шума. Для каждого найденного файла дополнительно выполните stat -x 文件路径 и проверьте время изменения, время смены метаданных и том, на котором расположен файл.

Если аномалии сосредоточены в одном каталоге, источник обычно можно быстро сузить до конкретного этапа загрузки, распаковки, синхронизации или генерации кода. Если они распределены по всему репозиторию, проверьте способ копирования рабочего каталога и скрипты инициализации, запускаемые перед началом задачи.

Выбор способа исправления с учётом источника

Для разных источников требуются разные способы исправления. Маскировать все случаи с помощью touch нельзя.

Симптом Типичный источник Рекомендуемое действие
Время небольшого числа исходных файлов находится в будущем Ошибка в архиве или источнике синхронизации Получить файлы повторно и проверить время на исходной машине
Сгенерированные файлы обновляются при каждом запуске Генератор безусловно перезаписывает файлы Выполнять атомарную замену только после изменения содержимого
Временная последовательность в каталоге кэша нарушена При восстановлении сохранены некорректные метаданные Отказаться от этой партии кэша и заново сформировать ключ кэша
Время всего рабочего каталога близко ко времени копирования Параметры копирования изменяют метаданные Унифицировать способ получения кода и не смешивать разные стратегии синхронизации
Выходные файлы постоянно старше входных Некорректное время внутри пакета кэша Создать кэш заново и проверить его манифест

Для исходного кода под управлением Git наиболее надёжным решением обычно будет убедиться в отсутствии незакоммиченных изменений и заново получить затронутый каталог, а не переписывать временные метки во всём репозитории. Генератор должен сначала записывать результат во временный файл, сравнивать содержимое и только затем выполнять замену:

generate_config > Config.generated.swift.tmp
if ! cmp -s Config.generated.swift.tmp Config.generated.swift; then
  mv Config.generated.swift.tmp Config.generated.swift
else
  rm Config.generated.swift.tmp
fi

Если содержимое не изменилось, время изменения исходного файла также останется прежним, поэтому зависящие от него задачи компиляции не будут запускаться без необходимости.

Проверка границ кэширования и синхронизации

До восстановления кэша необходимо определить, какие каталоги можно повторно использовать между задачами. Артефакты сборки, кэш модулей и кэши менеджеров пакетов имеют разные жизненные циклы, поэтому их не следует объединять в один большой непрозрачный архив. Манифест кэша должен как минимум содержать ключ кэша, версию Xcode, архитектуру, команду генерации и количество файлов. После восстановления сначала выполняйте предварительную проверку временных меток и только затем переходите к основному этапу сборки.

Для инструментов синхронизации также следует явно определить, нужно ли сохранять время изменения файлов. Сохранение времени полезно для инкрементального анализа, но при ошибочных часах на исходной машине некорректные значения распространяются без изменений. Если время не сохранять, все файлы могут выглядеть как только что изменённые. Важно не выбрать какой-то один параметр сам по себе, а применять в команде единую проверенную стратегию и зафиксировать её в скриптах задач.

Если код передаётся в виде архива, сначала распакуйте его в изолированный каталог, проверьте файлы с будущим временем, количество файлов и состояние Git, а затем атомарно переключитесь на подготовленный рабочий каталог. Не распаковывайте архив непосредственно поверх каталога, в котором сохранились старые промежуточные артефакты.

Добавьте проверку временной базы в начало задачи

В итоговой конфигурации проверка должна выполняться до разрешения зависимостей и запуска сборки Xcode. Если обнаружены файлы с будущим временем, немедленно завершайте задачу с ошибкой и выводите ограниченное число путей. Не исправляйте время автоматически с последующим продолжением сборки: такое исправление уничтожит данные, необходимые для диагностики.

Для каждой задачи рекомендуется сохранять:

  1. вывод date и systemsetup -gettimezone;
  2. текущий коммит, путь к Xcode и сведения об SDK;
  3. число файлов с будущим временем и несколько первых путей;
  4. ключ кэша и результат восстановления;
  5. различия между задачами первой и второй сборок.

После исправления трижды подряд выполните задачу для одного и того же коммита: первый запуск создаёт кэш, второй проверяет инкрементальную сборку, а третий подтверждает, что результат не был случайным. Если во втором и третьем запусках снова выполняется одна и та же задача, продолжайте анализировать её входные файлы вместо очередной полной очистки. Цель управления временными метками состоит не в том, чтобы назначить всем файлам одинаковое время, а в том, чтобы обеспечить объяснимую и проверяемую последовательность между исходным кодом, сгенерированными файлами и кэшем.

Часто задаваемые вопросы

Нужно ли выполнять touch для всего репозитория после обнаружения файла с будущей датой?

Нет. Это изменит время всех файлов и обычно вызовет полную перекомпиляцию. Исправьте только затронутые файлы либо заново получите соответствующий каталог.

Почему чистая сборка стабильна, а инкрементальная постепенно замедляется?

Инкрементальная сборка сравнивает существующие результаты и граф зависимостей. Будущая дата файла или кэш с неверными метаданными может делать этот граф устаревшим при каждом запуске.

Выделенная среда разработки

Настройте облачный Mac для следующей задачи разработки

Выберите Oak M4, Oak M4 Plus или Oak M4 Pro и настройте тариф аренды на одном из пяти физических узлов с учетом расположения команды.

Настроить облачный Mac