Skip to content

Add .NET 10 support via NativeAOT-LLVM along side .NET 8 support - #4915

Merged
lisandroct merged 148 commits into
masterfrom
rekhoff/nativeaot-net10
Jul 17, 2026
Merged

Add .NET 10 support via NativeAOT-LLVM along side .NET 8 support#4915
lisandroct merged 148 commits into
masterfrom
rekhoff/nativeaot-net10

Conversation

@rekhoff

@rekhoff rekhoff commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Description of changes

Builds on #4741 to clean up and refine the NativeAOT-LLVM integration. The major changes:

  • Standardized the repo on .NET 10 SDK (global.json10.0.100). The team is migrating to .NET 10 ahead of .NET 8 EOL. End users are unaffected as their SDK version is controlled by their own global.json emitted by spacetime init.

  • Multi-target NuGet packages instead of conditional single-target:

    • SpacetimeDB.Runtime now unconditionally targets net8.0;net10.0
    • SpacetimeDB.BSATN.Runtime now unconditionally targets netstandard2.1;net8.0;net10.0
    • NuGet packages ship both TFMs in a single .nupkg. No EXPERIMENTAL_WASM_AOT env var needed at pack time
    • EXPERIMENTAL_WASM_AOT define constant is set automatically when TargetFramework == net10.0
    • ILCompiler.LLVM package references conditioned on TargetFramework == net10.0 (not env var)
  • Centralized NativeAOT-LLVM build logic in SpacetimeDB.Runtime.props and SpacetimeDB.Runtime.targets:

    • Moved PublishTrimmed, SelfContained, WasmEnableThreads into .props (previously duplicated across 4+ .csproj files)
    • Moved the .wit-stripping UseWasiRuntimeOverlayWithoutComponentWit target into .targets (was duplicated in every server .csproj)
    • Added IlcLlvmTarget=wasm32-unknown-wasip1 override after ILCompiler.LLVM.targets import (previously passed as /p: arg)
    • Added _InitializeWasiSdk to ObtainWasiSdk BeforeTargets for .NET 10 compatibility
    • Consumer .csproj files no longer need any AOT boilerplate
  • Refactored csharp.rs build logic to support three build paths via CsharpBuildPath enum:

    • Net8Jit : stable .NET 8 path using wasi-experimental workload (Mono WASM)
    • Net8Aot : .NET 8 NativeAOT-LLVM (opt-in via --native-aot or EXPERIMENTAL_WASM_AOT=1)
    • Net10Aot : .NET 10 NativeAOT-LLVM (auto-detected when .NET 10 SDK is active)
    • Auto-detects .NET SDK version from dotnet --version (respects global.json)
    • Automatically sets EXPERIMENTAL_WASM_AOT=1 for AOT paths so MSBuild conditionals activate
    • Unified dotnet publish command for all paths. Build-specific config handled by props/targets
    • Removed redundant /p:IlcLlvmTarget and /p:WasmEnableThreads CLI args (now in .targets)
  • Refactored init.rs for .NET version-aware scaffolding:

    • Added --dotnet-version CLI arg to explicitly select .NET 8 or 10
    • Added resolve_dotnet_major() which auto-detects or prompts interactively when multiple SDKs are installed
    • .NET 10 auto-enables NativeAOT-LLVM (--native-aot not required)
    • .NET 10 template emits <TargetFramework>net10.0</TargetFramework> directly (no conditional)
    • .NET 8 AOT template adds ILCompiler.LLVM 8.0.0-* package refs gated on EXPERIMENTAL_WASM_AOT=1
    • Emits correct global.json per SDK version (8.0.100 or 10.0.100)
  • Added _initialize call in wasmtime_module.rs: NativeAOT-LLVM modules are WASI reactors that export _initialize to bootstrap the native runtime. This is called before preinit functions. Traditional .NET 8 WASI modules export _start instead and are unaffected.

  • Updated FFI.cs WasmImportLinkageAttribute guard to #if EXPERIMENTAL_WASM_AOT && NET10_0_OR_GREATER, ensuring the real attribute is only used when both the AOT flag and .NET 10+ TFM are active. The dummy shim is used for all other builds.

  • Updated csharp_aot_module.rs smoketest:

    • Auto-detects .NET SDK version and adjusts TFM accordingly
    • .NET 8 AOT: Windows-only (ILCompiler.LLVM 8.0.0-* not published for Linux)
    • .NET 10 AOT: Windows and Linux
    • Removed emscripten dependency (NativeAOT-LLVM uses WASI SDK, not emscripten)
  • Updated CLI reference docs to reflect --native-aot behavior change (not needed for .NET 10) and new --dotnet-version arg.

