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:
ARIA
2026-07-18 09:53:41 +00:00
parent 6f8538428c
commit e4dd003bd6
3 changed files with 323 additions and 0 deletions
+11
View File
@@ -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
+70
View File
@@ -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.
+242
View File
@@ -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."