Compare commits
4 Commits
f0f5f12ffc
...
60ac6fc64f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60ac6fc64f | ||
|
|
304b290a02 | ||
|
|
cc65e2cb61 | ||
|
|
5fe3cd669c |
@@ -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/ <NAME>.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
|
||||
|
||||
@@ -77,7 +77,59 @@ branch and manual runs never receive release authority, and no personal secret o
|
||||
introduced. Promotion rechecks the clean source commit, archive checksum, and accepted smoke result after
|
||||
artifact download. It is retry-safe but fail-closed: a matching partial release can receive missing assets,
|
||||
while mismatched release identity or a same-name/different-size asset is never edited, deleted, or overwritten.
|
||||
The next acceptance gate is a deliberate first version tag and inspection of its five release attachments.
|
||||
The first hosted develop run containing promotion completed successfully at `f0f5f12` on 2026-08-03 and Gitea
|
||||
reported the tag-only job as skipped, accepting the non-tag permission boundary. The remaining acceptance gate
|
||||
was a deliberate first version tag and inspection of its five release attachments. That gate passed with the
|
||||
lightweight `v0.1.0` tag at `5fe3cd6` on 2026-08-03: the tag build and promotion job completed successfully, and
|
||||
the Gitea release carries the archive, archive checksum, build metadata, payload ledger, and smoke log. The Linux
|
||||
CI/CD path is therefore accepted end to end.
|
||||
|
||||
## Planned Windows x64 CI/CD artifact
|
||||
|
||||
The Windows artifact will be cross-built on the existing Linux runner; neither a Windows runner nor Wine is part
|
||||
of its build contract. The .NET SDK publishes `win-x64`, the Linux Godot editor exports through the pinned Windows
|
||||
x86-64 template, and MinGW-w64 compiles the one project-owned native component. The pinned BtbN Windows FFmpeg
|
||||
SDK already carries the required headers, runtime DLLs, and MinGW `.dll.a` import libraries. The conditional
|
||||
`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
|
||||
exact self-contained `ExportRelease/win-x64` publish, and run the real Godot Windows export. The low-memory publish
|
||||
proxy will become target-parameterized rather than duplicating its fail-closed project/configuration/RID/output
|
||||
checks. The accepted Linux path must remain byte- and behavior-compatible throughout this extension.
|
||||
|
||||
CI acceptance is structural because the produced executable will not run on the Linux host. The verifier must
|
||||
require the Windows executable/PCK/self-contained managed directory, `Age.Engine.Text.Windows.dll`, the project
|
||||
shim, all five pinned FFmpeg DLLs, and the FFmpeg license; reject Linux shared objects and incomplete/cross-target
|
||||
payloads; and inspect every project-owned PE image as AMD64. The portable ZIP will carry the project license,
|
||||
third-party notices, clean source/dependency metadata, and a complete payload checksum ledger. A manual packaged
|
||||
`--package-smoke` command remains available for real-Windows acceptance, but its result is not fabricated or
|
||||
reported as a hosted CI gate.
|
||||
|
||||
Develop, manual, and tag events will run independent Linux and Windows build jobs, each with read-only repository
|
||||
permission and its own manifest-keyed cache/workflow artifact. Tag promotion will require both jobs, download
|
||||
both already-built archives, verify their commit/build evidence, and publish two distributable release assets:
|
||||
`OpenMaidEngine-Himegari-linux-x64.tar.gz` and `OpenMaidEngine-Himegari-windows-x64.zip`. A third small
|
||||
`RELEASE-SHA256SUMS` attachment will authenticate both archives while keeping the release below Gitea's observed
|
||||
five-attachment boundary; detailed build metadata, payload ledgers, and logs remain inside each archive and in
|
||||
the retained workflow artifacts. The release notes will state that Linux received the dynamic package smoke and
|
||||
Windows received structural cross-target verification only.
|
||||
|
||||
Code signing, an installer, Windows-on-Linux execution, and retroactively modifying the accepted Linux-only
|
||||
`v0.1.0` release are outside this effort. A future signing slice can use `osslsigncode` and protected credentials
|
||||
without changing how the unsigned portable ZIP is assembled.
|
||||
|
||||
## Dependency inventory
|
||||
|
||||
@@ -225,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/<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
|
||||
|
||||
@@ -1025,8 +1025,49 @@ do not mix mechanical moves with semantic changes.
|
||||
smoke result, and fixed archive/evidence set after download. Retries may
|
||||
complete missing attachments on a matching partial release, but mismatches and same-name/different-size
|
||||
collisions fail without edit, deletion, or overwrite. Pure creation/resume/refusal regressions and workflow
|
||||
lint pass locally. The immediate acceptance gate is a deliberately chosen first `v*` tag and inspection of
|
||||
the resulting release/archive/evidence; no tag is created by this slice.
|
||||
lint pass locally. The first hosted develop run containing this job completed successfully at `f0f5f12` on
|
||||
2026-08-03 and the promotion job was skipped, accepting the non-tag permission boundary. The remaining gate
|
||||
then passed with lightweight tag `v0.1.0` at `5fe3cd6`: its hosted build and promotion completed successfully,
|
||||
and the resulting Gitea release exposes all five expected archive/evidence attachments. This accepts the Linux
|
||||
CI/CD path end to end; future version tags use the same build-once/promote-on-success route.
|
||||
|
||||
**Planned Linux-hosted Windows release artifact (2026-08-03):** extend the accepted pipeline without a
|
||||
Windows runner or Wine. The technical target and verification boundary live in `docs/platform-portability.md`.
|
||||
The bounded execution order is:
|
||||
|
||||
1. **MinGW native foundation.** Add Linux-hosted bootstrap/build scripts over the existing pinned Windows
|
||||
FFmpeg manifest and its `.dll.a` import libraries. Produce an AMD64 `age_movie_ffmpeg.dll`, copy the exact
|
||||
five runtime DLLs/license, and verify PE architecture, exported ABI entry points, and FFmpeg imports. Keep
|
||||
the existing MSVC path as the independent Windows-local builder.
|
||||
2. **Cross-export and package contract.** Add the Windows x86-64 Godot preset and selectively pinned template;
|
||||
parameterize the low-memory publish proxy for `win-x64`; generalize package smoke so the downloaded ZIP can
|
||||
be tested manually on Windows; and add one Linux-hosted `build-windows-x64.sh` entry point. A new Windows
|
||||
verifier/packager will reject Linux or incomplete payloads and emit a normalized, licensed, checksummed
|
||||
`OpenMaidEngine-Himegari-windows-x64.zip`. Synthetic package/proxy regressions and a local WSL cross-export
|
||||
are the slice gate; executing the `.exe` is not.
|
||||
3. **Hosted Windows artifact job.** Add a read-only Windows-target job beside the existing Linux job on the
|
||||
same `ubuntu-latest` runner. Provision/report MinGW, cache only hash-verified Godot/FFmpeg inputs, call the
|
||||
accepted Windows build command, and retain the ZIP plus structural verification/build evidence. A develop
|
||||
run must leave tag promotion skipped and keep the existing Linux job green.
|
||||
4. **Dual-platform tag promotion.** Require both build jobs before promotion, download rather than rebuild
|
||||
their outputs, refactor the release helper around two platform archives, and publish those two distributable
|
||||
assets plus one combined archive-checksum attachment. Preserve job-local release authority, retry-safe
|
||||
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
|
||||
receives release-write permission. Signing, installers, Wine, Windows runners, and mutation of `v0.1.0` remain
|
||||
out of scope.
|
||||
|
||||
**Not cleanup targets:** generated `build/` output, the two intentional solution files, historical
|
||||
`docs/superpowers/` plans/specifications, and fidelity-specific complexity that is directly covered by the
|
||||
|
||||
@@ -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 <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/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/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` |
|
||||
|
||||
@@ -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*" ) {
|
||||
|
||||
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_dotnet_publish_proxy.py",
|
||||
"test_publish_gitea_release.py",
|
||||
"test_verify_windows_native.py",
|
||||
"test_diff_optrace.py",
|
||||
"test_engine_ctx.py",
|
||||
"test_ghidra_handler_map.py",
|
||||
|
||||
142
tools/verify_windows_native.py
Executable file
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