Files
macos-usb-stick-creator/native-macos/create-stick.sh
T
ARIA e4dd003bd6 feat(native-macos): create-stick.sh — nativer createinstallmedia-Weg fuer echte Macs
Alternative zum Docker/Linux-Tool: nutzt softwareupdate --fetch-full-installer
+ createinstallmedia direkt, wenn ein Mac (10.15+) verfuegbar ist. Bewusst
unabhaengig vom ./downloads-Ordner, da createinstallmedia die volle
Install-App braucht, nicht die rohen Katalog-Images des Docker-Pfads.
Reines Terminal-Menue ohne externe Abhaengigkeiten (softwareupdate, diskutil,
PlistBuddy). Zukunftssicher analog zum Docker-Tool, da Apples eigene
Versionsliste live abgefragt wird.
2026-07-18 09:53:41 +00:00

243 lines
8.0 KiB
Bash
Executable File

#!/bin/bash
#
# create-stick.sh — natives macOS-Terminal-Tool fuer bootfaehige
# Installer-Sticks via Apples eigenem createinstallmedia.
#
# Laeuft NUR auf echtem macOS (10.15 Catalina oder neuer). Nutzt
# ausschliesslich Bordmittel: softwareupdate, diskutil, PlistBuddy,
# createinstallmedia. Kein Homebrew, keine externen Abhaengigkeiten.
#
# Warum ein eigener Pfad neben dem Docker/Linux-Tool im Projekt-Root:
# Das Docker-Tool zieht rohe Katalog-Images (BaseSystem.dmg bzw.
# InstallAssistant.pkg) und schreibt sie per dd — das funktioniert ohne
# Mac, aber createinstallmedia braucht die VOLLSTAENDIGE "Install macOS
# X.app", nicht die rohen Katalog-Dateien. Der offizielle, von Apple
# unterstuetzte Weg an diese App zu kommen OHNE App Store ist
# `softwareupdate --fetch-full-installer`. Deshalb ist dieses Script
# bewusst komplett unabhaengig vom ./downloads/library-Ordner des
# Docker-Tools — beide Formate sind nicht kompatibel zueinander.
#
# Zukunftssicherheit: `softwareupdate --list-full-installers` fragt
# Apples eigene Server live ab. Neue macOS-Versionen tauchen automatisch
# auf, ohne dass dieses Script angefasst werden muss.
set -u
# ---------- Darstellung ----------
BOLD="$(tput bold 2>/dev/null || true)"
RED="$(tput setaf 1 2>/dev/null || true)"
GREEN="$(tput setaf 2 2>/dev/null || true)"
YELLOW="$(tput setaf 3 2>/dev/null || true)"
CYAN="$(tput setaf 6 2>/dev/null || true)"
RESET="$(tput sgr0 2>/dev/null || true)"
hr() { printf '%s\n' "----------------------------------------------------------------------"; }
title() { clear; hr; printf "%s%s%s\n" "$BOLD$CYAN" " $1" "$RESET"; hr; echo; }
err() { printf "%s%s%s\n" "$RED" "FEHLER: $1" "$RESET" >&2; }
ok() { printf "%s%s%s\n" "$GREEN" "$1" "$RESET"; }
warn() { printf "%s%s%s\n" "$YELLOW" "$1" "$RESET"; }
# ---------- Vorpruefungen ----------
if [[ "$(uname)" != "Darwin" ]]; then
err "Dieses Script laeuft nur auf echtem macOS (kein Linux, keine VM ohne macOS)."
exit 1
fi
if ! command -v softwareupdate >/dev/null 2>&1; then
err "'softwareupdate' nicht gefunden — ungewoehnliches macOS-Setup, Abbruch."
exit 1
fi
MACOS_VER="$(sw_vers -productVersion)"
# fetch-full-installer gibt es erst ab Catalina (10.15)
if [[ "$MACOS_VER" == 10.* ]]; then
MINOR="${MACOS_VER#10.}"
MINOR="${MINOR%%.*}"
if [[ "$MINOR" -lt 15 ]]; then
err "macOS ${MACOS_VER} erkannt — 'softwareupdate --fetch-full-installer' braucht mind. Catalina (10.15)."
exit 1
fi
fi
# ---------- Schritt 1: verfuegbare Installer laden ----------
title "macOS USB-Stick-Creator (nativ) — Schritt 1/4: Verfuegbare Versionen"
echo "Frage Apples Software-Update-Server nach verfuegbaren kompletten Installern ab..."
echo
LIST_RAW="$(softwareupdate --list-full-installers 2>/dev/null)"
if [[ -z "$LIST_RAW" ]] || ! grep -q "Title:" <<<"$LIST_RAW"; then
err "Keine Installer gefunden. Internetverbindung pruefen, oder Apple hat gerade ein Problem."
exit 1
fi
# Zeilenformat: "* Title: macOS Sonoma, Version: 14.6.1, Size: 12345678900, Build: 23G93"
declare -a TITLES=()
declare -a VERSIONS=()
while IFS= read -r line; do
[[ "$line" == *"Title:"* ]] || continue
t="$(sed -E 's/^\* Title: ([^,]+),.*/\1/' <<<"$line")"
v="$(sed -E 's/.*Version: ([^,]+),.*/\1/' <<<"$line")"
TITLES+=("$t")
VERSIONS+=("$v")
done <<<"$LIST_RAW"
if [[ ${#TITLES[@]} -eq 0 ]]; then
err "Konnte die Liste nicht parsen. Rohausgabe:"
echo "$LIST_RAW"
exit 1
fi
echo "Verfuegbar:"
echo
for i in "${!TITLES[@]}"; do
printf " %s%2d)%s %s (%s)\n" "$BOLD" "$((i+1))" "$RESET" "${TITLES[$i]}" "${VERSIONS[$i]}"
done
echo
printf "%s0)%s Abbrechen\n" "$BOLD" "$RESET"
echo
read -r -p "Welche Version soll heruntergeladen/benutzt werden? [Nummer]: " CHOICE
if [[ "$CHOICE" == "0" || -z "$CHOICE" ]]; then
echo "Abgebrochen."
exit 0
fi
if ! [[ "$CHOICE" =~ ^[0-9]+$ ]] || (( CHOICE < 1 || CHOICE > ${#TITLES[@]} )); then
err "Ungueltige Auswahl."
exit 1
fi
SEL_TITLE="${TITLES[$((CHOICE-1))]}"
SEL_VERSION="${VERSIONS[$((CHOICE-1))]}"
# Apples Titel-Feld liefert z.B. "macOS Sonoma" -> App heisst "Install macOS Sonoma.app"
APP_PATH="/Applications/Install ${SEL_TITLE}.app"
echo
ok "Gewaehlt: ${SEL_TITLE} (${SEL_VERSION})"
# ---------- Schritt 2: Download (falls noetig) ----------
title "Schritt 2/4: Installer-App besorgen"
if [[ -d "$APP_PATH" ]]; then
ok "'${APP_PATH}' existiert bereits — Download wird uebersprungen."
else
warn "Das laedt mehrere GB von Apple und braucht sudo-Rechte."
echo "Zielort: ${APP_PATH}"
read -r -p "Download jetzt starten? [j/N]: " GO
if [[ ! "$GO" =~ ^[jJ]$ ]]; then
echo "Abgebrochen."
exit 0
fi
echo
echo "Starte Download (kann je nach Version 30-90 Minuten dauern)..."
sudo softwareupdate --fetch-full-installer --full-installer-version "$SEL_VERSION"
RC=$?
if [[ $RC -ne 0 || ! -d "$APP_PATH" ]]; then
err "Download fehlgeschlagen oder App nicht am erwarteten Ort. Erwartet: ${APP_PATH}"
echo "Tipp: 'ls /Applications | grep Install' zeigt den tatsaechlichen Namen."
exit 1
fi
ok "Download abgeschlossen: ${APP_PATH}"
fi
CIM_BIN="${APP_PATH}/Contents/Resources/createinstallmedia"
if [[ ! -x "$CIM_BIN" ]]; then
err "createinstallmedia nicht gefunden unter: ${CIM_BIN}"
exit 1
fi
# ---------- Schritt 3: Ziel-USB-Stick waehlen ----------
title "Schritt 3/4: USB-Stick auswaehlen"
echo "Externe, physische Laufwerke (nur ganze Platten, keine Partitionen):"
echo
PLIST_TMP="$(mktemp /tmp/diskutil_ext.XXXXXX.plist)"
diskutil list -plist external physical > "$PLIST_TMP" 2>/dev/null
declare -a DISKS=()
declare -a DISK_DESC=()
i=0
while :; do
DID="$(/usr/libexec/PlistBuddy -c "Print :AllDisksAndPartitions:${i}:DeviceIdentifier" "$PLIST_TMP" 2>/dev/null)"
[[ -z "$DID" ]] && break
INFO="$(diskutil info -plist "$DID" 2>/dev/null)"
NAME="$(echo "$INFO" | /usr/libexec/PlistBuddy -c "Print :MediaName" /dev/stdin 2>/dev/null)"
SIZE_BYTES="$(echo "$INFO" | /usr/libexec/PlistBuddy -c "Print :TotalSize" /dev/stdin 2>/dev/null)"
SIZE_GB="?"
if [[ "$SIZE_BYTES" =~ ^[0-9]+$ ]]; then
SIZE_GB="$(( SIZE_BYTES / 1000 / 1000 / 1000 ))"
fi
DISKS+=("$DID")
DISK_DESC+=("/dev/${DID}${NAME:-unbekannt} (~${SIZE_GB} GB)")
i=$((i+1))
done
rm -f "$PLIST_TMP"
if [[ ${#DISKS[@]} -eq 0 ]]; then
err "Keine externen physischen Laufwerke gefunden. Stick eingesteckt?"
exit 1
fi
for i in "${!DISKS[@]}"; do
printf " %s%2d)%s %s\n" "$BOLD" "$((i+1))" "$RESET" "${DISK_DESC[$i]}"
done
echo
printf "%s0)%s Abbrechen\n" "$BOLD" "$RESET"
echo
warn "ACHTUNG: Der gewaehlte Stick wird KOMPLETT GELOESCHT."
echo
read -r -p "Welcher Stick? [Nummer]: " DCHOICE
if [[ "$DCHOICE" == "0" || -z "$DCHOICE" ]]; then
echo "Abgebrochen."
exit 0
fi
if ! [[ "$DCHOICE" =~ ^[0-9]+$ ]] || (( DCHOICE < 1 || DCHOICE > ${#DISKS[@]} )); then
err "Ungueltige Auswahl."
exit 1
fi
TARGET_DISK="${DISKS[$((DCHOICE-1))]}"
TARGET_DESC="${DISK_DESC[$((DCHOICE-1))]}"
echo
warn "Letzte Sicherheitsabfrage — dieser Stick wird geloescht:"
echo " ${TARGET_DESC}"
echo
read -r -p "Tippe exakt '/dev/${TARGET_DISK}' zum Bestaetigen: " CONFIRM
if [[ "$CONFIRM" != "/dev/${TARGET_DISK}" ]]; then
err "Eingabe stimmt nicht ueberein — abgebrochen, nichts wurde veraendert."
exit 1
fi
# ---------- Schritt 4: createinstallmedia ausfuehren ----------
title "Schritt 4/4: Stick wird erstellt"
echo "App: ${APP_PATH}"
echo "Ziel: /dev/${TARGET_DISK}"
echo
echo "createinstallmedia laeuft jetzt (braucht sudo, dauert typischerweise 10-30 Minuten)..."
echo "Der Stick wird zwischendurch umbenannt/neu gemountet — das ist normal."
echo
sudo "$CIM_BIN" --volume "/dev/${TARGET_DISK}" --nointeraction
RC=$?
echo
if [[ $RC -eq 0 ]]; then
ok "Fertig! Der Stick ist bootfaehig."
echo "Am Ziel-Mac: beim Start Option/Alt gedrueckt halten, den Stick im Boot-Picker waehlen."
else
err "createinstallmedia ist mit Fehlercode ${RC} beendet — siehe Ausgabe oben."
exit $RC
fi
echo
read -r -p "Die heruntergeladene Installer-App (mehrere GB) jetzt loeschen um Platz zu sparen? [j/N]: " CLEAN
if [[ "$CLEAN" =~ ^[jJ]$ ]]; then
sudo rm -rf "$APP_PATH"
ok "Geloescht: ${APP_PATH}"
fi
echo
ok "Alles erledigt."