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