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.
243 lines
8.0 KiB
Bash
Executable File
243 lines
8.0 KiB
Bash
Executable File
#!/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."
|