10 октября 2026 г. · 12 мин. чтения
Как очистить Xcode DerivedData и почему она занимает десятки гигабайт на Mac
Когда свободное место на Mac заканчивается, рано или поздно взгляд падает на директорию Xcode. У iOS-разработчиков там часто лежат десятки гигабайт DerivedData: кэши, индексы, продукты сборок, зависимости Swift Package Manager.
Удалить всё одной командой просто. Но потом Xcode долго собирает заново и восстанавливает индекс, а первая сборка после очистки становится медленнее обычной. Поэтому полезно сначала понять, что именно разрослось, а уже потом решать, как это чистить.
Короткий ответ: для большинства случаев лучше взять готовое бесплатное решение, например DevCleaner for Xcode из Mac App Store или одну из open-source утилит ниже. Пара кликов, никакого Terminal, типично находит 10–50 GB за один проход. Но если хочется понимать, что именно удаляется, или встроить проверку размера в собственный workflow и CI, можно собрать и свой маленький zsh-скрипт.
Разбираем всё по порядку: что внутри DerivedData, как измерить, какие есть способы очистки, и в конце отдельно мой xcode-dd как пример своего скрипта.
Для чего Xcode нужна DerivedData
DerivedData это рабочая директория Xcode. В ней сохраняются продукты и промежуточные результаты сборок, индекс исходного кода, данные Swift Package Manager, логи и кэши. Благодаря им Xcode не приходится каждый раз выполнять всю работу с нуля.
По умолчанию путь такой:
~/Library/Developer/Xcode/DerivedData
Его можно проверить или изменить в Xcode → Settings → Locations → Derived Data.
После изменения одного Swift-файла build system может переиспользовать результаты предыдущей сборки и перекомпилировать только затронутые части проекта. Индексатор тоже не начинает анализировать все исходники при каждом переходе к определению. За эту экономию времени приходится платить дисковым пространством.
DerivedData обычно можно удалить без потери исходников: Xcode создаст необходимые файлы снова. Но удаление кэша не ускоряет работу само по себе. Первая сборка после очистки, наоборот, часто становится медленнее.
Заглянем внутрь DerivedData
В корне DerivedData можно встретить каталоги проектов и общие кэши:
DerivedData/
├── MyApp-hglrthxzzosvzfecqgfkqfqcjpvj/
├── AnotherApp-fgqhqozcpoxefycwqgslpklsgkzi/
├── CompilationCache.noindex/
├── ModuleCache.noindex/
└── SDKStatCaches.noindex/
Внутри каталога конкретного проекта обычно примерно так:
MyApp-hglrthxzzosvzfecqgfkqfqcjpvj/
├── Build/
│ ├── Intermediates.noindex/
│ └── Products/
├── Index.noindex/
├── SourcePackages/
├── Logs/
└── info.plist
Build: промежуточные файлы и результаты сборок. В Intermediates.noindex данные разных этапов компиляции и линковки, в Products собранные продукты для соответствующих конфигураций и платформ.
Index.noindex: индекс для навигации по исходникам, поиска определений и использований символов. После удаления IDE придётся индексировать проект повторно.
SourcePackages: данные Swift Package Manager, checkout зависимостей и связанные артефакты. На проектах с большим количеством пакетов эта директория часто крупнее Build.
Logs: данные сборок, тестов и других операций.
info.plist помогает понять, какому проекту принадлежит каталог. Там может находиться запись:
"WorkspacePath" => "/Users/eugene/Test-ios/Test/Test.xcodeproj"
Её удобно читать через PlistBuddy:
/usr/libexec/PlistBuddy \
-c "Print :WorkspacePath" \
~/Library/Developer/Xcode/DerivedData/<project>/info.plist
Это удобнее, чем пытаться опознать проект по длинному имени папки. Особенно когда среди них остались данные от старых checkout.
CompilationCache.noindex, ModuleCache.noindex и SDKStatCaches.noindex на верхнем уровне не привязаны к одному проекту. Поэтому удаление project-specific каталога не обязательно освободит большую часть места.
Откуда берутся десятки гигабайт
Xcode хранит результаты сборок для разных конфигураций и destinations. Swift Package Manager держит данные зависимостей, индексатор информацию об исходниках. По мере работы эти файлы накапливаются. Удаление проекта или временного Git worktree тоже не гарантирует немедленного удаления его DerivedData.
Для примера возьмём условную DerivedData размером около 15.7 GB:
10.5 GB Test-civxctdjvxfxvzewaauzvbgmsmgg
3.5 GB CompilationCache.noindex
1.6 GB ModuleCache.noindex
11.0 MB SDKStatCaches.noindex
Внутри самого большого проекта распределение может быть таким:
SourcePackages 7.6 GB
Build 2.7 GB
Index.noindex 135.0 MB
Logs 2.5 MB
Значения округлены и приведены для иллюстрации. Главное здесь другое: больше места занимают зависимости и общие кэши, а не только продукты сборки. Поэтому полезнее сначала посмотреть, что лежит внутри.
Как узнать, что занимает место
Общий размер:
du -sh ~/Library/Developer/Xcode/DerivedData
Распределение по каталогам верхнего уровня:
du -sh ~/Library/Developer/Xcode/DerivedData/* 2>/dev/null | sort -hr
А для отдельного проекта:
du -hd 1 \
~/Library/Developer/Xcode/DerivedData/<project> \
2>/dev/null | sort -hr
Это уже позволяет понять, кто занимает диск: конкретный проект, SourcePackages или общие кэши. Если путь к DerivedData менялся в настройках Xcode, в командах нужно использовать фактическое расположение.
Способы очистки DerivedData
Теперь к удалению. Способов несколько, у каждого свои плюсы.
1. Через Xcode Settings и Finder
Закрыть Xcode, открыть DerivedData через Xcode → Settings → Locations (там рядом с путём есть маленькая стрелка, открывающая каталог в Finder) и удалить нужные папки вручную. Если проблема касается одного проекта, можно ограничиться его каталогом.
Это самый безопасный способ: виден размер каждой папки, можно удалить только лишнее.
2. Через Terminal
Содержимое стандартной директории удаляют так:
rm -rf ~/Library/Developer/Xcode/DerivedData/*
Обратите внимание: * не захватывает скрытые элементы. Кроме того, rm -rf удаляет файлы без корзины и подтверждения. Перед запуском проверьте путь.
Если проблема связана именно с build artifacts, начните с Product → Clean Build Folder прямо в Xcode. Это не равнозначно полному удалению DerivedData: индекс, состояние пакетов и общие кэши она целиком не сбрасывает, зато часто этого достаточно для сбоев в сборке.
Не всё лежит в DerivedData. Архивы приложений хранятся отдельно, в ~/Library/Developer/Xcode/Archives. Данные симуляторов находятся в ~/Library/Developer/CoreSimulator. Если диск занят ими, очистка DerivedData почти ничего не изменит.
3. Xcode 27: Product → Delete Derived Data
В Xcode 27 наконец-то появилась команда Product → Delete Derived Data (подробнее). Она удаляет DerivedData текущего проекта прямо из IDE, не нужно искать нужный каталог вручную.
Удобный вариант, когда нужно сбросить состояние одного проекта. Но превращать очистку в обязательный ритуал перед каждой сборкой не стоит: сохранённые данные обычно как раз и нужны, чтобы собирать быстрее. Если одна и та же ошибка регулярно исчезает только после удаления DerivedData, лучше искать причину.
4. Готовые утилиты (все бесплатные)
Если не хочется поддерживать собственный скрипт или возиться с Terminal, есть готовые решения. Все перечисленные ниже бесплатные: freeware из Mac App Store или open source на GitHub.
DevCleaner for Xcode: пожалуй, самое ходовое решение именно под эту задачу. GUI-утилита в Mac App Store, разбирает весь ~/Library/Developer целиком: DerivedData, старые симуляторы, iOS Device Support, documentation caches. Freeware с tip jar, типично находит 10–50 GB за один проход. Для большинства пользователей это и есть «супер готовое» решение, скрипт не нужен.
XcodeClean: open-source утилита для menu bar, собирает распространённые действия Xcode, включая очистку DerivedData и package caches. Мне в ней больше всего понравились именно menu-bar подход и простой интерфейс: кнопка очистки на расстоянии одного клика, без лишних экранов.
mac-dev-clean: open-source CLI для более широкой уборки developer storage. Имеет смысл, если место занимают не только данные Xcode, но и кэши других инструментов.
ClearDisk: open-source графическая утилита для анализа и очистки developer caches.
Для тех, кто уже работает с coding agents: Xcode Disk Cleanup Agent Skill от Antoine van der Lee (open source). Вместо фиксированной команды можно попросить агента провести аудит, показать кандидатов на удаление и обсудить, какие данные действительно больше не нужны. Автоматический анализ всё равно не отменяет проверки путей перед удалением.
5. Свой zsh-скрипт xcode-dd
Раз есть DevCleaner, зачем вообще свой велосипед? Разумный вопрос, и короткий ответ такой: не всем нужен. Для разового избавления от 30 GB хватит GUI-утилиты. Свой скрипт имеет смысл, когда:
- хочется завести алиасы и встроить проверку размера в собственный workflow (
git status-подобная привычка), - нужен запуск из CI, pre-commit хуков или сценариев coding agent без GUI,
- важны кастомные правила защищённых путей, фильтры по проектам или свой формат вывода,
- всё необходимое уже есть в zsh и
du, лишнее приложение ставить не хочется.
Если это про вас, дальше разбираем такой скрипт.
xcode-dd: один вызов вместо нескольких команд
У xcode-dd три режима:
| Команда | Результат |
|---|---|
xcode-dd или xcode-dd status | Общий размер, свободное место, крупнейшие каталоги |
xcode-dd details | Путь к workspace и размеры Build, Index.noindex, SourcePackages, Logs |
xcode-dd clean | Очистка содержимого DerivedData после подтверждения |
Пример xcode-dd:
Xcode DerivedData
Path: /Users/eugene/Library/Developer/Xcode/DerivedData
Total: 15.7 GB
Disk: 460.4 GB
Free: 99.2 GB
Of disk: 3.4%
Of free: 15.8%
Largest:
10.5 GB Test-civxctdjvxfxvzewaauzvbgmsmgg
3.5 GB CompilationCache.noindex
1.6 GB ModuleCache.noindex
11.0 MB SDKStatCaches.noindex
Команда xcode-dd details добавляет сведения о конкретном проекте:
Test-civxctdjvxfxvzewaauzvbgmsmgg 10.5 GB
Workspace /Users/eugene/Test-ios/Test/Test.xcodeproj
Build 2.7 GB
Index.noindex 135.0 MB
SourcePackages 7.6 GB
Logs 2.5 MB
Ядро в одном сниппете
Основа скрипта простая: du считает размеры, sort и awk сортируют и форматируют. Вот минимальная версия status, которая делает именно это:
#!/bin/zsh
DD="$HOME/Library/Developer/Xcode/DerivedData"
echo "Total:"
du -sh "$DD" 2>/dev/null | awk '{print $1}'
echo
echo "Largest:"
du -sk "$DD"/* 2>/dev/null \
| sort -rn \
| head -10 \
| awk '{ kb=$1; $1=""; printf "%7.1f GB %s\n", kb/1048576, substr($0, 2) }'
Этого достаточно, чтобы в одной команде увидеть, что именно разрослось.
Полная версия добавляет сверху: разбор проекта через WorkspacePath в info.plist (режим details), очистку с подтверждением и ожиданием закрытия Xcode через AppleScript (clean), проверку целевого пути против списка защищённых каталогов перед rm -rf, а также человекочитаемый вывод размеров через общий human_size.
Значение Freed в полном варианте означает разницу между измеренными размерами содержимого, а не гарантированный прирост свободного места на APFS.
Установка
Сохраните скрипт ниже в файл xcode-dd и сделайте исполняемым:
chmod +x xcode-dd
mkdir -p ~/.local/bin
mv xcode-dd ~/.local/bin/
Если ~/.local/bin ещё нет в PATH, добавьте в ~/.zshrc:
export PATH="$HOME/.local/bin:$PATH"
После открытия нового окна Terminal будут доступны команды xcode-dd, xcode-dd details и xcode-dd clean. Для другого расположения DerivedData можно задать XCODE_DD_PATH.
Полный исходный код
Перед использованием
cleanна рабочих данных попробуйте его на тестовом каталоге. Скрипт не проходил запуск на macOS в рамках подготовки этой публикации.
Развернуть полный исходный код xcode-dd
#!/bin/zsh
# xcode-dd: inspect and clean Xcode DerivedData.
# Usage: xcode-dd [status|details|clean]
# XCODE_DD_PATH overrides the default path.
set -u
DEFAULT_DD="$HOME/Library/Developer/Xcode/DerivedData"
DD="${XCODE_DD_PATH:-$DEFAULT_DD}"
FORBIDDEN_PATHS=("/" "/Users" "${HOME:A}" "${HOME:A}/Library" "${HOME:A}/Library/Developer" "${HOME:A}/Library/Developer/Xcode")
human_size() {
local kb="${1:-0}"
awk -v kb="$kb" 'BEGIN { if (kb >= 1048576) printf "%.1f GB", kb / 1048576; else if (kb >= 1024) printf "%.1f MB", kb / 1024; else printf "%.0f KB", kb }'
}
directory_size_kb() {
local target="$1"
[[ -e "$target" ]] || { echo 0; return; }
du -sk "$target" 2>/dev/null | awk -F '\t' '{print $1}'
}
scan_dd() {
SCAN_ENTRIES=()
SCAN_TOTAL_KB=0
local -a top_level
top_level=("$DD"/*(ND))
(( ${#top_level[@]} )) || return 0
local kb name
while IFS=$'\t' read -r kb name; do
[[ -n "$kb" ]] || continue
SCAN_ENTRIES+=("$kb"$'\t'"${name:t}")
(( SCAN_TOTAL_KB += kb ))
done < <(du -sk -- "${top_level[@]}" 2>/dev/null)
}
sorted_scan_entries() {
local entry
for entry in "${SCAN_ENTRIES[@]}"; do
print -r -- "$entry"
done | sort -t $'\t' -k1,1nr
}
show_status() {
[[ -d "$DD" ]] || { echo "DerivedData directory does not exist: $DD"; return 0; }
scan_dd
print_status_from_scan
}
print_status_from_scan() {
local disk_info total_kb free_kb
disk_info=$(df -k "$DD" | tail -1)
total_kb=$(echo "$disk_info" | awk '{print $2}')
free_kb=$(echo "$disk_info" | awk '{print $4}')
echo "Xcode DerivedData"
echo
printf "%-14s %s\n" "Path:" "$DD"
printf "%-14s %s\n" "Total:" "$(human_size "$SCAN_TOTAL_KB")"
printf "%-14s %s\n" "Disk:" "$(human_size "$total_kb")"
printf "%-14s %s\n" "Free:" "$(human_size "$free_kb")"
if (( total_kb > 0 )); then
awk -v dd="$SCAN_TOTAL_KB" -v total="$total_kb" 'BEGIN { printf "%-14s %.1f%%\n", "Of disk:", dd * 100 / total }'
fi
if (( free_kb > 0 )); then
awk -v dd="$SCAN_TOTAL_KB" -v free="$free_kb" 'BEGIN { printf "%-14s %.1f%%\n", "Of free:", dd * 100 / free }'
fi
echo
echo "Largest:"
(( ${#SCAN_ENTRIES[@]} )) || { echo " No DerivedData directories."; return 0; }
sorted_scan_entries | while IFS=$'\t' read -r kb name; do
printf "%12s %s\n" "$(human_size "$kb")" "$name"
done
}
workspace_path_for() {
local plist="$1/info.plist"
[[ -f "$plist" ]] || return 1
/usr/libexec/PlistBuddy -c "Print :WorkspacePath" "$plist" 2>/dev/null
}
show_details() {
[[ -d "$DD" ]] || { echo "DerivedData directory does not exist: $DD"; return 0; }
scan_dd
(( ${#SCAN_ENTRIES[@]} )) || { echo "No DerivedData directories."; return 0; }
sorted_scan_entries | while IFS=$'\t' read -r kb name; do
local dir="$DD/$name"
echo
printf "%-45s %s\n" "$name" "$(human_size "$kb")"
if [[ -d "$dir" ]]; then
local workspace=""
workspace=$(workspace_path_for "$dir")
[[ -z "$workspace" ]] || printf " %-12s %s\n" "Workspace" "$workspace"
local -a components=(Build Index.noindex SourcePackages Logs)
local component="" component_path="" component_kb=""
for component in "${components[@]}"; do
component_path="$dir/$component"
if [[ -e "$component_path" ]]; then
component_kb=$(directory_size_kb "$component_path")
printf " %-25s %s\n" "$component" "$(human_size "$component_kb")"
fi
done
fi
done
}
wait_for_xcode_to_quit() {
local timeout=30 elapsed=0
pgrep -x Xcode >/dev/null || return 0
echo
echo "Xcode is running. Asking it to quit..."
osascript -e 'tell application "Xcode" to quit' >/dev/null || return 1
while pgrep -x Xcode >/dev/null; do
sleep 1
(( elapsed++ ))
if (( elapsed >= timeout )); then
echo "Xcode did not quit after ${timeout}s. Nothing was deleted."
return 1
fi
done
echo "Xcode closed."
}
assert_safe_dd_path() {
local resolved="${DD:A}" forbidden
if [[ -z "$resolved" || "$resolved" == "/" ]]; then
echo "Refusing to clean unsafe path: ${resolved:-${DD:-<empty>}}"
return 1
fi
for forbidden in "${FORBIDDEN_PATHS[@]}"; do
if [[ "$resolved" == "$forbidden" ]]; then
echo "Refusing to clean protected path: $resolved"
return 1
fi
done
}
clean_derived_data() {
[[ -d "$DD" ]] || { echo "DerivedData directory does not exist: $DD"; return 0; }
assert_safe_dd_path || return 1
scan_dd
local before=$SCAN_TOTAL_KB
print_status_from_scan
echo
echo "This will delete the CONTENTS of:"
echo "${DD:A}"
echo
printf "Potential cleanup: %s\n" "$(human_size "$before")"
echo
local answer=""
read "answer?Delete all DerivedData? [y/N] "
[[ "$answer" =~ ^[Yy]$ ]] || { echo "Cancelled."; return 0; }
wait_for_xcode_to_quit || return 1
assert_safe_dd_path || return 1
echo
echo "Deleting DerivedData..."
rm -rf -- "$DD"/*(ND) || return 1
local after freed
after=$(directory_size_kb "$DD")
freed=$(( before - after ))
(( freed < 0 )) && freed=0
echo
printf "%-10s %s\n" "Before:" "$(human_size "$before")"
printf "%-10s %s\n" "After:" "$(human_size "$after")"
printf "%-10s %s\n" "Freed:" "$(human_size "$freed")"
}
case "${1:-status}" in
status) show_status ;;
details) show_details ;;
clean) clean_derived_data ;;
*) echo "Usage: xcode-dd [status|details|clean]"; exit 1 ;;
esac
Отдельная история: worktrees и CI
Coding agents часто работают в отдельных Git worktrees. Например, рядом с основным checkout может существовать временный:
/Users/eugene/Test-ios
/Users/eugene/.codex/worktrees/5198/Test-ios
Если такой checkout открывали или собирали в Xcode, его рабочие данные могут сохраниться и после удаления worktree. При этом не каждый worktree обязательно создаёт отдельную DerivedData. Чтобы понять, какой каталог к чему относится, полезно смотреть WorkspacePath.
В CI расположение DerivedData можно задать явно:
xcodebuild \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-derivedDataPath /tmp/MyApp-DerivedData \
build
Это позволяет изолировать состояние сборки конкретной job. Удалять его после каждого запуска или сохранять для ускорения следующих сборок уже вопрос стратегии кэширования.
Вместо заключения
DerivedData занимает место не потому, что Xcode обязательно работает неправильно. Большая часть этих данных помогает компилятору, индексатору и Swift Package Manager не повторять уже выполненную работу.
Поэтому сначала стоит выяснить, что именно выросло: Build, SourcePackages, индекс или общие кэши. Для разовой проверки хватает du, для одного проекта удобна Product → Delete Derived Data в Xcode 27, для привычного обзора готовые утилиты или свой xcode-dd. Полную очистку лучше оставлять для случаев, когда она действительно нужна.