58 Commits

Author SHA1 Message Date
nocode e313fc4dde docs(drivers/intel-vmd): add intel vmd/rst driver installation guide
Add README documentation for Intel VMD (Virtual Machine Device) and RST (Rapid Storage Technology) driver installation. This guide explains:

- Where to place Intel VMD driver files (iaStorVD.inf, .sys, .cat)
- Download location and extraction instructions
- How FWS automatically injects drivers into Windows deployments
- Notes on Windows 11 25H2 compatibility and AMD systems

This enables support for machines with Intel VMD/RST enabled in BIOS.
2026-07-09 15:58:00 +02:00
nocode 384a45f70e feat(fws-windows-deploy): add intel vmd driver injection support
Add support for injecting Intel VMD drivers during Windows deployment. The script now:

- Parses the intel_vmd configuration option from the state file
- Checks for Intel VMD drivers in /usr/local/share/fws/drivers/intel-vmd/
- Injects drivers via WinPE (dism) when requested and available
- Falls back to Windows inbox VMD driver with a warning if no driver found
- Forces WinPE path when VMD driver injection is needed
- Passes driver path to fws-windows-bcdfix for processing
2026-07-09 15:57:52 +02:00
nocode 89e1321ab7 feat(fws-windows-bcdfix): add Intel VMD driver injection support for WinPE
Add support for injecting Intel VMD drivers into WinPE environment via a new DRV parameter. This enables proper NVMe device detection during Windows PE boot and installation.

Changes:
- Accept optional 5th parameter (DRV) for driver directory path
- Embed driver files into WinPE boot image via drvload command
- Register driver in target Windows installation via DISM
- Copy driver directory into boot.wim fwsdrv folder for accessibility
2026-07-09 15:57:45 +02:00
nocode e2dd1abc18 feat(fws_windows): add intel vmd/rst support checkbox to windows deployment ui
Add a new GtkCheckButton for Intel VMD/RST support in the Anaconda Windows deployment UI. The checkbox is auto-detected and injects the iaStorVD driver into deployed Windows systems to enable boot on Intel machines with VMD/RST enabled in BIOS. The control has no effect on AMD systems. Update position index for subsequent UI elements.
2026-07-09 15:57:38 +02:00
nocode d050af8279 feat(fws_windows): add intel vmd device detection and support
Add support for detecting Intel VMD (Volume Management Device) controllers and enable/disable the VMD checkbox based on detection.

Changes:
- Add _intel_vmd_present() function to detect VMD controllers via lspci
- Initialize _vmd attribute based on controller detection
- Add VMD checkbox widget binding and state management
- Update checkbox sensitivity when enabling/disabling dual-boot
- Persist VMD state in configuration file
- Load VMD state from saved configuration
2026-07-09 15:57:33 +02:00
nocode 64763c3707 feat(fws_recap): add intel vmd/rst support indicator to windows dualboot summary
Display a notification in the Windows dualboot recap when Intel VMD/RST support is enabled, indicating that the driver has been injected during Windows boot.
2026-07-09 15:57:27 +02:00
nocode 2e2108d2b8 build(pyanaconda): update fws_windows python bytecode 2026-07-09 15:57:24 +02:00
nocode d4e479cc15 build(pyanaconda): update fws_recap pyc cache file 2026-07-09 15:56:58 +02:00
nocode d592b24858 test(vm): add test-vm.sh script for safe gameboot validation
Add test-vm.sh script for VM-based testing of gameboot and dual-boot Windows functionality.

The script sets up a QEMU VM with:
- UEFI firmware (OVMF) with persistent boot entries
- TPM 2.0 emulation (swtpm)
- 3 virtual NVMe disks (sparse qcow2 format)
- Optional Windows ISO mounting for deployment testing

Validates: ISO build/boot, dual-boot Windows spoke, conflict detection, fail-safe mechanisms, carving, wimlib apply, unattend injection, BCD configuration, Windows OOBE and FirstBoot, hibernation cycles.

Does not validate: GPU resume, real Secure Boot activation, VANGUARD (which rejects VMs).
2026-07-09 14:41:01 +02:00
nocode 9836925a37 docs(gameboot): add hardware validation checklist for gameboot dual-boot
Add comprehensive test checklist for validating gameboot Windows dual-boot functionality across 8 phases, from ISO build through hibernation, Secure Boot, Windows deployment, BCD configuration, and full gameboot cycle testing. Includes safety precautions, prerequisite checks, and recovery procedures.
2026-07-09 14:40:53 +02:00
nocode 47d59e51fe chore: add python cache file for fws_recap module 2026-07-09 14:40:46 +02:00
nocode 6a1ce74169 feat(fws_recap): add windows dual-boot configuration display section
Add _windows_dualboot_lines() function to read and parse /tmp/fws-windows configuration file. Display dual-boot status, disk allocation, ISO information, and conflict warnings when the selected Windows disk overlaps with FWS installation targets. Integrate the windows dual-boot section into the installation summary output.
2026-07-09 14:40:35 +02:00
nocode fa3b953d99 docs(windows): add comprehensive dual-boot windows installation plan
Add detailed technical specification for offline Windows 11 deployment via FWS spoke installer. Covers:

