eugenemind.com
← กลับไปที่บล็อก

10 ตุลาคม 2569 · 22 นาทีในการอ่าน

วิธีล้าง Xcode DerivedData และทำไมมันกินพื้นที่หลายสิบกิกะไบต์บน Mac

#ios #xcode #macos #deriveddata #zsh

เมื่อพื้นที่ว่างบน Mac เริ่มหมด ไม่ช้าก็เร็วสายตาของเราก็จะจับไปที่โฟลเดอร์ของ Xcode สำหรับนักพัฒนา iOS โฟลเดอร์นั้นมักเก็บ DerivedData ขนาดหลายสิบกิกะไบต์ ทั้งแคช ดัชนีซอร์สโค้ด ผลลัพธ์การบิลด์ และ dependency ของ Swift Package Manager

การลบทั้งหมดด้วยคำสั่งเดียวทำได้ง่าย แต่หลังจากนั้น Xcode ต้องคอมไพล์ dependency ใหม่ สร้างดัชนีใหม่ และสร้างแคชขึ้นมาอีกครั้ง ทำให้การบิลด์รอบแรกช้ากว่าปกติอย่างเห็นได้ชัด ดังนั้นก่อนตัดสินใจลบ ควรทำความเข้าใจก่อนว่าอะไรที่โตขึ้นจริง ๆ

คำตอบสั้น ๆ ในกรณีส่วนใหญ่ ควรใช้เครื่องมือฟรีที่ออกแบบมาเฉพาะทาง เช่น DevCleaner for Xcode จาก Mac App Store หรือหนึ่งในยูทิลิตี open source ที่แนะนำไว้ด้านล่าง คลิกไม่กี่ครั้ง ไม่ต้องเปิดเทอร์มินัล โดยปกติเรียกคืนพื้นที่ได้ 10–50 GB ต่อการสแกนหนึ่งครั้ง แต่ถ้าอยากเข้าใจว่าอะไรถูกลบไปจริง ๆ หรืออยากฝังการตรวจสอบขนาดลงในขั้นตอนการทำงานและ CI ของตัวเอง การเขียนสคริปต์ zsh เล็ก ๆ ของตัวเองก็เป็นทางเลือกที่สมเหตุสมผล

บทความนี้จะพาไปทีละขั้นตอน ตั้งแต่สิ่งที่อยู่ภายใน DerivedData วิธีวัดการใช้งาน ตัวเลือกการล้างทั้งหมด และตอนท้ายคือส่วนเฉพาะสำหรับ xcode-dd ของผม ในฐานะตัวอย่างของแนวทางเขียนเอง

DerivedData มีไว้ทำอะไร

DerivedData คือโฟลเดอร์ที่ Xcode ใช้เก็บข้อมูลระหว่างการพัฒนา เช่น ผลลัพธ์การบิลด์ ไฟล์ชั่วคราวจากการคอมไพล์ ดัชนีซอร์สโค้ด ข้อมูลของ Swift Package Manager รวมถึงล็อกและแคชต่าง ๆ

ตำแหน่งเริ่มต้นคือ:

~/Library/Developer/Xcode/DerivedData

สามารถตรวจสอบหรือเปลี่ยนตำแหน่งได้ที่ Xcode → Settings → Locations → Derived Data

สมมติว่าเราแก้ Swift เพียงไฟล์เดียว ระบบบิลด์ก็อาจนำผลลัพธ์เดิมกลับมาใช้ แล้วคอมไพล์ใหม่เฉพาะส่วนที่ได้รับผลกระทบ ไม่จำเป็นต้องเริ่มทั้งหมดตั้งแต่ต้น ส่วนดัชนีโค้ดก็ช่วยให้การค้นหาสัญลักษณ์และการกระโดดไปยังจุดประกาศทำงานได้รวดเร็ว

ดังนั้น DerivedData ไม่ใช่โฟลเดอร์ขยะ แต่เป็นข้อมูลที่ช่วยประหยัดเวลาในการทำงาน เราสามารถลบได้โดยไม่ทำให้ซอร์สโค้ดหาย แต่ Xcode จะต้องสร้างข้อมูลที่จำเป็นขึ้นมาใหม่

