Разработчик на Mac в терминале анализирует занятость диска Xcode DerivedData

В два часа ночи CI шлёт тревогу: на узле сборки осталось меньше 2 ГБ, xcodebuild завершается с ошибкой. В терминале df -h показывает: SSD на 256 ГБ заполнена — ~/Library/Developer занимает 180 ГБ, из них более 90 ГБ — каталог DerivedData.

Это не редкость. На M4 Mac Mini без очистки восемь месяцев DerivedData накапливает в среднем 40–100 ГБ; с образами симулятора и символами устройств суммарно 150+ ГБ в ~/Library/Developer — обычное дело. Статья проведёт по цепочке причина → быстрый поиск → выборочная очистка → защита CI → автоматизация — готовое руководство к действию.

После прочтения у вас будет: назначение и безопасность подкаталогов DerivedData, одна команда для поиска главного «пожирателя» диска, скрипты для CI и локальной машины и схема автоматизации без повторных алертов.

Quick Answer: три частых вопроса

Вопрос Прямой ответ Риск
Можно ли просто удалить DerivedData? rm -rf ~/Library/Developer/Xcode/DerivedData/* — полностью безопасно Следующая сборка полная, +2–5 мин.
Можно ли удалить Archives? Версии без активных пользователей — да; текущие — сохранить dSYM Старые краши не символизируются
Диск CI почти полон — быстрая помощь? DerivedData → unavailable-симуляторы → старый DeviceSupport по порядку Переиндексация, ~3 мин. медленнее

Что хранится в DerivedData

Многие знают: «очистить DerivedData — и магия исчезает», но не знают содержимого. Итог: удаляют лишнее или копят мусор до переполнения диска. Главные пути в ~/Library/Developer:

Путь Содержимое Типичный размер Безопасно удалить?
Xcode/DerivedData Артефакты сборки, индекс, логи, кэш Swift-модулей 5–60 ГБ ✅ Полностью безопасно, пересоздаётся при сборке
Xcode/Archives Релизные сборки (.xcarchive) с dSYM 2–30 ГБ ⚠️ Старые — можно; активные — сохранить dSYM
Xcode/iOS DeviceSupport Символы отладки при подключении устройства 1–3 ГБ / версия iOS ✅ Старые версии — да, скачаются снова
CoreSimulator/Profiles/Runtimes Образы runtime симулятора 5–10 ГБ / версия iOS ⚠️ Ненужные версии можно удалить
CoreSimulator/Devices Данные экземпляров симулятора 1–15 ГБ ✅ Статус unavailable — безопасно
Caches/org.swift.swiftpm Кэш пакетов SPM 1–8 ГБ ✅ Удаляется, resolve восстановит
Caches/com.apple.dt.Xcode Внутренний кэш Xcode (превью, IB) 0,5–3 ГБ ✅ Можно удалить
Главное: Всё в DerivedData — восстанавливаемый кэш, не связанный с исходниками, Provisioning Profile и отправками в App Store. Единственная цена — следующая сборка полная (средний проект +2–5 мин.).

Структура подкаталогов DerivedData

У каждого проекта свой каталог {имя}-{хеш}/:

MyApp-abcdefghijklmn/
├── Build/
│   ├── Products/          # 编译产物(.app, .framework, .o 文件)
│   └── Intermediates.noindex/  # 中间文件(.o, .d 依赖图)
├── Index.noindex/         # SourceKit 索引(代码跳转、自动补全)
├── Logs/                  # 构建日志(xcodebuild -resultBundlePath 也在这里)
└── info.plist             # 项目路径记录

Чаще всего растёт Build/Intermediates.noindex — средний проект на 200 Swift-файлов легко превышает 2 ГБ; с Index.noindex на проект уходит 5–10 ГБ.

Пять шагов к виновнику блокировки диска

Не чистите вслепую — 3 минуты диагностики экономят время. На 256 ГБ M4 Mac Mini этот сценарий за 2 минуты находит топ-5 каталогов.

Шаг 1: общая загрузка диска

# 查看全盘使用率
df -h /

# 快速定位前 10 大目录(整个根盘)
sudo du -sh /* 2>/dev/null | sort -rh | head -10

Шаг 2: разбивка Developer

du -sh ~/Library/Developer/Xcode/* 2>/dev/null | sort -rh
du -sh ~/Library/Developer/CoreSimulator/Profiles/Runtimes/* 2>/dev/null | sort -rh
du -sh ~/Library/Developer/Xcode/iOS\ DeviceSupport/* 2>/dev/null | sort -rh

Пример (CI-узел за 8 месяцев):

91G    /Users/ci/Library/Developer/Xcode/DerivedData
28G    /Users/ci/Library/Developer/CoreSimulator
18G    /Users/ci/Library/Developer/Xcode/iOS DeviceSupport
12G    /Users/ci/Library/Developer/Xcode/Archives
 6G    /Users/ci/Library/Developer/Xcode/Caches

Шаг 3: самые тяжёлые проекты в DerivedData

du -sh ~/Library/Developer/Xcode/DerivedData/* 2>/dev/null | sort -rh | head -10
Замеры: Машина с 15 историческими проектами: топ-3 — 18 ГБ, 12 ГБ и 9 ГБ; два проекта не открывались больше года — 30 ГБ впустую.

Шаг 4: симуляторы и статус

# 列出所有模拟器及状态
xcrun simctl list devices

# 只看"已不可用"(系统升级后遗留)
xcrun simctl list devices | grep -i "unavailable"

Шаг 5: версии DeviceSupport

ls -lh ~/Library/Developer/Xcode/iOS\ DeviceSupport/

Так выглядит накопление за годы:

drwxr-xr-x  3 dev  staff   96B  iOS 15.4 (19E241)
drwxr-xr-x  3 dev  staff   96B  iOS 16.1 (20B82)
drwxr-xr-x  3 dev  staff   96B  iOS 17.2 (21C62)
drwxr-xr-x  3 dev  staff   96B  iOS 18.3.2 (22D82)

Символы iOS 15 и 16 в 2026 почти бесполезны (минимум в App Store часто iOS 17) — можно удалять.

Стратегия по каталогам: удалять, осторожно, не трогать

Удалять сразу (нулевой риск)

# 1. 清空所有项目的编译缓存
rm -rf ~/Library/Developer/Xcode/DerivedData/*

# 2. 删除已不可用的模拟器
xcrun simctl delete unavailable

# 3. 清理 SPM 包缓存
rm -rf ~/Library/Caches/org.swift.swiftpm/*

# 4. 清理 Xcode 内部缓存
rm -rf ~/Library/Caches/com.apple.dt.Xcode/*

# 5. 清理 SwiftUI 预览缓存
rm -rf ~/Library/Developer/Xcode/UserData/Previews/*

Выборочно (старое — вон, новое — оставить)

# 删除 2 个版本以前的设备符号(保留最新两个 iOS 版本)
# 先确认当前有哪些版本
ls ~/Library/Developer/Xcode/iOS\ DeviceSupport/

# 示例:只保留 iOS 18.x,删除更早的
rm -rf ~/Library/Developer/Xcode/iOS\ DeviceSupport/iOS\ 15*
rm -rf ~/Library/Developer/Xcode/iOS\ DeviceSupport/iOS\ 16*
rm -rf ~/Library/Developer/Xcode/iOS\ DeviceSupport/iOS\ 17*

Осторожно с Archives

  • Версия снята с App Store или без активных пользователей
  • При живых пользователях: экспорт dSYM (Organizer → правый клик по Archive → Показать в Finder → .dSYM)
  • Загрузить dSYM в Firebase Crashlytics / Sentry и т. п.
  • Удалять Archive только после подтверждения загрузки

Экстренная команда (диск на исходе)

В этом порядке обычно освобождается 20–60 ГБ:

#!/bin/bash
# emergency-clean-xcode.sh
# 执行前确认:已无正在运行的 xcodebuild 进程
echo "=== Xcode 磁盘紧急清理 ==="

echo "[1/4] 清理 DerivedData..."
du -sh ~/Library/Developer/Xcode/DerivedData 2>/dev/null
rm -rf ~/Library/Developer/Xcode/DerivedData/*
echo "✓ DerivedData 已清空"

echo "[2/4] 删除不可用模拟器..."
xcrun simctl delete unavailable
echo "✓ 不可用模拟器已删除"

echo "[3/4] 清理 SPM 和 Xcode 缓存..."
rm -rf ~/Library/Caches/org.swift.swiftpm/*
rm -rf ~/Library/Caches/com.apple.dt.Xcode/*
echo "✓ 缓存已清理"

echo "[4/4] 清理预览缓存..."
rm -rf ~/Library/Developer/Xcode/UserData/Previews/*
echo "✓ 预览缓存已清理"

echo ""
echo "=== 清理完成,当前磁盘状态 ==="
df -h /
💡 Корневое решение: Локальный CI-узел — вечная борьба с диском. Macstripe даёт выделенные M4 Mac Mini в облаке с оплатой по дням/неделям/месяцам и сбросом узла после цикла — проблема DerivedData исчезает. Смотреть тарифы →

Защита CI/CD от блокировок диска

Локально можно чистить вручную; CI-раннеры без присмотра требуют системы. Пример: self-hosted macOS (GitHub Actions / GitLab CI), три подхода.

Вариант A: фиксированный DerivedDataPath + инкрементальный кэш

Направить DerivedData на контролируемый путь и делить кэш между job'ами:

# .github/workflows/ios-build.yml 片段
- name: Build
  run: |
    xcodebuild \
      -project MyApp.xcodeproj \
      -scheme MyApp \
      -configuration Release \
      -derivedDataPath /tmp/derived-data \
      -resultBundlePath /tmp/result.xcresult \
      build

- name: Cache DerivedData
  uses: actions/cache@v4
  with:
    path: /tmp/derived-data
    key: derived-data-${{ runner.os }}-${{ hashFiles('**/Package.resolved') }}-${{ hashFiles('**/*.xcodeproj/project.pbxproj') }}
    restore-keys: |
      derived-data-${{ runner.os }}-
Важно: Ключ кэша должен включать хеш Package.resolved и project.pbxproj — иначе после обновления SPM сборка упадёт на старом кэше.

Вариант B: очистка устаревших подкаталогов перед сборкой

# 在构建步骤之前:清理超过 7 天未使用的 DerivedData 子目录
- name: Clean stale DerivedData
  run: |
    find ~/Library/Developer/Xcode/DerivedData \
      -maxdepth 1 -mindepth 1 \
      -type d \
      -atime +7 \
      -exec rm -rf {} +
    echo "DerivedData 清理完成,当前大小:"
    du -sh ~/Library/Developer/Xcode/DerivedData 2>/dev/null || echo "目录为空"

Вариант C: проверка диска в GitLab CI

Pre-job в .gitlab-ci.yml — fail-fast при нехватке места:

# .gitlab-ci.yml
check_disk:
  stage: .pre
  script:
    - |
      AVAIL=$(df -k / | awk 'NR==2 {print $4}')
      THRESHOLD=$((10 * 1024 * 1024))  # 10 GB in KB
      if [ "$AVAIL" -lt "$THRESHOLD" ]; then
        echo "ERROR: 磁盘剩余不足 10 GB(当前: $((AVAIL/1024/1024)) GB),请先清理"
        exit 1
      fi
      echo "磁盘检查通过,剩余: $((AVAIL/1024/1024)) GB"
  tags:
    - macos-runner

Сравнение трёх вариантов

Вариант Сценарий Затраты Повторное использование кэша
A: путь + кэш Средние/крупные проекты, частые PR, критично время сборки Средние (настройка cache action) Высокое (между job'ами)
B: регулярная чистка старых папок Несколько проектов на узле, диска достаточно Низкие (одна find-команда) Среднее (в тот же день)
C: контроль ёмкости Везде как последняя линия обороны Минимальные (5 строк) Не влияет

Лучше комбинировать: A — скорость, B — рост, C — страховка.

Автоматизация: расписание и предупреждения

Ручная чистка ненадёжна. Скрипт + launchd переводят реакцию в профилактику.

Полный скрипт обслуживания

#!/bin/bash
# xcode-maintenance.sh
# 建议每周日凌晨 3 点执行
# 放置路径:~/scripts/xcode-maintenance.sh

LOGFILE=~/Library/Logs/xcode-maintenance.log
THRESHOLD_GB=20  # 当剩余磁盘低于此值时触发深度清理

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOGFILE"; }

log "=== Xcode 定期维护开始 ==="

# 1. 记录清理前的磁盘状态
BEFORE=$(df -h / | awk 'NR==2 {print $4}')
log "清理前磁盘可用: $BEFORE"

# 2. 删除 14 天未访问的 DerivedData 子目录
log "清理 14 天以上未使用的 DerivedData..."
find ~/Library/Developer/Xcode/DerivedData \
  -maxdepth 1 -mindepth 1 -type d -atime +14 \
  -exec rm -rf {} + 2>/dev/null
log "✓ DerivedData 老旧条目已清理"

# 3. 删除不可用模拟器
log "清理不可用模拟器..."
xcrun simctl delete unavailable 2>/dev/null
log "✓ 不可用模拟器已删除"

# 4. 清理 SPM 缓存(超过 30 天)
find ~/Library/Caches/org.swift.swiftpm -maxdepth 2 \
  -type f -atime +30 -delete 2>/dev/null
log "✓ 老旧 SPM 缓存已清理"

# 5. 检查剩余空间,若低于阈值触发深度清理
AVAIL_GB=$(df -g / | awk 'NR==2 {print $4}')
if [ "$AVAIL_GB" -lt "$THRESHOLD_GB" ]; then
  log "⚠️  磁盘剩余 ${AVAIL_GB}GB < ${THRESHOLD_GB}GB,触发深度清理"
  rm -rf ~/Library/Developer/Xcode/DerivedData/*
  rm -rf ~/Library/Caches/com.apple.dt.Xcode/*
  rm -rf ~/Library/Developer/Xcode/UserData/Previews/*
  log "✓ 深度清理完成"
fi

AFTER=$(df -h / | awk 'NR==2 {print $4}')
log "清理后磁盘可用: $AFTER"
log "=== 维护完成 ==="

Регистрация в launchd

Каждое воскресенье в 3:00:

# 1. 给脚本加执行权限
chmod +x ~/scripts/xcode-maintenance.sh

# 2. 创建 launchd plist
cat > ~/Library/LaunchAgents/com.myteam.xcode-maintenance.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.myteam.xcode-maintenance</string>
  <key>ProgramArguments</key>
  <array>
    <string>/bin/bash</string>
    <string>/Users/YOUR_USER/scripts/xcode-maintenance.sh</string>
  </array>
  <key>StartCalendarInterval</key>
  <dict>
    <key>Weekday</key><integer>0</integer>
    <key>Hour</key><integer>3</integer>
    <key>Minute</key><integer>0</integer>
  </dict>
  <key>StandardOutPath</key>
  <string>/tmp/xcode-maintenance-stdout.log</string>
  <key>StandardErrorPath</key>
  <string>/tmp/xcode-maintenance-stderr.log</string>
</dict>
</plist>
EOF

# 3. 加载
launchctl load ~/Library/LaunchAgents/com.myteam.xcode-maintenance.plist
echo "定时任务已注册"
Эффект: Команда на четырёх Mac Mini CI: три месяца без алертов диска. Еженедельно 3–5 ГБ, стабильно >60 ГБ свободно.

Облачный Mac: уйти от дисковых проблем

Всё выше — смягчение при ограниченном диске. При оценке CI стоит рассмотреть выделенный облачный Mac Mini вместо локальных узлов.

В облаке другие рычаги:

  • Расширенный SSD: M4 Mac Mini Macstripe с большим диском — проще бесконечной чистки
  • Периодический сброс: месячный узел заново — DerivedData с нуля, без скриптов
  • Масштаб по спросу: перед релизом больше узлов, в затишье меньше
  • SSH-отладка: при странных сборках ssh runner@your-node удобнее удалённого железа

Кейс: мобильная команда заменила три локальных Mac Mini облаком Macstripe — ~8–12 ч/мес меньше ops (диск, Xcode, сбои раннера). По ставке инженера это покрывает большую часть аренды.

Macstripe облачный Mac: Выделенные физические M4 Mac Mini, пять регионов (Сингапур / Япония / Корея / Гонконг / запад США), SSH + VNC, ~5 мин. до доступа, посуточно без долгого контракта. Тарифы и цены →

Для GitHub Actions см. также iOS-разработка с Windows: Mac правда больше не нужен? — полное сравнение платформ.

FAQ

В: Удаление DerivedData затронет исходники или релизы?

Нет. Только артефакты сборки, индекс SourceKit и логи — отдельно от кода, Git, Provisioning Profile и App Store Connect. Xcode пересоберёт; цена — полная сборка, +2–5 мин. на среднем проекте.

В: Archives можно удалять свободно?

Не свободно. .xcarchive содержит dSYM для крашей. Проверьте: версия без пользователей или dSYM в Crashlytics/Sentry. Храните последние три релиза, старше — после экспорта dSYM.

В: CI каждый раз полная пересборка — есть кэш?

Да. -derivedDataPath /tmp/derived-data и actions/cache или GitLab cache:. Ключ с Package.resolved и project.pbxproj. При многих Swift Package инкремент экономит 40–60 % времени.

В: xcodebuild падает из-за диска — быстрое восстановление?

По порядку: ① rm -rf ~/Library/Developer/Xcode/DerivedData/* (10–60 ГБ); ② xcrun simctl delete unavailable (5–20 ГБ); ③ DeviceSupport старше двух версий (5–15 ГБ). Часто хватает ①.

В: «Product > Clean Build Folder» vs удаление DerivedData из CLI?

Clean Build Folder — только открытый проект; другие проекты, симулятор и DeviceSupport не трогает. rm -rf ~/Library/Developer/Xcode/DerivedData/*все проекты. CI: CLI; локально: Clean Build Folder точнее.

В: DerivedData на CI растёт очень быстро?

Часто несколько scheme/веток создают новые подкаталоги. ① -derivedDataPath на один путь; ② find ... -atime +7 -exec rm -rf {} +; ③ больший SSD или облако Macstripe.

Итог

Блокировка диска DerivedData — почти неизбежна для iOS-разработчика, но решается системно, а не «магической» паникой.

  • Причина: DerivedData — чистый кэш; Archives с dSYM — осторожно
  • Диагностика: du -sh ~/Library/Developer/Xcode/* | sort -rh
  • Выборочно: DerivedData — удалять; DeviceSupport — старое вон; Archives — сначала dSYM
  • CI: -derivedDataPath + кэш + контроль ёмкости
  • Автоматизация: launchd + скрипт еженедельно
  • Корень: облачный Mac с SSD или сбросом — конец тревоги о диске

CI снова и снова падает из-за диска? Запустите диагностику из статьи — часто 30+ ГБ за 10 минут. Долгосрочно — выделенный облачный Mac Mini: масштаб без обслуживания диска.

Читать также