- Architecture overview: automatic offline WIM application without Windows Setup
- Security considerations: unified Secure Boot signing chain and protection against wrong-disk erasure
- Five-phase implementation roadmap: package dependencies, boot fixes, deployer tool, kickstart integration, validation
- Spoke amendments: safe defaults, disk occupation warnings, explicit confirmation for non-empty disks
- Safe partitioning with closed-fail guards against resolver failures
- Offline BCD construction via hivex with WinPE fallback
- Integration with FWS hibernation and gameboot-windows components
- Honest documentation of single irreducible manual action: firmware Setup Mode for Secure Boot activation
2026-07-08 23:58:11 +02:00
nocode d47686efd9 feat(polkit): add gameboot polkit policy for fws-gameboot execution
Add PolicyKit policy configuration that authorizes gameboot launchers (desktop entries) to execute /usr/local/bin/fws-gameboot via pkexec.

The policy allows active users (physically present) to switch to Windows without password prompt since the switch is non-destructive (FWS hibernates and restores the session on return). Remote or inactive sessions require admin authentication.
2026-07-08 23:58:03 +02:00
nocode fe9b30b745 feat(icons): add fws-gameboot scalable icon for gaming boot launcher
Add a new SVG icon for the FWS gameboot launcher application. The icon features a gradient blue background with stylized game controller handles and a play triangle, designed as a neutral generic gameboot icon without brand-specific logos.
2026-07-08 23:57:53 +02:00
nocode 7a4f36c916 feat(gameboot): add valorant gameboot desktop launcher
Add desktop application entry for launching Valorant via gameboot, which hibernates FWS and switches to bare-metal Windows. Includes French and English translations for name, description, and keywords.
2026-07-08 23:57:46 +02:00
nocode e3a0b988a5 feat(releng): add desktop entry for league of legends gameboot launcher
Add fws-play-lol.desktop file that provides a graphical launcher for switching to bare-metal Windows to play League of Legends with Vanguard anticheat support. Includes localized French and English labels and descriptions.
2026-07-08 23:57:40 +02:00
nocode 05a44af161 feat(gameboot-windows): add windows 11 offline unattend answer file template
Add unattend.offline.xml.tmpl for Windows 11 deployment via fws-windows-deploy.

This offline answer file handles specialize and oobeSystem passes after wimlib
image application. It configures:
- Computer name and locale settings
- BitLocker and OOBE registry tweaks
- Local admin gaming account with temporary auto-login
- FirstBoot PowerShell script execution

Tokens (__HOSTNAME__, __USER__, __DISPLAY__, __LOCALE__, __INPUT__,
__FIRSTBOOT__) are substituted by fws-windows-deploy via sed.
2026-07-08 23:57:28 +02:00
nocode 4bc9ccc1f7 feat(fws): add fws-bcd-write script for offline bcd generation
Add a new script that constructs a Windows BCD store offline from Linux using hivex.

The script copies the BCD-Template from Windows and patches string elements.
However, the binary device element encoding (GPT partition info) is not reliably
encoded in this implementation, so the script returns exit code 1 to trigger
fallback to the WinPE-based fws-windows-bcdfix which uses the official bcdboot tool.

Args: ESP_MOUNT WIN_MOUNT WINDOWS_PARTUUID DISK_GUID
Exit codes: 0 = BCD written safely; 1 = fallback to WinPE approach
2026-07-08 23:57:14 +02:00
nocode d1fa5143e8 feat(windows-deploy): add offline Windows 11 deployment script for dual-boot provisioning
Add fws-windows-deploy script for secure, offline Windows 11 deployment on dedicated disk.

Features:
- Fail-closed safety validation (disk, ISO, protected mounts)
- Partition carving with sgdisk (ESP, MSR, NTFS, swap)
- WIM image application via wimlib
- BCD bootloader setup with hivex fallback
- Offline BitLocker prevention
- Answer file injection and gameboot component
- FWS hibernation swap provisioning
- Comprehensive guards against accidental data loss

