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 # win-x64's .dll can only ever be produced here — nothing else in # this project can build or run one to check it. linux-x64's .so # IS also committed (for scripts/run-sample.sh's convenience on a # dev machine), but CI rebuilds and overwrites it fresh too, every # run: a real failure found by this exact workflow, not a # hypothetical one — the committed .so, built locally against a # newer glibc than ubuntu-latest ships, loaded fine here and threw # DllNotFoundException on the runner. Only a binary built on the # actual target platform is trustworthy there. - name: Place native libraries shell: bash run: | mkdir -p "plugins/engine.physics/Engine.Physics/native/${{ matrix.rid }}" mkdir -p "plugins/engine.audio/Engine.Audio/native/${{ matrix.rid }}" find native/physics-native/build -iname "liblingua_physics.so" -o -iname "lingua_physics.dll" \ | xargs -I{} cp {} "plugins/engine.physics/Engine.Physics/native/${{ matrix.rid }}/" find native/audio-native/build -iname "liblingua_audio.so" -o -iname "lingua_audio.dll" \ | xargs -I{} cp {} "plugins/engine.audio/Engine.Audio/native/${{ matrix.rid }}/" ls "plugins/engine.physics/Engine.Physics/native/${{ matrix.rid }}" ls "plugins/engine.audio/Engine.Audio/native/${{ matrix.rid }}" - 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/" # A deliberately minimal, engine.windowing/engine.render-free stage, # separate from the full one above: found the hard way that --headless # only controls whether Engine.Host's own loop pumps a window — it # does nothing to stop a *loaded* engine.windowing/engine.render from # creating a real window and GL context regardless, which the full # PhysicsDemo project always does. That's fine on a real desktop; on # windows-latest's GPU-less runner it's "WGL: The driver does not # appear to support OpenGL," failing before physics ever got to run. # engine.physics needs no display at all, so this checks only that. - name: Stage a minimal headless-safe build for verification shell: bash run: | set -euo pipefail VERIFY="verify/${{ matrix.rid }}" mkdir -p "$VERIFY/plugins/engine.physics" cp -r plugins/engine.physics/Engine.Physics/bin/Release/net9.0/. "$VERIFY/plugins/engine.physics/" cp plugins/engine.physics/plugin.json "$VERIFY/plugins/engine.physics/" mkdir -p "$VERIFY/engine" cp -r src/Engine.Host/bin/Release/net9.0/. "$VERIFY/engine/" mkdir -p "$VERIFY/samples/PhysicsDemo" cp samples/PhysicsDemo/project.ci-headless.json "$VERIFY/samples/PhysicsDemo/project.json" cp samples/PhysicsDemo/scene.ci-headless.json "$VERIFY/samples/PhysicsDemo/scene.json" - name: Verify the shippable build actually runs (headless, real physics + real scene load, real P/Invoke) 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. VERIFY="$(pwd)/verify/${{ matrix.rid }}" cd "$VERIFY/samples/PhysicsDemo" dotnet "$VERIFY/engine/Engine.Host.dll" run --headless \ --plugins "$VERIFY/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 }}