From e4dd003bd6c905ac4c07de8dcd3cbe64d93796e3 Mon Sep 17 00:00:00 2001 From: ARIA Date: Sat, 18 Jul 2026 09:53:41 +0000 Subject: [PATCH] =?UTF-8?q?feat(native-macos):=20create-stick.sh=20?= =?UTF-8?q?=E2=80=94=20nativer=20createinstallmedia-Weg=20fuer=20echte=20M?= =?UTF-8?q?acs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- README.md | 11 ++ native-macos/README.md | 70 ++++++++++ native-macos/create-stick.sh | 242 +++++++++++++++++++++++++++++++++++ 3 files changed, 323 insertions(+) create mode 100644 native-macos/README.md create mode 100755 native-macos/create-stick.sh diff --git a/README.md b/README.md index 295b466..b37f350 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/native-macos/README.md b/native-macos/README.md new file mode 100644 index 0000000..29f22b0 --- /dev/null +++ b/native-macos/README.md @@ -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. diff --git a/native-macos/create-stick.sh b/native-macos/create-stick.sh new file mode 100755 index 0000000..b642a1e --- /dev/null +++ b/native-macos/create-stick.sh @@ -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."