Script runs in %post --nochroot during ISO build, targets /mnt/sysroot.
2026-07-08 23:57:07 +02:00
nocode 39baaa67e6 feat(releng): add fws-windows-bcdfix script for windows bcd repair
Add new script to make Windows bootable by executing bcdboot in WinPE launched once via BootNext after FWS installation.

The script:
1. Stages bootable WinPE from ISO (bootmgr + BCD + boot.wim) to ESP
2. Injects automation (winpeshl.ini + fwspe.cmd) into boot.wim boot image
   - WinPE finds Windows drive and ESP partition
   - Executes bcdboot and reboots
3. Creates UEFI entry with one-shot BootNext to WinPE

Note: Not validated on real hardware. Recommended path: bcdboot writes correct BCD with one automatic WinPE reboot before returning to FWS.
2026-07-08 23:56:56 +02:00
nocode 976e24876d feat(ui): add windows gaming dual-boot installation spoke
Add fws_windows.glade UI definition for Windows 11 dual-boot configuration screen in Anaconda installer. Implements spoke interface with disk selection, partition size configuration, Windows ISO image selection, and validation feedback. Supports UEFI/TPM 2.0 requirements for kernel-anticheat game compatibility.
2026-07-08 23:56:48 +02:00
nocode ce6e9533c2 feat(gui): add dual-boot windows spoke for anaconda installer
Add new FWSWindowsSpoke to handle dual-boot Windows gaming configuration in the Anaconda GUI. This spoke allows users to:

- Enable/disable dual-boot Windows gaming mode
- Select a disk to dedicate to Windows
- Configure NTFS partition size (100-200 GiB default)
- Provide path to Windows 11 ISO

The spoke persists configuration to /tmp/fws-windows for processing by the kickstart script. It implements validation for disk selection, ISO path, and partition size constraints. Platform detection ensures the feature only appears on UEFI systems.
2026-07-08 23:56:41 +02:00
nocode 4d15dc2632 chore(pyanaconda): add python 3.14 compiled cache for fws_windows module 2026-07-08 23:56:34 +02:00
nocode e811cce88d docs(hibernate-swap-dualboot): update windows version references from 24H2 to 25H2
Update documentation to reflect Windows 11 25H2 as the current default version in three locations:
- Initial requirements section
- BitLocker risk mitigation (row 5)
- VBS/Hyper-V validation note (row 11)

These changes align the documentation with the latest Windows 11 release cycle.
2026-07-08 23:56:29 +02:00
nocode 0dc5329d90 chore(releng): add file permissions for windows deployment utilities
Add executable permissions configuration for three new Windows deployment related utilities:
- fws-windows-deploy
- fws-windows-bcdfix
- fws-bcd-write library script
2026-07-08 23:56:23 +02:00
nocode fd21293725 feat(anaconda): add secure boot setup and windows deployment support
Add support for Secure Boot preparation and Windows dual-boot deployment during Anaconda installation.

- Implement Secure Boot chain preparation using sbctl when Windows gaming dual-boot is selected
- Add Windows deployment phase via fws-windows-deploy in nochroot post-installation hook
- Include safety checks: literal file inspection to prevent code injection, fail-closed guards
- Deployment is non-blocking: failures don't prevent FWS from booting
- Add comprehensive comments explaining the Windows spoke integration and deployment flow
2026-07-08 23:56:17 +02:00
nocode e69c071969 fix(fws-secureboot-setup): update grub bootloader id to FWS and sign all firmware boot paths
Change GRUB bootloader identity from GRUB to FWS to match kickstart configuration.

Add signing of both primary (\EFI\FWS\grubx64.efi) and fallback removable
(\EFI\BOOT\BOOTX64.EFI) boot paths, as these are the paths actually loaded
by the firmware.

Extract GRUB_MODULES to a variable for reuse in both standard and removable
installation commands. Add objdump availability check before attempting SBAT
verification. Add sbat.csv existence check before reinstalling GRUB.
2026-07-08 23:56:10 +02:00
nocode 3b47ef30e7 feat(releng): add gameboot packages for windows gaming deployment
Add packages required for offline Windows gaming dual-boot deployment with Secure Boot support:
- wimlib: for applying Windows install.wim/.esd images
- hivex: for offline BCD and anti-BitLocker modifications
- sbctl/sbsigntools/mokutil: for Secure Boot management
- binutils: for objdump (.sbat) inspection

Note: nvidia driver is conditional and managed separately via fws-setup-hardware.
2026-07-08 23:56:00 +02:00
nocode e5bc082049 fix(fws-gameboot-bootfix): remove residual fws winpe boot entries
Clean up residual 'FWS WinPE' boot entries left over from Windows deployment fallback (bcdboot one-shot already consumed). These entries were cluttering the boot menu and needed to be removed during the boot fix process.
2026-07-08 23:55:48 +02:00
nocode 8d1b6bf23b fix(fws-gameboot): improve windows not found error messaging
Replace terse die message with user-friendly notification before the error.

When Windows Boot Manager entry is not found, first notify the user with
clear instructions about installing gaming Windows and checking UEFI boot
entries before terminating with the technical error message.
2026-07-08 23:55:41 +02:00
nocode 3f6fb08af6 chore(packages): add gameboot and secure boot related packages
Add wimlib, hivex, sbctl, sbsigntools, mokutil, and binutils packages required for dual-boot Windows gaming support and Secure Boot setup. These are universal tools used by fws-windows-deploy and fws-secureboot-setup scripts.
2026-07-08 23:55:21 +02:00
nocode c90e48f14f feat(gameboot-windows): add game installers configuration with valorant and lol
Add installers.json configuration file with official installation sources for Valorant and League of Legends. This file specifies download URLs from Riot's CDN and installation parameters for the game installer script.

Note: URLs are region-specific (EU default) and may require periodic verification as Riot frequently updates these resources.
2026-07-08 03:38:19 +02:00
nocode 226af87379 feat(gameboot-windows): add powershell script to install games from official sources
Add Install-Games.ps1 script that downloads and launches official game installers marked with autoinstall=true in installers.json.

The script:
- Downloads installers from official Riot servers (not redistributed by FWS)
- Supports silent installation via configurable arguments
- Handles TLS 1.2 compatibility for older .NET stacks
- Provides user feedback and error handling for download/launch failures
- Note: Valorant installation requires manual confirmation due to Vanguard kernel driver and mandatory restart
2026-07-08 03:38:12 +02:00
nocode 8e3bd2b53c feat(gameboot-windows): add FirstBoot-FwsGameboot orchestrator script
Add PowerShell script to orchestrate first-boot initialization on Windows gaming system.

The script executes on first boot only:
1. Install-FwsGameboot.ps1 for gaming account setup and auto-login
2. Install-Games.ps1 to download and install configured games from official sources

A marker file prevents re-execution on subsequent boots. Idempotent and safe to re-run.
2026-07-08 03:38:04 +02:00
nocode af7ed4e469 docs(gameboot-windows): expand README with first boot automation and game installer details
- Add FirstBoot-FwsGameboot.ps1 script for automated first boot setup
- Document Install-Games.ps1 for downloading official Riot game installers
- Add installers.json for managing Riot installer URLs
- Explain why games cannot be pre-bundled (licensing and Vanguard constraints)
- Provide two methods for triggering first boot: autounattend.xml integration and manual execution
- Clarify prerequisites: local admin account, no password, Secure Boot/TPM/VBS-HVCI
- Expand complete flow documentation with first boot step
- Add security reminders about BitLocker and physical access implications
- Reorganize sections for better clarity and structure
2026-07-08 03:37:44 +02:00
nocode a1be0e6d49 feat(gameboot-windows): add support for installers.json and Install-Games.ps1 deployment
Extend the file deployment loop to include installers.json and Install-Games.ps1 files. Add conditional path validation before copying to gracefully handle missing files instead of failing on non-existent paths.
2026-07-08 03:37:31 +02:00
nocode afe884b9e4 feat(gameboot-windows): add games configuration for fws gameboot
Add games.json configuration file with support for Riot client games (Valorant and League of Legends). This file defines launchable games with their process names and patchline information for fws-gameboot to-windows command.
2026-07-08 03:21:36 +02:00
nocode 2bee8ed359 docs(hibernate-swap-dualboot): add hibernation performance optimization tips
Add recommended GRUB parameters for faster hibernation and resume:
- hibernation.compressor=lz4 for faster compression with lower CPU overhead
- GRUB_TIMEOUT and GRUB_TIMEOUT_STYLE settings for faster boot experience

These optimizations are particularly beneficial for large swap images (60-90 GB).
2026-07-08 03:21:30 +02:00
nocode e6c0224db9 feat(gameboot-windows): implement automatic game launch with consume-once token mechanism
Replace hardcoded Valorant launch with flexible game resolution system:

