Build and export¶
Building VHI's C# needs a .NET 8 SDK (below). Producing a standalone export has two further gotchas — all three are covered here.
Prerequisite: a .NET 8 SDK¶
VHI targets net8.0, and global.json is already committed pinning that:
latestFeature rolls forward within the 8.0.x feature band only — deliberately, so
the build cannot drift onto a newer major without someone deciding to. You do not need
to create this file, and you should not edit it.
The consequence is worth being explicit about: a machine with only a newer SDK cannot
build this project at all — not just the export. dotnet build fails during SDK
resolution, before compiling anything:
A compatible .NET SDK was not found.
Requested SDK version: 8.0.0
global.json file: …/Virtual-Hand-Interface/global.json
Installing .NET 8 side by side¶
Install it alongside whatever you already have rather than relaxing the pin. The
official installer places a self-contained SDK under ~/.dotnet and needs no
administrator rights:
curl -fsSL https://dot.net/v1/dotnet-install.sh -o dotnet-install.sh
bash dotnet-install.sh --channel 8.0 # installs into ~/.dotnet
~/.dotnet is not added to PATH, so invoke that muxer explicitly — a dotnet from a
package manager resolves only its own SDKs and will still fail:
export DOTNET_ROOT="$HOME/.dotnet"
"$HOME/.dotnet/dotnet" --version # → 8.0.x, run inside the repo
"$HOME/.dotnet/dotnet" build VHI_godot.csproj # compile without the editor
Verify by running dotnet --version inside the repo: it should report an 8.0.x,
because that is global.json being honoured. Outside the repo the same command may
legitimately report something newer.
Do not relax the pin to work around a missing SDK
Setting rollForward: latestMajor makes the build succeed on a newer SDK, and it
will appear to work. It also changes the toolchain for everyone and for CI, where
.github/workflows/release.yml provisions 8.0.x explicitly. A newer compiler also
reports different diagnostics — building under the pinned SDK caught a
partially-documented parameter list that a newer one had let through silently.
Install the SDK the project asks for instead.
Export¶
export_presets.cfg defines presets for macOS, Windows Desktop,
Linux and Android - pass the preset name exactly:
godot --headless --export-release "macOS" VHI.app
godot --headless --export-release "Windows Desktop" VHI.exe
godot --headless --export-release "Linux" VHI.x86_64
A working export bundles the .NET runtime, all managed assemblies, and the
ASP.NET Core shared framework (Kestrel, the Microsoft.Extensions.* and
Microsoft.AspNetCore.* assemblies) that VHI's gRPC server
needs.
Godot runs the system dotnet, not the one on your PATH¶
On macOS the exporter looks for /usr/local/share/dotnet/dotnet before anything on
PATH, and that muxer only sees the SDKs installed under its own root. With a .NET 8 SDK
in ~/.dotnet and a newer one in /usr/local/share/dotnet, the export reports
dotnet publish exited with code: 155 and produces a bundle with no
data_VHI_godot_macos_* folders, so the app opens with no C# running (2026-09-12).
Two ways out. Install the .NET 8 SDK into the system root as well
(sudo bash dotnet-install.sh --channel 8.0 --install-dir /usr/local/share/dotnet), or
publish by hand and assemble the bundle yourself, which is exactly what the exporter does:
export DOTNET_ROOT="$HOME/.dotnet" PATH="$HOME/.dotnet:$PATH"
for rid in osx-arm64 osx-x64; do
dotnet publish VHI_godot.csproj -c ExportRelease -r $rid --self-contained \
-p:GodotTargetPlatform=macos -p:DebugType=None -p:DebugSymbols=false -o /tmp/pub-$rid
done
cp -R /tmp/pub-osx-arm64 VHI.app/Contents/Resources/data_VHI_godot_macos_arm64
cp -R /tmp/pub-osx-x64 VHI.app/Contents/Resources/data_VHI_godot_macos_x86_64
then re-sign as below.
macOS: re-sign without the hardened runtime¶
This is the one that bites. Godot ad-hoc-signs the macOS .app with the
hardened runtime enabled. Under the hardened runtime, Godot's embedded
.NET host fails to bring up the runtime - silently. The window opens, the
Godot engine runs, but no C# executes: no gRPC server, no hand logic, no
output.
The fix is to re-sign the bundle ad-hoc without the hardened runtime:
After that the exported app's .NET side starts normally - the gRPC server
comes up and the hands work. Verify by checking the signature flags went from
adhoc,runtime to just adhoc:
For real distribution
Ad-hoc re-signing is fine for lab machines. To hand the build to other people without Gatekeeper friction, sign all nested code with a Developer ID certificate and notarize the app - the hardened-runtime incompatibility is specifically with ad-hoc signing.
Why this happens¶
Godot doesn't bundle a .NET runtime - it loads one through its own managed
host, which doesn't follow the standard .NET shared-framework probing rules
(godotengine/godot#112701).
VHI works around the loading with
SharedFrameworkAssemblyLoader;
the hardened-runtime signing incompatibility is a separate, macOS-specific
issue on top of that.
Building the docs site (CI)¶
The docs site has one prerequisite a fresh checkout has to satisfy: the
C# API reference is generated from the compiled
assembly at build time and the docs/reference/api/ tree is gitignored, so
properdocs build will fail until it exists. The full sequence:
dotnet tool restore # installs DefaultDocumentation.Console
./tools/gen_api_docs.sh # builds the assembly, regenerates the API md
uv run --group docs properdocs build # the docs site itself
A CI runner needs the .NET 8 SDK (as for export), uv, and Python 3
(used by the API-doc post-process scripts under tools/). For local
authoring uv run --group docs properdocs serve watches docs/ and the
generated API tree both - re-run ./tools/gen_api_docs.sh whenever a ///
comment in src/ or a comment in proto/remote_control.proto changes.