feat(native-macos): Multiboot-Stick-Modus fuer createinstallmedia/asr
Ein Stick, mehrere macOS-Versionen, frei mischbar aus Apple-Server- und Bibliotheks-Quelle. Partitioniert den Stick per diskutil partitionDisk in eine eigene HFS+-Partition je Version (eindeutige Labels), schreibt danach pro Partition gezielt per createinstallmedia --volume bzw. asr restore --target -- analog zur bereits vorhandenen Multiboot-Technik im Docker/Linux-Tool. Inkl. Groessen-Check mit Sicherheitspuffer vor jeder Aenderung am Stick, robustem Fehlerpfad je Partition (Weiter/Abbruch statt Stillschweigen) und End-to-End-Test der kompletten Ablauflogik unter Linux mit gestubbten macOS-Bordmitteln (Einzel-Modus + Multiboot, reine Apple-Quelle + gemischte Apple/Bibliotheks-Quelle).
This commit is contained in:
+58
-14
@@ -1,8 +1,9 @@
|
||||
# Nativer macOS-Weg (createinstallmedia / asr)
|
||||
|
||||
Alternative zum Docker/Linux-Tool im Projekt-Root — **nur nutzbar, wenn
|
||||
gerade ein echter Mac zur Verfuegung steht**. Zwei Quellen fuer den
|
||||
Installer-Payload stehen zur Auswahl, direkt im Script:
|
||||
gerade ein echter Mac zur Verfuegung steht**. Zwei Modi (Einzel-Stick /
|
||||
Multiboot-Stick) und zwei Quellen fuer den Installer-Payload stehen zur
|
||||
Auswahl, direkt im Script:
|
||||
|
||||
1. **Von Apple herunterladen** (macOS 10.15 Catalina oder neuer als Host
|
||||
noetig) — der offiziell von Apple unterstuetzte Weg ueber
|
||||
@@ -61,9 +62,12 @@ 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`,
|
||||
`installer`, `asr`, `createinstallmedia`, alles Bordmittel):
|
||||
Fragt zuerst den **Modus** ab (Einzel-Stick oder Multiboot-Stick), dann
|
||||
fuehrt es durch die passenden Schritte (reines Terminal-Menue, keine
|
||||
externen Abhaengigkeiten — nur `softwareupdate`, `diskutil`,
|
||||
`PlistBuddy`, `installer`, `asr`, `createinstallmedia`, alles Bordmittel).
|
||||
|
||||
### Modus 1: Einzelner Stick (4 Schritte)
|
||||
|
||||
1. **Quelle + Version waehlen** — Apple-Server oder lokale Bibliothek
|
||||
(Pfad wird abgefragt, Default rät `../downloads/library` relativ zum
|
||||
@@ -80,6 +84,40 @@ Abhaengigkeiten — nur `softwareupdate`, `diskutil`, `PlistBuddy`,
|
||||
`diskutil eraseDisk` + `asr restore --erase` (Bibliothek,
|
||||
bis Catalina). Beides mit `sudo`, live-Ausgabe im Terminal.
|
||||
|
||||
### Modus 2: Multiboot-Stick (5 Schritte)
|
||||
|
||||
Ein Stick, **mehrere** macOS-Versionen gleichzeitig — frei mischbar aus
|
||||
Apple-Server und lokaler Bibliothek (z.B. eine Version per Download, zwei
|
||||
weitere aus der Offline-Bibliothek, in einem Lauf).
|
||||
|
||||
1. **Versionen auswaehlen** — kombinierte Liste aus Apple-Server +
|
||||
Bibliothek, Mehrfachauswahl per Komma (`1,3,4`), 2 bis 8 Versionen.
|
||||
2. **Payloads besorgen** — pro gewaehlter Version wie in Modus 1, Schritt
|
||||
2 (Download/Entpacken/direktes Image), nacheinander.
|
||||
3. **USB-Stick auswaehlen** — wie Modus 1, plus **Groessen-Check**: die
|
||||
Summe aus geschaetztem Platzbedarf je Version (inkl. Sicherheitspuffer:
|
||||
+33% bei `createinstallmedia`-Versionen, +5% bei reinen Image-Restores,
|
||||
jeweils mit Mindestgroesse) wird gegen die Stick-Kapazitaet geprueft.
|
||||
Reicht der Platz nicht, bricht das Script **vor** jeder Aenderung am
|
||||
Stick mit klarer Meldung ab.
|
||||
4. **Partitionieren** — `diskutil partitionDisk` legt fuer jede gewaehlte
|
||||
Version eine eigene HFS+-Partition mit eindeutigem Label an (GPT-Schema
|
||||
mit automatischer EFI-Partition). Die letzte Partition bekommt den
|
||||
restlichen Platz (`R`), um Rundungsverluste zu vermeiden.
|
||||
5. **Partitionen beschreiben** — pro Partition laeuft `createinstallmedia
|
||||
--volume ...` bzw. `asr restore --target ...` genau wie im Einzel-Modus,
|
||||
nur gezielt gegen die jeweils eigene Partition statt gegen die ganze
|
||||
Platte. Beide Tools reformatieren dabei ausschliesslich ihr eigenes
|
||||
Zielvolume — die anderen Partitionen bleiben unberuehrt. Schlaegt eine
|
||||
Version fehl, fragt das Script ob mit den restlichen weitergemacht
|
||||
werden soll, statt den ganzen Lauf stillschweigend abzubrechen.
|
||||
|
||||
Am Ziel-Mac zeigt der Boot-Picker (Option/Alt beim Start) danach jede
|
||||
erfolgreich geschriebene Version als eigenen Eintrag — das ist dieselbe,
|
||||
in der Community etablierte Partitionierungstechnik, die auch das
|
||||
Docker/Linux-Tool dieses Projekts fuer seinen `dd`-basierten Multiboot-Weg
|
||||
nutzt.
|
||||
|
||||
## Grenzen
|
||||
|
||||
- Nur macOS 10.15+ (Catalina oder neuer) als **Host**, auf dem das Script
|
||||
@@ -87,15 +125,21 @@ Abhaengigkeiten — nur `softwareupdate`, `diskutil`, `PlistBuddy`,
|
||||
jede von Apple unterstuetzte Version sein. Diese Einschraenkung gilt nur
|
||||
fuer die Apple-Download-Quelle (`softwareupdate --fetch-full-installer`);
|
||||
`installer`/`asr` funktionieren grundsaetzlich auch auf aelteren Hosts.
|
||||
- Kein Multiboot-Support (anders als das Docker-Tool) —
|
||||
jeder Lauf erstellt immer einen kompletten Single-Version-Stick.
|
||||
- **Ungetestet auf echtem Mac.** Die Auswahl-/Verzweigungslogik (Quelle,
|
||||
Modus-Erkennung ueber `meta.json`) wurde unter Linux mit einer
|
||||
simulierten Bibliothek end-to-end durchgespielt (`bash -n` sauber,
|
||||
beide Zweige laufen bis zum jeweils ersten macOS-only-Befehl korrekt
|
||||
durch). Der eigentliche `installer`/`asr`/`createinstallmedia`-Lauf
|
||||
selbst ist nur gegen offizielle Apple-Doku/Community-Quellen verifiziert,
|
||||
nicht live auf einem Mac getestet.
|
||||
- Multiboot-Groessenschaetzung ist ein **Puffer, keine Garantie** — bei
|
||||
sehr knapp bemessenen Sticks im Zweifel weniger Versionen waehlen oder
|
||||
einen groesseren Stick nehmen.
|
||||
- **Ungetestet auf echtem Mac.** Die komplette Ablauflogik (Modus- und
|
||||
Quellenwahl, Mehrfachauswahl-Parsing, Groessen-/Puffer-Berechnung,
|
||||
Label-Vergabe, Partitionierung, sequentielles Schreiben inkl.
|
||||
Fehlerpfade) wurde unter Linux mit gestubbten `softwareupdate` /
|
||||
`diskutil` / `PlistBuddy` / `installer` / `asr` /
|
||||
`createinstallmedia`-Bordmitteln end-to-end durchgespielt — Einzel-Modus
|
||||
(Apple- und Bibliotheks-Quelle), Multiboot mit reinen Apple-Downloads
|
||||
und Multiboot mit gemischten Apple+Bibliotheks-Quellen liefen dabei
|
||||
jeweils fehlerfrei bis zum Abschluss durch, inklusive Sicherheitsabbruch
|
||||
bei falscher Bestaetigung. Der eigentliche `installer`/`asr`/
|
||||
`createinstallmedia`-Lauf selbst ist nur gegen offizielle Apple-Doku/
|
||||
Community-Quellen verifiziert, nicht live auf einem Mac getestet.
|
||||
|
||||
## Quellen (fuer den asr-Weg, live nachgeschlagen)
|
||||
|
||||
|
||||
+659
-196
@@ -32,6 +32,21 @@
|
||||
# So wird aus jeder bereits vom Docker-Tool gebauten Offline-Bibliothek
|
||||
# (externe Platte, ohne Internet) auch am echten Mac ein bootfaehiger
|
||||
# Stick, ganz ohne doppelten Download.
|
||||
#
|
||||
# ZWEI MODI:
|
||||
#
|
||||
# A) Einzelner Stick — ein Stick, eine macOS-Version (Original-Modus).
|
||||
# B) Multiboot-Stick — ein Stick, MEHRERE macOS-Versionen gleichzeitig.
|
||||
# Technik: der Stick wird per 'diskutil partitionDisk' in N eigene
|
||||
# HFS+-Partitionen aufgeteilt (eine pro gewaehlter Version). Danach
|
||||
# laeuft createinstallmedia bzw. asr restore genau wie im
|
||||
# Einzel-Modus, nur pro Durchlauf gegen die jeweils EIGENE Partition
|
||||
# statt gegen die ganze Platte -- beide Tools reformatieren dabei
|
||||
# ausschliesslich ihr eigenes Zielvolume, die anderen Partitionen
|
||||
# bleiben unberuehrt. Macs EFI-Bootpicker (Alt/Option beim Start)
|
||||
# scannt alle Partitionen und zeigt jede bootfaehige einzeln an.
|
||||
# Das ist dieselbe, in der Community etablierte Technik wie sie das
|
||||
# Docker/Linux-Tool dieses Projekts fuer den dd-Weg schon nutzt.
|
||||
|
||||
set -u
|
||||
|
||||
@@ -57,13 +72,264 @@ json_field() {
|
||||
sed -n 's/.*"'"$key"'":[[:space:]]*"\{0,1\}\([^",}]*\)"\{0,1\}.*/\1/p' "$file" | head -n1
|
||||
}
|
||||
|
||||
# Bytes -> aufgerundete ganze GB (fuer Anzeige und Partitionsgroessen).
|
||||
bytes_to_gb_ceil() {
|
||||
local b="$1"
|
||||
echo $(( (b + 999999999) / 1000000000 ))
|
||||
}
|
||||
|
||||
# Kuerzt/saeubert einen Titel zu einem gueltigen, eindeutigen HFS+-Label.
|
||||
# HFS+-Volume-Namen: max. 27 Zeichen, kein ':' und kein '/'. Wir haengen
|
||||
# den Auswahl-Index voran, damit gleichnamige Versionen nie kollidieren.
|
||||
make_label() {
|
||||
local idx="$1" title="$2"
|
||||
local clean
|
||||
clean="$(echo "$title" | tr -d ':/' | tr -s ' ' ' ')"
|
||||
clean="V${idx}_${clean}"
|
||||
echo "${clean:0:27}"
|
||||
}
|
||||
|
||||
# ---------- Vorpruefungen ----------
|
||||
if [[ "$(uname)" != "Darwin" ]]; then
|
||||
err "Dieses Script laeuft nur auf echtem macOS (kein Linux, keine VM ohne macOS)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------- Schritt 1: Quelle + Version waehlen ----------
|
||||
DEFAULT_LIB_DIR="$(cd "$(dirname "$0")/.." 2>/dev/null && pwd)/downloads/library"
|
||||
|
||||
# ======================================================================
|
||||
# Gemeinsame Listen-Beschaffung (fuer Einzel- UND Multiboot-Modus)
|
||||
# ======================================================================
|
||||
|
||||
# Fuellt die globalen Arrays A_TITLES/A_VERSIONS/A_SIZES mit Apples
|
||||
# aktuell verfuegbaren kompletten Installern. Gibt 1 zurueck wenn keine
|
||||
# Liste beschafft werden konnte (kein Abbruch des Scripts -- der Aufrufer
|
||||
# entscheidet, ob das schlimm ist).
|
||||
fetch_apple_list() {
|
||||
A_TITLES=()
|
||||
A_VERSIONS=()
|
||||
A_SIZES=()
|
||||
|
||||
command -v softwareupdate >/dev/null 2>&1 || return 1
|
||||
|
||||
local list_raw
|
||||
list_raw="$(softwareupdate --list-full-installers 2>/dev/null)"
|
||||
grep -q "Title:" <<<"$list_raw" || return 1
|
||||
|
||||
local line t v s
|
||||
while IFS= read -r line; do
|
||||
[[ "$line" == *"Title:"* ]] || continue
|
||||
t="$(sed -E 's/^\* Title: ([^,]+),.*/\1/' <<<"$line")"
|
||||
v="$(sed -E 's/.*Version: ([^,]+),.*/\1/' <<<"$line")"
|
||||
s="$(sed -E 's/.*Size: ([0-9]+),.*/\1/' <<<"$line")"
|
||||
[[ "$s" =~ ^[0-9]+$ ]] || s="0"
|
||||
A_TITLES+=("$t")
|
||||
A_VERSIONS+=("$v")
|
||||
A_SIZES+=("$s")
|
||||
done <<<"$list_raw"
|
||||
|
||||
[[ ${#A_TITLES[@]} -gt 0 ]]
|
||||
}
|
||||
|
||||
# Fuellt die globalen Arrays L_DIRS/L_TITLES/L_VERSIONS/L_MODES/L_SIZES/
|
||||
# L_FILENAMES mit den vollstaendigen Eintraegen der lokalen Bibliothek.
|
||||
# $1 = Pfad zur Bibliothek. Gibt 1 zurueck wenn nichts gefunden wurde.
|
||||
fetch_library_list() {
|
||||
local lib_dir="$1"
|
||||
L_DIRS=()
|
||||
L_TITLES=()
|
||||
L_VERSIONS=()
|
||||
L_MODES=()
|
||||
L_SIZES=()
|
||||
L_FILENAMES=()
|
||||
|
||||
[[ -d "$lib_dir" ]] || return 1
|
||||
|
||||
local entry_dir meta complete et ev emode esize efn
|
||||
for entry_dir in "$lib_dir"/*/; do
|
||||
[[ -d "$entry_dir" ]] || continue
|
||||
meta="${entry_dir}meta.json"
|
||||
[[ -f "$meta" ]] || continue
|
||||
complete="$(json_field "$meta" complete)"
|
||||
[[ "$complete" == "true" ]] || continue
|
||||
et="$(json_field "$meta" title)"
|
||||
ev="$(json_field "$meta" version)"
|
||||
emode="$(json_field "$meta" mode)"
|
||||
esize="$(json_field "$meta" size_bytes)"
|
||||
efn="$(json_field "$meta" filename)"
|
||||
[[ "$esize" =~ ^[0-9]+$ ]] || esize="0"
|
||||
L_DIRS+=("$entry_dir")
|
||||
L_TITLES+=("$et")
|
||||
L_VERSIONS+=("$ev")
|
||||
L_MODES+=("$emode")
|
||||
L_SIZES+=("$esize")
|
||||
L_FILENAMES+=("$efn")
|
||||
done
|
||||
|
||||
[[ ${#L_DIRS[@]} -gt 0 ]]
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# Payload-Beschaffung fuer EINEN gewaehlten Eintrag (von Apple oder aus
|
||||
# der Bibliothek). Setzt bei Erfolg die globalen Variablen:
|
||||
# RESOLVED_WRITE_MODE ("cim" oder "asr")
|
||||
# RESOLVED_APP_PATH (bei cim)
|
||||
# RESOLVED_DMG_PATH (bei asr)
|
||||
# RESOLVED_SIZE_BYTES (reale Groesse nach Beschaffung, fuer Partition)
|
||||
# Gibt bei Fehler != 0 zurueck (Aufrufer entscheidet ueber Abbruch).
|
||||
#
|
||||
# Argumente: resolve_payload <src:"apple"|"library"> <title> <version> \
|
||||
# [lib_dir lib_mode lib_filename]
|
||||
# ======================================================================
|
||||
resolve_payload() {
|
||||
local src="$1" title="$2" version="$3"
|
||||
local lib_dir="${4:-}" lib_mode="${5:-}" lib_filename="${6:-}"
|
||||
|
||||
RESOLVED_WRITE_MODE=""
|
||||
RESOLVED_APP_PATH=""
|
||||
RESOLVED_DMG_PATH=""
|
||||
RESOLVED_SIZE_BYTES="0"
|
||||
|
||||
if [[ "$src" == "apple" ]]; then
|
||||
local app_path="/Applications/Install ${title}.app"
|
||||
RESOLVED_WRITE_MODE="cim"
|
||||
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 (${title}, ${version})."
|
||||
read -r -p "Download jetzt starten? [j/N]: " GO
|
||||
if [[ ! "$GO" =~ ^[jJ]$ ]]; then
|
||||
err "Abgebrochen fuer ${title}."
|
||||
return 1
|
||||
fi
|
||||
echo
|
||||
echo "Starte Download (kann je nach Version 30-90 Minuten dauern)..."
|
||||
sudo softwareupdate --fetch-full-installer --full-installer-version "$version"
|
||||
if [[ $? -ne 0 || ! -d "$app_path" ]]; then
|
||||
err "Download fehlgeschlagen oder App nicht am erwarteten Ort. Erwartet: ${app_path}"
|
||||
return 1
|
||||
fi
|
||||
ok "Download abgeschlossen: ${app_path}"
|
||||
fi
|
||||
RESOLVED_APP_PATH="$app_path"
|
||||
RESOLVED_SIZE_BYTES="$(du -sk "$app_path" 2>/dev/null | awk '{print $1*1024}')"
|
||||
[[ "$RESOLVED_SIZE_BYTES" =~ ^[0-9]+$ ]] || RESOLVED_SIZE_BYTES="0"
|
||||
local cim_bin="${app_path}/Contents/Resources/createinstallmedia"
|
||||
if [[ ! -x "$cim_bin" ]]; then
|
||||
err "createinstallmedia nicht gefunden unter: ${cim_bin}"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
# src == "library"
|
||||
local payload_path="${lib_dir}${lib_filename}"
|
||||
if [[ ! -f "$payload_path" ]]; then
|
||||
err "Payload-Datei nicht gefunden: ${payload_path}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$lib_mode" == "recovery" ]]; then
|
||||
RESOLVED_WRITE_MODE="cim"
|
||||
local app_path="/Applications/Install ${title}.app"
|
||||
if [[ -d "$app_path" ]]; then
|
||||
ok "'${app_path}' existiert bereits — Installer-Lauf wird uebersprungen."
|
||||
else
|
||||
echo "Entpacke ${payload_path} nach /Applications (installer -pkg, braucht sudo)..."
|
||||
sudo installer -pkg "$payload_path" -target /
|
||||
if [[ $? -ne 0 ]]; then
|
||||
err "'installer -pkg' fehlgeschlagen fuer ${title}."
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -d "$app_path" ]]; then
|
||||
local fallback
|
||||
fallback="$(ls -dt /Applications/Install\ macOS*.app 2>/dev/null | head -n1)"
|
||||
[[ -n "$fallback" ]] && app_path="$fallback"
|
||||
fi
|
||||
if [[ ! -d "$app_path" ]]; then
|
||||
err "Konnte die installierte App nach 'installer -pkg' nicht finden (${title})."
|
||||
return 1
|
||||
fi
|
||||
ok "Installiert: ${app_path}"
|
||||
fi
|
||||
RESOLVED_APP_PATH="$app_path"
|
||||
RESOLVED_SIZE_BYTES="$(du -sk "$app_path" 2>/dev/null | awk '{print $1*1024}')"
|
||||
[[ "$RESOLVED_SIZE_BYTES" =~ ^[0-9]+$ ]] || RESOLVED_SIZE_BYTES="0"
|
||||
local cim_bin="${app_path}/Contents/Resources/createinstallmedia"
|
||||
if [[ ! -x "$cim_bin" ]]; then
|
||||
err "createinstallmedia nicht gefunden unter: ${cim_bin}"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
RESOLVED_WRITE_MODE="asr"
|
||||
RESOLVED_DMG_PATH="$payload_path"
|
||||
RESOLVED_SIZE_BYTES="$(stat -f%z "$payload_path" 2>/dev/null)"
|
||||
[[ "$RESOLVED_SIZE_BYTES" =~ ^[0-9]+$ ]] || RESOLVED_SIZE_BYTES="0"
|
||||
ok "Bibliothek liefert bereits ein fertiges, bootfaehiges Image (${payload_path}) -- kein App-Download noetig."
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# Externe physische Laufwerke auflisten. Fuellt D_IDS/D_DESCS/D_SIZE_BYTES.
|
||||
# ======================================================================
|
||||
list_external_disks() {
|
||||
D_IDS=()
|
||||
D_DESCS=()
|
||||
D_SIZE_BYTES=()
|
||||
|
||||
local plist_tmp
|
||||
plist_tmp="$(mktemp /tmp/diskutil_ext.XXXXXX.plist)"
|
||||
diskutil list -plist external physical > "$plist_tmp" 2>/dev/null
|
||||
|
||||
local i=0 did info name size_bytes size_gb
|
||||
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_bytes" =~ ^[0-9]+$ ]] || size_bytes="0"
|
||||
size_gb="$(bytes_to_gb_ceil "$size_bytes")"
|
||||
D_IDS+=("$did")
|
||||
D_DESCS+=("/dev/${did} — ${name:-unbekannt} (~${size_gb} GB)")
|
||||
D_SIZE_BYTES+=("$size_bytes")
|
||||
i=$((i+1))
|
||||
done
|
||||
rm -f "$plist_tmp"
|
||||
|
||||
[[ ${#D_IDS[@]} -gt 0 ]]
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# MODUS-Wahl
|
||||
# ======================================================================
|
||||
title "macOS USB-Stick-Creator (nativ) — Modus waehlen"
|
||||
|
||||
echo "Was soll erstellt werden?"
|
||||
echo
|
||||
echo " ${BOLD}1)${RESET} Einzelner Stick (eine macOS-Version)"
|
||||
echo " ${BOLD}2)${RESET} Multiboot-Stick (mehrere macOS-Versionen auf einem Stick)"
|
||||
echo
|
||||
printf "%s0)%s Abbrechen\n" "$BOLD" "$RESET"
|
||||
echo
|
||||
read -r -p "Modus waehlen [Nummer]: " MODE_CHOICE
|
||||
|
||||
if [[ "$MODE_CHOICE" == "0" || -z "$MODE_CHOICE" ]]; then
|
||||
echo "Abgebrochen."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$MODE_CHOICE" != "1" && "$MODE_CHOICE" != "2" ]]; then
|
||||
err "Ungueltige Auswahl."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ======================================================================
|
||||
# ================ MODUS 1: EINZELNER STICK ==========================
|
||||
# ======================================================================
|
||||
if [[ "$MODE_CHOICE" == "1" ]]; then
|
||||
|
||||
title "macOS USB-Stick-Creator (nativ) — Schritt 1/4: Quelle + Version"
|
||||
|
||||
echo "Woher soll der macOS-Installer kommen?"
|
||||
@@ -80,26 +346,18 @@ if [[ "$SRC_CHOICE" == "0" || -z "$SRC_CHOICE" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SOURCE=""
|
||||
WRITE_MODE="" # "cim" (createinstallmedia) oder "asr" (direktes Image-Restore)
|
||||
APP_PATH=""
|
||||
DMG_PATH=""
|
||||
LIB_TITLE=""
|
||||
LIB_VERSION=""
|
||||
|
||||
case "$SRC_CHOICE" in
|
||||
1)
|
||||
SOURCE="apple"
|
||||
;;
|
||||
2)
|
||||
SOURCE="library"
|
||||
;;
|
||||
*)
|
||||
err "Ungueltige Auswahl."
|
||||
exit 1
|
||||
;;
|
||||
1) SOURCE="apple" ;;
|
||||
2) SOURCE="library" ;;
|
||||
*) err "Ungueltige Auswahl."; exit 1 ;;
|
||||
esac
|
||||
|
||||
SEL_TITLE=""
|
||||
SEL_VERSION=""
|
||||
SEL_LIB_DIR=""
|
||||
SEL_LIB_MODE=""
|
||||
SEL_LIB_FILENAME=""
|
||||
|
||||
if [[ "$SOURCE" == "apple" ]]; then
|
||||
if ! command -v softwareupdate >/dev/null 2>&1; then
|
||||
err "'softwareupdate' nicht gefunden — ungewoehnliches macOS-Setup, Abbruch."
|
||||
@@ -107,7 +365,6 @@ if [[ "$SOURCE" == "apple" ]]; then
|
||||
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%%.*}"
|
||||
@@ -121,33 +378,15 @@ if [[ "$SOURCE" == "apple" ]]; then
|
||||
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
|
||||
if ! fetch_apple_list; 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]}"
|
||||
for i in "${!A_TITLES[@]}"; do
|
||||
printf " %s%2d)%s %s (%s)\n" "$BOLD" "$((i+1))" "$RESET" "${A_TITLES[$i]}" "${A_VERSIONS[$i]}"
|
||||
done
|
||||
echo
|
||||
printf "%s0)%s Abbrechen\n" "$BOLD" "$RESET"
|
||||
@@ -158,56 +397,25 @@ if [[ "$SOURCE" == "apple" ]]; then
|
||||
echo "Abgebrochen."
|
||||
exit 0
|
||||
fi
|
||||
if ! [[ "$CHOICE" =~ ^[0-9]+$ ]] || (( CHOICE < 1 || CHOICE > ${#TITLES[@]} )); then
|
||||
if ! [[ "$CHOICE" =~ ^[0-9]+$ ]] || (( CHOICE < 1 || CHOICE > ${#A_TITLES[@]} )); then
|
||||
err "Ungueltige Auswahl."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SEL_TITLE="${TITLES[$((CHOICE-1))]}"
|
||||
SEL_VERSION="${VERSIONS[$((CHOICE-1))]}"
|
||||
WRITE_MODE="cim"
|
||||
# Apples Titel-Feld liefert z.B. "macOS Sonoma" -> App heisst "Install macOS Sonoma.app"
|
||||
APP_PATH="/Applications/Install ${SEL_TITLE}.app"
|
||||
|
||||
SEL_TITLE="${A_TITLES[$((CHOICE-1))]}"
|
||||
SEL_VERSION="${A_VERSIONS[$((CHOICE-1))]}"
|
||||
echo
|
||||
ok "Gewaehlt: ${SEL_TITLE} (${SEL_VERSION}) -- Quelle: Apple-Server"
|
||||
else
|
||||
echo
|
||||
DEFAULT_LIB_DIR="$(cd "$(dirname "$0")/.." 2>/dev/null && pwd)/downloads/library"
|
||||
echo "Pfad zur lokalen Downloads-Bibliothek des Docker-Tools (Ordner mit einem"
|
||||
echo "Unterordner pro macOS-Version, je mit meta.json + Installer-Datei)."
|
||||
echo
|
||||
read -r -p "Pfad [Enter fuer ${DEFAULT_LIB_DIR}]: " LIB_DIR
|
||||
LIB_DIR="${LIB_DIR:-$DEFAULT_LIB_DIR}"
|
||||
read -r -p "Pfad [Enter fuer ${DEFAULT_LIB_DIR}]: " LIB_DIR_IN
|
||||
LIB_DIR="${LIB_DIR_IN:-$DEFAULT_LIB_DIR}"
|
||||
[[ "$LIB_DIR" == */ ]] || LIB_DIR="${LIB_DIR}/"
|
||||
|
||||
if [[ ! -d "$LIB_DIR" ]]; then
|
||||
err "Ordner nicht gefunden: ${LIB_DIR}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
declare -a LIB_ENTRY_DIRS=()
|
||||
declare -a LIB_ENTRY_DESC=()
|
||||
for entry_dir in "$LIB_DIR"/*/; do
|
||||
[[ -d "$entry_dir" ]] || continue
|
||||
meta="${entry_dir}meta.json"
|
||||
[[ -f "$meta" ]] || continue
|
||||
complete="$(json_field "$meta" complete)"
|
||||
[[ "$complete" == "true" ]] || continue
|
||||
et="$(json_field "$meta" title)"
|
||||
ev="$(json_field "$meta" version)"
|
||||
emode="$(json_field "$meta" mode)"
|
||||
esize="$(json_field "$meta" size_bytes)"
|
||||
esize_gb="?"
|
||||
if [[ "$esize" =~ ^[0-9]+$ ]]; then
|
||||
esize_gb="$(( esize / 1000 / 1000 / 1000 ))"
|
||||
fi
|
||||
mode_desc="bis Catalina, robust"
|
||||
[[ "$emode" == "recovery" ]] && mode_desc="Big Sur+, ueber installer-pkg"
|
||||
LIB_ENTRY_DIRS+=("$entry_dir")
|
||||
LIB_ENTRY_DESC+=("${et} (${ev}) -- ~${esize_gb} GB, ${mode_desc}")
|
||||
done
|
||||
|
||||
if [[ ${#LIB_ENTRY_DIRS[@]} -eq 0 ]]; then
|
||||
if ! fetch_library_list "$LIB_DIR"; then
|
||||
err "Keine vollstaendigen Installer in ${LIB_DIR} gefunden."
|
||||
exit 1
|
||||
fi
|
||||
@@ -215,8 +423,11 @@ else
|
||||
echo
|
||||
echo "In der Bibliothek gefunden:"
|
||||
echo
|
||||
for i in "${!LIB_ENTRY_DIRS[@]}"; do
|
||||
printf " %s%2d)%s %s\n" "$BOLD" "$((i+1))" "$RESET" "${LIB_ENTRY_DESC[$i]}"
|
||||
for i in "${!L_DIRS[@]}"; do
|
||||
esize_gb="$(bytes_to_gb_ceil "${L_SIZES[$i]}")"
|
||||
mode_desc="bis Catalina, robust"
|
||||
[[ "${L_MODES[$i]}" == "recovery" ]] && mode_desc="Big Sur+, ueber installer-pkg"
|
||||
printf " %s%2d)%s %s (%s) -- ~%s GB, %s\n" "$BOLD" "$((i+1))" "$RESET" "${L_TITLES[$i]}" "${L_VERSIONS[$i]}" "$esize_gb" "$mode_desc"
|
||||
done
|
||||
echo
|
||||
printf "%s0)%s Abbrechen\n" "$BOLD" "$RESET"
|
||||
@@ -227,134 +438,43 @@ else
|
||||
echo "Abgebrochen."
|
||||
exit 0
|
||||
fi
|
||||
if ! [[ "$LCHOICE" =~ ^[0-9]+$ ]] || (( LCHOICE < 1 || LCHOICE > ${#LIB_ENTRY_DIRS[@]} )); then
|
||||
if ! [[ "$LCHOICE" =~ ^[0-9]+$ ]] || (( LCHOICE < 1 || LCHOICE > ${#L_DIRS[@]} )); then
|
||||
err "Ungueltige Auswahl."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SEL_ENTRY_DIR="${LIB_ENTRY_DIRS[$((LCHOICE-1))]}"
|
||||
SEL_META="${SEL_ENTRY_DIR}meta.json"
|
||||
LIB_TITLE="$(json_field "$SEL_META" title)"
|
||||
LIB_VERSION="$(json_field "$SEL_META" version)"
|
||||
LIB_MODE="$(json_field "$SEL_META" mode)"
|
||||
LIB_FILENAME="$(json_field "$SEL_META" filename)"
|
||||
LIB_PAYLOAD_PATH="${SEL_ENTRY_DIR}${LIB_FILENAME}"
|
||||
|
||||
if [[ ! -f "$LIB_PAYLOAD_PATH" ]]; then
|
||||
err "Payload-Datei nicht gefunden: ${LIB_PAYLOAD_PATH}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$LIB_MODE" == "recovery" ]]; then
|
||||
WRITE_MODE="cim"
|
||||
else
|
||||
WRITE_MODE="asr"
|
||||
DMG_PATH="$LIB_PAYLOAD_PATH"
|
||||
fi
|
||||
|
||||
idx=$((LCHOICE-1))
|
||||
SEL_TITLE="${L_TITLES[$idx]}"
|
||||
SEL_VERSION="${L_VERSIONS[$idx]}"
|
||||
SEL_LIB_DIR="${L_DIRS[$idx]}"
|
||||
SEL_LIB_MODE="${L_MODES[$idx]}"
|
||||
SEL_LIB_FILENAME="${L_FILENAMES[$idx]}"
|
||||
echo
|
||||
ok "Gewaehlt: ${LIB_TITLE} (${LIB_VERSION}) -- Quelle: lokale Bibliothek (${LIB_PAYLOAD_PATH})"
|
||||
ok "Gewaehlt: ${SEL_TITLE} (${SEL_VERSION}) -- Quelle: lokale Bibliothek"
|
||||
fi
|
||||
|
||||
# ---------- Schritt 2: Installer-Payload besorgen ----------
|
||||
title "Schritt 2/4: Installer-Payload besorgen"
|
||||
|
||||
if [[ "$SOURCE" == "apple" ]]; then
|
||||
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
|
||||
elif [[ "$WRITE_MODE" == "cim" ]]; then
|
||||
# Bibliothek, Big Sur+: InstallAssistant.pkg -> komplette Install-App
|
||||
# nach /Applications entpacken (Apples Standardverhalten von 'installer').
|
||||
if [[ -d "$APP_PATH" ]]; then
|
||||
ok "'${APP_PATH}' existiert bereits — Installer-Lauf wird uebersprungen."
|
||||
else
|
||||
echo "Entpacke ${LIB_PAYLOAD_PATH} nach /Applications (installer -pkg, braucht sudo)..."
|
||||
echo "Das dauert je nach Groesse ein paar Minuten."
|
||||
echo
|
||||
sudo installer -pkg "$LIB_PAYLOAD_PATH" -target /
|
||||
RC=$?
|
||||
if [[ $RC -ne 0 ]]; then
|
||||
err "'installer -pkg' fehlgeschlagen ($RC)."
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "$APP_PATH" ]]; then
|
||||
# Fallback: Apples interner App-Name wich vom Bibliotheks-Titel ab --
|
||||
# nimm das zuletzt angelegte "Install macOS *.app"-Bundle.
|
||||
FALLBACK_APP="$(ls -dt /Applications/Install\ macOS*.app 2>/dev/null | head -n1)"
|
||||
[[ -n "$FALLBACK_APP" ]] && APP_PATH="$FALLBACK_APP"
|
||||
fi
|
||||
if [[ ! -d "$APP_PATH" ]]; then
|
||||
err "Konnte die installierte App nach 'installer -pkg' nicht finden."
|
||||
echo "Tipp: 'ls /Applications | grep Install' zeigt was tatsaechlich da ist."
|
||||
exit 1
|
||||
fi
|
||||
ok "Installiert: ${APP_PATH}"
|
||||
fi
|
||||
else
|
||||
ok "Bibliothek liefert bereits ein fertiges, bootfaehiges Image (${DMG_PATH}) -- kein App-Download noetig."
|
||||
fi
|
||||
|
||||
if [[ "$WRITE_MODE" == "cim" ]]; then
|
||||
CIM_BIN="${APP_PATH}/Contents/Resources/createinstallmedia"
|
||||
if [[ ! -x "$CIM_BIN" ]]; then
|
||||
err "createinstallmedia nicht gefunden unter: ${CIM_BIN}"
|
||||
exit 1
|
||||
fi
|
||||
if ! resolve_payload "$SOURCE" "$SEL_TITLE" "$SEL_VERSION" "$SEL_LIB_DIR" "$SEL_LIB_MODE" "$SEL_LIB_FILENAME"; then
|
||||
exit 1
|
||||
fi
|
||||
WRITE_MODE="$RESOLVED_WRITE_MODE"
|
||||
APP_PATH="$RESOLVED_APP_PATH"
|
||||
DMG_PATH="$RESOLVED_DMG_PATH"
|
||||
|
||||
# ---------- 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
|
||||
if ! list_external_disks; 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]}"
|
||||
for i in "${!D_IDS[@]}"; do
|
||||
printf " %s%2d)%s %s\n" "$BOLD" "$((i+1))" "$RESET" "${D_DESCS[$i]}"
|
||||
done
|
||||
echo
|
||||
printf "%s0)%s Abbrechen\n" "$BOLD" "$RESET"
|
||||
@@ -367,13 +487,13 @@ if [[ "$DCHOICE" == "0" || -z "$DCHOICE" ]]; then
|
||||
echo "Abgebrochen."
|
||||
exit 0
|
||||
fi
|
||||
if ! [[ "$DCHOICE" =~ ^[0-9]+$ ]] || (( DCHOICE < 1 || DCHOICE > ${#DISKS[@]} )); then
|
||||
if ! [[ "$DCHOICE" =~ ^[0-9]+$ ]] || (( DCHOICE < 1 || DCHOICE > ${#D_IDS[@]} )); then
|
||||
err "Ungueltige Auswahl."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARGET_DISK="${DISKS[$((DCHOICE-1))]}"
|
||||
TARGET_DESC="${DISK_DESC[$((DCHOICE-1))]}"
|
||||
TARGET_DISK="${D_IDS[$((DCHOICE-1))]}"
|
||||
TARGET_DESC="${D_DESCS[$((DCHOICE-1))]}"
|
||||
|
||||
echo
|
||||
warn "Letzte Sicherheitsabfrage — dieser Stick wird geloescht:"
|
||||
@@ -389,6 +509,7 @@ fi
|
||||
title "Schritt 4/4: Stick wird erstellt"
|
||||
|
||||
if [[ "$WRITE_MODE" == "cim" ]]; then
|
||||
CIM_BIN="${APP_PATH}/Contents/Resources/createinstallmedia"
|
||||
echo "App: ${APP_PATH}"
|
||||
echo "Ziel: /dev/${TARGET_DISK}"
|
||||
echo
|
||||
@@ -415,12 +536,6 @@ if [[ "$WRITE_MODE" == "cim" ]]; then
|
||||
ok "Geloescht: ${APP_PATH}"
|
||||
fi
|
||||
else
|
||||
# asr-Pfad: BaseSystem.dmg/InstallESD.dmg ist schon ein fertiges,
|
||||
# bootfaehiges Volume. Erst Stick neu formatieren (liefert einen
|
||||
# Mountpunkt), dann Apples 'asr restore' das eigentliche Image drauf
|
||||
# kopieren lassen -- das re-formatiert dabei nochmal komplett nach dem
|
||||
# Schema des Quell-Images (--erase). Etablierte Community-Technik fuer
|
||||
# macOS-Installer bis Catalina ohne createinstallmedia.
|
||||
echo "Quelle: ${DMG_PATH}"
|
||||
echo "Ziel: /dev/${TARGET_DISK}"
|
||||
echo
|
||||
@@ -458,3 +573,351 @@ fi
|
||||
|
||||
echo
|
||||
ok "Alles erledigt."
|
||||
exit 0
|
||||
|
||||
fi
|
||||
|
||||
# ======================================================================
|
||||
# ================ MODUS 2: MULTIBOOT-STICK ==========================
|
||||
# ======================================================================
|
||||
|
||||
MAX_MULTIBOOT=8
|
||||
|
||||
title "Multiboot-Stick — Schritt 1/5: Versionen auswaehlen"
|
||||
|
||||
echo "Baue die Auswahlliste (Apple-Server + lokale Bibliothek)..."
|
||||
echo
|
||||
|
||||
HAVE_APPLE=0
|
||||
if command -v softwareupdate >/dev/null 2>&1 && fetch_apple_list; then
|
||||
HAVE_APPLE=1
|
||||
fi
|
||||
|
||||
HAVE_LIB=0
|
||||
echo
|
||||
read -r -p "Pfad zur lokalen Downloads-Bibliothek pruefen [Enter fuer ${DEFAULT_LIB_DIR}, '-' zum Ueberspringen]: " LIB_DIR_IN
|
||||
if [[ "$LIB_DIR_IN" != "-" ]]; then
|
||||
LIB_DIR="${LIB_DIR_IN:-$DEFAULT_LIB_DIR}"
|
||||
[[ "$LIB_DIR" == */ ]] || LIB_DIR="${LIB_DIR}/"
|
||||
if fetch_library_list "$LIB_DIR"; then
|
||||
HAVE_LIB=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $HAVE_APPLE -eq 0 && $HAVE_LIB -eq 0 ]]; then
|
||||
err "Weder Apple-Server noch lokale Bibliothek haben Installer geliefert. Abbruch."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Kombinierte Liste aufbauen: C_SRC/C_TITLE/C_VERSION/C_SIZE/C_LIBIDX
|
||||
C_SRC=()
|
||||
C_TITLE=()
|
||||
C_VERSION=()
|
||||
C_SIZE=()
|
||||
C_LIBIDX=()
|
||||
|
||||
if [[ $HAVE_APPLE -eq 1 ]]; then
|
||||
for i in "${!A_TITLES[@]}"; do
|
||||
C_SRC+=("apple")
|
||||
C_TITLE+=("${A_TITLES[$i]}")
|
||||
C_VERSION+=("${A_VERSIONS[$i]}")
|
||||
C_SIZE+=("${A_SIZES[$i]}")
|
||||
C_LIBIDX+=("-1")
|
||||
done
|
||||
fi
|
||||
if [[ $HAVE_LIB -eq 1 ]]; then
|
||||
for i in "${!L_DIRS[@]}"; do
|
||||
C_SRC+=("library")
|
||||
C_TITLE+=("${L_TITLES[$i]}")
|
||||
C_VERSION+=("${L_VERSIONS[$i]}")
|
||||
C_SIZE+=("${L_SIZES[$i]}")
|
||||
C_LIBIDX+=("$i")
|
||||
done
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Verfuegbare Versionen (Mehrfachauswahl):"
|
||||
echo
|
||||
for i in "${!C_TITLE[@]}"; do
|
||||
tag="[Apple, Download]"
|
||||
mode_desc=""
|
||||
if [[ "${C_SRC[$i]}" == "library" ]]; then
|
||||
tag="[Bibliothek, offline]"
|
||||
lidx="${C_LIBIDX[$i]}"
|
||||
mode_desc=" -- bis Catalina, robust"
|
||||
[[ "${L_MODES[$lidx]}" == "recovery" ]] && mode_desc=" -- Big Sur+, ueber installer-pkg"
|
||||
fi
|
||||
sgb="$(bytes_to_gb_ceil "${C_SIZE[$i]}")"
|
||||
printf " %s%2d)%s %-22s %s (%s) -- ~%s GB%s\n" "$BOLD" "$((i+1))" "$RESET" "$tag" "${C_TITLE[$i]}" "${C_VERSION[$i]}" "$sgb" "$mode_desc"
|
||||
done
|
||||
echo
|
||||
printf "%s0)%s Abbrechen\n" "$BOLD" "$RESET"
|
||||
echo
|
||||
echo "Mehrere Nummern durch Komma trennen, z.B. 1,3,4 (mind. 2, max. ${MAX_MULTIBOOT})."
|
||||
read -r -p "Welche Versionen fuer den Multiboot-Stick? " MCHOICE_RAW
|
||||
|
||||
if [[ "$MCHOICE_RAW" == "0" || -z "$MCHOICE_RAW" ]]; then
|
||||
echo "Abgebrochen."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
IFS=',' read -r -a MCHOICE_PARTS <<<"$MCHOICE_RAW"
|
||||
SEL_IDX=()
|
||||
for part in "${MCHOICE_PARTS[@]}"; do
|
||||
part="$(echo "$part" | tr -d '[:space:]')"
|
||||
[[ -z "$part" ]] && continue
|
||||
if ! [[ "$part" =~ ^[0-9]+$ ]] || (( part < 1 || part > ${#C_TITLE[@]} )); then
|
||||
err "Ungueltige Nummer: '${part}'."
|
||||
exit 1
|
||||
fi
|
||||
SEL_IDX+=("$((part-1))")
|
||||
done
|
||||
|
||||
if (( ${#SEL_IDX[@]} < 2 )); then
|
||||
err "Fuer nur eine Version reicht der Einzel-Modus (Option 1 im Startmenue)."
|
||||
exit 1
|
||||
fi
|
||||
if (( ${#SEL_IDX[@]} > MAX_MULTIBOOT )); then
|
||||
err "Zu viele Versionen gewaehlt (${#SEL_IDX[@]}), Maximum ist ${MAX_MULTIBOOT}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
ok "Gewaehlt: ${#SEL_IDX[@]} Versionen fuer den Multiboot-Stick."
|
||||
|
||||
# ---------- Schritt 2/5: Payload je Version besorgen ----------
|
||||
title "Schritt 2/5: Installer-Payloads besorgen"
|
||||
|
||||
M_WRITE_MODE=()
|
||||
M_APP_PATH=()
|
||||
M_DMG_PATH=()
|
||||
M_SIZE_BYTES=()
|
||||
M_TITLE=()
|
||||
M_LABEL=()
|
||||
|
||||
n=1
|
||||
for ci in "${SEL_IDX[@]}"; do
|
||||
t="${C_TITLE[$ci]}"
|
||||
v="${C_VERSION[$ci]}"
|
||||
src="${C_SRC[$ci]}"
|
||||
echo
|
||||
echo "${BOLD}--- (${n}/${#SEL_IDX[@]}) ${t} (${v}) ---${RESET}"
|
||||
|
||||
lib_dir="" lib_mode="" lib_fn=""
|
||||
if [[ "$src" == "library" ]]; then
|
||||
lidx="${C_LIBIDX[$ci]}"
|
||||
lib_dir="${L_DIRS[$lidx]}"
|
||||
lib_mode="${L_MODES[$lidx]}"
|
||||
lib_fn="${L_FILENAMES[$lidx]}"
|
||||
fi
|
||||
|
||||
if ! resolve_payload "$src" "$t" "$v" "$lib_dir" "$lib_mode" "$lib_fn"; then
|
||||
err "Konnte Payload fuer '${t}' nicht beschaffen — Auswahl wird komplett abgebrochen (noch nichts am Stick veraendert)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
M_WRITE_MODE+=("$RESOLVED_WRITE_MODE")
|
||||
M_APP_PATH+=("$RESOLVED_APP_PATH")
|
||||
M_DMG_PATH+=("$RESOLVED_DMG_PATH")
|
||||
M_SIZE_BYTES+=("$RESOLVED_SIZE_BYTES")
|
||||
M_TITLE+=("$t")
|
||||
M_LABEL+=("$(make_label "$n" "$t")")
|
||||
n=$((n+1))
|
||||
done
|
||||
|
||||
# ---------- Schritt 3/5: Ziel-USB-Stick waehlen ----------
|
||||
title "Schritt 3/5: USB-Stick auswaehlen"
|
||||
|
||||
echo "Externe, physische Laufwerke (nur ganze Platten, keine Partitionen):"
|
||||
echo
|
||||
if ! list_external_disks; then
|
||||
err "Keine externen physischen Laufwerke gefunden. Stick eingesteckt?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for i in "${!D_IDS[@]}"; do
|
||||
printf " %s%2d)%s %s\n" "$BOLD" "$((i+1))" "$RESET" "${D_DESCS[$i]}"
|
||||
done
|
||||
echo
|
||||
printf "%s0)%s Abbrechen\n" "$BOLD" "$RESET"
|
||||
echo
|
||||
warn "ACHTUNG: Der gewaehlte Stick wird KOMPLETT GELOESCHT (alle Partitionen)."
|
||||
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 > ${#D_IDS[@]} )); then
|
||||
err "Ungueltige Auswahl."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARGET_DISK="${D_IDS[$((DCHOICE-1))]}"
|
||||
TARGET_DESC="${D_DESCS[$((DCHOICE-1))]}"
|
||||
TARGET_SIZE_BYTES="${D_SIZE_BYTES[$((DCHOICE-1))]}"
|
||||
|
||||
# ---------- Groessen-Check ----------
|
||||
# Puffer je Partition: cim braucht etwas Kopfraum ueber der reinen App-
|
||||
# Groesse (createinstallmedia kopiert BaseSystem etc. dazu), asr braucht
|
||||
# kaum mehr als die Quellgroesse. Zusaetzlich Mindestgroessen, damit
|
||||
# kleine/kaputte Groessenangaben nicht zu einer zu kleinen Partition fuehren.
|
||||
TOTAL_NEEDED=0
|
||||
declare -a PART_SIZE_BYTES=()
|
||||
for i in "${!M_TITLE[@]}"; do
|
||||
raw="${M_SIZE_BYTES[$i]}"
|
||||
[[ "$raw" =~ ^[0-9]+$ ]] || raw=0
|
||||
if [[ "${M_WRITE_MODE[$i]}" == "cim" ]]; then
|
||||
need=$(( raw + raw / 3 )) # +33% Puffer
|
||||
min=$(( 14 * 1000 * 1000 * 1000 ))
|
||||
else
|
||||
need=$(( raw + raw / 20 )) # +5% Puffer
|
||||
min=$(( 6 * 1000 * 1000 * 1000 ))
|
||||
fi
|
||||
(( need < min )) && need=$min
|
||||
PART_SIZE_BYTES+=("$need")
|
||||
TOTAL_NEEDED=$(( TOTAL_NEEDED + need ))
|
||||
done
|
||||
|
||||
# ~1GB Reserve fuer EFI-Partition + GPT-Tabellen.
|
||||
GPT_OVERHEAD=$(( 1 * 1000 * 1000 * 1000 ))
|
||||
TOTAL_NEEDED_WITH_OVERHEAD=$(( TOTAL_NEEDED + GPT_OVERHEAD ))
|
||||
|
||||
echo
|
||||
echo "Benoetigter Platz (mit Puffer):"
|
||||
for i in "${!M_TITLE[@]}"; do
|
||||
printf " - %-30s ~%s GB (%s)\n" "${M_TITLE[$i]}" "$(bytes_to_gb_ceil "${PART_SIZE_BYTES[$i]}")" "${M_WRITE_MODE[$i]}"
|
||||
done
|
||||
echo " Gesamt (inkl. EFI-Reserve): ~$(bytes_to_gb_ceil "$TOTAL_NEEDED_WITH_OVERHEAD") GB"
|
||||
echo " Stick-Kapazitaet: ~$(bytes_to_gb_ceil "$TARGET_SIZE_BYTES") GB"
|
||||
echo
|
||||
|
||||
if (( TOTAL_NEEDED_WITH_OVERHEAD > TARGET_SIZE_BYTES )); then
|
||||
err "Der gewaehlte Stick ist zu klein fuer diese Auswahl. Kleinere/weniger Versionen waehlen oder groesseren Stick nehmen."
|
||||
echo "Es wurde NICHTS am Stick veraendert."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
warn "Letzte Sicherheitsabfrage — dieser Stick wird GELOESCHT und in ${#M_TITLE[@]} Partitionen aufgeteilt:"
|
||||
echo " ${TARGET_DESC}"
|
||||
for i in "${!M_TITLE[@]}"; do
|
||||
echo " -> ${M_LABEL[$i]} (${M_TITLE[$i]})"
|
||||
done
|
||||
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/5: Partitionieren ----------
|
||||
title "Schritt 4/5: Stick wird partitioniert"
|
||||
|
||||
# diskutil partitionDisk <device> <#Partitionen> GPT [Format Name Groesse] ...
|
||||
# Letzte Partition bekommt 'R' (restlicher Platz) statt einer festen
|
||||
# Groesse -- vermeidet Rundungsverluste und nutzt automatisch den vollen
|
||||
# verbleibenden Stick fuer die letzte gewaehlte Version.
|
||||
PART_ARGS=()
|
||||
n_parts=${#M_TITLE[@]}
|
||||
for i in "${!M_TITLE[@]}"; do
|
||||
if (( i == n_parts - 1 )); then
|
||||
PART_ARGS+=("JHFS+" "${M_LABEL[$i]}" "R")
|
||||
else
|
||||
gb="$(bytes_to_gb_ceil "${PART_SIZE_BYTES[$i]}")"
|
||||
PART_ARGS+=("JHFS+" "${M_LABEL[$i]}" "${gb}G")
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Partitioniere /dev/${TARGET_DISK} in ${n_parts} Partitionen (braucht sudo)..."
|
||||
echo
|
||||
sudo diskutil partitionDisk "/dev/${TARGET_DISK}" "${n_parts}" GPT "${PART_ARGS[@]}"
|
||||
if [[ $? -ne 0 ]]; then
|
||||
err "diskutil partitionDisk fehlgeschlagen — siehe Ausgabe oben."
|
||||
exit 1
|
||||
fi
|
||||
ok "Partitionierung abgeschlossen."
|
||||
|
||||
# ---------- Schritt 5/5: Jede Partition beschreiben ----------
|
||||
title "Schritt 5/5: Partitionen werden beschrieben"
|
||||
|
||||
FAILED_TITLES=()
|
||||
OK_TITLES=()
|
||||
|
||||
for i in "${!M_TITLE[@]}"; do
|
||||
t="${M_TITLE[$i]}"
|
||||
label="${M_LABEL[$i]}"
|
||||
vol="/Volumes/${label}"
|
||||
|
||||
echo
|
||||
echo "${BOLD}--- $((i+1))/${n_parts}: ${t} -> ${label} ---${RESET}"
|
||||
|
||||
if [[ ! -d "$vol" ]]; then
|
||||
err "Erwartetes Volume ${vol} nicht gefunden — ueberspringe ${t}."
|
||||
FAILED_TITLES+=("$t")
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "${M_WRITE_MODE[$i]}" == "cim" ]]; then
|
||||
cim_bin="${M_APP_PATH[$i]}/Contents/Resources/createinstallmedia"
|
||||
echo "createinstallmedia laeuft gegen ${vol} (braucht sudo, kann 10-30 Minuten dauern)..."
|
||||
sudo "$cim_bin" --volume "$vol" --nointeraction
|
||||
rc=$?
|
||||
else
|
||||
echo "asr restore laeuft gegen ${vol} (braucht sudo, kann 10-20 Minuten dauern)..."
|
||||
sudo asr restore --source "${M_DMG_PATH[$i]}" --target "$vol" --erase --noprompt
|
||||
rc=$?
|
||||
fi
|
||||
|
||||
if [[ $rc -eq 0 ]]; then
|
||||
ok "${t}: fertig."
|
||||
OK_TITLES+=("$t")
|
||||
else
|
||||
err "${t}: fehlgeschlagen (Code ${rc})."
|
||||
FAILED_TITLES+=("$t")
|
||||
echo
|
||||
read -r -p "Trotzdem mit den restlichen Versionen fortfahren? [j/N]: " CONT
|
||||
if [[ ! "$CONT" =~ ^[jJ]$ ]]; then
|
||||
err "Abgebrochen — restliche Partitionen wurden NICHT beschrieben."
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
hr
|
||||
if (( ${#OK_TITLES[@]} > 0 )); then
|
||||
ok "Erfolgreich geschrieben: ${OK_TITLES[*]}"
|
||||
fi
|
||||
if (( ${#FAILED_TITLES[@]} > 0 )); then
|
||||
err "Fehlgeschlagen/uebersprungen: ${FAILED_TITLES[*]}"
|
||||
fi
|
||||
echo
|
||||
echo "Am Ziel-Mac: beim Start Option/Alt gedrueckt halten — alle erfolgreich"
|
||||
echo "geschriebenen Versionen erscheinen einzeln im Boot-Picker."
|
||||
|
||||
# Aufraeumen: heruntergeladene/entpackte Apps (mehrere GB je Version).
|
||||
CLEAN_APPS=()
|
||||
for i in "${!M_TITLE[@]}"; do
|
||||
[[ "${M_WRITE_MODE[$i]}" == "cim" ]] || continue
|
||||
[[ -n "${M_APP_PATH[$i]}" ]] || continue
|
||||
CLEAN_APPS+=("${M_APP_PATH[$i]}")
|
||||
done
|
||||
if (( ${#CLEAN_APPS[@]} > 0 )); then
|
||||
echo
|
||||
read -r -p "Heruntergeladene/entpackte Installer-Apps (${#CLEAN_APPS[@]} Stueck, mehrere GB) jetzt loeschen? [j/N]: " CLEAN
|
||||
if [[ "$CLEAN" =~ ^[jJ]$ ]]; then
|
||||
for app in "${CLEAN_APPS[@]}"; do
|
||||
sudo rm -rf "$app"
|
||||
done
|
||||
ok "Aufgeraeumt."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
if (( ${#FAILED_TITLES[@]} > 0 )); then
|
||||
exit 1
|
||||
fi
|
||||
ok "Alles erledigt."
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user