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).
This commit is contained in:
ARIA
2026-07-18 12:04:03 +02:00
parent e4dd003bd6
commit 0d0c137de3
2 changed files with 415 additions and 161 deletions
+80 -44
View File
@@ -1,34 +1,57 @@
# Nativer macOS-Weg (createinstallmedia) # Nativer macOS-Weg (createinstallmedia / asr)
Alternative zum Docker/Linux-Tool im Projekt-Root — **nur nutzbar, wenn Alternative zum Docker/Linux-Tool im Projekt-Root **nur nutzbar, wenn
gerade ein echter Mac zur Verfuegung steht** (macOS 10.15 Catalina oder gerade ein echter Mac zur Verfuegung steht**. Zwei Quellen fuer den
neuer). Dafuer bekommst du hierueber den offiziell von Apple sauber Installer-Payload stehen zur Auswahl, direkt im Script:
funktionierenden Weg: die echte `createinstallmedia`, statt des
Recovery-Extraktions-Workarounds fuer den Big-Sur+-Pfad im Docker-Tool.
## 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. Kurz: **`createinstallmedia` (der offiziell unterstuetzte macOS-Weg)
`InstallAssistant.pkg`) und schreibt sie per `dd` direkt auf den Stick. braucht die vollstaendige, fertig gepackte "Install macOS X.app"** —
Funktioniert ohne Mac, aber `createinstallmedia` kann mit diesen rohen nicht die rohen Katalog-Dateien, die das Docker-Tool per `dd` schreibt.
Dateien nichts anfangen — es braucht die komplette, fertig gepackte Deshalb dieser separate, macOS-native Pfad.
**"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.
Deshalb ist dieses Script komplett unabhaengig vom `./downloads`-Ordner ## Wie die Bibliotheks-Wiederverwendung funktioniert
des Docker-Tools. Es laedt sich seine App-Kopie selbst, direkt von Apple.
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 ## Zukunftssicherheit
`softwareupdate --list-full-installers` fragt Apples eigene Server live ab Bei der Apple-Quelle fragt `softwareupdate --list-full-installers`
— genau wie der Katalog-Check im Docker-Tool, nur eben Apples eigenes Apples eigene Server live ab — genau wie der Katalog-Check im Docker-Tool,
Tool statt eigenem Parser. Neue macOS-Versionen tauchen automatisch in der nur eben Apples eigenes Tool statt eigenem Parser. Neue macOS-Versionen
Liste auf, ohne dass hier Code angepasst werden muss. 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 ## 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 Fuehrt durch vier Schritte (reines Terminal-Menue, keine externen
Abhaengigkeiten — nur `softwareupdate`, `diskutil`, `PlistBuddy`, Abhaengigkeiten nur `softwareupdate`, `diskutil`, `PlistBuddy`,
`createinstallmedia`, alles Bordmittel): `installer`, `asr`, `createinstallmedia`, alles Bordmittel):
1. **Versionsliste** von Apple abrufen, Version auswaehlen. 1. **Quelle + Version waehlen** — Apple-Server oder lokale Bibliothek
2. **Download** der vollen Installer-App via `softwareupdate (Pfad wird abgefragt, Default rät `../downloads/library` relativ zum
--fetch-full-installer` (wird uebersprungen, wenn die App schon unter Script — passt, wenn das komplette Projekt kopiert wurde).
`/Applications` liegt). 2. **Installer-Payload besorgen** — je nach Quelle/Modus: Download via
3. **USB-Stick auswaehlen** — Liste nur externer, physischer Laufwerke `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: (`diskutil list -plist external physical`), danach Sicherheitsabfrage:
exakter Geraetepfad muss nochmal eingetippt werden, bevor irgendwas exakter Geraetepfad muss nochmal eingetippt werden, bevor irgendwas
geloescht wird. geloescht wird.
4. **`createinstallmedia --nointeraction`** laeuft mit `sudo`, live-Ausgabe 4. **Schreiben** — je nach Modus entweder `createinstallmedia
im Terminal. Am Ende optional die (mehrere GB grosse) Installer-App --nointeraction` (Apple-Quelle und Bibliothek/Big-Sur+) oder
wieder loeschen. `diskutil eraseDisk` + `asr restore --erase` (Bibliothek,
bis Catalina). Beides mit `sudo`, live-Ausgabe im Terminal.
## Grenzen ## Grenzen
- Nur macOS 10.15+ (Catalina oder neuer) als **Host**, auf dem das Script - Nur macOS 10.15+ (Catalina oder neuer) als **Host**, auf dem das Script
laeuft — das Zielsystem, auf das installiert wird, kann trotzdem laeuft das Zielsystem, auf das installiert wird, kann trotzdem
jede von Apple unterstuetzte Version sein. jede von Apple unterstuetzte Version sein. Diese Einschraenkung gilt nur
- Kein Multiboot-Support (anders als das Docker-Tool) — fuer die Apple-Download-Quelle (`softwareupdate --fetch-full-installer`);
`createinstallmedia` erstellt immer einen kompletten Single-Version-Stick. `installer`/`asr` funktionieren grundsaetzlich auch auf aelteren Hosts.
Waere technisch nachruestbar (mehrere Partitionen, mehrere - Kein Multiboot-Support (anders als das Docker-Tool) —
`createinstallmedia`-Laeufe mit `--volume` auf je eine Partition), aber jeder Lauf erstellt immer einen kompletten Single-Version-Stick.
aktuell nicht gebaut. - **Ungetestet auf echtem Mac.** Die Auswahl-/Verzweigungslogik (Quelle,
- Downloads landen unter `/Applications` (Apples Standardverhalten von Modus-Erkennung ueber `meta.json`) wurde unter Linux mit einer
`softwareupdate`), nicht im Projektordner — dieser Zweig ist bewusst simulierten Bibliothek end-to-end durchgespielt (`bash -n` sauber,
nicht auf Portabilitaet/externe Platte ausgelegt wie das Docker-Tool, beide Zweige laufen bis zum jeweils ersten macOS-only-Befehl korrekt
sondern auf den einfachsten Weg wenn gerade ein Mac da ist. 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.
+335 -117
View File
@@ -1,25 +1,37 @@
#!/bin/bash #!/bin/bash
# #
# create-stick.sh — natives macOS-Terminal-Tool fuer bootfaehige # create-stick.sh — natives macOS-Terminal-Tool fuer bootfaehige
# Installer-Sticks via Apples eigenem createinstallmedia. # Installer-Sticks. Laeuft NUR auf echtem macOS. Nutzt ausschliesslich
# # Bordmittel: softwareupdate, diskutil, PlistBuddy, installer, asr,
# Laeuft NUR auf echtem macOS (10.15 Catalina oder neuer). Nutzt
# ausschliesslich Bordmittel: softwareupdate, diskutil, PlistBuddy,
# createinstallmedia. Kein Homebrew, keine externen Abhaengigkeiten. # createinstallmedia. Kein Homebrew, keine externen Abhaengigkeiten.
# #
# Warum ein eigener Pfad neben dem Docker/Linux-Tool im Projekt-Root: # Zwei moegliche QUELLEN fuer den Installer-Payload:
# 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 # 1) Apple-Server (Download)
# Apples eigene Server live ab. Neue macOS-Versionen tauchen automatisch # softwareupdate --fetch-full-installer laedt die komplette
# auf, ohne dass dieses Script angefasst werden muss. # "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 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"; } ok() { printf "%s%s%s\n" "$GREEN" "$1" "$RESET"; }
warn() { printf "%s%s%s\n" "$YELLOW" "$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 ---------- # ---------- Vorpruefungen ----------
if [[ "$(uname)" != "Darwin" ]]; then if [[ "$(uname)" != "Darwin" ]]; then
err "Dieses Script laeuft nur auf echtem macOS (kein Linux, keine VM ohne macOS)." err "Dieses Script laeuft nur auf echtem macOS (kein Linux, keine VM ohne macOS)."
exit 1 exit 1
fi fi
if ! command -v softwareupdate >/dev/null 2>&1; then # ---------- Schritt 1: Quelle + Version waehlen ----------
err "'softwareupdate' nicht gefunden — ungewoehnliches macOS-Setup, Abbruch." title "macOS USB-Stick-Creator (nativ) — Schritt 1/4: Quelle + Version"
exit 1
fi
MACOS_VER="$(sw_vers -productVersion)" echo "Woher soll der macOS-Installer kommen?"
# 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 echo
echo " ${BOLD}1)${RESET} Von Apple herunterladen (braucht Internet, mehrere GB)"
LIST_RAW="$(softwareupdate --list-full-installers 2>/dev/null)" echo " ${BOLD}2)${RESET} Aus lokaler Downloads-Bibliothek (Docker-Tool) verwenden -- kein Internet noetig"
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 echo
printf "%s0)%s Abbrechen\n" "$BOLD" "$RESET" printf "%s0)%s Abbrechen\n" "$BOLD" "$RESET"
echo 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." echo "Abgebrochen."
exit 0 exit 0
fi fi
if ! [[ "$CHOICE" =~ ^[0-9]+$ ]] || (( CHOICE < 1 || CHOICE > ${#TITLES[@]} )); then
err "Ungueltige Auswahl."
exit 1
fi
SEL_TITLE="${TITLES[$((CHOICE-1))]}" SOURCE=""
SEL_VERSION="${VERSIONS[$((CHOICE-1))]}" WRITE_MODE="" # "cim" (createinstallmedia) oder "asr" (direktes Image-Restore)
# Apples Titel-Feld liefert z.B. "macOS Sonoma" -> App heisst "Install macOS Sonoma.app" APP_PATH=""
APP_PATH="/Applications/Install ${SEL_TITLE}.app" DMG_PATH=""
LIB_TITLE=""
LIB_VERSION=""
echo case "$SRC_CHOICE" in
ok "Gewaehlt: ${SEL_TITLE} (${SEL_VERSION})" 1)
SOURCE="apple"
;;
2)
SOURCE="library"
;;
*)
err "Ungueltige Auswahl."
exit 1
;;
esac
# ---------- Schritt 2: Download (falls noetig) ---------- if [[ "$SOURCE" == "apple" ]]; then
title "Schritt 2/4: Installer-App besorgen" 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 MACOS_VER="$(sw_vers -productVersion)"
ok "'${APP_PATH}' existiert bereits — Download wird uebersprungen." # fetch-full-installer gibt es erst ab Catalina (10.15)
else if [[ "$MACOS_VER" == 10.* ]]; then
warn "Das laedt mehrere GB von Apple und braucht sudo-Rechte." MINOR="${MACOS_VER#10.}"
echo "Zielort: ${APP_PATH}" MINOR="${MINOR%%.*}"
read -r -p "Download jetzt starten? [j/N]: " GO if [[ "$MINOR" -lt 15 ]]; then
if [[ ! "$GO" =~ ^[jJ]$ ]]; 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." echo "Abgebrochen."
exit 0 exit 0
fi fi
echo if ! [[ "$CHOICE" =~ ^[0-9]+$ ]] || (( CHOICE < 1 || CHOICE > ${#TITLES[@]} )); then
echo "Starte Download (kann je nach Version 30-90 Minuten dauern)..." err "Ungueltige Auswahl."
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 exit 1
fi 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 fi
CIM_BIN="${APP_PATH}/Contents/Resources/createinstallmedia" # ---------- Schritt 2: Installer-Payload besorgen ----------
if [[ ! -x "$CIM_BIN" ]]; then title "Schritt 2/4: Installer-Payload besorgen"
err "createinstallmedia nicht gefunden unter: ${CIM_BIN}"
exit 1 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 fi
# ---------- Schritt 3: Ziel-USB-Stick waehlen ---------- # ---------- Schritt 3: Ziel-USB-Stick waehlen ----------
@@ -209,33 +385,75 @@ if [[ "$CONFIRM" != "/dev/${TARGET_DISK}" ]]; then
exit 1 exit 1
fi fi
# ---------- Schritt 4: createinstallmedia ausfuehren ---------- # ---------- Schritt 4: Stick schreiben ----------
title "Schritt 4/4: Stick wird erstellt" title "Schritt 4/4: Stick wird erstellt"
echo "App: ${APP_PATH}" if [[ "$WRITE_MODE" == "cim" ]]; then
echo "Ziel: /dev/${TARGET_DISK}" echo "App: ${APP_PATH}"
echo echo "Ziel: /dev/${TARGET_DISK}"
echo "createinstallmedia laeuft jetzt (braucht sudo, dauert typischerweise 10-30 Minuten)..." echo
echo "Der Stick wird zwischendurch umbenannt/neu gemountet — das ist normal." echo "createinstallmedia laeuft jetzt (braucht sudo, dauert typischerweise 10-30 Minuten)..."
echo echo "Der Stick wird zwischendurch umbenannt/neu gemountet — das ist normal."
echo
sudo "$CIM_BIN" --volume "/dev/${TARGET_DISK}" --nointeraction sudo "$CIM_BIN" --volume "/dev/${TARGET_DISK}" --nointeraction
RC=$? RC=$?
echo echo
if [[ $RC -eq 0 ]]; then if [[ $RC -eq 0 ]]; then
ok "Fertig! Der Stick ist bootfaehig." ok "Fertig! Der Stick ist bootfaehig."
echo "Am Ziel-Mac: beim Start Option/Alt gedrueckt halten, den Stick im Boot-Picker waehlen." 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 else
err "createinstallmedia ist mit Fehlercode ${RC} beendet — siehe Ausgabe oben." # asr-Pfad: BaseSystem.dmg/InstallESD.dmg ist schon ein fertiges,
exit $RC # bootfaehiges Volume. Erst Stick neu formatieren (liefert einen
fi # 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 VOL_MOUNT="/Volumes/INSTALL"
read -r -p "Die heruntergeladene Installer-App (mehrere GB) jetzt loeschen um Platz zu sparen? [j/N]: " CLEAN if [[ ! -d "$VOL_MOUNT" ]]; then
if [[ "$CLEAN" =~ ^[jJ]$ ]]; then err "Erwarteter Mountpunkt ${VOL_MOUNT} nicht gefunden nach dem Formatieren."
sudo rm -rf "$APP_PATH" echo "Tipp: 'diskutil list /dev/${TARGET_DISK}' zeigt den tatsaechlichen Zustand."
ok "Geloescht: ${APP_PATH}" 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 fi
echo echo