- Add Get-RequestedGame() to read and consume launch.json tokens from all ESP partitions (dual-disk support)
- Add Resolve-RiotClient() to locate Riot Client via RiotClientInstalls.json canonical source instead of hardcoded paths
- Add Start-Game() to support multiple game types (riot, uri, exe) via games.json configuration
- Implement token consume-once pattern to prevent replay of stale launch requests
- Remove scheduled task management (FWS-Return-OnStart) - now handled purely via BootNext assertions
- Add re-assertion of BootNext=Windows after 25s delay to win race against ONSTART task
- Validate requested game against games.json with fallback to valorant
- Improve robustness: handle dual disks, missing tokens, unreadable tokens, and missing processes gracefully
- Update documentation to reflect new auto-launch architecture and admin requirement
2026-07-08 03:21:21 +02:00
nocode 057898524a docs(gameboot-windows): clarify workflow and add game registration documentation
Update README to reflect the complete automated game launch flow:
- Clarify that fws-play reads the requested game from launch.json on ESP
- Add games.json registration system allowing users to add custom games
- Document auto-login setup and password handling in installer
- Update workflow steps with auto-login and logon task details
- Add section explaining how to register new games
- Improve clarity on security considerations and boot fallback mechanism
- Refine status notes and technical details for better accuracy
2026-07-08 03:21:10 +02:00
nocode 11a531003e feat(gameboot-windows): add auto-login and game launch task to fws gameboot installer
Enhance Install-FwsGameboot.ps1 with automatic login and game launch capabilities:

- Add -Password parameter for optional auto-login password configuration
- Verify prerequisites: local admin account and password requirements
- Deploy games.json configuration file alongside scripts
- Implement auto-login via Windows registry (AutoAdminLogon settings)
- Add scheduled task for auto-launching fws-play at user logon
- Tighten file ACLs on deployment directory (SYSTEM/Admins full, Users read-only)
- Replace hibernation note with full reboot explanation
- Enhance documentation with parameter descriptions and prerequisite validation details
- Update user warnings to reflect auto-login kiosk security implications

Workflow: boot → auto-login → fws-play launch → game execution → return to FWS.
2026-07-08 03:21:02 +02:00
nocode 9b80effc77 feat(fws-gameboot): add game selection support for windows boot with --game option
Add ability to specify which game to launch on Windows via --game CLI argument.

- Introduce FWS_DEFAULT_GAME environment variable (defaults to 'valorant')
- Parse --game option in to-windows command to override default
- Sanitize game identifiers using lowercase and allowed chars [a-z0-9_-]
- Write atomic game token (launch.json) to ESP FAT partition for Windows to consume
- Purge stale tokens on write failure to prevent replay of old choices
- Update help text with new option and game identifier constraints
2026-07-08 03:20:50 +02:00
nocode 810a98fc34 chore(gameboot): add default game configuration option
Add FWS_DEFAULT_GAME configuration parameter to specify the default game launched when fws-gameboot to-windows is called without the --game argument. The game identifier must exist in the Windows games.json file (e.g., valorant, lol).
2026-07-08 03:20:28 +02:00
nocode 96da52f0e1 feat(releng): add pacman hook for secure boot signing after kernel updates
Add a new libalpm hook that automatically re-signs bootloader binaries with sbctl after kernel or GRUB updates. This ensures that Secure Boot remains functional after system updates that modify unsigned binaries.

The hook triggers on updates to kernel, GRUB, and EFI files, and runs sbctl sign-all to maintain signature consistency. This is idempotent and harmless if sbctl has no enrolled keys.
2026-07-08 02:39:31 +02:00
nocode beec38a899 feat(gameboot-windows): add Windows dual-boot orchestration scripts for FWS gaming
Add PowerShell scripts to manage dual-boot transitions between FWS and Windows 11 for gaming sessions with Valorant.

Includes:
- fws-return.ps1: Arms firmware boot-next (one-shot) to target OS via bcdedit
- fws-play.ps1: Orchestrates Valorant session with robust crash recovery
- Install-FwsGameboot.ps1: Deploys scripts, disables hibernation, creates ONSTART task
- README.md: Documentation on dual-boot workflow and prerequisites

Design principles:
- Minimal footprint, no Vanguard interference (no injection/kernel hooks)
- Permanent fallback: BootOrder[0]=FWS catches unmanaged reboots
- Robust locale-independent bcdedit parsing via EFI paths and GUIDs
- Crash-proof game session: ONSTART task disabled during play to prevent mid-match boot changes
2026-07-08 02:39:27 +02:00
nocode 3cb4132727 feat(secureboot): add fws-secureboot-setup script for secure boot configuration
Add a new script that prepares FWS to boot signed under Secure Boot without enabling kernel lockdown, preserving hibernation capability.

The script:
- Checks UEFI firmware availability
- Installs sbctl if needed
- Locates ESP and GRUB EFI binary
- Reinstalls GRUB with SBAT section if missing
- Creates and enrolls sbctl keys while preserving Microsoft keys
- Signs GRUB and available kernel images
- Provides instructions for manual firmware setup steps
- Enables automatic re-signing after kernel/GRUB updates via pacman hooks

