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