Cross-build Windows native bundle on Linux
All checks were successful
Core validation / Linux core gate (push) Successful in 2m13s
Linux release build / Linux x64 artifact (push) Successful in 1m21s
Linux release build / Publish tagged Gitea release (push) Has been skipped

This commit is contained in:
gamer147
2026-08-03 21:28:18 -04:00
parent 304b290a02
commit 60ac6fc64f
10 changed files with 395 additions and 11 deletions

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env python3
from __future__ import annotations
import struct
import tempfile
import unittest
from pathlib import Path
from unittest.mock import Mock, patch
import verify_windows_native
def write_pe(path: Path, machine: int = verify_windows_native.PE_MACHINE_AMD64) -> None:
data = bytearray(128)
data[:2] = b"MZ"
struct.pack_into("<I", data, 0x3C, 64)
data[64:68] = b"PE\0\0"
struct.pack_into("<H", data, 68, machine)
path.write_bytes(data)
def objdump_text(*, omit_export: str | None = None, extra_import: str | None = None) -> str:
lines = [*(f"DLL Name: {name}" for name in verify_windows_native.RUNTIME_DLLS)]
lines.extend(
f"[ 0] {name}" for name in sorted(verify_windows_native.REQUIRED_EXPORTS)
if name != omit_export
)
if extra_import is not None:
lines.append(f"DLL Name: {extra_import}")
return "\n".join(lines)
def create_bundle(root: Path) -> Path:
for name in (verify_windows_native.SHIM, *verify_windows_native.RUNTIME_DLLS):
write_pe(root / name)
(root / "FFmpeg-LICENSE.txt").write_text("LGPL\n", encoding="utf-8")
return root
class VerifyWindowsNativeTests(unittest.TestCase):
def test_accepts_exact_amd64_bundle_and_contract(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
bundle = create_bundle(Path(temporary))
completed = Mock(returncode=0, stdout=objdump_text(), stderr="")
with patch.object(verify_windows_native.subprocess, "run", return_value=completed):
report = verify_windows_native.verify_bundle(bundle, "objdump")
self.assertEqual("win-x64", report["target"])
self.assertEqual(len(verify_windows_native.REQUIRED_EXPORTS), len(report["shim_exports"]))
def test_rejects_wrong_machine_and_unexpected_dll(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
bundle = create_bundle(Path(temporary))
write_pe(bundle / verify_windows_native.RUNTIME_DLLS[0], machine=0x014C)
with self.assertRaisesRegex(ValueError, "not AMD64"):
verify_windows_native.verify_bundle(bundle, "objdump")
write_pe(bundle / verify_windows_native.RUNTIME_DLLS[0])
write_pe(bundle / "stale.dll")
with self.assertRaisesRegex(ValueError, "unexpected DLL"):
verify_windows_native.verify_bundle(bundle, "objdump")
def test_rejects_missing_export_and_compatibility_runtime(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
bundle = create_bundle(Path(temporary))
missing = next(iter(verify_windows_native.REQUIRED_EXPORTS))
completed = Mock(
returncode=0,
stdout=objdump_text(omit_export=missing, extra_import="cygwin1.dll"),
stderr="",
)
with patch.object(verify_windows_native.subprocess, "run", return_value=completed):
with self.assertRaisesRegex(ValueError, "compatibility runtime"):
verify_windows_native.verify_bundle(bundle, "objdump")
completed.stdout = objdump_text(omit_export=missing)
with patch.object(verify_windows_native.subprocess, "run", return_value=completed):
with self.assertRaisesRegex(ValueError, "missing AGE ABI export"):
verify_windows_native.verify_bundle(bundle, "objdump")
if __name__ == "__main__":
unittest.main()

View File

@@ -34,6 +34,7 @@ CORE_TESTS = (
"test_package_linux_x64.py",
"test_dotnet_publish_proxy.py",
"test_publish_gitea_release.py",
"test_verify_windows_native.py",
"test_diff_optrace.py",
"test_engine_ctx.py",
"test_ghidra_handler_map.py",

142
tools/verify_windows_native.py Executable file
View File

@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""Verify the project-owned Windows x64 FFmpeg bundle without executing it."""
from __future__ import annotations
import argparse
import json
import re
import struct
import subprocess
import sys
from pathlib import Path
from typing import Any
SHIM = "age_movie_ffmpeg.dll"
RUNTIME_DLLS = (
"avformat-62.dll",
"avcodec-62.dll",
"avutil-60.dll",
"swscale-9.dll",
"swresample-6.dll",
)
REQUIRED_FILES = (SHIM, *RUNTIME_DLLS, "FFmpeg-LICENSE.txt")
REQUIRED_EXPORTS = frozenset({
"age_movie_abi_version",
"age_movie_open",
"age_movie_seek",
"age_movie_decode_video",
"age_movie_decode_audio",
"age_movie_last_error",
"age_movie_close",
})
FORBIDDEN_RUNTIME_IMPORTS = frozenset({"cygwin1.dll", "msys-2.0.dll"})
PE_MACHINE_AMD64 = 0x8664
def pe_machine(path: Path) -> int:
with path.open("rb") as stream:
header = stream.read(64)
if len(header) != 64 or header[:2] != b"MZ":
raise ValueError(f"file has no DOS/PE header: {path}")
pe_offset = struct.unpack_from("<I", header, 0x3C)[0]
stream.seek(pe_offset)
pe_header = stream.read(6)
if len(pe_header) != 6 or pe_header[:4] != b"PE\0\0":
raise ValueError(f"file has no PE signature: {path}")
return struct.unpack_from("<H", pe_header, 4)[0]
def parse_objdump(output: str) -> tuple[set[str], set[str]]:
imports = {
match.group(1).strip()
for match in re.finditer(r"^\s*DLL Name:\s*(\S+)\s*$", output, re.MULTILINE)
}
exports = {
name for name in REQUIRED_EXPORTS
if re.search(rf"(?<![A-Za-z0-9_]){re.escape(name)}(?![A-Za-z0-9_])", output)
}
return imports, exports
def verify_bundle(bundle: Path, objdump: str) -> dict[str, Any]:
bundle = bundle.resolve()
if not bundle.is_dir():
raise ValueError(f"Windows native bundle directory was not found: {bundle}")
missing = [name for name in REQUIRED_FILES if not (bundle / name).is_file()]
if missing:
raise ValueError("Windows native bundle is incomplete; missing: " + ", ".join(missing))
expected_dlls = {name.lower() for name in (SHIM, *RUNTIME_DLLS)}
unexpected_dlls = sorted(
path.name
for path in bundle.iterdir()
if path.is_file() and path.suffix.lower() == ".dll" and path.name.lower() not in expected_dlls
)
if unexpected_dlls:
raise ValueError("Windows native bundle has unexpected DLLs: " + ", ".join(unexpected_dlls))
machines: dict[str, str] = {}
for name in (SHIM, *RUNTIME_DLLS):
machine = pe_machine(bundle / name)
if machine != PE_MACHINE_AMD64:
raise ValueError(f"Windows native file is not AMD64 PE (0x{machine:04x}): {name}")
machines[name] = "AMD64"
process = subprocess.run(
(objdump, "-p", str(bundle / SHIM)),
check=False,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
if process.returncode != 0:
raise RuntimeError(f"objdump failed for {SHIM}: {process.stderr.strip()}")
imports, exports = parse_objdump(process.stdout)
import_names = {name.lower() for name in imports}
missing_imports = sorted(name for name in RUNTIME_DLLS if name.lower() not in import_names)
if missing_imports:
raise ValueError("Windows shim is missing FFmpeg imports: " + ", ".join(missing_imports))
forbidden_imports = sorted(import_names & FORBIDDEN_RUNTIME_IMPORTS)
if forbidden_imports:
raise ValueError("Windows shim imports a non-native compatibility runtime: " + ", ".join(forbidden_imports))
missing_exports = sorted(REQUIRED_EXPORTS - exports)
if missing_exports:
raise ValueError("Windows shim is missing AGE ABI exports: " + ", ".join(missing_exports))
return {
"schema_version": 1,
"target": "win-x64",
"bundle": str(bundle),
"machines": machines,
"shim_imports": sorted(imports, key=str.lower),
"shim_exports": sorted(exports),
}
def main(arguments: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("bundle", type=Path)
parser.add_argument("--objdump", default="x86_64-w64-mingw32-objdump")
parser.add_argument("--report", type=Path)
args = parser.parse_args(arguments)
report = verify_bundle(args.bundle, args.objdump)
rendered = json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
if args.report is not None:
args.report.parent.mkdir(parents=True, exist_ok=True)
with args.report.open("w", encoding="utf-8", newline="\n") as stream:
stream.write(rendered)
print(
f"WINDOWS NATIVE OK: dlls={len(report['machines'])} "
f"exports={len(report['shim_exports'])} ffmpeg-imports={len(RUNTIME_DLLS)}"
)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (OSError, RuntimeError, ValueError) as error:
print(f"Windows native verification failed: {error}", file=sys.stderr)
raise SystemExit(1)