解決したいこと
macOS 上の Xcode 開発環境において、不要なシミュレータドライブ(APFS Volumes)や関連ファイルが蓄積されることでストレージを圧迫したり、起動時に不要なマウントが発生したりする問題を解決する。
現象としては、Xcodeの iOS / iPadOS / tvOS / watchOS / xrOS シミュレータランタイムのディスクイメージが、Mac のストレージを大量に消費し、特に以下のフォルダ配下にある .dmg
ファイルが数10GB 単位で数10個、つまり数100GBを占有することもある。
- /System/Library/AssetsV2/com_apple_MobileAsset_appleTVOSSimulatorRuntime
- /System/Library/AssetsV2/com_apple_MobileAsset_iOSSimulatorRuntime
- /System/Library/AssetsV2/com_apple_MobileAsset_watchOSSimulatorRuntime
- /System/Library/AssetsV2/com_apple_MobileAsset_xrOSSimulatorRuntime
対策案
- Finder から直接削除 → システム影響の懸念があるため不採用。
- Xcode → Settings → Components メニューで削除 →
.dmg
ファイルが残存するため不十分。 -
xcrun simctl runtime delete
を用いて不要ランタイムを削除 → 正規手段で安全に削除可能。
解決方法
以下のコマンドでインストール済みランタイムを一覧化した。
xcrun simctl runtime list
たとえば、こんな感じで一覧される。
== Disk Images ==
-- iOS --
iOS 18.4 (22E5216h) - EB213AC6-D0C7-494C-B960-A0888D020599 (Ready)
iOS 18.0 (22A3351) - 5D4E1222-3B4D-4FE2-B62D-A1D5000BE5A9 (Ready)
iOS 17.4 (21E213) - 10683CAC-E667-40D3-AB34-B7C508F9A3A1 (Ready)
iOS 17.0.1 (21A342) - CC2ED6CD-A5FB-41F8-A62A-ADA4C43E6C59 (Ready)
iOS 18.2 (22C150) - 256C95AE-0B0D-4B07-95CD-0380F600A568 (Ready)
iOS 12.0 (16A366) - 690226E9-ACA2-417C-A520-4AEDF888E860 (Ready)
iOS 16.4 (20E247) - CDF2D2EE-DDC8-483B-A5C7-AA1F35801FAF (Ready)
iOS 17.0 (21A5326a) - 220E6084-63BB-4D5D-867E-5DAA0EDE31BE (Ready)
iOS 17.0 (21A5291g) - 028C26F1-B385-4DBD-8D13-626C16CCFCC4 (Ready)
iOS 18.0 (22A5346a) - E80E49E1-10CE-4285-9923-F25A7E315481 (Ready)
iOS 11.4 (15F79) - D47B6F35-67DD-4B70-B8F3-0383BFFC89BA (Ready)
iOS 26.0 (23A5308g) - C7631006-779D-483A-AE06-78C007666E49 (Ready)
iOS 18.4 (22E238) - 6405A7E6-DD08-4966-B1A3-CDFA6BCD4EE3 (Ready)
iOS 17.0 (21A5277g) - A66289A0-6F84-414D-B379-0578C16D46F9 (Ready)
iOS 17.5 (21F79) - 112BCC4F-C369-4ED6-A6A9-A3E99DE608C2 (Ready)
iOS 17.2 (21C5029e) - 7E11080E-0182-4CB5-86AE-454FE5B30808 (Ready)
iOS 17.0 (21A5268h) - 6222FA97-552B-41DA-B434-3EA935B9628D (Ready)
iOS 26.0 (23A5260l) - 15C56CD2-A592-4A97-AD4C-07BCEBA2B4BD (Ready)
iOS 17.2 (21C62) - 4CEEA457-94A6-4D3A-A392-3977A3CCC11F (Ready)
iOS 17.2 (21C5046b) - 3E570035-21B4-4D98-922B-595FBD8395FD (Ready)
iOS 17.0 (21A5303d) - 202E5208-EEDA-4F82-B6D7-CE8AF007FCA3 (Ready)
-- tvOS --
tvOS 18.0 (22J356) - 01E63235-64AD-44C5-8D18-DACEC57CB582 (Ready)
tvOS 18.2 (22K154) - 0CF1F4CF-509B-4C73-91E9-384E2391FC03 (Ready)
-- watchOS --
watchOS 10.0 (21R5355a) - C324CC4D-DABF-4867-AFB2-1DA6842AEB70 (Ready)
watchOS 11.0 (22R349) - B03C9E06-BDC6-4618-89D9-858978F11DAA (Ready)
watchOS 10.5 (21T575) - 3EA7C48B-94DC-4E6F-B4B7-05851EDE5C1B (Ready)
watchOS 10.4 (21T214) - D8668730-8EE5-4C6F-9AB2-55A514131803 (Ready)
watchOS 10.0 (21R5320g) - 7CE64B6F-6D2A-4E07-B4C7-28980E0F382D (Ready)
watchOS 11.2 (22S99) - 6B2DF1B5-A7C5-45EA-B426-60188368EAB2 (Ready)
watchOS 10.0 (21R355) - A8A313A3-9B8F-45B7-A79E-F13F7E373C5B (Ready)
-- xrOS --
xrOS 2.4 (22O237) - 12E0EF92-99A8-4FD0-8B2D-5416A43E9E7F (Ready)
xrOS 2.0 (22N318) - C983135E-73B8-45AC-B8D0-62087B1DAA51 (Ready)
xrOS 26.0 (23M5311g) - A53FD054-539A-49DB-8716-06526E03E34A (Ready)
xrOS 2.0 (22N5314a) - 7C4C0037-CDBB-4752-B2CD-CFC19AD03138 (Ready)
xrOS 26.0 (23M5263m) - 5929D270-CB9E-4301-A7EC-9C814CD0C1D8 (Ready)
xrOS 1.1 (21O209) - 4A2DCD8C-95C9-489C-9EB8-1272087E8EEE (Ready)
xrOS 2.2 (22N840) - CA224F4D-C1AB-4351-AD16-E79B71853184 (Ready)
xrOS 1.0 (21N5300a) - 37913758-4475-4F0E-B574-BBE2A9B00475 (Ready)
xrOS 1.0 (21N305) - 5CCEADAF-4C3B-4519-BDDC-61E9479A6530 (Ready)
この中で不要なランタイムを以下のコマンドで削除。
例)xrOS 1.0 (21N305)を削除する場合
xcrun simctl runtime delete 21N305
削除コマンド発行ごとに /Library/Developer/CoreSimulator/Volumes
内の大容量 .dmg
が 1つずつ消える。
結果
- 数10GB 単位のディスク容量を削減できた。
- 不要なランタイムを正規の方法で安全に削除できた。
- Xcode / Simulator の動作に問題なし。
スクリプト化
削除作業を簡略化するため、不要ランタイムを一括削除するスクリプトを作成。
#!/bin/bash
# ============================================================
# 🧹 iOS/tvOS/watchOS/xrOS Simulator Runtime Cleanup Tool
# ============================================================
# List installed runtimes
echo "📋 Listing installed runtimes..."
xcrun simctl runtime list
echo ""
echo "⚠️ This script will delete ALL runtimes except the latest ones."
echo "Press Ctrl+C to cancel or Enter to continue."
read
# Get latest major versions to keep (example: iOS 18, watchOS 11, etc.)
KEEP_VERSIONS=("iOS 18" "tvOS 18" "watchOS 11" "xrOS 26")
# Loop through all runtimes and delete if not in keep list
xcrun simctl runtime list | grep -E "Ready" | while read -r line; do
UUID=$(echo "$line" | awk '{print $NF}' | tr -d '()')
NAME=$(echo "$line" | cut -d'-' -f1 | sed 's/ *$//')
if [[ " ${KEEP_VERSIONS[*]} " =~ " $NAME " ]]; then
echo "✅ Keeping: $NAME"
else
echo "🗑️ Deleting: $NAME ($UUID)"
xcrun simctl runtime delete "$UUID"
fi
done
echo "🎉 Cleanup complete."
スクリプトの使い方と注意点
- スクリプトを
cleanup_sim_runtimes.sh
として保存。 - 実行権限を付与:
chmod +x cleanup_sim_runtimes.sh
- 実行:
./cleanup_sim_runtimes.sh
-
KEEP_VERSIONS
配列を編集し、残したい最新バージョンを指定すること。 - 誤削除を防ぐため、削除前に確認プロンプトあり。
もっと細かく削除操作を指定できるスクリプト
#!/usr/bin/env bash
set -euo pipefail
# ==========================================================
# SimRuntime Pruner: bulk-delete unnecessary Simulator runtimes
# - Lists installed runtimes via `xcrun simctl runtime list`
# - Supports interactive selection or non-interactive flags
# - Uses official deletion: `xcrun simctl runtime delete <build>`
#
# Requirements:
# - macOS with Xcode command line tools
# - Close Xcode / Simulator before running
#
# Usage (examples):
# bash simruntime-prune.sh # interactive mode
# bash simruntime-prune.sh --list # show runtimes only
# bash simruntime-prune.sh --delete-build 22A3351,21C62 --yes
# bash simruntime-prune.sh --keep-latest-per-platform --yes
# bash simruntime-prune.sh --platform ios,watchos --keep-latest-per-platform --yes
# bash simruntime-prune.sh --dry-run --delete-build 22A3351
#
# Flags:
# --list Only list runtimes and exit
# --platform p1,p2 Filter platforms: ios,tvos,watchos,xros
# --delete-build b1,b2 Delete specified build IDs
# --keep-latest-per-platform Keep newest version per platform, delete others
# --dry-run Show what would be deleted
# --yes Skip confirmation prompts
# ==========================================================
# ---------- helpers ----------
err() { echo "ERROR: $*" >&2; exit 1; }
info() { echo "$*"; }
require_cmd() { command -v "$1" >/dev/null 2>&1 || err "Command not found: $1"; }
require_cmd xcrun
require_cmd awk
require_cmd grep
require_cmd sed
require_cmd sort
PLAT_FILTER="" # csv of platforms to include: ios,tvos,watchos,xros
DELETE_BUILDS="" # csv of build IDs to delete
LIST_ONLY=false
KEEP_LATEST_PER_PLATFORM=false
DRY_RUN=false
ASSUME_YES=false
while [[ $# -gt 0 ]]; do
case "$1" in
--list) LIST_ONLY=true ;;
--platform) shift; PLAT_FILTER="${1:-}";;
--delete-build) shift; DELETE_BUILDS="${1:-}";;
--keep-latest-per-platform) KEEP_LATEST_PER_PLATFORM=true ;;
--dry-run) DRY_RUN=true ;;
--yes) ASSUME_YES=true ;;
-h|--help)
sed -n '1,60p' "$0" | sed -e 's/^# \{0,1\}//'
exit 0
;;
*) err "Unknown argument: $1";;
esac
shift
done
# Convert platform filter to regex if provided
PLAT_RE=""
if [[ -n "$PLAT_FILTER" ]]; then
# normalize to lowercase, split commas
lower="$(echo "$PLAT_FILTER" | tr '[:upper:]' '[:lower:]')"
# validate tokens
IFS=',' read -r -a toks <<< "$lower"
ok_re='^(ios|tvos|watchos|xros)$'
tmp=""
for t in "${toks[@]}"; do
[[ "$t" =~ $ok_re ]] || err "Invalid platform: $t (allowed: ios,tvos,watchos,xros)"
tmp+="$t|"
done
PLAT_RE="^($(echo "$tmp" | sed 's/|$//'))$"
fi
# ---------- fetch & parse runtimes ----------
# We parse lines like:
# iOS 18.4 (22E5216h) - EB21... (Ready)
# Extract fields: platform|version|build|uuid
RAW_LIST="$(xcrun simctl runtime list || true)"
PARSED="$(
echo "$RAW_LIST" \
| grep -E '^(iOS|tvOS|watchOS|xrOS) ' \
| sed -E 's/^(iOS|tvOS|watchOS|xrOS) ([^ ]+) \(([A-Za-z0-9]+)\) - ([A-F0-9-]+).*/\1|\2|\3|\4/gI' \
| awk -F'|' '
{
# Normalize platform to lowercase: ios,tvos,watchos,xros
p=tolower($1)
# Keep version as-is, but ensure sortable form exists as key
v=$2
b=$3
u=$4
print p "|" v "|" b "|" u
}'
)"
if [[ -z "$PARSED" ]]; then
err "No runtimes found by simctl. Is Xcode installed?"
fi
# Apply platform filter if requested
if [[ -n "$PLAT_RE" ]]; then
PARSED="$(echo "$PARSED" | awk -F'|' -v re="$PLAT_RE" '$1 ~ re')"
[[ -n "$PARSED" ]] || err "No runtimes match platform filter: $PLAT_FILTER"
fi
# ---------- listing ----------
print_table() {
# Pretty print with index
awk -F'|' '
BEGIN { printf("%-4s %-8s %-12s %-12s %-36s\n", "No.", "Plat", "Version", "Build", "UUID");
print "--------------------------------------------------------------------------" }
{ printf("%-4d %-8s %-12s %-12s %-36s\n", NR, $1, $2, $3, $4) }
' <<< "$PARSED"
}
if $LIST_ONLY; then
info "Installed Simulator Runtimes:"
print_table
exit 0
fi
# ---------- build deletion set ----------
to_delete_builds=()
# Mode A: explicit build IDs
if [[ -n "$DELETE_BUILDS" ]]; then
IFS=',' read -r -a bs <<< "$DELETE_BUILDS"
for b in "${bs[@]}"; do
b_trim="$(echo "$b" | xargs)"
[[ -n "$b_trim" ]] && to_delete_builds+=("$b_trim")
done
fi
# Mode B: keep-latest-per-platform
# We select the highest semantic version per platform and mark others for deletion.
semver_key() {
# Convert "18.4" or "17.0.1" to comparable key: 00018.00004.00001
local v="$1"
IFS='.' read -r a b c <<< "$v"
a=${a:-0}; b=${b:-0}; c=${c:-0}
printf "%05d.%05d.%05d" "$a" "$b" "$c"
}
if $KEEP_LATEST_PER_PLATFORM; then
# Build arrays per platform
mapfile -t rows < <(echo "$PARSED")
declare -A best_key
declare -A best_build
declare -A best_line
for row in "${rows[@]}"; do
plat="${row%%|*}"
rest="${row#*|}"
ver="${rest%%|*}"
rest="${rest#*|}"
build="${rest%%|*}"
key="$(semver_key "$ver")"
if [[ -z "${best_key[$plat]+x}" || "$key" > "${best_key[$plat]}" ]]; then
best_key[$plat]="$key"
best_build[$plat]="$build"
best_line[$plat]="$row"
fi
done
# Anything not the best per platform → delete
while IFS='|' read -r plat ver build uuid; do
if [[ "${best_build[$plat]:-}" != "$build" ]]; then
to_delete_builds+=("$build")
fi
done <<< "$PARSED"
fi
# Mode C: interactive (default when nothing specified)
if [[ ${#to_delete_builds[@]} -eq 0 ]]; then
info "Installed Simulator Runtimes:"
print_table
echo
read -r -p "Enter No. to delete (e.g. 1,3-5) or press Enter to cancel: " sel
if [[ -z "${sel// }" ]]; then
info "No selection. Exit."
exit 0
fi
# expand selection like "1,3-5"
expand_selection() {
local s="$1"
local out=()
IFS=',' read -r -a parts <<< "$s"
for p in "${parts[@]}"; do
if [[ "$p" =~ ^[0-9]+-[0-9]+$ ]]; then
IFS='-' read -r a z <<< "$p"
for ((i=a; i<=z; i++)); do out+=("$i"); done
elif [[ "$p" =~ ^[0-9]+$ ]]; then
out+=("$p")
fi
done
printf "%s\n" "${out[@]}"
}
mapfile -t idxs < <(expand_selection "$sel")
# get selected builds
n=0
while IFS='|' read -r plat ver build uuid; do
n=$((n+1))
for i in "${idxs[@]}"; do
if [[ "$i" -eq "$n" ]]; then
to_delete_builds+=("$build")
fi
done
done <<< "$PARSED"
if [[ ${#to_delete_builds[@]} -eq 0 ]]; then
info "No valid selection. Exit."
exit 0
fi
fi
# Deduplicate builds
if [[ ${#to_delete_builds[@]} -gt 0 ]]; then
mapfile -t to_delete_builds < <(printf "%s\n" "${to_delete_builds[@]}" | awk '!seen[$0]++')
fi
# Show summary
summary_table() {
local builds_set="$1"
awk -F'|' -v list="$builds_set" '
BEGIN {
split(list, arr, ",");
for (i in arr) want[arr[i]]=1;
printf("Will delete:\n");
printf("%-8s %-12s %-12s %-36s\n", "Plat", "Version", "Build", "UUID");
print "---------------------------------------------------------------"
}
want[$3]==1 { printf("%-8s %-12s %-12s %-36s\n", $1, $2, $3, $4) }
' <<< "$PARSED"
}
builds_csv="$(IFS=','; echo "${to_delete_builds[*]}")"
if [[ -z "$builds_csv" ]]; then
info "Nothing to delete. Exit."
exit 0
fi
summary_table "$builds_csv"
echo
if $DRY_RUN; then
info "[DRY-RUN] No changes will be made."
exit 0
fi
if ! $ASSUME_YES; then
read -r -p "Proceed with deletion? [y/N] " ans
[[ "${ans,,}" == "y" || "${ans,,}" == "yes" ]] || { info "Cancelled."; exit 0; }
fi
# ---------- deletion ----------
for b in ${builds_csv//,/ }; do
info "Deleting runtime build: $b"
if ! xcrun simctl runtime delete "$b"; then
err "Failed to delete build: $b (try closing Xcode/Simulator and retry)"
fi
done
info "Done."
スクリプトの使い方
-
インタラクティブ(番号で選んで一括削除)
bash simruntime-prune.sh -
一覧だけ表示
bash simruntime-prune.sh --list -
ビルド番号で一括削除(確認省略)
bash simruntime-prune.sh --delete-build 22A3351,21C62 --yes -
プラットフォームごとに「最新だけ残す」(他は削除)
bash simruntime-prune.sh --keep-latest-per-platform --yes -
iOS と watchOS だけ対象にして「最新だけ残す」
bash simruntime-prune.sh --platform ios,watchos --keep-latest-per-platform --yes -
まずはドライラン(削除せずに対象確認)
bash simruntime-prune.sh --delete-build 22A3351 --dry-run
考察
-
xcrun simctl runtime delete
は公式に用意された手段のため、システムへの悪影響がない。 -
.dmg
を直接削除するよりも安全で、依存関係の整合性も保たれる。 - Xcode のベータ利用などでランタイムが大量に溜まる前に、定期的にスクリプトを実行して整理するのが望ましい。