This PR addresses several issues from #4741 review:

  1. NativeAOT-LLVM build logic was duplicated in every server .csproj (4+ files had identical PropertyGroup and Target blocks). This is now centralized in the shipped .props/.targets.

  2. NuGet packages only shipped a single TFM. The env var controlled which TFM was built, meaning you needed EXPERIMENTAL_WASM_AOT=1 at pack time to get the net10.0 DLL. Now both TFMs are always included.

  3. spacetime init emitted unnecessary conditional wrappers in the generated .csproj for .NET 10 projects. Since the project is definitively targeting .NET 10, the TFM should be unconditional.

  4. spacetime publish passed MSBuild properties via /p: args that are now handled by centralized .targets, simplifying the CLI code.

API and ABI breaking changes

  • global.json now requires .NET 10 SDK for repo development. All developers and CI agents need .NET SDK 10.0+ installed.
  • End user module projects are not affected. They continue to use whichever SDK their local global.json specifies.
  • .NET 8 JIT and .NET 8 AOT paths remain functional for end users until .NET 8 EOL.

Expected complexity level and risk

2 - The core risk is the same as #4741: future changes in the upstream NativeAOT-LLVM/WASI pipeline. Centralizing the build logic in .props/.targets reduces the surface area for breaking changes (one place to update vs. N consumer projects). The .wit-stripping workaround remains necessary until dotnet/runtimelab#3144 is resolved.

Testing

  • Ran C# module publish + client connection tests for all three build paths:
    • .NET 8 JIT (spacetime publish without --native-aot)
    • .NET 8 AOT (spacetime publish with EXPERIMENTAL_WASM_AOT=1 and .NET 8 SDK)
    • .NET 10 AOT (spacetime publish with .NET 10 SDK, auto-detected)
  • Verified spacetime init --lang csharp generates correct project structure for both .NET 8 and .NET 10
  • Verified spacetime init --lang csharp --dotnet-version 10 emits net10.0 TFM and 10.0.100 in global.json
  • Verified C# regression tests complete successfully when ran under .NET 8 JIT, .NET 8 AOT, .NET 10 AOT
    • This required making local customizations to the repo versions of the regression tests, in order to conform to the configuration output by spacetime init, but the tests themselves remained identical.
  • Verified NuGet packages contain both lib/net8.0/ and lib/net10.0/ DLLs
  • Updated smoketest test_build_csharp_module_aot to work with both .NET 8 and .NET 10 SDK

@rekhoff rekhoff self-assigned this Apr 29, 2026
rekhoff added 26 commits April 28, 2026 17:59
1) Created exception to `check_global_json_policy` to allow for .NET 8 templates to be generated.
2) Removed .NET 10 DLLs from SDK to prevent Unity issue.
3) Updated NativeAOT smoketest test include `packageSourceMapping` in `NuGet.Config`.
- Add --dotnet-version flag to build/publish/init commands for explicit .NET version control
- Fix smoketests to use local SpacetimeDB.Runtime/BSATN.Runtime packages (not public NuGet)
- Fix CLI binary path detection for debug vs release builds
- Add NativeAOT-LLVM output paths (native/ subdirectory) to csharp.rs
- Force .NET 8 for C# templates in template tests to match template TFM
Comment thread tools/ci/src/main.rs Outdated
Comment thread crates/cli/src/subcommands/mod.rs Outdated
Comment thread crates/cli/src/subcommands/init.rs Outdated
Comment thread crates/cli/src/subcommands/build.rs Outdated
Comment thread crates/cli/src/subcommands/dotnet.rs Outdated
Comment thread crates/cli/src/tasks/csharp.rs Outdated
Comment thread crates/cli/src/tasks/csharp.rs
Comment thread crates/cli/src/tasks/csharp.rs Outdated

@bfops bfops left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left comments on my code-owned files.

The init, dev, and tasks::csharp subcommand changes are large enough that I find them hard to review, so I can't say that I've been super thorough with them.

@bfops bfops left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all my comments on my code-owned files have been addressed.

my previous comment still stands:

The init, dev, and tasks::csharp subcommand changes are large enough that I find them hard to review, so I can't say that I've been super thorough with them.

