From 0d0c137de365eba369c6df40a875fe6164ad34fb Mon Sep 17 00:00:00 2001 From: ARIA Date: Sat, 18 Jul 2026 12:04:03 +0200 Subject: [PATCH] native-macos: Downloads-Bibliothek des Docker-Tools als zweite Quelle nutzbar machen - Neuer Auswahlschritt: Apple-Server (Download) vs. lokale downloads/library (Docker-Tool) -- letzteres komplett offline nutzbar. - InstallAssistant.pkg (Big Sur+) wird per installer -pkg -target / zur vollstaendigen Install-App entpackt, danach normaler createinstallmedia-Weg. - BaseSystem.dmg/InstallESD.dmg (bis Catalina) ist bereits ein fertiges bootfaehiges Volume -- kein createinstallmedia noetig, stattdessen diskutil eraseDisk + asr restore --erase (etablierte Community-Technik). - Flat-JSON-Parser (sed) fuer meta.json ohne jq/python3-Abhaengigkeit. - Auswahl-/Verzweigungslogik unter Linux mit simulierter Bibliothek end-to-end durchgetestet (beide Modi laufen korrekt bis zum jeweils ersten macOS-only-Befehl). --- native-macos/README.md | 124 ++++++---- native-macos/create-stick.sh | 452 ++++++++++++++++++++++++++--------- 2 files changed, 415 insertions(+), 161 deletions(-) diff --git a/native-macos/README.md b/native-macos/README.md index 29f22b0..1d03b95 100644 --- a/native-macos/README.md +++ b/native-macos/README.md @@ -1,34 +1,57 @@ -# Nativer macOS-Weg (createinstallmedia) +# Nativer macOS-Weg (createinstallmedia / asr) -Alternative zum Docker/Linux-Tool im Projekt-Root — **nur nutzbar, wenn -gerade ein echter Mac zur Verfuegung steht** (macOS 10.15 Catalina oder -neuer). Dafuer bekommst du hierueber den offiziell von Apple sauber -funktionierenden Weg: die echte `createinstallmedia`, statt des -Recovery-Extraktions-Workarounds fuer den Big-Sur+-Pfad im Docker-Tool. +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: -## Warum ein eigenes Script, warum nicht die `downloads/`-Bibliothek wiederverwenden? +1. **Von Apple herunterladen** (macOS 10.15 Catalina oder neuer als Host + noetig) — der offiziell von Apple unterstuetzte Weg ueber + `softwareupdate --fetch-full-installer`. +2. **Aus der lokalen Downloads-Bibliothek des Docker-Tools** — wenn das + ganze Projekt inkl. `downloads/`-Ordner z.B. auf eine externe Platte + kopiert wurde, braucht dieser Weg **kein Internet und keinen erneuten + Download**. -Kurz: **andere Bildformate, nicht kompatibel.** +## Warum ein eigenes Script, warum nicht einfach dd wie das Docker-Tool? -- Das Docker-Tool laedt rohe Katalog-Images (`BaseSystem.dmg` bzw. - `InstallAssistant.pkg`) und schreibt sie per `dd` direkt auf den Stick. - Funktioniert ohne Mac, aber `createinstallmedia` kann mit diesen rohen - Dateien nichts anfangen — es braucht die komplette, fertig gepackte - **"Install macOS X.app"**. -- Der offizielle, von Apple unterstuetzte Weg an genau diese App zu kommen - (auch ohne App-Store-Anmeldung) ist der macOS-Bordbefehl - `softwareupdate --fetch-full-installer`. Das gibt es nur unter echtem - macOS ab Catalina. +Kurz: **`createinstallmedia` (der offiziell unterstuetzte macOS-Weg) +braucht die vollstaendige, fertig gepackte "Install macOS X.app"** — +nicht die rohen Katalog-Dateien, die das Docker-Tool per `dd` schreibt. +Deshalb dieser separate, macOS-native Pfad. -Deshalb ist dieses Script komplett unabhaengig vom `./downloads`-Ordner -des Docker-Tools. Es laedt sich seine App-Kopie selbst, direkt von Apple. +## Wie die Bibliotheks-Wiederverwendung funktioniert + +Die `downloads/library/`-Bibliothek des Docker-Tools enthaelt pro +macOS-Version genau eine von zwei Rohdateien (siehe `meta.json` im +jeweiligen Unterordner, Feld `mode`): + +- **`InstallAssistant.pkg`** (Big Sur und neuer, `mode: "recovery"`): + Apples eigenes `installer -pkg ... -target /` entpackt daraus die + komplette Install-App (inkl. `createinstallmedia`-Binary) nach + `/Applications` — bestaetigt per Apple-Community-Thread, dass ein + `.pkg`-Installer aus dem Software-Update-Katalog 1:1 dasselbe Paket + ist wie der App-Store-Download. Danach laeuft der normale + `createinstallmedia`-Weg wie bei der Apple-Quelle. +- **`BaseSystem.dmg`/`InstallESD.dmg`** (bis Catalina, `mode: + "legacy_dmg"`): das ist bereits ein fertiges, bootfaehiges Volume. + `createinstallmedia` braucht es gar nicht erst — Apples eigenes + Bordmittel `asr restore --source ... --target ... --erase --noprompt` + schreibt es direkt und sauber auf den Stick (etablierte Technik, siehe + Quellen unten). Kein App-Download, kein `createinstallmedia` noetig. + +In beiden Faellen: **kein zweiter Download**, die Datei aus der Bibliothek +wird 1:1 weiterverwendet. ## Zukunftssicherheit -`softwareupdate --list-full-installers` fragt Apples eigene Server live ab -— genau wie der Katalog-Check im Docker-Tool, nur eben Apples eigenes -Tool statt eigenem Parser. Neue macOS-Versionen tauchen automatisch in der -Liste auf, ohne dass hier Code angepasst werden muss. +Bei der Apple-Quelle fragt `softwareupdate --list-full-installers` +Apples eigene Server live ab — genau wie der Katalog-Check im Docker-Tool, +nur eben Apples eigenes Tool statt eigenem Parser. Neue macOS-Versionen +tauchen automatisch in der Liste auf. + +Bei der Bibliotheks-Quelle haengt die Auswahl logischerweise davon ab, was +das Docker-Tool zuletzt heruntergeladen hat — dafuer aber komplett ohne +Internetzugriff nutzbar. ## Nutzung @@ -39,32 +62,45 @@ chmod +x create-stick.sh # falls das Ausfuehr-Bit beim Kopieren verloren ging ``` Fuehrt durch vier Schritte (reines Terminal-Menue, keine externen -Abhaengigkeiten — nur `softwareupdate`, `diskutil`, `PlistBuddy`, -`createinstallmedia`, alles Bordmittel): +Abhaengigkeiten — nur `softwareupdate`, `diskutil`, `PlistBuddy`, +`installer`, `asr`, `createinstallmedia`, alles Bordmittel): -1. **Versionsliste** von Apple abrufen, Version auswaehlen. -2. **Download** der vollen Installer-App via `softwareupdate - --fetch-full-installer` (wird uebersprungen, wenn die App schon unter - `/Applications` liegt). -3. **USB-Stick auswaehlen** — Liste nur externer, physischer Laufwerke +1. **Quelle + Version waehlen** — Apple-Server oder lokale Bibliothek + (Pfad wird abgefragt, Default rät `../downloads/library` relativ zum + Script — passt, wenn das komplette Projekt kopiert wurde). +2. **Installer-Payload besorgen** — je nach Quelle/Modus: Download via + `softwareupdate`, Entpacken via `installer -pkg`, oder (legacy_dmg + aus der Bibliothek) gar nichts, das Image liegt schon bereit. +3. **USB-Stick auswaehlen** — Liste nur externer, physischer Laufwerke (`diskutil list -plist external physical`), danach Sicherheitsabfrage: exakter Geraetepfad muss nochmal eingetippt werden, bevor irgendwas geloescht wird. -4. **`createinstallmedia --nointeraction`** laeuft mit `sudo`, live-Ausgabe - im Terminal. Am Ende optional die (mehrere GB grosse) Installer-App - wieder loeschen. +4. **Schreiben** — je nach Modus entweder `createinstallmedia + --nointeraction` (Apple-Quelle und Bibliothek/Big-Sur+) oder + `diskutil eraseDisk` + `asr restore --erase` (Bibliothek, + bis Catalina). Beides mit `sudo`, live-Ausgabe im Terminal. ## Grenzen - Nur macOS 10.15+ (Catalina oder neuer) als **Host**, auf dem das Script - laeuft — das Zielsystem, auf das installiert wird, kann trotzdem - jede von Apple unterstuetzte Version sein. -- Kein Multiboot-Support (anders als das Docker-Tool) — - `createinstallmedia` erstellt immer einen kompletten Single-Version-Stick. - Waere technisch nachruestbar (mehrere Partitionen, mehrere - `createinstallmedia`-Laeufe mit `--volume` auf je eine Partition), aber - aktuell nicht gebaut. -- Downloads landen unter `/Applications` (Apples Standardverhalten von - `softwareupdate`), nicht im Projektordner — dieser Zweig ist bewusst - nicht auf Portabilitaet/externe Platte ausgelegt wie das Docker-Tool, - sondern auf den einfachsten Weg wenn gerade ein Mac da ist. + laeuft — das Zielsystem, auf das installiert wird, kann trotzdem + 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. + +## Quellen (fuer den asr-Weg, live nachgeschlagen) + +- Apple Developer/Community — `asr restore` ist das Bordmittel fuer + Block-Level-Restores von Disk Images, seit jeher Teil von macOS. +- Cody Krieger — "Creating bootable macOS Sierra installation media on + macOS Catalina": beschreibt exakt den `diskutil eraseDisk` + + `asr restore --erase`-Ablauf fuer aeltere macOS-Installer. diff --git a/native-macos/create-stick.sh b/native-macos/create-stick.sh index b642a1e..7a6b6e9 100755 --- a/native-macos/create-stick.sh +++ b/native-macos/create-stick.sh @@ -1,25 +1,37 @@ #!/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, +# Installer-Sticks. Laeuft NUR auf echtem macOS. Nutzt ausschliesslich +# Bordmittel: softwareupdate, diskutil, PlistBuddy, installer, asr, # 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. +# Zwei moegliche QUELLEN fuer den Installer-Payload: # -# Zukunftssicherheit: `softwareupdate --list-full-installers` fragt -# Apples eigene Server live ab. Neue macOS-Versionen tauchen automatisch -# auf, ohne dass dieses Script angefasst werden muss. +# 1) Apple-Server (Download) +# softwareupdate --fetch-full-installer laedt die komplette +# "Install macOS X.app" direkt von Apple. Danach normaler +# createinstallmedia-Weg. Zukunftssicher: die Versionsliste kommt +# live von Apples eigenen Servern, neue macOS-Versionen tauchen +# automatisch auf. +# +# 2) Lokale Downloads-Bibliothek des Docker/Linux-Tools +# (../downloads/library relativ zu diesem Script, wenn das ganze +# Projekt inkl. Downloads z.B. per externer Platte auf den Mac +# kopiert wurde). Kein Internet noetig, kein erneuter Download. +# Zwei Payload-Formate stecken dort je nach macOS-Version, beide +# werden hier unterstuetzt: +# - InstallAssistant.pkg (Big Sur+): 'installer -pkg ... -target /' +# entpackt daraus die komplette Install-App (inkl. +# createinstallmedia-Binary) nach /Applications -- danach der +# normale createinstallmedia-Weg wie bei Quelle 1. +# - BaseSystem.dmg/InstallESD.dmg (bis Catalina): das ist schon +# ein fertiges, bootfaehiges Volume. createinstallmedia braucht +# es nicht extra -- Apples eigenes 'asr restore' schreibt es +# direkt auf den Stick. Kein App-Download noetig. +# +# 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. set -u @@ -37,112 +49,276 @@ 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"; } +# Sehr simpler Flat-JSON-Feld-Extraktor -- reicht fuer die meta.json-Dateien +# der Bibliothek (keine verschachtelten Objekte), spart eine Abhaengigkeit +# zu jq/python3 die auf einem frischen Mac nicht garantiert vorhanden ist. +json_field() { + local file="$1" key="$2" + sed -n 's/.*"'"$key"'":[[:space:]]*"\{0,1\}\([^",}]*\)"\{0,1\}.*/\1/p' "$file" | head -n1 +} + # ---------- 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 +# ---------- Schritt 1: Quelle + Version waehlen ---------- +title "macOS USB-Stick-Creator (nativ) — Schritt 1/4: Quelle + Version" -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 "Woher soll der macOS-Installer kommen?" 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 " ${BOLD}1)${RESET} Von Apple herunterladen (braucht Internet, mehrere GB)" +echo " ${BOLD}2)${RESET} Aus lokaler Downloads-Bibliothek (Docker-Tool) verwenden -- kein Internet noetig" echo printf "%s0)%s Abbrechen\n" "$BOLD" "$RESET" echo -read -r -p "Welche Version soll heruntergeladen/benutzt werden? [Nummer]: " CHOICE +read -r -p "Quelle waehlen [Nummer]: " SRC_CHOICE -if [[ "$CHOICE" == "0" || -z "$CHOICE" ]]; then +if [[ "$SRC_CHOICE" == "0" || -z "$SRC_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" +SOURCE="" +WRITE_MODE="" # "cim" (createinstallmedia) oder "asr" (direktes Image-Restore) +APP_PATH="" +DMG_PATH="" +LIB_TITLE="" +LIB_VERSION="" -echo -ok "Gewaehlt: ${SEL_TITLE} (${SEL_VERSION})" +case "$SRC_CHOICE" in + 1) + SOURCE="apple" + ;; + 2) + SOURCE="library" + ;; + *) + err "Ungueltige Auswahl." + exit 1 + ;; +esac -# ---------- Schritt 2: Download (falls noetig) ---------- -title "Schritt 2/4: Installer-App besorgen" +if [[ "$SOURCE" == "apple" ]]; then + if ! command -v softwareupdate >/dev/null 2>&1; then + err "'softwareupdate' nicht gefunden — ungewoehnliches macOS-Setup, Abbruch." + exit 1 + fi -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 + 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 + + echo + 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 - 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." + if ! [[ "$CHOICE" =~ ^[0-9]+$ ]] || (( CHOICE < 1 || CHOICE > ${#TITLES[@]} )); then + err "Ungueltige Auswahl." exit 1 fi - ok "Download abgeschlossen: ${APP_PATH}" + + 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" + + 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}" + + 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 + err "Keine vollstaendigen Installer in ${LIB_DIR} gefunden." + exit 1 + fi + + 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]}" + done + echo + printf "%s0)%s Abbrechen\n" "$BOLD" "$RESET" + echo + read -r -p "Welche Version verwenden? [Nummer]: " LCHOICE + + if [[ "$LCHOICE" == "0" || -z "$LCHOICE" ]]; then + echo "Abgebrochen." + exit 0 + fi + if ! [[ "$LCHOICE" =~ ^[0-9]+$ ]] || (( LCHOICE < 1 || LCHOICE > ${#LIB_ENTRY_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 + + echo + ok "Gewaehlt: ${LIB_TITLE} (${LIB_VERSION}) -- Quelle: lokale Bibliothek (${LIB_PAYLOAD_PATH})" fi -CIM_BIN="${APP_PATH}/Contents/Resources/createinstallmedia" -if [[ ! -x "$CIM_BIN" ]]; then - err "createinstallmedia nicht gefunden unter: ${CIM_BIN}" - exit 1 +# ---------- 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 fi # ---------- Schritt 3: Ziel-USB-Stick waehlen ---------- @@ -209,33 +385,75 @@ if [[ "$CONFIRM" != "/dev/${TARGET_DISK}" ]]; then exit 1 fi -# ---------- Schritt 4: createinstallmedia ausfuehren ---------- +# ---------- Schritt 4: Stick schreiben ---------- 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 +if [[ "$WRITE_MODE" == "cim" ]]; then + 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=$? + 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." + 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 installierte App (mehrere GB) jetzt wieder loeschen um Platz zu sparen? [j/N]: " CLEAN + if [[ "$CLEAN" =~ ^[jJ]$ ]]; then + sudo rm -rf "$APP_PATH" + ok "Geloescht: ${APP_PATH}" + fi else - err "createinstallmedia ist mit Fehlercode ${RC} beendet — siehe Ausgabe oben." - exit $RC -fi + # 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 + echo "Formatiere /dev/${TARGET_DISK} neu (HFS+) als Vorbereitung (braucht sudo)..." + sudo diskutil eraseDisk JHFS+ INSTALL "/dev/${TARGET_DISK}" + if [[ $? -ne 0 ]]; then + err "diskutil eraseDisk fehlgeschlagen." + exit 1 + 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}" + VOL_MOUNT="/Volumes/INSTALL" + if [[ ! -d "$VOL_MOUNT" ]]; then + err "Erwarteter Mountpunkt ${VOL_MOUNT} nicht gefunden nach dem Formatieren." + echo "Tipp: 'diskutil list /dev/${TARGET_DISK}' zeigt den tatsaechlichen Zustand." + exit 1 + fi + + echo + echo "asr restore laeuft jetzt (braucht sudo, dauert typischerweise 10-20 Minuten)..." + echo "Der Stick wird dabei nochmal komplett neu formatiert -- das ist normal." + echo + + sudo asr restore --source "$DMG_PATH" --target "$VOL_MOUNT" --erase --noprompt + 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 "asr restore ist mit Fehlercode ${RC} beendet — siehe Ausgabe oben." + exit $RC + fi fi echo