Include a self-contained Windows build and portable profiles
This commit is contained in:
1 parent
64e59fda1f
commit
1b52466f4c
20 files changed
+729
-34
No files matched your search
+3
-1
@@ -1,9 +1,11 @@
|
||||
# Preserve the catalog and packaged Linux runtime byte for byte on checkout.
|
||||
# Preserve the catalog and packaged runtimes byte for byte on checkout.
|
||||
/data/** -text -diff
|
||||
/bin/** -text -diff
|
||||
/lib/** -text -diff
|
||||
/mods/** -text
|
||||
/data/INDEX diff
|
||||
/data/hd/hd.json diff
|
||||
|
||||
# The launcher must retain Unix line endings.
|
||||
*.sh text eol=lf
|
||||
*.cmd text eol=crlf
|
||||
@@ -0,0 +1,76 @@
|
||||
name: Windows build and validation
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- "src/**"
|
||||
- "tests/**"
|
||||
- "mods/**"
|
||||
- "data/**"
|
||||
- "art/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- "run.cmd"
|
||||
- "runtime-manifest.json"
|
||||
- ".gitattributes"
|
||||
- "tools/assemble_windows.py"
|
||||
- "tools/assemble_runtime.py"
|
||||
- ".github/workflows/windows.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install the pinned Rust toolchain
|
||||
run: |
|
||||
rustup toolchain install 1.99.0 --profile minimal
|
||||
rustup default 1.99.0
|
||||
- name: Download and verify the official SDL2 SDK
|
||||
shell: pwsh
|
||||
run: |
|
||||
$url = "https://github.com/libsdl-org/SDL/releases/download/release-2.32.10/SDL2-devel-2.32.10-VC.zip"
|
||||
Invoke-WebRequest $url -OutFile "$env:RUNNER_TEMP\SDL2.zip"
|
||||
$expected = "af347939395a58b365846aaea27391e69f9ec9d4dd650d6ac40802159b418a6e"
|
||||
if ((Get-FileHash "$env:RUNNER_TEMP\SDL2.zip" -Algorithm SHA256).Hash.ToLower() -ne $expected) {
|
||||
throw "SDL2 SDK checksum mismatch"
|
||||
}
|
||||
Expand-Archive "$env:RUNNER_TEMP\SDL2.zip" "$env:RUNNER_TEMP\sdl2"
|
||||
- name: Run portable simulation, mod and Windows save tests
|
||||
run: cargo test --locked --no-default-features
|
||||
- name: Build and package Windows desktop
|
||||
run: python tools/assemble_windows.py --target x86_64-pc-windows-msvc --sdl2-dir "$env:RUNNER_TEMP\sdl2\SDL2-2.32.10" --zip dist/RustRaptor-windows-x86_64.zip
|
||||
- name: Validate packaged data, HD artwork, mods and SDL gameplay
|
||||
shell: pwsh
|
||||
run: |
|
||||
$portable = "$env:RUNNER_TEMP\Portable Windows Ω folder"
|
||||
Expand-Archive "$env:GITHUB_WORKSPACE\dist\RustRaptor-windows-x86_64.zip" $portable
|
||||
$package = "$portable\RustRaptor"
|
||||
$exe = "$package\bin\raptor-rust.exe"
|
||||
Set-Location $env:RUNNER_TEMP
|
||||
& "$package\run.cmd" --verify-assets
|
||||
if ($LASTEXITCODE -ne 0) { throw "Catalog validation failed" }
|
||||
& $exe --validate-hd
|
||||
if ($LASTEXITCODE -ne 0) { throw "HD validation failed" }
|
||||
$mods = (Get-ChildItem "$package\mods" -Directory | Where-Object { Test-Path "$($_.FullName)\mod.toml" } | ForEach-Object { $_.Name }) -join ","
|
||||
& $exe --mods "$package\mods" --profile-root "$env:RUNNER_TEMP\raptor-profile" --validate-mods $mods
|
||||
if ($LASTEXITCODE -ne 0) { throw "Mod validation failed" }
|
||||
$env:SDL_VIDEODRIVER = "dummy"
|
||||
$env:SDL_AUDIODRIVER = "dummy"
|
||||
& $exe --profile-root "$env:RUNNER_TEMP\raptor-profile" --smoke-frames 32 --preview-mission base:sector_1_wave_1 --screenshot "$env:GITHUB_WORKSPACE\dist\windows-gameplay.png"
|
||||
if ($LASTEXITCODE -ne 0) { throw "Windows SDL gameplay failed" }
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: RustRaptor-windows-x86_64
|
||||
path: |
|
||||
dist/RustRaptor-windows-x86_64.zip
|
||||
dist/windows-gameplay.png
|
||||
if-no-files-found: error
|
||||
@@ -1,6 +1,6 @@
|
||||
# RustRaptor
|
||||
|
||||
The independent Linux Rust game and its complete local mod packages live here.
|
||||
The independent Linux and Windows Rust game and its complete local mod packages live here.
|
||||
SDL2 handles platform services; gameplay, asset decoding, rendering, audio,
|
||||
saving and mod scripting run in Rust.
|
||||
|
||||
@@ -16,6 +16,9 @@ and the bundled `mods/`. `./run.sh` launches that executable without Cargo or
|
||||
the old engine; paths remain valid after moving the folder. For development,
|
||||
the launcher falls back to Cargo when no packaged executable is present.
|
||||
`--data DIR` selects another catalog. Some optional comparison tests use retained source.
|
||||
On Windows, double-click `run.cmd`; it starts the included
|
||||
`bin/raptor-rust.exe` with its adjacent SDL2 DLL and the same data and mods.
|
||||
See [Windows builds and packaging](docs/windows-build.md).
|
||||
|
||||
Refresh the executable and copied original/approved-HD data with
|
||||
`python3 tools/assemble_runtime.py`. Add `--zip dist/RustRaptor-linux-x86_64.zip`
|
||||
@@ -33,8 +36,9 @@ weapon mods. Enable them in Mods and buy them in the supply room. The cannon
|
||||
is a selected special that chains through nearby targets; the shield fires
|
||||
with your primary guns while Fire is held. Both ship SD/HD icons and sounds.
|
||||
For a user mods folder, pass `--mods DIR` or create
|
||||
`$XDG_DATA_HOME/RaptorReplica/mods/`. Saves and configuration use separate XDG
|
||||
`RaptorReplica` directories, with read-only legacy import.
|
||||
`$XDG_DATA_HOME/RaptorReplica/mods/` on Linux or
|
||||
`%APPDATA%\RaptorReplica\mods\` on Windows. Saves and configuration use separate
|
||||
user profile directories, with read-only legacy import.
|
||||
|
||||
Enter the **Hangar** to fly, choose missions, visit the supply room, manage
|
||||
equipment or **Save pilot**. New pilots have editable names, callsigns and ID
|
||||
@@ -61,7 +65,7 @@ Existing untouched Rust default bindings migrate to these controls.
|
||||
| `tests/` | Portable fixtures and opt-in original-data checks |
|
||||
| `tools/` | Replay comparator, content-table generator and asset reference tools |
|
||||
| `docs/` | API, artwork, build and validation documentation |
|
||||
| `bin/`, `data/`, `lib/` | Local standalone runtime and any bundled SDL2 library |
|
||||
| `bin/`, `data/`, `lib/` | Included Linux/Windows executables, shared data and SDL2 runtimes |
|
||||
| `target/` | Local Cargo builds, excluded from Git |
|
||||
|
||||
The bundled executable, libraries, original game catalog and approved HD assets
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
|
||||
Executable
BIN
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
+1
-1
@@ -52,7 +52,7 @@ common_tools = "^1.2"
|
||||
|
||||
IDs use lowercase ASCII letters, digits, `_` and `-`, with a maximum of 64 bytes. `base` is reserved. Package versions and dependency requirements use semantic version syntax. The in-game manager selects compatible installed dependencies when enabling a package. It never downloads or installs missing packages. The CLI still requires a complete explicit `--enabled` set. Cycles, missing packages, unsupported API versions, incompatible dependency versions and declared conflicts reject activation. Two installed folders claiming the same ID make that ID unusable; rename or remove one folder, then rescan.
|
||||
|
||||
All package paths are relative. Absolute paths, parent traversal, symlinks and special files are rejected. Packages are limited to 10,000 files, 16 directory levels, 64 MiB per file and 256 MiB in total. Manifests are limited to 64 KiB; display names to 128 bytes; descriptions to 4 KiB; script declarations to 64, with a 1 MiB limit per source file. Every file, including artwork and documentation, contributes to the package SHA-256 hash. A package changed after discovery requires rescanning before it can be applied.
|
||||
All package paths are relative. Absolute paths, parent traversal, symlinks and special files are rejected. Filenames must be UTF-8 and cannot contain literal backslashes or colons. Hashes use forward-slash paths and UTF-8 component ordering on both Linux and Windows, preserving existing Linux save pins. Keep the original file bytes when copying packages between systems; the repository disables line-ending conversion for mods. Packages are limited to 10,000 files, 16 directory levels, 64 MiB per file and 256 MiB in total. Manifests are limited to 64 KiB; display names to 128 bytes; descriptions to 4 KiB; script declarations to 64, with a 1 MiB limit per source file. Every file, including artwork and documentation, contributes to the package SHA-256 hash. A package changed after discovery requires rescanning before it can be applied.
|
||||
|
||||
## Content definitions and overrides
|
||||
|
||||
|
||||
+4
-3
@@ -5,11 +5,12 @@ Cargo commands from this folder. The public remote is
|
||||
[Quintine/RustRaptor](https://github.com/Quintine/RustRaptor), on branch
|
||||
`codex/rust-rewrite`.
|
||||
|
||||
The repository includes the Linux executable in `bin/`, its bundled libraries
|
||||
in `lib/`, the complete original game catalog and approved HD snapshot in
|
||||
The repository includes Linux and Windows executables in `bin/`, the Windows
|
||||
SDL2 DLL beside its executable, Linux libraries in `lib/`, and the complete
|
||||
original game catalog and approved HD snapshot in
|
||||
`data/`, and `runtime-manifest.json`. All authored mod definitions, scripts,
|
||||
SD/HD artwork, sounds and music are included. A fresh checkout can launch the
|
||||
bundled game with `./run.sh` without compiling it.
|
||||
bundled game with `./run.sh` on Linux or `run.cmd` on Windows without compiling it.
|
||||
|
||||
Build output in `target/`, local review evidence in `.reference/`, generation
|
||||
source references and duplicate zip exports in `dist/` are ignored. Working
|
||||
|
||||
@@ -24,9 +24,18 @@ the relocation. See [repository layout](repository.md).
|
||||
|
||||
## Engine and package
|
||||
|
||||
- Windows support is now included: `bin/raptor-rust.exe`, adjacent `SDL2.dll`,
|
||||
`run.cmd`, `runtime-windows-manifest.json` and the Windows export tool/CI.
|
||||
The GNU executable SHA-256 is
|
||||
`e414d2df745369115082439414099f528aca8457f9c58d1c1745eb1b511fc484`.
|
||||
All 279 portable tests pass under Wine, including Windows save replacement;
|
||||
all 22 mod hashes match existing Linux save pins. Windows paths respect
|
||||
APPDATA/LOCALAPPDATA and mods preserve exact bytes through Git checkouts.
|
||||
See [Windows build and validation](windows-build.md). The tracked catalog
|
||||
remains the 340-frame reviewed snapshot while broader HD review continues.
|
||||
- Packaged executable now includes the `c0a6fd3` reviewed source checkpoint,
|
||||
including diagnostics navigation and interpolated Lightning effects. Its
|
||||
SHA-256 is `4b164987d9fabf7388d2fc66168a9f2aedc71392d42a6ce4066e3607684b1479`.
|
||||
SHA-256 is `4b4304f625381cbbf1331e126c62ca0913d49a52e7a35191f12e734067660e66`.
|
||||
The original `fdf2ff0` checkpoint supplied standalone RustRaptor, widescreen
|
||||
menus and always-4:3 original cinematics, including when gameplay stretching
|
||||
is selected. Fresh 1080p intro capture confirms black 240-pixel side borders.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Standalone Linux package
|
||||
# Standalone game packages
|
||||
|
||||
`RustRaptor/` contains the Rust executable, its original data catalog, approved
|
||||
HD replacements, authored mods, SD/HD mod artwork, sound effects and music.
|
||||
Normal play needs no C++ engine, Cargo, Python or source checkout. Run
|
||||
`./run.sh`; installed pilots and configuration remain in the user's separate
|
||||
`./run.sh` on Linux or `run.cmd` on Windows; installed pilots and configuration remain in the user's separate
|
||||
`RaptorReplica` profile directories.
|
||||
|
||||
The executable finds its package from `bin/raptor-rust`, including when launched
|
||||
@@ -27,8 +27,10 @@ dependencies. Unapproved HD drafts, reference screenshots and the Cargo build
|
||||
cache are excluded from the zip. The exported `RustRaptor` folder can be moved
|
||||
or unpacked anywhere, including paths containing spaces.
|
||||
|
||||
The binary targets Linux x86_64. It still uses the operating system's C runtime
|
||||
and desktop/audio drivers; this package is not a Windows or macOS executable.
|
||||
The Linux binary targets Linux x86_64 and uses the operating system's C runtime
|
||||
and desktop/audio drivers. The included Windows x86_64 executable and SDL2 DLL
|
||||
use the same data and mods. See [Windows packaging](windows-build.md)
|
||||
for its separate launcher, build tool, manifest, zip and validation.
|
||||
Source, Cargo.lock, code licenses and mod API documentation are included.
|
||||
The packaged executable, runtime libraries, original catalog and approved HD
|
||||
artwork are tracked in Git, along with the assembler and authored mod images
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Windows build
|
||||
|
||||
The repository includes `bin/raptor-rust.exe`, `bin/SDL2.dll` and `run.cmd`.
|
||||
Double-click `run.cmd` after extracting the entire `RustRaptor` folder.
|
||||
Windows and Linux share the same `data/`, `art/` and `mods/` directories;
|
||||
the Windows launcher needs no Rust, Python, SDL installation or old engine.
|
||||
Use a 64-bit Windows 10 or Windows 11 machine.
|
||||
|
||||
Windows pilots, settings and user mods live in `%APPDATA%\RaptorReplica`.
|
||||
Derived artwork and audio caches use `%LOCALAPPDATA%\RaptorReplica\cache`.
|
||||
Redirected folders are respected; `%USERPROFILE%\AppData` is a fallback.
|
||||
Legacy import reads `%APPDATA%\Raptor`. Moving the game leaves profiles intact.
|
||||
|
||||
`runtime-windows-manifest.json` records executable, DLL, license and launcher
|
||||
hashes and checked PE dependencies. The shared `runtime-manifest.json`
|
||||
records every original/approved-HD data-file hash. The Windows exporter
|
||||
excludes Linux libraries, build caches, local toolchains, review drafts and Git.
|
||||
|
||||
## Rebuild
|
||||
|
||||
The included build uses Rust 1.99.0, `x86_64-pc-windows-gnu` and the official
|
||||
SDL2 2.32.10 MinGW SDK. Native Windows CI builds the same engine with MSVC and
|
||||
statically links its C runtime.
|
||||
|
||||
On Linux with Rust's Windows target and MinGW-w64 installed:
|
||||
|
||||
```sh
|
||||
rustup target add x86_64-pc-windows-gnu
|
||||
python3 tools/assemble_windows.py \
|
||||
--sdl2-dir /path/to/SDL2-2.32.10 \
|
||||
--linker x86_64-w64-mingw32-gcc \
|
||||
--zip dist/RustRaptor-windows-x86_64.zip
|
||||
```
|
||||
|
||||
Download the SDK from the [official SDL2 release](https://github.com/libsdl-org/SDL/releases/tag/release-2.32.10).
|
||||
The MinGW tarball SHA256 is
|
||||
`83a5d74012311edc3c0d40ea6faecbe57ad692aa033fa5dc273cc937e3938ff2`.
|
||||
Rust's [Windows GNU target documentation](https://doc.rust-lang.org/rustc/platform-support/windows-gnu.html)
|
||||
describes cross-toolchain requirements.
|
||||
|
||||
On Windows with Rust and Visual Studio C++ build tools:
|
||||
|
||||
```powershell
|
||||
python tools/assemble_windows.py --target x86_64-pc-windows-msvc `
|
||||
--sdl2-dir C:\SDKs\SDL2-2.32.10 `
|
||||
--zip dist\RustRaptor-windows-x86_64.zip
|
||||
```
|
||||
|
||||
Use the Visual C SDK for that command. Its official zip SHA256 is
|
||||
`af347939395a58b365846aaea27391e69f9ec9d4dd650d6ac40802159b418a6e`.
|
||||
The assembler checks architecture, SDL import, complete data hashes and DLL
|
||||
dependencies. Supply `--extra-dll FILE` if another compiler introduces another
|
||||
runtime dependency. Optional `--rustc` and `--sysroot` support an isolated
|
||||
matching cross toolchain without changing the system Rust installation.
|
||||
|
||||
The [Windows workflow](../.github/workflows/windows.yml) runs simulation,
|
||||
modding and save tests on Windows, validates packaged assets and all local
|
||||
mods, captures SDL gameplay and publishes the zip as a workflow artifact.
|
||||
|
||||
## Validation checkpoint: 11 October 2026
|
||||
|
||||
The included GNU executable passed 279 portable tests under Wine 11.19,
|
||||
including Windows save replacement and preserving a locked existing save.
|
||||
All 22 installed mod hashes match their previous Linux hashes exactly.
|
||||
The shared catalog validates 6,449 assets and the current packaged HD snapshot
|
||||
validates all 340 supplied frames. The exporter verifies all 6,593 shared
|
||||
data-file hashes and includes only checked Windows DLL dependencies.
|
||||
|
||||
Wine checks exercise the Windows executable from Linux; native Windows
|
||||
builds and runtime checks are also configured in the workflow above.
|
||||
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
"%~dp0bin\raptor-rust.exe" %*
|
||||
exit /b %errorlevel%
|
||||
@@ -3,7 +3,7 @@
|
||||
"platform": "Linux x86_64",
|
||||
"catalog_entries": 6449,
|
||||
"approved_hd_frames": 340,
|
||||
"binary_sha256": "4b164987d9fabf7388d2fc66168a9f2aedc71392d42a6ce4066e3607684b1479",
|
||||
"binary_sha256": "4b4304f625381cbbf1331e126c62ca0913d49a52e7a35191f12e734067660e66",
|
||||
"data_files": 6593,
|
||||
"data_bytes": 195469368,
|
||||
"files": {
|
||||
@@ -6602,11 +6602,11 @@
|
||||
"data/ui/WINGAME_SWD": "304630a1223419d69723902ffd22c2c4eea0388491e37dfca663fcbe78e99aff"
|
||||
},
|
||||
"system_dependencies": [
|
||||
"\tlinux-vdso.so.1 (0x00007f23a012d000)",
|
||||
"\tlibSDL2-2.0.so.0 => /home/quintine/.local/lib/libSDL2-2.0.so.0 (0x00007f239f400000)",
|
||||
"\tlibgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007f23a00d9000)",
|
||||
"\tlibm.so.6 => /usr/lib/libm.so.6 (0x00007f239f6c9000)",
|
||||
"\tlibc.so.6 => /usr/lib/libc.so.6 (0x00007f239f000000)",
|
||||
"\t/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f23a012f000)"
|
||||
"\tlinux-vdso.so.1 (0x00007fcb5dcd3000)",
|
||||
"\tlibSDL2-2.0.so.0 => /home/quintine/.local/lib/libSDL2-2.0.so.0 (0x00007fcb5ce00000)",
|
||||
"\tlibgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007fcb5dc7f000)",
|
||||
"\tlibm.so.6 => /usr/lib/libm.so.6 (0x00007fcb5db48000)",
|
||||
"\tlibc.so.6 => /usr/lib/libc.so.6 (0x00007fcb5ca00000)",
|
||||
"\t/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fcb5dcd5000)"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"platform": "Windows x86_64",
|
||||
"target": "x86_64-pc-windows-gnu",
|
||||
"catalog_entries": 6449,
|
||||
"approved_hd_frames": 340,
|
||||
"shared_data_manifest": "runtime-manifest.json",
|
||||
"dependencies": {
|
||||
"raptor-rust.exe": [
|
||||
"api-ms-win-core-synch-l1-2-0.dll",
|
||||
"api-ms-win-crt-environment-l1-1-0.dll",
|
||||
"api-ms-win-crt-heap-l1-1-0.dll",
|
||||
"api-ms-win-crt-locale-l1-1-0.dll",
|
||||
"api-ms-win-crt-math-l1-1-0.dll",
|
||||
"api-ms-win-crt-private-l1-1-0.dll",
|
||||
"api-ms-win-crt-runtime-l1-1-0.dll",
|
||||
"api-ms-win-crt-stdio-l1-1-0.dll",
|
||||
"api-ms-win-crt-string-l1-1-0.dll",
|
||||
"bcryptprimitives.dll",
|
||||
"kernel32.dll",
|
||||
"ntdll.dll",
|
||||
"sdl2.dll"
|
||||
],
|
||||
"SDL2.dll": [
|
||||
"advapi32.dll",
|
||||
"gdi32.dll",
|
||||
"imm32.dll",
|
||||
"kernel32.dll",
|
||||
"msvcrt.dll",
|
||||
"ole32.dll",
|
||||
"oleaut32.dll",
|
||||
"setupapi.dll",
|
||||
"shell32.dll",
|
||||
"user32.dll",
|
||||
"version.dll",
|
||||
"winmm.dll"
|
||||
]
|
||||
},
|
||||
"files": {
|
||||
"bin/raptor-rust.exe": "e414d2df745369115082439414099f528aca8457f9c58d1c1745eb1b511fc484",
|
||||
"bin/SDL2.dll": "53e8fa7e9ed43c30bd650c39396f0d9d2067d2fff0b926c9433b75202592e4aa",
|
||||
"bin/SDL2-WINDOWS-LICENSE.txt": "97f35b302b361680ec1e891e95d2d52097bb95abff361434916d99dc1305f127",
|
||||
"run.cmd": "b007382d9ed3560191856514a9ebd4e429801fc25428840e304bb297f3553269"
|
||||
},
|
||||
"launcher": "run.cmd",
|
||||
"validation": "PE imports and complete shared data hashes; see docs/windows-build.md"
|
||||
}
|
||||
+28
-8
@@ -20,7 +20,7 @@ use sha2::{Digest, Sha256};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
path::{Component, Path, PathBuf},
|
||||
};
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -108,6 +108,25 @@ fn read_package_file(root: &Path, relative: &Path) -> Result<Vec<u8>> {
|
||||
);
|
||||
Ok(data)
|
||||
}
|
||||
fn package_hash_name(relative: &Path) -> Result<String> {
|
||||
let mut parts = Vec::new();
|
||||
for component in relative.components() {
|
||||
let Component::Normal(name) = component else {
|
||||
anyhow::bail!("package hash paths must contain only relative filename components");
|
||||
};
|
||||
let name = name.to_str().context("package filenames must be UTF-8")?;
|
||||
// A literal backslash is a Unix filename but a Windows separator;
|
||||
// a colon can address a Windows drive or alternate data stream. Do not
|
||||
// turn either into an alias when generating portable hash keys.
|
||||
ensure!(
|
||||
!name.contains(['\\', ':']),
|
||||
"package filenames contain an ambiguous Windows separator or colon: {name}"
|
||||
);
|
||||
parts.push(name);
|
||||
}
|
||||
ensure!(!parts.is_empty(), "empty package hash path");
|
||||
Ok(parts.join("/"))
|
||||
}
|
||||
fn load_package(path: &Path) -> Result<InstalledMod> {
|
||||
ensure!(
|
||||
fs::symlink_metadata(path)?.is_dir(),
|
||||
@@ -117,7 +136,10 @@ fn load_package(path: &Path) -> Result<InstalledMod> {
|
||||
let mut files = Vec::new();
|
||||
let mut bytes = 0;
|
||||
walk(&root, &root, &mut files, &mut bytes, 0)?;
|
||||
files.sort();
|
||||
// Compare UTF-8 component strings on every platform. Whole-path string
|
||||
// ordering would place art-qa.json before art/... and invalidate existing
|
||||
// Linux saves, whose v1 hashes used PathBuf's component ordering.
|
||||
files.sort_by(|(a, _), (b, _)| a.split('/').cmp(b.split('/')));
|
||||
let manifest_data = read_package_file(&root, Path::new("mod.toml"))?;
|
||||
ensure!(
|
||||
manifest_data.len() <= 64 * 1024,
|
||||
@@ -174,10 +196,7 @@ fn load_package(path: &Path) -> Result<InstalledMod> {
|
||||
}
|
||||
let mut hash = Sha256::new();
|
||||
hash.update(b"raptor-mod-package-v1\0");
|
||||
for relative in files {
|
||||
let name = relative
|
||||
.to_str()
|
||||
.context("package filenames must be UTF-8")?;
|
||||
for (name, relative) in files {
|
||||
let data = read_package_file(&root, &relative)?;
|
||||
hash.update((name.len() as u64).to_le_bytes());
|
||||
hash.update(name.as_bytes());
|
||||
@@ -250,7 +269,7 @@ pub fn discover(root: &Path) -> Discovery {
|
||||
fn walk(
|
||||
root: &Path,
|
||||
current: &Path,
|
||||
files: &mut Vec<PathBuf>,
|
||||
files: &mut Vec<(String, PathBuf)>,
|
||||
bytes: &mut u64,
|
||||
depth: usize,
|
||||
) -> Result<()> {
|
||||
@@ -261,6 +280,7 @@ fn walk(
|
||||
for e in fs::read_dir(current)? {
|
||||
let e = e?;
|
||||
let relative = e.path().strip_prefix(root)?.to_path_buf();
|
||||
let name = package_hash_name(&relative)?;
|
||||
ensure!(
|
||||
!e.file_type()?.is_symlink(),
|
||||
"mod packages may not contain symlinks"
|
||||
@@ -280,7 +300,7 @@ fn walk(
|
||||
.checked_add(e.metadata()?.len())
|
||||
.context("package size overflow")?;
|
||||
ensure!(*bytes <= MAX_PACKAGE_BYTES, "mod package exceeds 256 MiB");
|
||||
files.push(relative);
|
||||
files.push((name, relative));
|
||||
ensure!(files.len() <= MAX_FILES, "mod package has too many files");
|
||||
}
|
||||
}
|
||||
|
||||
+199
-4
@@ -20,9 +20,16 @@ pub struct Paths {
|
||||
}
|
||||
impl Paths {
|
||||
pub fn discover() -> Result<Self> {
|
||||
let home = PathBuf::from(std::env::var_os("HOME").context("HOME is unset")?);
|
||||
let dir = |name: &str, fallback: &str| {
|
||||
std::env::var_os(name)
|
||||
#[cfg(windows)]
|
||||
return Self::windows_from_env(|name| std::env::var_os(name));
|
||||
#[cfg(not(windows))]
|
||||
Self::unix_from_env(|name| std::env::var_os(name))
|
||||
}
|
||||
#[cfg(any(not(windows), test))]
|
||||
fn unix_from_env(mut env: impl FnMut(&str) -> Option<std::ffi::OsString>) -> Result<Self> {
|
||||
let home = PathBuf::from(env("HOME").context("HOME is unset")?);
|
||||
let mut dir = |name: &str, fallback: &str| {
|
||||
env(name)
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| home.join(fallback))
|
||||
};
|
||||
@@ -33,6 +40,31 @@ impl Paths {
|
||||
legacy: home.join(".local/share/Raptor"),
|
||||
})
|
||||
}
|
||||
#[cfg(any(windows, test))]
|
||||
fn windows_from_env(mut env: impl FnMut(&str) -> Option<std::ffi::OsString>) -> Result<Self> {
|
||||
let mut dir = |name: &str| {
|
||||
env(name)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(PathBuf::from)
|
||||
};
|
||||
let profile = dir("USERPROFILE");
|
||||
// Respect redirected application-data folders. A normal Windows login
|
||||
// provides these variables; USERPROFILE also supports stripped-down
|
||||
// launch environments without relying on Unix HOME or XDG variables.
|
||||
let roaming = dir("APPDATA")
|
||||
.or_else(|| profile.as_ref().map(|p| p.join("AppData/Roaming")))
|
||||
.context("APPDATA and USERPROFILE are unset; cannot locate Windows saves")?;
|
||||
let local = dir("LOCALAPPDATA")
|
||||
.or_else(|| profile.as_ref().map(|p| p.join("AppData/Local")))
|
||||
.context("LOCALAPPDATA and USERPROFILE are unset; cannot locate Windows cache")?;
|
||||
Ok(Self {
|
||||
data: roaming.join("RaptorReplica"),
|
||||
config: roaming.join("RaptorReplica"),
|
||||
cache: local.join("RaptorReplica/cache"),
|
||||
// The legacy SDL_GetPrefPath("", "Raptor") location on Windows.
|
||||
legacy: roaming.join("Raptor"),
|
||||
})
|
||||
}
|
||||
pub fn mods(&self) -> PathBuf {
|
||||
self.data.join("mods")
|
||||
}
|
||||
@@ -452,7 +484,15 @@ pub fn atomic_write(path: &Path, data: &[u8]) -> Result<()> {
|
||||
let result = (|| -> Result<()> {
|
||||
file.write_all(data)?;
|
||||
file.sync_all()?;
|
||||
fs::rename(&temp, path)?;
|
||||
// Release the temporary handle before replacement. rename replaces an
|
||||
// existing destination on Windows as well as Unix; never remove the
|
||||
// previous save first or retry by copying over it.
|
||||
drop(file);
|
||||
fs::rename(&temp, path)
|
||||
.with_context(|| format!("cannot atomically replace {}", path.display()))?;
|
||||
// Directory handles opened by ordinary File::open cannot be synced on
|
||||
// Windows. The file itself was synced before its name was committed.
|
||||
#[cfg(unix)]
|
||||
fs::File::open(parent)?.sync_all()?;
|
||||
Ok(())
|
||||
})();
|
||||
@@ -1167,6 +1207,112 @@ fn decrypt(bytes: &[u8], key: &[u8]) -> Vec<u8> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
fn environment(values: &[(&str, &str)]) -> impl FnMut(&str) -> Option<std::ffi::OsString> {
|
||||
let values = values
|
||||
.iter()
|
||||
.map(|(key, value)| (key.to_string(), std::ffi::OsString::from(value)))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
move |key| values.get(key).cloned()
|
||||
}
|
||||
#[test]
|
||||
fn windows_paths_respect_redirected_data_folders_without_home() {
|
||||
let paths = Paths::windows_from_env(environment(&[
|
||||
("APPDATA", "redirected-roaming"),
|
||||
("LOCALAPPDATA", "redirected-local"),
|
||||
("HOME", "unused-unix-home"),
|
||||
("XDG_DATA_HOME", "unused-xdg-data"),
|
||||
("XDG_CONFIG_HOME", "unused-xdg-config"),
|
||||
("XDG_CACHE_HOME", "unused-xdg-cache"),
|
||||
]))
|
||||
.unwrap();
|
||||
let roaming = Path::new("redirected-roaming");
|
||||
assert_eq!(paths.data, roaming.join("RaptorReplica"));
|
||||
assert_eq!(paths.config, roaming.join("RaptorReplica"));
|
||||
assert_eq!(paths.mods(), roaming.join("RaptorReplica/mods"));
|
||||
assert_eq!(
|
||||
paths.save_path(3),
|
||||
roaming.join("RaptorReplica/pilot-0003.json")
|
||||
);
|
||||
assert_eq!(paths.legacy, roaming.join("Raptor"));
|
||||
assert_eq!(
|
||||
paths.cache,
|
||||
Path::new("redirected-local").join("RaptorReplica/cache")
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn windows_paths_fall_back_to_unicode_user_profile() {
|
||||
let profile = "profile-Quintine-é-雪";
|
||||
let paths = Paths::windows_from_env(environment(&[("USERPROFILE", profile)])).unwrap();
|
||||
let profile = Path::new(profile);
|
||||
assert_eq!(paths.data, profile.join("AppData/Roaming/RaptorReplica"));
|
||||
assert_eq!(paths.config, paths.data);
|
||||
assert_eq!(
|
||||
paths.cache,
|
||||
profile.join("AppData/Local/RaptorReplica/cache")
|
||||
);
|
||||
assert_eq!(paths.legacy, profile.join("AppData/Roaming/Raptor"));
|
||||
}
|
||||
#[test]
|
||||
fn windows_missing_or_empty_application_paths_fall_back_independently() {
|
||||
let paths = Paths::windows_from_env(environment(&[
|
||||
("USERPROFILE", "profile"),
|
||||
("APPDATA", "roaming-redirect"),
|
||||
("LOCALAPPDATA", ""),
|
||||
]))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
paths.data,
|
||||
Path::new("roaming-redirect").join("RaptorReplica")
|
||||
);
|
||||
assert_eq!(
|
||||
paths.cache,
|
||||
Path::new("profile").join("AppData/Local/RaptorReplica/cache")
|
||||
);
|
||||
let paths = Paths::windows_from_env(environment(&[
|
||||
("USERPROFILE", "profile"),
|
||||
("APPDATA", ""),
|
||||
("LOCALAPPDATA", "local-redirect"),
|
||||
]))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
paths.data,
|
||||
Path::new("profile").join("AppData/Roaming/RaptorReplica")
|
||||
);
|
||||
assert_eq!(
|
||||
paths.cache,
|
||||
Path::new("local-redirect").join("RaptorReplica/cache")
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn windows_missing_profile_has_an_actionable_error() {
|
||||
let error = Paths::windows_from_env(environment(&[("HOME", "unix-home")])).unwrap_err();
|
||||
assert!(error.to_string().contains("APPDATA and USERPROFILE"));
|
||||
let error = Paths::windows_from_env(environment(&[("APPDATA", "roaming")])).unwrap_err();
|
||||
assert!(error.to_string().contains("LOCALAPPDATA and USERPROFILE"));
|
||||
}
|
||||
#[test]
|
||||
fn unix_paths_keep_home_and_xdg_behavior() {
|
||||
let paths = Paths::unix_from_env(environment(&[("HOME", "unix-home")])).unwrap();
|
||||
let home = Path::new("unix-home");
|
||||
assert_eq!(paths.data, home.join(".local/share/RaptorReplica"));
|
||||
assert_eq!(paths.config, home.join(".config/RaptorReplica"));
|
||||
assert_eq!(paths.cache, home.join(".cache/RaptorReplica"));
|
||||
assert_eq!(paths.legacy, home.join(".local/share/Raptor"));
|
||||
let paths = Paths::unix_from_env(environment(&[
|
||||
("HOME", "unix-home"),
|
||||
("XDG_DATA_HOME", "custom-data"),
|
||||
("XDG_CONFIG_HOME", "custom-config"),
|
||||
("XDG_CACHE_HOME", "custom-cache"),
|
||||
]))
|
||||
.unwrap();
|
||||
assert_eq!(paths.data, Path::new("custom-data").join("RaptorReplica"));
|
||||
assert_eq!(
|
||||
paths.config,
|
||||
Path::new("custom-config").join("RaptorReplica")
|
||||
);
|
||||
assert_eq!(paths.cache, Path::new("custom-cache").join("RaptorReplica"));
|
||||
assert!(Paths::unix_from_env(environment(&[("APPDATA", "roaming")])).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn writes_replace_atomically() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
@@ -1176,4 +1322,53 @@ mod tests {
|
||||
assert_eq!(fs::read(p).unwrap(), b"second");
|
||||
assert_eq!(fs::read_dir(tmp.path()).unwrap().count(), 1);
|
||||
}
|
||||
#[test]
|
||||
fn replacement_failure_preserves_the_destination_and_cleans_the_temporary() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let p = tmp.path().join("save.json");
|
||||
fs::create_dir(&p).unwrap();
|
||||
let sentinel = p.join("keep-original");
|
||||
fs::write(&sentinel, b"original contents").unwrap();
|
||||
let error = atomic_write(&p, b"new save").unwrap_err();
|
||||
assert!(error.to_string().contains("cannot atomically replace"));
|
||||
assert_eq!(fs::read(sentinel).unwrap(), b"original contents");
|
||||
assert_eq!(fs::read_dir(tmp.path()).unwrap().count(), 1);
|
||||
}
|
||||
#[test]
|
||||
fn temporary_name_collision_and_unicode_backup_are_preserved() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let p = tmp.path().join("pilot-é-雪.json");
|
||||
let stale = tmp
|
||||
.path()
|
||||
.join(format!(".pilot-é-雪.json.{}.0.tmp", std::process::id()));
|
||||
fs::write(&stale, b"interrupted write").unwrap();
|
||||
atomic_write(&p, b"original pilot").unwrap();
|
||||
let backup = tmp.path().join("pilot-é-雪.original.json");
|
||||
atomic_write(&backup, &fs::read(&p).unwrap()).unwrap();
|
||||
atomic_write(&p, b"updated pilot").unwrap();
|
||||
assert_eq!(fs::read(&p).unwrap(), b"updated pilot");
|
||||
assert_eq!(fs::read(&backup).unwrap(), b"original pilot");
|
||||
assert_eq!(fs::read(&stale).unwrap(), b"interrupted write");
|
||||
assert_eq!(fs::read_dir(tmp.path()).unwrap().count(), 3);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn locked_windows_save_is_never_deleted_before_replacement() {
|
||||
use std::os::windows::fs::OpenOptionsExt;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let p = tmp.path().join("save.json");
|
||||
atomic_write(&p, b"original pilot").unwrap();
|
||||
let locked = OpenOptions::new()
|
||||
.read(true)
|
||||
.share_mode(0)
|
||||
.open(&p)
|
||||
.unwrap();
|
||||
assert!(atomic_write(&p, b"updated pilot").is_err());
|
||||
assert!(p.exists());
|
||||
assert_eq!(fs::read_dir(tmp.path()).unwrap().count(), 1);
|
||||
drop(locked);
|
||||
assert_eq!(fs::read(&p).unwrap(), b"original pilot");
|
||||
atomic_write(&p, b"updated pilot").unwrap();
|
||||
assert_eq!(fs::read(&p).unwrap(), b"updated pilot");
|
||||
}
|
||||
}
|
||||
@@ -203,6 +203,61 @@ fn package_hash_covers_added_assets_and_manifest_changes() {
|
||||
assert!(error(ActiveMods::build(&base(), &d, &ids(&["alpha"]), None)).contains("rescan"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_package_hash_is_portable_and_preserves_existing_linux_order() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let root = temp.path().join("fixture");
|
||||
// Intentionally independent of the TOML serializer and native path spelling.
|
||||
// The fixed digest uses UTF-8 component ordering (art/... before art-qa.json),
|
||||
// forward slash names, little-endian u64 byte lengths and these exact bytes.
|
||||
// The BMP/supplementary names also distinguish UTF-8 from UTF-16 ordering.
|
||||
let files: &[(&str, &[u8])] = &[
|
||||
("art/deep/hull.bin", b"\x00\x7f\xff"),
|
||||
("art/hull-é.png", b"synthetic PNG bytes\r\n"),
|
||||
("art/\u{e000}.bin", b"bmp filename"),
|
||||
("art/\u{10000}.bin", b"supplementary filename"),
|
||||
("art-qa.json", b"{\"ok\":true}\n"),
|
||||
("content.json", b"{}"),
|
||||
("mod.toml", b"id = \"fixture\"\nname = \"Portable fixture\"\nversion = \"1.0.0\"\napi_version = 1\ncontent = \"content.json\"\nscripts = [\"scripts/weapon.rhai\"]\n"),
|
||||
("scripts/weapon.rhai", b"fn on_tick(event, state, random) { #{ commands: [], state: state } }\n"),
|
||||
];
|
||||
// Reverse creation order to ensure directory enumeration cannot set the hash.
|
||||
for (name, data) in files.iter().rev() {
|
||||
let path = root.join(name);
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
fs::write(path, data).unwrap();
|
||||
}
|
||||
let discovered = modding::discover(temp.path());
|
||||
assert!(discovered.errors.is_empty(), "{:?}", discovered.errors);
|
||||
assert_eq!(
|
||||
discovered.mods["fixture"].hash,
|
||||
"1c851b694b601dc9658d0bbbc6a6edd2d5359571c5b558cf0c3f076afa08cf3d"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn ambiguous_windows_filename_components_and_non_utf8_names_are_rejected() {
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
for filename in ["art\\hull.bin", "hull.bin:extra"] {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
install(temp.path(), "alpha", "alpha", json!({}));
|
||||
fs::write(temp.path().join("alpha").join(filename), b"ambiguous asset").unwrap();
|
||||
let discovered = modding::discover(temp.path());
|
||||
assert!(discovered.mods.is_empty());
|
||||
assert_eq!(discovered.errors.len(), 1);
|
||||
assert!(discovered.errors[0].contains("ambiguous Windows"));
|
||||
}
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
install(temp.path(), "alpha", "alpha", json!({}));
|
||||
let filename = std::ffi::OsString::from_vec(b"asset-\xff.bin".to_vec());
|
||||
fs::write(temp.path().join("alpha").join(filename), b"invalid name").unwrap();
|
||||
let discovered = modding::discover(temp.path());
|
||||
assert!(discovered.mods.is_empty());
|
||||
assert_eq!(discovered.errors.len(), 1);
|
||||
assert!(discovered.errors[0].contains("filenames must be UTF-8"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn package_paths_depth_and_size_are_bounded() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -127,13 +127,15 @@ def main():
|
||||
if args.zip:
|
||||
archive = args.zip.resolve()
|
||||
archive.parent.mkdir(parents=True, exist_ok=True)
|
||||
skipped = {"target", "dist", ".reference", "__pycache__", ".git"}
|
||||
skipped = {"target", "dist", ".reference", ".local", "derived", "__pycache__", ".git"}
|
||||
with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as output:
|
||||
for path in sorted(package.rglob("*")):
|
||||
relative = path.relative_to(package)
|
||||
if path.is_file() and not skipped.intersection(relative.parts) and path.resolve() != archive:
|
||||
if any(part.startswith(".runtime-data-") for part in relative.parts):
|
||||
continue
|
||||
if len(relative.parts) >= 4 and relative.parts[0] == "mods" and relative.parts[2:4] == ("art", "source"):
|
||||
continue
|
||||
output.write(path, str(Path("RustRaptor") / relative))
|
||||
print(json.dumps({key: value for key, value in report.items() if key not in {"files", "system_dependencies"}}, indent=2))
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build and include a Windows x86_64 runtime using the shared packaged catalog."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import zipfile
|
||||
|
||||
from assemble_runtime import checked_file, digest
|
||||
|
||||
|
||||
SYSTEM_DLLS = {
|
||||
"advapi32.dll", "bcrypt.dll", "bcryptprimitives.dll", "comdlg32.dll", "crypt32.dll", "dinput8.dll",
|
||||
"gdi32.dll", "hid.dll", "imm32.dll", "iphlpapi.dll", "kernel32.dll",
|
||||
"msvcrt.dll", "ntdll.dll", "ole32.dll", "oleaut32.dll", "powrprof.dll",
|
||||
"psapi.dll", "rpcrt4.dll", "secur32.dll", "setupapi.dll", "shell32.dll",
|
||||
"shlwapi.dll", "ucrtbase.dll", "user32.dll", "userenv.dll", "version.dll",
|
||||
"winmm.dll", "winspool.drv", "ws2_32.dll",
|
||||
}
|
||||
|
||||
|
||||
def pe_imports(path):
|
||||
"""Read checked PE32+ headers and DLL imports without host-specific tools."""
|
||||
payload = path.read_bytes()
|
||||
def u32(offset):
|
||||
return struct.unpack_from("<I", payload, offset)[0]
|
||||
if len(payload) < 64 or payload[:2] != b"MZ":
|
||||
raise ValueError(f"not a Windows executable: {path}")
|
||||
header = u32(60)
|
||||
if payload[header:header+4] != b"PE\0\0":
|
||||
raise ValueError(f"invalid PE signature: {path}")
|
||||
machine, sections = struct.unpack_from("<HH", payload, header+4)
|
||||
optional_size = struct.unpack_from("<H", payload, header+20)[0]
|
||||
optional = header+24
|
||||
if machine != 0x8664 or struct.unpack_from("<H", payload, optional)[0] != 0x20B or optional_size < 128:
|
||||
raise ValueError(f"expected a Windows x86_64 PE32+ image: {path}")
|
||||
headers_size = u32(optional+60)
|
||||
# Dependency closure must include every runtime-loaded import. Delay
|
||||
# imports are not used by our builds; reject them rather than overlooking
|
||||
# a DLL needed only after the game has started.
|
||||
if u32(optional+108) > 13:
|
||||
if optional_size < 224 or any(struct.unpack_from("<II", payload, optional+216)):
|
||||
raise ValueError(f"unsupported PE delay imports: {path}")
|
||||
table = optional+optional_size
|
||||
def offset(rva):
|
||||
if rva < headers_size and rva < len(payload):
|
||||
return rva
|
||||
for index in range(sections):
|
||||
start = table+index*40
|
||||
virtual_size, address, raw_size, raw_offset = struct.unpack_from("<IIII", payload, start+8)
|
||||
if address <= rva < address+max(virtual_size, raw_size):
|
||||
delta = rva-address
|
||||
if delta < raw_size and raw_offset+delta < len(payload):
|
||||
return raw_offset+delta
|
||||
raise ValueError(f"unmapped PE address in {path}")
|
||||
imports, size = struct.unpack_from("<II", payload, optional+120)
|
||||
if not imports:
|
||||
return []
|
||||
result = []
|
||||
for index in range(min(size//20, 4096)):
|
||||
descriptor = struct.unpack_from("<IIIII", payload, offset(imports+index*20))
|
||||
if not any(descriptor):
|
||||
return sorted(set(result))
|
||||
start = offset(descriptor[3])
|
||||
end = payload.find(b"\0", start, min(start+256, len(payload)))
|
||||
if end < 0:
|
||||
raise ValueError(f"unterminated PE import name: {path}")
|
||||
result.append(payload[start:end].decode("ascii").lower())
|
||||
raise ValueError(f"unterminated PE import table: {path}")
|
||||
|
||||
|
||||
def sdk_files(root, target):
|
||||
if target.endswith("-msvc"):
|
||||
library = root / "lib/x64/SDL2.lib"
|
||||
dll = root / "lib/x64/SDL2.dll"
|
||||
else:
|
||||
if (root / "x86_64-w64-mingw32").is_dir():
|
||||
root = root / "x86_64-w64-mingw32"
|
||||
library = root / "lib/libSDL2.dll.a"
|
||||
dll = root / "bin/SDL2.dll"
|
||||
for path in (library, dll):
|
||||
if not path.is_file():
|
||||
raise ValueError(f"missing SDL2 Windows SDK file: {path}")
|
||||
return library, dll
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--sdl2-dir", type=Path, required=True, help="extracted SDL2 MinGW or Visual C SDK")
|
||||
parser.add_argument("--target", default="x86_64-pc-windows-msvc" if os.name == "nt" else "x86_64-pc-windows-gnu",
|
||||
choices=["x86_64-pc-windows-gnu", "x86_64-pc-windows-msvc"])
|
||||
parser.add_argument("--cargo", default="cargo")
|
||||
parser.add_argument("--rustc", type=Path, help="optional matching compiler from an isolated toolchain")
|
||||
parser.add_argument("--linker", help="cross-linker command or full path")
|
||||
parser.add_argument("--sysroot", type=Path, help="optional isolated matching Rust target standard library")
|
||||
parser.add_argument("--skip-build", action="store_true")
|
||||
parser.add_argument("--extra-dll", type=Path, action="append", default=[])
|
||||
parser.add_argument("--zip", type=Path, help="export a Windows-only playable folder with source, docs and mods")
|
||||
args = parser.parse_args()
|
||||
package = Path(__file__).resolve().parents[1]
|
||||
library, sdl_dll = sdk_files(args.sdl2_dir.resolve(), args.target)
|
||||
license = args.sdl2_dir.resolve() / "COPYING.txt"
|
||||
if not license.is_file():
|
||||
license = args.sdl2_dir.resolve() / "LICENSE.txt"
|
||||
if not license.is_file():
|
||||
raise ValueError("SDL2 SDK license is missing")
|
||||
environment = os.environ.copy()
|
||||
if args.rustc:
|
||||
environment["RUSTC"] = str(args.rustc.resolve())
|
||||
key = args.target.upper().replace("-", "_")
|
||||
flags = ["-L", "native="+str(library.parent)]
|
||||
if args.sysroot:
|
||||
flags += ["--sysroot", str(args.sysroot.resolve())]
|
||||
if args.target.endswith("-msvc"):
|
||||
flags += ["-C", "target-feature=+crt-static"]
|
||||
# Encoded flags preserve SDK/sysroot paths containing spaces. Cargo's
|
||||
# explicit --target keeps these flags away from host build scripts.
|
||||
environment["CARGO_ENCODED_RUSTFLAGS"] = "\x1f".join(flags)
|
||||
if args.linker:
|
||||
environment["CARGO_TARGET_"+key+"_LINKER"] = args.linker
|
||||
linker = Path(args.linker)
|
||||
if linker.is_file():
|
||||
# Rust also invokes the cross toolchain's dlltool for raw Windows
|
||||
# imports; an isolated linker needs its sibling tools on PATH.
|
||||
environment["PATH"] = str(linker.resolve().parent)+os.pathsep+environment.get("PATH", "")
|
||||
build = package / "target/windows-runtime"
|
||||
if not args.skip_build:
|
||||
subprocess.run([args.cargo, "build", "--locked", "--release", "--target", args.target,
|
||||
"--target-dir", str(build), "--bin", "raptor-rust", "--features", "desktop"],
|
||||
cwd=package, env=environment, check=True)
|
||||
executable = build / args.target / "release/raptor-rust.exe"
|
||||
dependencies = pe_imports(executable)
|
||||
if "sdl2.dll" not in dependencies:
|
||||
raise ValueError("Windows binary has no SDL2 import; rebuild the desktop feature")
|
||||
bundle = {"raptor-rust.exe": executable, "SDL2.dll": sdl_dll}
|
||||
for extra in args.extra_dll:
|
||||
if extra.name.lower() in {name.lower() for name in bundle}:
|
||||
raise ValueError(f"duplicate bundled DLL: {extra.name}")
|
||||
bundle[extra.name] = extra.resolve()
|
||||
bundled_names = {name.lower() for name in bundle}
|
||||
dependency_report = {}
|
||||
for name, path in bundle.items():
|
||||
imports = pe_imports(path)
|
||||
unresolved = [dll for dll in imports if dll not in SYSTEM_DLLS and dll not in bundled_names
|
||||
and not dll.startswith(("api-ms-win-", "ext-ms-win-"))]
|
||||
if unresolved:
|
||||
raise ValueError(f"unbundled Windows dependency for {name}: {unresolved}; supply --extra-dll")
|
||||
dependency_report[name] = imports
|
||||
data_manifest = json.loads((package / "runtime-manifest.json").read_text())
|
||||
for name, expected in data_manifest["files"].items():
|
||||
if digest(checked_file(package, name)) != expected:
|
||||
raise ValueError(f"packaged data differs from runtime manifest: {name}; refresh the shared bundle first")
|
||||
(package / "bin").mkdir(exist_ok=True)
|
||||
for name, path in bundle.items():
|
||||
shutil.copy2(path, package / "bin" / name)
|
||||
shutil.copy2(license, package / "bin/SDL2-WINDOWS-LICENSE.txt")
|
||||
report = {"format_version": 1, "platform": "Windows x86_64", "target": args.target,
|
||||
"catalog_entries": data_manifest["catalog_entries"],
|
||||
"approved_hd_frames": data_manifest["approved_hd_frames"],
|
||||
"shared_data_manifest": "runtime-manifest.json", "dependencies": dependency_report,
|
||||
"files": {"bin/"+name: digest(package / "bin" / name) for name in bundle},
|
||||
"launcher": "run.cmd", "validation": "PE imports and complete shared data hashes; see docs/windows-build.md"}
|
||||
report["files"]["bin/SDL2-WINDOWS-LICENSE.txt"] = digest(package / "bin/SDL2-WINDOWS-LICENSE.txt")
|
||||
report["files"]["run.cmd"] = digest(package / "run.cmd")
|
||||
(package / "runtime-windows-manifest.json").write_text(json.dumps(report, indent=2)+"\n")
|
||||
if os.name == "nt":
|
||||
subprocess.run([str(package / "bin/raptor-rust.exe"), "--verify-assets"], cwd=package.parent, check=True)
|
||||
if args.zip:
|
||||
archive = args.zip.resolve()
|
||||
archive.parent.mkdir(parents=True, exist_ok=True)
|
||||
skipped = {"target", "dist", ".reference", ".local", "derived", "__pycache__", ".git", "lib"}
|
||||
with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as output:
|
||||
for path in sorted(package.rglob("*")):
|
||||
relative = path.relative_to(package)
|
||||
if not path.is_file() or skipped.intersection(relative.parts) or path.resolve() == archive:
|
||||
continue
|
||||
if relative == Path("bin/raptor-rust") or any(part.startswith(".runtime-data-") for part in relative.parts):
|
||||
continue
|
||||
if len(relative.parts) >= 4 and relative.parts[0] == "mods" and relative.parts[2:4] == ("art", "source"):
|
||||
continue
|
||||
output.write(path, str(Path("RustRaptor") / relative))
|
||||
print(json.dumps(report, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user