В два часа ночи 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
У каждого проекта свой каталог {имя}-{хеш}/:
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
Шаг 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/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: уйти от дисковых проблем
Всё выше — смягчение при ограниченном диске. При оценке CI стоит рассмотреть выделенный облачный Mac Mini вместо локальных узлов.
В облаке другие рычаги:
- Расширенный SSD: M4 Mac Mini Macstripe с большим диском — проще бесконечной чистки
- Периодический сброс: месячный узел заново — DerivedData с нуля, без скриптов
- Масштаб по спросу: перед релизом больше узлов, в затишье меньше
- SSH-отладка: при странных сборках
ssh runner@your-nodeудобнее удалённого железа
Кейс: мобильная команда заменила три локальных Mac Mini облаком Macstripe — ~8–12 ч/мес меньше ops (диск, Xcode, сбои раннера). По ставке инженера это покрывает большую часть аренды.
Для 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: масштаб без обслуживания диска.