Compare commits
58 Commits
0.6.0
...
Hypervisor
| Author | SHA1 | Date | |
|---|---|---|---|
|
e313fc4dde
|
|||
|
384a45f70e
|
|||
|
89e1321ab7
|
|||
|
e2dd1abc18
|
|||
|
d050af8279
|
|||
|
64763c3707
|
|||
|
2e2108d2b8
|
|||
|
d4e479cc15
|
|||
|
d592b24858
|
|||
|
9836925a37
|
|||
|
47d59e51fe
|
|||
|
6a1ce74169
|
|||
|
fa3b953d99
|
|||
|
d47686efd9
|
|||
|
fe9b30b745
|
|||
|
7a4f36c916
|
|||
|
e3a0b988a5
|
|||
|
05a44af161
|
|||
|
4bc9ccc1f7
|
|||
|
d1fa5143e8
|
|||
|
39baaa67e6
|
|||
|
976e24876d
|
|||
|
ce6e9533c2
|
|||
|
4d15dc2632
|
|||
|
e811cce88d
|
|||
|
0dc5329d90
|
|||
|
fd21293725
|
|||
|
e69c071969
|
|||
|
3b47ef30e7
|
|||
|
e5bc082049
|
|||
|
8d1b6bf23b
|
|||
|
3f6fb08af6
|
|||
|
c90e48f14f
|
|||
|
226af87379
|
|||
|
8e3bd2b53c
|
|||
|
af7ed4e469
|
|||
|
a1be0e6d49
|
|||
|
afe884b9e4
|
|||
|
2bee8ed359
|
|||
|
e6c0224db9
|
|||
|
057898524a
|
|||
|
11a531003e
|
|||
|
9b80effc77
|
|||
|
810a98fc34
|
|||
|
96da52f0e1
|
|||
|
beec38a899
|
|||
|
3cb4132727
|
|||
|
367233eae5
|
|||
|
3d91ef3ac5
|
|||
|
bc84731290
|
|||
|
e5b36d6c07
|
|||
|
585c4691a1
|
|||
|
732082144a
|
|||
|
ae8140ccff
|
|||
|
f3ffcb62b8
|
|||
|
7730ac7b00
|
|||
|
bee59a2fb3
|
|||
|
00d34dfc49
|
@@ -22,6 +22,11 @@ Added
|
|||||||
- **firewalld** activé par défaut sur le système installé.
|
- **firewalld** activé par défaut sur le système installé.
|
||||||
- Bureaux Hyprland et i3 à parité visuelle (waybar/polybar, wofi/rofi, Catppuccin Mocha), thème SDDM « fws »,
|
- Bureaux Hyprland et i3 à parité visuelle (waybar/polybar, wofi/rofi, Catppuccin Mocha), thème SDDM « fws »,
|
||||||
bannière GDM.
|
bannière GDM.
|
||||||
|
- **Choix du nom de machine à l'installation** : champ « Nom de machine » dans la page Utilisateur d'anaconda,
|
||||||
|
appliqué au système installé (``/etc/hostname``, repli ``fws``).
|
||||||
|
- **Script de build interactif** (``build-offi.sh``) : version (``major.minor.build``) et variante (``cli``, ``gnome``,
|
||||||
|
``kde``…) demandées sur l'hôte avant d'entrer dans le conteneur, ISO nommée
|
||||||
|
``fws-<version>-<variante>-<AAAA.MM.JJ>.iso``, logo FWS dans fastfetch.
|
||||||
|
|
||||||
Changed
|
Changed
|
||||||
-------
|
-------
|
||||||
@@ -31,6 +36,7 @@ Changed
|
|||||||
Fedora/Arch sur le splash et le journal de récupération.
|
Fedora/Arch sur le splash et le journal de récupération.
|
||||||
- Toutes les entrées de boot plafonnent les workers udev bloqués (``udev.event_timeout=45``) ; le menu BIOS démarre
|
- Toutes les entrées de boot plafonnent les workers udev bloqués (``udev.event_timeout=45``) ; le menu BIOS démarre
|
||||||
explicitement l'entrée FWS (``DEFAULT fws``).
|
explicitement l'entrée FWS (``DEFAULT fws``).
|
||||||
|
- ``systemd-timesyncd`` explicitement activé sur le système installé pour éviter la dérive d'horloge (TLS, pacman).
|
||||||
- Use xz compression for the baseline profile's bootstrap tarball so that it has more differences when compared to the
|
- Use xz compression for the baseline profile's bootstrap tarball so that it has more differences when compared to the
|
||||||
releng profile.
|
releng profile.
|
||||||
- Match network interfaces by their type, instead of name, in systemd-networkd configuration.
|
- Match network interfaces by their type, instead of name, in systemd-networkd configuration.
|
||||||
@@ -48,6 +54,11 @@ Fixed
|
|||||||
le cap udev et les entrées de boot FWS d'ISO précédentes).
|
le cap udev et les entrées de boot FWS d'ISO précédentes).
|
||||||
- DNS du système installé (NetworkManager + systemd-resolved), hook ``lvm2`` dans l'initramfs, GRUB de secours
|
- DNS du système installé (NetworkManager + systemd-resolved), hook ``lvm2`` dans l'initramfs, GRUB de secours
|
||||||
``--removable`` en UEFI.
|
``--removable`` en UEFI.
|
||||||
|
- Disposition clavier dérivée de la locale complète et non de la seule langue : ``fr_CH`` donne désormais ``ch(fr)``
|
||||||
|
au lieu de ``fr``, appliqué à la console (``vconsole.conf``) et à X11 (via langtable) ; anaconda écrivait par
|
||||||
|
ailleurs un ``/etc/hostname`` vide sur le système installé.
|
||||||
|
- DNS dans le conteneur de build : ``podman run --network=host`` pour passer par la pile réseau de l'hôte
|
||||||
|
(corrige les timeouts avec firewalld/NAT).
|
||||||
- Use ``systemd-run`` to invoke ``curl`` with a transient service that depends on ``network-online.target`` since no
|
- Use ``systemd-run`` to invoke ``curl`` with a transient service that depends on ``network-online.target`` since no
|
||||||
services are currently pulling in ``network_online.target``.
|
services are currently pulling in ``network_online.target``.
|
||||||
|
|
||||||
|
|||||||
@@ -353,6 +353,31 @@ echo "==> Correction des retours à la ligne Windows (dos2unix)..."
|
|||||||
pacman -S --needed --noconfirm dos2unix
|
pacman -S --needed --noconfirm dos2unix
|
||||||
find /tmp/fws-build/releng -type f -exec dos2unix {} + 2>/dev/null
|
find /tmp/fws-build/releng -type f -exec dos2unix {} + 2>/dev/null
|
||||||
|
|
||||||
|
# ── Pré-vol : résolution de TOUS les paquets du profil ──
|
||||||
|
# « error: target not found » ne doit plus surgir en plein mkarchiso : on
|
||||||
|
# résout la liste complète (packages.x86_64 fusionné) contre le pacman.conf
|
||||||
|
# du profil (fws-local injecté inclus) dans une base temporaire. S'il manque
|
||||||
|
# un paquet, on échoue ICI avec la liste + le geste correctif.
|
||||||
|
echo "==> Pré-vol : résolution des paquets du profil..."
|
||||||
|
PREFLIGHT_DB="$(mktemp -d)"
|
||||||
|
pacman -Sy --config /tmp/fws-build/releng/pacman.conf --dbpath "$PREFLIGHT_DB" >/dev/null
|
||||||
|
mapfile -t PROFILE_PKGS < <(sed -e 's/#.*//' -e 's/[[:space:]]//g' -e '/^$/d' \
|
||||||
|
/tmp/fws-build/releng/packages.x86_64)
|
||||||
|
# -Sp n'installe rien (affiche les URLs) ; LC_ALL=C fige le motif d'erreur.
|
||||||
|
# stderr → capture, stdout → poubelle ; le pipeline sed sort toujours 0.
|
||||||
|
MISSING="$(LC_ALL=C pacman -Sp --config /tmp/fws-build/releng/pacman.conf \
|
||||||
|
--dbpath "$PREFLIGHT_DB" "${PROFILE_PKGS[@]}" 2>&1 >/dev/null \
|
||||||
|
| sed -n 's/^error: target not found:[[:space:]]*//p')"
|
||||||
|
rm -rf "$PREFLIGHT_DB"
|
||||||
|
if [ -n "$MISSING" ]; then
|
||||||
|
echo -e "\e[31mPaquets introuvables dans les dépôts du profil :\e[0m" >&2
|
||||||
|
printf ' \e[31m%s\e[0m\n' $MISSING >&2
|
||||||
|
echo -e "\e[33mDépôt fws-local absent/incomplet ? Lance ./setup-aur.sh (construit" >&2
|
||||||
|
echo -e "anaconda, winboat-bin, fws-dnd, …) puis relance ce script.\e[0m" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "==> Pré-vol OK : ${#PROFILE_PKGS[@]} paquets résolus."
|
||||||
|
|
||||||
echo "==> Lancement de mkarchiso..."
|
echo "==> Lancement de mkarchiso..."
|
||||||
mkdir -p "$OUT_DIR"
|
mkdir -p "$OUT_DIR"
|
||||||
# mkarchiso fait un « rm » interne de l'ISO cible avant de la (ré)écrire ; si
|
# mkarchiso fait un « rm » interne de l'ISO cible avant de la (ré)écrire ; si
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+40
@@ -165,6 +165,40 @@ def _config_lines():
|
|||||||
return lines, desk, method
|
return lines, desk, method
|
||||||
|
|
||||||
|
|
||||||
|
def _windows_dualboot_lines(selected):
|
||||||
|
"""Section dual-boot Windows : lit /tmp/fws-windows (parse LITTÉRAL, jamais
|
||||||
|
sourcé), montre le disque qui SERA EFFACÉ et ALERTE en cas de conflit avec la
|
||||||
|
cible d'installation FWS (même disque choisi pour les deux)."""
|
||||||
|
state = {}
|
||||||
|
try:
|
||||||
|
with open("/tmp/fws-windows") as fh:
|
||||||
|
for line in fh:
|
||||||
|
if "=" in line and not line.lstrip().startswith("#"):
|
||||||
|
key, _, val = line.partition("=")
|
||||||
|
state[key.strip()] = val.strip()
|
||||||
|
except OSError:
|
||||||
|
return []
|
||||||
|
if state.get("enabled") != "1":
|
||||||
|
return [" • Dual-boot Windows : désactivé (FWS uniquement)"]
|
||||||
|
disk = state.get("disk", "?")
|
||||||
|
size = state.get("size_gib", "?")
|
||||||
|
iso = state.get("iso", "")
|
||||||
|
iso_name = iso.rsplit("/", 1)[-1] if iso else "(aucune ISO fournie)"
|
||||||
|
lines = [
|
||||||
|
f" • Dual-boot Windows : ACTIVÉ — {disk} ({size} GiO NTFS)",
|
||||||
|
f" ISO : {iso_name}",
|
||||||
|
f" ⚠ TOUT le disque {disk} sera EFFACÉ et dédié à Windows.",
|
||||||
|
]
|
||||||
|
if state.get("intel_vmd") == "1":
|
||||||
|
lines.append(" • Support Intel VMD/RST : activé (pilote injecté au boot Windows)")
|
||||||
|
base = disk.rsplit("/", 1)[-1]
|
||||||
|
if base and base in selected:
|
||||||
|
lines.append(
|
||||||
|
f" ⛔ CONFLIT : {disk} est AUSSI une cible d'installation FWS —"
|
||||||
|
" choisissez des disques DIFFÉRENTS (sinon l'un écrase l'autre).")
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def _build_summary():
|
def _build_summary():
|
||||||
config_lines, desk, method = _config_lines()
|
config_lines, desk, method = _config_lines()
|
||||||
config_text = "\n".join(config_lines) \
|
config_text = "\n".join(config_lines) \
|
||||||
@@ -214,6 +248,9 @@ def _build_summary():
|
|||||||
extras = ("Steam, Wine, VR (Monado/OpenXR), winboat-bin"
|
extras = ("Steam, Wine, VR (Monado/OpenXR), winboat-bin"
|
||||||
if desk != "cli" else "rien de plus (choix CLI)")
|
if desk != "cli" else "rien de plus (choix CLI)")
|
||||||
|
|
||||||
|
win_text = "\n".join(_windows_dualboot_lines(selected)) \
|
||||||
|
or " • Dual-boot Windows : désactivé (FWS uniquement)"
|
||||||
|
|
||||||
return f"""\
|
return f"""\
|
||||||
══════════════════════════════════════════════════════════
|
══════════════════════════════════════════════════════════
|
||||||
FWS — RÉCAPITULATIF AVANT INSTALLATION
|
FWS — RÉCAPITULATIF AVANT INSTALLATION
|
||||||
@@ -226,6 +263,9 @@ DISQUES
|
|||||||
{disk_warn}
|
{disk_warn}
|
||||||
{disk_text}
|
{disk_text}
|
||||||
|
|
||||||
|
DUAL-BOOT WINDOWS (gaming — Valorant/LoL)
|
||||||
|
{win_text}
|
||||||
|
|
||||||
MATÉRIEL DÉTECTÉ
|
MATÉRIEL DÉTECTÉ
|
||||||
• Virtualisation : {virt}
|
• Virtualisation : {virt}
|
||||||
• Réseau : {net}
|
• Réseau : {net}
|
||||||
|
|||||||
+343
@@ -0,0 +1,343 @@
|
|||||||
|
# FWS — Spoke « Dual-boot Windows gaming » : case + disque + taille + ISO.
|
||||||
|
#
|
||||||
|
# POURQUOI ICI (airootfs, pas dans le paquet anaconda) : identique à
|
||||||
|
# fws_desktop.py / fws_recap.py — Anaconda découvre les spokes au RUNTIME via
|
||||||
|
# collect() (os.listdir du dossier des spokes puis import). Déposer ce .py dans
|
||||||
|
# le dossier des spokes via l'overlay airootfs suffit à le faire apparaître SANS
|
||||||
|
# rebuild du paquet anaconda (juste ./build.sh). Le glade associé est dans
|
||||||
|
# /usr/share/anaconda/ui/spokes/fws_windows.glade (aussi via l'airootfs).
|
||||||
|
#
|
||||||
|
# CATÉGORIE = SystemCategory (pas SoftwareCategory) : c'est du disque/boot, il
|
||||||
|
# vit sur le hub Résumé à côté de « Destination de l'installation », pas dans
|
||||||
|
# « Logiciel ».
|
||||||
|
#
|
||||||
|
# CE QU'IL FAIT : coche « Dual-boot Windows gaming », choix du disque à dédier à
|
||||||
|
# Windows, taille NTFS (défaut 200 GiO, min 100, max = taille du disque), chemin
|
||||||
|
# de l'ISO Windows 11 fournie par l'utilisateur. Il n'exécute AUCUNE opération
|
||||||
|
# disque : son seul canal vers l'installation est /tmp/fws-windows, relu par le
|
||||||
|
# kickstart (%post --nochroot : carve + wimlib apply + injection gameboot ;
|
||||||
|
# %post chroot : swap/resume + Secure Boot). Exactement le modèle fws_desktop.
|
||||||
|
#
|
||||||
|
# ⚠ Chemin /usr/lib/python3.14/... couplé à la version de Python d'Arch (3.14 au
|
||||||
|
# build d'anaconda) — à mettre à jour au bump majeur de Python (= rebuild
|
||||||
|
# anaconda de toute façon). SystemCategory / internes GUI (anaconda 45.8) à
|
||||||
|
# re-vérifier à chaque bump, comme l'avertit l'en-tête de fws_recap.py.
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
from pyanaconda.anaconda_loggers import get_module_logger
|
||||||
|
from pyanaconda.core import util
|
||||||
|
from pyanaconda.core.i18n import CN_, _
|
||||||
|
from pyanaconda.ui.categories.system import SystemCategory
|
||||||
|
from pyanaconda.ui.gui.spokes import NormalSpoke
|
||||||
|
|
||||||
|
log = get_module_logger(__name__)
|
||||||
|
|
||||||
|
__all__ = ["FWSWindowsSpoke"]
|
||||||
|
|
||||||
|
# Contrat de fichier relu par le kickstart (clé=valeur, une par ligne).
|
||||||
|
_STATE_FILE = "/tmp/fws-windows"
|
||||||
|
|
||||||
|
_DEFAULT_SIZE_GIB = 200
|
||||||
|
_MIN_SIZE_GIB = 100
|
||||||
|
# Marge à laisser sur le disque en fin de partition NTFS (ESP Windows + MSR).
|
||||||
|
_TAIL_RESERVE_GIB = 3
|
||||||
|
|
||||||
|
|
||||||
|
def _capture(cmd, args):
|
||||||
|
try:
|
||||||
|
return util.execWithCapture(cmd, args) or ""
|
||||||
|
except (OSError, RuntimeError):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _list_disks():
|
||||||
|
"""[(devnode, libellé, taille_gib)] pour chaque disque physique (lsblk)."""
|
||||||
|
disks = []
|
||||||
|
for line in _capture("lsblk", ["-dpno", "NAME,SIZE,MODEL,TYPE"]).splitlines():
|
||||||
|
parts = line.split()
|
||||||
|
# NAME SIZE [MODEL les mots du milieu] TYPE ; TYPE = dernier champ.
|
||||||
|
if len(parts) < 2 or parts[-1] != "disk":
|
||||||
|
continue
|
||||||
|
name = parts[0]
|
||||||
|
size = parts[1]
|
||||||
|
model = " ".join(parts[2:-1]) or "?"
|
||||||
|
label = f"{name} — {size} {model}"
|
||||||
|
disks.append((name, label, _disk_size_gib(name)))
|
||||||
|
return disks
|
||||||
|
|
||||||
|
|
||||||
|
def _disk_size_gib(devnode):
|
||||||
|
"""Taille du disque en GiO entiers (0 si inconnue)."""
|
||||||
|
raw = _capture("lsblk", ["-dbno", "SIZE", devnode]).strip().split("\n")[0]
|
||||||
|
try:
|
||||||
|
return int(raw) // (1024 ** 3)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _disk_nonempty(devnode):
|
||||||
|
"""True si le disque porte des partitions (donc des données à écraser)."""
|
||||||
|
return "part" in _capture("lsblk", ["-rno", "TYPE", devnode]).split()
|
||||||
|
|
||||||
|
|
||||||
|
def _hib_reserve_gib():
|
||||||
|
"""Réserve de queue = swap d'hibernation (RAM+VRAM+marge) + ESP/MSR, ALIGNÉE
|
||||||
|
sur fws-windows-deploy (qui exige size_gib <= disque − swap − 4). Sans ça, le
|
||||||
|
GUI proposerait une taille NTFS que le déployeur refuserait (ou qui déborde)."""
|
||||||
|
mem = 0
|
||||||
|
try:
|
||||||
|
with open("/proc/meminfo") as fh:
|
||||||
|
for line in fh:
|
||||||
|
if line.startswith("MemTotal"):
|
||||||
|
mem = int(line.split()[1]) // (1024 * 1024)
|
||||||
|
break
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
vram = 0
|
||||||
|
for tok in _capture(
|
||||||
|
"nvidia-smi",
|
||||||
|
["--query-gpu=memory.total", "--format=csv,noheader,nounits"]).split():
|
||||||
|
if tok.isdigit():
|
||||||
|
vram += int(tok) // 1024
|
||||||
|
swap = max(96, mem + vram + 4)
|
||||||
|
return swap + 6 # + ESP (1 GiO) + MSR + marge
|
||||||
|
|
||||||
|
|
||||||
|
def _intel_vmd_present():
|
||||||
|
"""True si un contrôleur Intel VMD (Volume Management Device) est présent →
|
||||||
|
le Windows déployé aura besoin du pilote iaStorVD pour voir/booter les NVMe.
|
||||||
|
Sur AMD ou machine sans VMD : False (case décochée, no-op)."""
|
||||||
|
return "volume management device" in _capture("lspci", ["-nn"]).lower()
|
||||||
|
|
||||||
|
|
||||||
|
class FWSWindowsSpoke(NormalSpoke):
|
||||||
|
"""Dual-boot Windows gaming : écrit /tmp/fws-windows (enabled/disk/size/iso)."""
|
||||||
|
|
||||||
|
builderObjects = ["adj_size", "fwsWindowsWindow"]
|
||||||
|
mainWidgetName = "fwsWindowsWindow"
|
||||||
|
uiFile = "spokes/fws_windows.glade"
|
||||||
|
category = SystemCategory
|
||||||
|
icon = "applications-games-symbolic"
|
||||||
|
title = CN_("GUI|Spoke", "_Dual-boot Windows")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_screen_id():
|
||||||
|
return "fws-windows-dualboot"
|
||||||
|
|
||||||
|
def __init__(self, *args):
|
||||||
|
NormalSpoke.__init__(self, *args)
|
||||||
|
# État courant (persisté dans /tmp/fws-windows par apply()).
|
||||||
|
self._enabled = False
|
||||||
|
self._disk = ""
|
||||||
|
self._size = _DEFAULT_SIZE_GIB
|
||||||
|
self._iso = ""
|
||||||
|
self._uefi = os.path.exists("/sys/firmware/efi")
|
||||||
|
self._disks = []
|
||||||
|
# Réserve de queue alignée sur le swap d'hibernation du déployeur [M7].
|
||||||
|
self._reserve = _hib_reserve_gib()
|
||||||
|
# Support Intel VMD/RST : auto-coché si un contrôleur VMD est détecté.
|
||||||
|
self._vmd = _intel_vmd_present()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ cycle
|
||||||
|
def initialize(self):
|
||||||
|
NormalSpoke.initialize(self)
|
||||||
|
self._chk = self.builder.get_object("chk_enable")
|
||||||
|
self._box = self.builder.get_object("box_options")
|
||||||
|
self._combo = self.builder.get_object("combo_disk")
|
||||||
|
self._adj = self.builder.get_object("adj_size")
|
||||||
|
self._spin = self.builder.get_object("spin_size")
|
||||||
|
self._entry = self.builder.get_object("entry_iso")
|
||||||
|
self._btn = self.builder.get_object("btn_iso")
|
||||||
|
self._warn = self.builder.get_object("iso_warn")
|
||||||
|
self._note = self.builder.get_object("note_platform")
|
||||||
|
self._vmd_chk = self.builder.get_object("chk_vmd")
|
||||||
|
|
||||||
|
# Peuple le combo des disques (une fois).
|
||||||
|
self._disks = _list_disks()
|
||||||
|
for name, label, _gib in self._disks:
|
||||||
|
self._combo.append(name, label)
|
||||||
|
|
||||||
|
# Reprend un choix déjà écrit (revisite du spoke).
|
||||||
|
self._load_state()
|
||||||
|
|
||||||
|
# Câblage des signaux (pas de handler dans le .glade, comme fws_recap).
|
||||||
|
self._chk.connect("toggled", self._on_toggle)
|
||||||
|
self._combo.connect("changed", self._on_disk_changed)
|
||||||
|
self._entry.connect("changed", self._on_iso_changed)
|
||||||
|
self._btn.connect("clicked", self._on_browse)
|
||||||
|
|
||||||
|
# Sur BIOS (non-UEFI), le dual-boot est impossible : on grise tout.
|
||||||
|
if not self._uefi:
|
||||||
|
self._enabled = False
|
||||||
|
self._chk.set_sensitive(False)
|
||||||
|
self._note.set_visible(True)
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
# Reflète l'état courant dans les widgets.
|
||||||
|
self._chk.set_active(self._enabled)
|
||||||
|
if self._disk:
|
||||||
|
self._combo.set_active_id(self._disk)
|
||||||
|
# AUCUNE présélection par défaut : sans choix explicite de l'utilisateur,
|
||||||
|
# `completed` refuse (il exige `disk`) → jamais d'effacement « par
|
||||||
|
# accident » d'un disque en cliquant simplement Suivant. (Bug corrigé :
|
||||||
|
# set_active(0) ciblait le 1er disque lsblk — souvent un disque de jeux.)
|
||||||
|
self._update_size_bounds()
|
||||||
|
self._spin.set_value(self._size)
|
||||||
|
self._entry.set_text(self._iso)
|
||||||
|
self._box.set_sensitive(self._enabled and self._uefi)
|
||||||
|
self._vmd_chk.set_active(self._vmd)
|
||||||
|
self._vmd_chk.set_sensitive(self._enabled and self._uefi)
|
||||||
|
self._validate()
|
||||||
|
|
||||||
|
def apply(self):
|
||||||
|
# Relit les widgets et persiste pour le kickstart.
|
||||||
|
self._enabled = self._chk.get_active() and self._uefi
|
||||||
|
self._disk = self._combo.get_active_id() or ""
|
||||||
|
self._size = int(self._spin.get_value())
|
||||||
|
self._iso = self._entry.get_text().strip()
|
||||||
|
self._vmd = self._vmd_chk.get_active()
|
||||||
|
self._save_state()
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- handlers
|
||||||
|
def _on_toggle(self, _btn):
|
||||||
|
active = self._chk.get_active()
|
||||||
|
self._box.set_sensitive(active)
|
||||||
|
self._vmd_chk.set_sensitive(active)
|
||||||
|
self._validate()
|
||||||
|
|
||||||
|
def _on_disk_changed(self, _combo):
|
||||||
|
self._update_size_bounds()
|
||||||
|
self._validate()
|
||||||
|
|
||||||
|
def _on_iso_changed(self, _entry):
|
||||||
|
self._validate()
|
||||||
|
|
||||||
|
def _on_browse(self, _btn):
|
||||||
|
dialog = Gtk.FileChooserNative.new(
|
||||||
|
_("Select the Windows 11 ISO"), self.window,
|
||||||
|
Gtk.FileChooserAction.OPEN, _("_Select"), _("_Cancel"))
|
||||||
|
flt = Gtk.FileFilter()
|
||||||
|
flt.set_name(_("ISO images"))
|
||||||
|
flt.add_pattern("*.iso")
|
||||||
|
flt.add_pattern("*.ISO")
|
||||||
|
dialog.add_filter(flt)
|
||||||
|
# Démarrer sur les médias amovibles montés (clé USB avec l'ISO).
|
||||||
|
for start in ("/run/media", "/mnt", os.path.expanduser("~")):
|
||||||
|
if os.path.isdir(start):
|
||||||
|
dialog.set_current_folder(start)
|
||||||
|
break
|
||||||
|
if dialog.run() == Gtk.ResponseType.ACCEPT:
|
||||||
|
path = dialog.get_filename()
|
||||||
|
if path:
|
||||||
|
self._entry.set_text(path)
|
||||||
|
dialog.destroy()
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------- helpers
|
||||||
|
def _update_size_bounds(self):
|
||||||
|
"""Borne haute du spin = taille du disque choisi − réserve ESP/MSR."""
|
||||||
|
name = self._combo.get_active_id()
|
||||||
|
gib = 0
|
||||||
|
for dn, _label, dgib in self._disks:
|
||||||
|
if dn == name:
|
||||||
|
gib = dgib
|
||||||
|
break
|
||||||
|
upper = max(_MIN_SIZE_GIB, gib - self._reserve) if gib else 2000
|
||||||
|
self._adj.set_lower(_MIN_SIZE_GIB)
|
||||||
|
self._adj.set_upper(upper)
|
||||||
|
if self._spin.get_value() > upper:
|
||||||
|
self._spin.set_value(upper)
|
||||||
|
|
||||||
|
def _validate(self):
|
||||||
|
"""Message inline (jamais bloquant) : reflète la validité des choix."""
|
||||||
|
if not self._chk.get_active():
|
||||||
|
self._warn.set_text("")
|
||||||
|
return
|
||||||
|
iso = self._entry.get_text().strip()
|
||||||
|
msgs = []
|
||||||
|
if not self._combo.get_active_id():
|
||||||
|
msgs.append(_("Select the disk to dedicate to Windows."))
|
||||||
|
if not iso:
|
||||||
|
msgs.append(_("Provide the path to your Windows 11 ISO."))
|
||||||
|
elif not os.path.isfile(iso):
|
||||||
|
msgs.append(_("ISO not found: %s") % iso)
|
||||||
|
if msgs:
|
||||||
|
self._warn.set_text("⚠ " + " ".join(msgs))
|
||||||
|
else:
|
||||||
|
disk = self._combo.get_active_id()
|
||||||
|
extra = _(" — ⚠ THIS DISK CONTAINS DATA") if _disk_nonempty(disk) else ""
|
||||||
|
self._warn.set_text(
|
||||||
|
(_("⚠ The whole disk %s will be ERASED and dedicated to Windows.")
|
||||||
|
% disk) + extra)
|
||||||
|
|
||||||
|
def _load_state(self):
|
||||||
|
try:
|
||||||
|
with open(_STATE_FILE) as fh:
|
||||||
|
data = fh.read()
|
||||||
|
except OSError:
|
||||||
|
return
|
||||||
|
for line in data.splitlines():
|
||||||
|
if "=" not in line:
|
||||||
|
continue
|
||||||
|
key, _, val = line.partition("=")
|
||||||
|
key = key.strip()
|
||||||
|
val = val.strip()
|
||||||
|
if key == "enabled":
|
||||||
|
self._enabled = val == "1"
|
||||||
|
elif key == "disk":
|
||||||
|
self._disk = val
|
||||||
|
elif key == "size_gib" and val.isdigit():
|
||||||
|
self._size = int(val)
|
||||||
|
elif key == "iso":
|
||||||
|
self._iso = val
|
||||||
|
elif key == "intel_vmd":
|
||||||
|
self._vmd = val == "1"
|
||||||
|
|
||||||
|
def _save_state(self):
|
||||||
|
lines = [
|
||||||
|
"# Écrit par le spoke FWS « Dual-boot Windows ». Relu par le kickstart.",
|
||||||
|
"enabled=%d" % (1 if self._enabled else 0),
|
||||||
|
"disk=%s" % self._disk,
|
||||||
|
"size_gib=%d" % self._size,
|
||||||
|
"iso=%s" % self._iso,
|
||||||
|
"intel_vmd=%d" % (1 if self._vmd else 0),
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
with open(_STATE_FILE, "w") as fh:
|
||||||
|
fh.write("\n".join(lines) + "\n")
|
||||||
|
except OSError as exc:
|
||||||
|
log.warning("FWS windows: écriture %s impossible : %s",
|
||||||
|
_STATE_FILE, exc)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------- propriétés
|
||||||
|
@property
|
||||||
|
def status(self):
|
||||||
|
if not self._uefi:
|
||||||
|
return _("Unavailable (requires UEFI)")
|
||||||
|
if not self._enabled:
|
||||||
|
return _("Disabled (FWS only)")
|
||||||
|
iso = os.path.basename(self._iso) if self._iso else _("no ISO")
|
||||||
|
return _("Windows on %(disk)s — %(size)d GiB — %(iso)s") % {
|
||||||
|
"disk": self._disk or "?", "size": self._size, "iso": iso}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def completed(self):
|
||||||
|
# Désactivé = choix valide → jamais bloquant. Activé = exige disque +
|
||||||
|
# ISO lisible (sinon l'écran passe « orange » : l'utilisateur voit qu'il
|
||||||
|
# manque quelque chose, mais l'install FWS reste possible).
|
||||||
|
if not self._enabled:
|
||||||
|
return True
|
||||||
|
return bool(self._disk) and bool(self._iso) and os.path.isfile(self._iso)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mandatory(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ready(self):
|
||||||
|
return True
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- FWS — onglet « Dual-boot Windows gaming » (case + disque + taille + ISO).
|
||||||
|
Calqué sur fws_desktop.glade (AnacondaSpokeWindow + ses enfants internes
|
||||||
|
nav_box/nav_area, alignment/action_area). Catégorie SYSTÈME côté .py :
|
||||||
|
l'écran vit à côté de « Destination de l'installation », pas dans « Logiciel ».
|
||||||
|
Les widgets sont pilotés/peuplés au runtime par fws_windows.py. -->
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk+" version="3.6"/>
|
||||||
|
<requires lib="AnacondaWidgets" version="1.0"/>
|
||||||
|
<!-- Bornes de la taille NTFS ; upper réévalué en Python selon le disque choisi. -->
|
||||||
|
<object class="GtkAdjustment" id="adj_size">
|
||||||
|
<property name="lower">100</property>
|
||||||
|
<property name="upper">2000</property>
|
||||||
|
<property name="value">200</property>
|
||||||
|
<property name="step-increment">10</property>
|
||||||
|
<property name="page-increment">50</property>
|
||||||
|
</object>
|
||||||
|
<object class="AnacondaSpokeWindow" id="fwsWindowsWindow">
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
|
<property name="window-name" translatable="yes">DUAL-BOOT WINDOWS</property>
|
||||||
|
<signal name="button-clicked" handler="on_back_clicked" swapped="no"/>
|
||||||
|
<child internal-child="main_box">
|
||||||
|
<object class="GtkBox" id="AnacondaSpokeWindow-main_box1">
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<child internal-child="nav_box">
|
||||||
|
<object class="GtkEventBox" id="AnacondaSpokeWindow-nav_box1">
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<child internal-child="nav_area">
|
||||||
|
<object class="GtkGrid" id="AnacondaSpokeWindow-nav_area1">
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="margin-left">6</property>
|
||||||
|
<property name="margin-right">6</property>
|
||||||
|
<property name="margin-top">6</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child internal-child="alignment">
|
||||||
|
<object class="GtkAlignment" id="AnacondaSpokeWindow-alignment1">
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="margin-top">12</property>
|
||||||
|
<property name="yalign">0</property>
|
||||||
|
<property name="xscale">0</property>
|
||||||
|
<property name="yscale">0</property>
|
||||||
|
<property name="bottom-padding">48</property>
|
||||||
|
<property name="left-padding">24</property>
|
||||||
|
<property name="right-padding">24</property>
|
||||||
|
<child internal-child="action_area">
|
||||||
|
<object class="GtkBox" id="AnacondaSpokeWindow-action_area1">
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">8</property>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="fws_windows_desc">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="xalign">0</property>
|
||||||
|
<property name="margin-bottom">6</property>
|
||||||
|
<property name="wrap">True</property>
|
||||||
|
<property name="max-width-chars">64</property>
|
||||||
|
<property name="label" translatable="yes">Install a bare-metal Windows 11 alongside FWS to play kernel-anticheat games (Valorant, League of Legends) that refuse virtualization. FWS deploys Windows automatically from the ISO you provide. Requires UEFI + TPM 2.0.</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<!-- Case maître : active/grise le bloc d'options ci-dessous -->
|
||||||
|
<child>
|
||||||
|
<object class="GtkCheckButton" id="chk_enable">
|
||||||
|
<property name="label" translatable="yes">Enable Windows gaming dual-boot</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="receives-default">False</property>
|
||||||
|
<property name="draw-indicator">True</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<!-- Bloc d'options (grisé tant que la case est décochée) -->
|
||||||
|
<child>
|
||||||
|
<object class="GtkGrid" id="box_options">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="sensitive">False</property>
|
||||||
|
<property name="margin-left">24</property>
|
||||||
|
<property name="margin-top">4</property>
|
||||||
|
<property name="row-spacing">8</property>
|
||||||
|
<property name="column-spacing">12</property>
|
||||||
|
|
||||||
|
<!-- Ligne 0 : disque -->
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="label_disk">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="xalign">0</property>
|
||||||
|
<property name="label" translatable="yes">Disk dedicated to Windows:</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="left-attach">0</property>
|
||||||
|
<property name="top-attach">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkComboBoxText" id="combo_disk">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="left-attach">1</property>
|
||||||
|
<property name="top-attach">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<!-- Ligne 1 : taille NTFS -->
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="label_size">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="xalign">0</property>
|
||||||
|
<property name="label" translatable="yes">Windows partition size (GiB):</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="left-attach">0</property>
|
||||||
|
<property name="top-attach">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkSpinButton" id="spin_size">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="adjustment">adj_size</property>
|
||||||
|
<property name="numeric">True</property>
|
||||||
|
<property name="value">200</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="left-attach">1</property>
|
||||||
|
<property name="top-attach">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<!-- Ligne 2 : ISO (champ libre + bouton Parcourir) -->
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="label_iso">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="xalign">0</property>
|
||||||
|
<property name="label" translatable="yes">Windows 11 ISO image:</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="left-attach">0</property>
|
||||||
|
<property name="top-attach">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="box_iso">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkEntry" id="entry_iso">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="placeholder-text" translatable="yes">/run/media/…/Win11_25H2.iso</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="btn_iso">
|
||||||
|
<property name="label" translatable="yes">Browse…</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="receives-default">True</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="left-attach">1</property>
|
||||||
|
<property name="top-attach">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<!-- Ligne 3 : avertissement de validation (rempli en Python) -->
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="iso_warn">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="xalign">0</property>
|
||||||
|
<property name="wrap">True</property>
|
||||||
|
<property name="max-width-chars">64</property>
|
||||||
|
<property name="label"></property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="left-attach">0</property>
|
||||||
|
<property name="top-attach">3</property>
|
||||||
|
<property name="width">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<!-- Support Intel VMD/RST : auto-coché si un contrôleur Intel VMD
|
||||||
|
est détecté. Injecte le pilote iaStorVD dans le Windows
|
||||||
|
déployé pour qu'il boote sur les machines Intel (BIOS en mode
|
||||||
|
VMD/RST). Aucun effet sur AMD. -->
|
||||||
|
<child>
|
||||||
|
<object class="GtkCheckButton" id="chk_vmd">
|
||||||
|
<property name="label" translatable="yes">Intel VMD/RST support (auto-detected — Intel machines only)</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="sensitive">False</property>
|
||||||
|
<property name="margin-left">24</property>
|
||||||
|
<property name="margin-top">4</property>
|
||||||
|
<property name="draw-indicator">True</property>
|
||||||
|
<property name="tooltip-text" translatable="yes">Injects the Intel VMD/RST driver into the deployed Windows so it boots on Intel systems with VMD enabled in the BIOS. Auto-detected; has no effect on AMD.</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">3</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<!-- Note plate-forme (affichée par le .py si BIOS/non-UEFI) -->
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="note_platform">
|
||||||
|
<property name="visible">False</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="xalign">0</property>
|
||||||
|
<property name="margin-top">8</property>
|
||||||
|
<property name="wrap">True</property>
|
||||||
|
<property name="max-width-chars">64</property>
|
||||||
|
<property name="label" translatable="yes">Dual-boot requires UEFI firmware (this machine booted in legacy BIOS mode). The option is disabled.</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">4</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child internal-child="accessible">
|
||||||
|
<object class="AtkObject" id="fwsWindowsWindow-atkobject">
|
||||||
|
<property name="AtkObject::accessible-name" translatable="yes">DUAL-BOOT WINDOWS</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
||||||
@@ -121,3 +121,13 @@ python-crypt_r
|
|||||||
# Namespace GI 'OSTree-1.0' : pyanaconda/rescue.py importe le payload rpm_ostree
|
# Namespace GI 'OSTree-1.0' : pyanaconda/rescue.py importe le payload rpm_ostree
|
||||||
# (inconditionnellement, même si FWS utilise liveimg) → fourni par 'ostree'.
|
# (inconditionnellement, même si FWS utilise liveimg) → fourni par 'ostree'.
|
||||||
ostree
|
ostree
|
||||||
|
|
||||||
|
# --- Gameboot : dual-boot Windows gaming (déploiement offline + Secure Boot) ---
|
||||||
|
# Outils universels utilisés par fws-windows-deploy (%post, hors ligne) et
|
||||||
|
# fws-secureboot-setup. NB : PAS de 'nvidia' ici (conditionnel via fws-setup-hardware).
|
||||||
|
wimlib
|
||||||
|
hivex
|
||||||
|
sbctl
|
||||||
|
sbsigntools
|
||||||
|
mokutil
|
||||||
|
binutils
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# ============================================================
|
||||||
|
# Configuration de l'orchestrateur FWS « gameboot » (dual-boot hibernate-swap)
|
||||||
|
# Lu par /usr/local/bin/fws-gameboot. Voir docs/hibernate-swap-dualboot.md.
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# Entrée de boot Windows (numéro Boot#### hex, ex. "0003").
|
||||||
|
# Vide = auto-détection de l'entrée « Windows Boot Manager » via efibootmgr.
|
||||||
|
FWS_WINDOWS_BOOTNUM=""
|
||||||
|
|
||||||
|
# Comportement si le préflight d'hibernation ÉCHOUE (ou si l'hibernation
|
||||||
|
# elle-même échoue) AVANT d'être entré dans Windows :
|
||||||
|
# reboot = on bascule quand même vers Windows par un REBOOT SIMPLE. La session
|
||||||
|
# FWS n'est pas préservée, mais on entre dans le jeu (c'est le "filet"
|
||||||
|
# demandé : ne jamais laisser l'utilisateur bloqué).
|
||||||
|
# abort = on annule, on efface BootNext, on RESTE sur FWS avec une erreur
|
||||||
|
# claire (choix prudent : jamais de bascule surprise).
|
||||||
|
FWS_HIBERNATE_FALLBACK="reboot"
|
||||||
|
|
||||||
|
# Marge de sécurité (Mo) ajoutée à (RAM + VRAM en usage) pour vérifier que
|
||||||
|
# l'image d'hibernation tiendra dans le swap. La VRAM des GPU récents (ex. 32 Go
|
||||||
|
# sur une RTX 5090) est recopiée dans l'image → le swap doit couvrir RAM+VRAM.
|
||||||
|
FWS_SWAP_MARGIN_MB=2048
|
||||||
|
|
||||||
|
# Jeu lancé par défaut si « fws-gameboot to-windows » est appelé sans --game.
|
||||||
|
# L'identifiant doit exister dans games.json côté Windows (valorant, lol, …).
|
||||||
|
FWS_DEFAULT_GAME="valorant"
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=FWS — réasserte BootOrder=FWS et le marqueur de retour Windows (gameboot)
|
||||||
|
Documentation=file:///usr/share/doc/fws/hibernate-swap-dualboot.md
|
||||||
|
# Jamais sur le live, uniquement sur un système installé UEFI.
|
||||||
|
ConditionPathExists=!/run/archiso
|
||||||
|
ConditionPathExists=/sys/firmware/efi
|
||||||
|
After=local-fs.target
|
||||||
|
# Ne pas retarder le boot : purement best-effort.
|
||||||
|
DefaultDependencies=no
|
||||||
|
Before=multi-user.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/fws-gameboot-bootfix
|
||||||
|
# Ne jamais faire échouer le boot si efivars indisponible.
|
||||||
|
SuccessExitStatus=0 1
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# fws-gameboot — orchestrateur dual-boot « hibernate-swap » (FWS ⇄ Windows)
|
||||||
|
#
|
||||||
|
# But : jouer à un jeu à anticheat kernel (Valorant/Vanguard) qui REFUSE la
|
||||||
|
# virtualisation, sans quitter durablement FWS. On hiberne FWS (RAM → swap), on
|
||||||
|
# arme un boot UEFI one-shot (BootNext) vers un Windows 11 BARE-METAL, et on
|
||||||
|
# redémarre. Windows tourne sur le vrai matériel (AUCUN hyperviseur) → Vanguard
|
||||||
|
# content. Au retour de Windows, le noyau reprend la session FWS via resume=.
|
||||||
|
#
|
||||||
|
# Ce N'EST PAS une VM et il n'y a AUCUN hyperviseur : c'est un dual-boot
|
||||||
|
# orchestré. Le « seamless » vient de l'hibernation (session restaurée), pas
|
||||||
|
# d'une exécution simultanée (impossible sans hyperviseur = ban Vanguard).
|
||||||
|
#
|
||||||
|
# Filet de sûreté PERMANENT : BootOrder[0] = FWS (posé par fws-gameboot-bootfix
|
||||||
|
# à chaque démarrage). BootNext ne sert QUE pour les transitions one-shot ; tout
|
||||||
|
# crash/reboot non géré retombe sur FWS.
|
||||||
|
#
|
||||||
|
# ⚠ Exige une VALIDATION MATÉRIELLE réelle (hibernation + resume GPU NVIDIA,
|
||||||
|
# sémantique BootNext du firmware). Voir docs/hibernate-swap-dualboot.md §7.
|
||||||
|
# ============================================================
|
||||||
|
set -u
|
||||||
|
|
||||||
|
CONF=/etc/fws/gameboot.conf
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
[ -r "$CONF" ] && . "$CONF"
|
||||||
|
: "${FWS_WINDOWS_BOOTNUM:=}"
|
||||||
|
: "${FWS_HIBERNATE_FALLBACK:=reboot}"
|
||||||
|
: "${FWS_SWAP_MARGIN_MB:=2048}"
|
||||||
|
: "${FWS_DEFAULT_GAME:=valorant}"
|
||||||
|
|
||||||
|
log() { printf '\e[36m[gameboot]\e[0m %s\n' "$*" >&2; }
|
||||||
|
warn() { printf '\e[33m[gameboot] %s\e[0m\n' "$*" >&2; }
|
||||||
|
die() { printf '\e[31m[gameboot] ERREUR : %s\e[0m\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Notifie l'utilisateur graphique (best-effort ; on tourne en root via pkexec).
|
||||||
|
notify_user() {
|
||||||
|
command -v notify-send >/dev/null 2>&1 || return 0
|
||||||
|
local u uid
|
||||||
|
while IFS=: read -r u _ uid _; do
|
||||||
|
[ "$uid" -ge 1000 ] && [ "$uid" -le 60000 ] || continue
|
||||||
|
[ -S "/run/user/$uid/bus" ] || continue
|
||||||
|
sudo -u "$u" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$uid/bus" \
|
||||||
|
notify-send -a FWS -u critical "FWS Gaming" "$1" 2>/dev/null || true
|
||||||
|
done < /etc/passwd
|
||||||
|
}
|
||||||
|
|
||||||
|
need_root() { [ "$(id -u)" -eq 0 ] || die "root requis — lance : pkexec fws-gameboot ${1:-}"; }
|
||||||
|
|
||||||
|
# --- Entrée UEFI « Windows Boot Manager » (numéro Boot####, HEX MAJUSCULE) ---
|
||||||
|
find_windows_bootnum() {
|
||||||
|
local n
|
||||||
|
if [ -n "$FWS_WINDOWS_BOOTNUM" ]; then n="$FWS_WINDOWS_BOOTNUM"; else
|
||||||
|
n="$(efibootmgr 2>/dev/null | sed -n 's/^Boot\([0-9A-Fa-f]\{4\}\)\*\? *Windows Boot Manager.*/\1/p' | head -1)"
|
||||||
|
fi
|
||||||
|
# Normaliser en MAJUSCULE : efibootmgr affiche « BootNext: 000A » en majuscule ;
|
||||||
|
# une valeur config en minuscule ferait échouer la relecture (m6).
|
||||||
|
printf '%s' "${n^^}"
|
||||||
|
}
|
||||||
|
# --- Notre propre entrée (FWS ou, à défaut, GRUB) ---
|
||||||
|
find_fws_bootnum() {
|
||||||
|
efibootmgr 2>/dev/null | sed -n 's/^Boot\([0-9A-Fa-f]\{4\}\)\*\? *\(FWS\|GRUB\).*/\1/p' | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- VRAM en usage (Mo), 0 si pas de NVIDIA — recopiée dans l'image S4 ---
|
||||||
|
vram_used_mb() {
|
||||||
|
command -v nvidia-smi >/dev/null 2>&1 || { echo 0; return; }
|
||||||
|
nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits 2>/dev/null \
|
||||||
|
| awk '{s+=$1} END{print s+0}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Renvoie 0 si tout est OK pour hiberner. Décrit chaque échec sur stderr.
|
||||||
|
preflight() {
|
||||||
|
local mem_kb swap_kb vram_mb need_mb have_mb
|
||||||
|
grep -q disk /sys/power/state 2>/dev/null || { warn "hibernation indisponible (/sys/power/state sans « disk »)"; return 1; }
|
||||||
|
grep -q 'resume=' /proc/cmdline || { warn "resume= absent de la cmdline noyau (hibernation non reprenable)"; return 1; }
|
||||||
|
swap_kb=$(awk '/^SwapTotal:/{print $2}' /proc/meminfo)
|
||||||
|
[ "${swap_kb:-0}" -gt 0 ] || { warn "aucun swap actif (rien où écrire l'image)"; return 1; }
|
||||||
|
mem_kb=$(awk '/^MemTotal:/{print $2}' /proc/meminfo)
|
||||||
|
vram_mb=$(vram_used_mb)
|
||||||
|
need_mb=$(( mem_kb/1024 + vram_mb + FWS_SWAP_MARGIN_MB ))
|
||||||
|
have_mb=$(( swap_kb/1024 ))
|
||||||
|
if [ "$have_mb" -lt "$need_mb" ]; then
|
||||||
|
warn "swap trop petit : ${have_mb} Mo < ${need_mb} Mo requis (RAM $((mem_kb/1024)) + VRAM ${vram_mb} + marge ${FWS_SWAP_MARGIN_MB})"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Résout le point de montage de l'ESP (FAT). Vide si aucun.
|
||||||
|
find_esp() {
|
||||||
|
findmnt -rno TARGET /boot/efi 2>/dev/null \
|
||||||
|
|| findmnt -rno TARGET /efi 2>/dev/null \
|
||||||
|
|| findmnt -rno TARGET /boot 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_doctor() {
|
||||||
|
local sb f w
|
||||||
|
sb=$(bootctl status 2>/dev/null | sed -n 's/^ *Secure Boot: *//p' | head -1)
|
||||||
|
f="$(find_fws_bootnum)"; w="$(find_windows_bootnum)"
|
||||||
|
echo "== fws-gameboot doctor =="
|
||||||
|
echo "Entrée FWS : ${f:-?}"
|
||||||
|
echo "Entrée Windows : ${w:-INTROUVABLE}"
|
||||||
|
echo "RAM : $(( $(awk '/^MemTotal:/{print $2}' /proc/meminfo)/1024 )) Mo"
|
||||||
|
echo "Swap : $(( $(awk '/^SwapTotal:/{print $2}' /proc/meminfo)/1024 )) Mo"
|
||||||
|
echo "VRAM en usage : $(vram_used_mb) Mo"
|
||||||
|
echo "resume= cmdline : $(grep -o 'resume=[^ ]*' /proc/cmdline || echo 'ABSENT')"
|
||||||
|
echo "Secure Boot : ${sb:-inconnu}"
|
||||||
|
echo -n "Préflight : "
|
||||||
|
if preflight 2>/dev/null; then printf '\e[32mOK\e[0m\n'; else printf '\e[31mÉCHEC\e[0m\n'; preflight; fi
|
||||||
|
case "$sb" in
|
||||||
|
enabled) ;;
|
||||||
|
*) warn "Secure Boot inactif → Vanguard refusera Windows (VAN9003). Configure d'abord : sudo fws-secureboot-setup" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_to_windows() {
|
||||||
|
need_root "to-windows"
|
||||||
|
local win esp tok bootnext_set="" rc m game="${FWS_DEFAULT_GAME:-valorant}" game_req=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--game) game="${2:-}"; game_req=1; shift 2 ;;
|
||||||
|
--game=*) game="${1#--game=}"; game_req=1; shift ;;
|
||||||
|
*) die "argument inconnu : $1 (usage : to-windows [--game <id>])" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
# Charte d'identifiant alignée sur les clés de games.json : [a-z0-9_-]
|
||||||
|
# (l'underscore doit passer, sinon divergence FWS↔Windows).
|
||||||
|
game="$(printf '%s' "$game" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_-')"
|
||||||
|
[ -n "$game_req" ] && [ -z "$game" ] && warn "identifiant --game invalide après nettoyage — jeu par défaut appliqué."
|
||||||
|
win="$(find_windows_bootnum)"
|
||||||
|
if [ -z "$win" ]; then
|
||||||
|
notify_user "Windows introuvable — installe d'abord le Windows gaming (entrée de boot UEFI absente)."
|
||||||
|
die "entrée « Windows Boot Manager » introuvable (Windows installé + entrée UEFI présente ?)"
|
||||||
|
fi
|
||||||
|
esp="$(find_esp)"
|
||||||
|
|
||||||
|
# Jeton « quel jeu lancer », lu ET CONSOMMÉ par fws-play côté Windows.
|
||||||
|
# Écriture ATOMIQUE (.tmp puis mv) sur l'ESP FAT uniquement. En cas d'échec,
|
||||||
|
# on PURGE tout jeton périmé (jamais de rejeu d'un ancien choix) → Windows
|
||||||
|
# lancera le jeu par défaut plutôt qu'un mauvais jeu.
|
||||||
|
if [ -n "$esp" ] && [ "$(findmnt -no FSTYPE "$esp" 2>/dev/null)" = vfat ]; then
|
||||||
|
if [ -n "$game" ]; then
|
||||||
|
mkdir -p "$esp/EFI/FWS" 2>/dev/null || true
|
||||||
|
tok="$esp/EFI/FWS/launch.json"
|
||||||
|
if printf '{"game":"%s"}\n' "$game" > "$tok.tmp" 2>/dev/null && mv -f "$tok.tmp" "$tok" 2>/dev/null; then
|
||||||
|
log "Jeu demandé : $game"
|
||||||
|
else
|
||||||
|
rm -f "$tok" "$tok.tmp" 2>/dev/null || true
|
||||||
|
warn "écriture du jeton échouée → jeton purgé (Windows lancera le jeu par défaut)."
|
||||||
|
notify_user "Choix du jeu non transmis — Windows lancera le jeu par défaut."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif [ -n "$game_req" ]; then
|
||||||
|
warn "ESP FAT introuvable → le choix du jeu ne peut pas être transmis à Windows."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Filet : ne JAMAIS laisser BootNext armé si on ne bascule pas réellement
|
||||||
|
# (sinon départ surprise vers Windows au prochain reboot manuel).
|
||||||
|
cleanup() { [ -n "$bootnext_set" ] && { efibootmgr -N >/dev/null 2>&1 || true; log "BootNext annulé (nettoyage)"; }; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Démonter toute NTFS montée : jamais rw persistante à travers le cycle.
|
||||||
|
# `findmnt -lno` (liste, PAS -r/raw) → chemins NON échappés (espaces OK) ; on
|
||||||
|
# se limite aux vrais types NTFS (pas fuseblk générique).
|
||||||
|
while IFS= read -r m; do
|
||||||
|
[ -n "$m" ] && umount "$m" 2>/dev/null || true
|
||||||
|
done < <(findmnt -lno TARGET -t ntfs,ntfs3 2>/dev/null)
|
||||||
|
|
||||||
|
if ! preflight; then
|
||||||
|
case "$FWS_HIBERNATE_FALLBACK" in
|
||||||
|
reboot)
|
||||||
|
warn "Préflight hibernation KO → bascule en REBOOT SIMPLE (session FWS NON préservée)."
|
||||||
|
notify_user "Hibernation indisponible — bascule vers Windows par reboot simple (session non sauvegardée)."
|
||||||
|
efibootmgr -n "$win" >/dev/null || die "écriture de BootNext échouée"
|
||||||
|
bootnext_set=1 # ARMÉ : si la relecture échoue, le trap nettoie (M2)
|
||||||
|
efibootmgr 2>/dev/null | grep -qi "BootNext: $win" || die "BootNext non confirmé par le firmware"
|
||||||
|
trap - EXIT # bascule assumée
|
||||||
|
log "Reboot simple vers Windows (Boot$win)…"
|
||||||
|
exec systemctl reboot
|
||||||
|
;;
|
||||||
|
*) die "Préflight hibernation KO et fallback='abort' → on reste sur FWS." ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Bascule NOMINALE : BootNext=Windows puis hibernation ---
|
||||||
|
efibootmgr -n "$win" >/dev/null || die "écriture de BootNext échouée"
|
||||||
|
bootnext_set=1
|
||||||
|
# RELECTURE obligatoire : certains firmwares ignorent/écrasent BootNext.
|
||||||
|
efibootmgr 2>/dev/null | grep -qi "BootNext: $win" \
|
||||||
|
|| die "BootNext non pris en compte par le firmware (entrée Boot$win) — carte non compatible ?"
|
||||||
|
log "BootNext armé vers Windows (Boot$win). Hibernation…"
|
||||||
|
notify_user "Lancement de Windows pour le jeu — FWS s'endort, ta session sera restaurée au retour."
|
||||||
|
|
||||||
|
# M3 : l'ESP (FAT, potentiellement partagée avec Windows) ne doit pas rester
|
||||||
|
# montée rw à travers le cycle — un cache FAT périmé réécrit au resume
|
||||||
|
# corromprait les bootloaders (seul chemin réaliste vers un vrai brick). On
|
||||||
|
# la remonte en lecture seule le temps de l'hibernation, rw au retour.
|
||||||
|
[ -n "$esp" ] && { mount -o remount,ro "$esp" 2>/dev/null || true; }
|
||||||
|
|
||||||
|
# NB : on n'enveloppe PAS « systemctl hibernate » dans un timeout. Un
|
||||||
|
# hibernate RÉUSSI ne « rend la main » qu'au RESUME (après la session
|
||||||
|
# Windows, potentiellement > 1 h) : un timeout tuerait les hibernations
|
||||||
|
# réussies. Une hibernation qui HANG (driver refusant S4) laisse la machine
|
||||||
|
# allumée et n'est PAS mitigeable en userspace (le process est gelé) ; c'est
|
||||||
|
# un risque à valider sur matériel réel (docs §7 #6). Le préflight a déjà
|
||||||
|
# éliminé les causes courantes (swap absent/petit, resume= manquant).
|
||||||
|
if systemctl hibernate; then
|
||||||
|
# On revient ici au RESUME. Bascule réussie : le firmware a normalement
|
||||||
|
# consommé BootNext au boot de Windows — MAIS un override manuel (menu
|
||||||
|
# F12) peut le laisser armé. On le purge donc INCONDITIONNELLEMENT (M1) :
|
||||||
|
# no-op s'il est déjà consommé, rétablit le filet BootOrder=FWS sinon.
|
||||||
|
[ -n "$esp" ] && { mount -o remount,rw "$esp" 2>/dev/null || true; }
|
||||||
|
efibootmgr -N >/dev/null 2>&1 || true
|
||||||
|
bootnext_set=""
|
||||||
|
trap - EXIT
|
||||||
|
log "Session FWS restaurée. Bon retour."
|
||||||
|
else
|
||||||
|
rc=$?
|
||||||
|
[ -n "$esp" ] && { mount -o remount,rw "$esp" 2>/dev/null || true; }
|
||||||
|
warn "Hibernation échouée (rc=$rc)."
|
||||||
|
if [ "$FWS_HIBERNATE_FALLBACK" = reboot ]; then
|
||||||
|
notify_user "Hibernation impossible — bascule vers Windows par reboot simple (session non sauvegardée)."
|
||||||
|
if efibootmgr -n "$win" >/dev/null 2>&1 && efibootmgr 2>/dev/null | grep -qi "BootNext: $win"; then
|
||||||
|
bootnext_set="" # on assume la bascule, pas de nettoyage
|
||||||
|
trap - EXIT
|
||||||
|
log "Reboot simple vers Windows…"
|
||||||
|
exec systemctl reboot
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
die "Bascule annulée — on reste sur FWS (BootNext nettoyé)."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
fws-gameboot — dual-boot hibernate-swap (FWS ⇄ Windows bare-metal, anticheat kernel)
|
||||||
|
|
||||||
|
fws-gameboot doctor Vérifie l'environnement (préflight, boot)
|
||||||
|
fws-gameboot to-windows [--game <id>] Hiberne FWS, bascule + lance <id> (root/pkexec)
|
||||||
|
fws-gameboot status Alias de « doctor »
|
||||||
|
|
||||||
|
Le jeu par défaut (FWS_DEFAULT_GAME) est « valorant ». Les identifiants connus
|
||||||
|
sont définis dans games.json côté Windows (valorant, lol, …).
|
||||||
|
|
||||||
|
Config : $CONF
|
||||||
|
Détails et risques : docs/hibernate-swap-dualboot.md
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
to-windows) shift; cmd_to_windows "$@" ;;
|
||||||
|
doctor|status) cmd_doctor ;;
|
||||||
|
''|-h|--help|help) usage ;;
|
||||||
|
*) die "sous-commande inconnue : $1 (voir --help)" ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# fws-gameboot-bootfix — exécuté à CHAQUE démarrage de FWS (oneshot systemd).
|
||||||
|
#
|
||||||
|
# Réasserte BootOrder[0] = FWS. Filet de sûreté PERMANENT : après un Windows
|
||||||
|
# Update qui remet « Windows Boot Manager » en tête de BootOrder, ou après tout
|
||||||
|
# crash/reboot non géré, un boot SANS BootNext retombe toujours sur FWS (jamais
|
||||||
|
# coincé côté Windows ; l'image d'hibernation reste accessible).
|
||||||
|
#
|
||||||
|
# Inerte sur le live (le service porte ConditionPathExists=!/run/archiso).
|
||||||
|
# ============================================================
|
||||||
|
set -u
|
||||||
|
[ -d /sys/firmware/efi ] || exit 0
|
||||||
|
command -v efibootmgr >/dev/null 2>&1 || exit 0
|
||||||
|
|
||||||
|
fws="$(efibootmgr 2>/dev/null | sed -n 's/^Boot\([0-9A-Fa-f]\{4\}\)\*\? *\(FWS\|GRUB\).*/\1/p' | head -1)"
|
||||||
|
[ -n "$fws" ] || exit 0
|
||||||
|
|
||||||
|
# Nettoyer une entrée « FWS WinPE » résiduelle du déploiement Windows (repli
|
||||||
|
# bcdboot, one-shot déjà consommé) : sinon elle traîne dans le menu de boot.
|
||||||
|
for pe in $(efibootmgr 2>/dev/null | sed -n 's/^Boot\([0-9A-Fa-f]\{4\}\)\*\? *FWS WinPE.*/\1/p'); do
|
||||||
|
efibootmgr -b "$pe" -B >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
|
||||||
|
# BootOrder[0] = FWS (sans dupliquer, en préservant l'ordre existant derrière).
|
||||||
|
order="$(efibootmgr 2>/dev/null | sed -n 's/^BootOrder: //p' | tr -d '[:space:]')"
|
||||||
|
if [ -n "$order" ] && [ "${order%%,*}" != "$fws" ]; then
|
||||||
|
rest="$(printf '%s' "$order" | tr ',' '\n' | grep -vx "$fws" | paste -sd, -)"
|
||||||
|
efibootmgr -o "${fws}${rest:+,$rest}" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# fws-secureboot-setup — prépare FWS à booter SIGNÉ sous Secure Boot,
|
||||||
|
# SANS activer le lockdown noyau (→ hibernation préservée).
|
||||||
|
#
|
||||||
|
# POURQUOI : Vanguard exige Secure Boot (réglage firmware GLOBAL) → FWS doit
|
||||||
|
# lui aussi booter sous SB, sinon le firmware refuse de le lancer. Fait vérifié :
|
||||||
|
# sur noyau mainline Arch, activer Secure Boot n'arme PAS le lockdown (le patch
|
||||||
|
# Fedora/Ubuntu qui lie les deux a été refusé upstream). Signature (firmware) et
|
||||||
|
# lockdown (LSM runtime) sont ORTHOGONAUX → on peut signer ET hiberner.
|
||||||
|
#
|
||||||
|
# VOIE RETENUE : sbctl (clés « db » custom) en CONSERVANT les clés Microsoft
|
||||||
|
# (--microsoft), pour que Windows et les OpROM continuent de booter.
|
||||||
|
#
|
||||||
|
# ⚠ ORDRE IMPÉRATIF : tout SIGNER avant d'activer Secure Boot au firmware.
|
||||||
|
# Détails, alternative shim+MOK et vérifications : docs §4.
|
||||||
|
# ============================================================
|
||||||
|
set -u
|
||||||
|
[ "$(id -u)" -eq 0 ] || { echo "root requis (sudo fws-secureboot-setup)"; exit 1; }
|
||||||
|
[ -d /sys/firmware/efi ] || { echo "Système non-UEFI : Secure Boot sans objet."; exit 1; }
|
||||||
|
|
||||||
|
log() { printf '\e[36m==>\e[0m %s\n' "$*"; }
|
||||||
|
warn() { printf '\e[33m[!] %s\e[0m\n' "$*"; }
|
||||||
|
die() { printf '\e[31m[ERREUR] %s\e[0m\n' "$*"; exit 1; }
|
||||||
|
|
||||||
|
command -v sbctl >/dev/null 2>&1 || { log "Installation de sbctl…"; pacman -S --noconfirm --needed sbctl || die "sbctl introuvable (réseau ?)"; }
|
||||||
|
command -v objdump >/dev/null 2>&1 || pacman -S --noconfirm --needed binutils || true
|
||||||
|
|
||||||
|
# Localiser l'ESP et grubx64.efi. IDENTITÉ = FWS, cohérente avec le kickstart
|
||||||
|
# install_grub (--bootloader-id=FWS + --removable → \EFI\FWS\ ET \EFI\BOOT\).
|
||||||
|
# Signer un \EFI\GRUB\ séparé ne servirait à RIEN : le firmware charge \EFI\FWS\
|
||||||
|
# (ou le secours \EFI\BOOT\BOOTX64.EFI) → ce sont EUX qu'il faut signer.
|
||||||
|
ESP_DIR=/boot/efi; [ -d "$ESP_DIR/EFI" ] || ESP_DIR=/boot
|
||||||
|
GRUB_EFI="$ESP_DIR/EFI/FWS/grubx64.efi"
|
||||||
|
[ -f "$GRUB_EFI" ] || GRUB_EFI="$(find "$ESP_DIR/EFI" -iname 'grubx64.efi' 2>/dev/null | head -1)"
|
||||||
|
[ -n "$GRUB_EFI" ] && [ -f "$GRUB_EFI" ] || die "grubx64.efi introuvable sous $ESP_DIR/EFI"
|
||||||
|
BOOTX64="$ESP_DIR/EFI/BOOT/BOOTX64.EFI" # chemin de secours removable (à signer aussi)
|
||||||
|
|
||||||
|
# 1) GRUB doit embarquer SBAT (+ shim_lock), sinon shim refusera de le charger.
|
||||||
|
# Un grub-install « nu » produit un binaire sans .sbat → on réinstalle EN FWS
|
||||||
|
# (et on régénère le secours removable \EFI\BOOT\BOOTX64.EFI avec le même SBAT).
|
||||||
|
if ! command -v objdump >/dev/null 2>&1; then
|
||||||
|
warn "objdump (binutils) absent → SBAT non vérifiable, réinstallation de GRUB SAUTÉE (on suppose GRUB déjà correct)."
|
||||||
|
elif ! objdump -h "$GRUB_EFI" 2>/dev/null | grep -q '\.sbat'; then
|
||||||
|
[ -f /usr/share/grub/sbat.csv ] || die "sbat.csv introuvable — impossible d'ajouter SBAT"
|
||||||
|
warn "grubx64.efi sans section .sbat → réinstallation de GRUB (--bootloader-id=FWS + --sbat)."
|
||||||
|
GRUB_MODULES='normal search part_gpt part_msdos fat ext2 configfile linux echo test true loadenv all_video efi_gop tpm'
|
||||||
|
grub-install --target=x86_64-efi --efi-directory="$ESP_DIR" --bootloader-id=FWS \
|
||||||
|
--sbat /usr/share/grub/sbat.csv --modules="$GRUB_MODULES" \
|
||||||
|
|| die "grub-install (FWS) a échoué"
|
||||||
|
grub-install --target=x86_64-efi --efi-directory="$ESP_DIR" --bootloader-id=FWS --removable \
|
||||||
|
--sbat /usr/share/grub/sbat.csv --modules="$GRUB_MODULES" \
|
||||||
|
|| warn "grub-install --removable (secours) a échoué"
|
||||||
|
GRUB_EFI="$ESP_DIR/EFI/FWS/grubx64.efi"
|
||||||
|
objdump -h "$GRUB_EFI" 2>/dev/null | grep -q '\.sbat' || die "SBAT toujours absent après réinstallation"
|
||||||
|
grub-mkconfig -o /boot/grub/grub.cfg 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2) Clés sbctl (idempotent).
|
||||||
|
if ! sbctl status 2>/dev/null | grep -qi 'setup mode.*✓\|setup mode.*enabled\|Setup Mode.*Enabled'; then
|
||||||
|
warn "Firmware pas en Setup Mode : « enroll-keys » échouera. Mets le firmware en Setup Mode (efface les clés d'usine), puis relance. On continue quand même la génération/signature."
|
||||||
|
fi
|
||||||
|
[ -d /var/lib/sbctl/keys ] || sbctl create-keys || die "sbctl create-keys a échoué"
|
||||||
|
|
||||||
|
# 3) Enrôler NOS clés + CONSERVER Microsoft (sinon Windows/OpROM cassés).
|
||||||
|
log "Enrôlement des clés (avec Microsoft)…"
|
||||||
|
sbctl enroll-keys --microsoft \
|
||||||
|
|| warn "enroll-keys a échoué (firmware pas en Setup Mode ?) — à refaire une fois en Setup Mode."
|
||||||
|
|
||||||
|
# 4) Signer les TROIS chemins réellement chargés par le firmware + les noyaux :
|
||||||
|
# \EFI\FWS\grubx64.efi, le secours \EFI\BOOT\BOOTX64.EFI, et chaque vmlinuz.
|
||||||
|
log "Signature de GRUB (FWS + secours removable) et des noyaux…"
|
||||||
|
sbctl sign -s "$GRUB_EFI" || die "signature de $GRUB_EFI échouée"
|
||||||
|
[ -f "$BOOTX64" ] && { sbctl sign -s "$BOOTX64" || warn "signature de $BOOTX64 échouée"; }
|
||||||
|
for k in /boot/vmlinuz-linux /boot/vmlinuz-linux-lts /boot/vmlinuz-linux-zen /boot/vmlinuz-linux-hardened; do
|
||||||
|
[ -f "$k" ] && { sbctl sign -s "$k" || warn "signature de $k échouée"; }
|
||||||
|
done
|
||||||
|
sbctl verify || warn "sbctl verify signale des binaires non signés (voir ci-dessus)."
|
||||||
|
|
||||||
|
cat <<'EOF'
|
||||||
|
|
||||||
|
=== Étapes MANUELLES restantes (firmware) ===
|
||||||
|
1. Redémarre dans le setup du firmware (UEFI/BIOS).
|
||||||
|
2. Active Secure Boot (les clés viennent d'être enrôlées, Microsoft conservé).
|
||||||
|
3. Au boot FWS suivant, vérifie :
|
||||||
|
bootctl status | grep 'Secure Boot' → Secure Boot: enabled
|
||||||
|
cat /sys/kernel/security/lockdown → [none] (hibernation OK)
|
||||||
|
cat /sys/power/state → contient « disk »
|
||||||
|
Puis : fws-gameboot doctor
|
||||||
|
|
||||||
|
Re-signature AUTOMATIQUE après chaque MAJ noyau/GRUB via le hook pacman
|
||||||
|
/usr/share/libalpm/hooks/95-fws-secureboot-sign.hook (+ le hook natif de sbctl).
|
||||||
|
EOF
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# fws-windows-bcdfix — rend Windows bootable en exécutant le VRAI `bcdboot`
|
||||||
|
# dans une WinPE lancée UNE FOIS (BootNext), juste après l'install FWS.
|
||||||
|
#
|
||||||
|
# ⚠ NON VALIDÉ SUR MATÉRIEL RÉEL. Voie RECOMMANDÉE par défaut : bcdboot écrit un
|
||||||
|
# BCD correct. Coût : 1 reboot automatique en WinPE, puis retour à FWS
|
||||||
|
# (BootOrder[0]=FWS réasserté par fws-windows-deploy + fws-gameboot-bootfix).
|
||||||
|
#
|
||||||
|
# Mécanique :
|
||||||
|
# 1. Copier la WinPE bootable de l'ISO (bootmgr + BCD WinPE + boot.wim) sur l'ESP.
|
||||||
|
# 2. Injecter winpeshl.ini + fwspe.cmd DANS l'image AMORÇABLE de boot.wim
|
||||||
|
# (Boot Index, typiquement 2 « Setup », + index 1 en ceinture) : au démarrage,
|
||||||
|
# WinPE lance fwspe.cmd → trouve le lecteur Windows (\Windows\System32\
|
||||||
|
# winload.efi) et l'ESP (label WINESP via diskpart) → bcdboot → wpeutil reboot.
|
||||||
|
# 3. Poser une entrée UEFI + BootNext one-shot vers cette WinPE.
|
||||||
|
#
|
||||||
|
# Args : <ISO_MOUNT> <ESP_MOUNT> <WIN_DISK> <ESP_PARTNUM>
|
||||||
|
# ============================================================
|
||||||
|
set -u
|
||||||
|
ISO="${1:-}"; ESP="${2:-}"; WIN_DISK="${3:-}"; ESP_PN="${4:-1}"; DRV="${5:-}"
|
||||||
|
log(){ printf '\e[36m[bcdfix]\e[0m %s\n' "$*"; }
|
||||||
|
die(){ printf '\e[31m[bcdfix] ERREUR : %s\e[0m\n' "$*" >&2; exit 1; }
|
||||||
|
[ -d "$ISO" ] && [ -d "$ESP" ] && [ -b "$WIN_DISK" ] || die "args invalides"
|
||||||
|
command -v wimlib-imagex >/dev/null 2>&1 || die "wimlib absent"
|
||||||
|
|
||||||
|
# 1) Structure de boot WinPE depuis l'ISO (bootmgr + BCD WinPE prêts).
|
||||||
|
log "Staging de la WinPE de l'ISO sur l'ESP…"
|
||||||
|
mkdir -p "$ESP/EFI/Microsoft/Boot" "$ESP/sources"
|
||||||
|
cp -rf "$ISO"/efi/microsoft/boot/. "$ESP/EFI/Microsoft/Boot/" 2>/dev/null || true
|
||||||
|
[ -f "$ESP/EFI/Microsoft/Boot/bootmgfw.efi" ] \
|
||||||
|
|| cp -f "$ISO/efi/boot/bootx64.efi" "$ESP/EFI/Microsoft/Boot/bootmgfw.efi" 2>/dev/null \
|
||||||
|
|| die "bootmgfw.efi WinPE introuvable dans l'ISO"
|
||||||
|
|
||||||
|
# 2) Injecter l'automation dans l'image AMORÇABLE de boot.wim.
|
||||||
|
TMP="$(mktemp -d)"
|
||||||
|
cp -f "$ISO/sources/boot.wim" "$TMP/boot.wim" || die "boot.wim introuvable dans l'ISO"
|
||||||
|
cat > "$TMP/winpeshl.ini" <<'INI'
|
||||||
|
[LaunchApps]
|
||||||
|
"cmd.exe", "/c X:\fwspe.cmd"
|
||||||
|
INI
|
||||||
|
# ⚠ NON VALIDÉ. bcdboot dans WinPE : trouver le lecteur Windows par présence de
|
||||||
|
# winload.efi (NTFS auto-monté), et l'ESP par son label WINESP via diskpart.
|
||||||
|
cat > "$TMP/fwspe.cmd" <<'CMD'
|
||||||
|
@echo off
|
||||||
|
rem Pilote Intel VMD (si injecté) : le charger pour que WinPE VOIE les NVMe.
|
||||||
|
if exist X:\fwsdrv\*.inf for %%I in (X:\fwsdrv\*.inf) do drvload %%I
|
||||||
|
set WIN=
|
||||||
|
for %%D in (C D E F G H I J K L M N O P) do if exist %%D:\Windows\System32\winload.efi set WIN=%%D:
|
||||||
|
echo list volume > X:\dp.txt
|
||||||
|
diskpart /s X:\dp.txt > X:\vol.txt
|
||||||
|
for /f "tokens=2" %%V in ('findstr /i "WINESP" X:\vol.txt') do (
|
||||||
|
echo select volume %%V > X:\dp2.txt
|
||||||
|
echo assign letter=S >> X:\dp2.txt
|
||||||
|
diskpart /s X:\dp2.txt
|
||||||
|
)
|
||||||
|
if not defined WIN goto reboot
|
||||||
|
rem Enregistrer le pilote VMD dans le Windows appliqué (boot device) via DISM.
|
||||||
|
if exist X:\fwsdrv\*.inf dism /image:%WIN%\ /add-driver /driver:X:\fwsdrv /recurse /forceunsigned
|
||||||
|
if exist S:\ ( bcdboot %WIN%\Windows /s S: /f UEFI ) else ( bcdboot %WIN%\Windows /f UEFI )
|
||||||
|
:reboot
|
||||||
|
wpeutil reboot
|
||||||
|
CMD
|
||||||
|
# Pilote Intel VMD à embarquer dans la WinPE (pour drvload + dism) ?
|
||||||
|
if [ -n "$DRV" ] && [ -d "$DRV" ]; then
|
||||||
|
mkdir -p "$TMP/fwsdrv"; cp -rf "$DRV"/. "$TMP/fwsdrv/" 2>/dev/null || true
|
||||||
|
log "Pilote Intel VMD embarqué dans la WinPE (drvload + dism)."
|
||||||
|
fi
|
||||||
|
# Image amorcée = « Boot Index » (typiquement 2) ; on injecte aussi l'index 1 en ceinture.
|
||||||
|
BOOTIDX="$(wimlib-imagex info "$TMP/boot.wim" 2>/dev/null | sed -n 's/^Boot Index:[[:space:]]*//p' | head -1)"
|
||||||
|
[ -n "$BOOTIDX" ] || BOOTIDX=2
|
||||||
|
INJECTED=0
|
||||||
|
for i in $BOOTIDX 1; do
|
||||||
|
wimlib-imagex update "$TMP/boot.wim" "$i" \
|
||||||
|
--command="add '$TMP/winpeshl.ini' /Windows/System32/winpeshl.ini" 2>/dev/null \
|
||||||
|
&& wimlib-imagex update "$TMP/boot.wim" "$i" \
|
||||||
|
--command="add '$TMP/fwspe.cmd' /fwspe.cmd" 2>/dev/null \
|
||||||
|
&& INJECTED=1
|
||||||
|
[ -d "$TMP/fwsdrv" ] && wimlib-imagex update "$TMP/boot.wim" "$i" \
|
||||||
|
--command="add '$TMP/fwsdrv' /fwsdrv" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
[ "$INJECTED" = 1 ] || die "injection winpeshl/fwspe dans boot.wim échouée"
|
||||||
|
cp -f "$TMP/boot.wim" "$ESP/sources/boot.wim" || die "copie boot.wim → ESP échouée"
|
||||||
|
rm -rf "$TMP"
|
||||||
|
|
||||||
|
# 3) Entrée UEFI one-shot vers la WinPE stagée.
|
||||||
|
log "Création de l'entrée UEFI WinPE (one-shot BootNext)…"
|
||||||
|
efibootmgr -c -d "$WIN_DISK" -p "$ESP_PN" -L 'FWS WinPE (bcdboot)' \
|
||||||
|
-l '\EFI\Microsoft\Boot\bootmgfw.efi' >/dev/null 2>&1 \
|
||||||
|
|| die "création de l'entrée UEFI WinPE échouée"
|
||||||
|
PE_BN="$(efibootmgr 2>/dev/null | sed -n "s/^Boot\([0-9A-Fa-f]\{4\}\)\*\? *FWS WinPE.*/\1/p" | head -1)"
|
||||||
|
[ -n "$PE_BN" ] || die "numéro de boot WinPE introuvable"
|
||||||
|
efibootmgr -n "$PE_BN" >/dev/null || die "BootNext WinPE non armé"
|
||||||
|
log "WinPE armée en BootNext (Boot$PE_BN). Au 1er reboot : bcdboot puis retour FWS."
|
||||||
|
log "⚠ NON VALIDÉ sur matériel — à tester (assignation lettres WinPE, index boot.wim)."
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
#!/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; intel_vmd=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" ;;
|
||||||
|
intel_vmd) intel_vmd="$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')"
|
||||||
|
# Pilote Intel VMD à injecter ? (case cochée dans le spoke + .inf présents).
|
||||||
|
DRV=""
|
||||||
|
if [ "${intel_vmd:-0}" = 1 ]; then
|
||||||
|
if ls /usr/local/share/fws/drivers/intel-vmd/*.inf >/dev/null 2>&1; then
|
||||||
|
DRV=/usr/local/share/fws/drivers/intel-vmd
|
||||||
|
log "Intel VMD demandé + pilote présent → injection via WinPE (dism)."
|
||||||
|
else
|
||||||
|
warn "Intel VMD demandé mais AUCUN pilote dans .../drivers/intel-vmd/ — Windows tentera son pilote VMD inbox (25H2). Dépose le pilote Intel si le boot échoue."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Avec un pilote VMD à injecter, on FORCE la voie WinPE (dism y tourne). Sinon
|
||||||
|
# hivex (qui bascule de toute façon sur WinPE tant que non validé).
|
||||||
|
if [ -z "$DRV" ] && /usr/local/lib/fws/fws-bcd-write "$ESP_MNT" "$WIN_MNT" "$WIN_PARTUUID" "$DISK_GUID"; then
|
||||||
|
log "BCD écrit (voie hivex)."
|
||||||
|
BCDFIX=0
|
||||||
|
else
|
||||||
|
[ -n "$DRV" ] && log "Bascule WinPE (bcdboot + injection pilote VMD)." \
|
||||||
|
|| 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 "$DRV" \
|
||||||
|
|| 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" <<CHROOT
|
||||||
|
set -u
|
||||||
|
grep -q "UUID=$SWAP_UUID" /etc/fstab 2>/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
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# fws-bcd-write — construit un magasin BCD Windows OFFLINE depuis Linux (hivex).
|
||||||
|
#
|
||||||
|
# ⚠⚠ NON VALIDÉ SUR MATÉRIEL RÉEL. Le BCD est un hive registre propriétaire ;
|
||||||
|
# `bcdboot` (l'outil officiel qui l'écrit correctement) est Windows-only. On
|
||||||
|
# copie le BCD-Template livré avec Windows et on patche les éléments STRING
|
||||||
|
# qu'on maîtrise ; l'élément BINAIRE « device »/« osdevice » (blob encodant la
|
||||||
|
# partition GPT — DISK_GUID + PARTUUID) est LA surface à auditer et n'est PAS
|
||||||
|
# encodé de façon fiable ici. Tant que ce n'est pas prouvé sur un Windows 11
|
||||||
|
# 25H2 réel, on renvoie un code d'échec → l'orchestrateur bascule sur
|
||||||
|
# fws-windows-bcdfix (WinPE-bcdboot), qui utilise le vrai bcdboot.
|
||||||
|
#
|
||||||
|
# Args : <ESP_MOUNT> <WIN_MOUNT> <WINDOWS_PARTUUID> <DISK_GUID>
|
||||||
|
# Codes : 0 = BCD écrit & jugé sûr ; 1 = bascule sur le repli WinPE.
|
||||||
|
# ============================================================
|
||||||
|
set -u
|
||||||
|
ESP="${1:-}"; WIN="${2:-}"; PARTUUID="${3:-}"; DISKGUID="${4:-}"
|
||||||
|
[ -d "$ESP" ] && [ -d "$WIN" ] || { echo "[bcd-write] args invalides"; exit 1; }
|
||||||
|
command -v hivexregedit >/dev/null 2>&1 || { echo "[bcd-write] hivex absent → repli"; exit 1; }
|
||||||
|
|
||||||
|
TMPL="$WIN/Windows/System32/config/BCD-Template"
|
||||||
|
[ -f "$TMPL" ] || { echo "[bcd-write] BCD-Template absent ($TMPL) → repli"; exit 1; }
|
||||||
|
|
||||||
|
DST="$ESP/EFI/Microsoft/Boot/BCD"
|
||||||
|
mkdir -p "$(dirname "$DST")"
|
||||||
|
cp -f "$TMPL" "$DST"
|
||||||
|
echo "[bcd-write] BCD-Template copié → $DST (device cible $PARTUUID / disque $DISKGUID)."
|
||||||
|
|
||||||
|
# ⚠ Le patch du blob « device » (Element 0x11000001) encodant la partition n'est
|
||||||
|
# pas implémenté de façon vérifiée ici → on force la voie robuste WinPE-bcdboot.
|
||||||
|
echo "[bcd-write] ⚠ NON VALIDÉ (blob device non encodé) → bascule sur fws-windows-bcdfix."
|
||||||
|
exit 1
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Pilote Intel VMD/RST (iaStorVD) — à déposer ici
|
||||||
|
|
||||||
|
FWS **ne peut pas embarquer** le pilote Intel par défaut (binaire propriétaire).
|
||||||
|
Pour activer le support des machines **Intel avec VMD/RST activé dans le BIOS**,
|
||||||
|
dépose ici les fichiers du pilote **Intel VMD** (« f6 » VMD driver) :
|
||||||
|
|
||||||
|
```
|
||||||
|
iaStorVD.inf
|
||||||
|
iaStorVD.sys
|
||||||
|
iaStorVD.cat
|
||||||
|
(+ éventuels iaStorAC.* / autres .inf/.sys du paquet)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Où le télécharger
|
||||||
|
|
||||||
|
- Intel « Rapid Storage Technology (RST) » / VMD driver, package **f6vmdflpy**
|
||||||
|
(Intel Download Center, ou le site du fabricant de la carte mère).
|
||||||
|
- Extraire le contenu du dossier `f6vmdflpy-x64\` ici.
|
||||||
|
|
||||||
|
## Ce que FWS en fait (automatique)
|
||||||
|
|
||||||
|
Si la case **« Intel VMD/RST support »** est cochée dans l'installateur
|
||||||
|
(auto-cochée quand un contrôleur VMD est détecté) **et** que des `.inf` sont
|
||||||
|
présents ici, `fws-windows-deploy` les injecte dans le Windows déployé via la
|
||||||
|
WinPE (`drvload` pour que WinPE voie les NVMe, puis `dism /add-driver` pour
|
||||||
|
enregistrer le pilote comme boot-critique) → Windows boote sur les NVMe Intel VMD.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Windows 11 25H2** inclut déjà iaStorVD *inbox* pour beaucoup de contrôleurs
|
||||||
|
VMD → l'injection n'est nécessaire que pour les contrôleurs non couverts.
|
||||||
|
- **Aucun effet sur AMD** : sans contrôleur VMD, l'option reste décochée / ignorée.
|
||||||
|
- ⚠ Chemin **NON VALIDÉ** sur matériel Intel réel (impossible à tester ici).
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
<#
|
||||||
|
FirstBoot-FwsGameboot.ps1 — orchestrateur de PREMIER DEMARRAGE (une seule fois).
|
||||||
|
|
||||||
|
Au tout premier boot du Windows gaming, execute automatiquement :
|
||||||
|
1. Install-FwsGameboot.ps1 (composant de retour + auto-login + taches) ;
|
||||||
|
2. Install-Games.ps1 (telecharge+lance les installeurs officiels des
|
||||||
|
jeux marques autoinstall dans installers.json).
|
||||||
|
Puis pose un marqueur pour ne PLUS rien refaire aux boots suivants.
|
||||||
|
|
||||||
|
Cablage (le declencheur du "premier boot") : soit via autounattend.xml
|
||||||
|
<FirstLogonCommands> quand FWS pilote l'install Windows (cf. README), soit via
|
||||||
|
une entree RunOnce. Idempotent : re-lancable sans risque (le marqueur protege).
|
||||||
|
#>
|
||||||
|
#Requires -RunAsAdministrator
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$done = Join-Path $Env:ProgramData 'FWS\.firstboot-done'
|
||||||
|
|
||||||
|
if (Test-Path $done) { Write-Host "FWS first boot deja effectue — rien a faire."; return }
|
||||||
|
|
||||||
|
Write-Host "== FWS premier demarrage : configuration gameboot + installation des jeux =="
|
||||||
|
|
||||||
|
# 1) Composant de retour + auto-login (compte gaming SANS mot de passe recommande).
|
||||||
|
try { & (Join-Path $here 'Install-FwsGameboot.ps1') }
|
||||||
|
catch { Write-Warning "Install-FwsGameboot a echoue : $_" }
|
||||||
|
|
||||||
|
# 2) Installation des jeux (sources officielles Riot).
|
||||||
|
try { & (Join-Path $here 'Install-Games.ps1') }
|
||||||
|
catch { Write-Warning "Install-Games a echoue : $_" }
|
||||||
|
|
||||||
|
# Marqueur "fait" (dans le meme dossier durci que le reste).
|
||||||
|
New-Item -ItemType Directory -Force -Path (Split-Path $done) | Out-Null
|
||||||
|
New-Item -ItemType File -Force -Path $done | Out-Null
|
||||||
|
Write-Host "== FWS premier demarrage termine =="
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<#
|
||||||
|
Install-FwsGameboot.ps1 — installe le composant de RETOUR + LANCEMENT AUTO.
|
||||||
|
|
||||||
|
A lancer UNE FOIS, EN ADMIN, dans le Windows gaming. Met en place le
|
||||||
|
"sans login, jeu lance tout seul" :
|
||||||
|
- AUTO-LOGIN du compte gaming (pas d'ecran de mot de passe) ;
|
||||||
|
- tache AU LOGON qui lance fws-play (lit le jeton, lance le bon jeu, retour) ;
|
||||||
|
- tache ONSTART qui arme le boot suivant vers FWS (filet anti-crash) ;
|
||||||
|
- powercfg /h off (reboot PLEIN → NTFS jamais "sale").
|
||||||
|
|
||||||
|
PREREQUIS VERIFIES par ce script (echec explicite sinon) :
|
||||||
|
- le compte courant est ADMINISTRATEUR local (bcdedit + taches SYSTEM) ;
|
||||||
|
- le compte n'a PAS de mot de passe, OU -Password est fourni.
|
||||||
|
|
||||||
|
Empreinte minimale, SANS interference avec Vanguard (aucune injection/hook noyau).
|
||||||
|
|
||||||
|
Parametres :
|
||||||
|
-Password <str> Mot de passe du compte pour l'auto-login. OMETTRE si le
|
||||||
|
compte n'a pas de mot de passe. Fourni => stocke EN CLAIR
|
||||||
|
dans le registre (limitation AutoAdminLogon) : preferer
|
||||||
|
Sysinternals Autologon.exe (secret LSA chiffre).
|
||||||
|
#>
|
||||||
|
#Requires -RunAsAdministrator
|
||||||
|
param([string]$Password)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# --- Prereq 1 : le compte gaming DOIT etre admin local (SID, locale-independant) ---
|
||||||
|
try {
|
||||||
|
$adminGrp = (Get-LocalGroup -SID 'S-1-5-32-544').Name # "Administrators"/"Administrateurs"
|
||||||
|
$members = Get-LocalGroupMember -Group $adminGrp -ErrorAction Stop
|
||||||
|
$isAdmin = $members | Where-Object { $_.Name -ieq "$Env:COMPUTERNAME\$Env:USERNAME" -or $_.Name -ieq $Env:USERNAME }
|
||||||
|
} catch { $isAdmin = $null }
|
||||||
|
if (-not $isAdmin) {
|
||||||
|
throw "Le compte '$Env:USERNAME' doit etre ADMINISTRATEUR local : fws-play appelle bcdedit et pilote des taches SYSTEM. Ajoute-le au groupe administrateurs puis relance."
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Prereq 2 : auto-login fiable => compte sans mot de passe OU -Password fourni ---
|
||||||
|
$pwdRequired = $true
|
||||||
|
try { $pwdRequired = (Get-LocalUser -Name $Env:USERNAME).PasswordRequired } catch { }
|
||||||
|
if ($pwdRequired -and -not $PSBoundParameters.ContainsKey('Password')) {
|
||||||
|
throw "Le compte '$Env:USERNAME' exige un mot de passe : relance avec -Password '<mdp>', ou retire le mot de passe du compte. Sinon l'auto-login echoue -> ecran de connexion -> la boucle gameboot se bloque."
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Deploiement des scripts + durcissement ACL (exe potentiellement lance ELEVE) ---
|
||||||
|
$dest = Join-Path $Env:ProgramData 'FWS'
|
||||||
|
New-Item -ItemType Directory -Force -Path $dest | Out-Null
|
||||||
|
foreach ($f in 'fws-return.ps1','fws-play.ps1','games.json','installers.json','Install-Games.ps1') {
|
||||||
|
$src = Join-Path $PSScriptRoot $f
|
||||||
|
if (Test-Path $src) { Copy-Item $src $dest -Force }
|
||||||
|
}
|
||||||
|
# SYSTEM + Administrateurs = plein controle ; Utilisateurs = lecture seule ; heritage retire.
|
||||||
|
& icacls $dest /inheritance:r /grant:r 'SYSTEM:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' '*S-1-5-32-545:(OI)(CI)RX' | Out-Null
|
||||||
|
|
||||||
|
# (1) Reboot PLEIN : pas de Fast Startup → volumes propres.
|
||||||
|
& powercfg /h off 2>$null
|
||||||
|
|
||||||
|
# (2) AUTO-LOGIN du compte courant.
|
||||||
|
$winlogon = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
|
||||||
|
Set-ItemProperty $winlogon 'AutoAdminLogon' '1' -Force
|
||||||
|
Set-ItemProperty $winlogon 'ForceAutoLogon' '1' -Force
|
||||||
|
Set-ItemProperty $winlogon 'DefaultUserName' $Env:USERNAME -Force
|
||||||
|
Set-ItemProperty $winlogon 'DefaultDomainName' '.' -Force # '.' = local, immune au renommage
|
||||||
|
if ($PSBoundParameters.ContainsKey('Password')) {
|
||||||
|
Set-ItemProperty $winlogon 'DefaultPassword' $Password -Force
|
||||||
|
Write-Warning "Mot de passe stocke EN CLAIR dans le registre. Preferer Sysinternals Autologon.exe (LSA chiffre)."
|
||||||
|
} else {
|
||||||
|
Remove-ItemProperty $winlogon 'DefaultPassword' -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "Auto-login sans mot de passe stocke (compte sans mot de passe verifie)."
|
||||||
|
}
|
||||||
|
|
||||||
|
# (3) Tache AU LOGON : lancer fws-play (lit le jeton, lance le jeu, gere le retour).
|
||||||
|
$playAction = New-ScheduledTaskAction -Execute 'powershell.exe' `
|
||||||
|
-Argument ('-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "{0}\fws-play.ps1"' -f $dest)
|
||||||
|
$playTrigger = New-ScheduledTaskTrigger -AtLogOn -User $Env:USERNAME
|
||||||
|
$playPrincipal = New-ScheduledTaskPrincipal -UserId $Env:USERNAME -LogonType Interactive -RunLevel Highest
|
||||||
|
$playSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||||
|
Register-ScheduledTask -TaskName 'FWS-Play-OnLogon' -Action $playAction -Trigger $playTrigger `
|
||||||
|
-Principal $playPrincipal -Settings $playSettings -Force | Out-Null
|
||||||
|
|
||||||
|
# (4) Tache ONSTART (SYSTEM) : armer le boot suivant vers FWS au demarrage.
|
||||||
|
# Filet anti-crash : si Windows redemarre seul (WU, plantage) sans fws-play,
|
||||||
|
# le boot suivant repart vers FWS. fws-play RE-AFFIRME Windows apres lancement
|
||||||
|
# (gere la course avec cette tache — voir fws-play.ps1).
|
||||||
|
$retAction = New-ScheduledTaskAction -Execute 'powershell.exe' `
|
||||||
|
-Argument ('-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "{0}\fws-return.ps1" -Target FWS' -f $dest)
|
||||||
|
$retTrigger = New-ScheduledTaskTrigger -AtStartup
|
||||||
|
$retPrincipal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
||||||
|
$retSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||||
|
Register-ScheduledTask -TaskName 'FWS-Return-OnStart' -Action $retAction -Trigger $retTrigger `
|
||||||
|
-Principal $retPrincipal -Settings $retSettings -Force | Out-Null
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Composant FWS installe dans $dest." -ForegroundColor Green
|
||||||
|
Write-Host "Auto-login + lancement auto du jeu configures. Flux : boot -> login auto -> jeu." -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "RAPPELS IMPORTANTS :" -ForegroundColor Yellow
|
||||||
|
Write-Host " - POSTE KIOSQUE : auto-login + taches non-UAC => acces physique = bureau admin."
|
||||||
|
Write-Host " - NE PAS activer BitLocker sans sauvegarder la cle (flip Secure Boot cote FWS"
|
||||||
|
Write-Host " change PCR7 -> ecran de recuperation). Verifie : manage-bde -status"
|
||||||
|
Write-Host " - Ne desactive pas Secure Boot / TPM / VBS-HVCI : Vanguard les exige."
|
||||||
|
Write-Host " - Ajouter des jeux : editer $dest\games.json (id en [a-z0-9_-])."
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<#
|
||||||
|
Install-Games.ps1 — telecharge et lance les installeurs OFFICIELS des jeux
|
||||||
|
marques autoinstall=true dans installers.json.
|
||||||
|
|
||||||
|
FWS ne redistribue AUCUN binaire Riot : chaque installeur est telecharge
|
||||||
|
depuis les serveurs de Riot. Riot change regulierement ses URLs -> en cas
|
||||||
|
d'echec, verifier/mettre a jour installers.json.
|
||||||
|
|
||||||
|
NB : l'installation de Valorant n'est PAS pleinement silencieuse (Vanguard
|
||||||
|
installe un driver noyau et impose un redemarrage) : ce script LANCE
|
||||||
|
l'installeur, l'utilisateur termine les eventuelles confirmations.
|
||||||
|
#>
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$cfg = Join-Path $here 'installers.json'
|
||||||
|
if (-not (Test-Path $cfg)) { Write-Warning "installers.json introuvable — aucun jeu installe."; return }
|
||||||
|
|
||||||
|
$installers = Get-Content $cfg -Raw | ConvertFrom-Json
|
||||||
|
$tmp = Join-Path $Env:TEMP 'fws-games'
|
||||||
|
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
|
||||||
|
|
||||||
|
# TLS 1.2 pour Invoke-WebRequest sur d'anciennes stacks .NET.
|
||||||
|
try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch { }
|
||||||
|
|
||||||
|
$names = $installers.PSObject.Properties.Name | Where-Object { $_ -notlike '_*' }
|
||||||
|
foreach ($name in $names) {
|
||||||
|
$g = $installers.$name
|
||||||
|
if (-not $g.autoinstall) { Write-Host "$name : autoinstall=false, ignore."; continue }
|
||||||
|
if (-not $g.url) {
|
||||||
|
Write-Warning "$name : aucune URL dans installers.json — installe-le manuellement."
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$out = Join-Path $tmp $g.installer
|
||||||
|
try {
|
||||||
|
Write-Host "Telechargement de l'installeur $name ..."
|
||||||
|
Invoke-WebRequest -Uri $g.url -OutFile $out -UseBasicParsing
|
||||||
|
} catch {
|
||||||
|
Write-Warning "$name : telechargement echoue ($_). URL a verifier dans installers.json (Riot les change souvent)."
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Write-Host "Lancement de l'installeur $name ..."
|
||||||
|
if ($g.silentargs) { Start-Process $out -ArgumentList $g.silentargs -Wait }
|
||||||
|
else { Start-Process $out }
|
||||||
|
} catch {
|
||||||
|
Write-Warning "$name : lancement de l'installeur echoue ($_)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Installation des jeux lancee. Termine les eventuelles fenetres d'installation + le redemarrage impose par Vanguard."
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# FWS Gameboot — composant Windows
|
||||||
|
|
||||||
|
Composant à déployer **dans le Windows 11 gaming** (partition bare-metal dédiée)
|
||||||
|
pour le dual-boot orchestré « hibernate-swap » de FWS : **retour vers FWS** après
|
||||||
|
la session, **lancement automatique du jeu**, **auto-login**, et **premier
|
||||||
|
démarrage tout automatique** (config + installation des jeux).
|
||||||
|
|
||||||
|
> ⚠️ **Statut : non validé sur matériel réel.** Sémantique `bcdedit bootsequence`,
|
||||||
|
> auto-login, URLs d'installeurs Riot, resume GPU — à tester. Plan :
|
||||||
|
> `docs/hibernate-swap-dualboot.md`.
|
||||||
|
|
||||||
|
## Fichiers
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
|---|---|
|
||||||
|
| `FirstBoot-FwsGameboot.ps1` | **1er démarrage (une fois)** : lance Install-FwsGameboot puis Install-Games, pose un marqueur. |
|
||||||
|
| `Install-FwsGameboot.ps1` | Auto-login + tâche **logon** (fws-play) + tâche **ONSTART** + `powercfg /h off`. Vérifie compte admin/sans mot de passe. |
|
||||||
|
| `Install-Games.ps1` | Télécharge + lance les installeurs **officiels** des jeux `autoinstall` (Riot). |
|
||||||
|
| `installers.json` | URLs des installeurs Riot (à vérifier — Riot les change). |
|
||||||
|
| `fws-play.ps1` | Lit le jeu demandé, le lance, attend, rebascule vers FWS. |
|
||||||
|
| `fws-return.ps1` | Arme le boot suivant vers `FWS` ou `Windows` (locale-indépendant). |
|
||||||
|
| `games.json` | Registre des jeux : `id → {type, …, process}`. |
|
||||||
|
|
||||||
|
## Ce que FWS ne peut PAS faire (et ce qu'il fait à la place)
|
||||||
|
|
||||||
|
FWS **ne pré-embarque pas** Valorant/LoL : bundler Windows (licence) et
|
||||||
|
redistribuer les binaires Riot est interdit, et Vanguard flaggerait un jeu
|
||||||
|
pré-copié. **À la place**, `Install-Games.ps1` **télécharge les installeurs
|
||||||
|
officiels depuis Riot** au premier boot → même résultat (« ils sont déjà là »)
|
||||||
|
sans rien redistribuer. L'install de Valorant n'est pas 100 % silencieuse
|
||||||
|
(Vanguard installe un driver noyau + impose un reboot).
|
||||||
|
|
||||||
|
## Câbler le « premier démarrage automatique »
|
||||||
|
|
||||||
|
Le déclencheur dépend de comment Windows est installé :
|
||||||
|
|
||||||
|
**A. Install Windows piloté par FWS (recommandé, via `autounattend.xml`)** — ajouter
|
||||||
|
dans `<FirstLogonCommands>` :
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<SynchronousCommand wcm:action="add">
|
||||||
|
<Order>1</Order>
|
||||||
|
<CommandLine>powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\FWS\deploy\FirstBoot-FwsGameboot.ps1"</CommandLine>
|
||||||
|
<Description>FWS Gameboot first boot</Description>
|
||||||
|
</SynchronousCommand>
|
||||||
|
```
|
||||||
|
(en ayant copié ce dossier dans `C:\ProgramData\FWS\deploy\` pendant le déploiement).
|
||||||
|
|
||||||
|
**B. Manuel (pour tester maintenant)** — dans le Windows gaming, PowerShell admin :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-ExecutionPolicy -Scope Process Bypass -Force
|
||||||
|
.\FirstBoot-FwsGameboot.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Idempotent : le marqueur `C:\ProgramData\FWS\.firstboot-done` empêche toute
|
||||||
|
réexécution.
|
||||||
|
|
||||||
|
## Prérequis (vérifiés par Install-FwsGameboot, échec explicite sinon)
|
||||||
|
|
||||||
|
- Compte gaming = **administrateur local** (bcdedit + tâches SYSTEM).
|
||||||
|
- Compte **sans mot de passe** (auto-login fiable), ou lancer avec `-Password`.
|
||||||
|
- **Secure Boot + TPM 2.0 + VBS/HVCI** actifs (Vanguard les exige).
|
||||||
|
|
||||||
|
## Flux complet (aucune interaction, hors confirmations Vanguard)
|
||||||
|
|
||||||
|
1. **1er boot** → `FirstBoot` → auto-login configuré + Valo/LoL téléchargés et installés.
|
||||||
|
2. Depuis FWS : `fws-gameboot to-windows --game valorant` → jeton sur l'ESP, hibernation, bascule.
|
||||||
|
3. Windows démarre **bare-metal** → **auto-login** → `fws-play` lit le jeton, lance le jeu.
|
||||||
|
4. À la fermeture/crash → retour, **session FWS restaurée** (apps/pages intactes).
|
||||||
|
|
||||||
|
Filet permanent : côté FWS, `BootOrder[0] = FWS` ré-affirmé à chaque démarrage.
|
||||||
|
|
||||||
|
## Ajouter un jeu
|
||||||
|
|
||||||
|
- Lancement : ajouter une entrée dans `games.json` (`id` en `[a-z0-9_-]`).
|
||||||
|
- Auto-install : ajouter l'URL de l'installeur officiel dans `installers.json`
|
||||||
|
(`autoinstall: true`). Types de lancement : `riot` / `uri` (`steam://…`,
|
||||||
|
`com.epicgames.launcher://…`) / `exe`.
|
||||||
|
|
||||||
|
## Rappels sécurité
|
||||||
|
|
||||||
|
- **Poste kiosque** : auto-login + tâches non-UAC ⇒ accès physique = bureau admin.
|
||||||
|
- **BitLocker** : ne pas l'activer sans sauvegarder la clé (flip Secure Boot →
|
||||||
|
écran de récupération). `manage-bde -status`.
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
<#
|
||||||
|
fws-play.ps1 — session de jeu Windows orchestree, lancement AUTO du jeu choisi.
|
||||||
|
|
||||||
|
Lit (et CONSOMME) le jeton depose par FWS sur l'ESP
|
||||||
|
(\EFI\FWS\launch.json = {"game":"<id>"}), cherche <id> dans games.json, lance
|
||||||
|
le jeu, attend sa fermeture/crash, puis rebascule vers FWS.
|
||||||
|
|
||||||
|
Robustesse (aucune interaction requise) :
|
||||||
|
- resolution du jeton sur TOUTES les ESP (dual-disque) + suppression apres
|
||||||
|
lecture (consume-once : pas de rejeu d'un ancien choix) ;
|
||||||
|
- resolution du Riot Client par sa source canonique (pas de chemin en dur) ;
|
||||||
|
- re-affirmation de BootNext=Windows apres le lancement, pour gagner la course
|
||||||
|
contre la tache ONSTART (fws-play = dernier ecrivain) — un crash pendant le
|
||||||
|
jeu revient donc bien sur Windows ;
|
||||||
|
- FINALLY : rearme FWS et reboote SI le jeu a bien ete vu.
|
||||||
|
|
||||||
|
Filet permanent cote FWS : BootOrder[0]=FWS. A lancer a l'ouverture de session
|
||||||
|
(autologin du compte gaming, qui DOIT etre admin local). Tourne EN ADMIN. #>
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$return = Join-Path $here 'fws-return.ps1'
|
||||||
|
$gamesDb = Join-Path $here 'games.json'
|
||||||
|
$seen = $false
|
||||||
|
|
||||||
|
$EFI_TYPE = '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' # GUID GPT "EFI System Partition"
|
||||||
|
|
||||||
|
function Get-RequestedGame {
|
||||||
|
# Sonde toutes les ESP, lit \EFI\FWS\launch.json, le SUPPRIME (consume-once).
|
||||||
|
# Defaut : valorant. Distingue "jeton absent" de "jeton illisible" (log).
|
||||||
|
$default = 'valorant'; $result = $default
|
||||||
|
$mounts = @()
|
||||||
|
try { $esps = Get-Partition -ErrorAction SilentlyContinue | Where-Object { $_.GptType -eq $EFI_TYPE } }
|
||||||
|
catch { $esps = @() }
|
||||||
|
|
||||||
|
foreach ($p in $esps) {
|
||||||
|
$used = (Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue).Name
|
||||||
|
$letter = (67..90 | ForEach-Object { [char]$_ }) |
|
||||||
|
Where-Object { $used -notcontains "$_" -and ($mounts.Letter -notcontains "$_`:") } |
|
||||||
|
Select-Object -First 1
|
||||||
|
if (-not $letter) { continue }
|
||||||
|
try {
|
||||||
|
Add-PartitionAccessPath -DiskNumber $p.DiskNumber -PartitionNumber $p.PartitionNumber -AccessPath "$letter`:" -ErrorAction Stop
|
||||||
|
$mounts += @{ Letter = "$letter`:"; Disk = $p.DiskNumber; Part = $p.PartitionNumber }
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($m in $mounts) {
|
||||||
|
$tok = "$($m.Letter)\EFI\FWS\launch.json"
|
||||||
|
if (Test-Path $tok) {
|
||||||
|
try {
|
||||||
|
$g = (Get-Content $tok -Raw -ErrorAction Stop | ConvertFrom-Json).game
|
||||||
|
if ($g) { $result = "$g".ToLower() }
|
||||||
|
} catch { Write-Warning "Jeton illisible ($tok) : $_ — repli sur $default." }
|
||||||
|
Remove-Item $tok -Force -ErrorAction SilentlyContinue # consume-once
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($m in $mounts) {
|
||||||
|
try { Remove-PartitionAccessPath -DiskNumber $m.Disk -PartitionNumber $m.Part -AccessPath $m.Letter -ErrorAction SilentlyContinue } catch { }
|
||||||
|
}
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-RiotClient {
|
||||||
|
# Riot s'installe par defaut a la RACINE (C:\Riot Games\...), pas sous
|
||||||
|
# Program Files. Source canonique : RiotClientInstalls.json (rc_default).
|
||||||
|
$cands = @()
|
||||||
|
$inst = Join-Path $Env:ProgramData 'Riot Games\RiotClientInstalls.json'
|
||||||
|
if (Test-Path $inst) { try { $cands += (Get-Content $inst -Raw | ConvertFrom-Json).rc_default } catch { } }
|
||||||
|
$cands += 'C:\Riot Games\Riot Client\RiotClientServices.exe'
|
||||||
|
$cands += (Join-Path ${Env:ProgramFiles} 'Riot Games\Riot Client\RiotClientServices.exe')
|
||||||
|
if (${Env:ProgramFiles(x86)}) { $cands += (Join-Path ${Env:ProgramFiles(x86)} 'Riot Games\Riot Client\RiotClientServices.exe') }
|
||||||
|
return $cands | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function Start-Game($entry) {
|
||||||
|
switch ($entry.type) {
|
||||||
|
'riot' {
|
||||||
|
$riot = Resolve-RiotClient
|
||||||
|
if ($riot) { Start-Process $riot -ArgumentList "--launch-product=$($entry.product)","--launch-patchline=$($entry.patchline)" }
|
||||||
|
else { Write-Warning "Riot Client introuvable (ni RiotClientInstalls.json, ni chemins connus)." }
|
||||||
|
}
|
||||||
|
'uri' { Start-Process $entry.uri }
|
||||||
|
'exe' {
|
||||||
|
if ($entry.args) { Start-Process -FilePath $entry.path -ArgumentList @($entry.args) }
|
||||||
|
else { Start-Process -FilePath $entry.path }
|
||||||
|
}
|
||||||
|
default { Write-Warning "Type de lancement inconnu : $($entry.type)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
# Pendant la session, un crash/reboot doit rester sur WINDOWS.
|
||||||
|
try { & $return -Target Windows } catch { Write-Warning "Arm Windows a echoue : $_" }
|
||||||
|
|
||||||
|
# Resoudre + VALIDER le jeu (rejeter les cles '_*' et les entrees sans .type).
|
||||||
|
$game = Get-RequestedGame
|
||||||
|
$games = Get-Content $gamesDb -Raw | ConvertFrom-Json
|
||||||
|
$entry = $games.$game
|
||||||
|
if ($game -like '_*' -or -not $entry -or -not $entry.type) {
|
||||||
|
Write-Warning "Jeu '$game' invalide/absent de games.json — repli sur valorant."
|
||||||
|
$game = 'valorant'; $entry = $games.valorant
|
||||||
|
}
|
||||||
|
if (-not $entry) { throw "Entree 'valorant' absente de games.json." }
|
||||||
|
Write-Host "Lancement du jeu : $game"
|
||||||
|
Start-Game $entry
|
||||||
|
|
||||||
|
# Course avec la tache ONSTART (qui arme FWS au boot) : on la laisse passer
|
||||||
|
# puis on RE-AFFIRME Windows → fws-play est le dernier ecrivain de BootNext.
|
||||||
|
Start-Sleep -Seconds 25
|
||||||
|
try { & $return -Target Windows } catch { Write-Warning "Re-arm Windows a echoue : $_" }
|
||||||
|
|
||||||
|
# Attendre l'apparition PUIS la disparition du process (boucle de presence,
|
||||||
|
# robuste aux relances client). Ne rebooter que si le jeu a ete VU.
|
||||||
|
$proc = $entry.process
|
||||||
|
if ($proc) {
|
||||||
|
for ($i = 0; $i -lt 1440; $i++) { # ~2 h max d'attente d'apparition
|
||||||
|
if (Get-Process $proc -ErrorAction SilentlyContinue) { $seen = $true; break }
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
}
|
||||||
|
if ($seen) {
|
||||||
|
while (Get-Process $proc -ErrorAction SilentlyContinue) { Start-Sleep -Seconds 5 }
|
||||||
|
} else {
|
||||||
|
Write-Warning "Process '$proc' jamais detecte — pas de reboot automatique."
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Warning "Aucun 'process' defini pour '$game' — pas d'attente ni de reboot auto."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
# Rearmer FWS, QUOI QU'IL ARRIVE.
|
||||||
|
try { & $return -Target FWS } catch { Write-Warning "Arm FWS a echoue : $_ — BootOrder[0]=FWS reste le filet." }
|
||||||
|
if ($seen) {
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
Restart-Computer -Force
|
||||||
|
} else {
|
||||||
|
Write-Warning "Session sans jeu detecte : redemarre manuellement pour revenir a FWS (BootNext deja arme vers FWS)."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<#
|
||||||
|
fws-return.ps1 — arme le "boot suivant" du firmware (one-shot) vers une cible.
|
||||||
|
|
||||||
|
Cote FWS, l'equivalent est « efibootmgr --bootnext ». Cote Windows, on utilise
|
||||||
|
« bcdedit /set {fwbootmgr} bootsequence <id> » : la sequence de boot UNE FOIS
|
||||||
|
du firmware (consommee au prochain demarrage puis oubliee), pendant exact de
|
||||||
|
BootNext.
|
||||||
|
|
||||||
|
IMPORTANT (robustesse locale) : on NE parse PAS les libelles traduits de
|
||||||
|
« bcdedit /enum firmware » (sur un Windows francais « identifier » devient
|
||||||
|
« identificateur », « Windows Boot Manager » -> « Gestionnaire d'amorçage
|
||||||
|
Windows »). On resout :
|
||||||
|
- Windows : par l'alias NON localise {bootmgr} (aucun parsing) ;
|
||||||
|
- FWS : par le CHEMIN EFI (\EFI\FWS\ ou \EFI\GRUB\ ou grubx64.efi), qui
|
||||||
|
n'est pas traduit, en associant le GUID (motif stable) du bloc.
|
||||||
|
Si FWS est introuvable, on DESARME la one-shot (deletevalue) pour retomber
|
||||||
|
proprement sur BootOrder[0]=FWS plutot que de laisser une bascule erronee.
|
||||||
|
|
||||||
|
Doit tourner EN ADMIN. Usage : fws-return.ps1 -Target FWS | -Target Windows
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[ValidateSet('FWS','Windows')]
|
||||||
|
[string]$Target = 'FWS'
|
||||||
|
)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Set-BootSequence([string]$id) {
|
||||||
|
& bcdedit /set '{fwbootmgr}' bootsequence $id | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "bcdedit bootsequence $id a echoue (code $LASTEXITCODE)." }
|
||||||
|
}
|
||||||
|
function Clear-BootSequence {
|
||||||
|
& bcdedit /deletevalue '{fwbootmgr}' bootsequence 2>$null | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Target -eq 'Windows') {
|
||||||
|
# Alias non localise -> aucun parsing, aucune dependance a la langue.
|
||||||
|
Set-BootSequence '{bootmgr}'
|
||||||
|
Write-Host "Boot suivant arme vers Windows ({bootmgr})."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Target = FWS : resoudre le GUID de l'entree firmware pointant vers FWS ---
|
||||||
|
# /v force l'affichage des GUID complets. On matche le CHEMIN (non localise),
|
||||||
|
# pas la description (localisee).
|
||||||
|
$fw = & bcdedit /enum firmware /v 2>$null
|
||||||
|
if (-not $fw) { throw "bcdedit /enum firmware n'a rien renvoye (droits admin ?)." }
|
||||||
|
|
||||||
|
$reId = '\{[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\}|\{[a-z]+\}'
|
||||||
|
$rePath = '(?i)\\EFI\\(FWS|GRUB)\\|grubx64\.efi'
|
||||||
|
|
||||||
|
$curId = $null; $fwsId = $null
|
||||||
|
foreach ($line in $fw) {
|
||||||
|
$m = [regex]::Match($line, $reId)
|
||||||
|
if ($m.Success) { $curId = $m.Value; continue }
|
||||||
|
if ($line -match $rePath -and $curId) { $fwsId = $curId; break }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $fwsId) {
|
||||||
|
# Rien trouve par le chemin : on ne devine PAS. On desarme toute one-shot
|
||||||
|
# pour retomber sur BootOrder[0]=FWS (filet permanent cote FWS).
|
||||||
|
Clear-BootSequence
|
||||||
|
throw "Entree firmware FWS introuvable (chemin \EFI\FWS ou \EFI\GRUB). One-shot desarmee -> BootOrder assure le retour vers FWS."
|
||||||
|
}
|
||||||
|
|
||||||
|
Set-BootSequence $fwsId
|
||||||
|
Write-Host "Boot suivant arme vers FWS = $fwsId"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Registre des jeux lançables par fws-play. Cle = identifiant (charte [a-z0-9_-]) passe a 'fws-gameboot to-windows --game <id>'. 'process' = nom du processus a attendre (sans .exe). Types: 'riot' (Riot Client), 'uri' (steam://rungameid/NNN, com.epicgames.launcher://...), 'exe' (chemin direct, args = tableau). Les cles commencant par '_' sont ignorees. Pour ajouter un jeu, copie une entree ci-dessous.",
|
||||||
|
|
||||||
|
"valorant": {
|
||||||
|
"type": "riot",
|
||||||
|
"product": "valorant",
|
||||||
|
"patchline": "live",
|
||||||
|
"process": "VALORANT-Win64-Shipping"
|
||||||
|
},
|
||||||
|
|
||||||
|
"lol": {
|
||||||
|
"type": "riot",
|
||||||
|
"product": "league_of_legends",
|
||||||
|
"patchline": "live",
|
||||||
|
"process": "League of Legends"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Sources d'installation OFFICIELLES, telechargees au 1er boot par Install-Games.ps1. FWS ne redistribue AUCUN binaire de jeu : tout est telecharge depuis Riot. ATTENTION : Riot CHANGE regulierement ces URLs (region/patchline) -> a VERIFIER (bouton Telecharger sur playvalorant.com / leagueoflegends.com, copier le lien). Region par defaut : EU (utilisateur fr_CH). Mets 'autoinstall': false pour ne pas installer un jeu au 1er boot.",
|
||||||
|
|
||||||
|
"valorant": {
|
||||||
|
"autoinstall": true,
|
||||||
|
"url": "https://valorant.secure.dyn.riotcdn.net/channels/public/x/installer/current/live.live.eu.exe",
|
||||||
|
"installer": "valorant-setup.exe",
|
||||||
|
"silentargs": ""
|
||||||
|
},
|
||||||
|
|
||||||
|
"lol": {
|
||||||
|
"autoinstall": true,
|
||||||
|
"url": "https://lol.secure.dyn.riotcdn.net/channels/public/x/installer/current/live.euw.exe",
|
||||||
|
"installer": "lol-setup.exe",
|
||||||
|
"silentargs": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
FWS — answer file OFFLINE pour Windows 11, deploye a \Windows\Panther\unattend.xml
|
||||||
|
par fws-windows-deploy APRES l'application de install.wim (wimlib). Setup NE
|
||||||
|
TOURNE JAMAIS : pas de passe windowsPE, pas de DiskConfiguration (la partition
|
||||||
|
cible est adressee cote Linux par PARTLABEL). Seuls specialize + oobeSystem
|
||||||
|
s'executent au 1er boot de l'image generalisee.
|
||||||
|
|
||||||
|
Tokens substitues par sed (fws-windows-deploy) :
|
||||||
|
__HOSTNAME__ __USER__ __DISPLAY__ __LOCALE__ __INPUT__ __FIRSTBOOT__
|
||||||
|
Compte gaming LOCAL admin SANS mot de passe (contrat auto-login) — JAMAIS nomme
|
||||||
|
'Administrator' (25H2 le confond avec le compte integre). AutoLogon temporaire
|
||||||
|
(LogonCount) survit au reboot Vanguard le temps que FirstBoot pose l'auto-login
|
||||||
|
permanent (Winlogon) via Install-FwsGameboot.
|
||||||
|
-->
|
||||||
|
<unattend xmlns="urn:schemas-microsoft-com:unattend">
|
||||||
|
|
||||||
|
<settings pass="specialize">
|
||||||
|
<component name="Microsoft-Windows-Shell-Setup"
|
||||||
|
processorArchitecture="amd64"
|
||||||
|
language="neutral"
|
||||||
|
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
|
||||||
|
versionScope="nonSxS">
|
||||||
|
<ComputerName>__HOSTNAME__</ComputerName>
|
||||||
|
</component>
|
||||||
|
<component name="Microsoft-Windows-Deployment"
|
||||||
|
processorArchitecture="amd64"
|
||||||
|
language="neutral"
|
||||||
|
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
|
||||||
|
versionScope="nonSxS">
|
||||||
|
<RunSynchronous>
|
||||||
|
<!-- Ceintures (doublons du reg offline hivex de fws-windows-deploy). -->
|
||||||
|
<RunSynchronousCommand wcm:action="add">
|
||||||
|
<Order>1</Order>
|
||||||
|
<Path>reg add "HKLM\SYSTEM\CurrentControlSet\Control\BitLocker" /v PreventDeviceEncryption /t REG_DWORD /d 1 /f</Path>
|
||||||
|
</RunSynchronousCommand>
|
||||||
|
<RunSynchronousCommand wcm:action="add">
|
||||||
|
<Order>2</Order>
|
||||||
|
<Path>reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f</Path>
|
||||||
|
</RunSynchronousCommand>
|
||||||
|
</RunSynchronous>
|
||||||
|
</component>
|
||||||
|
</settings>
|
||||||
|
|
||||||
|
<settings pass="oobeSystem">
|
||||||
|
<component name="Microsoft-Windows-International-Core"
|
||||||
|
processorArchitecture="amd64"
|
||||||
|
language="neutral"
|
||||||
|
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
|
||||||
|
versionScope="nonSxS">
|
||||||
|
<InputLocale>__INPUT__</InputLocale>
|
||||||
|
<SystemLocale>__LOCALE__</SystemLocale>
|
||||||
|
<!-- Pas de <UILanguage> : une install.wim mono-langue (en-US) n'a pas le
|
||||||
|
pack UI __LOCALE__ → le forcer casse specialize. On garde en-US pour l'UI. -->
|
||||||
|
<UserLocale>__LOCALE__</UserLocale>
|
||||||
|
</component>
|
||||||
|
<component name="Microsoft-Windows-Shell-Setup"
|
||||||
|
processorArchitecture="amd64"
|
||||||
|
language="neutral"
|
||||||
|
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
|
||||||
|
versionScope="nonSxS">
|
||||||
|
<OOBE>
|
||||||
|
<HideEULAPage>true</HideEULAPage>
|
||||||
|
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
|
||||||
|
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
|
||||||
|
<HideLocalAccountScreen>true</HideLocalAccountScreen>
|
||||||
|
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
|
||||||
|
<ProtectYourPC>3</ProtectYourPC>
|
||||||
|
</OOBE>
|
||||||
|
<UserAccounts>
|
||||||
|
<LocalAccounts>
|
||||||
|
<LocalAccount wcm:action="add">
|
||||||
|
<Name>__USER__</Name>
|
||||||
|
<DisplayName>__DISPLAY__</DisplayName>
|
||||||
|
<Group>Administrators</Group>
|
||||||
|
<!-- PAS de <Password> : compte sans mot de passe (contrat auto-login FWS). -->
|
||||||
|
</LocalAccount>
|
||||||
|
</LocalAccounts>
|
||||||
|
</UserAccounts>
|
||||||
|
<AutoLogon>
|
||||||
|
<Enabled>true</Enabled>
|
||||||
|
<Username>__USER__</Username>
|
||||||
|
<LogonCount>5</LogonCount>
|
||||||
|
</AutoLogon>
|
||||||
|
<FirstLogonCommands>
|
||||||
|
<SynchronousCommand wcm:action="add">
|
||||||
|
<Order>1</Order>
|
||||||
|
<Description>FWS Gameboot first boot</Description>
|
||||||
|
<CommandLine>powershell.exe -NoProfile -ExecutionPolicy Bypass -File "__FIRSTBOOT__"</CommandLine>
|
||||||
|
</SynchronousCommand>
|
||||||
|
</FirstLogonCommands>
|
||||||
|
</component>
|
||||||
|
</settings>
|
||||||
|
|
||||||
|
</unattend>
|
||||||
@@ -45,24 +45,25 @@
|
|||||||
# zerombr : réinitialise toute table de partitions invalide
|
# zerombr : réinitialise toute table de partitions invalide
|
||||||
# (répond « oui » au lieu de bloquer sur un disque
|
# (répond « oui » au lieu de bloquer sur un disque
|
||||||
# non reconnu).
|
# non reconnu).
|
||||||
# clearpart --all : supprime TOUTES les partitions de TOUS les disques.
|
# clearpart --all : supprime TOUTES les partitions des disques
|
||||||
|
# SÉLECTIONNÉS dans le GUI (l'utilisateur choisit le
|
||||||
|
# disque cible — les autres, ex. disques de jeux, ne
|
||||||
|
# sont pas touchés s'il ne les sélectionne pas).
|
||||||
# --initlabel : réécrit une étiquette de disque neuve.
|
# --initlabel : réécrit une étiquette de disque neuve.
|
||||||
# --disklabel=msdos : force une table MBR (et non GPT). CRUCIAL en BIOS :
|
# --disklabel : type de table CHOISI SELON LE FIRMWARE par le %pre
|
||||||
# en BIOS+GPT, GRUB i386-pc exige une partition « BIOS
|
# plus bas (ne surtout PAS coder en dur) :
|
||||||
# Boot » (~1 Mo, type EF02) pour embarquer core.img,
|
# • UEFI → gpt : l'ESP peut être créée → grub-install
|
||||||
# qu'Anaconda NE crée PAS avec « bootloader --disabled
|
# EFI fonctionne. Forcer « msdos » en UEFI empêchait
|
||||||
# » → « this GPT partition label contains no BIOS Boot
|
# l'ESP (→ install NON bootable) et réécrivait la
|
||||||
# Partition; embedding won't be possible » →
|
# table pour rien : c'est LE bug corrigé ici.
|
||||||
# grub-install échoue. En MBR, core.img se loge dans
|
# • BIOS → msdos : core.img se loge dans l'espace
|
||||||
# l'espace post-MBR, aucune partition dédiée requise.
|
# post-MBR (pas de partition BIOS Boot EF02 requise,
|
||||||
# ⚠ Ce choix vise le BIOS (cas VMware actuel). Pour l'UEFI il faudra du GPT
|
# qu'Anaconda ne crée pas avec « bootloader --disabled »).
|
||||||
# (+ ESP) — à rendre dépendant du firmware (%pre) le jour où on cible l'UEFI.
|
|
||||||
# L'utilisateur garde la main dans le GUI (il peut repasser en manuel), mais le
|
# L'utilisateur garde la main dans le GUI (il peut repasser en manuel), mais le
|
||||||
# DÉFAUT est désormais un disque net → réinstall fiable à chaque fois.
|
# DÉFAUT est un disque net → réinstall fiable à chaque fois.
|
||||||
# ⚠ VM mono-disque : « --all » efface le seul disque, c'est voulu. Sur une
|
|
||||||
# machine multi-disques il faudrait cibler avec « --drives=sda ».
|
|
||||||
zerombr
|
zerombr
|
||||||
clearpart --all --initlabel --disklabel=msdos
|
# Table de partitions injectée par le %pre (GPT en UEFI, MBR en BIOS).
|
||||||
|
%include /tmp/fws-clearpart.ks
|
||||||
|
|
||||||
# --- Bootloader : géré à la main en %post ------------------------------------
|
# --- Bootloader : géré à la main en %post ------------------------------------
|
||||||
# Anaconda lancerait « grub2-install » / « grub2-mkconfig » (binaires Fedora,
|
# Anaconda lancerait « grub2-install » / « grub2-mkconfig » (binaires Fedora,
|
||||||
@@ -88,6 +89,23 @@ bootloader --disabled
|
|||||||
# poweroff en plein install au 1er test réel).
|
# poweroff en plein install au 1er test réel).
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# %pre — TABLE DE PARTITIONS SELON LE FIRMWARE
|
||||||
|
# Génère /tmp/fws-clearpart.ks (inclus dans la section commandes ci-dessus) :
|
||||||
|
# GPT en UEFI (→ ESP créable → grub-install EFI fonctionne), MBR/msdos en BIOS.
|
||||||
|
# Les scripts %pre s'exécutent AVANT l'application des commandes du kickstart,
|
||||||
|
# donc le %include voit toujours le fichier généré ici.
|
||||||
|
# ============================================================================
|
||||||
|
%pre --log=/tmp/fws-pre.log
|
||||||
|
if [ -d /sys/firmware/efi ]; then
|
||||||
|
disklabel=gpt
|
||||||
|
else
|
||||||
|
disklabel=msdos
|
||||||
|
fi
|
||||||
|
echo "clearpart --all --initlabel --disklabel=$disklabel" > /tmp/fws-clearpart.ks
|
||||||
|
%end
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# %post --nochroot — s'exécute dans le LIVE, la cible montée sous /mnt/sysroot
|
# %post --nochroot — s'exécute dans le LIVE, la cible montée sous /mnt/sysroot
|
||||||
# But : déposer dans la cible ce dont le %post chrooté aura besoin (DNS pour
|
# But : déposer dans la cible ce dont le %post chrooté aura besoin (DNS pour
|
||||||
@@ -121,6 +139,12 @@ if [ -f /tmp/fws-desktop ]; then
|
|||||||
cp -f /tmp/fws-desktop "$SYSROOT/tmp/fws-desktop" 2>/dev/null || true
|
cp -f /tmp/fws-desktop "$SYSROOT/tmp/fws-desktop" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Choix « Dual-boot Windows gaming » (spoke fws_windows) → relu par le %post
|
||||||
|
# chrooté (Secure Boot) ET par fws-windows-deploy (%post --nochroot, plus bas).
|
||||||
|
if [ -f /tmp/fws-windows ]; then
|
||||||
|
cp -f /tmp/fws-windows "$SYSROOT/tmp/fws-windows" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Noyau : archiso RETIRE /boot/vmlinuz-linux du squashfs ------------------
|
# --- Noyau : archiso RETIRE /boot/vmlinuz-linux du squashfs ------------------
|
||||||
# mkarchiso boote le noyau depuis l'ISO et l'EXCLUT de l'airootfs squashfs (gain
|
# mkarchiso boote le noyau depuis l'ISO et l'EXCLUT de l'airootfs squashfs (gain
|
||||||
# de place). Conséquence : le payload LiveOS recopie un rootfs SANS noyau →
|
# de place). Conséquence : le payload LiveOS recopie un rootfs SANS noyau →
|
||||||
@@ -592,6 +616,37 @@ ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
|
|||||||
systemctl enable systemd-resolved.service 2>/dev/null || true
|
systemctl enable systemd-resolved.service 2>/dev/null || true
|
||||||
install -d /etc/NetworkManager/conf.d
|
install -d /etc/NetworkManager/conf.d
|
||||||
printf '[main]\ndns=systemd-resolved\n' > /etc/NetworkManager/conf.d/dns.conf
|
printf '[main]\ndns=systemd-resolved\n' > /etc/NetworkManager/conf.d/dns.conf
|
||||||
|
|
||||||
|
# --- 7) Secure Boot (uniquement si « Dual-boot Windows gaming » demandé) ------
|
||||||
|
# Vanguard exige Secure Boot. On PRÉPARE la chaîne pendant l'install : sbctl
|
||||||
|
# create-keys + signature des 3 chemins réels (\EFI\FWS, \EFI\BOOT\BOOTX64, noyaux).
|
||||||
|
# L'enrôlement des clés + l'activation de SB au firmware restent MANUELS (Setup
|
||||||
|
# Mode requis, non automatisable) — instructions au 1er boot. Idempotent.
|
||||||
|
# On NE source PAS /tmp/fws-windows (un chemin d'ISO piégé pourrait injecter du
|
||||||
|
# code) : simple test littéral de la ligne « enabled=1 ».
|
||||||
|
if grep -qx 'enabled=1' /tmp/fws-windows 2>/dev/null \
|
||||||
|
&& [ -x /usr/local/bin/fws-secureboot-setup ]; then
|
||||||
|
/usr/local/bin/fws-secureboot-setup \
|
||||||
|
|| echo "[FWS] Secure Boot : préparation partielle (à finaliser au 1er boot)."
|
||||||
|
fi
|
||||||
|
%end
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# %post --nochroot — DÉPLOIEMENT WINDOWS (option A) si « Dual-boot Windows »
|
||||||
|
# demandé. Tourne dans le LIVE (cible sous /mnt/sysroot). NON bloquant : un
|
||||||
|
# échec de déploiement N'EMPÊCHE PAS FWS de booter (garde-fous fail-closed +
|
||||||
|
# statut DEPLOY_FAILED, aucune entrée UEFI Windows piégeuse). Le carve du disque
|
||||||
|
# Windows vit ICI (le %pre tourne avant le GUI → ne voit pas /tmp/fws-windows).
|
||||||
|
# Voir fws-windows-deploy + docs/windows-spoke-plan.md.
|
||||||
|
# ============================================================================
|
||||||
|
%post --nochroot --log=/tmp/fws-windows-deploy.log
|
||||||
|
SYSROOT=/mnt/sysroot
|
||||||
|
[ -d "$SYSROOT" ] || SYSROOT=/mnt/sysimage
|
||||||
|
if [ -f /tmp/fws-windows ] && [ -x /usr/local/bin/fws-windows-deploy ]; then
|
||||||
|
/usr/local/bin/fws-windows-deploy --state /tmp/fws-windows --sysroot "$SYSROOT" \
|
||||||
|
|| echo "[FWS] Déploiement Windows échoué (voir le log) — FWS reste bootable."
|
||||||
|
fi
|
||||||
%end
|
%end
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
# FWS — lanceur gameboot : hiberne FWS et bascule vers le Windows bare-metal
|
||||||
|
# pour jouer à League of Legends (anticheat Vanguard). Aucun mot de passe pour la
|
||||||
|
# session active (cf. polkit org.fws.gameboot.policy).
|
||||||
|
Type=Application
|
||||||
|
Name=Jouer à League of Legends
|
||||||
|
Name[en]=Play League of Legends
|
||||||
|
GenericName=Bascule vers Windows (gameboot)
|
||||||
|
GenericName[en]=Switch to Windows (gameboot)
|
||||||
|
Comment=Hiberne FWS et démarre League of Legends sur le Windows bare-metal
|
||||||
|
Comment[en]=Hibernate FWS and boot League of Legends on bare-metal Windows
|
||||||
|
Exec=pkexec /usr/local/bin/fws-gameboot to-windows --game lol
|
||||||
|
Icon=fws-gameboot
|
||||||
|
Terminal=false
|
||||||
|
Categories=Game;
|
||||||
|
Keywords=league;legends;lol;vanguard;windows;gameboot;dual-boot;jeu;
|
||||||
|
Keywords[en]=league;legends;lol;vanguard;windows;gameboot;dual-boot;game;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
# FWS — lanceur gameboot : hiberne FWS et bascule vers le Windows bare-metal
|
||||||
|
# pour jouer à Valorant (anticheat Vanguard). Aucun mot de passe pour la session
|
||||||
|
# active (cf. polkit org.fws.gameboot.policy).
|
||||||
|
Type=Application
|
||||||
|
Name=Jouer à Valorant
|
||||||
|
Name[en]=Play Valorant
|
||||||
|
GenericName=Bascule vers Windows (gameboot)
|
||||||
|
GenericName[en]=Switch to Windows (gameboot)
|
||||||
|
Comment=Hiberne FWS et démarre Valorant sur le Windows bare-metal
|
||||||
|
Comment[en]=Hibernate FWS and boot Valorant on bare-metal Windows
|
||||||
|
Exec=pkexec /usr/local/bin/fws-gameboot to-windows --game valorant
|
||||||
|
Icon=fws-gameboot
|
||||||
|
Terminal=false
|
||||||
|
Categories=Game;
|
||||||
|
Keywords=valorant;vanguard;windows;gameboot;dual-boot;jeu;
|
||||||
|
Keywords[en]=valorant;vanguard;windows;gameboot;dual-boot;game;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- FWS — icône générique « gameboot » (lanceur de bascule vers Windows gaming).
|
||||||
|
Neutre, sans logo de marque (Valorant/LoL sont des marques de Riot). -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="fwsGameGrad" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#89b4fa"/>
|
||||||
|
<stop offset="1" stop-color="#5b7fd4"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="12" y="12" width="104" height="104" rx="26" fill="url(#fwsGameGrad)"/>
|
||||||
|
<!-- Poignées de manette stylisées -->
|
||||||
|
<rect x="30" y="52" width="68" height="34" rx="17" fill="#ffffff" opacity="0.18"/>
|
||||||
|
<!-- Triangle « play » = lancer le jeu -->
|
||||||
|
<path d="M54 44 L88 64 L54 84 Z" fill="#ffffff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 809 B |
@@ -0,0 +1,26 @@
|
|||||||
|
# ============================================================
|
||||||
|
# FWS — re-signature Secure Boot (sbctl) après une MAJ de noyau ou de GRUB.
|
||||||
|
#
|
||||||
|
# Sans ça, « pacman -Syu » qui met à jour linux/grub réécrit des binaires NON
|
||||||
|
# signés → au reboot sous Secure Boot, le firmware refuse de démarrer FWS.
|
||||||
|
# « sbctl sign-all » re-signe tous les fichiers suivis (idempotent). Complète
|
||||||
|
# le hook natif de sbctl (filet redondant, sans effet s'il est déjà passé).
|
||||||
|
#
|
||||||
|
# Inoffensif tant que sbctl n'a rien enrôlé (sign-all ne fait alors rien).
|
||||||
|
# ============================================================
|
||||||
|
[Trigger]
|
||||||
|
Type = Path
|
||||||
|
Operation = Install
|
||||||
|
Operation = Upgrade
|
||||||
|
Target = usr/lib/modules/*/vmlinuz
|
||||||
|
Target = boot/vmlinuz-*
|
||||||
|
Target = usr/lib/grub/*
|
||||||
|
Target = boot/grub/x86_64-efi/*
|
||||||
|
Target = boot/efi/EFI/*/grubx64.efi
|
||||||
|
Target = efi/EFI/*/grubx64.efi
|
||||||
|
|
||||||
|
[Action]
|
||||||
|
Description = FWS : re-signature Secure Boot des binaires de boot (sbctl)…
|
||||||
|
When = PostTransaction
|
||||||
|
Exec = /usr/bin/sbctl sign-all
|
||||||
|
Depends = sbctl
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE policyconfig PUBLIC
|
||||||
|
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
|
||||||
|
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
|
||||||
|
<!--
|
||||||
|
FWS — autorise les lanceurs gameboot (entrées .desktop « Jouer à … ») à
|
||||||
|
exécuter /usr/local/bin/fws-gameboot via pkexec.
|
||||||
|
|
||||||
|
allow_active = yes : AUCUN mot de passe pour l'utilisateur physiquement présent
|
||||||
|
(poste gaming perso). La bascule n'est PAS destructive — FWS hiberne, la
|
||||||
|
session est préservée et restaurée au retour. auth_admin pour les sessions
|
||||||
|
inactives/distantes.
|
||||||
|
-->
|
||||||
|
<policyconfig>
|
||||||
|
<vendor>FWS</vendor>
|
||||||
|
<action id="org.fws.gameboot">
|
||||||
|
<description>Basculer vers Windows pour jouer (gameboot)</description>
|
||||||
|
<description xml:lang="en">Switch to Windows to play (gameboot)</description>
|
||||||
|
<message>Authentification requise pour basculer vers Windows</message>
|
||||||
|
<message xml:lang="en">Authentication is required to switch to Windows</message>
|
||||||
|
<icon_name>fws-gameboot</icon_name>
|
||||||
|
<defaults>
|
||||||
|
<allow_any>auth_admin</allow_any>
|
||||||
|
<allow_inactive>auth_admin</allow_inactive>
|
||||||
|
<allow_active>yes</allow_active>
|
||||||
|
</defaults>
|
||||||
|
<annotate key="org.freedesktop.policykit.exec.path">/usr/local/bin/fws-gameboot</annotate>
|
||||||
|
</action>
|
||||||
|
</policyconfig>
|
||||||
@@ -121,3 +121,15 @@ ostree
|
|||||||
xterm
|
xterm
|
||||||
# Gestionnaire D&D 5e (dés, personnages, grimoire, sessions, combat).
|
# Gestionnaire D&D 5e (dés, personnages, grimoire, sessions, combat).
|
||||||
fws-dnd
|
fws-dnd
|
||||||
|
|
||||||
|
# --- Gameboot : dual-boot Windows gaming (déploiement offline + Secure Boot) ---
|
||||||
|
# Outils universels de fws-windows-deploy (%post, hors ligne) + fws-secureboot-setup.
|
||||||
|
# NB : PAS de 'nvidia' ici (conditionnel via fws-setup-hardware).
|
||||||
|
# wimlib = apply install.wim/.esd ; hivex = BCD + anti-BitLocker offline ;
|
||||||
|
# sbctl/sbsigntools/mokutil = Secure Boot ; binutils = objdump (.sbat).
|
||||||
|
wimlib
|
||||||
|
hivex
|
||||||
|
sbctl
|
||||||
|
sbsigntools
|
||||||
|
mokutil
|
||||||
|
binutils
|
||||||
|
|||||||
@@ -36,5 +36,11 @@ file_permissions=(
|
|||||||
["/usr/local/bin/fws-recovery-gui"]="0:0:755"
|
["/usr/local/bin/fws-recovery-gui"]="0:0:755"
|
||||||
["/usr/local/bin/fws-powermenu"]="0:0:755"
|
["/usr/local/bin/fws-powermenu"]="0:0:755"
|
||||||
["/usr/local/bin/fws-about"]="0:0:755"
|
["/usr/local/bin/fws-about"]="0:0:755"
|
||||||
|
["/usr/local/bin/fws-gameboot"]="0:0:755"
|
||||||
|
["/usr/local/bin/fws-gameboot-bootfix"]="0:0:755"
|
||||||
|
["/usr/local/bin/fws-secureboot-setup"]="0:0:755"
|
||||||
|
["/usr/local/bin/fws-windows-deploy"]="0:0:755"
|
||||||
|
["/usr/local/bin/fws-windows-bcdfix"]="0:0:755"
|
||||||
|
["/usr/local/lib/fws/fws-bcd-write"]="0:0:755"
|
||||||
["/etc/grub.d/42_fws-recovery"]="0:0:755"
|
["/etc/grub.d/42_fws-recovery"]="0:0:755"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# Checklist de validation matérielle — gameboot + dual-boot Windows
|
||||||
|
|
||||||
|
> **Rien de tout ça n'a encore tourné.** Le code a passé deux revues adversariales,
|
||||||
|
> mais le déploiement Windows et le BCD sont marqués **NON VALIDÉ** : cette phase
|
||||||
|
> **va faire remonter des bugs**, surtout aux phases 5–6. Avance dans l'ordre
|
||||||
|
> (du moins risqué au plus destructif) et **ne saute pas un ✅ pour passer au suivant**.
|
||||||
|
|
||||||
|
## ⚠️ Avant tout — sécurité données
|
||||||
|
|
||||||
|
- [ ] **Sauvegarde** de tout ce qui compte sur la machine cible (les tests des phases 5+ **effacent un disque entier**).
|
||||||
|
- [ ] Un **disque à sacrifier** identifié (≠ disques de jeux `Games-L1/L2`). Tes 3 NVMe sont pleins → libère-en un ou ajoute-en un.
|
||||||
|
- [ ] Note écrite du **disque exact** à dédier à Windows (ex. `/dev/nvmeXn1`) — tu la revérifieras à chaque étape.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0 — Prérequis
|
||||||
|
|
||||||
|
- [ ] Machine cible **UEFI** (pas BIOS) — `[ -d /sys/firmware/efi ] && echo UEFI`
|
||||||
|
- [ ] **TPM 2.0** présent — `ls /sys/class/tpm/tpm0`
|
||||||
|
- [ ] **IOMMU** activable au firmware (Vanguard l'exige) — à vérifier dans le setup BIOS.
|
||||||
|
- [ ] **ISO Windows 11 25H2** (édition **Pro** de préférence) sur clé USB, branchée.
|
||||||
|
- [ ] Licence Windows valide sous la main (activation post-install).
|
||||||
|
|
||||||
|
## Phase 1 — Build de l'ISO
|
||||||
|
|
||||||
|
- [ ] `./setup-aur.sh` a déjà tourné (repo local fws-local : anaconda, fws-dnd…).
|
||||||
|
- [ ] `./build-offi.sh` → l'ISO se construit **sans `target not found`** (les nouveaux paquets `wimlib hivex sbctl sbsigntools mokutil binutils` sont officiels, tirés par mkarchiso).
|
||||||
|
- [ ] ISO produite dans `out/…`.
|
||||||
|
- [ ] Écrire l'ISO sur une clé USB de boot (`dd` / Ventoy).
|
||||||
|
|
||||||
|
## Phase 2 — Tests NON destructifs (aucune écriture disque)
|
||||||
|
|
||||||
|
- [ ] Booter l'ISO FWS en **live**.
|
||||||
|
- [ ] `fws-gameboot doctor` s'exécute et liste RAM / swap / VRAM / Secure Boot / entrées de boot.
|
||||||
|
- [ ] Lancer l'installateur : la spoke **« Dual-boot Windows »** apparaît sur le hub Résumé.
|
||||||
|
- [ ] Cocher la case → le combo disque **n'a AUCUN disque présélectionné** (choix explicite obligatoire).
|
||||||
|
- [ ] Choisir un disque **non vide** → l'avertissement affiche **« CE DISQUE CONTIENT DES DONNÉES »**.
|
||||||
|
- [ ] Sélectionner le **même** disque pour FWS (Destination) et pour Windows → le **Récapitulatif** affiche **⛔ CONFLIT** (contrôle croisé).
|
||||||
|
- [ ] Les lanceurs **« Jouer à Valorant / LoL »** apparaissent dans le menu d'applications (wofi/rofi).
|
||||||
|
- [ ] `pkexec fws-gameboot to-windows` **sans mot de passe** en session active (polkit).
|
||||||
|
|
||||||
|
## Phase 3 — Hibernation / resume (LE point dur matériel n°1)
|
||||||
|
|
||||||
|
> À valider **avant** de câbler quoi que ce soit à Windows. RTX 5090 (Blackwell) =
|
||||||
|
> suspend/hibernate historiquement fragile.
|
||||||
|
|
||||||
|
- [ ] Provisionner un swap ≥ RAM+VRAM (~96 Go) et `resume=` (cf. `docs/hibernate-swap-dualboot.md` §1-2).
|
||||||
|
- [ ] `NVreg_PreserveVideoMemoryAllocations=1` posé + services `nvidia-suspend/hibernate/resume` actifs.
|
||||||
|
- [ ] `systemctl hibernate` avec une **session graphique chargée** (jeu/desktop lourd).
|
||||||
|
- [ ] Au rallumage : **session restaurée** ET **affichage NVIDIA intact** (pas d'écran noir / Xid).
|
||||||
|
- [ ] `journalctl -b -1 -k | grep -iE 'hibernation|Image saved|resume'` → propre.
|
||||||
|
|
||||||
|
## Phase 4 — Secure Boot
|
||||||
|
|
||||||
|
- [ ] `sudo fws-secureboot-setup` → signe `\EFI\FWS\grubx64.efi` + `\EFI\BOOT\BOOTX64.EFI` + noyaux.
|
||||||
|
- [ ] Firmware : Setup Mode → `sbctl enroll-keys --microsoft` → activer Secure Boot.
|
||||||
|
- [ ] `bootctl status | grep 'Secure Boot'` → **enabled**.
|
||||||
|
- [ ] `cat /sys/kernel/security/lockdown` → **[none]** (hibernation préservée).
|
||||||
|
- [ ] FWS **boote toujours** sous Secure Boot (sinon → média de récupération).
|
||||||
|
|
||||||
|
## Phase 5 — Déploiement Windows 🔴 DESTRUCTIF (disque sacrificiel UNIQUEMENT)
|
||||||
|
|
||||||
|
> **Danger maximum.** Vérifie 3× le disque cible. Les garde-fous fail-closed
|
||||||
|
> doivent refuser tout disque protégé — teste-les d'abord exprès.
|
||||||
|
|
||||||
|
- [ ] **Test négatif** : dans la spoke, choisir le disque de FWS/racine → le déploiement doit **ABORTER** (`DEPLOY_FAILED`), rien d'effacé.
|
||||||
|
- [ ] Install FWS + dual-boot activé sur le **disque sacrificiel**, ISO USB branchée.
|
||||||
|
- [ ] Surveiller `/tmp/fws-windows-deploy.log` : garde-fous OK → carve → `wimlib apply` → BCD.
|
||||||
|
- [ ] Vérifier que **seul** le disque sacrificiel a été touché (les Games intacts — `lsblk -f`).
|
||||||
|
- [ ] `cat /var/lib/fws/windows-deploy.status` → `DEPLOY_OK` (ou `DEPLOY_FAILED` explicite).
|
||||||
|
- [ ] **Au 1er reboot post-install : la machine boote FWS** (pas Windows, pas WinPE en boucle).
|
||||||
|
|
||||||
|
## Phase 6 — Bootabilité Windows (⚠ voie BCD NON VALIDÉE — debug attendu ici)
|
||||||
|
|
||||||
|
- [ ] Voie **WinPE-bcdfix** : au 1er reboot, WinPE tourne, `bcdboot` s'exécute, puis retour FWS.
|
||||||
|
- Si **boucle WinPE** : vérifier l'assignation des lettres (`FWSWIN`/`WINESP`) et l'index de `boot.wim` dans `fws-windows-bcdfix`.
|
||||||
|
- [ ] Depuis FWS, `efibootmgr` : entrée **Windows Boot Manager** présente, **FWS en tête** de BootOrder, **pas** d'entrée « FWS WinPE » résiduelle.
|
||||||
|
- [ ] Boot manuel de Windows (menu firmware) → **Windows démarre** jusqu'à l'OOBE unattended.
|
||||||
|
|
||||||
|
## Phase 7 — Premier démarrage Windows (auto)
|
||||||
|
|
||||||
|
- [ ] OOBE **sans intervention** : compte `gaming` local, **auto-login**, pas d'écran compte Microsoft.
|
||||||
|
- [ ] `FirstBoot-FwsGameboot.ps1` s'exécute (tâches ONSTART + logon créées, `powercfg /h off`).
|
||||||
|
- [ ] `Install-Games.ps1` télécharge/lance les installeurs Valorant + LoL (vérifier les URLs `installers.json` si échec).
|
||||||
|
- [ ] `manage-bde -status` → **BitLocker OFF** (anti-lockout OK).
|
||||||
|
|
||||||
|
## Phase 8 — Aller-retour gameboot complet (le but final)
|
||||||
|
|
||||||
|
- [ ] Depuis FWS : clic **« Jouer à Valorant »** → FWS **hiberne** → reboot → Windows bare-metal.
|
||||||
|
- [ ] **Vanguard démarre** et Valorant se lance (pas de VAN9003 / erreur VM).
|
||||||
|
- [ ] Quitter Valorant → reboot → **FWS reprend la session hibernée intacte** (apps/onglets).
|
||||||
|
- [ ] `BootOrder[0]=FWS` toujours vrai après plusieurs cycles (`fws-gameboot-bootfix`).
|
||||||
|
- [ ] Test crash : tuer Valorant brutalement → retour propre à FWS (ou reste sur Windows selon le filet).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Filets de récupération (à préparer AVANT la phase 5)
|
||||||
|
|
||||||
|
- [ ] **Média de récupération FWS** prêt (ré-enrôlement clés / re-signature) en cas de brick Secure Boot.
|
||||||
|
- [ ] Clé USB d'install Windows officielle (pour réparer un BCD cassé avec `bcdboot` natif).
|
||||||
|
- [ ] Savoir entrer dans le menu de boot firmware (touche F2/F12/Suppr selon la carte).
|
||||||
|
|
||||||
|
## Attentes réalistes (marqué NON VALIDÉ dans le code)
|
||||||
|
|
||||||
|
- **BCD / WinPE-bcdboot** (phase 6) : le plus susceptible d'échouer au 1er essai → itérer.
|
||||||
|
- **Sémantique BootNext** du firmware (phase 8) : à confirmer sur ta carte mère précise.
|
||||||
|
- **Resume GPU 5090** (phase 3) : dépend du pilote ; si KO → mode reboot simple (fallback déjà câblé).
|
||||||
|
- **URLs installeurs Riot** (phase 7) : Riot les change → `installers.json` à mettre à jour si le download échoue.
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
# Plan de build — FWS « Hibernate‑Swap Dual‑Boot » (Valorant/Vanguard)
|
||||||
|
|
||||||
|
> Document de build final. Le design est validé et acquis : ce plan le rend **buildable**, intègre les corrections bloquantes de la revue adversariale et fixe l'ordre d'exécution. Cible réelle : UEFI, Zen 5, 60,45 GiB RAM, 3× NVMe 2 To (Samsung 990 PRO), **RTX 5090 Blackwell** (pilote propriétaire 610.43.02), TPM 2.0, GRUB, noyau 7.1.2‑arch3‑1, Secure Boot **OFF**, aucun swap.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Résumé & principe
|
||||||
|
|
||||||
|
L'utilisateur clique **« Jouer à Valorant »** dans FWS. FWS **hiberne** (RAM → swap disque) après avoir armé un boot **one‑shot UEFI** (`BootNext`) vers un **Windows 11 bare‑metal** installé sur une partition NTFS dédiée. Le firmware lance Windows **directement** (`\EFI\Microsoft\Boot\bootmgfw.efi`, jamais via GRUB) : Vanguard voit un vrai Windows natif. À la fermeture **ou** au crash de Valorant, Windows arme `BootNext=FWS` et **reboote**, le noyau voit `resume=` et **reprend la session FWS intacte**. C'est un dual‑boot bare‑metal des deux côtés — configuration que Vanguard tolère — avec `BootOrder[0]=FWS` comme filet de sûreté permanent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Prérequis matériels / logiciels
|
||||||
|
|
||||||
|
**Matériel (vérifié sur cible)**
|
||||||
|
- UEFI en User Mode (SetupMode=0), db d'usine Microsoft présents, TPM 2.0, IOMMU activable au firmware (à confirmer dans le setup).
|
||||||
|
- 3× NVMe **entièrement partitionnés, zéro espace non alloué** (à re‑confirmer via `parted … print free`).
|
||||||
|
- **GPU RTX 5090** : facteur critique d'hibernation (VRAM 32 Go) — voir §7.
|
||||||
|
|
||||||
|
**Logiciels côté FWS (Arch)**
|
||||||
|
- Paquets à ajouter : `sbsigntools` / `sbctl`, `efibootmgr`, `mokutil`, `wimlib`, `ntfs-3g`, `polkit`, `python-gobject`, `gtk3`, `nvidia`/`nvidia-utils` + `nvidia-hibernate`/`nvidia-resume` (hooks S4 NVIDIA).
|
||||||
|
- **Interdits** (casseraient l'hibernation) : `linux-hardened`, patch auto‑lockdown Fedora, `lockdown=` en cmdline, initramfs systemd non planifié.
|
||||||
|
- Invariants à figer en CI : `/sys/kernel/security/lockdown == [none]`, `/sys/power/state` contient `disk`, cmdline sans `lockdown=`.
|
||||||
|
|
||||||
|
**Côté Windows (fourni par l'utilisateur, non bundlé)**
|
||||||
|
- ISO Windows 11 légitime, TPM 2.0 + Secure Boot exigés par Riot, VBS/HVCI actifs (défaut 25H2).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Roadmap par phases
|
||||||
|
|
||||||
|
### Phase 0 — Recon disque + firmware (root requis, BLOQUANT avant toute écriture)
|
||||||
|
```bash
|
||||||
|
for d in /dev/nvme0n1 /dev/nvme1n1 /dev/nvme2n1; do sudo parted "$d" unit MiB print free; sudo sgdisk -p "$d"; done
|
||||||
|
sudo lsblk -dpno NAME,SIZE,MODEL
|
||||||
|
```
|
||||||
|
Confirmer l'absence réelle d'espace libre → détermine chemin **neuf (Anaconda)** vs **shrink post‑install**. Dans le firmware : vérifier IOMMU (VT‑d/AMD‑Vi) **actif** et BIOS à jour (exigence Vanguard).
|
||||||
|
|
||||||
|
### Phase 1 — Provisionner l'hibernation (chemin machine réelle : swapfile)
|
||||||
|
Sizing **RAM + VRAM + marge = 96 GiB** (et non 64 : la VRAM préservée est recopiée dans l'image — voir §7).
|
||||||
|
```bash
|
||||||
|
sudo mkswap -U clear --size 96G --file /swapfile
|
||||||
|
sudo chmod 600 /swapfile
|
||||||
|
sudo swapon /swapfile && swapon --show
|
||||||
|
# fstab : ajouter '/swapfile none swap defaults 0 0' ; supprimer la ligne /mnt/games2 dupliquée
|
||||||
|
N=$(sudo filefrag -v /swapfile | awk '$1=="0:"{print substr($4,1,length($4)-2)}') # resume_offset
|
||||||
|
sudo chattr +i /swapfile # fige l'offset (anti-dérive silencieuse)
|
||||||
|
```
|
||||||
|
UUID de reprise = **FS hôte (racine)** `84888eb3-cab9-407b-b407-ea4b3133c4a6`, pas l'UUID interne du swap.
|
||||||
|
|
||||||
|
### Phase 2 — Hook resume + cmdline
|
||||||
|
```bash
|
||||||
|
# /etc/mkinitcpio.conf : resume APRÈS block, AVANT filesystems (busybox, PAS systemd)
|
||||||
|
# HOOKS=(base udev autodetect keyboard keymap modconf block resume filesystems fsck)
|
||||||
|
sudo mkinitcpio -P
|
||||||
|
sudo lsinitcpio /boot/initramfs-linux.img | grep -i resume # doit matcher
|
||||||
|
# /etc/default/grub :
|
||||||
|
# GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3 quiet resume=UUID=84888eb3-cab9-407b-b407-ea4b3133c4a6 resume_offset=N hibernate.compressor=lz4"
|
||||||
|
# → hibernate.compressor=lz4 : compression la plus RAPIDE de l'image (moins de
|
||||||
|
# CPU que lzo) → hibernate/resume plus courts sur une image de 60-90 Go.
|
||||||
|
# GRUB_DISABLE_OS_PROBER=true (aucune entrée GRUB->Windows -> PCR4/PCR7 propres)
|
||||||
|
# GRUB_TIMEOUT=1 + GRUB_TIMEOUT_STYLE=hidden → resume quasi immédiat (menu
|
||||||
|
# masqué, maintenir Maj/Échap pour l'afficher). Boot le plus rapide côté FWS.
|
||||||
|
sudo grub-mkconfig -o /boot/grub/grub.cfg
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3 — GPU NVIDIA (sans quoi le resume graphique est cassé)
|
||||||
|
```bash
|
||||||
|
# /etc/modprobe.d/nvidia-hibernate.conf
|
||||||
|
# options nvidia NVreg_PreserveVideoMemoryAllocations=1 NVreg_TemporaryFilePath=/var/tmp
|
||||||
|
sudo systemctl enable nvidia-suspend.service nvidia-hibernate.service nvidia-resume.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4 — Valider l'hibernation AVANT de la câbler au bouton « Jouer »
|
||||||
|
```bash
|
||||||
|
sync; echo test_resume | sudo tee /sys/power/disk; sudo systemctl hibernate # non destructif
|
||||||
|
echo platform | sudo tee /sys/power/disk
|
||||||
|
# Puis cycle réel, SESSION GRAPHIQUE CHARGÉE (jeu/desktop lourd) :
|
||||||
|
sudo systemctl hibernate # rallumer
|
||||||
|
journalctl -b -1 -k | grep -iE 'PM: hibernation|Image saved|resume|Restarting tasks'
|
||||||
|
```
|
||||||
|
Critère de succès : session préservée **et** affichage NVIDIA intact au resume. Échec → le flux « Jouer » doit basculer en reboot propre (§7).
|
||||||
|
|
||||||
|
### Phase 5 — Secure Boot signé, lockdown OFF (§4)
|
||||||
|
### Phase 6 — Installateur : carve NTFS + outillage Windows (§6)
|
||||||
|
### Phase 7 — Orchestrateur one‑shot + GUI (§5)
|
||||||
|
### Phase 8 — Spike end‑to‑end sur ISO réelle : hibernate → BootNext → Windows → Valorant → retour FWS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Point dur Secure Boot ↔ hibernation — SOLUTION RETENUE
|
||||||
|
|
||||||
|
**Le nœud :** Vanguard exige Secure Boot **globalement** au firmware → FWS doit aussi booter sous SB. Or Fedora/Ubuntu bloquent l'hibernation sous SB via le **lockdown**.
|
||||||
|
|
||||||
|
**Fait établi et vérifié :** sur **noyau mainline Arch**, activer Secure Boot **n'arme PAS** le lockdown. Le patch Garrett/Howells liant SB↔lockdown a été **refusé upstream** ; il est downstream Fedora/Ubuntu uniquement. Sur la cible : `CONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOT` **absent**, `CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE=y`, pas de `LSM_EARLY`, `/sys/kernel/security/lockdown = [none]`. **Signer ≠ lockdown : ils sont orthogonaux.**
|
||||||
|
|
||||||
|
**Solution retenue : `sbctl` (clés db custom), en CONSERVANT les clés Microsoft.** Justification : firmware déjà en User Mode avec db Microsoft → Windows boote sans y toucher ; friction minimale ; hook pacman de re‑signature intégré. On **garde GRUB** (pas d'UKI) : cmdline `resume=` éditable et **PCR4 firmware stable** à chaque MAJ noyau (favorable à un futur scellement TPM du swap LUKS). *Voie alternative documentée : shim+MOK (modèle Ubuntu/Fedora, plus robuste sur OEM verrouillés, au prix d'un prompt MokManager) — à retenir si le firmware refuse l'enrôlement db custom.*
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo pacman -S sbctl
|
||||||
|
# PRÉREQUIS NON NÉGOCIABLE : le grubx64.efi installé n'a NI .sbat NI shim_lock -> réinstaller GRUB d'abord :
|
||||||
|
sudo grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB \
|
||||||
|
--sbat /usr/share/grub/sbat.csv --modules='normal search part_gpt part_msdos fat ext2 configfile linux echo test true loadenv all_video efi_gop tpm'
|
||||||
|
objdump -h /boot/EFI/GRUB/grubx64.efi | grep -E 'sbat' # doit exister
|
||||||
|
# Setup Mode au firmware, puis :
|
||||||
|
sudo sbctl create-keys
|
||||||
|
sudo sbctl enroll-keys --microsoft # IMPÉRATIF : réinjecte MS -> Windows/OROM bootent
|
||||||
|
sudo sbctl sign -s /boot/EFI/GRUB/grubx64.efi
|
||||||
|
sudo sbctl sign -s /boot/vmlinuz-linux
|
||||||
|
sudo sbctl verify
|
||||||
|
ls /usr/share/libalpm/hooks/ | grep sbctl # re-signe après MAJ noyau/grub
|
||||||
|
```
|
||||||
|
**Ordre impératif :** signer **avant** d'activer SB au firmware. Après tout `grub-install`, **re‑signer** (il régénère un binaire non signé). Vérif post‑activation : `bootctl status` → SB enabled ; `/sys/kernel/security/lockdown` → `[none]` ; `/sys/power/state` contient `disk`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Orchestration du boot
|
||||||
|
|
||||||
|
**Invariant de sûreté :** `BootOrder[0]=FWS` en permanence. `BootNext` (var UEFI one‑shot `8be4df61-93ca-11d2-aa0d-00e098032b8c`) **uniquement** pour les transitions. Tout crash retombe sur FWS.
|
||||||
|
|
||||||
|
### Aller FWS → Windows (`/usr/local/bin/fws-gameboot to-windows`, via pkexec)
|
||||||
|
Preflight **avant toute écriture NVRAM** — abort si un test échoue :
|
||||||
|
```bash
|
||||||
|
grep -q disk /sys/power/state || die 'hibernation indispo'
|
||||||
|
awk '/^SwapTotal/{s=$2}/^MemTotal/{m=$2}END{exit !(s>=m)}' /proc/meminfo || die 'swap < RAM'
|
||||||
|
# + garde VRAM (voir §7) : image projetée <= swap
|
||||||
|
grep -q 'resume=' /proc/cmdline || die 'resume= absent'
|
||||||
|
bootctl status 2>/dev/null | grep -qi 'Secure Boot: enabled' || die 'Secure Boot inactif'
|
||||||
|
WIN=$(efibootmgr | sed -n 's/^Boot\([0-9A-Fa-f]\{4\}\)\*\? Windows Boot Manager.*/\1/p' | head -1)
|
||||||
|
[ -n "$WIN" ] || die 'entrée Windows introuvable'
|
||||||
|
findmnt -rno TARGET -t ntfs3,ntfs | while read m; do umount "$m" || true; done # NTFS jamais rw persistante
|
||||||
|
efibootmgr -n "$WIN"
|
||||||
|
efibootmgr | grep -q "BootNext: $WIN" || { efibootmgr -N; die 'BootNext non écrit'; } # RELECTURE obligatoire
|
||||||
|
# Watchdog anti-hang (§7) autour de :
|
||||||
|
timeout 180 systemctl hibernate || { efibootmgr -N; die 'échec/hang hibernation'; }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Retour Windows → FWS (`C:\ProgramData\FWS\fws-play.ps1`, compte jeu autologin)
|
||||||
|
```powershell
|
||||||
|
& "$PSScriptRoot\fws-return.ps1" -Target Windows # RE-ARME BootNext=Windows au boot -> crash/WU réatterrit sur Windows
|
||||||
|
Start-Process 'C:\Riot Games\Riot Client\RiotClientServices.exe' -ArgumentList '--launch-product=valorant','--launch-patchline=live'
|
||||||
|
do { Start-Sleep 5 } until (Get-Process VALORANT-Win64-Shipping -EA SilentlyContinue)
|
||||||
|
Get-Process VALORANT-Win64-Shipping -EA SilentlyContinue | Wait-Process # quit OU crash
|
||||||
|
& "$PSScriptRoot\fws-return.ps1" -Target FWS # pose BootNext=FWS
|
||||||
|
shutdown /r /t 0
|
||||||
|
```
|
||||||
|
Pose de `BootNext` côté Windows = **écriture directe** de la variable UEFI (`SetFirmwareEnvironmentVariableExW` + `SeSystemEnvironmentPrivilege`), repli `bcdedit /set {fwbootmgr} bootsequence`. Belt‑and‑suspenders : tâche `ONSTART` (SYSTEM) qui arme `BootNext=FWS` dès le boot (couvre les crashs), + hook GPO shutdown, + `powercfg /h off` (reboot plein, NTFS jamais sale).
|
||||||
|
|
||||||
|
### À chaque boot FWS — `fws-gameboot-bootfix.service` (oneshot, `ConditionPathExists=!/run/archiso`)
|
||||||
|
Réasserte `BootOrder[0]=FWS` (auto‑guérison anti Windows Update) et réécrit le marqueur FAT `<ESP>/EFI/FWS/return.json = {fws_bootnum, windows_bootnum}` (les `Boot####` bougent avec `efibootmgr`).
|
||||||
|
|
||||||
|
**Filet ultime :** même mécanisme Windows HS → `BootOrder[0]=FWS` ramène à FWS, image d'hibernation intacte (Windows ne touche jamais l'ext4).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Intégration installateur FWS
|
||||||
|
|
||||||
|
Deux chemins (la cible réelle est déjà installée, sans espace libre).
|
||||||
|
|
||||||
|
**Chemin NEUF (Anaconda).** Nouveau spoke `fws_windows.py` (clone exact de `fws_desktop.py`, découvert au runtime, aucun rebuild) : case « Dual‑boot Windows (Valorant) », combo disque, spin taille (défaut **200 GiO** — Win11 ~40 + Valorant ~40 + updates ; min 100). Écrit `/tmp/fws-windows`. Le **partitionnement NTFS se fait en `%pre`** (blivet ne sait pas créer du NTFS) :
|
||||||
|
```bash
|
||||||
|
# %pre — REMPLACE le clearpart --all destructeur (voir §7). UEFI + enabled=1 requis :
|
||||||
|
sgdisk --zap-all "$DISK"
|
||||||
|
sgdisk -n1:0:+1GiB -t1:ef00 -c1:EFI "$DISK" # ESP >=1 GiO
|
||||||
|
sgdisk -n2:0:+96GiB -t2:8200 -c2:swap "$DISK" # swap DÉDIÉ = RAM+VRAM -> pas de resume_offset
|
||||||
|
sgdisk -n3:0:+${WIN_GIB}GiB -t3:0700 -c3:WINDOWS "$DISK" # slot NTFS (Microsoft basic data)
|
||||||
|
sgdisk -n4:0:0 -t4:8300 -c4:FWSROOT "$DISK"
|
||||||
|
partprobe "$DISK"; udevadm settle
|
||||||
|
mkfs.fat -F32 ${DISK}p1; mkswap ${DISK}p2; mkfs.ntfs -Q -L WINDOWS ${DISK}p3
|
||||||
|
```
|
||||||
|
```ks
|
||||||
|
ignoredisk --only-use=nvme2n1 # protège nvme0n1/nvme1n1 (games)
|
||||||
|
clearpart --none
|
||||||
|
part /boot --onpart=nvme2n1p1 --fstype=efi --noformat
|
||||||
|
part swap --onpart=nvme2n1p2 --noformat
|
||||||
|
part / --onpart=nvme2n1p4 --fstype=ext4
|
||||||
|
# nvme2n1p3 NON référencé -> NTFS intact ; en %post : resume=UUID=<swap> (partition dédiée, pas d'offset)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Chemin POST‑INSTALL (machine réelle) — `fws-windows-setup`.** Shrink hors‑ligne, ordre **load‑bearing** (cibler un NVMe games pour garder la racine intacte) :
|
||||||
|
```bash
|
||||||
|
e2fsck -f /dev/nvme1n1p1
|
||||||
|
resize2fs /dev/nvme1n1p1 1550G # FS sous la taille finale
|
||||||
|
parted /dev/nvme1n1 resizepart 1 1560GiB # puis la partition
|
||||||
|
resize2fs /dev/nvme1n1p1 # re-remplit
|
||||||
|
sgdisk -n0:0:0 -t0:0700 -c0:WINDOWS /dev/nvme1n1
|
||||||
|
mkfs.ntfs -Q -L WINDOWS /dev/nvme1n1p2
|
||||||
|
```
|
||||||
|
|
||||||
|
**Outillage Windows (aucun bundling).**
|
||||||
|
- `fws-windows-media` : clé USB Secure‑Boot‑propre depuis l'ISO **de l'utilisateur**. `install.wim` >4 GiO illégal en FAT32 → FAT32 + `wimlib-imagex split install.wim /usb/sources/install.swm 3800` (pas de uefi‑ntfs, `bootmgfw.efi` signé MS).
|
||||||
|
- `fws-windows-postinstall` : lit le `Boot####` « Windows Boot Manager » → `/etc/fws/windows-boot-id` ; `efibootmgr -o <FWS>,…,<Windows>` (Setup Windows se remet en tête) ; vérifie SB/TPM2/VBS ; **rappels DANS Windows** : `powercfg /h off`, **NE PAS activer BitLocker** (ou suspendre aux bascules).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Risques & mitigations (revue adversariale intégrée)
|
||||||
|
|
||||||
|
| # | Sévérité | Problème | Mitigation retenue |
|
||||||
|
|---|----------|----------|--------------------|
|
||||||
|
| 1 | **BLOQUANT** | `clearpart --all --disklabel=msdos` inconditionnel dans `interactive-defaults.ks` → efface les 3 NVMe (dont games) + force MBR sur UEFI. | **Ne jamais livrer l'ISO en l'état.** `%pre` conditionné `/sys/firmware/efi` + `ignoredisk --only-use` + `clearpart --none` + `part --onpart` sur layout sgdisk. Protéger nvme0/1 explicitement. Tester sur ISO. |
|
||||||
|
| 2 | **BLOQUANT** | Resume GPU cassé : RTX 5090 Blackwell sans `PreserveVideoMemoryAllocations` → écran noir/corruption au S4. Design muet sur le GPU. | `NVreg_PreserveVideoMemoryAllocations=1` + `NVreg_TemporaryFilePath` ; `nvidia-hibernate/resume` actifs ; **tester le resume session chargée AVANT** de câbler le bouton ; fallback reboot obligatoire si échec (Phase 4). |
|
||||||
|
| 3 | Majeur | Swap = RAM ignore la VRAM recopiée (jusqu'à 32 Go) → image 75‑90 GiO > swap 64 → échec. | **Swap = RAM + VRAM + marge = 96 GiO.** Preflight : estimer VRAM en usage (`nvidia-smi`), refuser/basculer reboot si image projetée > swap. Tester pire cas (jeu déjà lancé côté Linux). |
|
||||||
|
| 4 | Majeur | Brick FWS par révocation **dbx/SBAT** poussée par Windows Update (précédents BootHole/SBAT réels). Windows booté **fréquemment** ici → risque élevé. | shim/grub à jour et re‑signés ; **différer/figer Windows Update** sur ce Windows gaming minimal ; tester bascule après chaque MAJ Windows/Vanguard ; **fournir un média de récupération FWS** (ré‑enrôlement/re‑signature). |
|
||||||
|
| 5 | Majeur | **BitLocker auto‑activé** par TPM+SB (Win11 25H2 Device Encryption) : le flip SB OFF→ON change PCR7 → écran de récupération, clé dans le compte MS. | **Activer Secure Boot AVANT d'installer Windows** (PCR7 stable dès l'install, pas de flip). Documenter : suspendre/désactiver BitLocker, **sauvegarder la clé**, `powercfg /h off`. Vérifier device‑encryption au postinstall et alerter. Nuance : les bascules `BootNext` récurrentes ne touchent PAS PCR7 (bootmgfw chargé à l'identique). |
|
||||||
|
| 6 | Majeur | `systemctl hibernate` qui **hang** (driver refuse S4) : machine allumée, session ni sauvée ni basculée, `BootNext=Windows` resté armé → départ surprise. | ⚠️ **NON mitigeable en userspace** : pendant un hang S4 le process (et tout watchdog) est gelé — un `timeout` est impossible (il tuerait aussi les hibernations *réussies*, qui ne rendent la main qu'au resume). Parades réelles : **préflight strict** (élimine les causes courantes) + **auto-guérison** au reset (BootNext=Windows → Windows → tâche ONSTART arme FWS → retour) + **filet BootOrder[0]=FWS**. Tester la robustesse S4 sur la carte ; documenter la récupération post-hang. |
|
||||||
|
| 7 | Majeur | Resume **silencieux** en échec (offset dérivé/image corrompue) → cold boot, session perdue **sans notification**. Incohérence swapfile+offset (fragile) vs partition dédiée (robuste). | **Unifier sur partition swap dédiée** dès que possible (chemin installeur) ; `chattr +i` sur tout swapfile résiduel ; revalider l'offset au preflight ET au retour ; **notifier explicitement** « session non restaurée » au cold boot post‑hibernation. |
|
||||||
|
| 8 | Majeur | Politique de fallback **contradictoire** entre docs (`hibernate || die` reste sur FWS vs `|| reboot` part sur Windows sans session). | **Trancher UNE politique** (reco : échec hibernation → confirmation utilisateur puis reboot Windows sans session ; sinon annuler proprement `efibootmgr -N`) et l'implémenter à l'identique. Interdire la divergence en CI. |
|
||||||
|
| 9 | Majeur | Sémantiques `BootNext`/`BootOrder` **non validées** (Windows pas installé) : firmware peut ignorer BootNext, réordonner, purger l'entrée FWS. Mapping `bcdedit {fwbootmgr}` → BootNext non documenté MS. | Valider empiriquement dès la 1re install (écrire BootNext côté Windows, relire via `efibootmgr` côté Linux). Entrée de secours `\EFI\BOOT\BOOTX64.EFI` (`grub-install --removable`). Ne pas dépendre que de BootNext. |
|
||||||
|
| 10 | Majeur | Prérequis SB non résolu : `grubx64.efi` sans `.sbat` ni `shim_lock` → shim refuse GRUB ; `grub-install` régénère non signé. | Réinstall GRUB (`--sbat` + modules) + signature **avant** d'activer SB ; hooks re‑signature couvrant `linux` + LTS/fallback ; vérifier `objdump 'sbat'` ; ne jamais activer SB sans `sbverify` OK (§4). |
|
||||||
|
| 11 | Mineur | Fondement « bit CPUID hyperviseur = 0 » **FAUX** : Zen 5 + Win11 25H2 → VBS/Hyper‑V actif, bit=1, toléré par Vanguard. | **Ne pas combattre VBS.** Corriger la doc : Vanguard vérifie SB actif + TPM présent + **absence d'hyperviseur TIERS**, pas un bit à 0. Valider que Valorant lance avec VBS on par défaut. |
|
||||||
|
| 12 | Mineur | SB sans lockdown côté Linux = conformité, pas défense : initramfs non signé, `grub.cfg` éditable, image d'hibernation **en clair** sur ext4. | Assumer honnêtement (SB ici = case Vanguard). Durcissement hors‑MVP : UKI cmdline scellée, **LUKS+TPM sur le swap** (impose repartition). |
|
||||||
|
| 13 | Mineur | Risque fournisseur Vanguard (changement de politique) + overhead ~3‑5 min/session. | Compat annoncée **best‑effort**, re‑test après chaque bump. **Offrir un mode « reboot simple »** (sans hibernation) comme voie robuste par défaut, hibernation en confort opt‑in → réduit drastiquement la surface de brick/perte de session. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Décisions ouvertes à trancher avec l'utilisateur
|
||||||
|
|
||||||
|
1. **Hibernation vs reboot simple par défaut.** La couche hibernation ajoute VRAM/offset/resume fragiles pour ~3‑5 min gagnées. Recommandation revue : **reboot simple robuste par défaut, hibernation opt‑in**. À arbitrer (c'est un choix produit majeur).
|
||||||
|
2. **Swap : partition dédiée (repartition, robuste, sans offset) vs swapfile 96 GiO (aucun repartition, offset fragile).** Sur la machine déjà pleine, swapfile pragmatique ; sur install neuve, partition dédiée. Unifier ?
|
||||||
|
3. **Chiffrement de l'image d'hibernation (MVP en clair vs LUKS+TPM d'emblée).** Conditionne planning et gestion PCR.
|
||||||
|
4. **Voie Secure Boot : `sbctl` (db custom, reco) vs shim+MOK** (plus robuste OEM, prompt MokManager). Dépend de l'acceptation du firmware pour l'enrôlement db custom.
|
||||||
|
5. **Disque Windows : shrink de la racine nvme2n1 (même NVMe que l'ESP) vs un NVMe games (nvme1n1, boot cross‑disque).** Quel disque l'utilisateur sacrifie ?
|
||||||
|
6. **ESP partagée 512 MiO vs ESP Windows dédiée** lors du carve‑out (512 MiO très juste avec bootmgfw + noyaux + marqueur `return.json`).
|
||||||
|
7. **NTFS pré‑formaté (`mkfs.ntfs -Q`) vs espace non alloué + MSR** pour des mesures PCR/BitLocker les plus propres (à trancher par test).
|
||||||
|
8. **Retour auto (watcher WMI, risque heuristique AV) vs bouton manuel** (défaut reco = manuel).
|
||||||
|
9. **Bootstrap Windows : FWS pilote l'install (autounattend/SetupComplete, le plus propre) vs RunOnce offline** sur Windows existant.
|
||||||
|
10. **Politique clé de signature FWS : commune dans l'ISO (simple, fuite = compromission de flotte) vs générée par machine** (sûre, non industrialisable en masse).
|
||||||
|
11. **Taille NTFS par défaut : 150 vs 200 GiO** (recommandation : 200).
|
||||||
|
12. **Différer/figer Windows Update** sur le Windows gaming (mitigation brick dbx/SBAT #4) — acceptable pour l'utilisateur ?
|
||||||
|
|
||||||
|
> **Portes bloquantes avant tout ship :** (a) kickstart rendu non destructif et testé sur ISO ; (b) resume GPU NVIDIA validé session chargée ; (c) swap ≥ RAM+VRAM ; (d) GRUB signé+`.sbat` avant activation SB ; (e) `BootNext` validé empiriquement sur la carte mère ; (f) média de récupération FWS prêt.
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
# PLAN DE BUILD — Spoke installateur FWS « Dual-boot Windows gaming » (option A, OFFLINE-APPLY)
|
||||||
|
|
||||||
|
> État vérifié dans le dépôt au moment de ce plan : le spoke `fws_windows.py` + `fws_windows.glade` **existent déjà** (à amender, pas à créer). `wimlib`, `hivex`, `sbctl`, `sbsigntools`, `nvidia*`, `binutils` sont **absents** des deux `packages.x86_64` (présents : `efibootmgr`, `ntfs-3g`, `gptfdisk`, `dosfstools`, `parted`, `e2fsprogs`). `fws-windows-deploy`, `fws-bcd-write`, le template `unattend.offline.xml.tmpl` et le repli `fws-windows-bcdfix` **n'existent pas encore**. `gameboot.conf`, `fws-gameboot-bootfix(.service)`, le hook de re-signature, `fws-secureboot-setup`, `fws_recap.py` **existent**. Le `%pre` (l.99‑106) est disque-agnostique (juste `clearpart` firmware-conditionnel) ; le `%post` a 3 blocs : nochroot early (114‑157), chroot (165‑613), nochroot final d'exfil de log (624‑628).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1) Résumé & principe
|
||||||
|
|
||||||
|
L'utilisateur, pendant l'install FWS, coche **« Dual-boot Windows gaming »**, choisit **un disque à dédier**, une **taille NTFS** et **fournit son ISO Windows 11**. FWS mène ensuite **tout** de façon automatique : carve des partitions, dépôt de Windows 11 par **application offline de `install.wim`** (jamais Windows Setup), rendu bootable depuis Linux, injection du composant `gameboot-windows` + answer file, provisioning FWS (swap d'hibernation dédié, `resume=`, NVIDIA S4, chaîne Secure Boot). Une **seule** action manuelle irréductible subsiste : la visite firmware pour passer en Setup Mode et **activer Secure Boot** (non automatisable depuis l'OS) — à assumer honnêtement dans la doc et l'UI (voir §8).
|
||||||
|
|
||||||
|
Le spoke est un **clone du pattern `fws_desktop.py`** : `.py` + `.glade` déposés dans l'overlay airootfs, découverts au runtime par `collect()` d'Anaconda (**aucun rebuild du paquet anaconda**). Son **seul canal** vers l'installation est le fichier `/tmp/fws-windows` (clé=valeur), relu par le kickstart. Il **n'exécute aucune opération disque**. Tout le travail dépendant des choix GUI vit en `%post --nochroot` (le `%pre` tourne **avant** le GUI → ne voit pas `/tmp/fws-windows` : contrainte dure, source unique de vérité = `%post`).
|
||||||
|
|
||||||
|
Répartition des 3 NVMe = garde-fou anti-mauvais-disque : **(a)** disque FWS = celui sélectionné dans « Destination », partitionné nativement par blivet ; **(b)** disque Windows = `disk=` du spoke, jamais donné à Anaconda, carvé 100 % par nos soins ; **(c)** tout le reste = jamais nommé, donc physiquement intouchable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) Approche de déploiement Windows retenue (justifiée)
|
||||||
|
|
||||||
|
**RETENU : (A) OFFLINE-APPLY, 100 % depuis le live FWS**, modèle « apply image » de WDS/MDT. On **n'allume jamais** Windows Setup.
|
||||||
|
|
||||||
|
Pipeline : `mount -o loop,ro` de l'ISO → `wimlib-imagex apply` de `install.wim`/`.esd` **directement sur le bloc device NTFS** (backend libntfs-3g : descripteurs de sécurité, ADS, hardlinks, reparse préservés) → copie des ressources bootmgr + **construction du magasin BCD offline** → dépôt du `unattend.xml` offline + du dossier `gameboot-windows` par simples `cp` → entrée UEFI. Le 1er boot enchaîne `specialize` + `oobeSystem` unattended sur l'image généralisée.
|
||||||
|
|
||||||
|
**Pourquoi pas (B) BOOT-INTO-SETUP** : lancer Setup depuis le disque sans clé USB exige de toute façon d'écrire un BCD depuis Linux (**même point dur**), plus le gate de compatibilité TPM/SB (bypass LabConfig), le repartitionnement piloté par Setup (risque pour les 3 NVMe), `Windows.old`, et des reboots entrelacés avec l'install FWS. (B) n'est « béni » qu'en variante clé-USB-bootée-à-la-main → viole « plus rien à la main ».
|
||||||
|
|
||||||
|
**Avantages décisifs de (A)** : la partition cible est adressée **côté Linux par `/dev/disk/by-partlabel/WINDOWS`** — pas de `DiskID`/`DiskConfiguration`/`WillWipeDisk` dans le XML → **la classe de panne « effacer le mauvais NVMe » n'existe pas**. Le gate TPM2/SB ne se déclenche jamais (il n'existe que dans Setup). L'answer file est un **unattend OFFLINE sans passe windowsPE** (Setup ne tourne pas). Sur 25H2, `\Windows\Panther\unattend.xml` + vrais fichiers sur le NTFS est **plus fiable** que l'autounattend média (SetupPrep.exe, injections Rufus/Ventoy qui « sautent » specialize/oobe).
|
||||||
|
|
||||||
|
**Point dur unique et honnête = le BCD offline** (format registre propriétaire, `bcdboot` est Windows-only). Co-conçu avec un **repli WinPE-bcdboot automatique** qui reste mains-libres (§6, §9).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) Roadmap par phases (fichiers + commandes)
|
||||||
|
|
||||||
|
### Phase 0 — Paquets (BLOQUANT, prérequis de tout le reste)
|
||||||
|
Ajouter aux **deux** `configs/{releng,baseline}/packages.x86_64` :
|
||||||
|
- **Live/déploiement** : `wimlib` (apply WIM/ESD), `hivex` (BCD + anti-BitLocker hors ligne).
|
||||||
|
- **Secure Boot** : `sbctl`, `sbsigntools`, `binutils` (objdump du hook SBAT), `mokutil`.
|
||||||
|
- **Hibernation** : `nvidia`, `nvidia-utils` (hooks `nvidia-suspend/hibernate/resume`).
|
||||||
|
|
||||||
|
Déjà présents et suffisants : `ntfs-3g` (fournit `mkntfs`/`ntfs-3g`), `gptfdisk` (`sgdisk`), `dosfstools` (`mkfs.fat`), `efibootmgr`, `parted`, `e2fsprogs`. Sans `wimlib`+`hivex`, le `%post` offline est **impossible** ou retombe sur pacman réseau pendant l'install (fragile).
|
||||||
|
|
||||||
|
### Phase 1 — Corriger la chaîne Secure Boot (BLOQUANT, sinon FWS non bootable sous SB)
|
||||||
|
**Défaut vérifié** : `install_grub` (kickstart l.212‑218) pose GRUB en `--bootloader-id=FWS` (+ `--removable` → `\EFI\FWS\grubx64.efi` **et** `\EFI\BOOT\BOOTX64.EFI`), mais `fws-secureboot-setup` (l.37‑45) réinstalle un GRUB **séparé** en `--bootloader-id=GRUB` (`\EFI\GRUB\grubx64.efi`) et **ne signe que ce chemin** + les vmlinuz. Résultat : activer SB → firmware charge `\EFI\FWS\grubx64.efi` **non signé** → refus ; le repli `\EFI\BOOT\BOOTX64.EFI` est **aussi non signé** → **machine sans OS bootable**.
|
||||||
|
|
||||||
|
**Fix (unifier l'identité bootloader) :** aligner `fws-secureboot-setup` sur `--bootloader-id=FWS` **et** signer **les trois chemins réellement utilisés** :
|
||||||
|
```
|
||||||
|
sbctl sign -s /boot/efi/EFI/FWS/grubx64.efi
|
||||||
|
sbctl sign -s /boot/efi/EFI/BOOT/BOOTX64.EFI # repli removable (le firmware peut le charger)
|
||||||
|
for k in /boot/vmlinuz-linux*; do sbctl sign -s "$k"; done
|
||||||
|
sbctl verify # DOIT être clean sur les 3 chemins AVANT d'instruire l'activation SB
|
||||||
|
```
|
||||||
|
Et faire pointer `fws-gameboot-bootfix` sur l'entrée NVRAM **signée**. Ne jamais instruire l'utilisateur d'activer SB tant que `sbctl verify` n'est pas clean.
|
||||||
|
|
||||||
|
### Phase 2 — Amender le spoke (BLOQUANT : perte de données par défaut)
|
||||||
|
`fws_windows.py` `refresh()` l.147 fait `set_active(0)` → cible par défaut = 1er disque lsblk = **`nvme0n1`, un disque de jeux plein**. Un utilisateur qui coche, parcourt l'ISO et clique Suivant **sans toucher au combo** fait `sgdisk --zap-all` sur un disque plein. Corriger (voir §4).
|
||||||
|
|
||||||
|
### Phase 3 — Créer le déployeur `fws-windows-deploy` (+ helpers)
|
||||||
|
Nouveaux fichiers :
|
||||||
|
- `configs/releng/airootfs/usr/local/bin/fws-windows-deploy` — orchestrateur bash (garde-fous → carve → apply → BCD → registre → answer file → injection → UEFI → vérif → handoff). Exécutable. Rejouable hors installateur.
|
||||||
|
- `configs/releng/airootfs/usr/local/lib/fws/fws-bcd-write` — helper BCD offline via hivex (**voie primaire, à auditer**).
|
||||||
|
- `configs/releng/airootfs/usr/local/bin/fws-windows-bcdfix` — **repli WinPE-bcdboot** (chemin par défaut recommandé pour la 1re validation matériel).
|
||||||
|
- `configs/releng/airootfs/usr/local/share/fws/gameboot-windows/unattend.offline.xml.tmpl` — template tokenisé (§6).
|
||||||
|
|
||||||
|
### Phase 4 — Câbler le kickstart
|
||||||
|
`interactive-defaults.ks` :
|
||||||
|
1. `%pre` : **inchangé** (juste un commentaire fort : « aucune op disque dépendante du GUI ici — voir %post »).
|
||||||
|
2. nochroot early (~l.138) : propager `[ -f /tmp/fws-windows ] && cp -f /tmp/fws-windows "$SYSROOT/tmp/fws-windows"`.
|
||||||
|
3. `%post` chroot (avant l'unmount, après `install_grub`/`fws-secureboot-setup`) : bloc gaté `. /tmp/fws-windows; [ "${enabled:-0}" = 1 ]` → hibernation (hook `resume`, `resume=UUID=`, NVIDIA), enable `fws-gameboot-bootfix.service`.
|
||||||
|
4. **Nouveau** `%post --nochroot` **inséré entre la fin du chroot (l.613) et le nochroot final d'exfil (l.624)** : appel non bloquant `/usr/local/bin/fws-windows-deploy --state /tmp/fws-windows --sysroot "$SYSROOT" 2>&1 | tee /tmp/fws-windows-deploy.log`.
|
||||||
|
|
||||||
|
### Phase 5 — Récap & validation
|
||||||
|
`fws_recap.py` : ligne « Dual-boot Windows : <disque> <taille> GiO — CE DISQUE SERA EFFACÉ » + **contrôle croisé** (si `disk=` ∈ `SelectedDisks` → avertissement rouge). Puis `./build.sh`, boot ISO, vérifier l'écran sur le hub Résumé, puis **spike end-to-end sur une vraie ISO Win11 25H2**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4) Le spoke Anaconda (py + glade) — amendements
|
||||||
|
|
||||||
|
Le squelette existant est correct (`NormalSpoke`, `category=SystemCategory`, `builderObjects=['adj_size','fwsWindowsWindow']`, cycle `initialize/refresh/apply`, écrit `/tmp/fws-windows`). **Trois corrections load-bearing** :
|
||||||
|
|
||||||
|
**(a) Supprimer la présélection dangereuse.** `refresh()` : retirer le `elif self._disks: self._combo.set_active(0)`. Combo **sans choix par défaut** (id vide) → l'utilisateur DOIT choisir explicitement. `completed` (déjà : exige `disk` + ISO lisible) refuse alors tant que rien n'est choisi.
|
||||||
|
|
||||||
|
**(b) Enrichir l'affichage disque + garde-fous UI.** Dans `_list_disks()`, afficher pour chaque disque : modèle, taille, **et un indicateur d'occupation** (`lsblk -no FSUSE%,FSTYPE,MOUNTPOINT` ou présence de partitions). Marquer visuellement « ⚠ contient des données » un disque non vide. Ajouter dans `_validate()` : si le disque choisi n'est **pas vide**, exiger une **case « effacer quand même »** (nouveau `chk_confirm_erase`, propagé en `confirm_erase=1`) avant de considérer `completed`. Le message reste `« ⚠ TOUT le disque %s sera EFFACÉ »` avec le **modèle exact**.
|
||||||
|
|
||||||
|
**(c) Contrat `/tmp/fws-windows` figé** (relu par le kickstart) :
|
||||||
|
```
|
||||||
|
enabled=1|0
|
||||||
|
disk=/dev/nvme1n1 # disque ENTIÈREMENT dédié à Windows (mode DEDICATE)
|
||||||
|
size_gib=200 # taille de la NTFS
|
||||||
|
iso=/run/media/.../Win11_25H2.iso
|
||||||
|
edition=Windows 11 Pro # optionnel ; défaut Pro (deferral Windows Update)
|
||||||
|
confirm_erase=1 # exigé si le disque n'est pas vide
|
||||||
|
```
|
||||||
|
|
||||||
|
**Glade** (`fws_windows.glade`, existant) : `AnacondaSpokeWindow 'fwsWindowsWindow'` + `GtkAdjustment 'adj_size'` (lower 100, value 200, step 10). `chk_enable` (maître) grise `box_options` (`combo_disk`, `spin_size`, `entry_iso`+`btn_iso`, `iso_warn`, **`chk_confirm_erase`**), `note_platform` masqué affiché si `/sys/firmware/efi` absent. `GtkFileChooserNative` filtre `*.iso` démarrant sur `/run/media` ; le champ libre `entry_iso` reste le secours si le chooser natif est bancal en X minimal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5) Partitionnement non destructif
|
||||||
|
|
||||||
|
**Timing (dur)** : le carve **ne peut pas** vivre en `%pre` (tourne avant le GUI) ni dans les directives de partitionnement. Il vit dans `fws-windows-deploy` (`%post --nochroot`, live, cible sous `/mnt/sysroot`). Anaconda ne partitionne **que** le disque FWS (flux `clearpart --all` sur le seul disque « Destination »).
|
||||||
|
|
||||||
|
**Garde-fous AVANT le premier `sgdisk` — fail-CLOSED** (le défaut critique : un resolver muet renvoie « » et `"" != /dev/nvmeXn1` est toujours vrai → garde qui s'ouvre). Règle : **abort si un resolver renvoie vide ou non-bloc-device**, et comparer par **identifiant stable** quand possible.
|
||||||
|
```bash
|
||||||
|
fws_disk_of(){ 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; exit}'; }
|
||||||
|
|
||||||
|
WIN_DISK="$disk"
|
||||||
|
[ -b "$WIN_DISK" ] || die "cible invalide" # (a)
|
||||||
|
[ -d /mnt/sysroot ] && findmnt -no SOURCE /mnt/sysroot >/dev/null || die "sysroot non monté"
|
||||||
|
for probe in /mnt/sysroot /mnt/sysroot/boot/efi /run/archiso/bootmnt; do
|
||||||
|
d=$(fws_disk_of "$probe") || die "resolver muet ($probe) → abort" # fail-closed
|
||||||
|
[ "$WIN_DISK" != "$d" ] || die "$WIN_DISK == disque protégé ($probe)" # (b)(c)(d)
|
||||||
|
done
|
||||||
|
ISO_DISK=$(fws_disk_of "$(df --output=target "$iso" | tail -1)") && \
|
||||||
|
[ "$WIN_DISK" != "$ISO_DISK" ] || die "l'ISO vit sur la cible" # (e)
|
||||||
|
DGIB=$(( $(lsblk -dbno SIZE "$WIN_DISK") / 1073741824 ))
|
||||||
|
[ "$size_gib" -ge 100 ] && [ "$size_gib" -le $((DGIB-100)) ] || die "taille hors bornes" # (f)
|
||||||
|
# (g) occupation : aucune partition de $WIN_DISK montée/swap/VG/MD active
|
||||||
|
```
|
||||||
|
|
||||||
|
**Neutraliser l'auto-activation blivet** (sinon corruption) : pour chaque partition de `$WIN_DISK`, `umount` + `swapoff`, puis `vgchange -an` / `mdadm --stop` de tout VG/MD assis dessus ; revérifier via `findmnt`/`swapon --show`/`lsblk` qu'aucune n'est occupée. En mode DEDICATE, si quelque chose reste occupé de façon inattendue → **abort** (plus sûr que forcer).
|
||||||
|
|
||||||
|
**Carve (mode DEDICATE, défaut promis par le spoke)** — swap d'hibernation calculé dynamiquement (RAM+VRAM+marge, plancher 96 GiO) :
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
sgdisk --zap-all "$WIN_DISK"
|
||||||
|
sgdisk -n1:0:+1GiB -t1:ef00 -c1:WINESP "$WIN_DISK"
|
||||||
|
sgdisk -n2:0:+16MiB -t2:0c01 -c2:MSR "$WIN_DISK" # optionnel (décision ouverte)
|
||||||
|
sgdisk -n3:0:+${size_gib}GiB -t3:0700 -c3:WINDOWS "$WIN_DISK"
|
||||||
|
sgdisk -n4:0:+${SWAP_GIB}GiB -t4:8200 -c4:FWSSWAP "$WIN_DISK"
|
||||||
|
partprobe "$WIN_DISK"; udevadm settle
|
||||||
|
mkfs.fat -F32 -n WINESP /dev/disk/by-partlabel/WINESP
|
||||||
|
mkswap -L FWSSWAP /dev/disk/by-partlabel/FWSSWAP
|
||||||
|
```
|
||||||
|
**Décision swap** : partition dédiée `8200` **sur le disque Windows** (le seul qu'on pilote de bout en bout depuis le kickstart → seul endroit déterministe pour un swap de 96 GiO ; partition dédiée ⇒ **pas de `resume_offset` fragile** ; Windows ignore le type 8200). Contrepartie documentée : retirer/réordonner ce disque perd le resume FWS (filet `BootOrder[0]=FWS`).
|
||||||
|
|
||||||
|
**Adressage NTFS** : par **PARTLABEL** — mais asserter que `by-partlabel/WINDOWS` **ne résout qu'UN device**, sur `$WIN_DISK` (sinon un `WINDOWS` préexistant sur un autre NVMe piège l'apply). Envisager un label unique `FWSWIN` si collision possible.
|
||||||
|
|
||||||
|
Mode **SHRINK** (conserver les données du disque cible) = chemin risqué optionnel (`e2fsck -fy` → `resize2fs -P` pour vérifier le libérable → `resize2fs <taille>` → `parted resizepart` **table-only, jamais l'ancien `resize`** → `resize2fs` re-remplit → carve de la queue). Exige un champ spoke `shrink=1`+`shrink_part`, ext4 seulement, abort propre si FS réellement plein. À traiter en **enhancement**, pas en voie de prod.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6) autounattend OFFLINE + injection first-boot
|
||||||
|
|
||||||
|
**Nature** : unattend **OFFLINE**, `\Windows\Panther\unattend.xml` — **PAS** de passe `windowsPE`, **PAS** de `DiskConfiguration` (Setup ne tourne jamais). Seuls `specialize` + `oobeSystem` s'exécutent au 1er boot. Taille et partition **ne sont PAS** dans le XML (gérées par le carve + l'adressage PARTLABEL).
|
||||||
|
|
||||||
|
**Génération** (pattern sed déjà utilisé pour `hyprland.conf.tmpl`) — template tokenisé `unattend.offline.xml.tmpl` (`__USER__ __DISPLAY__ __HOSTNAME__ __LOCALE__ __INPUT__ __FIRSTBOOT__`), substitué par `sed` (évite tout piège d'échappement des `\` Windows et `&` XML). Contenu clé :
|
||||||
|
- `specialize` : `ComputerName`, `TimeZone`, et **ceinture** `reg add … OOBE /v BypassNRO /d 1` + `reg add … BitLocker /v PreventDeviceEncryption /d 1` (doublons du reg offline hivex).
|
||||||
|
- `oobeSystem` : `International-Core` fr-CH (`InputLocale 100C:0000100C`), `<OOBE>` avec **`<HideOnlineAccountScreens>true</HideOnlineAccountScreens>`** (clé anti compte-MS 25H2 ; `bypassnro` retiré ~03/2025 → ne pas s'y fier), `ProtectYourPC=3` ; `<UserAccounts><LocalAccount Group=Administrators Name=gaming>` **sans `<Password>`** (contrat auto-login ; **jamais** `Administrator` que 25H2 confond avec le compte intégré) ; `<AutoLogon LogonCount=5>` (survit au reboot Vanguard le temps qu'`Install-FwsGameboot` pose l'auto-login **permanent** Winlogon) ; `<FirstLogonCommands>` →
|
||||||
|
```
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\FWS\deploy\FirstBoot-FwsGameboot.ps1"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Injection** (le cœur) — vrais fichiers sur le NTFS (évite la méthode « scripts embarqués » cassée par SetupPrep.exe en 25H2) :
|
||||||
|
```bash
|
||||||
|
install -Dm644 /tmp/unattend.rendered /mnt/win/Windows/Panther/unattend.xml
|
||||||
|
mkdir -p /mnt/win/ProgramData/FWS/deploy
|
||||||
|
cp -r /usr/local/share/fws/gameboot-windows/* /mnt/win/ProgramData/FWS/deploy/ # source = rootfs LIVE
|
||||||
|
sync
|
||||||
|
```
|
||||||
|
`FirstBoot-FwsGameboot.ps1` utilise `$PSScriptRoot` → **doit** vivre sous `C:\ProgramData\FWS\deploy\` (satisfait ci-dessus). Compte admin ⇒ `FirstLogonCommands` s'exécute **élevé**, `#Requires -RunAsAdministrator` satisfait sans UAC.
|
||||||
|
|
||||||
|
**Anti-BitLocker HORS LIGNE (primaire, crucial)** : le flip SB OFF→ON change PCR7 → écran de récupération si Device Encryption s'active, **clé partie vers un compte Microsoft inexistant** (compte local) ⇒ verrouillage définitif. Écrire dans le **ControlSet pointé par `SYSTEM\Select\Current`** (= 001 sur image fraîche, **jamais `CurrentControlSet`**) :
|
||||||
|
```bash
|
||||||
|
SEL=$(hivexget /mnt/win/Windows/System32/config/SYSTEM /Select Current) # → 1
|
||||||
|
hivexregedit --merge --prefix 'HKEY_LOCAL_MACHINE\SYSTEM' \
|
||||||
|
/mnt/win/Windows/System32/config/SYSTEM <<< \
|
||||||
|
"[HKEY_LOCAL_MACHINE\SYSTEM\ControlSet00$SEL\Control\BitLocker]
|
||||||
|
\"PreventDeviceEncryption\"=dword:00000001"
|
||||||
|
hivexget /mnt/win/.../SYSTEM /ControlSet00$SEL/Control/BitLocker PreventDeviceEncryption # relire == 1
|
||||||
|
```
|
||||||
|
Défense en profondeur : les `reg add` `specialize` ci-dessus **et** un `manage-bde -off` / vérification « Device Encryption OFF » au 1er boot conditionnant le go.
|
||||||
|
|
||||||
|
**BCD + bootabilité (point dur)** :
|
||||||
|
- **4a — ressources** : `cp -rn /mnt/win/Windows/Boot/EFI/. /mnt/esp/EFI/Microsoft/Boot/` (+ `Fonts/`, `Resources/`) ; `bootmgfw.efi` reste signé MS → boote sous SB (clés MS conservées par `enroll-keys --microsoft`).
|
||||||
|
- **4b — voie primaire** `fws-bcd-write` : `cp .../config/BCD-Template /mnt/esp/EFI/Microsoft/Boot/BCD` puis patch hivex de `{bootmgr}` (device→WINESP, default, displayorder) et de l'OS loader (device/osdevice→partition WINDOWS via blob GPT `PARTUUID`+`DISK_GUID`, `path=\Windows\system32\winload.efi`, `systemroot=\Windows`). Le **blob binaire « device »** est **la** surface à auditer.
|
||||||
|
- **4c — repli automatique `fws-windows-bcdfix`** (recommandé pour la 1re validation) : stager la WinPE de l'ISO sur la WINESP + un `autounattend.xml` racine à **passe windowsPE uniquement** faisant `bcdboot W:\Windows /s S: /f UEFI` puis `wpeutil reboot`, **W:/S: épinglés par `diskpart` (label), aucune `DiskConfiguration`**. Coût : 1 reboot, 100 % automatique. **Basculer sur 4c si la vérif du BCD rejette la voie 4b.**
|
||||||
|
|
||||||
|
**Entrée UEFI (transactionnel)** — **ne créer qu'après vérif complète** et **placer explicitement en DERNIER** dans `BootOrder` (ne jamais laisser le tout 1er boot post-install partir sur Windows) :
|
||||||
|
```bash
|
||||||
|
efibootmgr -c -d "$WIN_DISK" -p 1 -L 'Windows Boot Manager' -l '\EFI\Microsoft\Boot\bootmgfw.efi'
|
||||||
|
# relire le Boot####, réasserter BootOrder avec FWS en tête, Windows en queue
|
||||||
|
```
|
||||||
|
Back-fill : `sed -i 's/^FWS_WINDOWS_BOOTNUM=.*/FWS_WINDOWS_BOOTNUM="'$WBN'"/' $SYSROOT/etc/fws/gameboot.conf`.
|
||||||
|
|
||||||
|
**Vérification post-déploiement (avant de déclarer succès)** : `ntoskrnl.exe` + `winload.efi` présents ; `bootmgfw.efi` + BCD présents et relus non vides ; `PreventDeviceEncryption==1` ; entrée UEFI relue. Échec → repli 4c **ou** `echo DEPLOY_FAILED > $SYSROOT/var/lib/fws/windows-deploy.status` **sans** laisser d'entrée Windows piégeuse. FWS reste toujours seul bootable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7) Ordre global de bout en bout
|
||||||
|
|
||||||
|
1. **`%pre`** : `clearpart` firmware-conditionnel sur le seul disque « Destination » (disque-agnostique, inchangé).
|
||||||
|
2. **Commandes de partitionnement** : blivet partitionne uniquement le disque FWS (ESP + ext4 + swap éventuel). Le disque Windows n'est jamais référencé.
|
||||||
|
3. **Copie payload LiveOS** (racine FWS montée sous `/mnt/sysroot`).
|
||||||
|
4. **nochroot early** : propage `/tmp/fws-windows` (+ `/tmp/fws-desktop`, hostname, noyau) vers la cible.
|
||||||
|
5. **`%post` chroot** : purge résidus live → `mkinitcpio` (**hook `resume` après `block`, avant `filesystems`, busybox**) → `resume=UUID=$SWAP_UUID hibernate.compressor=lz4` + `GRUB_DISABLE_OS_PROBER=true` dans `/etc/default/grub` → NVIDIA (`NVreg_PreserveVideoMemoryAllocations=1`, `NVreg_TemporaryFilePath=/var/tmp`, enable `nvidia-suspend/hibernate/resume`) → `install_grub` (**`--bootloader-id=FWS`**) → **`fws-secureboot-setup` (signe FWS + BOOTX64 + vmlinuz, `sbctl verify` clean)** → `systemctl enable fws-gameboot-bootfix.service` → `gameboot.conf`.
|
||||||
|
6. **Nouveau `%post --nochroot`** (`fws-windows-deploy`) : garde-fous fail-closed → neutralisation blivet → carve → `mkntfs -Q -f -L WINDOWS` → `wimlib-imagex apply` par PARTLABEL → 4a/4b(/4c) BCD → anti-BitLocker offline → `unattend.xml` + injection `gameboot-windows` → entrée UEFI **en queue** + back-fill `FWS_WINDOWS_BOOTNUM` → vérif → `sync; umount`.
|
||||||
|
7. **nochroot final** : exfil de log.
|
||||||
|
8. **Hors installateur (manuel, irréductible)** : visite firmware → **Setup Mode + activer Secure Boot** (`sbctl verify` déjà clean) — écran/rappel d'instructions au 1er boot FWS (motd/`fws-hello`) avec les commandes exactes.
|
||||||
|
9. **1er `fws-gameboot to-windows`** (déclenché par l'utilisateur ; **preflight refuse si SB≠enabled** → l'ordre « SB ON avant 1er Windows » s'auto-impose, PCR7 stable). Idéalement un **boot de PROVISIONING** unique (installer les jeux, **pas les lancer**, retour auto à FWS) distinct des boots de jeu (voir §8).
|
||||||
|
10. Régime établi : `fws-gameboot-bootfix` réasserte `BootOrder[0]=FWS` à chaque boot FWS.
|
||||||
|
|
||||||
|
**Contrainte d'ordre unique** : SB doit être ON **avant le 1er boot Windows** (l'apply offline ne « mesure » rien → SB peut être activé après l'apply mais avant ce 1er boot).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8) Risques & mitigations (critique intégrée)
|
||||||
|
|
||||||
|
**Bloquants**
|
||||||
|
- **Perte de données par défaut (spoke `set_active(0)`)** → §4(a) : combo sans présélection, indicateur d'occupation, case « effacer quand même » + modèle exact, `completed` refuse tant que rien de choisi.
|
||||||
|
- **Garde-fous fail-open** (resolver muet → « » ≠ devnode toujours vrai) → §5 : **fail-closed**, abort sur valeur vide/non-bloc, asserter `/mnt/sysroot` monté, comparer par identifiant stable.
|
||||||
|
- **Chaîne SB incohérente → FWS non bootable** (`\EFI\FWS`+`BOOTX64` non signés) → Phase 1 : unifier `--bootloader-id=FWS`, signer les 3 chemins, `sbctl verify` clean avant d'instruire l'activation.
|
||||||
|
- **Contradiction carve `%pre` vs GUI** → source unique = `%post --nochroot` ; commentaire fort interdisant toute op disque dépendante du GUI en `%pre`.
|
||||||
|
|
||||||
|
**Majeurs**
|
||||||
|
- **BCD Linux non validé** (`0xc000000e`) → voie hivex **pas primaire** tant que non validée sur 25H2 réel ; **repli WinPE-bcdboot par défaut** (W:/S: épinglés par label) ; vérif post-déploiement obligatoire, transactionnel.
|
||||||
|
- **Lockout BitLocker** → écriture ControlSet001 vérifiée par relecture + doublons `specialize` + `manage-bde -off` au 1er boot ; **ne jamais activer BitLocker** ; conditionner le 1er boot à « Device Encryption OFF ».
|
||||||
|
- **1er boot post-install part sur Windows** → entrée UEFI créée **en dernier** de `BootOrder`, NVRAM relue avant `umount`, pas de dépendance au seul bootfix.
|
||||||
|
- **Orchestration 1er boot ratée** (OOBE + download Riot + `FWS-Play-OnLogon` qui boucle sur un jeu pas installé, jamais de retour FWS) → **boot de provisioning séparé** (installer, pas lancer, retour auto FWS) ; `fws-play` tolérant à « jeu pas encore installé ».
|
||||||
|
- **Promesse « 100 % automatique » fausse** (activation SB = visite firmware manuelle) → corriger doc/UI ; écran guidé avec commandes/étapes firmware exactes ; détecter Setup Mode.
|
||||||
|
- **Variance ISO** (Home/N/localisée, pas de clé, tiny11) → énumérer les éditions dans le spoke (`edition=`), résoudre par **nom** via `wimlib-imagex info` et **échouer proprement** si absent ; Pro par défaut (deferral Windows Update = mitigation brick dbx/SBAT) ; gérer « pas de clé » ; refuser les ISO non standard avec message actionnable.
|
||||||
|
|
||||||
|
**Mineurs**
|
||||||
|
- **PARTLABEL `WINDOWS` ambigu** (autre install antérieure) → asserter résolution unique sur `$WIN_DISK` ou label unique `FWSWIN`.
|
||||||
|
- **Transactionnel incomplet** (`--zap-all` avant apply) → tout valider **avant** le 1er `sgdisk` (ISO montée, édition résolue, espace re-vérifié) ; entrée UEFI seulement après vérif.
|
||||||
|
- **WinRE non installé** par l'apply seul → Windows boote sans, à documenter (gaming minimal).
|
||||||
|
- **`mkntfs -Q`** parfois mal digéré par 25H2 → valider le NTFS produit, repli full format sans `-Q`.
|
||||||
|
- **ISO retirée pendant le `%post`** → statut `DEPLOY_DEFERRED` + relance depuis le système installé ; documenter « laisser la clé branchée ».
|
||||||
|
- **efivars réordonnées par le firmware** → filet `BootOrder[0]=FWS` + chemin de secours `\EFI\BOOT\BOOTX64.EFI` (signé, cf. Phase 1) ; sémantique `BootNext` à valider sur matériel réel.
|
||||||
|
- **Resume GPU RTX 5090 (Blackwell)** non validé → tester session chargée **avant** de se fier au bouton « Jouer » ; fallback reboot déjà câblé.
|
||||||
|
- **Couplage version** `python3.14` / `SystemCategory` / anaconda 45.8 → re-vérifier à chaque bump (comme l'en-tête `fws_recap.py`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9) Décisions ouvertes
|
||||||
|
|
||||||
|
1. **BCD primaire** : hivex `fws-bcd-write` vs WinPE-bcdboot par défaut. **Recommandation** : WinPE-bcdboot comme chemin **par défaut** pour la 1re validation matériel ; hivex promu primaire seulement après succès reproductible sur 25H2 réel.
|
||||||
|
2. **Partition MSR** (0c01, 16 MiB) : recommandée pour un GPT Windows propre, **non requise** pour booter. Trancher (carvée par prudence dans le plan) après validation.
|
||||||
|
3. **Emplacement du swap** : sur le disque Windows (retenu, déterministe depuis le kickstart) vs sur le disque FWS (plus propre conceptuellement, mais exige de capter le disque FWS dans le spoke). Rebasculer si le spoke est étendu.
|
||||||
|
4. **Mode SHRINK** : livrer ou non le chemin non destructif (conserver les données du disque cible). Enhancement fragile — hors v1.
|
||||||
|
5. **`edition=` dans le spoke** : énumérer les éditions à l'écran (choix explicite) vs Pro par défaut silencieux. Recommandation : énumérer si l'ISO le permet.
|
||||||
|
6. **ProductKey / activation** : suppose un droit numérique de l'utilisateur ; définir le comportement si l'édition ciblée n'est pas activable.
|
||||||
|
7. **Label NTFS** : `WINDOWS` vs `FWSWIN` unique (anti-collision multi-NVMe).
|
||||||
|
8. **Boot de provisioning vs boot de jeu** : formaliser un premier passage Windows « installer et revenir » distinct des lancements de jeu (impacte `FirstBoot-FwsGameboot.ps1` et `fws-play`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Fichiers à créer** : `usr/local/bin/fws-windows-deploy`, `usr/local/lib/fws/fws-bcd-write`, `usr/local/bin/fws-windows-bcdfix`, `usr/local/share/fws/gameboot-windows/unattend.offline.xml.tmpl` (tous sous `configs/releng/airootfs/`).
|
||||||
|
**Fichiers à modifier** : `configs/{releng,baseline}/packages.x86_64` (+wimlib, hivex, sbctl, sbsigntools, binutils, mokutil, nvidia, nvidia-utils) ; `configs/releng/airootfs/usr/share/anaconda/interactive-defaults.ks` (propagation + bloc chroot hibernation/SB + nouveau `%post --nochroot`) ; `configs/releng/airootfs/usr/local/bin/fws-secureboot-setup` (unifier `--bootloader-id=FWS`, signer les 3 chemins) ; `.../spokes/fws_windows.py` + `.glade` (anti-présélection + confirm_erase) ; `.../spokes/fws_recap.py` (ligne + contrôle croisé).
|
||||||
|
**Réutilisés tels quels** : `gameboot.conf`, `fws-gameboot(-bootfix)(.service)`, `95-fws-secureboot-sign.hook`, `gameboot-windows/*` (`FirstBoot`/`Install-FwsGameboot`/`Install-Games`/`games.json`/`installers.json`), `docs/hibernate-swap-dualboot.md` (référence de conception §4/§6/§7).
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Maintainer: FWS
|
||||||
|
pkgname=fws-dnd
|
||||||
|
pkgver=2.0.0
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="Gestionnaire D&D 5e pour FWS (GTK4) : dés, créateur de personnage (règles officielles), grimoire, sessions, tracker de combat"
|
||||||
|
arch=('any')
|
||||||
|
url="https://fws.local/fws-dnd"
|
||||||
|
license=('MIT')
|
||||||
|
depends=('python' 'python-gobject' 'gtk4')
|
||||||
|
source=("${pkgname}-${pkgver}.tar.gz")
|
||||||
|
sha256sums=('SKIP')
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||||
|
|
||||||
|
# Module Python (application GTK4)
|
||||||
|
install -d "${pkgdir}/usr/share/${pkgname}"
|
||||||
|
cp -r gui/fwsdnd "${pkgdir}/usr/share/${pkgname}/"
|
||||||
|
# pas de bytecode pré-généré dans le paquet
|
||||||
|
find "${pkgdir}/usr/share/${pkgname}" -name '__pycache__' -type d -exec rm -rf {} +
|
||||||
|
|
||||||
|
# Exécutable
|
||||||
|
install -Dm755 bin/fws-dnd "${pkgdir}/usr/bin/fws-dnd"
|
||||||
|
|
||||||
|
# Intégration bureau
|
||||||
|
install -Dm644 fws-dnd.desktop "${pkgdir}/usr/share/applications/fws-dnd.desktop"
|
||||||
|
install -Dm644 fws-dnd.svg "${pkgdir}/usr/share/icons/hicolor/scalable/apps/fws-dnd.svg"
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
install -Dm644 README.md "${pkgdir}/usr/share/doc/${pkgname}/README.md"
|
||||||
|
}
|
||||||
Binary file not shown.
Executable
+92
@@ -0,0 +1,92 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# test-vm.sh — VM de test pour gameboot + dual-boot Windows, SANS RISQUE.
|
||||||
|
#
|
||||||
|
# Reproduit la machine cible : UEFI (OVMF, avec vars persistantes pour les
|
||||||
|
# entrées de boot), TPM 2.0 (swtpm), 3 disques NVMe virtuels (vide FWS / faux
|
||||||
|
# disque « jeux » / cible Windows). Disques qcow2 CREUX → coût disque réel = ce
|
||||||
|
# qui est écrit, pas la taille nominale. Aucune donnée réelle touchée.
|
||||||
|
#
|
||||||
|
# CE QUE ÇA VALIDE : build/boot de l'ISO, spoke « Dual-boot Windows », contrôle
|
||||||
|
# de conflit du récap, garde-fous fail-closed (choisir le disque FWS comme
|
||||||
|
# cible Windows → doit ABORTER), carve, wimlib apply, injection unattend, BCD
|
||||||
|
# (voie WinPE), OOBE Windows + auto-login + FirstBoot, l'aller-retour hibernate.
|
||||||
|
# CE QUE ÇA NE VALIDE PAS (→ vrai matériel) : resume du GPU RTX 5090, activation
|
||||||
|
# Secure Boot au firmware réel, et surtout VANGUARD (qui refuse justement la VM).
|
||||||
|
#
|
||||||
|
# Usage :
|
||||||
|
# scripts/test-vm.sh [ISO_FWS] [ISO_WINDOWS]
|
||||||
|
# ISO_FWS : défaut = la plus récente dans out/ (REBUILD d'abord : ./build-offi.sh !)
|
||||||
|
# ISO_WINDOWS : optionnelle, montée en 2e CD pour tester le déploiement Windows.
|
||||||
|
#
|
||||||
|
# Prérequis (Arch) : sudo pacman -S --needed qemu-full edk2-ovmf swtpm
|
||||||
|
# ============================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
WORK="$REPO/out/test-vm"
|
||||||
|
mkdir -p "$WORK"
|
||||||
|
|
||||||
|
# --- ISO FWS (rebuild indispensable pour avoir la feature gameboot) ---------
|
||||||
|
ISO_FWS="${1:-}"
|
||||||
|
if [ -z "$ISO_FWS" ]; then
|
||||||
|
ISO_FWS="$(find "$REPO/out" -name '*.iso' -not -path '*/test-vm/*' -printf '%T@ %p\n' 2>/dev/null \
|
||||||
|
| sort -rn | head -1 | cut -d' ' -f2-)"
|
||||||
|
fi
|
||||||
|
[ -n "$ISO_FWS" ] && [ -f "$ISO_FWS" ] || { echo "ISO FWS introuvable — lance d'abord ./build-offi.sh"; exit 1; }
|
||||||
|
echo "==> ISO FWS : $ISO_FWS"
|
||||||
|
echo " (⚠ vérifie que c'est un build RÉCENT, avec gameboot — pas une vieille ISO de out/)"
|
||||||
|
ISO_WIN="${2:-}"
|
||||||
|
|
||||||
|
# --- Outils ---
|
||||||
|
command -v qemu-system-x86_64 >/dev/null || { echo "installe : sudo pacman -S --needed qemu-full edk2-ovmf swtpm"; exit 1; }
|
||||||
|
|
||||||
|
# --- OVMF (firmware UEFI) : code RO + vars RW par-VM ------------------------
|
||||||
|
OVMF_CODE=""; OVMF_VARS_SRC=""
|
||||||
|
for c in /usr/share/edk2/x64/OVMF_CODE.4m.fd /usr/share/edk2/x64/OVMF_CODE.fd \
|
||||||
|
/usr/share/OVMF/OVMF_CODE.fd /usr/share/edk2-ovmf/x64/OVMF_CODE.fd; do
|
||||||
|
[ -f "$c" ] && { OVMF_CODE="$c"; break; }; done
|
||||||
|
for v in /usr/share/edk2/x64/OVMF_VARS.4m.fd /usr/share/edk2/x64/OVMF_VARS.fd \
|
||||||
|
/usr/share/OVMF/OVMF_VARS.fd /usr/share/edk2-ovmf/x64/OVMF_VARS.fd; do
|
||||||
|
[ -f "$v" ] && { OVMF_VARS_SRC="$v"; break; }; done
|
||||||
|
[ -n "$OVMF_CODE" ] && [ -n "$OVMF_VARS_SRC" ] || { echo "OVMF introuvable — sudo pacman -S edk2-ovmf"; exit 1; }
|
||||||
|
[ -f "$WORK/OVMF_VARS.fd" ] || cp "$OVMF_VARS_SRC" "$WORK/OVMF_VARS.fd"
|
||||||
|
|
||||||
|
# --- Disques NVMe virtuels CREUX (recréés si absents) -----------------------
|
||||||
|
# nvme0 = FWS (install) · nvme1 = faux « jeux » (doit rester intact) ·
|
||||||
|
# nvme2 = cible Windows (assez grand : swap 96 Gio + NTFS + ESP).
|
||||||
|
mk(){ [ -f "$WORK/$1" ] || qemu-img create -f qcow2 "$WORK/$1" "$2" >/dev/null; }
|
||||||
|
mk fws.qcow2 64G
|
||||||
|
mk games.qcow2 32G
|
||||||
|
mk win.qcow2 300G
|
||||||
|
echo "==> Disques : $WORK/{fws,games,win}.qcow2 (creux)"
|
||||||
|
|
||||||
|
# --- TPM 2.0 (swtpm) --------------------------------------------------------
|
||||||
|
TPMDIR="$WORK/tpm"; mkdir -p "$TPMDIR"
|
||||||
|
swtpm socket --tpmstate dir="$TPMDIR" --ctrl type=unixio,path="$TPMDIR/sock" \
|
||||||
|
--tpm2 --terminate &
|
||||||
|
SWTPM_PID=$!
|
||||||
|
trap 'kill "$SWTPM_PID" 2>/dev/null || true' EXIT
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# --- CD Windows optionnel ---------------------------------------------------
|
||||||
|
WINCD=()
|
||||||
|
if [ -n "$ISO_WIN" ] && [ -f "$ISO_WIN" ]; then
|
||||||
|
WINCD=(-drive file="$ISO_WIN",media=cdrom,readonly=on)
|
||||||
|
echo "==> ISO Windows montée en 2e CD (dans le live : la monter, puis la pointer dans le spoke)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Lancement de la VM (UEFI + TPM2 + 3 NVMe). Ferme la fenêtre pour quitter."
|
||||||
|
exec qemu-system-x86_64 \
|
||||||
|
-machine q35,smm=on,accel=kvm -cpu host -smp 4 -m 8G \
|
||||||
|
-drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
|
||||||
|
-drive if=pflash,format=raw,file="$WORK/OVMF_VARS.fd" \
|
||||||
|
-chardev socket,id=chrtpm,path="$TPMDIR/sock" \
|
||||||
|
-tpmdev emulator,id=tpm0,chardev=chrtpm -device tpm-crb,tpmdev=tpm0 \
|
||||||
|
-drive file="$WORK/fws.qcow2",if=none,id=d0 -device nvme,drive=d0,serial=FWS0 \
|
||||||
|
-drive file="$WORK/games.qcow2",if=none,id=d1 -device nvme,drive=d1,serial=GAMES1 \
|
||||||
|
-drive file="$WORK/win.qcow2",if=none,id=d2 -device nvme,drive=d2,serial=WIN2 \
|
||||||
|
-cdrom "$ISO_FWS" "${WINCD[@]}" \
|
||||||
|
-boot menu=on \
|
||||||
|
-netdev user,id=n0 -device virtio-net,netdev=n0 \
|
||||||
|
-vga virtio -display gtk,gl=on
|
||||||
+3
-2
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# FWS — Build des paquets AUR (winboat-bin) + PKGBUILD locaux (deps Anaconda)
|
# FWS — Build des paquets AUR (winboat-bin) + PKGBUILD locaux (deps Anaconda,
|
||||||
# et création du repo pacman local fws-local/.
|
# fws-dnd) et création du repo pacman local fws-local/.
|
||||||
#
|
#
|
||||||
# Rejoue-le si tu changes la liste AUR_PACKAGES ou pour
|
# Rejoue-le si tu changes la liste AUR_PACKAGES ou pour
|
||||||
# rafraîchir le repo local.
|
# rafraîchir le repo local.
|
||||||
@@ -297,6 +297,7 @@ done
|
|||||||
# --- PKGBUILD locaux FWS (ordre explicite, feuilles d'abord, anaconda dernier) ---
|
# --- PKGBUILD locaux FWS (ordre explicite, feuilles d'abord, anaconda dernier) ---
|
||||||
LOCAL_PKGBUILDS_DIR="$SCRIPT_DIR/pkgbuilds"
|
LOCAL_PKGBUILDS_DIR="$SCRIPT_DIR/pkgbuilds"
|
||||||
LOCAL_BUILD_ORDER=(
|
LOCAL_BUILD_ORDER=(
|
||||||
|
fws-dnd
|
||||||
pykickstart python-productmd python-requests-file python-simpleline
|
pykickstart python-productmd python-requests-file python-simpleline
|
||||||
python-crypt_r python-iso639 python-langtable python-meh python-blivet
|
python-crypt_r python-iso639 python-langtable python-meh python-blivet
|
||||||
anaconda-widgets anaconda
|
anaconda-widgets anaconda
|
||||||
|
|||||||
Reference in New Issue
Block a user