ข้างใน DerivedData มีอะไรบ้าง

เมื่อเปิดโฟลเดอร์ จะเห็นโครงสร้างคล้ายตัวอย่างนี้:

DerivedData/
├── MyApp-hglrthxzzosvzfecqgfkqfqcjpvj/
├── AnotherApp-fgqhqozcpoxefycwqgslpklsgkzi/
├── CompilationCache.noindex/
├── ModuleCache.noindex/
└── SDKStatCaches.noindex/

Xcode จะสร้างโฟลเดอร์แยกตามโปรเจกต์ ภายในแต่ละโฟลเดอร์มักมีข้อมูลประมาณนี้:

MyApp-hglrthxzzosvzfecqgfkqfqcjpvj/
├── Build/
│   ├── Intermediates.noindex/
│   └── Products/
├── Index.noindex/
├── SourcePackages/
├── Logs/
└── info.plist

Build เก็บไฟล์ระหว่างการคอมไพล์และผลลัพธ์จากการบิลด์

Index.noindex ใช้สำหรับการค้นหาและนำทางในซอร์สโค้ด

SourcePackages เกี่ยวข้องกับ Swift Package Manager ทั้งไฟล์ dependency ที่ดาวน์โหลดมาและข้อมูลอื่น ๆ ของแพ็กเกจ ในบางโปรเจกต์โฟลเดอร์นี้อาจใหญ่กว่าผลลัพธ์การบิลด์เสียอีก

Logs เก็บข้อมูลจากการบิลด์และการทดสอบ

ไฟล์ info.plist เป็นไฟล์ที่น่าสนใจ เพราะคีย์ WorkspacePath ช่วยบอกได้ว่า DerivedData โฟลเดอร์นั้นเป็นของโปรเจกต์ใด เราอ่านค่าได้ด้วยคำสั่ง:

/usr/libexec/PlistBuddy -c 'Print :WorkspacePath' \
  ~/Library/Developer/Xcode/DerivedData/<project>/info.plist

ส่วน CompilationCache.noindex, ModuleCache.noindex และ SDKStatCaches.noindex ที่อยู่ระดับบนสุดเป็นแคชที่ใช้ร่วมกัน ไม่ได้เป็นของโปรเจกต์ใดโปรเจกต์หนึ่งโดยเฉพาะ ดังนั้นลบโฟลเดอร์ของโปรเจกต์แล้ว พื้นที่ส่วนนี้อาจยังคงอยู่

ทำไมถึงกินพื้นที่หลายสิบกิกะไบต์

ยิ่งเราบิลด์หลาย configuration ใช้หลายอุปกรณ์หรือ simulator และมี dependency มากเท่าไร ข้อมูลที่สะสมไว้ก็ยิ่งมากขึ้นเท่านั้น ที่สำคัญ การลบซอร์สโค้ดของโปรเจกต์ไม่ได้หมายความว่า Xcode จะลบ 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

จะเห็นว่า SourcePackages ใช้พื้นที่มากกว่า Build อย่างชัดเจน ตัวเลขเหล่านี้เป็นเพียงตัวอย่างที่ปัดเศษแล้ว ไม่ใช่ผลจากการรันสคริปต์จริงในครั้งล่าสุด

ตรวจสอบว่าอะไรใช้พื้นที่มากที่สุด

เริ่มจากดูขนาดรวมของ DerivedData:

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

ถ้าเปลี่ยนตำแหน่ง DerivedData ในการตั้งค่า Xcode ก็ต้องใช้พาธนั้นแทน

วิธีล้าง DerivedData

มีหลายวิธีให้เลือก แต่ละวิธีมีข้อดีข้อเสียต่างกัน

1. Xcode Settings และ Finder

ปิด Xcode เปิดโฟลเดอร์ DerivedData ผ่าน Xcode → Settings → Locations (ข้างพาธมีลูกศรเล็ก ๆ กดเพื่อเปิดโฟลเดอร์ใน Finder) แล้วลบเฉพาะโฟลเดอร์โปรเจกต์ที่ไม่จำเป็น

