From 39564842468aa7b1648a0551851e365ab1c22565 Mon Sep 17 00:00:00 2001 From: ARIA Date: Sat, 18 Jul 2026 12:12:36 +0200 Subject: [PATCH] feat(native-macos): Multiboot-Stick-Modus fuer createinstallmedia/asr Ein Stick, mehrere macOS-Versionen, frei mischbar aus Apple-Server- und Bibliotheks-Quelle. Partitioniert den Stick per diskutil partitionDisk in eine eigene HFS+-Partition je Version (eindeutige Labels), schreibt danach pro Partition gezielt per createinstallmedia --volume bzw. asr restore --target -- analog zur bereits vorhandenen Multiboot-Technik im Docker/Linux-Tool. Inkl. Groessen-Check mit Sicherheitspuffer vor jeder Aenderung am Stick, robustem Fehlerpfad je Partition (Weiter/Abbruch statt Stillschweigen) und End-to-End-Test der kompletten Ablauflogik unter Linux mit gestubbten macOS-Bordmitteln (Einzel-Modus + Multiboot, reine Apple-Quelle + gemischte Apple/Bibliotheks-Quelle). --- native-macos/README.md | 72 ++- native-macos/create-stick.sh | 855 +++++++++++++++++++++++++++-------- 2 files changed, 717 insertions(+), 210 deletions(-) diff --git a/native-macos/README.md b/native-macos/README.md index 1d03b95..e53d93d 100644 --- a/native-macos/README.md +++ b/native-macos/README.md @@ -1,8 +1,9 @@ # Nativer macOS-Weg (createinstallmedia / asr) Alternative zum Docker/Linux-Tool im Projekt-Root — **nur nutzbar, wenn -gerade ein echter Mac zur Verfuegung steht**. Zwei Quellen fuer den -Installer-Payload stehen zur Auswahl, direkt im Script: +gerade ein echter Mac zur Verfuegung steht**. Zwei Modi (Einzel-Stick / +Multiboot-Stick) und zwei Quellen fuer den Installer-Payload stehen zur +Auswahl, direkt im Script: 1. **Von Apple herunterladen** (macOS 10.15 Catalina oder neuer als Host noetig) — der offiziell von Apple unterstuetzte Weg ueber @@ -61,9 +62,12 @@ chmod +x create-stick.sh # falls das Ausfuehr-Bit beim Kopieren verloren ging ./create-stick.sh ``` -Fuehrt durch vier Schritte (reines Terminal-Menue, keine externen -Abhaengigkeiten — nur `softwareupdate`, `diskutil`, `PlistBuddy`, -`installer`, `asr`, `createinstallmedia`, alles Bordmittel): +Fragt zuerst den **Modus** ab (Einzel-Stick oder Multiboot-Stick), dann +fuehrt es durch die passenden Schritte (reines Terminal-Menue, keine +externen Abhaengigkeiten — nur `softwareupdate`, `diskutil`, +`PlistBuddy`, `installer`, `asr`, `createinstallmedia`, alles Bordmittel). + +### Modus 1: Einzelner Stick (4 Schritte) 1. **Quelle + Version waehlen** — Apple-Server oder lokale Bibliothek (Pfad wird abgefragt, Default rät `../downloads/library` relativ zum @@ -80,6 +84,40 @@ Abhaengigkeiten — nur `softwareupdate`, `diskutil`, `PlistBuddy`, `diskutil eraseDisk` + `asr restore --erase` (Bibliothek, bis Catalina). Beides mit `sudo`, live-Ausgabe im Terminal. +### Modus 2: Multiboot-Stick (5 Schritte) + +Ein Stick, **mehrere** macOS-Versionen gleichzeitig — frei mischbar aus +Apple-Server und lokaler Bibliothek (z.B. eine Version per Download, zwei +weitere aus der Offline-Bibliothek, in einem Lauf). + +1. **Versionen auswaehlen** — kombinierte Liste aus Apple-Server + + Bibliothek, Mehrfachauswahl per Komma (`1,3,4`), 2 bis 8 Versionen. +2. **Payloads besorgen** — pro gewaehlter Version wie in Modus 1, Schritt + 2 (Download/Entpacken/direktes Image), nacheinander. +3. **USB-Stick auswaehlen** — wie Modus 1, plus **Groessen-Check**: die + Summe aus geschaetztem Platzbedarf je Version (inkl. Sicherheitspuffer: + +33% bei `createinstallmedia`-Versionen, +5% bei reinen Image-Restores, + jeweils mit Mindestgroesse) wird gegen die Stick-Kapazitaet geprueft. + Reicht der Platz nicht, bricht das Script **vor** jeder Aenderung am + Stick mit klarer Meldung ab. +4. **Partitionieren** — `diskutil partitionDisk` legt fuer jede gewaehlte + Version eine eigene HFS+-Partition mit eindeutigem Label an (GPT-Schema + mit automatischer EFI-Partition). Die letzte Partition bekommt den + restlichen Platz (`R`), um Rundungsverluste zu vermeiden. +5. **Partitionen beschreiben** — pro Partition laeuft `createinstallmedia + --volume ...` bzw. `asr restore --target ...` genau wie im Einzel-Modus, + nur gezielt gegen die jeweils eigene Partition statt gegen die ganze + Platte. Beide Tools reformatieren dabei ausschliesslich ihr eigenes + Zielvolume — die anderen Partitionen bleiben unberuehrt. Schlaegt eine + Version fehl, fragt das Script ob mit den restlichen weitergemacht + werden soll, statt den ganzen Lauf stillschweigend abzubrechen. + +Am Ziel-Mac zeigt der Boot-Picker (Option/Alt beim Start) danach jede +erfolgreich geschriebene Version als eigenen Eintrag — das ist dieselbe, +in der Community etablierte Partitionierungstechnik, die auch das +Docker/Linux-Tool dieses Projekts fuer seinen `dd`-basierten Multiboot-Weg +nutzt. + ## Grenzen - Nur macOS 10.15+ (Catalina oder neuer) als **Host**, auf dem das Script @@ -87,15 +125,21 @@ Abhaengigkeiten — nur `softwareupdate`, `diskutil`, `PlistBuddy`, jede von Apple unterstuetzte Version sein. Diese Einschraenkung gilt nur fuer die Apple-Download-Quelle (`softwareupdate --fetch-full-installer`); `installer`/`asr` funktionieren grundsaetzlich auch auf aelteren Hosts. -- Kein Multiboot-Support (anders als das Docker-Tool) — - jeder Lauf erstellt immer einen kompletten Single-Version-Stick. -- **Ungetestet auf echtem Mac.** Die Auswahl-/Verzweigungslogik (Quelle, - Modus-Erkennung ueber `meta.json`) wurde unter Linux mit einer - simulierten Bibliothek end-to-end durchgespielt (`bash -n` sauber, - beide Zweige laufen bis zum jeweils ersten macOS-only-Befehl korrekt - durch). Der eigentliche `installer`/`asr`/`createinstallmedia`-Lauf - selbst ist nur gegen offizielle Apple-Doku/Community-Quellen verifiziert, - nicht live auf einem Mac getestet. +- Multiboot-Groessenschaetzung ist ein **Puffer, keine Garantie** — bei + sehr knapp bemessenen Sticks im Zweifel weniger Versionen waehlen oder + einen groesseren Stick nehmen. +- **Ungetestet auf echtem Mac.** Die komplette Ablauflogik (Modus- und + Quellenwahl, Mehrfachauswahl-Parsing, Groessen-/Puffer-Berechnung, + Label-Vergabe, Partitionierung, sequentielles Schreiben inkl. + Fehlerpfade) wurde unter Linux mit gestubbten `softwareupdate` / + `diskutil` / `PlistBuddy` / `installer` / `asr` / + `createinstallmedia`-Bordmitteln end-to-end durchgespielt — Einzel-Modus + (Apple- und Bibliotheks-Quelle), Multiboot mit reinen Apple-Downloads + und Multiboot mit gemischten Apple+Bibliotheks-Quellen liefen dabei + jeweils fehlerfrei bis zum Abschluss durch, inklusive Sicherheitsabbruch + bei falscher Bestaetigung. Der eigentliche `installer`/`asr`/ + `createinstallmedia`-Lauf selbst ist nur gegen offizielle Apple-Doku/ + Community-Quellen verifiziert, nicht live auf einem Mac getestet. ## Quellen (fuer den asr-Weg, live nachgeschlagen) diff --git a/native-macos/create-stick.sh b/native-macos/create-stick.sh index 7a6b6e9..b068496 100755 --- a/native-macos/create-stick.sh +++ b/native-macos/create-stick.sh @@ -32,6 +32,21 @@ # So wird aus jeder bereits vom Docker-Tool gebauten Offline-Bibliothek # (externe Platte, ohne Internet) auch am echten Mac ein bootfaehiger # Stick, ganz ohne doppelten Download. +# +# ZWEI MODI: +# +# A) Einzelner Stick — ein Stick, eine macOS-Version (Original-Modus). +# B) Multiboot-Stick — ein Stick, MEHRERE macOS-Versionen gleichzeitig. +# Technik: der Stick wird per 'diskutil partitionDisk' in N eigene +# HFS+-Partitionen aufgeteilt (eine pro gewaehlter Version). Danach +# laeuft createinstallmedia bzw. asr restore genau wie im +# Einzel-Modus, nur pro Durchlauf gegen die jeweils EIGENE Partition +# statt gegen die ganze Platte -- beide Tools reformatieren dabei +# ausschliesslich ihr eigenes Zielvolume, die anderen Partitionen +# bleiben unberuehrt. Macs EFI-Bootpicker (Alt/Option beim Start) +# scannt alle Partitionen und zeigt jede bootfaehige einzeln an. +# Das ist dieselbe, in der Community etablierte Technik wie sie das +# Docker/Linux-Tool dieses Projekts fuer den dd-Weg schon nutzt. set -u @@ -57,13 +72,264 @@ json_field() { sed -n 's/.*"'"$key"'":[[:space:]]*"\{0,1\}\([^",}]*\)"\{0,1\}.*/\1/p' "$file" | head -n1 } +# Bytes -> aufgerundete ganze GB (fuer Anzeige und Partitionsgroessen). +bytes_to_gb_ceil() { + local b="$1" + echo $(( (b + 999999999) / 1000000000 )) +} + +# Kuerzt/saeubert einen Titel zu einem gueltigen, eindeutigen HFS+-Label. +# HFS+-Volume-Namen: max. 27 Zeichen, kein ':' und kein '/'. Wir haengen +# den Auswahl-Index voran, damit gleichnamige Versionen nie kollidieren. +make_label() { + local idx="$1" title="$2" + local clean + clean="$(echo "$title" | tr -d ':/' | tr -s ' ' ' ')" + clean="V${idx}_${clean}" + echo "${clean:0:27}" +} + # ---------- Vorpruefungen ---------- if [[ "$(uname)" != "Darwin" ]]; then err "Dieses Script laeuft nur auf echtem macOS (kein Linux, keine VM ohne macOS)." exit 1 fi -# ---------- Schritt 1: Quelle + Version waehlen ---------- +DEFAULT_LIB_DIR="$(cd "$(dirname "$0")/.." 2>/dev/null && pwd)/downloads/library" + +# ====================================================================== +# Gemeinsame Listen-Beschaffung (fuer Einzel- UND Multiboot-Modus) +# ====================================================================== + +# Fuellt die globalen Arrays A_TITLES/A_VERSIONS/A_SIZES mit Apples +# aktuell verfuegbaren kompletten Installern. Gibt 1 zurueck wenn keine +# Liste beschafft werden konnte (kein Abbruch des Scripts -- der Aufrufer +# entscheidet, ob das schlimm ist). +fetch_apple_list() { + A_TITLES=() + A_VERSIONS=() + A_SIZES=() + + command -v softwareupdate >/dev/null 2>&1 || return 1 + + local list_raw + list_raw="$(softwareupdate --list-full-installers 2>/dev/null)" + grep -q "Title:" <<<"$list_raw" || return 1 + + local line t v s + while IFS= read -r line; do + [[ "$line" == *"Title:"* ]] || continue + t="$(sed -E 's/^\* Title: ([^,]+),.*/\1/' <<<"$line")" + v="$(sed -E 's/.*Version: ([^,]+),.*/\1/' <<<"$line")" + s="$(sed -E 's/.*Size: ([0-9]+),.*/\1/' <<<"$line")" + [[ "$s" =~ ^[0-9]+$ ]] || s="0" + A_TITLES+=("$t") + A_VERSIONS+=("$v") + A_SIZES+=("$s") + done <<<"$list_raw" + + [[ ${#A_TITLES[@]} -gt 0 ]] +} + +# Fuellt die globalen Arrays L_DIRS/L_TITLES/L_VERSIONS/L_MODES/L_SIZES/ +# L_FILENAMES mit den vollstaendigen Eintraegen der lokalen Bibliothek. +# $1 = Pfad zur Bibliothek. Gibt 1 zurueck wenn nichts gefunden wurde. +fetch_library_list() { + local lib_dir="$1" + L_DIRS=() + L_TITLES=() + L_VERSIONS=() + L_MODES=() + L_SIZES=() + L_FILENAMES=() + + [[ -d "$lib_dir" ]] || return 1 + + local entry_dir meta complete et ev emode esize efn + for entry_dir in "$lib_dir"/*/; do + [[ -d "$entry_dir" ]] || continue + meta="${entry_dir}meta.json" + [[ -f "$meta" ]] || continue + complete="$(json_field "$meta" complete)" + [[ "$complete" == "true" ]] || continue + et="$(json_field "$meta" title)" + ev="$(json_field "$meta" version)" + emode="$(json_field "$meta" mode)" + esize="$(json_field "$meta" size_bytes)" + efn="$(json_field "$meta" filename)" + [[ "$esize" =~ ^[0-9]+$ ]] || esize="0" + L_DIRS+=("$entry_dir") + L_TITLES+=("$et") + L_VERSIONS+=("$ev") + L_MODES+=("$emode") + L_SIZES+=("$esize") + L_FILENAMES+=("$efn") + done + + [[ ${#L_DIRS[@]} -gt 0 ]] +} + +# ====================================================================== +# Payload-Beschaffung fuer EINEN gewaehlten Eintrag (von Apple oder aus +# der Bibliothek). Setzt bei Erfolg die globalen Variablen: +# RESOLVED_WRITE_MODE ("cim" oder "asr") +# RESOLVED_APP_PATH (bei cim) +# RESOLVED_DMG_PATH (bei asr) +# RESOLVED_SIZE_BYTES (reale Groesse nach Beschaffung, fuer Partition) +# Gibt bei Fehler != 0 zurueck (Aufrufer entscheidet ueber Abbruch). +# +# Argumente: resolve_payload <version> \ +# [lib_dir lib_mode lib_filename] +# ====================================================================== +resolve_payload() { + local src="$1" title="$2" version="$3" + local lib_dir="${4:-}" lib_mode="${5:-}" lib_filename="${6:-}" + + RESOLVED_WRITE_MODE="" + RESOLVED_APP_PATH="" + RESOLVED_DMG_PATH="" + RESOLVED_SIZE_BYTES="0" + + if [[ "$src" == "apple" ]]; then + local app_path="/Applications/Install ${title}.app" + RESOLVED_WRITE_MODE="cim" + 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 (${title}, ${version})." + read -r -p "Download jetzt starten? [j/N]: " GO + if [[ ! "$GO" =~ ^[jJ]$ ]]; then + err "Abgebrochen fuer ${title}." + return 1 + fi + echo + echo "Starte Download (kann je nach Version 30-90 Minuten dauern)..." + sudo softwareupdate --fetch-full-installer --full-installer-version "$version" + if [[ $? -ne 0 || ! -d "$app_path" ]]; then + err "Download fehlgeschlagen oder App nicht am erwarteten Ort. Erwartet: ${app_path}" + return 1 + fi + ok "Download abgeschlossen: ${app_path}" + fi + RESOLVED_APP_PATH="$app_path" + RESOLVED_SIZE_BYTES="$(du -sk "$app_path" 2>/dev/null | awk '{print $1*1024}')" + [[ "$RESOLVED_SIZE_BYTES" =~ ^[0-9]+$ ]] || RESOLVED_SIZE_BYTES="0" + local cim_bin="${app_path}/Contents/Resources/createinstallmedia" + if [[ ! -x "$cim_bin" ]]; then + err "createinstallmedia nicht gefunden unter: ${cim_bin}" + return 1 + fi + return 0 + fi + + # src == "library" + local payload_path="${lib_dir}${lib_filename}" + if [[ ! -f "$payload_path" ]]; then + err "Payload-Datei nicht gefunden: ${payload_path}" + return 1 + fi + + if [[ "$lib_mode" == "recovery" ]]; then + RESOLVED_WRITE_MODE="cim" + local app_path="/Applications/Install ${title}.app" + if [[ -d "$app_path" ]]; then + ok "'${app_path}' existiert bereits — Installer-Lauf wird uebersprungen." + else + echo "Entpacke ${payload_path} nach /Applications (installer -pkg, braucht sudo)..." + sudo installer -pkg "$payload_path" -target / + if [[ $? -ne 0 ]]; then + err "'installer -pkg' fehlgeschlagen fuer ${title}." + return 1 + fi + if [[ ! -d "$app_path" ]]; then + local fallback + fallback="$(ls -dt /Applications/Install\ macOS*.app 2>/dev/null | head -n1)" + [[ -n "$fallback" ]] && app_path="$fallback" + fi + if [[ ! -d "$app_path" ]]; then + err "Konnte die installierte App nach 'installer -pkg' nicht finden (${title})." + return 1 + fi + ok "Installiert: ${app_path}" + fi + RESOLVED_APP_PATH="$app_path" + RESOLVED_SIZE_BYTES="$(du -sk "$app_path" 2>/dev/null | awk '{print $1*1024}')" + [[ "$RESOLVED_SIZE_BYTES" =~ ^[0-9]+$ ]] || RESOLVED_SIZE_BYTES="0" + local cim_bin="${app_path}/Contents/Resources/createinstallmedia" + if [[ ! -x "$cim_bin" ]]; then + err "createinstallmedia nicht gefunden unter: ${cim_bin}" + return 1 + fi + else + RESOLVED_WRITE_MODE="asr" + RESOLVED_DMG_PATH="$payload_path" + RESOLVED_SIZE_BYTES="$(stat -f%z "$payload_path" 2>/dev/null)" + [[ "$RESOLVED_SIZE_BYTES" =~ ^[0-9]+$ ]] || RESOLVED_SIZE_BYTES="0" + ok "Bibliothek liefert bereits ein fertiges, bootfaehiges Image (${payload_path}) -- kein App-Download noetig." + fi + return 0 +} + +# ====================================================================== +# Externe physische Laufwerke auflisten. Fuellt D_IDS/D_DESCS/D_SIZE_BYTES. +# ====================================================================== +list_external_disks() { + D_IDS=() + D_DESCS=() + D_SIZE_BYTES=() + + local plist_tmp + plist_tmp="$(mktemp /tmp/diskutil_ext.XXXXXX.plist)" + diskutil list -plist external physical > "$plist_tmp" 2>/dev/null + + local i=0 did info name size_bytes size_gb + 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_bytes" =~ ^[0-9]+$ ]] || size_bytes="0" + size_gb="$(bytes_to_gb_ceil "$size_bytes")" + D_IDS+=("$did") + D_DESCS+=("/dev/${did} — ${name:-unbekannt} (~${size_gb} GB)") + D_SIZE_BYTES+=("$size_bytes") + i=$((i+1)) + done + rm -f "$plist_tmp" + + [[ ${#D_IDS[@]} -gt 0 ]] +} + +# ====================================================================== +# MODUS-Wahl +# ====================================================================== +title "macOS USB-Stick-Creator (nativ) — Modus waehlen" + +echo "Was soll erstellt werden?" +echo +echo " ${BOLD}1)${RESET} Einzelner Stick (eine macOS-Version)" +echo " ${BOLD}2)${RESET} Multiboot-Stick (mehrere macOS-Versionen auf einem Stick)" +echo +printf "%s0)%s Abbrechen\n" "$BOLD" "$RESET" +echo +read -r -p "Modus waehlen [Nummer]: " MODE_CHOICE + +if [[ "$MODE_CHOICE" == "0" || -z "$MODE_CHOICE" ]]; then + echo "Abgebrochen." + exit 0 +fi + +if [[ "$MODE_CHOICE" != "1" && "$MODE_CHOICE" != "2" ]]; then + err "Ungueltige Auswahl." + exit 1 +fi + +# ====================================================================== +# ================ MODUS 1: EINZELNER STICK ========================== +# ====================================================================== +if [[ "$MODE_CHOICE" == "1" ]]; then + title "macOS USB-Stick-Creator (nativ) — Schritt 1/4: Quelle + Version" echo "Woher soll der macOS-Installer kommen?" @@ -80,26 +346,18 @@ if [[ "$SRC_CHOICE" == "0" || -z "$SRC_CHOICE" ]]; then exit 0 fi -SOURCE="" -WRITE_MODE="" # "cim" (createinstallmedia) oder "asr" (direktes Image-Restore) -APP_PATH="" -DMG_PATH="" -LIB_TITLE="" -LIB_VERSION="" - case "$SRC_CHOICE" in - 1) - SOURCE="apple" - ;; - 2) - SOURCE="library" - ;; - *) - err "Ungueltige Auswahl." - exit 1 - ;; + 1) SOURCE="apple" ;; + 2) SOURCE="library" ;; + *) err "Ungueltige Auswahl."; exit 1 ;; esac +SEL_TITLE="" +SEL_VERSION="" +SEL_LIB_DIR="" +SEL_LIB_MODE="" +SEL_LIB_FILENAME="" + if [[ "$SOURCE" == "apple" ]]; then if ! command -v softwareupdate >/dev/null 2>&1; then err "'softwareupdate' nicht gefunden — ungewoehnliches macOS-Setup, Abbruch." @@ -107,7 +365,6 @@ if [[ "$SOURCE" == "apple" ]]; then 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%%.*}" @@ -121,33 +378,15 @@ if [[ "$SOURCE" == "apple" ]]; then 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 + if ! fetch_apple_list; 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]}" + for i in "${!A_TITLES[@]}"; do + printf " %s%2d)%s %s (%s)\n" "$BOLD" "$((i+1))" "$RESET" "${A_TITLES[$i]}" "${A_VERSIONS[$i]}" done echo printf "%s0)%s Abbrechen\n" "$BOLD" "$RESET" @@ -158,56 +397,25 @@ if [[ "$SOURCE" == "apple" ]]; then echo "Abgebrochen." exit 0 fi - if ! [[ "$CHOICE" =~ ^[0-9]+$ ]] || (( CHOICE < 1 || CHOICE > ${#TITLES[@]} )); then + if ! [[ "$CHOICE" =~ ^[0-9]+$ ]] || (( CHOICE < 1 || CHOICE > ${#A_TITLES[@]} )); then err "Ungueltige Auswahl." exit 1 fi - SEL_TITLE="${TITLES[$((CHOICE-1))]}" - SEL_VERSION="${VERSIONS[$((CHOICE-1))]}" - WRITE_MODE="cim" - # Apples Titel-Feld liefert z.B. "macOS Sonoma" -> App heisst "Install macOS Sonoma.app" - APP_PATH="/Applications/Install ${SEL_TITLE}.app" - + SEL_TITLE="${A_TITLES[$((CHOICE-1))]}" + SEL_VERSION="${A_VERSIONS[$((CHOICE-1))]}" echo ok "Gewaehlt: ${SEL_TITLE} (${SEL_VERSION}) -- Quelle: Apple-Server" else echo - DEFAULT_LIB_DIR="$(cd "$(dirname "$0")/.." 2>/dev/null && pwd)/downloads/library" echo "Pfad zur lokalen Downloads-Bibliothek des Docker-Tools (Ordner mit einem" echo "Unterordner pro macOS-Version, je mit meta.json + Installer-Datei)." echo - read -r -p "Pfad [Enter fuer ${DEFAULT_LIB_DIR}]: " LIB_DIR - LIB_DIR="${LIB_DIR:-$DEFAULT_LIB_DIR}" + read -r -p "Pfad [Enter fuer ${DEFAULT_LIB_DIR}]: " LIB_DIR_IN + LIB_DIR="${LIB_DIR_IN:-$DEFAULT_LIB_DIR}" + [[ "$LIB_DIR" == */ ]] || LIB_DIR="${LIB_DIR}/" - if [[ ! -d "$LIB_DIR" ]]; then - err "Ordner nicht gefunden: ${LIB_DIR}" - exit 1 - fi - - declare -a LIB_ENTRY_DIRS=() - declare -a LIB_ENTRY_DESC=() - for entry_dir in "$LIB_DIR"/*/; do - [[ -d "$entry_dir" ]] || continue - meta="${entry_dir}meta.json" - [[ -f "$meta" ]] || continue - complete="$(json_field "$meta" complete)" - [[ "$complete" == "true" ]] || continue - et="$(json_field "$meta" title)" - ev="$(json_field "$meta" version)" - emode="$(json_field "$meta" mode)" - esize="$(json_field "$meta" size_bytes)" - esize_gb="?" - if [[ "$esize" =~ ^[0-9]+$ ]]; then - esize_gb="$(( esize / 1000 / 1000 / 1000 ))" - fi - mode_desc="bis Catalina, robust" - [[ "$emode" == "recovery" ]] && mode_desc="Big Sur+, ueber installer-pkg" - LIB_ENTRY_DIRS+=("$entry_dir") - LIB_ENTRY_DESC+=("${et} (${ev}) -- ~${esize_gb} GB, ${mode_desc}") - done - - if [[ ${#LIB_ENTRY_DIRS[@]} -eq 0 ]]; then + if ! fetch_library_list "$LIB_DIR"; then err "Keine vollstaendigen Installer in ${LIB_DIR} gefunden." exit 1 fi @@ -215,8 +423,11 @@ else echo echo "In der Bibliothek gefunden:" echo - for i in "${!LIB_ENTRY_DIRS[@]}"; do - printf " %s%2d)%s %s\n" "$BOLD" "$((i+1))" "$RESET" "${LIB_ENTRY_DESC[$i]}" + for i in "${!L_DIRS[@]}"; do + esize_gb="$(bytes_to_gb_ceil "${L_SIZES[$i]}")" + mode_desc="bis Catalina, robust" + [[ "${L_MODES[$i]}" == "recovery" ]] && mode_desc="Big Sur+, ueber installer-pkg" + printf " %s%2d)%s %s (%s) -- ~%s GB, %s\n" "$BOLD" "$((i+1))" "$RESET" "${L_TITLES[$i]}" "${L_VERSIONS[$i]}" "$esize_gb" "$mode_desc" done echo printf "%s0)%s Abbrechen\n" "$BOLD" "$RESET" @@ -227,134 +438,43 @@ else echo "Abgebrochen." exit 0 fi - if ! [[ "$LCHOICE" =~ ^[0-9]+$ ]] || (( LCHOICE < 1 || LCHOICE > ${#LIB_ENTRY_DIRS[@]} )); then + if ! [[ "$LCHOICE" =~ ^[0-9]+$ ]] || (( LCHOICE < 1 || LCHOICE > ${#L_DIRS[@]} )); then err "Ungueltige Auswahl." exit 1 fi - SEL_ENTRY_DIR="${LIB_ENTRY_DIRS[$((LCHOICE-1))]}" - SEL_META="${SEL_ENTRY_DIR}meta.json" - LIB_TITLE="$(json_field "$SEL_META" title)" - LIB_VERSION="$(json_field "$SEL_META" version)" - LIB_MODE="$(json_field "$SEL_META" mode)" - LIB_FILENAME="$(json_field "$SEL_META" filename)" - LIB_PAYLOAD_PATH="${SEL_ENTRY_DIR}${LIB_FILENAME}" - - if [[ ! -f "$LIB_PAYLOAD_PATH" ]]; then - err "Payload-Datei nicht gefunden: ${LIB_PAYLOAD_PATH}" - exit 1 - fi - - if [[ "$LIB_MODE" == "recovery" ]]; then - WRITE_MODE="cim" - else - WRITE_MODE="asr" - DMG_PATH="$LIB_PAYLOAD_PATH" - fi - + idx=$((LCHOICE-1)) + SEL_TITLE="${L_TITLES[$idx]}" + SEL_VERSION="${L_VERSIONS[$idx]}" + SEL_LIB_DIR="${L_DIRS[$idx]}" + SEL_LIB_MODE="${L_MODES[$idx]}" + SEL_LIB_FILENAME="${L_FILENAMES[$idx]}" echo - ok "Gewaehlt: ${LIB_TITLE} (${LIB_VERSION}) -- Quelle: lokale Bibliothek (${LIB_PAYLOAD_PATH})" + ok "Gewaehlt: ${SEL_TITLE} (${SEL_VERSION}) -- Quelle: lokale Bibliothek" fi # ---------- Schritt 2: Installer-Payload besorgen ---------- title "Schritt 2/4: Installer-Payload besorgen" -if [[ "$SOURCE" == "apple" ]]; then - 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 -elif [[ "$WRITE_MODE" == "cim" ]]; then - # Bibliothek, Big Sur+: InstallAssistant.pkg -> komplette Install-App - # nach /Applications entpacken (Apples Standardverhalten von 'installer'). - if [[ -d "$APP_PATH" ]]; then - ok "'${APP_PATH}' existiert bereits — Installer-Lauf wird uebersprungen." - else - echo "Entpacke ${LIB_PAYLOAD_PATH} nach /Applications (installer -pkg, braucht sudo)..." - echo "Das dauert je nach Groesse ein paar Minuten." - echo - sudo installer -pkg "$LIB_PAYLOAD_PATH" -target / - RC=$? - if [[ $RC -ne 0 ]]; then - err "'installer -pkg' fehlgeschlagen ($RC)." - exit 1 - fi - if [[ ! -d "$APP_PATH" ]]; then - # Fallback: Apples interner App-Name wich vom Bibliotheks-Titel ab -- - # nimm das zuletzt angelegte "Install macOS *.app"-Bundle. - FALLBACK_APP="$(ls -dt /Applications/Install\ macOS*.app 2>/dev/null | head -n1)" - [[ -n "$FALLBACK_APP" ]] && APP_PATH="$FALLBACK_APP" - fi - if [[ ! -d "$APP_PATH" ]]; then - err "Konnte die installierte App nach 'installer -pkg' nicht finden." - echo "Tipp: 'ls /Applications | grep Install' zeigt was tatsaechlich da ist." - exit 1 - fi - ok "Installiert: ${APP_PATH}" - fi -else - ok "Bibliothek liefert bereits ein fertiges, bootfaehiges Image (${DMG_PATH}) -- kein App-Download noetig." -fi - -if [[ "$WRITE_MODE" == "cim" ]]; then - CIM_BIN="${APP_PATH}/Contents/Resources/createinstallmedia" - if [[ ! -x "$CIM_BIN" ]]; then - err "createinstallmedia nicht gefunden unter: ${CIM_BIN}" - exit 1 - fi +if ! resolve_payload "$SOURCE" "$SEL_TITLE" "$SEL_VERSION" "$SEL_LIB_DIR" "$SEL_LIB_MODE" "$SEL_LIB_FILENAME"; then + exit 1 fi +WRITE_MODE="$RESOLVED_WRITE_MODE" +APP_PATH="$RESOLVED_APP_PATH" +DMG_PATH="$RESOLVED_DMG_PATH" # ---------- 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 +if ! list_external_disks; 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]}" +for i in "${!D_IDS[@]}"; do + printf " %s%2d)%s %s\n" "$BOLD" "$((i+1))" "$RESET" "${D_DESCS[$i]}" done echo printf "%s0)%s Abbrechen\n" "$BOLD" "$RESET" @@ -367,13 +487,13 @@ if [[ "$DCHOICE" == "0" || -z "$DCHOICE" ]]; then echo "Abgebrochen." exit 0 fi -if ! [[ "$DCHOICE" =~ ^[0-9]+$ ]] || (( DCHOICE < 1 || DCHOICE > ${#DISKS[@]} )); then +if ! [[ "$DCHOICE" =~ ^[0-9]+$ ]] || (( DCHOICE < 1 || DCHOICE > ${#D_IDS[@]} )); then err "Ungueltige Auswahl." exit 1 fi -TARGET_DISK="${DISKS[$((DCHOICE-1))]}" -TARGET_DESC="${DISK_DESC[$((DCHOICE-1))]}" +TARGET_DISK="${D_IDS[$((DCHOICE-1))]}" +TARGET_DESC="${D_DESCS[$((DCHOICE-1))]}" echo warn "Letzte Sicherheitsabfrage — dieser Stick wird geloescht:" @@ -389,6 +509,7 @@ fi title "Schritt 4/4: Stick wird erstellt" if [[ "$WRITE_MODE" == "cim" ]]; then + CIM_BIN="${APP_PATH}/Contents/Resources/createinstallmedia" echo "App: ${APP_PATH}" echo "Ziel: /dev/${TARGET_DISK}" echo @@ -415,12 +536,6 @@ if [[ "$WRITE_MODE" == "cim" ]]; then ok "Geloescht: ${APP_PATH}" fi else - # asr-Pfad: BaseSystem.dmg/InstallESD.dmg ist schon ein fertiges, - # bootfaehiges Volume. Erst Stick neu formatieren (liefert einen - # Mountpunkt), dann Apples 'asr restore' das eigentliche Image drauf - # kopieren lassen -- das re-formatiert dabei nochmal komplett nach dem - # Schema des Quell-Images (--erase). Etablierte Community-Technik fuer - # macOS-Installer bis Catalina ohne createinstallmedia. echo "Quelle: ${DMG_PATH}" echo "Ziel: /dev/${TARGET_DISK}" echo @@ -458,3 +573,351 @@ fi echo ok "Alles erledigt." +exit 0 + +fi + +# ====================================================================== +# ================ MODUS 2: MULTIBOOT-STICK ========================== +# ====================================================================== + +MAX_MULTIBOOT=8 + +title "Multiboot-Stick — Schritt 1/5: Versionen auswaehlen" + +echo "Baue die Auswahlliste (Apple-Server + lokale Bibliothek)..." +echo + +HAVE_APPLE=0 +if command -v softwareupdate >/dev/null 2>&1 && fetch_apple_list; then + HAVE_APPLE=1 +fi + +HAVE_LIB=0 +echo +read -r -p "Pfad zur lokalen Downloads-Bibliothek pruefen [Enter fuer ${DEFAULT_LIB_DIR}, '-' zum Ueberspringen]: " LIB_DIR_IN +if [[ "$LIB_DIR_IN" != "-" ]]; then + LIB_DIR="${LIB_DIR_IN:-$DEFAULT_LIB_DIR}" + [[ "$LIB_DIR" == */ ]] || LIB_DIR="${LIB_DIR}/" + if fetch_library_list "$LIB_DIR"; then + HAVE_LIB=1 + fi +fi + +if [[ $HAVE_APPLE -eq 0 && $HAVE_LIB -eq 0 ]]; then + err "Weder Apple-Server noch lokale Bibliothek haben Installer geliefert. Abbruch." + exit 1 +fi + +# Kombinierte Liste aufbauen: C_SRC/C_TITLE/C_VERSION/C_SIZE/C_LIBIDX +C_SRC=() +C_TITLE=() +C_VERSION=() +C_SIZE=() +C_LIBIDX=() + +if [[ $HAVE_APPLE -eq 1 ]]; then + for i in "${!A_TITLES[@]}"; do + C_SRC+=("apple") + C_TITLE+=("${A_TITLES[$i]}") + C_VERSION+=("${A_VERSIONS[$i]}") + C_SIZE+=("${A_SIZES[$i]}") + C_LIBIDX+=("-1") + done +fi +if [[ $HAVE_LIB -eq 1 ]]; then + for i in "${!L_DIRS[@]}"; do + C_SRC+=("library") + C_TITLE+=("${L_TITLES[$i]}") + C_VERSION+=("${L_VERSIONS[$i]}") + C_SIZE+=("${L_SIZES[$i]}") + C_LIBIDX+=("$i") + done +fi + +echo +echo "Verfuegbare Versionen (Mehrfachauswahl):" +echo +for i in "${!C_TITLE[@]}"; do + tag="[Apple, Download]" + mode_desc="" + if [[ "${C_SRC[$i]}" == "library" ]]; then + tag="[Bibliothek, offline]" + lidx="${C_LIBIDX[$i]}" + mode_desc=" -- bis Catalina, robust" + [[ "${L_MODES[$lidx]}" == "recovery" ]] && mode_desc=" -- Big Sur+, ueber installer-pkg" + fi + sgb="$(bytes_to_gb_ceil "${C_SIZE[$i]}")" + printf " %s%2d)%s %-22s %s (%s) -- ~%s GB%s\n" "$BOLD" "$((i+1))" "$RESET" "$tag" "${C_TITLE[$i]}" "${C_VERSION[$i]}" "$sgb" "$mode_desc" +done +echo +printf "%s0)%s Abbrechen\n" "$BOLD" "$RESET" +echo +echo "Mehrere Nummern durch Komma trennen, z.B. 1,3,4 (mind. 2, max. ${MAX_MULTIBOOT})." +read -r -p "Welche Versionen fuer den Multiboot-Stick? " MCHOICE_RAW + +if [[ "$MCHOICE_RAW" == "0" || -z "$MCHOICE_RAW" ]]; then + echo "Abgebrochen." + exit 0 +fi + +IFS=',' read -r -a MCHOICE_PARTS <<<"$MCHOICE_RAW" +SEL_IDX=() +for part in "${MCHOICE_PARTS[@]}"; do + part="$(echo "$part" | tr -d '[:space:]')" + [[ -z "$part" ]] && continue + if ! [[ "$part" =~ ^[0-9]+$ ]] || (( part < 1 || part > ${#C_TITLE[@]} )); then + err "Ungueltige Nummer: '${part}'." + exit 1 + fi + SEL_IDX+=("$((part-1))") +done + +if (( ${#SEL_IDX[@]} < 2 )); then + err "Fuer nur eine Version reicht der Einzel-Modus (Option 1 im Startmenue)." + exit 1 +fi +if (( ${#SEL_IDX[@]} > MAX_MULTIBOOT )); then + err "Zu viele Versionen gewaehlt (${#SEL_IDX[@]}), Maximum ist ${MAX_MULTIBOOT}." + exit 1 +fi + +echo +ok "Gewaehlt: ${#SEL_IDX[@]} Versionen fuer den Multiboot-Stick." + +# ---------- Schritt 2/5: Payload je Version besorgen ---------- +title "Schritt 2/5: Installer-Payloads besorgen" + +M_WRITE_MODE=() +M_APP_PATH=() +M_DMG_PATH=() +M_SIZE_BYTES=() +M_TITLE=() +M_LABEL=() + +n=1 +for ci in "${SEL_IDX[@]}"; do + t="${C_TITLE[$ci]}" + v="${C_VERSION[$ci]}" + src="${C_SRC[$ci]}" + echo + echo "${BOLD}--- (${n}/${#SEL_IDX[@]}) ${t} (${v}) ---${RESET}" + + lib_dir="" lib_mode="" lib_fn="" + if [[ "$src" == "library" ]]; then + lidx="${C_LIBIDX[$ci]}" + lib_dir="${L_DIRS[$lidx]}" + lib_mode="${L_MODES[$lidx]}" + lib_fn="${L_FILENAMES[$lidx]}" + fi + + if ! resolve_payload "$src" "$t" "$v" "$lib_dir" "$lib_mode" "$lib_fn"; then + err "Konnte Payload fuer '${t}' nicht beschaffen — Auswahl wird komplett abgebrochen (noch nichts am Stick veraendert)." + exit 1 + fi + + M_WRITE_MODE+=("$RESOLVED_WRITE_MODE") + M_APP_PATH+=("$RESOLVED_APP_PATH") + M_DMG_PATH+=("$RESOLVED_DMG_PATH") + M_SIZE_BYTES+=("$RESOLVED_SIZE_BYTES") + M_TITLE+=("$t") + M_LABEL+=("$(make_label "$n" "$t")") + n=$((n+1)) +done + +# ---------- Schritt 3/5: Ziel-USB-Stick waehlen ---------- +title "Schritt 3/5: USB-Stick auswaehlen" + +echo "Externe, physische Laufwerke (nur ganze Platten, keine Partitionen):" +echo +if ! list_external_disks; then + err "Keine externen physischen Laufwerke gefunden. Stick eingesteckt?" + exit 1 +fi + +for i in "${!D_IDS[@]}"; do + printf " %s%2d)%s %s\n" "$BOLD" "$((i+1))" "$RESET" "${D_DESCS[$i]}" +done +echo +printf "%s0)%s Abbrechen\n" "$BOLD" "$RESET" +echo +warn "ACHTUNG: Der gewaehlte Stick wird KOMPLETT GELOESCHT (alle Partitionen)." +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 > ${#D_IDS[@]} )); then + err "Ungueltige Auswahl." + exit 1 +fi + +TARGET_DISK="${D_IDS[$((DCHOICE-1))]}" +TARGET_DESC="${D_DESCS[$((DCHOICE-1))]}" +TARGET_SIZE_BYTES="${D_SIZE_BYTES[$((DCHOICE-1))]}" + +# ---------- Groessen-Check ---------- +# Puffer je Partition: cim braucht etwas Kopfraum ueber der reinen App- +# Groesse (createinstallmedia kopiert BaseSystem etc. dazu), asr braucht +# kaum mehr als die Quellgroesse. Zusaetzlich Mindestgroessen, damit +# kleine/kaputte Groessenangaben nicht zu einer zu kleinen Partition fuehren. +TOTAL_NEEDED=0 +declare -a PART_SIZE_BYTES=() +for i in "${!M_TITLE[@]}"; do + raw="${M_SIZE_BYTES[$i]}" + [[ "$raw" =~ ^[0-9]+$ ]] || raw=0 + if [[ "${M_WRITE_MODE[$i]}" == "cim" ]]; then + need=$(( raw + raw / 3 )) # +33% Puffer + min=$(( 14 * 1000 * 1000 * 1000 )) + else + need=$(( raw + raw / 20 )) # +5% Puffer + min=$(( 6 * 1000 * 1000 * 1000 )) + fi + (( need < min )) && need=$min + PART_SIZE_BYTES+=("$need") + TOTAL_NEEDED=$(( TOTAL_NEEDED + need )) +done + +# ~1GB Reserve fuer EFI-Partition + GPT-Tabellen. +GPT_OVERHEAD=$(( 1 * 1000 * 1000 * 1000 )) +TOTAL_NEEDED_WITH_OVERHEAD=$(( TOTAL_NEEDED + GPT_OVERHEAD )) + +echo +echo "Benoetigter Platz (mit Puffer):" +for i in "${!M_TITLE[@]}"; do + printf " - %-30s ~%s GB (%s)\n" "${M_TITLE[$i]}" "$(bytes_to_gb_ceil "${PART_SIZE_BYTES[$i]}")" "${M_WRITE_MODE[$i]}" +done +echo " Gesamt (inkl. EFI-Reserve): ~$(bytes_to_gb_ceil "$TOTAL_NEEDED_WITH_OVERHEAD") GB" +echo " Stick-Kapazitaet: ~$(bytes_to_gb_ceil "$TARGET_SIZE_BYTES") GB" +echo + +if (( TOTAL_NEEDED_WITH_OVERHEAD > TARGET_SIZE_BYTES )); then + err "Der gewaehlte Stick ist zu klein fuer diese Auswahl. Kleinere/weniger Versionen waehlen oder groesseren Stick nehmen." + echo "Es wurde NICHTS am Stick veraendert." + exit 1 +fi + +echo +warn "Letzte Sicherheitsabfrage — dieser Stick wird GELOESCHT und in ${#M_TITLE[@]} Partitionen aufgeteilt:" +echo " ${TARGET_DESC}" +for i in "${!M_TITLE[@]}"; do + echo " -> ${M_LABEL[$i]} (${M_TITLE[$i]})" +done +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/5: Partitionieren ---------- +title "Schritt 4/5: Stick wird partitioniert" + +# diskutil partitionDisk <device> <#Partitionen> GPT [Format Name Groesse] ... +# Letzte Partition bekommt 'R' (restlicher Platz) statt einer festen +# Groesse -- vermeidet Rundungsverluste und nutzt automatisch den vollen +# verbleibenden Stick fuer die letzte gewaehlte Version. +PART_ARGS=() +n_parts=${#M_TITLE[@]} +for i in "${!M_TITLE[@]}"; do + if (( i == n_parts - 1 )); then + PART_ARGS+=("JHFS+" "${M_LABEL[$i]}" "R") + else + gb="$(bytes_to_gb_ceil "${PART_SIZE_BYTES[$i]}")" + PART_ARGS+=("JHFS+" "${M_LABEL[$i]}" "${gb}G") + fi +done + +echo "Partitioniere /dev/${TARGET_DISK} in ${n_parts} Partitionen (braucht sudo)..." +echo +sudo diskutil partitionDisk "/dev/${TARGET_DISK}" "${n_parts}" GPT "${PART_ARGS[@]}" +if [[ $? -ne 0 ]]; then + err "diskutil partitionDisk fehlgeschlagen — siehe Ausgabe oben." + exit 1 +fi +ok "Partitionierung abgeschlossen." + +# ---------- Schritt 5/5: Jede Partition beschreiben ---------- +title "Schritt 5/5: Partitionen werden beschrieben" + +FAILED_TITLES=() +OK_TITLES=() + +for i in "${!M_TITLE[@]}"; do + t="${M_TITLE[$i]}" + label="${M_LABEL[$i]}" + vol="/Volumes/${label}" + + echo + echo "${BOLD}--- $((i+1))/${n_parts}: ${t} -> ${label} ---${RESET}" + + if [[ ! -d "$vol" ]]; then + err "Erwartetes Volume ${vol} nicht gefunden — ueberspringe ${t}." + FAILED_TITLES+=("$t") + continue + fi + + if [[ "${M_WRITE_MODE[$i]}" == "cim" ]]; then + cim_bin="${M_APP_PATH[$i]}/Contents/Resources/createinstallmedia" + echo "createinstallmedia laeuft gegen ${vol} (braucht sudo, kann 10-30 Minuten dauern)..." + sudo "$cim_bin" --volume "$vol" --nointeraction + rc=$? + else + echo "asr restore laeuft gegen ${vol} (braucht sudo, kann 10-20 Minuten dauern)..." + sudo asr restore --source "${M_DMG_PATH[$i]}" --target "$vol" --erase --noprompt + rc=$? + fi + + if [[ $rc -eq 0 ]]; then + ok "${t}: fertig." + OK_TITLES+=("$t") + else + err "${t}: fehlgeschlagen (Code ${rc})." + FAILED_TITLES+=("$t") + echo + read -r -p "Trotzdem mit den restlichen Versionen fortfahren? [j/N]: " CONT + if [[ ! "$CONT" =~ ^[jJ]$ ]]; then + err "Abgebrochen — restliche Partitionen wurden NICHT beschrieben." + break + fi + fi +done + +echo +hr +if (( ${#OK_TITLES[@]} > 0 )); then + ok "Erfolgreich geschrieben: ${OK_TITLES[*]}" +fi +if (( ${#FAILED_TITLES[@]} > 0 )); then + err "Fehlgeschlagen/uebersprungen: ${FAILED_TITLES[*]}" +fi +echo +echo "Am Ziel-Mac: beim Start Option/Alt gedrueckt halten — alle erfolgreich" +echo "geschriebenen Versionen erscheinen einzeln im Boot-Picker." + +# Aufraeumen: heruntergeladene/entpackte Apps (mehrere GB je Version). +CLEAN_APPS=() +for i in "${!M_TITLE[@]}"; do + [[ "${M_WRITE_MODE[$i]}" == "cim" ]] || continue + [[ -n "${M_APP_PATH[$i]}" ]] || continue + CLEAN_APPS+=("${M_APP_PATH[$i]}") +done +if (( ${#CLEAN_APPS[@]} > 0 )); then + echo + read -r -p "Heruntergeladene/entpackte Installer-Apps (${#CLEAN_APPS[@]} Stueck, mehrere GB) jetzt loeschen? [j/N]: " CLEAN + if [[ "$CLEAN" =~ ^[jJ]$ ]]; then + for app in "${CLEAN_APPS[@]}"; do + sudo rm -rf "$app" + done + ok "Aufgeraeumt." + fi +fi + +echo +if (( ${#FAILED_TITLES[@]} > 0 )); then + exit 1 +fi +ok "Alles erledigt." +exit 0