From 60ac6fc64feca739617747ab4eb9940f9a326aad Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 3 Aug 2026 21:28:18 -0400 Subject: [PATCH] Cross-build Windows native bundle on Linux --- docs/PROJECT-STRUCTURE.md | 10 +- docs/platform-portability.md | 14 +- docs/remake-architecture-and-roadmap.md | 8 ++ docs/tools-reference.md | 9 +- native/age_movie_ffmpeg/bootstrap-win64.ps1 | 8 +- native/age_movie_ffmpeg/bootstrap-win64.sh | 59 ++++++++ native/age_movie_ffmpeg/build-win64.sh | 73 ++++++++++ tools/test_verify_windows_native.py | 82 +++++++++++ tools/validate.py | 1 + tools/verify_windows_native.py | 142 ++++++++++++++++++++ 10 files changed, 395 insertions(+), 11 deletions(-) create mode 100755 native/age_movie_ffmpeg/bootstrap-win64.sh create mode 100755 native/age_movie_ffmpeg/build-win64.sh create mode 100755 tools/test_verify_windows_native.py create mode 100755 tools/verify_windows_native.py diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md index d74ebb1..e4f7a1a 100644 --- a/docs/PROJECT-STRUCTURE.md +++ b/docs/PROJECT-STRUCTURE.md @@ -49,13 +49,15 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings) │ ├── build-linux-x64.sh one-command pinned Linux export/package/smoke pipeline │ ├── dotnet_publish_proxy.py guarded low-memory Godot export-time publish staging │ ├── publish_gitea_release.py guarded/retry-safe native Gitea release promotion + │ ├── verify_windows_native.py static AMD64 PE/native-bundle contract verifier │ ├── bootstrap-godot-linux-x64.sh repo-local Godot 4.7 Linux editor/template bootstrap │ ├── godot-linux-x64.json immutable Godot editor/template source + SHA-256 manifest │ ├── install_godot_templates.py selective HTTP-range installer for pinned template members │ ├── package_linux_x64.py export verifier + notice/checksum/archive packager │ ├── test_install_godot_templates.py, test_package_linux_x64.py, │ │ test_dotnet_publish_proxy.py, - │ │ test_publish_gitea_release.py source-only bootstrap/package/publish regressions + │ │ test_publish_gitea_release.py, + │ │ test_verify_windows_native.py source-only bootstrap/package/publish/native regressions │ ├── sys4load.py loader + disassembler (opcode-decoding) │ ├── age_opcodes.py GENERATED complete Python ABI view (do not hand-edit) │ ├── opcodes_build.py generator/linter: vm-map/opcodes.toml -> the 5 artifacts below @@ -107,7 +109,8 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings) ├── build/ DERIVED (our-work-side) — generated by tools/; disposable │ ├── downloads/, toolchains/ immutable dependency caches and repo-local Godot toolchain │ ├── export/linux-x64/ verified loose Godot Linux export - │ ├── native/linux-x64/ validated FFmpeg shim/shared-library bundle + │ ├── native/linux-x64/ validated Linux FFmpeg shim/shared-library bundle + │ ├── native/win-x64/ validated Windows FFmpeg shim/DLL bundle │ ├── package/ staged release tree, smoke log, and `.tar.gz` artifact │ ├── disasm/ .asm — human-readable disassembly, one per script │ ├── text/ extracted text: @@ -145,7 +148,8 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings) │ outside the platform-neutral VM/runtime core ├── native/ authored native runtime boundaries │ └── age_movie_ffmpeg/ project-owned FFmpeg C ABI, immutable Windows/Linux dependency manifests, - │ and target bootstrap/build scripts (outputs stay under disposable build/) + │ and native/MSVC plus Linux-hosted MinGW target bootstrap/build scripts + │ (outputs stay under disposable build/) ├── tools/frida/ runtime-capture + engine-dump scripts (see tools/frida/README.md) └── godot/ DELIVERABLE — the Godot/C# ADV front-end (references Age.Engine plus the optional exact Windows text adapter). Its assembly embeds diff --git a/docs/platform-portability.md b/docs/platform-portability.md index 1473541..d997fb7 100644 --- a/docs/platform-portability.md +++ b/docs/platform-portability.md @@ -93,6 +93,16 @@ SDK already carries the required headers, runtime DLLs, and MinGW `.dll.a` impor `Age.Engine.Text.Windows` project contains managed P/Invoke declarations and therefore compiles on Linux without loading GDI. Wine would add only an execution test, which is explicitly outside the requested CI gate. +The MinGW native foundation was completed and locally accepted on 2026-08-03. The Bash bootstrap resolves the +exact manifest-named SDK instead of an arbitrary previously extracted Windows archive; the matching build script +uses MinGW-w64, suppresses the PE timestamp, statically links its GCC support, and stages only the shim, five +pinned FFmpeg DLLs, and LGPL text. The source-only verifier checks all six images as AMD64 PE, requires the seven +AGE movie ABI exports and five FFmpeg imports, and rejects extra DLLs plus Cygwin/MSYS imports without executing +the result. Both the new MinGW output and the existing MSVC output pass that same contract. Two consecutive +MinGW builds produced the identical shim SHA-256 +`9852680ca32bc37200d0b63ad89acb5453ad913bdcf4441a15c8ac9356d4da8c`. This is a compiler-independent contract +comparison, not an expectation that MSVC and MinGW produce byte-identical binaries. + The Windows build command will mirror the accepted Linux entry point while keeping target checks separate. It will regenerate opcode metadata, selectively install the Windows template from the existing pinned Godot TPZ, bootstrap/hash-check the pinned Windows FFmpeg SDK, cross-build and inspect `age_movie_ffmpeg.dll`, perform an @@ -267,7 +277,9 @@ on every future native target rather than relying on a Windows-only DirectShow c The Windows-x64 spike is now complete. `bootstrap-win64.ps1` verifies the immutable archive SHA before extraction, and `build-win64.ps1` builds the shim with MSVC and places the DLL, import artifacts, required LGPL -shared libraries, and license under disposable `build/native/win-x64`. The managed resolver accepts +shared libraries, and license under disposable `build/native/win-x64`. The Linux-hosted +`bootstrap-win64.sh`/`build-win64.sh` path builds the same exported ABI with MinGW and applies the shared static +PE/bundle verifier. The managed resolver accepts `AGE_FFMPEG_NATIVE_DIR` for the isolated gate and otherwise reserves application-local and `runtimes//native` lookup. Representative VFS results are `MVB961` 280x352/500 ms, `MVB238` 280x352/866 ms, `MVB908` 400x400/333 ms, and `CHAPTER` 800x600/12016 ms, all with changing frames and diff --git a/docs/remake-architecture-and-roadmap.md b/docs/remake-architecture-and-roadmap.md index 80df45b..cb4e1c5 100644 --- a/docs/remake-architecture-and-roadmap.md +++ b/docs/remake-architecture-and-roadmap.md @@ -1055,6 +1055,14 @@ do not mix mechanical moves with semantic changes. missing-asset completion, collision refusal, and the existing Gitea attachment ceiling. No version tag is created until the user explicitly selects one after the hosted develop proof. + **Slice 1 completed 2026-08-03:** the Linux-hosted bootstrap and MinGW build now consume the exact pinned + Windows SDK and stage the six-file AMD64 native DLL set plus license. A source-only verifier pins PE machine, + seven AGE ABI exports, five FFmpeg imports, exact bundle membership, and the no-Cygwin/MSYS boundary. Its unit + tests are part of core validation. Both the MinGW bundle and the independent MSVC bundle pass the same + contract, and two timestamp-suppressed MinGW builds produced an identical shim hash. Slice 2 is now the active + next step: Windows Godot template/export, target-parameterized publish proxy, structural package contract, and + normalized ZIP. + **Completion gate:** one deliberately selected future tag produces a single Gitea release containing the Linux `.tar.gz`, Windows `.zip`, and combined archive checksums; both archives bind to the tag commit, Linux retains its dynamic packaged smoke, Windows passes all structural PE/payload gates, and neither platform job diff --git a/docs/tools-reference.md b/docs/tools-reference.md index 75b0a07..7279ccc 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -279,14 +279,17 @@ send either the `step-limit` console block or the generated JSON. ## Native FFmpeg movie shim (Windows and Linux x64) -The target-specific dependency manifests pin immutable LGPL shared FFmpeg archives and SHA-256 hashes. The -Windows tools use PowerShell/MSVC; the Linux tools run in Bash with Python 3, curl, a C compiler, binutils, and -the ordinary archive/core utilities available. +The target-specific dependency manifests pin immutable LGPL shared FFmpeg archives and SHA-256 hashes. Windows +development can use PowerShell/MSVC. Linux-hosted builds use Bash with Python 3, curl, MinGW-w64/binutils for the +Windows target, a native C compiler/binutils for Linux, and the ordinary archive/core utilities available. | Tool | Purpose | Run | Reads → Writes | |---|---|---|---| | `native/age_movie_ffmpeg/bootstrap-win64.ps1` | Download, hash-check, extract, and version-check the pinned FFmpeg SDK. Returns the resolved SDK root. | `.\\native\\age_movie_ffmpeg\\bootstrap-win64.ps1 [-Destination ]` | `dependency-win64.json`, network/archive cache → disposable `build/downloads/`, `build/ffmpeg-sdk/` | | `native/age_movie_ffmpeg/build-win64.ps1` | Discover the MSVC x64 toolchain, build `age_movie_ffmpeg.dll`, and stage its exact shared-library/license dependencies. | `.\\native\\age_movie_ffmpeg\\build-win64.ps1 -SdkRoot [-OutputDirectory ]` | C ABI source + FFmpeg SDK → disposable `build/native/win-x64/` by default | +| `native/age_movie_ffmpeg/bootstrap-win64.sh` | Download, SHA-256-check, and extract the exact pinned Windows FFmpeg SDK, including its MinGW import libraries. Prints the resolved SDK root. | `native/age_movie_ffmpeg/bootstrap-win64.sh [destination]` | `dependency-win64.json`, network/archive cache → disposable `build/downloads/`, `build/ffmpeg-sdk/` | +| `native/age_movie_ffmpeg/build-win64.sh` | Cross-build the AMD64 PE shim with MinGW, stage exactly five FFmpeg DLLs and the license, and run the static native-bundle contract gate. `MINGW_CC` and `MINGW_OBJDUMP` may override the standard tool names. | `sdk_root="$(native/age_movie_ffmpeg/bootstrap-win64.sh)"` then `native/age_movie_ffmpeg/build-win64.sh "$sdk_root" [output-directory]` | C ABI source + Windows FFmpeg SDK → disposable `build/native/win-x64/` by default, including `verification.json` | +| `tools/verify_windows_native.py` | Without executing Windows code, require the exact native bundle, AMD64 PE architecture, all seven ABI exports and five FFmpeg imports, and no Cygwin/MSYS compatibility runtime. Optionally writes the machine-readable verification report used by later packaging. | `python3 -X utf8 tools/verify_windows_native.py [--objdump ] [--report ]` | disposable Windows native bundle → stdout and optional JSON report | | `native/age_movie_ffmpeg/bootstrap-linux-x64.sh` | Download, SHA-256-check, extract, and version-check the pinned Linux x64 FFmpeg SDK. Prints the resolved SDK root. | `native/age_movie_ffmpeg/bootstrap-linux-x64.sh [destination]` | `dependency-linux-x64.json`, network/archive cache → disposable `build/downloads/`, `build/ffmpeg-sdk/` | | `native/age_movie_ffmpeg/build-linux-x64.sh` | Build the ELF64 x86-64 shim; stage the five exact FFmpeg SONAME libraries and license; reject stale `.so` files, unresolved/nonlocal FFmpeg dependencies, a missing `$ORIGIN` runpath, or a glibc requirement newer than the pinned 2.28 baseline. | `sdk_root="$(native/age_movie_ffmpeg/bootstrap-linux-x64.sh)"` then `native/age_movie_ffmpeg/build-linux-x64.sh "$sdk_root" [output-directory]` | C ABI source + FFmpeg SDK → disposable `build/native/linux-x64/` by default | | `tools/movie-corpus-gate` | Discover every MPEG program stream stored under an `.AGF` catalog entry; decode every video frame and, when present, every audio block through the unpaced FFmpeg session; validate independent sequence dimensions, metadata, RGBA size, stereo finite PCM, monotonic video/audio timestamps, EOF, timeout, and teardown; then emit a per-asset JSON report. Audio fields include sample rate/channels, block and PCM-frame counts, first/last PTS, signal presence, and decode time. Exit 0 means the expected corpus count and every asset passed; exit 1 is a gate failure; exit 2 means the native shim is absent. | `dotnet run --project tools/movie-corpus-gate -- --output build/movie-corpus-ffmpeg.json --expected-count 213 --max-item-ms 30000` · optional `--native-dir ` | `SYS4INI.BIN` + loose/ALF VFS assets + staged FFmpeg shim → stdout progress + disposable `build/movie-corpus-ffmpeg.json` | diff --git a/native/age_movie_ffmpeg/bootstrap-win64.ps1 b/native/age_movie_ffmpeg/bootstrap-win64.ps1 index 9a2b6f2..78625fb 100644 --- a/native/age_movie_ffmpeg/bootstrap-win64.ps1 +++ b/native/age_movie_ffmpeg/bootstrap-win64.ps1 @@ -23,10 +23,10 @@ if ($actualHash -ne $manifest.sha256) { if (-not (Test-Path -LiteralPath $extractRoot)) { Expand-Archive -LiteralPath $archivePath -DestinationPath $Destination } -$sdkRoot = Get-ChildItem -LiteralPath $Destination -Directory | - Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'include\libavformat\avformat.h') } | - Select-Object -First 1 -ExpandProperty FullName -if (-not $sdkRoot) { throw "FFmpeg SDK was not found under $Destination" } +$sdkRoot = $extractRoot +if (-not (Test-Path -LiteralPath (Join-Path $sdkRoot 'include\libavformat\avformat.h'))) { + throw "Pinned FFmpeg SDK was not found at $sdkRoot" +} $reported = & (Join-Path $sdkRoot 'bin\ffmpeg.exe') -version | Select-Object -First 1 $expectedVersion = $manifest.ffmpeg_version -replace '-20260721$', '' if ($reported -notlike "*$expectedVersion*" ) { diff --git a/native/age_movie_ffmpeg/bootstrap-win64.sh b/native/age_movie_ffmpeg/bootstrap-win64.sh new file mode 100755 index 0000000..c29252f --- /dev/null +++ b/native/age_movie_ffmpeg/bootstrap-win64.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "$script_dir/../.." && pwd)" +destination="${1:-$repo_root/build/ffmpeg-sdk}" +manifest="$script_dir/dependency-win64.json" + +manifest_value() { + python3 -c 'import json, sys; print(json.load(open(sys.argv[1], encoding="utf-8"))[sys.argv[2]])' \ + "$manifest" "$1" +} + +for command in python3 curl sha256sum awk; do + command -v "$command" >/dev/null 2>&1 || { + echo "required command was not found: $command" >&2 + exit 1 + } +done + +archive="$(manifest_value archive)" +url="$(manifest_value url)" +expected_hash="$(manifest_value sha256)" +archive_root="${archive%.zip}" +download_directory="$repo_root/build/downloads" +archive_path="$download_directory/$archive" +sdk_root="$destination/$archive_root" + +mkdir -p -- "$download_directory" "$destination" +if [[ ! -f "$archive_path" ]]; then + curl --fail --location --retry 3 --output "$archive_path" "$url" +fi + +actual_hash="$(sha256sum "$archive_path" | awk '{ print $1 }')" +if [[ "$actual_hash" != "$expected_hash" ]]; then + echo "FFmpeg archive SHA-256 mismatch: expected $expected_hash, got $actual_hash" >&2 + exit 1 +fi + +if [[ ! -d "$sdk_root" ]]; then + python3 -m zipfile -e "$archive_path" "$destination" +fi + +for required in \ + "$sdk_root/include/libavformat/avformat.h" \ + "$sdk_root/lib/libavformat.dll.a" \ + "$sdk_root/lib/libavcodec.dll.a" \ + "$sdk_root/lib/libavutil.dll.a" \ + "$sdk_root/lib/libswscale.dll.a" \ + "$sdk_root/lib/libswresample.dll.a" \ + "$sdk_root/bin/ffmpeg.exe" \ + "$sdk_root/LICENSE.txt"; do + if [[ ! -e "$required" ]]; then + echo "FFmpeg SDK file was not found after extraction: $required" >&2 + exit 1 + fi +done + +printf '%s\n' "$sdk_root" diff --git a/native/age_movie_ffmpeg/build-win64.sh b/native/age_movie_ffmpeg/build-win64.sh new file mode 100755 index 0000000..7344008 --- /dev/null +++ b/native/age_movie_ffmpeg/build-win64.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -lt 1 || $# -gt 2 ]]; then + echo "usage: $0 [output-directory]" >&2 + exit 2 +fi + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "$script_dir/../.." && pwd)" +sdk_root="$(cd -- "$1" && pwd)" +output_directory_input="${2:-$repo_root/build/native/win-x64}" +mkdir -p -- "$output_directory_input" +output_directory="$(cd -- "$output_directory_input" && pwd)" +include_directory="$sdk_root/include" +library_directory="$sdk_root/lib" +binary_directory="$sdk_root/bin" +compiler="${MINGW_CC:-x86_64-w64-mingw32-gcc}" +objdump="${MINGW_OBJDUMP:-x86_64-w64-mingw32-objdump}" +shim="$output_directory/age_movie_ffmpeg.dll" + +for command in "$compiler" "$objdump" python3; do + command -v "$command" >/dev/null 2>&1 || { + echo "required command was not found: $command" >&2 + exit 1 + } +done + +for required in \ + "$include_directory/libavformat/avformat.h" \ + "$library_directory/libavformat.dll.a" \ + "$library_directory/libavcodec.dll.a" \ + "$library_directory/libavutil.dll.a" \ + "$library_directory/libswscale.dll.a" \ + "$library_directory/libswresample.dll.a" \ + "$sdk_root/LICENSE.txt"; do + if [[ ! -e "$required" ]]; then + echo "missing FFmpeg SDK file: $required" >&2 + exit 1 + fi +done + +"$compiler" \ + -std=c11 -O2 -Wall -Wextra -static-libgcc \ + -I "$include_directory" \ + -shared -Wl,--no-undefined -Wl,--no-insert-timestamp \ + -o "$shim" "$script_dir/age_movie.c" \ + -L "$library_directory" \ + -lavformat -lavcodec -lavutil -lswscale -lswresample + +runtime_libraries=( + avformat-62.dll + avcodec-62.dll + avutil-60.dll + swscale-9.dll + swresample-6.dll +) +for runtime in "${runtime_libraries[@]}"; do + source_library="$binary_directory/$runtime" + if [[ ! -f "$source_library" ]]; then + echo "missing FFmpeg runtime library: $source_library" >&2 + exit 1 + fi + cp -- "$source_library" "$output_directory/$runtime" +done +cp -- "$sdk_root/LICENSE.txt" "$output_directory/FFmpeg-LICENSE.txt" + +python3 -X utf8 "$repo_root/tools/verify_windows_native.py" \ + "$output_directory" \ + --objdump "$objdump" \ + --report "$output_directory/verification.json" + +printf '%s\n' "$shim" diff --git a/tools/test_verify_windows_native.py b/tools/test_verify_windows_native.py new file mode 100755 index 0000000..60ba062 --- /dev/null +++ b/tools/test_verify_windows_native.py @@ -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(" 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() diff --git a/tools/validate.py b/tools/validate.py index 7b73e33..a201ddb 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -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", diff --git a/tools/verify_windows_native.py b/tools/verify_windows_native.py new file mode 100755 index 0000000..8b85723 --- /dev/null +++ b/tools/verify_windows_native.py @@ -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(" 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"(? 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)