วิธีนี้ปลอดภัยที่สุด เพราะเห็นขนาดของแต่ละโฟลเดอร์ และลบเฉพาะที่ต้องการได้

2. เทอร์มินัล

จากเทอร์มินัล คำสั่งที่พบบ่อยคือ:

rm -rf ~/Library/Developer/Xcode/DerivedData/*

ต้องระวังว่า rm -rf ลบไฟล์ทันทีโดยไม่ย้ายไปถังขยะ และ * จะไม่ครอบคลุมไฟล์ที่ซ่อนอยู่ ควรตรวจสอบพาธให้ถูกต้องทุกครั้ง

ถ้าปัญหาเกี่ยวข้องกับผลลัพธ์การบิลด์ ลองใช้ Product → Clean Build Folder ก่อน คำสั่งนี้ไม่ได้ลบข้อมูลทั้งหมดใน DerivedData ไม่ได้รีเซ็ตดัชนีและแคชที่ใช้ร่วมกันทั้งหมด แต่หลาย ๆ ครั้งก็เพียงพอสำหรับแก้ข้อผิดพลาดในการบิลด์

ข้อมูลของ Xcode ไม่ได้อยู่ใน DerivedData ทั้งหมด ไฟล์ archive อยู่ที่ ~/Library/Developer/Xcode/Archives ส่วนข้อมูล simulator อยู่ที่ ~/Library/Developer/CoreSimulator ถ้าสองโฟลเดอร์นี้คือตัวที่กินพื้นที่ การล้าง DerivedData จะไม่ช่วยอะไรมาก

3. Xcode 27: Product → Delete Derived Data

Xcode 27 เพิ่มคำสั่ง Product → Delete Derived Data เข้ามาในที่สุด (รายละเอียด) ซึ่งช่วยลบ DerivedData ของโปรเจกต์ปัจจุบันจากใน IDE โดยตรง ไม่ต้องตามหาโฟลเดอร์เอง

เป็นตัวเลือกที่สะดวกเมื่อต้องการรีเซ็ตสถานะของโปรเจกต์หนึ่งโปรเจกต์ แต่ไม่จำเป็นต้องลบแคชก่อนบิลด์ทุกครั้ง เพราะข้อมูลเหล่านี้คือสิ่งที่ทำให้การบิลด์ครั้งถัดไปเร็ว ถ้าข้อผิดพลาดเดิมหายไปเฉพาะหลังจากลบ DerivedData ทุกครั้ง ควรหาสาเหตุที่แท้จริง

4. เครื่องมือฟรีจากผู้พัฒนาอื่น

ถ้าไม่อยากเขียนสคริปต์เองหรือไม่สะดวกใช้เทอร์มินัล มีเครื่องมือพร้อมใช้งาน รายการด้านล่างทั้งหมดฟรี: เป็น freeware จาก Mac App Store หรือ open source บน GitHub

DevCleaner for Xcode: น่าจะเป็นโซลูชันเฉพาะทางที่ได้รับความนิยมมากที่สุด เป็นแอป GUI ใน Mac App Store สแกน ~/Library/Developer ทั้งโฟลเดอร์ รวมถึง DerivedData, simulator เก่า, iOS Device Support, แคชเอกสาร เป็น freeware พร้อม tip jar โดยปกติเรียกคืนพื้นที่ได้ 10–50 GB ต่อการสแกนหนึ่งครั้ง สำหรับผู้ใช้ส่วนใหญ่ นี่คือคำตอบแบบ “พร้อมใช้” ไม่จำเป็นต้องใช้สคริปต์

XcodeClean: ยูทิลิตีแบบ open source บนแถบเมนู รวมคำสั่งทำความสะอาด Xcode ที่ใช้บ่อย รวมถึงการล้าง DerivedData และแคชของแพ็กเกจ สิ่งที่ผมชอบมากที่สุดคือแนวทางบนแถบเมนูและอินเทอร์เฟซที่เรียบง่าย ปุ่มล้างข้อมูลอยู่ห่างออกไปแค่คลิกเดียว ไม่มีหน้าจออื่นให้ยุ่งยาก

mac-dev-clean: CLI แบบ open source สำหรับทำความสะอาดเครื่องมือพัฒนาในวงกว้าง เหมาะเมื่อพื้นที่ถูกใช้ไม่เฉพาะโดย Xcode แต่โดยแคชของเครื่องมืออื่น ๆ ด้วย

ClearDisk: GUI แบบ open source สำหรับวิเคราะห์และล้างแคชของเครื่องมือพัฒนา

สำหรับคนที่ใช้ coding agents อยู่แล้ว Xcode Disk Cleanup Agent Skill ของ Antoine van der Lee (open source) แทนที่จะเป็นคำสั่งตายตัว เราสามารถให้ agent ตรวจสอบพื้นที่ เสนอรายการที่ควรลบ และคุยกันว่าข้อมูลไหนไม่จำเป็นแล้วจริง ๆ แต่การวิเคราะห์อัตโนมัติก็ไม่ได้ลดความสำคัญของการตรวจสอบพาธด้วยตนเองก่อนลบ

5. สคริปต์ zsh ของตัวเอง xcode-dd

ถ้ามี DevCleaner อยู่แล้ว ทำไมต้องเขียนสคริปต์เอง คำถามดี คำตอบคือสำหรับคนส่วนใหญ่ไม่จำเป็น ถ้าแค่อยากได้พื้นที่คืน 30 GB ครั้งเดียว แอป GUI ก็เพียงพอ สคริปต์ของตัวเองมีประโยชน์เมื่อ:

  • อยากตรวจสอบขนาดเป็นส่วนหนึ่งของขั้นตอนการทำงาน (เป็นนิสัยแบบ git status ที่ตั้งเป็นนามแฝงไว้ใน shell),
  • ต้องการเรียกใช้จาก CI, pre-commit hook หรือสถานการณ์ 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 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"

เปิดเทอร์มินัลใหม่ แล้วใช้ 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

Git worktrees และ CI

ปัจจุบัน coding agents มักสร้าง Git worktree แยกสำหรับแต่ละงาน เช่น:

/Users/eugene/Test-ios
/Users/eugene/.codex/worktrees/5198/Test-ios

หากเคยเปิดหรือบิลด์ checkout เหล่านี้ใน Xcode ข้อมูล DerivedData อาจยังอยู่แม้ลบ worktree ไปแล้ว แต่ไม่ได้หมายความว่าทุก worktree จะมี DerivedData แยกกันเสมอไป เราสามารถใช้ WorkspacePath ช่วยตรวจสอบความเชื่อมโยงได้

ในระบบ CI สามารถกำหนดตำแหน่ง DerivedData ได้โดยตรง:

xcodebuild \
  -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -derivedDataPath /tmp/MyApp-DerivedData \
  build

จะลบโฟลเดอร์นี้หลังจบงานหรือเก็บไว้เป็นแคชสำหรับบิลด์ครั้งต่อไป ก็ขึ้นอยู่กับความต้องการด้านความเร็วและการแยกสภาพแวดล้อมของแต่ละโปรเจกต์

สรุป

DerivedData ใช้พื้นที่มากเพราะ Xcode เก็บผลลัพธ์ต่าง ๆ ไว้เพื่อลดงานที่ต้องทำซ้ำ ก่อนลบทั้งหมด ควรตรวจสอบก่อนว่าพื้นที่ส่วนใหญ่ไปอยู่ที่ Build, SourcePackages, ดัชนี หรือแคชส่วนกลาง สำหรับการตรวจสอบครั้งเดียว du ก็เพียงพอ สำหรับโปรเจกต์เดียว Product → Delete Derived Data ใน Xcode 27 คือทางสั้นที่สุด สำหรับการใช้งานประจำ เครื่องมือฟรีจากผู้พัฒนาอื่นหรือ xcode-dd ของตัวเองก็เป็นทางเลือกที่ดี ส่วนการล้างทั้งหมดเก็บไว้ใช้เฉพาะกรณีที่จำเป็นจริง ๆ