but from the point of view of this being okay CLI code, I'm fine to merge. I can't speak to the dotnet-validity / edge cases around that stuff, but that's not what my codeowner review is for.

@lisandroct
lisandroct enabled auto-merge July 17, 2026 17:12
@lisandroct
lisandroct added this pull request to the merge queue Jul 17, 2026
Merged via the queue into master with commit 484f3b5 Jul 17, 2026
59 of 60 checks passed
pull Bot pushed a commit to age-rs/SpacetimeDB that referenced this pull request Aug 18, 2026
…orklabs#5576)

# Description of Changes

Updates the LLM benchmark C# project template and generated NuGet
configuration to support the repository’s .NET 10 NativeAOT build path.

The repository now selects .NET 10 through `global.json`, but the LLM
benchmark template targeted only `net8.0`. During C# benchmark runs,
`spacetime publish` therefore selected `net10.0` while publishing a
`net8.0`-only project. This caused golden-module preparation to fail
with MSBuild error `NETSDK1005` before any model benchmarks could run.

The project template now changes from:

```xml
<TargetFramework>net8.0</TargetFramework>
```

to:

```xml
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
```

After enabling the .NET 10 target, the benchmark exposed a second issue:
its generated `NuGet.Config` did not include the `dotnet-experimental`
feed required by the .NET 10 NativeAOT LLVM packages. Restore
consequently failed to find:

- `Microsoft.DotNet.ILCompiler.LLVM`
- `runtime.<platform>.Microsoft.DotNet.ILCompiler.LLVM`

The generated NuGet configuration now includes the `dotnet-experimental`
feed and package-source mappings for those dependencies.

The first full C# run then exposed a benchmark lifecycle issue.
Generated databases remained loaded after scoring, allowing hundreds of
C# NativeAOT modules to accumulate on the local server. The server
eventually stopped responding, and the remaining publish failures were
recorded as zero-score results.

The benchmark harness now:

- Deletes each generated database immediately after its scoring finishes
- Keeps golden databases available while scorers compare schemas,
reducers, and data
- Deletes the golden databases after all models and modes finish
- Reuses the publishing CLI identity when deleting each database
- Treats cleanup and server availability failures as infrastructure
failures
- Stops remaining work and avoids uploading affected results when the
local server fails
- Reports the local server’s exit status when a benchmark run fails

Together, these changes align the LLM benchmark project with the current
C# server templates and prevent generated benchmark databases from
accumulating throughout long-running model evaluations.

This is a follow-up to the .NET 10 support introduced in clockworklabs#4915.

# API and ABI breaking changes

None.

# Expected complexity level and risk

**3 — moderate benchmark lifecycle change.**

The C# template and NuGet changes are isolated configuration updates.
The database cleanup changes affect the benchmark lifecycle for all
supported server languages, but do not affect production runtime
behavior.

The primary risks are deleting a generated database before its scorers
finish or deleting golden databases while they are still needed. The
harness keeps each generated database alive through scoring and keeps
all goldens alive until every model and mode has completed.

# Testing

- [x] Ran `cargo test -p xtask-llm-benchmark` — 10 tests passed
- [x] Ran `cargo check -p xtask-llm-benchmark --all-targets`
- [x] Ran `cargo clippy -p xtask-llm-benchmark --all-targets -- -D
warnings`
- [x] Verified with MSBuild that the template resolves
`TargetFrameworks` to `net8.0;net10.0`
- [x] Packed the local C# runtime packages successfully
- [x] Restored the .NET 10 NativeAOT LLVM dependencies using the
generated NuGet configuration
- [x] Published C# NativeAOT golden modules and produced
`StdbModule.wasm`
- [x] Verified multiple C# goldens remain published through preparation
and are deleted during final cleanup
- [x] Completed a full C# dry run for all default models in both
`guidelines` and `no_context` modes
- [x] Confirmed the dry run completed with 1 successful benchmark run
and 0 failures
- [x] Confirmed the dry run did not upload results
- [x] Ran `git diff --check`
- [ ] Confirm the upload-enabled C# benchmark completes successfully

Initial C# validation run:
https://github.com/clockworklabs/SpacetimeDB/actions/runs/29789520324

Successful full C# dry run:
https://github.com/clockworklabs/SpacetimeDB/actions/runs/29835979746

Upload-enabled C# run:
https://github.com/clockworklabs/SpacetimeDB/actions/runs/29848593880

---------

Co-authored-by: clockwork-labs-bot <clockwork-labs-bot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants