Cross-build Windows native bundle on Linux
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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` |
|
||||||
|
|||||||
@@ -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*" ) {
|
||||||
|
|||||||
59
native/age_movie_ffmpeg/bootstrap-win64.sh
Executable file
59
native/age_movie_ffmpeg/bootstrap-win64.sh
Executable 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"
|
||||||
73
native/age_movie_ffmpeg/build-win64.sh
Executable file
73
native/age_movie_ffmpeg/build-win64.sh
Executable 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"
|
||||||
82
tools/test_verify_windows_native.py
Executable file
82
tools/test_verify_windows_native.py
Executable 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()
|
||||||
@@ -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
142
tools/verify_windows_native.py
Executable 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)
|
||||||
Reference in New Issue
Block a user