#!/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."