Skip to content

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

Draft
rekhoff wants to merge 11 commits intomasterfrom
rekhoff/nativeaot-net10
Draft

Add .NET 10 support via NativeAOT-LLVM along side .NET 8 support#4915
rekhoff wants to merge 11 commits intomasterfrom
rekhoff/nativeaot-net10

Conversation

@rekhoff
Copy link
Copy Markdown
Contributor

@rekhoff rekhoff commented Apr 29, 2026

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 8 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`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants