0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Xcode】シミュレータのゾンビになった不要ランタイムのディスクイメージ削除

Last updated at Posted at 2025-08-20

解決したいこと

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

対策案

  1. Finder から直接削除 → システム影響の懸念があるため不採用。
  2. Xcode → Settings → Components メニューで削除 → .dmg ファイルが残存するため不十分。
  3. 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."

スクリプトの使い方と注意点

  1. スクリプトを cleanup_sim_runtimes.sh として保存。
  2. 実行権限を付与:
    chmod +x cleanup_sim_runtimes.sh
    
  3. 実行:
    ./cleanup_sim_runtimes.sh
    
  4. KEEP_VERSIONS 配列を編集し、残したい最新バージョンを指定すること。
  5. 誤削除を防ぐため、削除前に確認プロンプトあり。

もっと細かく削除操作を指定できるスクリプト

#!/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."

スクリプトの使い方

  1. インタラクティブ(番号で選んで一括削除)
    bash simruntime-prune.sh

  2. 一覧だけ表示
    bash simruntime-prune.sh --list

  3. ビルド番号で一括削除(確認省略)
    bash simruntime-prune.sh --delete-build 22A3351,21C62 --yes

  4. プラットフォームごとに「最新だけ残す」(他は削除)
    bash simruntime-prune.sh --keep-latest-per-platform --yes

  5. iOS と watchOS だけ対象にして「最新だけ残す」
    bash simruntime-prune.sh --platform ios,watchos --keep-latest-per-platform --yes

  6. まずはドライラン(削除せずに対象確認)
    bash simruntime-prune.sh --delete-build 22A3351 --dry-run

考察

  • xcrun simctl runtime delete は公式に用意された手段のため、システムへの悪影響がない。
  • .dmg を直接削除するよりも安全で、依存関係の整合性も保たれる。
  • Xcode のベータ利用などでランタイムが大量に溜まる前に、定期的にスクリプトを実行して整理するのが望ましい。
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?