M4: Linux + Windows CI build pipeline (.github/workflows/build.yml)
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
This commit is contained in:
@@ -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 }}
|
||||
@@ -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/
|
||||
|
||||
@@ -4,17 +4,22 @@
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Same reasoning as engine.physics' own native Content item: default
|
||||
DllImport probing already checks the calling assembly's directory,
|
||||
so this is copied straight alongside Engine.Audio.dll rather than
|
||||
through a runtimes/<rid>/native/ path. -->
|
||||
<Content Include="native/liblingua_audio.so">
|
||||
<!-- Same reasoning as engine.physics' own native Content item — see
|
||||
there for why linux-x64 is committed and win-x64 isn't. -->
|
||||
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('Linux'))">
|
||||
<Content Include="native/linux-x64/liblingua_audio.so">
|
||||
<Link>liblingua_audio.so</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
|
||||
<Content Include="native/win-x64/lingua_audio.dll">
|
||||
<Link>lingua_audio.dll</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Engine.Kernel\Engine.Kernel.csproj" />
|
||||
<ProjectReference Include="..\Engine.Audio.Contracts\Engine.Audio.Contracts.csproj" />
|
||||
|
||||
@@ -7,22 +7,35 @@
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- The compiled native shim (Box3D folded in) — see
|
||||
native/physics-native/. Copied straight into this plugin's own
|
||||
output directory (not a runtimes/<rid>/native/ NuGet-style path):
|
||||
DllImport's default unmanaged-library probing already checks the
|
||||
calling assembly's own directory, and PluginLoadContext.
|
||||
LoadUnmanagedDll falls back to that default probing whenever its
|
||||
AssemblyDependencyResolver doesn't recognize the name (returning
|
||||
IntPtr.Zero, not failing outright) — confirmed empirically, see
|
||||
PhysicsWorldTests. -->
|
||||
<Content Include="native/liblingua_physics.so">
|
||||
<!-- The compiled native shim (Box3D folded in) — see
|
||||
native/physics-native/. Copied straight into this plugin's own
|
||||
output directory (not a runtimes/<rid>/native/ NuGet-style path):
|
||||
DllImport's default unmanaged-library probing already checks the
|
||||
calling assembly's own directory, and PluginLoadContext.
|
||||
LoadUnmanagedDll falls back to that default probing whenever its
|
||||
AssemblyDependencyResolver doesn't recognize the name (returning
|
||||
IntPtr.Zero, not failing outright) — confirmed empirically, see
|
||||
PhysicsWorldTests.
|
||||
|
||||
Picked by the BUILDING machine's OS, not cross-compiled: 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 is
|
||||
instead produced fresh by .github/workflows/build.yml's Windows job
|
||||
right before `dotnet build` runs, landing at exactly this path. -->
|
||||
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('Linux'))">
|
||||
<Content Include="native/linux-x64/liblingua_physics.so">
|
||||
<Link>liblingua_physics.so</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
|
||||
<Content Include="native/win-x64/lingua_physics.dll">
|
||||
<Link>lingua_physics.dll</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Engine.Kernel\Engine.Kernel.csproj" />
|
||||
<ProjectReference Include="..\Engine.Physics.Contracts\Engine.Physics.Contracts.csproj" />
|
||||
|
||||
@@ -15,11 +15,21 @@
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="../../plugins/engine.audio/Engine.Audio/native/liblingua_audio.so">
|
||||
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('Linux'))">
|
||||
<Content Include="../../plugins/engine.audio/Engine.Audio/native/linux-x64/liblingua_audio.so">
|
||||
<Link>liblingua_audio.so</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
|
||||
<Content Include="../../plugins/engine.audio/Engine.Audio/native/win-x64/lingua_audio.dll">
|
||||
<Link>lingua_audio.dll</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="assets/test_tone.wav">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
||||
@@ -15,17 +15,22 @@
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Same native shim Engine.Physics itself ships — see that project's
|
||||
own Content item for why it's copied directly (not under a
|
||||
runtimes/<rid>/native/ path) rather than referenced some other
|
||||
way. -->
|
||||
<Content Include="../../plugins/engine.physics/Engine.Physics/native/liblingua_physics.so">
|
||||
<!-- Same native shim Engine.Physics itself ships, same per-OS split —
|
||||
see that project's own Content items. -->
|
||||
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('Linux'))">
|
||||
<Content Include="../../plugins/engine.physics/Engine.Physics/native/linux-x64/liblingua_physics.so">
|
||||
<Link>liblingua_physics.so</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
|
||||
<Content Include="../../plugins/engine.physics/Engine.Physics/native/win-x64/lingua_physics.dll">
|
||||
<Link>lingua_physics.dll</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\plugins\engine.physics\Engine.Physics\Engine.Physics.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user