diff --git a/configs/releng/airootfs/usr/local/bin/fws-windows-deploy b/configs/releng/airootfs/usr/local/bin/fws-windows-deploy new file mode 100644 index 0000000..6114728 --- /dev/null +++ b/configs/releng/airootfs/usr/local/bin/fws-windows-deploy @@ -0,0 +1,287 @@ +#!/bin/bash +# ============================================================ +# fws-windows-deploy — déploie Windows 11 en OFFLINE-APPLY sur un disque dédié, +# depuis le live FWS (appelé en %post --nochroot, cible sous /mnt/sysroot). +# +# Windows Setup ne tourne JAMAIS : on carve les partitions, on applique +# install.wim (wimlib), on rend bootable (BCD), on injecte l'answer file offline +# + le composant gameboot, on provisionne l'hibernation FWS. +# +# SÉCURITÉ (risque n°1 = effacer le mauvais NVMe) : garde-fous FAIL-CLOSED — +# état PARSÉ (jamais sourcé → pas d'injection), disque cible VALIDÉ contre la +# liste des disques réels, comparé à TOUS les disques protégés (racine FWS +# multi-PV incluse, ESP, média ISO, support live), aucun label FWS résiduel +# ailleurs, adressage des partitions par NUMÉRO sur le disque cible (jamais par +# by-partlabel global) avec vérif du parent. Le moindre doute → abort. Échec → +# DEPLOY_FAILED, aucune entrée UEFI piégeuse, FWS reste seul bootable. +# +# ⚠ NON VALIDÉ SUR MATÉRIEL RÉEL (déploiement Windows réel + BCD). +# ============================================================ +set -u + +STATE=/tmp/fws-windows +SYSROOT=/mnt/sysroot +while [ $# -gt 0 ]; do + case "$1" in + --state) STATE="${2:-}"; shift 2 ;; + --sysroot) SYSROOT="${2:-}"; shift 2 ;; + *) shift ;; + esac +done + +log(){ printf '\e[36m[win-deploy]\e[0m %s\n' "$*"; } +warn(){ printf '\e[33m[win-deploy] %s\e[0m\n' "$*" >&2; } +status(){ mkdir -p "$SYSROOT/var/lib/fws" 2>/dev/null; echo "$1" > "$SYSROOT/var/lib/fws/windows-deploy.status" 2>/dev/null || true; } +die(){ printf '\e[31m[win-deploy] ABORT : %s\e[0m\n' "$*" >&2; status "DEPLOY_FAILED: $*"; exit 1; } +canon(){ readlink -f "$1" 2>/dev/null || printf '%s' "$1"; } +# Device de la partition N sur le disque D (nvme/mmc → pN ; sata → N). +part_dev(){ case "$1" in *[0-9]) printf '%sp%s' "$1" "$2";; *) printf '%s%s' "$1" "$2";; esac; } + +# --- 0) État du spoke : PARSÉ en clé=valeur littéral (JAMAIS sourcé) [M3] ----- +[ -r "$STATE" ] || { log "pas de $STATE → dual-boot non demandé, rien à faire."; exit 0; } +enabled=0; disk=""; size_gib=0; iso=""; edition=""; confirm_erase=0 +while IFS='=' read -r k v; do + case "$k" in + enabled) enabled="$v" ;; + disk) disk="$v" ;; + size_gib) size_gib="$v" ;; + iso) iso="$v" ;; + edition) edition="$v" ;; + confirm_erase) confirm_erase="$v" ;; + esac +done < "$STATE" +[ "${enabled:-0}" = 1 ] || { log "dual-boot désactivé, rien à faire."; exit 0; } +[ "${size_gib:-0}" -eq "${size_gib:-0}" ] 2>/dev/null || die "taille non numérique" +log "Dual-boot Windows demandé : disque=$disk taille=${size_gib}Gio iso=$iso" + +# --- Résolveur : TOUS les disques physiques sous un montage (fail-closed) [M1] +fws_disks_of(){ + local src + src="$(findmnt -no SOURCE "$1" 2>/dev/null)" || return 1 + [ -n "$src" ] || return 1 + lsblk -spno NAME,TYPE "$src" 2>/dev/null | awk '$2=="disk"{print $1}' +} + +# --- 1) Garde-fous AVANT toute écriture (fail-closed) ----------------------- +WIN_DISK="$disk" +[ -b "$WIN_DISK" ] || die "cible '$WIN_DISK' n'est pas un disque bloc" +# [M3] 'disk' DOIT être un vrai disque physique (neutralise toute valeur piégée). +lsblk -dpno NAME,TYPE 2>/dev/null | awk '$2=="disk"{print $1}' | grep -qxF "$WIN_DISK" \ + || die "cible '$WIN_DISK' absente de la liste des disques physiques" +findmnt -no SOURCE "$SYSROOT" >/dev/null 2>&1 || die "$SYSROOT non monté" +[ -f "$iso" ] || die "ISO introuvable : $iso" +[ "${size_gib:-0}" -ge 100 ] || die "taille < 100 Gio" + +# Swap d'hibernation (RAM+VRAM+marge, plancher 96) — calculé ICI pour le garde taille [M7]. +MEM=$(awk '/^MemTotal/{print int($2/1024)}' /proc/meminfo) +VRAM=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null | awk '{s+=$1}END{print s+0}') +SWAP_GIB=$(( (MEM + VRAM + 4096 + 1023) / 1024 )); [ "$SWAP_GIB" -lt 96 ] && SWAP_GIB=96 + +# Le disque cible ne doit être AUCUN disque protégé. On sonde uniquement de VRAIS +# montages [m1], et on compare à TOUS les disques porteurs [M1]. Resolver muet → abort. +WIN_C="$(canon "$WIN_DISK")" +for probe in "$SYSROOT" "$SYSROOT/boot/efi" "$SYSROOT/boot" /run/archiso/bootmnt; do + findmnt -M "$probe" >/dev/null 2>&1 || continue + mapfile -t pds < <(fws_disks_of "$probe") + [ "${#pds[@]}" -gt 0 ] || die "resolver muet sur '$probe' → abort (fail-closed)" + for d in "${pds[@]}"; do + [ "$WIN_C" != "$(canon "$d")" ] || die "$WIN_DISK == disque protégé ($probe via $d) — refus d'effacer" + done +done +# L'ISO ne doit pas vivre sur la cible — fail-closed si le média est indéterminable [m2]. +iso_mnt="$(df --output=target "$iso" 2>/dev/null | tail -1)" +[ -n "$iso_mnt" ] || die "média de l'ISO indéterminable → abort (fail-closed)" +mapfile -t isods < <(fws_disks_of "$iso_mnt") +[ "${#isods[@]}" -gt 0 ] || die "resolver muet sur le média ISO → abort" +for d in "${isods[@]}"; do + [ "$WIN_C" != "$(canon "$d")" ] || die "l'ISO vit sur la cible $WIN_DISK — refus" +done +# Taille : NTFS + ESP + MSR + swap doivent tenir [M7]. +DGIB=$(( $(lsblk -dbno SIZE "$WIN_DISK" 2>/dev/null || echo 0) / 1073741824 )) +[ "$DGIB" -gt 0 ] && [ "$size_gib" -le $(( DGIB - SWAP_GIB - 4 )) ] \ + || die "taille ${size_gib} + swap ${SWAP_GIB} + réserve dépasse le disque ${DGIB} Gio" + +# [B1] Aucun label FWS résiduel ne doit exister sur un AUTRE disque (collision +# by-partlabel = risque d'écrire ailleurs). On refuse plutôt que de deviner. +while read -r dev lbl; do + case "$lbl" in + FWSWIN|WINESP|FWSSWAP) + pk="/dev/$(lsblk -no PKNAME "$dev" 2>/dev/null | head -1)" + [ "$(canon "$pk")" = "$WIN_C" ] \ + || die "label '$lbl' déjà présent sur $pk (≠ cible) — nettoie-le d'abord (wipefs -a $pk)" ;; + esac +done < <(lsblk -lnpo NAME,PARTLABEL 2>/dev/null) +log "Garde-fous OK : $WIN_DISK ($DGIB Gio) sûr à dédier à Windows (swap ${SWAP_GIB} Gio)." + +# --- 2) Neutraliser toute activation du disque cible [M2] ------------------- +log "Neutralisation des partitions de $WIN_DISK…" +for part in $(lsblk -lnpo NAME "$WIN_DISK" 2>/dev/null | tail -n +2); do + umount "$part" 2>/dev/null || true + swapoff "$part" 2>/dev/null || true + for vg in $(pvs --noheadings -o vg_name "$part" 2>/dev/null); do vgchange -an "$vg" 2>/dev/null || true; done + for h in $(lsblk -lno NAME "$part" 2>/dev/null | tail -n +2); do + case "$(lsblk -no TYPE "/dev/$h" 2>/dev/null | head -1)" in + raid*) mdadm --stop "/dev/$h" 2>/dev/null || true ;; + crypt) cryptsetup close "$h" 2>/dev/null || true ;; + esac + done +done +# Arrays md dont le disque cible est lui-même membre (détection par colonne TYPE). +for md in $(lsblk -lnpo NAME,TYPE "$WIN_DISK" 2>/dev/null | awk '$2 ~ /raid/{print $1}'); do + mdadm --stop "$md" 2>/dev/null || true +done +# Re-vérif fail-closed : plus rien d'actif sur le disque cible. +for part in $(lsblk -lnpo NAME "$WIN_DISK" 2>/dev/null | tail -n +2); do + findmnt -S "$part" >/dev/null 2>&1 && die "$part encore monté après neutralisation" + swapon --show=NAME --noheadings 2>/dev/null | grep -qx "$part" && die "$part encore en swap" + [ -n "$(lsblk -lno NAME "$part" 2>/dev/null | tail -n +2)" ] && die "$part a encore des holders (md/dm) actifs" +done + +# --- 3) Carve (mode DEDICATE) — adressage par NUMÉRO sur le disque cible [B1] +log "Carve : ESP 1Gio + MSR 16Mio + NTFS ${size_gib}Gio + swap ${SWAP_GIB}Gio" +sgdisk --zap-all "$WIN_DISK" || die "sgdisk --zap-all a échoué" +sgdisk -n1:0:+1GiB -t1:ef00 -c1:WINESP "$WIN_DISK" || die "création WINESP" +sgdisk -n2:0:+16MiB -t2:0c01 -c2:MSR "$WIN_DISK" || warn "MSR non créée (non bloquant)" +sgdisk -n3:0:+${size_gib}GiB -t3:0700 -c3:FWSWIN "$WIN_DISK" || die "création partition Windows" +sgdisk -n4:0:+${SWAP_GIB}GiB -t4:8200 -c4:FWSSWAP "$WIN_DISK" || die "création swap" +partprobe "$WIN_DISK"; udevadm settle +ESP_PART="$(part_dev "$WIN_DISK" 1)" +WIN_PART="$(part_dev "$WIN_DISK" 3)" +SWAP_PART="$(part_dev "$WIN_DISK" 4)" +for p in "$ESP_PART" "$WIN_PART" "$SWAP_PART"; do + [ -b "$p" ] || die "partition $p absente après carve" + pk="/dev/$(lsblk -no PKNAME "$p" 2>/dev/null | head -1)" + [ "$(canon "$pk")" = "$WIN_C" ] || die "$p n'est PAS sur $WIN_DISK — abort" +done +mkfs.fat -F32 -n WINESP "$ESP_PART" || die "mkfs.fat WINESP" +mkswap -L FWSSWAP "$SWAP_PART" || die "mkswap FWSSWAP" +mkntfs -Q -f -L FWSWIN "$WIN_PART" || mkntfs -f -L FWSWIN "$WIN_PART" || die "mkntfs FWSWIN" + +# --- 4) Appliquer install.wim (wimlib) sur la NTFS -------------------------- +ISO_MNT="$(mktemp -d)"; WIN_MNT="$(mktemp -d)"; ESP_MNT="$(mktemp -d)" +cleanup(){ for m in "$WIN_MNT" "$ESP_MNT" "$ISO_MNT"; do umount "$m" 2>/dev/null || true; done; } +trap cleanup EXIT +mount -o loop,ro "$iso" "$ISO_MNT" || die "montage ISO échoué" +IMG="$ISO_MNT/sources/install.wim"; [ -f "$IMG" ] || IMG="$ISO_MNT/sources/install.esd" +[ -f "$IMG" ] || die "install.wim/.esd introuvable dans l'ISO" +# Édition par NOM (insensible à la casse) ; repli image unique [m4]. +WANT="${edition:-Windows 11 Pro}" +IDX="$(wimlib-imagex info "$IMG" 2>/dev/null | awk -v w="$(printf '%s' "$WANT" | tr 'A-Z' 'a-z')" -F': *' ' + tolower($1) ~ /^index/{i=$2} index(tolower($0), w){print i; exit}')" +if [ -z "$IDX" ]; then + CNT="$(wimlib-imagex info "$IMG" 2>/dev/null | awk -F': *' 'tolower($1) ~ /image count/{print $2; exit}')" + [ "${CNT:-0}" = 1 ] && IDX=1 +fi +[ -n "$IDX" ] || die "édition « $WANT » absente de l'ISO (wimlib-imagex info) — ISO Home/N/tiny ?" +log "Application de l'image (index $IDX = $WANT) sur $WIN_PART — long…" +wimlib-imagex apply "$IMG" "$IDX" "$WIN_PART" || die "wimlib-imagex apply a échoué" + +# --- 5) Bootabilité : BCD (hivex → repli WinPE-bcdboot) --------------------- +mount "$WIN_PART" "$WIN_MNT" || die "montage NTFS échoué" +mount "$ESP_PART" "$ESP_MNT" || die "montage WINESP échoué" +mkdir -p "$ESP_MNT/EFI/Microsoft/Boot" +cp -rn "$WIN_MNT/Windows/Boot/EFI/." "$ESP_MNT/EFI/Microsoft/Boot/" 2>/dev/null || true +WIN_PARTUUID="$(blkid -s PARTUUID -o value "$WIN_PART" 2>/dev/null)" +DISK_GUID="$(sgdisk -p "$WIN_DISK" 2>/dev/null | sed -n 's/^Disk identifier (GUID): *//p')" +if /usr/local/lib/fws/fws-bcd-write "$ESP_MNT" "$WIN_MNT" "$WIN_PARTUUID" "$DISK_GUID"; then + log "BCD écrit (voie hivex)." + BCDFIX=0 +else + warn "Voie hivex non validée → repli WinPE-bcdboot (1 reboot auto)." + umount "$WIN_MNT" 2>/dev/null || true + /usr/local/bin/fws-windows-bcdfix "$ISO_MNT" "$ESP_MNT" "$WIN_DISK" 1 \ + || die "repli WinPE-bcdfix a échoué — Windows non bootable (FWS reste seul bootable)" + BCDFIX=1 + mount "$WIN_PART" "$WIN_MNT" 2>/dev/null || die "remontage NTFS échoué" +fi + +# --- 6) Anti-BitLocker HORS LIGNE (crucial : évite le lockout) -------------- +HIVE="$WIN_MNT/Windows/System32/config/SYSTEM" +if command -v hivexget >/dev/null 2>&1 && [ -f "$HIVE" ]; then + SEL="$(hivexget "$HIVE" Select Current 2>/dev/null | tr -cd '0-9')"; [ -n "$SEL" ] || SEL=1 + printf '[HKEY_LOCAL_MACHINE\\SYSTEM\\ControlSet%03d\\Control\\BitLocker]\n"PreventDeviceEncryption"=dword:00000001\n' "$SEL" \ + | hivexregedit --merge --prefix 'HKEY_LOCAL_MACHINE\SYSTEM' "$HIVE" \ + && log "Anti-BitLocker offline posé (ControlSet$(printf '%03d' "$SEL"))." \ + || warn "écriture anti-BitLocker offline échouée (ceinture reg add en specialize)." +else + warn "hivex/SYSTEM absent → anti-BitLocker via la ceinture specialize seule." +fi + +# --- 7) Answer file offline + injection du composant gameboot --------------- +TMPL=/usr/local/share/fws/gameboot-windows/unattend.offline.xml.tmpl +HN="$(tr -cd 'a-zA-Z0-9-' < /tmp/fws-hostname 2>/dev/null)"; [ -n "$HN" ] || HN=fws-win +LOC="$(. "$SYSROOT/etc/locale.conf" 2>/dev/null; printf '%s' "${LANG%%.*}")"; [ -n "$LOC" ] || LOC=fr-CH +LOC="${LOC/_/-}" +FIRSTBOOT='C:\ProgramData\FWS\deploy\FirstBoot-FwsGameboot.ps1' +mkdir -p "$WIN_MNT/Windows/Panther" "$WIN_MNT/ProgramData/FWS/deploy" +sed -e "s|__HOSTNAME__|$HN|g" -e "s|__USER__|gaming|g" -e "s|__DISPLAY__|Gaming|g" \ + -e "s|__LOCALE__|$LOC|g" -e "s|__INPUT__|$LOC|g" -e "s|__FIRSTBOOT__|${FIRSTBOOT//\\/\\\\}|g" \ + "$TMPL" > "$WIN_MNT/Windows/Panther/unattend.xml" || die "rendu unattend.xml échoué" +cp -rf /usr/local/share/fws/gameboot-windows/. "$WIN_MNT/ProgramData/FWS/deploy/" || die "injection gameboot échouée" +rm -f "$WIN_MNT/ProgramData/FWS/deploy/unattend.offline.xml.tmpl" 2>/dev/null || true +log "unattend.xml + composant gameboot injectés sur la NTFS." + +# --- 8) Vérification AVANT toute écriture NVRAM ----------------------------- +[ -f "$WIN_MNT/Windows/System32/ntoskrnl.exe" ] || die "ntoskrnl.exe absent → apply incomplet" +[ -f "$WIN_MNT/Windows/System32/winload.efi" ] || die "winload.efi absent" +[ -f "$ESP_MNT/EFI/Microsoft/Boot/bootmgfw.efi" ] || die "bootmgfw.efi absent de l'ESP" +[ "$BCDFIX" = 1 ] || [ -s "$ESP_MNT/EFI/Microsoft/Boot/BCD" ] || die "BCD absent/vide (voie hivex)" +sync + +# --- 9) Entrées de boot : FWS TOUJOURS en tête [M5][M6] --------------------- +# Voie hivex directe : on crée l'entrée Windows persistante (en queue). +if [ "$BCDFIX" = 0 ]; then + efibootmgr -c -d "$WIN_DISK" -p 1 -L 'Windows Boot Manager' -l '\EFI\Microsoft\Boot\bootmgfw.efi' >/dev/null 2>&1 || warn "création entrée UEFI Windows échouée" + WBN="$(efibootmgr 2>/dev/null | sed -n 's/^Boot\([0-9A-Fa-f]\{4\}\)\*\? *Windows Boot Manager.*/\1/p' | head -1)" + [ -n "$WBN" ] && sed -i "s/^FWS_WINDOWS_BOOTNUM=.*/FWS_WINDOWS_BOOTNUM=\"$WBN\"/" \ + "$SYSROOT/etc/fws/gameboot.conf" 2>/dev/null || true +fi +# Dans TOUS les cas : FWS en BootOrder[0] (Windows/WinPE jamais 1er). Si aucune +# entrée NVRAM FWS (booté via \EFI\BOOT\BOOTX64.EFI removable), en créer une. +FBN="$(efibootmgr 2>/dev/null | sed -n 's/^Boot\([0-9A-Fa-f]\{4\}\)\*\? *\(FWS\|GRUB\).*/\1/p' | head -1)" +if [ -z "$FBN" ]; then + esp_src="$(findmnt -no SOURCE "$SYSROOT/boot/efi" 2>/dev/null)" + if [ -b "$esp_src" ]; then + sdisk="/dev/$(lsblk -no PKNAME "$esp_src" 2>/dev/null | head -1)" + spn="$(lsblk -no PARTN "$esp_src" 2>/dev/null | head -1)" + efibootmgr -c -d "$sdisk" -p "${spn:-1}" -L 'FWS' -l '\EFI\BOOT\BOOTX64.EFI' >/dev/null 2>&1 || true + FBN="$(efibootmgr 2>/dev/null | sed -n 's/^Boot\([0-9A-Fa-f]\{4\}\)\*\? *FWS.*/\1/p' | head -1)" + fi +fi +order="$(efibootmgr 2>/dev/null | sed -n 's/^BootOrder: //p' | tr -d '[:space:]')" +if [ -n "$FBN" ] && [ -n "$order" ]; then + rest="$(printf '%s' "$order" | tr ',' '\n' | grep -vx "$FBN" | paste -sd, -)" + efibootmgr -o "${FBN}${rest:+,$rest}" >/dev/null 2>&1 || true # FWS 1er, reste (Windows/WinPE) derrière +fi + +# --- 10) Hibernation FWS (chroot cible) : swap dédié → resume= -------------- +SWAP_UUID="$(blkid -s UUID -o value "$SWAP_PART" 2>/dev/null)" +if [ -n "$SWAP_UUID" ] && command -v arch-chroot >/dev/null 2>&1; then + log "Config hibernation FWS (resume=UUID=$SWAP_UUID)…" + cat > "$SYSROOT/etc/fws-gameboot-swap.sh" </dev/null || printf 'UUID=%s none swap defaults 0 0\n' "$SWAP_UUID" >> /etc/fstab +grep -qE '^HOOKS=.*\bresume\b' /etc/mkinitcpio.conf || sed -i -E 's/^(HOOKS=\(.*)\bfilesystems\b/\1resume filesystems/' /etc/mkinitcpio.conf +mkinitcpio -P || true +if grep -q '^GRUB_CMDLINE_LINUX_DEFAULT=' /etc/default/grub; then + grep -q 'resume=UUID=$SWAP_UUID' /etc/default/grub || sed -i 's|^GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"|GRUB_CMDLINE_LINUX_DEFAULT="\1 resume=UUID=$SWAP_UUID hibernate.compressor=lz4"|' /etc/default/grub +fi +grep -q '^GRUB_DISABLE_OS_PROBER=' /etc/default/grub || echo 'GRUB_DISABLE_OS_PROBER=true' >> /etc/default/grub +grub-mkconfig -o /boot/grub/grub.cfg || true +install -Dm644 /dev/stdin /etc/modprobe.d/nvidia-hibernate.conf <<'NV' +options nvidia NVreg_PreserveVideoMemoryAllocations=1 NVreg_TemporaryFilePath=/var/tmp +NV +systemctl enable nvidia-suspend.service nvidia-hibernate.service nvidia-resume.service 2>/dev/null || true +systemctl enable fws-gameboot-bootfix.service 2>/dev/null || true +CHROOT + arch-chroot "$SYSROOT" bash /etc/fws-gameboot-swap.sh 2>&1 | tee -a /tmp/fws-windows-deploy.log || warn "config hibernation chroot partielle" + rm -f "$SYSROOT/etc/fws-gameboot-swap.sh" +else + warn "swap UUID introuvable ou arch-chroot absent → hibernation non configurée (reboot simple restera dispo)." +fi + +sync +status "DEPLOY_OK: Windows sur $WIN_DISK (${size_gib}Gio) — SB à activer au firmware" +log "Déploiement Windows terminé.$([ "$BCDFIX" = 1 ] && echo ' (BCD via WinPE au 1er reboot)')" +log "⚠ Reste manuel : activer Secure Boot au firmware (sbctl verify déjà clean)." +exit 0