This allows FWS to meet Vanguard's Secure Boot requirement without sacrificing hibernation support.
2026-07-08 02:39:17 +02:00
nocode 367233eae5 feat(gameboot): add fws-gameboot-bootfix systemd oneshot service
Add boot fix script that executes at every FWS startup to reassert BootOrder[0] = FWS.

This serves as a permanent safety net to handle cases where Windows Update or system crashes might reorder the EFI boot options, ensuring the system always boots into FWS when no explicit BootNext is set, preventing the system from becoming stuck in Windows.

The script is inactive on live systems via ConditionPathExists check.
2026-07-08 02:39:10 +02:00
nocode 3d91ef3ac5 feat(gameboot): add fws-gameboot dual-boot hibernation orchestrator script
Implement fws-gameboot, an orchestrator for seamless dual-boot between FWS and Windows
bare-metal using hibernate-swap mechanism. Enables playing kernel anticheat games
(e.g., Valorant/Vanguard) without persistent virtualization by hibernating FWS
to swap, setting BootNext to Windows UEFI one-shot, and rebooting. Upon Windows
shutdown, the kernel resumes the FWS session via resume= parameter.

Features:
- doctor/status subcommand: validates hibernation readiness (swap size, resume=
  parameter, VRAM usage, Secure Boot status)
- to-windows subcommand: hibernates FWS, unmounts NTFS filesystems, arms BootNext,
  and initiates hibernation; resumes FWS session on return from Windows
- Safety mechanisms: BootOrder[0] fallback to FWS via fws-gameboot-bootfix,
  permanent BootNext cleanup on resume, optional simple reboot fallback
- Configuration via /etc/fws/gameboot.conf (Windows boot entry number, swap margin,
  hibernation fallback mode)
- User notification via D-Bus notify-send for graphical feedback
- Comprehensive error handling and preflight validation

Requires validated hardware support for NVIDIA GPU hibernation/resume and firmware
BootNext semantics.
2026-07-08 02:39:04 +02:00
nocode bc84731290 feat(systemd): add fws-gameboot-bootfix service unit
Add a new systemd service that reasserts BootOrder=FWS and the Windows boot marker on gameboot systems. This service runs only on installed UEFI systems (not on live media) and is designed to be best-effort without delaying boot. It handles unavailable efivars gracefully by not failing the boot process.
2026-07-08 02:38:55 +02:00
nocode e5b36d6c07 feat(gameboot): add gameboot configuration file for hibernate-swap dual-boot
Add gameboot.conf configuration file for the FWS orchestrator. This file defines settings for dual-boot hibernation-swap mode including:
- Windows boot entry detection
- Hibernation preflight failure behavior (reboot or abort)
- Swap margin for hibernation image sizing

See docs/hibernate-swap-dualboot.md for details.
2026-07-08 02:38:51 +02:00
nocode 585c4691a1 docs(hibernate-swap-dualboot): clarify row 6 mitigation for systemctl hibernate hang
expand explanation of why timeout/watchdog approaches fail for S4 hangs since the process is frozen. document the actual viable mitigations: strict preflight validation, auto-recovery via reset, and BootOrder fallback. add note about testing and post-hang recovery documentation.
2026-07-08 02:38:46 +02:00
nocode 732082144a chore(releng): add file permissions for fws-gameboot and fws-secureboot-setup scripts
Add permission entries for three new executable scripts:
- fws-gameboot
- fws-gameboot-bootfix
- fws-secureboot-setup

All scripts are set with 0:0:755 permissions (owner:group:mode).
2026-07-08 02:38:30 +02:00
nocode ae8140ccff fix(anaconda): use firmware-dependent disklabel for bootable installs
Replace hardcoded msdos disklabel with firmware-aware configuration:
- UEFI systems now use GPT, allowing ESP creation for grub-install EFI
- BIOS systems continue using MBR/msdos where core.img fits in post-MBR space

This fixes the bug where forcing msdos on UEFI prevented ESP creation,
resulting in non-bootable installations. The disklabel is now dynamically
generated by a %pre script that detects firmware type and writes the
appropriate clearpart command to /tmp/fws-clearpart.ks for inclusion.

Clarifies that clearpart --all only affects GUI-selected disks, addressing
multi-disk scenarios where users may not want all disks wiped.
2026-07-08 02:03:54 +02:00
nocode f3ffcb62b8 docs: add hibernation and dual-boot setup guide for valorant with windows
Adds comprehensive documentation for FWS Hibernate-Swap Dual-Boot feature, enabling seamless switching between Arch Linux and Windows 11 bare-metal for Valorant gameplay.

