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

@@ -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 │ ├── build-linux-x64.sh one-command pinned Linux export/package/smoke pipeline
│ ├── dotnet_publish_proxy.py guarded low-memory Godot export-time publish staging │ ├── dotnet_publish_proxy.py guarded low-memory Godot export-time publish staging
│ ├── publish_gitea_release.py guarded/retry-safe native Gitea release promotion │ ├── 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 │ ├── 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 │ ├── godot-linux-x64.json immutable Godot editor/template source + SHA-256 manifest
│ ├── install_godot_templates.py selective HTTP-range installer for pinned template members │ ├── install_godot_templates.py selective HTTP-range installer for pinned template members
│ ├── package_linux_x64.py export verifier + notice/checksum/archive packager │ ├── package_linux_x64.py export verifier + notice/checksum/archive packager
│ ├── test_install_godot_templates.py, test_package_linux_x64.py, │ ├── test_install_godot_templates.py, test_package_linux_x64.py,
│ │ test_dotnet_publish_proxy.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) │ ├── sys4load.py loader + disassembler (opcode-decoding)
│ ├── age_opcodes.py GENERATED complete Python ABI view (do not hand-edit) │ ├── 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 │ ├── 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 ├── build/ DERIVED (our-work-side) — generated by tools/; disposable
│ ├── downloads/, toolchains/ immutable dependency caches and repo-local Godot toolchain │ ├── downloads/, toolchains/ immutable dependency caches and repo-local Godot toolchain
│ ├── export/linux-x64/ verified loose Godot Linux export │ ├── 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 │ ├── package/ staged release tree, smoke log, and `.tar.gz` artifact
│ ├── disasm/ <NAME>.asm — human-readable disassembly, one per script │ ├── disasm/ <NAME>.asm — human-readable disassembly, one per script
│ ├── text/ extracted text: │ ├── text/ extracted text:
@@ -145,7 +148,8 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings)
│ outside the platform-neutral VM/runtime core │ outside the platform-neutral VM/runtime core
├── native/ authored native runtime boundaries ├── native/ authored native runtime boundaries
│ └── age_movie_ffmpeg/ project-owned FFmpeg C ABI, immutable Windows/Linux dependency manifests, │ └── 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) ├── tools/frida/ runtime-capture + engine-dump scripts (see tools/frida/README.md)
└── godot/ DELIVERABLE — the Godot/C# ADV front-end (references Age.Engine └── godot/ DELIVERABLE — the Godot/C# ADV front-end (references Age.Engine
plus the optional exact Windows text adapter). Its assembly embeds plus the optional exact Windows text adapter). Its assembly embeds

View File

@@ -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 `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. 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 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, 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 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 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 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 `AGE_FFMPEG_NATIVE_DIR` for the isolated gate and otherwise reserves application-local and
`runtimes/<rid>/native` lookup. Representative VFS results are `MVB961` 280x352/500 ms, `MVB238` `runtimes/<rid>/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 280x352/866 ms, `MVB908` 400x400/333 ms, and `CHAPTER` 800x600/12016 ms, all with changing frames and

View File

@@ -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 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. 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 **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 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 retains its dynamic packaged smoke, Windows passes all structural PE/payload gates, and neither platform job

View File

@@ -279,14 +279,17 @@ send either the `step-limit` console block or the generated JSON.
## Native FFmpeg movie shim (Windows and Linux x64) ## Native FFmpeg movie shim (Windows and Linux x64)
The target-specific dependency manifests pin immutable LGPL shared FFmpeg archives and SHA-256 hashes. The The target-specific dependency manifests pin immutable LGPL shared FFmpeg archives and SHA-256 hashes. Windows
Windows tools use PowerShell/MSVC; the Linux tools run in Bash with Python 3, curl, a C compiler, binutils, and development can use PowerShell/MSVC. Linux-hosted builds use Bash with Python 3, curl, MinGW-w64/binutils for the
the ordinary archive/core utilities available. Windows target, a native C compiler/binutils for Linux, and the ordinary archive/core utilities available.
| Tool | Purpose | Run | Reads → Writes | | 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 <dir>]` | `dependency-win64.json`, network/archive cache → disposable `build/downloads/`, `build/ffmpeg-sdk/` | | `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 <dir>]` | `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 <bootstrap-output> [-OutputDirectory <dir>]` | C ABI source + FFmpeg SDK → disposable `build/native/win-x64/` by default | | `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 <bootstrap-output> [-OutputDirectory <dir>]` | 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 <bundle> [--objdump <MinGW-objdump>] [--report <json>]` | 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/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 | | `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 <dir>` | `SYS4INI.BIN` + loose/ALF VFS assets + staged FFmpeg shim → stdout progress + disposable `build/movie-corpus-ffmpeg.json` | | `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 <dir>` | `SYS4INI.BIN` + loose/ALF VFS assets + staged FFmpeg shim → stdout progress + disposable `build/movie-corpus-ffmpeg.json` |

View File

@@ -23,10 +23,10 @@ if ($actualHash -ne $manifest.sha256) {
if (-not (Test-Path -LiteralPath $extractRoot)) { if (-not (Test-Path -LiteralPath $extractRoot)) {
Expand-Archive -LiteralPath $archivePath -DestinationPath $Destination Expand-Archive -LiteralPath $archivePath -DestinationPath $Destination
} }
$sdkRoot = Get-ChildItem -LiteralPath $Destination -Directory | $sdkRoot = $extractRoot
Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'include\libavformat\avformat.h') } | if (-not (Test-Path -LiteralPath (Join-Path $sdkRoot 'include\libavformat\avformat.h'))) {
Select-Object -First 1 -ExpandProperty FullName throw "Pinned FFmpeg SDK was not found at $sdkRoot"
if (-not $sdkRoot) { throw "FFmpeg SDK was not found under $Destination" } }
$reported = & (Join-Path $sdkRoot 'bin\ffmpeg.exe') -version | Select-Object -First 1 $reported = & (Join-Path $sdkRoot 'bin\ffmpeg.exe') -version | Select-Object -First 1
$expectedVersion = $manifest.ffmpeg_version -replace '-20260721$', '' $expectedVersion = $manifest.ffmpeg_version -replace '-20260721$', ''
if ($reported -notlike "*$expectedVersion*" ) { if ($reported -notlike "*$expectedVersion*" ) {

View File

@@ -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"

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 1 || $# -gt 2 ]]; then
echo "usage: $0 <ffmpeg-sdk-root> [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"

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_package_linux_x64.py",
"test_dotnet_publish_proxy.py", "test_dotnet_publish_proxy.py",
"test_publish_gitea_release.py", "test_publish_gitea_release.py",
"test_verify_windows_native.py",
"test_diff_optrace.py", "test_diff_optrace.py",
"test_engine_ctx.py", "test_engine_ctx.py",
"test_ghidra_handler_map.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)