From faabfc2cb4eca95ced73c4e94731e91567098337 Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 2 Sep 2026 17:52:28 +0300 Subject: [PATCH] M4: Linux + Windows CI build pipeline (.github/workflows/build.yml) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The literal thing nothing on this dev machine can verify: whether the build actually runs on Windows at all. Solved by having real CI do it — ubuntu-latest and windows-latest both build the native Box3D/miniaudio shims from source via CMake, build and test the full .NET solution, stage a shippable configuration (engine.windowing/assets/render/input/physics/ audio + physics-demo-game — engine.editor deliberately excluded, that's M4's actual "done when"), and then run it headless against samples/ PhysicsDemo for 200 frames, asserting DemoBox settled at y≈1.0 in the resulting dump. Real physics, real scene load, real plugin loading, proven on both platforms, not just built. Required restructuring the native Content items into per-OS ItemGroups (native/linux-x64/ vs native/win-x64/, selected via $([MSBuild]::IsOSPlatform(...))): linux-x64's .so is committed (built and verified here); win-x64's .dll is never committed — nothing here can build or run one to verify — and only ever exists as something the Windows job produces fresh, in-place, right before `dotnet build`. Caught one real bug dry-running this exact staging locally before trusting it to a workflow run: PluginHost.Load calls Assembly. LoadFromAssemblyPath, which throws on a relative path — the verification step's `--plugins ../../plugins` failed immediately with "is not an absolute path." Every manual verification earlier this session happened to always pass an absolute --plugins path, which is exactly why this never surfaced before. Fixed by resolving to an absolute path before invoking Engine.Host. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N --- .github/workflows/build.yml | 123 ++++++++++++++++++ .gitignore | 6 + .../Engine.Audio/Engine.Audio.csproj | 17 ++- .../native/{ => linux-x64}/liblingua_audio.so | Bin .../Engine.Physics/Engine.Physics.csproj | 35 +++-- .../{ => linux-x64}/liblingua_physics.so | Bin .../Engine.Audio.Tests.csproj | 14 +- .../Engine.Physics.Tests.csproj | 17 ++- 8 files changed, 187 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/build.yml rename plugins/engine.audio/Engine.Audio/native/{ => linux-x64}/liblingua_audio.so (100%) rename plugins/engine.physics/Engine.Physics/native/{ => linux-x64}/liblingua_physics.so (100%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..9fbbfd0 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,123 @@ +name: Build + +# M4's "done when": the build runs on both platforms with no editor +# plugins in the shipped binary. Neither half of that is verifiable from +# a single Linux dev machine — this workflow is what actually proves it, +# real builds on real ubuntu-latest and windows-latest runners, not just +# a cross-platform-looking csproj and hope. + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + rid: linux-x64 + - os: windows-latest + rid: win-x64 + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + + - name: Build native physics shim (Box3D, pinned commit — see native/physics-native/CMakeLists.txt) + shell: bash + run: | + cmake -S native/physics-native -B native/physics-native/build -DCMAKE_BUILD_TYPE=Release + cmake --build native/physics-native/build --config Release + + - name: Build native audio shim (miniaudio) + shell: bash + run: | + cmake -S native/audio-native -B native/audio-native/build -DCMAKE_BUILD_TYPE=Release + cmake --build native/audio-native/build --config Release + + # linux-x64's .so is committed (built and verified on the dev + # machine); win-x64's .dll can only ever be produced here, since + # nothing else in this project can build or run one to check it — + # landing it at exactly the path Engine.Physics.csproj/Engine.Audio. + # csproj's win-x64 Content item expects. + - name: Place win-x64 native libraries + if: matrix.rid == 'win-x64' + shell: bash + run: | + mkdir -p plugins/engine.physics/Engine.Physics/native/win-x64 + mkdir -p plugins/engine.audio/Engine.Audio/native/win-x64 + find native/physics-native/build -iname "lingua_physics.dll" -exec cp {} plugins/engine.physics/Engine.Physics/native/win-x64/lingua_physics.dll \; + find native/audio-native/build -iname "lingua_audio.dll" -exec cp {} plugins/engine.audio/Engine.Audio/native/win-x64/lingua_audio.dll \; + test -f plugins/engine.physics/Engine.Physics/native/win-x64/lingua_physics.dll + test -f plugins/engine.audio/Engine.Audio/native/win-x64/lingua_audio.dll + + - name: Build + run: dotnet build LinguaEngine.sln -c Release + + # Nothing in the suite opens a real GL window or a real audio device + # (Engine.Audio.Tests forces miniaudio's null backend specifically so + # this works) — the whole thing runs headless on both runners. + - name: Test + run: dotnet test LinguaEngine.sln -c Release --no-build + + - name: Stage the shippable build (engine.editor deliberately excluded) + shell: bash + run: | + set -euo pipefail + DIST="dist/${{ matrix.rid }}" + STAGE="$DIST/plugins" + mkdir -p "$STAGE" + + for id in engine.windowing engine.assets engine.render engine.input engine.physics engine.audio; do + impl="$(awk -F. '{for(i=1;i<=NF;i++){$i=toupper(substr($i,1,1)) substr($i,2)}}1' OFS=. <<<"$id")" + src="plugins/$id/$impl/bin/Release/net9.0" + mkdir -p "$STAGE/$id" + cp -r "$src/." "$STAGE/$id/" + cp "plugins/$id/plugin.json" "$STAGE/$id/" + done + + mkdir -p "$DIST/engine" + cp -r src/Engine.Host/bin/Release/net9.0/. "$DIST/engine/" + + GAME_DST="$DIST/samples/PhysicsDemo/GamePlugins/physics-demo-game" + mkdir -p "$GAME_DST" + cp -r samples/PhysicsDemo/GamePlugins/physics-demo-game/bin/Release/net9.0/. "$GAME_DST/" + cp samples/PhysicsDemo/GamePlugins/physics-demo-game/plugin.json "$GAME_DST/" + + cp -r samples/PhysicsDemo/assets "$DIST/samples/PhysicsDemo/assets" + cp samples/PhysicsDemo/project.json samples/PhysicsDemo/scene.json "$DIST/samples/PhysicsDemo/" + + - name: Verify the shippable build actually runs (headless, real physics + real scene load) + shell: bash + run: | + set -euo pipefail + # Absolute paths only: Assembly.LoadFromAssemblyPath (which + # PluginHost.Load uses) throws on a relative one — found by + # hand running this exact staging locally before trusting it to + # a workflow run at all. + DIST="$(pwd)/dist/${{ matrix.rid }}" + cd "$DIST/samples/PhysicsDemo" + dotnet "$DIST/engine/Engine.Host.dll" run --headless \ + --plugins "$DIST/plugins" --project project.json --scene scene.json \ + --frames 200 --dump dump.json + cat dump.json + python3 -c " + import json + data = json.load(open('dump.json')) + box = next(o for o in data if o['name'] == 'DemoBox') + y = box['transform']['position'][1] + assert 0.9 < y < 1.1, f'DemoBox should have settled near y=1.0, got y={y}' + print(f'DemoBox settled at y={y:.4f} — physics ran for real on ${{ matrix.rid }}') + " + + - uses: actions/upload-artifact@v4 + with: + name: lingua-engine-${{ matrix.rid }} + path: dist/${{ matrix.rid }} diff --git a/.gitignore b/.gitignore index 7699cab..3377e8b 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,9 @@ native/*/.fetchcontent-cache/ ## the .csproj, and the .cs files there stay tracked. samples/*/GamePlugins/*/*.dll samples/*/GamePlugins/*/*.pdb + +## Windows builds of the native physics/audio shims — nothing here can +## build or verify one (no Windows machine), so unlike linux-x64's .so, +## win-x64's .dll is never committed. .github/workflows/build.yml's +## Windows job builds it fresh into this exact path before `dotnet build`. +**/native/win-x64/ diff --git a/plugins/engine.audio/Engine.Audio/Engine.Audio.csproj b/plugins/engine.audio/Engine.Audio/Engine.Audio.csproj index faa08c4..1769f76 100644 --- a/plugins/engine.audio/Engine.Audio/Engine.Audio.csproj +++ b/plugins/engine.audio/Engine.Audio/Engine.Audio.csproj @@ -4,17 +4,22 @@ true - - - + + + liblingua_audio.so PreserveNewest + + + lingua_audio.dll + PreserveNewest + + + diff --git a/plugins/engine.audio/Engine.Audio/native/liblingua_audio.so b/plugins/engine.audio/Engine.Audio/native/linux-x64/liblingua_audio.so similarity index 100% rename from plugins/engine.audio/Engine.Audio/native/liblingua_audio.so rename to plugins/engine.audio/Engine.Audio/native/linux-x64/liblingua_audio.so diff --git a/plugins/engine.physics/Engine.Physics/Engine.Physics.csproj b/plugins/engine.physics/Engine.Physics/Engine.Physics.csproj index bbbe534..aa9fc93 100644 --- a/plugins/engine.physics/Engine.Physics/Engine.Physics.csproj +++ b/plugins/engine.physics/Engine.Physics/Engine.Physics.csproj @@ -7,22 +7,35 @@ true - - - + + + liblingua_physics.so PreserveNewest + + + lingua_physics.dll + PreserveNewest + + + diff --git a/plugins/engine.physics/Engine.Physics/native/liblingua_physics.so b/plugins/engine.physics/Engine.Physics/native/linux-x64/liblingua_physics.so similarity index 100% rename from plugins/engine.physics/Engine.Physics/native/liblingua_physics.so rename to plugins/engine.physics/Engine.Physics/native/linux-x64/liblingua_physics.so diff --git a/tests/Engine.Audio.Tests/Engine.Audio.Tests.csproj b/tests/Engine.Audio.Tests/Engine.Audio.Tests.csproj index bdc9335..5152758 100644 --- a/tests/Engine.Audio.Tests/Engine.Audio.Tests.csproj +++ b/tests/Engine.Audio.Tests/Engine.Audio.Tests.csproj @@ -15,11 +15,21 @@ - - + + liblingua_audio.so PreserveNewest + + + + + lingua_audio.dll + PreserveNewest + + + + PreserveNewest diff --git a/tests/Engine.Physics.Tests/Engine.Physics.Tests.csproj b/tests/Engine.Physics.Tests/Engine.Physics.Tests.csproj index e707051..6df1ec7 100644 --- a/tests/Engine.Physics.Tests/Engine.Physics.Tests.csproj +++ b/tests/Engine.Physics.Tests/Engine.Physics.Tests.csproj @@ -15,17 +15,22 @@ - - - + + + liblingua_physics.so PreserveNewest + + + lingua_physics.dll + PreserveNewest + + +