fix(deploy-win): don't re-patch an already-patched ISO

deploy-win patched its input into vm_dir/install-noprompt.iso and only its
own sidecar cache could skip the repack — so feeding an already-patched ISO
(e.g. from `template patch-win-iso`, or via --win-iso) triggered a wasteful
second extract+repack that double-injected the answer files.

deploy-win now detects a patched input (autounattend.xml present at the ISO
root via `iso_patch.iso_has_root_file`) and uses it as-is, skipping Step 4.
Raw ISOs are still patched once and cached as before.
This commit is contained in:
2026-06-09 20:15:30 +02:00
parent 3680f07ee3
commit 6a57c1d815
4 changed files with 71 additions and 16 deletions
+21 -16
View File
@@ -2256,24 +2256,29 @@ def template_deploy_win(
# ── Step 4: patch ISO (inject answer files + no-prompt boot) ───────────────
_deploy_step("Step 4: patch ISO (inject autounattend + no-prompt boot)", 4, start_from_step)
if start_from_step <= 4:
key = _patched_iso_inputs_key(base_iso, autounattend_xml, post_install_ps1)
sidecar = patched_iso.with_suffix(patched_iso.suffix + ".inputs")
cached = (
patched_iso.is_file()
and sidecar.is_file()
and sidecar.read_text(encoding="utf-8").strip() == key
)
if cached:
click.echo(f" [OK] patched ISO up to date — skipping repack: {patched_iso}")
if win_iso_patch.iso_has_root_file(base_iso, "autounattend.xml"):
# Already patched (e.g. produced by `template patch-win-iso`) — use as-is.
click.echo(f" [OK] ISO already patched (autounattend.xml present) — using as-is: {base_iso}")
patched_iso = base_iso
else:
win_iso_patch.patch_iso(
base_iso,
autounattend_xml=autounattend_xml,
post_install_ps1=post_install_ps1,
out_iso=patched_iso,
work_dir=vm_dir,
key = _patched_iso_inputs_key(base_iso, autounattend_xml, post_install_ps1)
sidecar = patched_iso.with_suffix(patched_iso.suffix + ".inputs")
cached = (
patched_iso.is_file()
and sidecar.is_file()
and sidecar.read_text(encoding="utf-8").strip() == key
)
sidecar.write_text(key, encoding="utf-8")
if cached:
click.echo(f" [OK] patched ISO up to date — skipping repack: {patched_iso}")
else:
win_iso_patch.patch_iso(
base_iso,
autounattend_xml=autounattend_xml,
post_install_ps1=post_install_ps1,
out_iso=patched_iso,
work_dir=vm_dir,
)
sidecar.write_text(key, encoding="utf-8")
# ── Step 5: create VMDK ────────────────────────────────────────────────────
_deploy_step("Step 5: create VMDK", 5, start_from_step)
+12
View File
@@ -38,6 +38,18 @@ def _require_7z() -> str:
raise click.ClickException("7z not found. Install p7zip-full.")
def iso_has_root_file(iso: Path, name: str) -> bool:
"""True if ``name`` exists at the root of ``iso`` (used to detect a patched ISO)."""
sevenz = _require_7z()
proc = subprocess.run(
[sevenz, "l", str(iso), f"-i!{name}"],
capture_output=True,
text=True,
check=False,
)
return proc.returncode == 0 and name.lower() in proc.stdout.lower()
def _read_volid(base_iso: Path) -> str:
"""Read the ISO volume id (label); fall back to a sane default."""
label = ""
+24
View File
@@ -194,6 +194,30 @@ def test_deploy_win_skips_patch_when_cached(
assert "skipping repack" in result.output
def test_deploy_win_uses_already_patched_iso(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
root = tmp_path
(root / "iso").mkdir(parents=True)
(root / "iso" / "VMware-tools-windows.iso").write_bytes(b"tools")
base = root / "iso" / "ws.patched.iso"
base.write_bytes(b"win")
_patch_deploy(monkeypatch, root)
# Pretend the input ISO already carries autounattend.xml at its root.
monkeypatch.setattr(tmpl_mod.win_iso_patch, "iso_has_root_file", lambda *_a: True)
def boom(*_a: Any, **_k: Any) -> Path:
raise AssertionError("patch_iso must not run for an already-patched ISO")
monkeypatch.setattr(tmpl_mod.win_iso_patch, "patch_iso", boom)
result = CliRunner().invoke(
cli, ["template", "deploy-win", "--win-iso", str(base), "--nogui"]
)
assert result.exit_code == 0, result.output
assert "already patched" in result.output # and boom (patch_iso) was never raised
def test_deploy_win_iso_url_downloads(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
root = tmp_path
(root / "iso").mkdir(parents=True)
+14
View File
@@ -127,6 +127,20 @@ def test_patch_iso_happy(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Non
assert result == out and out.is_file()
def test_iso_has_root_file(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setattr(ip, "_require_7z", lambda: "7z")
iso = tmp_path / "x.iso"
iso.write_bytes(b"x")
def fake_run(argv: list[str], **_k: Any) -> subprocess.CompletedProcess[str]:
out = "2026 .... 6813 autounattend.xml\n" if "-i!autounattend.xml" in argv else ""
return subprocess.CompletedProcess(argv, 0, out, "")
monkeypatch.setattr(ip.subprocess, "run", fake_run)
assert ip.iso_has_root_file(iso, "autounattend.xml") is True
assert ip.iso_has_root_file(iso, "missing.xml") is False
def test_patch_iso_missing_efi_boot(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
base = tmp_path / "base.iso"
base.write_bytes(b"x" * 1000)