From 0df730584102202655b0d240f9fae0560c93dc43 Mon Sep 17 00:00:00 2001 From: Simone Date: Mon, 8 Jun 2026 23:31:18 +0200 Subject: [PATCH] fix(deploy-win): extract ISO with 7z so no-prompt boot patch works MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit xorriso -extract reads the ISO9660 name tree, which on a Windows ISO has no Rock Ridge — names come back upper-cased/truncated, so the `efi/microsoft/boot/efisys_noprompt.bin` lookup failed ("missing from ISO") and a repack from that tree would corrupt Windows' long file names. Extract with 7z instead (reads the UDF/Joliet tree, preserving the real lower-case paths), excluding 7z's synthetic `[BOOT]` dump. Verified end-to-end on the cached 26100.32860 ISO: 7z extract → xorriso repack yields a 5 GB ISO with autounattend.xml + post-install.ps1 at the root and efisys_noprompt.bin as the UEFI El Torito image (no boot prompt). Also: cap the Joliet volume id at 16 chars (silence xorriso warning), add a 7z preflight check, and add p7zip-full to setup-host-linux.sh. --- setup-host-linux.sh | 3 ++- src/ci_orchestrator/commands/template.py | 1 + src/ci_orchestrator/winimage/iso_patch.py | 23 ++++++++++++++++++++--- tests/python/test_winimage.py | 9 ++++++--- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/setup-host-linux.sh b/setup-host-linux.sh index 151f86c..db5a713 100755 --- a/setup-host-linux.sh +++ b/setup-host-linux.sh @@ -188,7 +188,8 @@ fi # xorriso/genisoimage — repack ISO bootable / build seed ISO (deploy-linux) # cabextract — estrazione pacchetti .cab durante la conversione UUP # chntpw — richiesto dal converter UUP (uup_download_linux.sh) -for pkg in aria2 wimtools xorriso genisoimage cabextract chntpw; do +# p7zip-full — 7z: estrae l'ISO Windows (UDF/Joliet) per il repack patch +for pkg in aria2 wimtools xorriso genisoimage cabextract chntpw p7zip-full; do if ! dpkg -s "$pkg" &>/dev/null; then echo " Installazione pacchetto $pkg..." apt-get install -y "$pkg" -qq diff --git a/src/ci_orchestrator/commands/template.py b/src/ci_orchestrator/commands/template.py index c02cc3d..a7152ac 100644 --- a/src/ci_orchestrator/commands/template.py +++ b/src/ci_orchestrator/commands/template.py @@ -2174,6 +2174,7 @@ def template_deploy_win( (f"vmrun found: {vmrun}", bool(shutil.which(vmrun) or Path(vmrun).is_file())), (f"vmware-vdiskmanager found: {vdisk_mgr}", Path(vdisk_mgr).is_file()), ("xorriso in PATH", shutil.which("xorriso") is not None), + ("7z in PATH", any(shutil.which(n) for n in ("7z", "7za", "7zr"))), (f"VMware Tools ISO: {tools_iso}", tools_iso.is_file()), ] for label, ok in checks: diff --git a/src/ci_orchestrator/winimage/iso_patch.py b/src/ci_orchestrator/winimage/iso_patch.py index f065d7a..1d5a490 100644 --- a/src/ci_orchestrator/winimage/iso_patch.py +++ b/src/ci_orchestrator/winimage/iso_patch.py @@ -30,6 +30,14 @@ def _require_xorriso() -> str: return exe +def _require_7z() -> str: + for name in ("7z", "7za", "7zr"): + exe = shutil.which(name) + if exe is not None: + return exe + raise click.ClickException("7z not found. Install p7zip-full.") + + def _read_volid(base_iso: Path) -> str: """Read the ISO volume id (label); fall back to a sane default.""" label = "" @@ -42,7 +50,8 @@ def _read_volid(base_iso: Path) -> str: check=False, ) label = proc.stdout.strip() - return (label or "WINSERVER")[:32] + # Joliet volume ids are capped at 16 chars; longer ids make xorriso warn. + return (label or "WINSERVER")[:16] def _check_free_space(base_iso: Path, work_parent: Path) -> None: @@ -66,6 +75,7 @@ def patch_iso( ) -> Path: """Inject answer files + no-prompt boot into ``base_iso`` → ``out_iso``.""" xorriso = _require_xorriso() + sevenz = _require_7z() if not base_iso.is_file(): raise click.ClickException(f"base ISO not found: {base_iso}") @@ -79,12 +89,19 @@ def patch_iso( tree.mkdir(parents=True) try: + # Extract with 7z (reads the UDF/Joliet tree) so the long, lower-case + # Windows paths survive. xorriso's -extract uses the ISO9660 name tree + # (upper-case / truncated, no Rock Ridge on Windows ISOs), which both + # breaks the efisys_noprompt.bin lookup and would corrupt Windows file + # names in the repacked image. ``[BOOT]`` is 7z's synthetic El Torito + # dump — excluded so it does not pollute the tree. click.echo(f" [iso] extracting base ISO: {base_iso.name}") subprocess.run( - [xorriso, "-osirrox", "on", "-indev", str(base_iso), "-extract", "/", str(tree)], + [sevenz, "x", str(base_iso), f"-o{tree}", "-y", "-bd", "-x![BOOT]"], check=True, + stdout=subprocess.DEVNULL, ) - # Extracted files are read-only; make the tree writable so we can add files. + # Extracted files may be read-only; make the tree writable so we can add files. subprocess.run(["chmod", "-R", "u+w", str(tree)], check=True) efi_boot = tree / _EFI_BOOT_NOPROMPT diff --git a/tests/python/test_winimage.py b/tests/python/test_winimage.py index 79b0588..fd53de7 100644 --- a/tests/python/test_winimage.py +++ b/tests/python/test_winimage.py @@ -199,13 +199,15 @@ def test_patch_iso_happy(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Non base.write_bytes(b"x" * 1000) out = tmp_path / "out.iso" monkeypatch.setattr(ip, "_require_xorriso", lambda: "xorriso") + monkeypatch.setattr(ip, "_require_7z", lambda: "7z") monkeypatch.setattr(ip, "_read_volid", lambda _p: "WINSERVER") monkeypatch.setattr(ip, "_check_free_space", lambda *_a: None) def fake_run(argv: list[str], **_kw: Any) -> subprocess.CompletedProcess[str]: - # simulate xorriso -extract creating the efi boot file in the tree - if "-extract" in argv: - tree = Path(argv[-1]) + # simulate 7z extraction creating the efi boot file in the tree + if argv[0] == "7z" and "x" in argv: + outflag = next(a for a in argv if a.startswith("-o")) + tree = Path(outflag[2:]) boot = tree / ip._EFI_BOOT_NOPROMPT boot.parent.mkdir(parents=True, exist_ok=True) boot.write_bytes(b"boot") @@ -225,6 +227,7 @@ def test_patch_iso_missing_efi_boot(monkeypatch: pytest.MonkeyPatch, tmp_path: P base = tmp_path / "base.iso" base.write_bytes(b"x" * 1000) monkeypatch.setattr(ip, "_require_xorriso", lambda: "xorriso") + monkeypatch.setattr(ip, "_require_7z", lambda: "7z") monkeypatch.setattr(ip, "_check_free_space", lambda *_a: None) monkeypatch.setattr( ip.subprocess, "run",