Covers:
- System architecture and boot flow with UEFI BootNext
- Hardware prerequisites (RTX 5090, NVMe, TPM 2.0)
- Phased implementation roadmap from disk reconnaissance to end-to-end testing
- Secure Boot integration with sbctl while maintaining hibernation capability
- UEFI boot orchestration and failsafe mechanisms
- Installer integration for both new installations and existing systems
- GPU NVIDIA-specific hibernation handling
- Windows integration via PowerShell scripts for automatic boot target management
- Vanguard anti-cheat compatibility constraints
2026-07-08 02:03:36 +02:00
nocode 7730ac7b00 feat(build): add preflight package resolution and fws-dnd package
Add preflight package resolution step in build-offi.sh to validate all profile
packages against pacman repositories before mkarchiso execution. This prevents
'target not found' errors during ISO creation by catching missing packages early
with helpful error messages and recovery instructions.

Introduce fws-dnd package (D&D 5e manager for FWS with GTK4 interface) to the
local package build process in setup-aur.sh. Include PKGBUILD definition and
source tarball.

When package resolution fails, the build now exits with clear feedback pointing
users to run setup-aur.sh to build missing dependencies like fws-dnd.
2026-07-07 21:39:29 +02:00
nocode bee59a2fb3 docs(changelog): update changelog with new features and fixes
Add entries for:
- Machine name selection at installation
- Interactive build script with versioning and naming
- systemd-timesyncd explicit activation
- Keyboard layout derivation from full locale
- DNS in build container configuration
2026-07-07 21:07:10 +02:00
nocode 00d34dfc49 merge(Multi-Desktop)!: Add Multi DE on the installer 2026-07-07 21:04:19 +02:00
41 changed files with 3124 additions and 17 deletions
+11
View File
@@ -22,6 +22,11 @@ Added
- **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 »,
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
-------
@@ -31,6 +36,7 @@ Changed
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
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
releng profile.
- 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).
- DNS du système installé (NetworkManager + systemd-resolved), hook ``lvm2`` dans l'initramfs, GRUB de secours
``--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
services are currently pulling in ``network_online.target``.
+25
View File
@@ -353,6 +353,31 @@ echo "==> Correction des retours à la ligne Windows (dos2unix)..."
pacman -S --needed --noconfirm dos2unix
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..."
mkdir -p "$OUT_DIR"
# mkarchiso fait un « rm » interne de l'ISO cible avant de la (ré)écrire ; si
@@ -165,6 +165,40 @@ def _config_lines():
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():
config_lines, desk, method = _config_lines()
config_text = "\n".join(config_lines) \
@@ -214,6 +248,9 @@ def _build_summary():
extras = ("Steam, Wine, VR (Monado/OpenXR), winboat-bin"
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"""\
══════════════════════════════════════════════════════════
FWS — RÉCAPITULATIF AVANT INSTALLATION
@@ -226,6 +263,9 @@ DISQUES
{disk_warn}
{disk_text}
DUAL-BOOT WINDOWS (gaming — Valorant/LoL)
{win_text}
MATÉRIEL DÉTECTÉ
• Virtualisation : {virt}
• Réseau : {net}
@@ -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>
+10
View File
@@ -121,3 +121,13 @@ python-crypt_r
# Namespace GI 'OSTree-1.0' : pyanaconda/rescue.py importe le payload rpm_ostree
# (inconditionnellement, même si FWS utilise liveimg) → fourni par '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).
@@ -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": ""
}
}
@@ -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
# (répond « oui » au lieu de bloquer sur un disque
# 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.
# --disklabel=msdos : force une table MBR (et non GPT). CRUCIAL en BIOS :
# en BIOS+GPT, GRUB i386-pc exige une partition « BIOS
# Boot » (~1 Mo, type EF02) pour embarquer core.img,
# qu'Anaconda NE crée PAS avec « bootloader --disabled
# » → « this GPT partition label contains no BIOS Boot
# Partition; embedding won't be possible » →
# grub-install échoue. En MBR, core.img se loge dans
# l'espace post-MBR, aucune partition dédiée requise.
# ⚠ Ce choix vise le BIOS (cas VMware actuel). Pour l'UEFI il faudra du GPT
# (+ ESP) — à rendre dépendant du firmware (%pre) le jour où on cible l'UEFI.
# --disklabel : type de table CHOISI SELON LE FIRMWARE par le %pre
# plus bas (ne surtout PAS coder en dur) :
# • UEFI → gpt : l'ESP peut être créée → grub-install
# EFI fonctionne. Forcer « msdos » en UEFI empêchait
# l'ESP (→ install NON bootable) et réécrivait la
# table pour rien : c'est LE bug corrigé ici.
# • BIOS → msdos : core.img se loge dans l'espace
# post-MBR (pas de partition BIOS Boot EF02 requise,
# qu'Anaconda ne crée pas avec « bootloader --disabled »).
# 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.
# ⚠ VM mono-disque : « --all » efface le seul disque, c'est voulu. Sur une
# machine multi-disques il faudrait cibler avec « --drives=sda ».
# DÉFAUT est un disque net → réinstall fiable à chaque fois.
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 ------------------------------------
# Anaconda lancerait « grub2-install » / « grub2-mkconfig » (binaires Fedora,
@@ -88,6 +89,23 @@ bootloader --disabled
# 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
# 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
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 ------------------
# 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 →
@@ -592,6 +616,37 @@ ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
systemctl enable systemd-resolved.service 2>/dev/null || true
install -d /etc/NetworkManager/conf.d
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
@@ -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>
+12
View File
@@ -121,3 +121,15 @@ ostree
xterm
# Gestionnaire D&D 5e (dés, personnages, grimoire, sessions, combat).
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
+6
View File
@@ -36,5 +36,11 @@ file_permissions=(
["/usr/local/bin/fws-recovery-gui"]="0:0:755"
["/usr/local/bin/fws-powermenu"]="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"
)
+108
View File
@@ -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 56. 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.
+231
View File
@@ -0,0 +1,231 @@
# Plan de build — FWS « HibernateSwap DualBoot » (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.2arch31, 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 **oneshot UEFI** (`BootNext`) vers un **Windows 11 baremetal** 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 dualboot baremetal 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é** (à reconfirmer 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 autolockdown 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 postinstall**. Dans le firmware : vérifier IOMMU (VTd/AMDVi) **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 oneshot + GUI (§5)
### Phase 8 — Spike endtoend 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 resignature 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`, **resigner** (il régénère un binaire non signé). Vérif postactivation : `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 oneshot `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`. Beltandsuspenders : 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` (autogué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 « Dualboot 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 POSTINSTALL (machine réelle) — `fws-windows-setup`.** Shrink horsligne, ordre **loadbearing** (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 SecureBootpropre 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 uefintfs, `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 7590 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 resigné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/resignature). |
| 5 | Majeur | **BitLocker autoactivé** 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 deviceencryption 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 posthibernation. |
| 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 resignature 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/HyperV 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 horsMVP : UKI cmdline scellée, **LUKS+TPM sur le swap** (impose repartition). |
| 13 | Mineur | Risque fournisseur Vanguard (changement de politique) + overhead ~35 min/session. | Compat annoncée **besteffort**, retest après chaque bump. **Offrir un mode « reboot simple »** (sans hibernation) comme voie robuste par défaut, hibernation en confort optin → 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 ~35 min gagnées. Recommandation revue : **reboot simple robuste par défaut, hibernation optin**. À 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 crossdisque).** Quel disque l'utilisateur sacrifie ?
6. **ESP partagée 512 MiO vs ESP Windows dédiée** lors du carveout (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.
+252
View File
@@ -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.99106) est disque-agnostique (juste `clearpart` firmware-conditionnel) ; le `%post` a 3 blocs : nochroot early (114157), chroot (165613), nochroot final d'exfil de log (624628).
---
## 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.212218) pose GRUB en `--bootloader-id=FWS` (+ `--removable``\EFI\FWS\grubx64.efi` **et** `\EFI\BOOT\BOOTX64.EFI`), mais `fws-secureboot-setup` (l.3745) 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).
+31
View File
@@ -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.
+92
View File
@@ -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
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# ============================================================
# FWS — Build des paquets AUR (winboat-bin) + PKGBUILD locaux (deps Anaconda)
# et création du repo pacman local fws-local/.
# FWS — Build des paquets AUR (winboat-bin) + PKGBUILD locaux (deps Anaconda,
# fws-dnd) et création du repo pacman local fws-local/.
#
# Rejoue-le si tu changes la liste AUR_PACKAGES ou pour
# rafraîchir le repo local.
@@ -297,6 +297,7 @@ done
# --- PKGBUILD locaux FWS (ordre explicite, feuilles d'abord, anaconda dernier) ---
LOCAL_PKGBUILDS_DIR="$SCRIPT_DIR/pkgbuilds"
LOCAL_BUILD_ORDER=(
fws-dnd
pykickstart python-productmd python-requests-file python-simpleline
python-crypt_r python-iso639 python-langtable python-meh python-blivet
anaconda-widgets anaconda