91e75e620b
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
507 lines
20 KiB
Python
507 lines
20 KiB
Python
"""Shared Windows unattended install helpers.
|
|
|
|
Used by both the QEMU builder and Hyper-V runtime to create Windows images
|
|
from scratch via Autounattend.xml + CUA server setup script.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import shutil
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ── Download helpers ────────────────────────────────────────────────────────
|
|
|
|
|
|
def download_file(url: str, dest: Path, description: str = "") -> Path:
|
|
"""Download a file with progress, skipping if it already exists."""
|
|
if dest.exists():
|
|
logger.info(f"Already downloaded: {dest}")
|
|
return dest
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
logger.info(f"Downloading {description or url} → {dest}")
|
|
import urllib.request
|
|
|
|
urllib.request.urlretrieve(url, str(dest))
|
|
logger.info(f"Downloaded {dest.stat().st_size / 1024 / 1024:.0f} MB")
|
|
return dest
|
|
|
|
|
|
def _resolve_server_eval_url(
|
|
server_version: str, culture: str = "en-us", country: str = "US"
|
|
) -> str:
|
|
"""Resolve Windows Server evaluation ISO download URL from Microsoft's eval center.
|
|
|
|
Adapted from quickemu/quickget's download_windows_server() which is itself
|
|
adapted from the Mido project (https://github.com/ElliotKillick/Mido).
|
|
|
|
Args:
|
|
server_version: e.g. "windows-server-2022", "windows-server-2025"
|
|
culture: Language culture code, e.g. "en-us"
|
|
country: Country code, e.g. "US"
|
|
|
|
Returns:
|
|
Direct download URL for the ISO.
|
|
"""
|
|
import re
|
|
import urllib.request
|
|
|
|
eval_url = f"https://www.microsoft.com/en-us/evalcenter/download-{server_version}"
|
|
logger.info(f"Parsing Microsoft Eval Center: {eval_url}")
|
|
|
|
headers = {
|
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
|
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
"Accept-Language": "en-US,en;q=0.5",
|
|
}
|
|
req = urllib.request.Request(eval_url, headers=headers)
|
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
html = resp.read().decode("utf-8", errors="replace")
|
|
|
|
# Extract go.microsoft.com/fwlink links with the matching culture
|
|
pattern = rf"https://go\.microsoft\.com/fwlink/p/\?LinkID=\d+&clcid=0x[0-9a-f]+&culture={re.escape(culture)}&country={re.escape(country)}"
|
|
links = re.findall(pattern, html)
|
|
|
|
if not links:
|
|
# Fallback: try any English link
|
|
pattern_fallback = r"https://go\.microsoft\.com/fwlink/p/\?LinkID=\d+&clcid=0x[0-9a-f]+&culture=en-us&country=US"
|
|
links = re.findall(pattern_fallback, html)
|
|
|
|
if not links:
|
|
raise RuntimeError(
|
|
f"Could not find download link on {eval_url}. "
|
|
f"Microsoft may have changed the page layout."
|
|
)
|
|
|
|
# First link is typically the x64 ISO
|
|
download_link = links[0]
|
|
logger.info(f"Resolved download link: {download_link}")
|
|
|
|
# Follow redirect to get the actual ISO URL
|
|
req2 = urllib.request.Request(download_link, method="HEAD", headers=headers)
|
|
with urllib.request.urlopen(req2, timeout=30) as resp2:
|
|
final_url = resp2.url
|
|
|
|
logger.info(f"Final ISO URL: {final_url}")
|
|
return final_url
|
|
|
|
|
|
def download_windows_iso(
|
|
version: str = "11",
|
|
dest: Optional[Path] = None,
|
|
iso_path: Optional[str] = None,
|
|
) -> Path:
|
|
"""Get a Windows ISO. If iso_path is given, use it directly."""
|
|
if iso_path:
|
|
p = Path(iso_path)
|
|
if not p.exists():
|
|
raise FileNotFoundError(f"Windows ISO not found: {iso_path}")
|
|
return p
|
|
|
|
dest_dir = dest or Path.home() / ".cua" / "cua-sandbox" / "iso"
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
iso_file = dest_dir / f"windows-{version}.iso"
|
|
|
|
if iso_file.exists():
|
|
logger.info(f"Using cached ISO: {iso_file}")
|
|
return iso_file
|
|
|
|
if version == "11":
|
|
url = "https://go.microsoft.com/fwlink/?linkid=2334167&clcid=0x409"
|
|
elif version == "10":
|
|
url = "https://go.microsoft.com/fwlink/?LinkId=691209"
|
|
elif version in ("server-2022", "2022"):
|
|
version = "server-2022"
|
|
iso_file = dest_dir / f"windows-{version}.iso"
|
|
if iso_file.exists():
|
|
logger.info(f"Using cached ISO: {iso_file}")
|
|
return iso_file
|
|
url = _resolve_server_eval_url("windows-server-2022")
|
|
elif version in ("server-2025", "2025"):
|
|
version = "server-2025"
|
|
iso_file = dest_dir / f"windows-{version}.iso"
|
|
if iso_file.exists():
|
|
logger.info(f"Using cached ISO: {iso_file}")
|
|
return iso_file
|
|
url = _resolve_server_eval_url("windows-server-2025")
|
|
elif version in ("server-2019", "2019"):
|
|
version = "server-2019"
|
|
iso_file = dest_dir / f"windows-{version}.iso"
|
|
if iso_file.exists():
|
|
logger.info(f"Using cached ISO: {iso_file}")
|
|
return iso_file
|
|
url = _resolve_server_eval_url("windows-server-2019")
|
|
else:
|
|
raise ValueError(
|
|
f"Cannot auto-download Windows {version}. "
|
|
f"Supported: 10, 11, server-2022, server-2025, server-2019. "
|
|
f"Or provide --iso-path to use your own ISO."
|
|
)
|
|
|
|
logger.info(f"Downloading Windows {version} Enterprise Evaluation ISO...")
|
|
logger.info("This is ~6GB and may take a while.")
|
|
return download_file(url, iso_file, f"Windows {version} ISO")
|
|
|
|
|
|
# ── Autounattend.xml generation ─────────────────────────────────────────────
|
|
|
|
|
|
def generate_autounattend_xml(
|
|
product_key: Optional[str] = None,
|
|
*,
|
|
include_virtio_drivers: bool = True,
|
|
version: str = "11",
|
|
) -> str:
|
|
"""Generate a Windows Autounattend.xml for unattended installation.
|
|
|
|
Args:
|
|
product_key: Windows product key. If None, uses generic key for edition.
|
|
include_virtio_drivers: Include VirtIO driver paths in WinPE pass.
|
|
Set False for Hyper-V (has enlightened drivers built-in).
|
|
version: Windows version - "11", "10", "server-2022", "server-2025", etc.
|
|
"""
|
|
is_server = version.startswith("server-")
|
|
|
|
# KMS Generic Volume License Keys (GVLKs) for Server editions
|
|
# https://learn.microsoft.com/en-us/windows-server/get-started/kms-client-activation-keys
|
|
_server_gvlks = {
|
|
"server-2025": "TVRH6-WHNXV-R9WG3-9XRFY-MY832", # Standard
|
|
"server-2022": "VDYBN-27WPP-V4HQT-9VMD4-VMK7H", # Standard
|
|
"server-2019": "N69G4-B89J2-4G8F4-WWYCC-J464C", # Standard
|
|
}
|
|
|
|
key_section = ""
|
|
if product_key:
|
|
key_section = f"""<ProductKey>
|
|
<Key>{product_key}</Key>
|
|
</ProductKey>"""
|
|
elif is_server:
|
|
# Server eval ISOs: omit product key entirely. The eval ISO has its own
|
|
# built-in eval key. GVLKs are for volume license media only.
|
|
# Users with SPLA can inject their GVLK via the product_key parameter.
|
|
pass
|
|
else:
|
|
key_section = """<ProductKey>
|
|
<Key>VK7JG-NPHTM-C97JM-9MPGT-3V66T</Key>
|
|
</ProductKey>"""
|
|
|
|
# Image selection: use /IMAGE/NAME for reliability (index varies by ISO).
|
|
# For server eval ISOs, standard pattern is:
|
|
# "Windows Server 2022 Standard Evaluation (Desktop Experience)"
|
|
# For desktop: /IMAGE/INDEX = 1 (single-edition ISOs)
|
|
_server_image_names = {
|
|
"server-2022": "Windows Server 2022 Standard Evaluation (Desktop Experience)",
|
|
"server-2025": "Windows Server 2025 Standard Evaluation (Desktop Experience)",
|
|
"server-2019": "Windows Server 2019 SERVERDATACENTEREVAL",
|
|
}
|
|
if is_server:
|
|
image_name = _server_image_names.get(version)
|
|
# Use /IMAGE/NAME for server (more reliable than index)
|
|
image_select_key = "/IMAGE/NAME"
|
|
image_select_value = (
|
|
image_name
|
|
or f"Windows Server {version.split('-')[1]} Standard Evaluation (Desktop Experience)"
|
|
)
|
|
else:
|
|
image_select_key = "/IMAGE/INDEX"
|
|
image_select_value = "1"
|
|
|
|
virtio_section = ""
|
|
if include_virtio_drivers:
|
|
virtio_section = """
|
|
<component name="Microsoft-Windows-PnpCustomizationsWinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
<DriverPaths>
|
|
<PathAndCredentials wcm:action="add" wcm:keyValue="1">
|
|
<Path>D:\\viostor\\w11\\amd64</Path>
|
|
</PathAndCredentials>
|
|
<PathAndCredentials wcm:action="add" wcm:keyValue="2">
|
|
<Path>E:\\viostor\\w11\\amd64</Path>
|
|
</PathAndCredentials>
|
|
<PathAndCredentials wcm:action="add" wcm:keyValue="3">
|
|
<Path>F:\\viostor\\w11\\amd64</Path>
|
|
</PathAndCredentials>
|
|
<PathAndCredentials wcm:action="add" wcm:keyValue="4">
|
|
<Path>D:\\NetKVM\\w11\\amd64</Path>
|
|
</PathAndCredentials>
|
|
<PathAndCredentials wcm:action="add" wcm:keyValue="5">
|
|
<Path>E:\\NetKVM\\w11\\amd64</Path>
|
|
</PathAndCredentials>
|
|
<PathAndCredentials wcm:action="add" wcm:keyValue="6">
|
|
<Path>F:\\NetKVM\\w11\\amd64</Path>
|
|
</PathAndCredentials>
|
|
</DriverPaths>
|
|
</component>"""
|
|
|
|
return f"""<?xml version="1.0" encoding="utf-8"?>
|
|
<unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
|
<settings pass="windowsPE">
|
|
<component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
<SetupUILanguage>
|
|
<UILanguage>en-US</UILanguage>
|
|
</SetupUILanguage>
|
|
<InputLocale>0409:00000409</InputLocale>
|
|
<SystemLocale>en-US</SystemLocale>
|
|
<UILanguage>en-US</UILanguage>
|
|
<UserLocale>en-US</UserLocale>
|
|
</component>{virtio_section}
|
|
<component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
<DiskConfiguration>
|
|
<Disk wcm:action="add">
|
|
<DiskID>0</DiskID>
|
|
<WillWipeDisk>true</WillWipeDisk>
|
|
<CreatePartitions>
|
|
<CreatePartition wcm:action="add">
|
|
<Order>1</Order>
|
|
<Type>EFI</Type>
|
|
<Size>128</Size>
|
|
</CreatePartition>
|
|
<CreatePartition wcm:action="add">
|
|
<Order>2</Order>
|
|
<Type>MSR</Type>
|
|
<Size>128</Size>
|
|
</CreatePartition>
|
|
<CreatePartition wcm:action="add">
|
|
<Order>3</Order>
|
|
<Type>Primary</Type>
|
|
<Extend>true</Extend>
|
|
</CreatePartition>
|
|
</CreatePartitions>
|
|
<ModifyPartitions>
|
|
<ModifyPartition wcm:action="add">
|
|
<Order>1</Order>
|
|
<PartitionID>1</PartitionID>
|
|
<Label>System</Label>
|
|
<Format>FAT32</Format>
|
|
</ModifyPartition>
|
|
<ModifyPartition wcm:action="add">
|
|
<Order>2</Order>
|
|
<PartitionID>2</PartitionID>
|
|
</ModifyPartition>
|
|
<ModifyPartition wcm:action="add">
|
|
<Order>3</Order>
|
|
<PartitionID>3</PartitionID>
|
|
<Label>Windows</Label>
|
|
<Letter>C</Letter>
|
|
<Format>NTFS</Format>
|
|
</ModifyPartition>
|
|
</ModifyPartitions>
|
|
</Disk>
|
|
</DiskConfiguration>
|
|
<ImageInstall>
|
|
<OSImage>
|
|
<InstallTo>
|
|
<DiskID>0</DiskID>
|
|
<PartitionID>3</PartitionID>
|
|
</InstallTo>
|
|
<InstallFrom>
|
|
<MetaData wcm:action="add">
|
|
<Key>{image_select_key}</Key>
|
|
<Value>{image_select_value}</Value>
|
|
</MetaData>
|
|
</InstallFrom>
|
|
<InstallToAvailablePartition>false</InstallToAvailablePartition>
|
|
</OSImage>
|
|
</ImageInstall>
|
|
<UserData>
|
|
<AcceptEula>true</AcceptEula>
|
|
<FullName>User</FullName>
|
|
<Organization>CUA</Organization>
|
|
{key_section}
|
|
</UserData>
|
|
<EnableFirewall>false</EnableFirewall>
|
|
<RunSynchronous>
|
|
<RunSynchronousCommand wcm:action="add">
|
|
<Order>1</Order>
|
|
<Path>reg.exe add "HKLM\\SYSTEM\\Setup\\LabConfig" /v BypassTPMCheck /t REG_DWORD /d 1 /f</Path>
|
|
</RunSynchronousCommand>
|
|
<RunSynchronousCommand wcm:action="add">
|
|
<Order>2</Order>
|
|
<Path>reg.exe add "HKLM\\SYSTEM\\Setup\\LabConfig" /v BypassSecureBootCheck /t REG_DWORD /d 1 /f</Path>
|
|
</RunSynchronousCommand>
|
|
<RunSynchronousCommand wcm:action="add">
|
|
<Order>3</Order>
|
|
<Path>reg.exe add "HKLM\\SYSTEM\\Setup\\LabConfig" /v BypassRAMCheck /t REG_DWORD /d 1 /f</Path>
|
|
</RunSynchronousCommand>
|
|
<RunSynchronousCommand wcm:action="add">
|
|
<Order>4</Order>
|
|
<Path>reg.exe add "HKLM\\SYSTEM\\Setup\\MoSetup" /v AllowUpgradesWithUnsupportedTPMOrCPU /t REG_DWORD /d 1 /f</Path>
|
|
</RunSynchronousCommand>
|
|
</RunSynchronous>
|
|
</component>
|
|
</settings>
|
|
<settings pass="offlineServicing">
|
|
<component name="Microsoft-Windows-LUA-Settings" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
<EnableLUA>false</EnableLUA>
|
|
</component>
|
|
</settings>
|
|
<settings pass="specialize">
|
|
<component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
<RunSynchronous>
|
|
<RunSynchronousCommand wcm:action="add">
|
|
<Order>1</Order>
|
|
<Path>reg.exe add "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f</Path>
|
|
</RunSynchronousCommand>
|
|
</RunSynchronous>
|
|
</component>
|
|
<component name="Microsoft-Windows-TerminalServices-LocalSessionManager" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
<fDenyTSConnections>false</fDenyTSConnections>
|
|
</component>
|
|
<component name="Microsoft-Windows-TerminalServices-RDP-WinStationExtensions" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
<UserAuthentication>0</UserAuthentication>
|
|
</component>
|
|
<component name="Networking-MPSSVC-Svc" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
<FirewallGroups>
|
|
<FirewallGroup wcm:action="add" wcm:keyValue="RemoteDesktop">
|
|
<Active>true</Active>
|
|
<Profile>all</Profile>
|
|
<Group>@FirewallAPI.dll,-28752</Group>
|
|
</FirewallGroup>
|
|
</FirewallGroups>
|
|
</component>
|
|
</settings>
|
|
<settings pass="oobeSystem">
|
|
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
<UserAccounts>
|
|
<LocalAccounts>
|
|
<LocalAccount wcm:action="add">
|
|
<Name>User</Name>
|
|
<Group>Administrators</Group>
|
|
<Password>
|
|
<Value />
|
|
<PlainText>true</PlainText>
|
|
</Password>
|
|
</LocalAccount>
|
|
</LocalAccounts>
|
|
</UserAccounts>
|
|
<AutoLogon>
|
|
<Username>User</Username>
|
|
<Enabled>true</Enabled>
|
|
<LogonCount>65432</LogonCount>
|
|
<Password>
|
|
<Value />
|
|
<PlainText>true</PlainText>
|
|
</Password>
|
|
</AutoLogon>
|
|
<Display>
|
|
<ColorDepth>32</ColorDepth>
|
|
<HorizontalResolution>1280</HorizontalResolution>
|
|
<VerticalResolution>720</VerticalResolution>
|
|
</Display>
|
|
<OOBE>
|
|
<HideEULAPage>true</HideEULAPage>
|
|
<HideLocalAccountScreen>true</HideLocalAccountScreen>
|
|
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
|
|
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
|
|
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
|
|
<NetworkLocation>Home</NetworkLocation>
|
|
<ProtectYourPC>3</ProtectYourPC>
|
|
<SkipUserOOBE>true</SkipUserOOBE>
|
|
<SkipMachineOOBE>true</SkipMachineOOBE>
|
|
</OOBE>
|
|
<FirstLogonCommands>
|
|
<SynchronousCommand wcm:action="add">
|
|
<Order>1</Order>
|
|
<CommandLine>cmd /C POWERCFG -H OFF</CommandLine>
|
|
<Description>Disable Hibernation</Description>
|
|
</SynchronousCommand>
|
|
<SynchronousCommand wcm:action="add">
|
|
<Order>2</Order>
|
|
<CommandLine>cmd /C POWERCFG -X -monitor-timeout-ac 0</CommandLine>
|
|
<Description>Disable monitor blanking</Description>
|
|
</SynchronousCommand>
|
|
<SynchronousCommand wcm:action="add">
|
|
<Order>3</Order>
|
|
<CommandLine>cmd /C POWERCFG -X -standby-timeout-ac 0</CommandLine>
|
|
<Description>Disable Sleep</Description>
|
|
</SynchronousCommand>
|
|
<SynchronousCommand wcm:action="add">
|
|
<Order>4</Order>
|
|
<CommandLine>reg.exe add "HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows\\WindowsUpdate\\AU" /v "NoAutoUpdate" /t REG_DWORD /d 1 /f</CommandLine>
|
|
<Description>Disable Windows Update</Description>
|
|
</SynchronousCommand>
|
|
<SynchronousCommand wcm:action="add">
|
|
<Order>5</Order>
|
|
<CommandLine>powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "foreach ($d in [System.IO.DriveInfo]::GetDrives()) {{ $f = Join-Path $d.Name 'setup-cua-server.ps1'; if (Test-Path $f) {{ & $f; break }} }}"</CommandLine>
|
|
<Description>Install CUA Computer Server</Description>
|
|
</SynchronousCommand>
|
|
</FirstLogonCommands>
|
|
</component>
|
|
</settings>
|
|
</unattend>"""
|
|
|
|
|
|
def create_unattend_iso(
|
|
work_dir: Path,
|
|
product_key: Optional[str] = None,
|
|
*,
|
|
include_virtio_drivers: bool = True,
|
|
include_startup_nsh: bool = True,
|
|
version: str = "11",
|
|
) -> Path:
|
|
"""Create a small data-only ISO containing Autounattend.xml + setup script.
|
|
|
|
Args:
|
|
include_virtio_drivers: Include VirtIO driver paths (QEMU needs them, Hyper-V doesn't).
|
|
include_startup_nsh: Include UEFI shell startup.nsh (QEMU/OVMF needs it, Hyper-V doesn't).
|
|
version: Windows version for autounattend generation.
|
|
"""
|
|
unattend_iso = work_dir / "unattend.iso"
|
|
if unattend_iso.exists():
|
|
logger.info(f"Using cached unattend ISO: {unattend_iso}")
|
|
return unattend_iso
|
|
|
|
xml = generate_autounattend_xml(
|
|
product_key, include_virtio_drivers=include_virtio_drivers, version=version
|
|
)
|
|
xml_path = work_dir / "Autounattend.xml"
|
|
xml_path.write_text(xml, encoding="utf-8")
|
|
|
|
from cua_sandbox.builder.build import SETUP_COMPUTER_SERVER_PS1
|
|
|
|
setup_path = work_dir / "setup-cua-server.ps1"
|
|
setup_path.write_text(SETUP_COMPUTER_SERVER_PS1, encoding="utf-8")
|
|
|
|
iso_src = work_dir / "unattend-iso"
|
|
iso_src.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(xml_path, iso_src / "Autounattend.xml")
|
|
shutil.copy2(setup_path, iso_src / "setup-cua-server.ps1")
|
|
|
|
if include_startup_nsh:
|
|
startup_nsh = work_dir / "startup.nsh"
|
|
startup_nsh.write_text("FS0:\\EFI\\BOOT\\BOOTX64.EFI\n", encoding="utf-8")
|
|
shutil.copy2(startup_nsh, iso_src / "startup.nsh")
|
|
|
|
_create_data_iso(iso_src, unattend_iso, "OEMDRV")
|
|
logger.info(f"Created unattend ISO: {unattend_iso}")
|
|
return unattend_iso
|
|
|
|
|
|
def _create_data_iso(src_dir: Path, dest_iso: Path, label: str = "OEMDRV") -> None:
|
|
"""Create a data-only ISO using pycdlib (pure Python, cross-platform)."""
|
|
import pycdlib
|
|
|
|
iso = pycdlib.PyCdlib()
|
|
iso.new(joliet=3, vol_ident=label)
|
|
|
|
for f in src_dir.iterdir():
|
|
if not f.is_file():
|
|
continue
|
|
iso_name = f.name.upper().replace("-", "_")
|
|
if "." in iso_name:
|
|
base, ext = iso_name.rsplit(".", 1)
|
|
iso9660_name = f"/{base[:8]}.{ext[:3]};1"
|
|
else:
|
|
iso9660_name = f"/{iso_name[:8]}.;1"
|
|
iso.add_file(
|
|
str(f),
|
|
iso_path=iso9660_name,
|
|
joliet_path=f"/{f.name}",
|
|
)
|
|
|
|
iso.write(str(dest_iso))
|
|
iso.close()
|