mirror of
https://github.com/dockur/windows.git
synced 2026-07-28 21:53:34 +07:00
Test
This commit is contained in:
@@ -49,6 +49,9 @@ ADD --chmod=664 https://github.com/qemus/virtiso-whql/releases/download/v${VERSI
|
||||
FROM dockurr/windows-arm:${VERSION_ARG} AS build-arm64
|
||||
FROM build-${TARGETARCH}
|
||||
|
||||
ARG VERSION_PYCDLIB="1.16.0"
|
||||
RUN python3 -m pip install --break-system-packages --no-cache-dir "pycdlib==${VERSION_PYCDLIB}"
|
||||
|
||||
ARG VERSION_ARG="0.00"
|
||||
RUN echo "$VERSION_ARG" > /etc/version
|
||||
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# PyCdlib ISO prototype test branch
|
||||
|
||||
This branch builds the modified Windows installer twice when `ISO_TEST=Y`:
|
||||
|
||||
1. PyCdlib rewrites the original ISO without extracting unchanged files.
|
||||
2. The existing `genisoimage` path builds a reference ISO from the extracted tree.
|
||||
3. The original, reference, and PyCdlib images are compared.
|
||||
4. The VM boots from the PyCdlib image.
|
||||
|
||||
The included `compose.yml` enables `ISO_TEST=Y`. Build and run it with:
|
||||
|
||||
```bash
|
||||
docker compose build --no-cache
|
||||
docker compose up
|
||||
```
|
||||
|
||||
Results are written under `/storage/iso-test`:
|
||||
|
||||
- `genisoimage.iso` — current builder reference
|
||||
- `report.json` — three-way descriptor, UDF, and El Torito comparison
|
||||
- `timing.json` — build durations and seconds saved
|
||||
|
||||
The log also prints:
|
||||
|
||||
```text
|
||||
ISO build timing: PyCdlib 123s, genisoimage 180s, saved 57s.
|
||||
```
|
||||
|
||||
`saved_seconds` is calculated as `genisoimage_seconds - pycdlib_seconds`; a negative value means PyCdlib was slower. The final `/storage/windows*.iso` used by QEMU is the PyCdlib image. Disable the experiment with `ISO_TEST=N` to use the unmodified production path.
|
||||
|
||||
This is deliberately a validation branch. Keep enough free space for the original ISO, extracted tree, PyCdlib ISO, and retained genisoimage reference ISO during the dual build.
|
||||
@@ -4,6 +4,7 @@ services:
|
||||
container_name: windows
|
||||
environment:
|
||||
VERSION: "11"
|
||||
ISO_TEST: "Y"
|
||||
devices:
|
||||
- /dev/kvm
|
||||
- /dev/net/tun
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ else
|
||||
fi
|
||||
|
||||
pid=$!
|
||||
( sleep 30; boot ) &
|
||||
waitForBoot "$pid" 30 &
|
||||
|
||||
rc=0
|
||||
wait "$pid" || rc=$?
|
||||
|
||||
+112
-5
@@ -1220,6 +1220,99 @@ bootWindows() {
|
||||
return 0
|
||||
}
|
||||
|
||||
buildIsoPrototype() {
|
||||
|
||||
local iso="$1"
|
||||
local dir="$2"
|
||||
local test_dir="$STORAGE/iso-test"
|
||||
local py_iso="$TMP/pycdlib.iso"
|
||||
local gen_iso="$test_dir/genisoimage.iso"
|
||||
local report="$test_dir/report.json"
|
||||
local timing="$test_dir/timing.json"
|
||||
local start end py_seconds gen_seconds compare_seconds total_seconds saved_seconds
|
||||
|
||||
mkdir -p "$test_dir" || return 1
|
||||
rm -f "$py_iso" "$gen_iso" "$report" "$timing" || return 1
|
||||
|
||||
start=$(date +%s)
|
||||
local total_start="$start"
|
||||
|
||||
info "Building PyCdlib prototype image..."
|
||||
local py_start="$start"
|
||||
local -a args=(
|
||||
build
|
||||
--source "$iso"
|
||||
--tree "$dir"
|
||||
--output "$py_iso"
|
||||
)
|
||||
|
||||
local boot_wim
|
||||
boot_wim=$(find "$dir" -maxdepth 2 -type f -ipath '*/sources/boot.wim' -print -quit) || return 1
|
||||
if [ -n "$boot_wim" ]; then
|
||||
args+=(--replace "sources/boot.wim")
|
||||
fi
|
||||
|
||||
if [ -d "$dir/\$OEM\$" ]; then
|
||||
args+=(--add-tree '$OEM$')
|
||||
fi
|
||||
|
||||
if ! python3 /run/iso-prototype.py "${args[@]}"; then
|
||||
error "Failed to build PyCdlib prototype image!"
|
||||
return 1
|
||||
fi
|
||||
end=$(date +%s)
|
||||
py_seconds=$((end - py_start))
|
||||
|
||||
info "Building genisoimage reference image..."
|
||||
local gen_start="$end"
|
||||
if ! buildImage "$dir"; then
|
||||
return 1
|
||||
fi
|
||||
end=$(date +%s)
|
||||
gen_seconds=$((end - gen_start))
|
||||
|
||||
if ! mv -f "$BOOT" "$gen_iso"; then
|
||||
error "Failed to preserve genisoimage reference image!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
info "Comparing ISO metadata..."
|
||||
local compare_start="$end"
|
||||
if ! python3 /run/iso-prototype.py compare \
|
||||
--original "$iso" \
|
||||
--genisoimage "$gen_iso" \
|
||||
--pycdlib "$py_iso" \
|
||||
--output "$report"; then
|
||||
error "Failed to compare prototype images!"
|
||||
return 1
|
||||
fi
|
||||
end=$(date +%s)
|
||||
compare_seconds=$((end - compare_start))
|
||||
total_seconds=$((end - total_start))
|
||||
saved_seconds=$((gen_seconds - py_seconds))
|
||||
|
||||
cat > "$timing" <<EOF
|
||||
{
|
||||
"pycdlib_seconds": $py_seconds,
|
||||
"genisoimage_seconds": $gen_seconds,
|
||||
"saved_seconds": $saved_seconds,
|
||||
"comparison_seconds": $compare_seconds,
|
||||
"total_test_seconds": $total_seconds
|
||||
}
|
||||
EOF
|
||||
|
||||
info "ISO build timing: PyCdlib ${py_seconds}s, genisoimage ${gen_seconds}s, saved ${saved_seconds}s."
|
||||
info "ISO test report: $report"
|
||||
info "ISO timing report: $timing"
|
||||
|
||||
if ! mv -f "$py_iso" "$BOOT"; then
|
||||
error "Failed to select PyCdlib image for boot testing!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
######################################
|
||||
|
||||
! parseVersion && exit 58
|
||||
@@ -1260,12 +1353,26 @@ if ! updateImage "$DIR" "$XML" "$LANGUAGE"; then
|
||||
exit 63
|
||||
fi
|
||||
|
||||
if ! removeImage "$ISO"; then
|
||||
exit 64
|
||||
fi
|
||||
if enabled "${ISO_TEST:-}"; then
|
||||
|
||||
if ! buildIsoPrototype "$ISO" "$DIR"; then
|
||||
exit 65
|
||||
fi
|
||||
|
||||
if ! removeImage "$ISO"; then
|
||||
exit 64
|
||||
fi
|
||||
|
||||
else
|
||||
|
||||
if ! removeImage "$ISO"; then
|
||||
exit 64
|
||||
fi
|
||||
|
||||
if ! buildImage "$DIR"; then
|
||||
exit 65
|
||||
fi
|
||||
|
||||
if ! buildImage "$DIR"; then
|
||||
exit 65
|
||||
fi
|
||||
|
||||
if ! finishInstall "$BOOT" "N"; then
|
||||
|
||||
@@ -0,0 +1,534 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prototype userspace Windows ISO modifier and metadata comparator.
|
||||
|
||||
Requires Python 3.10+ and pycdlib 1.16.0 for the ``build`` command.
|
||||
The ``compare`` command has no third-party Python dependencies.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Any, Iterable
|
||||
|
||||
SECTOR = 2048
|
||||
|
||||
|
||||
class PrototypeError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
h = hashlib.sha256()
|
||||
with path.open("rb") as f:
|
||||
for chunk in iter(lambda: f.read(8 * 1024 * 1024), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def sha256_region(f, offset: int, length: int) -> str:
|
||||
f.seek(offset)
|
||||
h = hashlib.sha256()
|
||||
remaining = length
|
||||
while remaining:
|
||||
chunk = f.read(min(1024 * 1024, remaining))
|
||||
if not chunk:
|
||||
raise PrototypeError(f"Unexpected EOF at byte {offset + length - remaining}")
|
||||
h.update(chunk)
|
||||
remaining -= len(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def decode_ascii(data: bytes) -> str:
|
||||
return data.decode("ascii", "replace").rstrip(" \x00")
|
||||
|
||||
|
||||
def both_endian_u16(data: bytes) -> int:
|
||||
return struct.unpack_from("<H", data, 0)[0]
|
||||
|
||||
|
||||
def both_endian_u32(data: bytes) -> int:
|
||||
return struct.unpack_from("<I", data, 0)[0]
|
||||
|
||||
|
||||
@dataclass
|
||||
class VolumeDescriptor:
|
||||
sector: int
|
||||
type: int
|
||||
identifier: str
|
||||
version: int
|
||||
sha256: str
|
||||
volume_id: str | None = None
|
||||
volume_space_size: int | None = None
|
||||
logical_block_size: int | None = None
|
||||
path_table_size: int | None = None
|
||||
l_path_table: int | None = None
|
||||
m_path_table: int | None = None
|
||||
escape_sequences_hex: str | None = None
|
||||
boot_system_id: str | None = None
|
||||
boot_catalog_lba: int | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class BootEntry:
|
||||
offset: int
|
||||
kind: str
|
||||
platform_id: int | None
|
||||
bootable: bool | None
|
||||
media_type: int | None
|
||||
load_segment: int | None
|
||||
system_type: int | None
|
||||
sector_count: int | None
|
||||
image_lba: int | None
|
||||
image_sha256: str | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class UdfTag:
|
||||
sector: int
|
||||
tag_id: int
|
||||
version: int
|
||||
serial: int
|
||||
location: int
|
||||
sha256: str
|
||||
|
||||
|
||||
def parse_volume_descriptors(path: Path) -> tuple[list[VolumeDescriptor], int | None]:
|
||||
result: list[VolumeDescriptor] = []
|
||||
catalog_lba = None
|
||||
with path.open("rb") as f:
|
||||
for sector in range(16, 16 + 128):
|
||||
f.seek(sector * SECTOR)
|
||||
data = f.read(SECTOR)
|
||||
if len(data) != SECTOR:
|
||||
break
|
||||
ident = decode_ascii(data[1:6])
|
||||
if ident != "CD001":
|
||||
# UDF bridge images may have BEA01/NSR0x/TEA01 descriptors here.
|
||||
if data[1:6] in (b"BEA01", b"NSR02", b"NSR03", b"TEA01", b"CDW02"):
|
||||
result.append(VolumeDescriptor(
|
||||
sector=sector,
|
||||
type=data[0],
|
||||
identifier=decode_ascii(data[1:6]),
|
||||
version=data[6],
|
||||
sha256=hashlib.sha256(data).hexdigest(),
|
||||
))
|
||||
continue
|
||||
if result:
|
||||
break
|
||||
continue
|
||||
|
||||
vd = VolumeDescriptor(
|
||||
sector=sector,
|
||||
type=data[0],
|
||||
identifier=ident,
|
||||
version=data[6],
|
||||
sha256=hashlib.sha256(data).hexdigest(),
|
||||
)
|
||||
if vd.type in (1, 2):
|
||||
vd.volume_id = decode_ascii(data[40:72])
|
||||
vd.volume_space_size = both_endian_u32(data[80:88])
|
||||
vd.logical_block_size = both_endian_u16(data[128:132])
|
||||
vd.path_table_size = both_endian_u32(data[132:140])
|
||||
vd.l_path_table = struct.unpack_from("<I", data, 140)[0]
|
||||
vd.m_path_table = struct.unpack_from(">I", data, 148)[0]
|
||||
if vd.type == 2:
|
||||
vd.escape_sequences_hex = data[88:120].rstrip(b"\x00").hex()
|
||||
elif vd.type == 0:
|
||||
vd.boot_system_id = decode_ascii(data[7:39])
|
||||
if vd.boot_system_id == "EL TORITO SPECIFICATION":
|
||||
vd.boot_catalog_lba = struct.unpack_from("<I", data, 71)[0]
|
||||
catalog_lba = vd.boot_catalog_lba
|
||||
result.append(vd)
|
||||
if vd.type == 255:
|
||||
break
|
||||
return result, catalog_lba
|
||||
|
||||
|
||||
def boot_image_hash(f, lba: int, sector_count: int) -> str | None:
|
||||
if not lba:
|
||||
return None
|
||||
# El Torito's count is in 512-byte virtual sectors. A zero count is legal in
|
||||
# some catalogs but gives us no bounded image length to hash.
|
||||
length = sector_count * 512
|
||||
if length <= 0:
|
||||
return None
|
||||
return sha256_region(f, lba * SECTOR, length)
|
||||
|
||||
|
||||
def parse_boot_catalog(path: Path, catalog_lba: int | None) -> dict[str, Any] | None:
|
||||
if catalog_lba is None:
|
||||
return None
|
||||
with path.open("rb") as f:
|
||||
f.seek(catalog_lba * SECTOR)
|
||||
data = f.read(SECTOR)
|
||||
if len(data) != SECTOR:
|
||||
raise PrototypeError(f"Cannot read El Torito catalog at LBA {catalog_lba}")
|
||||
|
||||
validation = data[:32]
|
||||
checksum_words = struct.unpack("<16H", validation)
|
||||
catalog: dict[str, Any] = {
|
||||
"lba": catalog_lba,
|
||||
"sha256": hashlib.sha256(data).hexdigest(),
|
||||
"validation_header_id": validation[0],
|
||||
"validation_platform_id": validation[1],
|
||||
"validation_signature_hex": validation[30:32].hex(),
|
||||
"validation_checksum_valid": (sum(checksum_words) & 0xFFFF) == 0,
|
||||
"entries": [],
|
||||
}
|
||||
|
||||
platform = validation[1]
|
||||
offset = 32
|
||||
entries: list[BootEntry] = []
|
||||
while offset + 32 <= len(data):
|
||||
entry = data[offset:offset + 32]
|
||||
indicator = entry[0]
|
||||
if entry == b"\x00" * 32:
|
||||
break
|
||||
if indicator in (0x90, 0x91):
|
||||
platform = entry[1]
|
||||
count = struct.unpack_from("<H", entry, 2)[0]
|
||||
entries.append(BootEntry(offset, "section_header", platform, None, None,
|
||||
None, None, count, None, None))
|
||||
elif indicator in (0x88, 0x00):
|
||||
media = entry[1]
|
||||
segment = struct.unpack_from("<H", entry, 2)[0]
|
||||
system_type = entry[4]
|
||||
count = struct.unpack_from("<H", entry, 6)[0]
|
||||
lba = struct.unpack_from("<I", entry, 8)[0]
|
||||
entries.append(BootEntry(
|
||||
offset=offset,
|
||||
kind="boot_entry",
|
||||
platform_id=platform,
|
||||
bootable=indicator == 0x88,
|
||||
media_type=media,
|
||||
load_segment=segment,
|
||||
system_type=system_type,
|
||||
sector_count=count,
|
||||
image_lba=lba,
|
||||
image_sha256=boot_image_hash(f, lba, count),
|
||||
))
|
||||
elif indicator == 0x44:
|
||||
entries.append(BootEntry(offset, "extension", platform, None, None,
|
||||
None, None, None, None, None))
|
||||
else:
|
||||
entries.append(BootEntry(offset, f"unknown_0x{indicator:02x}", platform,
|
||||
None, None, None, None, None, None, None))
|
||||
offset += 32
|
||||
catalog["entries"] = [asdict(e) for e in entries]
|
||||
return catalog
|
||||
|
||||
|
||||
def parse_udf_tags(path: Path) -> list[UdfTag]:
|
||||
size = path.stat().st_size
|
||||
sectors = size // SECTOR
|
||||
candidates = set(range(16, min(sectors, 16 + 512)))
|
||||
for base in (256, sectors - 256, sectors - 1):
|
||||
if 0 <= base < sectors:
|
||||
candidates.update(range(max(0, base - 4), min(sectors, base + 5)))
|
||||
|
||||
tags: list[UdfTag] = []
|
||||
with path.open("rb") as f:
|
||||
for sector in sorted(candidates):
|
||||
f.seek(sector * SECTOR)
|
||||
data = f.read(SECTOR)
|
||||
if len(data) != SECTOR:
|
||||
continue
|
||||
tag_id, version, checksum, _reserved, serial, _crc, _crc_len, location = struct.unpack_from(
|
||||
"<HHBBHHHI", data, 0
|
||||
)
|
||||
if not (1 <= tag_id <= 266 and version in (2, 3)):
|
||||
continue
|
||||
header = bytearray(data[:16])
|
||||
header[4] = 0
|
||||
if (sum(header) & 0xFF) != checksum:
|
||||
continue
|
||||
tags.append(UdfTag(
|
||||
sector=sector,
|
||||
tag_id=tag_id,
|
||||
version=version,
|
||||
serial=serial,
|
||||
location=location,
|
||||
sha256=hashlib.sha256(data).hexdigest(),
|
||||
))
|
||||
return tags
|
||||
|
||||
|
||||
def run_optional(command: list[str]) -> dict[str, Any]:
|
||||
try:
|
||||
proc = subprocess.run(command, text=True, capture_output=True, check=False)
|
||||
except FileNotFoundError:
|
||||
return {"available": False}
|
||||
return {
|
||||
"available": True,
|
||||
"returncode": proc.returncode,
|
||||
"stdout": proc.stdout,
|
||||
"stderr": proc.stderr,
|
||||
}
|
||||
|
||||
|
||||
def inspect_iso(path: Path) -> dict[str, Any]:
|
||||
descriptors, catalog_lba = parse_volume_descriptors(path)
|
||||
return {
|
||||
"path": str(path),
|
||||
"size": path.stat().st_size,
|
||||
"sha256": sha256_file(path),
|
||||
"volume_descriptors": [asdict(v) for v in descriptors],
|
||||
"el_torito": parse_boot_catalog(path, catalog_lba),
|
||||
"udf_tags": [asdict(t) for t in parse_udf_tags(path)],
|
||||
"tools": {
|
||||
"isoinfo": run_optional(["isoinfo", "-d", "-i", str(path)]),
|
||||
"xorriso_el_torito": run_optional([
|
||||
"xorriso", "-indev", str(path), "-report_el_torito", "plain"
|
||||
]),
|
||||
"xorriso_system_area": run_optional([
|
||||
"xorriso", "-indev", str(path), "-report_system_area", "plain"
|
||||
]),
|
||||
"udfinfo": run_optional(["udfinfo", str(path)]),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def descriptor_semantic(v: dict[str, Any]) -> dict[str, Any]:
|
||||
ignored = {"sector", "sha256", "l_path_table", "m_path_table", "boot_catalog_lba"}
|
||||
return {k: val for k, val in v.items() if k not in ignored}
|
||||
|
||||
|
||||
def boot_semantic(catalog: dict[str, Any] | None) -> Any:
|
||||
if catalog is None:
|
||||
return None
|
||||
entries = []
|
||||
for entry in catalog["entries"]:
|
||||
entries.append({k: v for k, v in entry.items()
|
||||
if k not in {"offset", "image_lba"}})
|
||||
return {
|
||||
"validation_header_id": catalog["validation_header_id"],
|
||||
"validation_platform_id": catalog["validation_platform_id"],
|
||||
"validation_signature_hex": catalog["validation_signature_hex"],
|
||||
"validation_checksum_valid": catalog["validation_checksum_valid"],
|
||||
"entries": entries,
|
||||
}
|
||||
|
||||
|
||||
def compare_pair(a: dict[str, Any], b: dict[str, Any]) -> dict[str, Any]:
|
||||
av = a["volume_descriptors"]
|
||||
bv = b["volume_descriptors"]
|
||||
return {
|
||||
"byte_identical": a["sha256"] == b["sha256"],
|
||||
"same_size": a["size"] == b["size"],
|
||||
"volume_descriptor_bytes_identical": [v["sha256"] for v in av] == [v["sha256"] for v in bv],
|
||||
"volume_descriptor_semantics_equal": [descriptor_semantic(v) for v in av] == [descriptor_semantic(v) for v in bv],
|
||||
"el_torito_catalog_bytes_identical": (
|
||||
a["el_torito"] is not None and b["el_torito"] is not None and
|
||||
a["el_torito"]["sha256"] == b["el_torito"]["sha256"]
|
||||
),
|
||||
"el_torito_semantics_equal": boot_semantic(a["el_torito"]) == boot_semantic(b["el_torito"]),
|
||||
"udf_tag_sequence_equal": [t["tag_id"] for t in a["udf_tags"]] == [t["tag_id"] for t in b["udf_tags"]],
|
||||
"udf_tag_bytes_identical": [t["sha256"] for t in a["udf_tags"]] == [t["sha256"] for t in b["udf_tags"]],
|
||||
}
|
||||
|
||||
|
||||
def print_summary(report: dict[str, Any]) -> None:
|
||||
print("ISO metadata comparison")
|
||||
print("=======================")
|
||||
for name, info in report["images"].items():
|
||||
print(f"{name:12} {info['size']:12d} bytes {info['sha256'][:16]}… {info['path']}")
|
||||
print()
|
||||
for pair, values in report["comparisons"].items():
|
||||
print(pair)
|
||||
for key, value in values.items():
|
||||
print(f" {key:39} {value}")
|
||||
print()
|
||||
|
||||
|
||||
def iso_candidates(path: PurePosixPath, is_dir: bool = False) -> list[str]:
|
||||
parts = []
|
||||
for part in path.parts:
|
||||
if part == "/":
|
||||
continue
|
||||
# Level 4 accepts long names, but records are conventionally uppercase.
|
||||
parts.append(part.upper())
|
||||
base = "/" + "/".join(parts)
|
||||
if is_dir:
|
||||
return [base]
|
||||
return [base + ";1", base]
|
||||
|
||||
|
||||
def find_existing_record(iso, logical: PurePosixPath) -> dict[str, str]:
|
||||
found: dict[str, str] = {}
|
||||
probes = {
|
||||
"udf_path": [str(logical)],
|
||||
"joliet_path": [str(logical)],
|
||||
"iso_path": iso_candidates(logical),
|
||||
}
|
||||
for namespace, candidates in probes.items():
|
||||
for candidate in candidates:
|
||||
try:
|
||||
iso.get_record(**{namespace: candidate})
|
||||
except Exception: # pycdlib uses several dedicated exception classes.
|
||||
continue
|
||||
found[namespace] = candidate
|
||||
break
|
||||
return found
|
||||
|
||||
|
||||
def namespace_kwargs(iso, logical: PurePosixPath, *, is_dir: bool = False) -> dict[str, str]:
|
||||
kwargs: dict[str, str] = {"iso_path": iso_candidates(logical, is_dir=is_dir)[0]}
|
||||
if iso.has_joliet():
|
||||
kwargs["joliet_path"] = str(logical)
|
||||
if iso.has_udf():
|
||||
kwargs["udf_path"] = str(logical)
|
||||
return kwargs
|
||||
|
||||
|
||||
def ensure_parent_directories(iso, logical: PurePosixPath) -> None:
|
||||
current = PurePosixPath("/")
|
||||
for part in logical.parent.parts:
|
||||
if part == "/":
|
||||
continue
|
||||
current /= part
|
||||
if find_existing_record(iso, current):
|
||||
continue
|
||||
iso.add_directory(**namespace_kwargs(iso, current, is_dir=True))
|
||||
|
||||
|
||||
def remove_logical(iso, logical: PurePosixPath) -> bool:
|
||||
found = find_existing_record(iso, logical)
|
||||
if not found:
|
||||
return False
|
||||
iso.rm_file(**found)
|
||||
return True
|
||||
|
||||
|
||||
def add_file_all_namespaces(iso, source: Path, logical: PurePosixPath) -> None:
|
||||
ensure_parent_directories(iso, logical)
|
||||
iso.add_file(str(source), **namespace_kwargs(iso, logical))
|
||||
|
||||
|
||||
def sync_file(iso, tree: Path, logical: PurePosixPath) -> None:
|
||||
source = tree / str(logical).lstrip("/")
|
||||
if not source.is_file():
|
||||
raise PrototypeError(f"Replacement does not exist: {source}")
|
||||
removed = remove_logical(iso, logical)
|
||||
if not removed:
|
||||
raise PrototypeError(
|
||||
f"Cannot map existing file {logical} in ISO9660, Joliet, or UDF; refusing replacement"
|
||||
)
|
||||
add_file_all_namespaces(iso, source, logical)
|
||||
|
||||
|
||||
def sync_tree(iso, tree: Path, logical_root: PurePosixPath) -> None:
|
||||
source_root = tree / str(logical_root).lstrip("/")
|
||||
if not source_root.is_dir():
|
||||
raise PrototypeError(f"Addition tree does not exist: {source_root}")
|
||||
for root, dirs, files in os.walk(source_root):
|
||||
root_path = Path(root)
|
||||
rel = root_path.relative_to(tree)
|
||||
logical_dir = PurePosixPath("/") / PurePosixPath(rel.as_posix())
|
||||
ensure_parent_directories(iso, logical_dir / "placeholder")
|
||||
if not find_existing_record(iso, logical_dir):
|
||||
iso.add_directory(**namespace_kwargs(iso, logical_dir, is_dir=True))
|
||||
for name in files:
|
||||
logical = logical_dir / name
|
||||
remove_logical(iso, logical)
|
||||
add_file_all_namespaces(iso, root_path / name, logical)
|
||||
|
||||
|
||||
def build_pycdlib(args: argparse.Namespace) -> None:
|
||||
try:
|
||||
import pycdlib # type: ignore
|
||||
except ImportError as exc:
|
||||
raise PrototypeError(
|
||||
"The build command requires pycdlib 1.16.0 (pip install pycdlib==1.16.0)"
|
||||
) from exc
|
||||
|
||||
source = Path(args.source).resolve()
|
||||
tree = Path(args.tree).resolve()
|
||||
output = Path(args.output).resolve()
|
||||
if output.exists():
|
||||
raise PrototypeError(f"Output already exists: {output}")
|
||||
|
||||
iso = pycdlib.PyCdlib()
|
||||
try:
|
||||
iso.open(str(source))
|
||||
for value in args.replace:
|
||||
sync_file(iso, tree, PurePosixPath("/") / value.lstrip("/"))
|
||||
for value in args.add_tree:
|
||||
sync_tree(iso, tree, PurePosixPath("/") / value.lstrip("/"))
|
||||
iso.force_consistency()
|
||||
iso.write(str(output))
|
||||
finally:
|
||||
iso.close()
|
||||
|
||||
print(f"Created {output}")
|
||||
|
||||
|
||||
def compare(args: argparse.Namespace) -> None:
|
||||
images: dict[str, dict[str, Any]] = {}
|
||||
for name, value in (("original", args.original), ("genisoimage", args.genisoimage),
|
||||
("pycdlib", args.pycdlib)):
|
||||
if value:
|
||||
path = Path(value).resolve()
|
||||
if not path.is_file():
|
||||
raise PrototypeError(f"ISO not found: {path}")
|
||||
images[name] = inspect_iso(path)
|
||||
|
||||
comparisons: dict[str, Any] = {}
|
||||
names = list(images)
|
||||
for i, left in enumerate(names):
|
||||
for right in names[i + 1:]:
|
||||
comparisons[f"{left} -> {right}"] = compare_pair(images[left], images[right])
|
||||
|
||||
report = {"images": images, "comparisons": comparisons}
|
||||
output = Path(args.output).resolve()
|
||||
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
print_summary(report)
|
||||
print(f"Full report: {output}")
|
||||
|
||||
|
||||
def make_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
build = sub.add_parser("build", help="Create an ISO by modifying the original with PyCdlib")
|
||||
build.add_argument("--source", required=True, help="Original Microsoft ISO")
|
||||
build.add_argument("--tree", required=True, help="Current modified extracted DVD tree")
|
||||
build.add_argument("--output", required=True, help="PyCdlib output ISO")
|
||||
build.add_argument("--replace", action="append", default=[],
|
||||
help="File to replace from --tree (repeatable)")
|
||||
build.add_argument("--add-tree", action="append", default=[],
|
||||
help="Directory tree to add/synchronize from --tree (repeatable)")
|
||||
build.set_defaults(func=build_pycdlib)
|
||||
|
||||
comp = sub.add_parser("compare", help="Compare filesystem and boot metadata")
|
||||
comp.add_argument("--original", required=True)
|
||||
comp.add_argument("--genisoimage", required=True)
|
||||
comp.add_argument("--pycdlib", required=True)
|
||||
comp.add_argument("--output", default="iso-metadata-report.json")
|
||||
comp.set_defaults(func=compare)
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = make_parser()
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
args.func(args)
|
||||
except PrototypeError as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
except Exception as exc:
|
||||
print(f"UNEXPECTED ERROR: {type(exc).__name__}: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+59
-15
@@ -15,36 +15,80 @@ CONSOLE_PID="$QEMU_DIR/console.pid"
|
||||
CONSOLE_SOCKET="$QEMU_DIR/console.sock"
|
||||
QEMU_START_PID="$QEMU_DIR/qemu.start.pid"
|
||||
|
||||
bootFailed() {
|
||||
bootStatus() {
|
||||
|
||||
local fail=""
|
||||
[ ! -s "$QEMU_PTY" ] && return 1
|
||||
|
||||
if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then
|
||||
grep -Fq "No bootable device." "$QEMU_PTY" && fail="y"
|
||||
grep -Fq "BOOTMGR is missing" "$QEMU_PTY" && fail="y"
|
||||
grep -Fq \
|
||||
-e "No bootable device." \
|
||||
-e "BOOTMGR is missing" \
|
||||
-e "Boot failed: not a bootable disk" \
|
||||
-e "Boot failed: could not read the boot disk" \
|
||||
"$QEMU_PTY" && return 2
|
||||
|
||||
grep -Eq '^Booting from (Hard|DVD/CD)' "$QEMU_PTY"
|
||||
return $?
|
||||
fi
|
||||
|
||||
[ -n "$fail" ]
|
||||
grep -Fq "UEFI Interactive Shell" "$QEMU_PTY" && return 2
|
||||
|
||||
local last
|
||||
last=$(grep -E \
|
||||
'BdsDxe: starting Boot[[:xdigit:]]{4} ' \
|
||||
"$QEMU_PTY" | tail -1)
|
||||
|
||||
[ -z "$last" ] && return 1
|
||||
|
||||
grep -Eq \
|
||||
-e '"Windows Boot Manager"' \
|
||||
-e '"UEFI QEMU .*DVD-ROM' \
|
||||
-e 'CDROM\(' \
|
||||
-e 'USB\(' \
|
||||
<<< "$last"
|
||||
}
|
||||
|
||||
boot() {
|
||||
waitForBoot() {
|
||||
|
||||
[ -f "$QEMU_END" ] && return 0
|
||||
local pid="$1"
|
||||
local timeout="${2:-30}"
|
||||
local deadline=$((SECONDS + timeout))
|
||||
local screen="visit http://127.0.0.1:$WEB_PORT/ to view the screen..."
|
||||
|
||||
if [ -s "$QEMU_PTY" ]; then
|
||||
if [ "$(stat -c%s "$QEMU_PTY")" -gt 7 ]; then
|
||||
if ! bootFailed; then
|
||||
while isAlive "$pid"; do
|
||||
|
||||
if bootStatus; then
|
||||
local status=0
|
||||
else
|
||||
local status=$?
|
||||
fi
|
||||
|
||||
case "$status" in
|
||||
0)
|
||||
if [[ "${DISPLAY,,}" == "web" ]] && ! disabled "${WEB:-Y}"; then
|
||||
info "$(app) started successfully, visit http://127.0.0.1:$WEB_PORT/ to view the screen..."
|
||||
info "$(app) started successfully, $screen"
|
||||
else
|
||||
info "$(app) started successfully."
|
||||
fi
|
||||
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
return 0 ;;
|
||||
2)
|
||||
if [[ "${DISPLAY,,}" == "web" ]] && ! disabled "${WEB:-Y}"; then
|
||||
warn "$(app) could not boot, $screen"
|
||||
else
|
||||
warn "$(app) could not boot."
|
||||
fi
|
||||
|
||||
return 0 ;;
|
||||
esac
|
||||
|
||||
(( SECONDS >= deadline )) && break
|
||||
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
! isAlive "$pid" && return 0
|
||||
[ -f "$QEMU_END" ] && return 0
|
||||
|
||||
error "Timeout while waiting for QEMU to boot the machine, aborting..."
|
||||
terminateQemu
|
||||
|
||||
Reference in New Issue
Block a user