diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bb00eb6f12..82c0642d241 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,6 +94,26 @@ jobs: ./emsdk install 4.0.21 ./emsdk activate 4.0.21 + # Install WASI SDK for .NET 10 NativeAOT-LLVM compilation. + - name: Install WASI SDK (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + WASI_SDK_VERSION="29" + WASI_SDK_PATH="/opt/wasi-sdk" + if [ -d "$WASI_SDK_PATH" ] && [ -f "$WASI_SDK_PATH/bin/clang" ]; then + echo "WASI SDK already installed at $WASI_SDK_PATH" + else + echo "Installing WASI SDK version $WASI_SDK_VERSION..." + wget -q "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz" + tar -xzf "wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz" + sudo mv "wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux" "$WASI_SDK_PATH" + rm -f "wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz" + echo "WASI SDK installed successfully" + fi + echo "WASI_SDK_PATH=$WASI_SDK_PATH" >> "$GITHUB_ENV" + echo "Using WASI SDK at: $WASI_SDK_PATH" + - name: Install emscripten (Windows) if: runner.os == 'Windows' shell: pwsh @@ -103,6 +123,29 @@ jobs: .\emsdk install 4.0.21 .\emsdk activate 4.0.21 + # Install WASI SDK for .NET 10 NativeAOT-LLVM compilation. + - name: Install WASI SDK (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $wasiSdkVersion = "25" + $wasiDir = "$env:USERPROFILE\.wasi-sdk" + $clangPath = Join-Path $wasiDir "bin\clang.exe" + if (Test-Path $clangPath) { + Write-Host "WASI SDK already installed at $wasiDir" + } else { + Write-Host "Installing WASI SDK version $wasiSdkVersion..." + $wasiUrl = "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${wasiSdkVersion}/wasi-sdk-${wasiSdkVersion}.0-x86_64-windows.tar.gz" + Invoke-WebRequest -Uri $wasiUrl -OutFile "$env:TEMP\wasi-sdk.tar.gz" + New-Item -ItemType Directory -Force -Path $wasiDir | Out-Null + & "$env:SystemRoot\System32\tar.exe" -xzf "$env:TEMP\wasi-sdk.tar.gz" -C $wasiDir --strip-components=1 + Remove-Item "$env:TEMP\wasi-sdk.tar.gz" -Force -ErrorAction SilentlyContinue + Write-Host "WASI SDK installed successfully" + } + echo "WASI_SDK_PATH=$wasiDir" >> $env:GITHUB_ENV + Write-Host "Using WASI SDK at: $wasiDir" + - name: Install psql (Windows) if: runner.os == 'Windows' shell: pwsh @@ -123,9 +166,14 @@ jobs: $PSNativeCommandUseErrorActionPreference = $true cd modules - # the sdk-manifests on windows-latest are messed up, so we need to update them dotnet workload config --update-mode manifests - dotnet workload update + dotnet workload update --from-previous-sdk + # Explicitly install wasi-experimental for .NET 8 SDK (needed for test_build_csharp_module) + # Create temp global.json to target .NET 8 SDK for workload install + $sdk8Json = '{"sdk":{"version":"8.0.400","rollForward":"latestFeature"}}' + $sdk8Json | Out-File -FilePath global.json -Encoding utf8 + dotnet workload install wasi-experimental + Remove-Item global.json - name: Override NuGet packages shell: bash @@ -178,7 +226,7 @@ jobs: test: needs: [lints] name: Test Suite - runs-on: spacetimedb-new-runner-2 + runs-on: ubuntu-latest env: CARGO_TARGET_DIR: ${{ github.workspace }}/target @@ -235,6 +283,25 @@ jobs: ./emsdk install 4.0.21 ./emsdk activate 4.0.21 + # Install WASI SDK for .NET 10 NativeAOT-LLVM compilation (idempotent - checks if already exists). + - name: Install WASI SDK + shell: bash + run: | + WASI_SDK_VERSION="29" + WASI_SDK_PATH="/opt/wasi-sdk" + if [ -d "$WASI_SDK_PATH" ] && [ -f "$WASI_SDK_PATH/bin/clang" ]; then + echo "WASI SDK already installed at $WASI_SDK_PATH" + else + echo "Installing WASI SDK version $WASI_SDK_VERSION..." + wget -q "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz" + tar -xzf "wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz" + sudo mv "wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux" "$WASI_SDK_PATH" + rm -f "wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz" + echo "WASI SDK installed successfully" + fi + echo "WASI_SDK_PATH=$WASI_SDK_PATH" >> "$GITHUB_ENV" + echo "Using WASI SDK at: $WASI_SDK_PATH" + - name: Install wasm-bindgen CLI run: | REQUIRED_WASM_BINDGEN_VERSION="$( @@ -760,6 +827,50 @@ jobs: with: global-json-file: global.json + - name: Install .NET workloads + run: | + dotnet workload config --update-mode manifests + dotnet workload update --from-previous-sdk + # Explicitly install wasi-experimental for .NET 8 SDK (needed for test_build_csharp_module) + # Create temp global.json to target .NET 8 SDK for workload install + echo '{"sdk":{"version":"8.0.400","rollForward":"latestFeature"}}' > global.json + dotnet workload install wasi-experimental + rm global.json + + # Install native WASI SDK toolchain (needed by WasiApp.Native.targets to compile native files). + - name: Install WASI SDK + run: | + WASI_SDK_VERSION="29" + WASI_SDK_PATH="/opt/wasi-sdk" + if [ -d "$WASI_SDK_PATH" ] && [ -f "$WASI_SDK_PATH/bin/clang" ]; then + echo "WASI SDK already installed at $WASI_SDK_PATH" + else + echo "Installing WASI SDK version $WASI_SDK_VERSION..." + wget -q "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz" + tar -xzf "wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz" + sudo mv "wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux" "$WASI_SDK_PATH" + rm -f "wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz" + echo "WASI SDK installed successfully" + fi + echo "WASI_SDK_PATH=$WASI_SDK_PATH" >> "$GITHUB_ENV" + echo "Using WASI SDK at: $WASI_SDK_PATH" + + # Ensure global.json files exist for regression tests (symlinks may not work in CI) + - name: Fix global.json symlinks + run: | + for dir in sdks/csharp/examples~/regression-tests/server \ + sdks/csharp/examples~/regression-tests/republishing/server-initial \ + sdks/csharp/examples~/regression-tests/republishing/server-republish; do + if [ -L "$dir/global.json" ] && [ ! -f "$dir/global.json" ]; then + echo "Fixing broken symlink at $dir/global.json" + rm -f "$dir/global.json" + fi + if [ ! -f "$dir/global.json" ]; then + echo "Creating $dir/global.json" + echo '{"sdk":{"version":"10.0.100","rollForward":"latestMinor"}}' > "$dir/global.json" + fi + done + - name: Override NuGet packages run: | dotnet pack crates/bindings-csharp/BSATN.Runtime @@ -856,6 +967,21 @@ jobs: - name: Run regression tests run: | bash sdks/csharp/tools~/run-regression-tests.sh + # Restore global.json symlinks (we replaced them with files to work around .NET 10 SDK bug) + # server is 5 levels deep from root, republishing dirs are 6 levels deep + if [ -f sdks/csharp/examples~/regression-tests/server/global.json ] && [ ! -L sdks/csharp/examples~/regression-tests/server/global.json ]; then + echo "Restoring symlink at server/global.json" + rm -f sdks/csharp/examples~/regression-tests/server/global.json + ln -s ../../../../../global.json sdks/csharp/examples~/regression-tests/server/global.json + fi + for dir in sdks/csharp/examples~/regression-tests/republishing/server-initial \ + sdks/csharp/examples~/regression-tests/republishing/server-republish; do + if [ -f "$dir/global.json" ] && [ ! -L "$dir/global.json" ]; then + echo "Restoring symlink at $dir/global.json" + rm -f "$dir/global.json" + ln -s ../../../../../../global.json "$dir/global.json" + fi + done tools/check-diff.sh sdks/csharp/examples~/regression-tests || { echo 'Error: Bindings are dirty. Please run `sdks/csharp/tools~/gen-regression-tests.sh`.' exit 1 diff --git a/crates/bindings-csharp/BSATN.Runtime/BSATN.Runtime.csproj b/crates/bindings-csharp/BSATN.Runtime/BSATN.Runtime.csproj index 566806393b1..fb276d41b2d 100644 --- a/crates/bindings-csharp/BSATN.Runtime/BSATN.Runtime.csproj +++ b/crates/bindings-csharp/BSATN.Runtime/BSATN.Runtime.csproj @@ -9,7 +9,7 @@ - netstandard2.1;net8.0 + netstandard2.1;net8.0;net10.0 SpacetimeDB diff --git a/crates/bindings-csharp/NATIVEAOT-LLVM.md b/crates/bindings-csharp/NATIVEAOT-LLVM.md index 3c1dc2438d3..45926ff26b4 100644 --- a/crates/bindings-csharp/NATIVEAOT-LLVM.md +++ b/crates/bindings-csharp/NATIVEAOT-LLVM.md @@ -1,174 +1,288 @@ # Using NativeAOT-LLVM with SpacetimeDB C# Modules -This guide provides instructions for enabling NativeAOT-LLVM compilation for C# SpacetimeDB modules, which can provide performance improvements. +This guide provides instructions for enabling NativeAOT-LLVM compilation for C# SpacetimeDB modules, which can provide performance improvements by compiling C# directly to native WebAssembly (WASM) using the .NET NativeAOT-LLVM toolchain. + +> [!WARNING] +> NativeAOT-LLVM is experimental. ## Overview -NativeAOT-LLVM compiles C# modules to native WebAssembly (WASM) instead of using the Mono runtime. +SpacetimeDB supports three build targets for C# modules: -> [!WARNING] -> This is currently only supported for Windows server modules and is experimental. +| Build Target | .NET Version | Platforms | Description | +|--------------|--------------|-----------|-------------| +| **JIT (Mono)** | .NET 8.0 | Windows, Linux, macOS | Uses the Mono runtime interpreter (default) | +| **NativeAOT-LLVM** | .NET 8.0 | **Windows only** | Compiles C# to native WASM | +| **NativeAOT-LLVM** | .NET 10.0+ | Windows, Linux | Compiles C# to native WASM | + +> [!NOTE] +> .NET 8.0 NativeAOT-LLVM is Windows-only because `runtime.linux-x64.Microsoft.DotNet.ILCompiler.LLVM` was never published to the dotnet-experimental feed. ## Prerequisites -- **.NET SDK 8.x** (same version used by SpacetimeDB) -- **Emscripten SDK (EMSDK)** installed (must contain `upstream/emscripten/emcc.bat`) -- **(Optional) Binaryen (wasm-opt)** installed and on `PATH` (recommended: `version_116`) -- **Windows** - NativeAOT-LLVM is currently only supported for Windows server modules - -## Prerequisites Installation - -### Install Emscripten SDK (EMSDK) - -The Emscripten SDK is required for NativeAOT-LLVM compilation: - -1. **Download and extract** the Emscripten SDK from `https://github.com/emscripten-core/emsdk` - - Example path: `D:\Tools\emsdk` - -2. **Set environment variable** (optional - the CLI will detect it automatically): - ``` - $env:EMSDK="D:\Tools\emsdk" - ``` - -### Install Binaryen (Optional) +- **.NET SDK 8.0** or **.NET SDK 10.0** +- **WASI SDK** (automatically downloaded during first AOT build) +- **(Optional) Binaryen (wasm-opt)** for WASM optimization + +### WASI SDK (Auto-Downloaded) + +The WASI SDK is required for NativeAOT-LLVM compilation and is **automatically downloaded**: + +| Platform | Download Location | +|----------|-------------------| +| Windows | `%USERPROFILE%\.wasi-sdk\wasi-sdk-29` | +| Linux/macOS | `~/.wasi-sdk/wasi-sdk-29` | + +Override with the `WASI_SDK_PATH` environment variable: + +```bash +# Windows +$env:WASI_SDK_PATH="C:\Tools\wasi-sdk" + +# Linux/macOS +export WASI_SDK_PATH=/opt/wasi-sdk +``` + +--- + +## Build Target: .NET 8.0 NativeAOT-LLVM (Windows Only) + +For Windows users who want NativeAOT-LLVM compilation using .NET 8.0 SDK. + +### Requirements +- .NET SDK 8.0 +- Windows operating system +- NuGet.Config with dotnet-experimental feed + +### Project Configuration + +Your `.csproj` must include the conditional LLVM package references: + +```xml + + + net8.0 + wasi-wasm + + + + + + + + + + + + +``` -Binaryen provides `wasm-opt` for WASM optimization (recommended for performance): +Your `NuGet.Config` must include: -1. Download Binaryen https://github.com/WebAssembly/binaryen/releases/tag/version_116 for Windows -2. Extract to e.g. `D:\Tools\binaryen` -3. Add `D:\Tools\binaryen\bin` to `PATH` - - To temporarily add to your current PowerShell session: - ``` - $env:PATH += ";D:\Tools\binaryen\bin" - ``` -4. Verify: - ``` - wasm-opt --version - ``` +```xml + + + + + + + + + + + + + + + + + +``` -## Creating a New NativeAOT Project +### Activating NativeAOT-LLVM (.NET 8) -When creating a new C# project, use the `--native-aot` flag: +There are three ways to enable NativeAOT-LLVM for .NET 8 builds. +**Option 1: `--native-aot` flag during init** ``` -spacetime init --lang csharp --native-aot my-native-aot-project +spacetime init --lang csharp --native-aot --dotnet-version 8 my-project ``` -This automatically: -- Creates a C# project with the required package references -- Generates a `spacetime.json` with `"native-aot": true` -- Configures the project for NativeAOT-LLVM compilation +**Option 2: `--native-aot` flag during publish** +``` +spacetime publish --native-aot my-database-name +``` -## Converting an Existing Project +**Option 3: `spacetime.json` configuration** +```json +{ + "module": "my-module", + "native-aot": true +} +``` -1. **Update spacetime.json** - Add `"native-aot": true` to your `spacetime.json`: - ```json - { - "module": "your-module-name", - "native-aot": true - } - ``` - - **Note:** Once `spacetime.json` has `"native-aot": true`, you can simply run `spacetime publish` without the `--native-aot` flag. The CLI will automatically detect the configuration and use NativeAOT compilation. +Technically all of these options just set the `EXPERIMENTAL_WASM_AOT` environment variable, but they provide different user experiences. Using `--native-aot` during `init` will create a project with a `spacetime.json` configured like Option 3 so the new project is consistently published with NativeAOT-LLVM. -2. **Ensure NuGet feed is configured** - NativeAOT-LLVM packages come from **dotnet-experimental**. Add to `NuGet.Config`: - ```xml - - - - - - - - - ``` +--- -3. **Add NativeAOT package references** - Add this `ItemGroup` to your `.csproj`: - ```xml - - - - - - ``` +## Build Target: .NET 10.0+ NativeAOT-LLVM (Windows & Linux) - Your complete `.csproj` should look like: - ```xml - - - net8.0 - wasi-wasm - enable - enable - - - - - - - - - - - ``` +For users who want NativeAOT-LLVM compilation on Windows **or** Linux. -## Publishing Your NativeAOT Module +### Requirements +- .NET SDK 10.0 +- Windows or Linux operating system +- NuGet.Config with dotnet-experimental feed -After completing either the **Creating a New NativeAOT Project** or **Converting an Existing Project** steps above, you can publish your module normally: +### Project Configuration -``` -# From your project directory -spacetime publish your-database-name -``` - -If you have `"native-aot": true` in your `spacetime.json`, the CLI will automatically detect this and use NativeAOT compilation. Alternatively, you can use: - -``` -spacetime publish --native-aot your-database-name -``` - -The CLI will display "Using NativeAOT-LLVM compilation (experimental)" when NativeAOT is enabled. +For .NET 10, the project configuration is simpler - no conditional package references needed: -## Troubleshooting +```xml + + + net10.0 + wasi-wasm + + + + + + +``` -### Package source mapping enabled -If you have **package source mapping** enabled in `NuGet.Config`, add mappings for the LLVM packages: +Your `NuGet.Config` must include: ```xml - - - - - - - + + + + + + + + - - + + - + - + + +``` + +### global.json (if needed) + +If .NET 10 is not your default SDK, create a `global.json`: + +```json +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestMinor" + } +} +``` + +This is automatically created by the CLI when using the `init` command with `--dotnet-version 10`. + +### Activating NativeAOT-LLVM (.NET 10) + +NativeAOT-LLVM is automatically used when targeting .NET 10. You can also explicitly enable it: + +**Option 1: Target .NET 10 during init (recommended)** +``` +spacetime init --lang csharp --dotnet-version 10 my-project +``` + +**Option 2: Use `--native-aot` flag** +``` +spacetime init --lang csharp --native-aot my-project +``` + +**Option 3: `spacetime.json` configuration** +```json +{ + "module": "my-module", + "native-aot": true +} +``` + +--- + +## Publishing Your Module + +Once configured, publish normally: + +``` +spacetime publish my-database-name +``` + +The CLI will display which build path is being used: +- "Using NativeAOT-LLVM compilation (experimental)" for AOT builds +- Standard output for JIT builds + +### Controlling the .NET Version During Publish + +To explicitly publish with a specific .NET version: + ``` +# Force .NET 8 build (requires --native-aot for AOT) +spacetime publish --dotnet-version 8 --native-aot my-database-name -### wasi-experimental workload install fails -If the CLI cannot install the `wasi-experimental` workload automatically, install it manually: +# Force .NET 10 build (automatically uses AOT) +spacetime publish --dotnet-version 10 my-database-name +``` + +--- + +## Troubleshooting + +### WASI SDK not found + +**Error**: +``` +error : Could not find wasi-sdk. Either set $(WASI_SDK_PATH), or use workloads to get the sdk. +``` + +**Solution**: +1. The WASI SDK should auto-download during first AOT build +2. If it fails, manually install from https://github.com/WebAssembly/wasi-sdk/releases +3. Set `WASI_SDK_PATH` environment variable +4. Restart your terminal/IDE + +### .NET 8 AOT fails on Linux + +**Error**: Missing `runtime.linux-x64.Microsoft.DotNet.ILCompiler.LLVM` + +**Cause**: .NET 8 NativeAOT-LLVM packages were only published for Windows. + +**Solution**: Use .NET 10 for Linux NativeAOT builds: +``` +spacetime init --lang csharp --dotnet-version 10 my-project +``` + +### JIT builds fail: Missing wasi-experimental workload + +For **JIT builds only** (not NativeAOT), you need the `wasi-experimental` workload: ``` dotnet workload install wasi-experimental ``` -### Duplicate PackageReference warning -You may see a `NU1504` warning about duplicate `PackageReference` items. This is expected and non-blocking. +NativeAOT-LLVM builds do **not** use this workload; they use the WASI SDK instead. ### Code generation failed -If you see errors like "Code generation failed for method", ensure: -1. You're using `SpacetimeDB.Runtime` version 2.0.4 or newer -2. All required package references are in your `.csproj` -3. The `dotnet-experimental` feed is configured in `NuGet.Config` + +If you see "Code generation failed for method" errors: +1. Ensure `NuGet.Config` includes the `dotnet-experimental` feed +2. For .NET 8: Verify the `EXPERIMENTAL_WASM_AOT` condition is in your `.csproj` +3. For .NET 10: Verify `TargetFramework` is `net10.0` +4. Check that `global.json` exists if .NET 10 is not your default SDK + +### Duplicate PackageReference warning (NU1504) + +This warning is expected for .NET 8 AOT builds and is non-blocking. diff --git a/crates/bindings-csharp/Runtime/Internal/FFI.cs b/crates/bindings-csharp/Runtime/Internal/FFI.cs index 261233303dc..3fe39825483 100644 --- a/crates/bindings-csharp/Runtime/Internal/FFI.cs +++ b/crates/bindings-csharp/Runtime/Internal/FFI.cs @@ -2,6 +2,12 @@ namespace SpacetimeDB.Internal; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; +#if EXPERIMENTAL_WASM_AOT && NET10_0_OR_GREATER +using WasmImportLinkageAttribute = System.Runtime.InteropServices.WasmImportLinkageAttribute; +#else +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +file sealed class WasmImportLinkageAttribute : Attribute { } +#endif // This type is outside of the hidden `FFI` class because for now we need to do some public // forwarding in the codegen for `__describe_module__` and `__call_reducer__` exports which both @@ -190,6 +196,7 @@ public readonly record struct RowIter(uint Handle) public static readonly RowIter INVALID = new(0); } + [WasmImportLinkage] [LibraryImport(StdbNamespace10_0)] public static partial CheckedStatus table_id_from_name( [In] byte[] name, @@ -197,6 +204,7 @@ public static partial CheckedStatus table_id_from_name( out TableId out_ ); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_0)] public static partial CheckedStatus index_id_from_name( [In] byte[] name, @@ -204,15 +212,18 @@ public static partial CheckedStatus index_id_from_name( out IndexId out_ ); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_0)] public static partial CheckedStatus datastore_table_row_count(TableId table_id, out ulong out_); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_0)] public static partial CheckedStatus datastore_table_scan_bsatn( TableId table_id, out RowIter out_ ); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_4)] public static partial CheckedStatus datastore_index_scan_point_bsatn( IndexId index_id, @@ -221,6 +232,7 @@ public static partial CheckedStatus datastore_index_scan_point_bsatn( out RowIter out_ ); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_4)] public static partial CheckedStatus datastore_delete_by_index_scan_point_bsatn( IndexId index_id, @@ -229,6 +241,7 @@ public static partial CheckedStatus datastore_delete_by_index_scan_point_bsatn( out uint out_ ); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_0)] public static partial CheckedStatus datastore_index_scan_range_bsatn( IndexId index_id, @@ -242,6 +255,7 @@ public static partial CheckedStatus datastore_index_scan_range_bsatn( out RowIter out_ ); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_0)] public static partial Errno row_iter_bsatn_advance( RowIter iter_handle, @@ -249,9 +263,11 @@ public static partial Errno row_iter_bsatn_advance( ref uint buffer_len ); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_0)] public static partial CheckedStatus row_iter_bsatn_close(RowIter iter_handle); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_0)] public static partial CheckedStatus datastore_insert_bsatn( TableId table_id, @@ -259,6 +275,7 @@ public static partial CheckedStatus datastore_insert_bsatn( ref uint row_len ); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_0)] public static partial CheckedStatus datastore_update_bsatn( TableId table_id, @@ -267,6 +284,7 @@ public static partial CheckedStatus datastore_update_bsatn( ref uint row_len ); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_0)] public static partial CheckedStatus datastore_delete_by_index_scan_range_bsatn( IndexId index_id, @@ -280,6 +298,7 @@ public static partial CheckedStatus datastore_delete_by_index_scan_range_bsatn( out uint out_ ); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_0)] public static partial CheckedStatus datastore_delete_all_by_eq_bsatn( TableId table_id, @@ -288,9 +307,11 @@ public static partial CheckedStatus datastore_delete_all_by_eq_bsatn( out uint out_ ); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_5)] public static partial CheckedStatus datastore_clear(TableId table_id, out ulong out_); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_0)] public static partial Errno bytes_source_read( BytesSource source, @@ -298,6 +319,7 @@ public static partial Errno bytes_source_read( ref uint buffer_len ); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_0)] public static partial CheckedStatus bytes_sink_write( BytesSink sink, @@ -315,6 +337,7 @@ public enum LogLevel : byte Panic = 5, } + [WasmImportLinkage] [LibraryImport(StdbNamespace10_0)] public static partial void console_log( LogLevel level, @@ -352,12 +375,15 @@ internal static class ConsoleTimerIdMarshaller } } + [WasmImportLinkage] [LibraryImport(StdbNamespace10_0)] public static partial ConsoleTimerId console_timer_start([In] byte[] name, uint name_len); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_0)] public static partial CheckedStatus console_timer_end(ConsoleTimerId stopwatch_id); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_0)] public static partial void volatile_nonatomic_schedule_immediate( [In] byte[] name, @@ -374,22 +400,28 @@ uint args_len // which prevents source-generated PInvokes from working with types from other assemblies, and // `Identity` lives in another assembly (`BSATN.Runtime`). Luckily, `DllImport` is enough here. #pragma warning disable SYSLIB1054 // Suppress "Use 'LibraryImportAttribute' instead of 'DllImportAttribute'" warning. + [WasmImportLinkage] [DllImport(StdbNamespace10_0)] public static extern void identity(out Identity dest); #pragma warning restore SYSLIB1054 + [WasmImportLinkage] [DllImport(StdbNamespace10_1)] public static extern Errno bytes_source_remaining_length(BytesSource source, ref uint len); + [WasmImportLinkage] [DllImport(StdbNamespace10_2)] public static extern Errno get_jwt(ref ConnectionId connectionId, out BytesSource source); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_3, EntryPoint = "procedure_start_mut_tx")] public static partial Errno procedure_start_mut_tx(out long micros); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_3, EntryPoint = "procedure_commit_mut_tx")] public static partial Errno procedure_commit_mut_tx(); + [WasmImportLinkage] [LibraryImport(StdbNamespace10_3, EntryPoint = "procedure_abort_mut_tx")] public static partial Errno procedure_abort_mut_tx(); @@ -400,6 +432,7 @@ public readonly struct BytesSourcePair public readonly BytesSource B; } + [WasmImportLinkage] [LibraryImport(StdbNamespace10_3, EntryPoint = "procedure_http_request")] public static partial Errno procedure_http_request( ReadOnlySpan request, diff --git a/crates/bindings-csharp/Runtime/Runtime.csproj b/crates/bindings-csharp/Runtime/Runtime.csproj index cd943a55af1..6c220ecc2a2 100644 --- a/crates/bindings-csharp/Runtime/Runtime.csproj +++ b/crates/bindings-csharp/Runtime/Runtime.csproj @@ -8,11 +8,15 @@ - net8.0 + net8.0;net10.0 true SpacetimeDB true - https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json;$(RestoreAdditionalProjectSources) + https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json;$(RestoreAdditionalProjectSources) + + + + $(DefineConstants);EXPERIMENTAL_WASM_AOT @@ -27,10 +31,9 @@ - - - - + + + diff --git a/crates/bindings-csharp/Runtime/bindings.c b/crates/bindings-csharp/Runtime/bindings.c index f2d2d1ca919..6c4ed570949 100644 --- a/crates/bindings-csharp/Runtime/bindings.c +++ b/crates/bindings-csharp/Runtime/bindings.c @@ -265,9 +265,24 @@ WASI_SHIM(path_remove_directory, (int32_t, int32_t, int32_t)); WASI_SHIM(path_rename, (int32_t, int32_t, int32_t, int32_t, int32_t, int32_t)); WASI_SHIM(path_symlink, (int32_t, int32_t, int32_t, int32_t, int32_t)); WASI_SHIM(path_unlink_file, (int32_t, int32_t, int32_t)); -WASI_SHIM(poll_oneoff, (int32_t, int32_t, int32_t, int32_t)); +int32_t WASI_NAME(poll_oneoff)(int32_t, int32_t, int32_t, int32_t nevents_ptr) { + if (nevents_ptr) { + *(__wasi_size_t*)(uintptr_t)nevents_ptr = 0; + } + // Returning success with uninitialized events can wedge the runtime. + // Fail explicitly so the caller surfaces the missing capability instead. + return __WASI_ERRNO_NOSYS; +} WASI_SHIM(sched_yield, ()); -WASI_SHIM(random_get, (int32_t, int32_t)); +int32_t WASI_NAME(random_get)(int32_t buf, int32_t len) { + static uint32_t state = 0x13579BDFu; + uint8_t* out = (uint8_t*)(uintptr_t)buf; + for (int32_t i = 0; i < len; i++) { + state = state * 1664525u + 1013904223u; + out[i] = (uint8_t)(state >> 24); + } + return 0; +} WASI_SHIM(sock_accept, (int32_t, int32_t, int32_t)); WASI_SHIM(sock_recv, (int32_t, int32_t, int32_t, int32_t, int32_t, int32_t)); WASI_SHIM(sock_send, (int32_t, int32_t, int32_t, int32_t, int32_t)); diff --git a/crates/bindings-csharp/Runtime/build/SpacetimeDB.Runtime.props b/crates/bindings-csharp/Runtime/build/SpacetimeDB.Runtime.props index 58ccaa0de6e..3522ea055ad 100644 --- a/crates/bindings-csharp/Runtime/build/SpacetimeDB.Runtime.props +++ b/crates/bindings-csharp/Runtime/build/SpacetimeDB.Runtime.props @@ -9,17 +9,26 @@ false - + + + <_UseNativeAotLlvm Condition="'$(TargetFramework)' == 'net10.0' or $(TargetFramework.StartsWith('net10.'))">true + <_UseNativeAotLlvm Condition="'$(EXPERIMENTAL_WASM_AOT)' == '1'">true + + + Library Shared $(DefineConstants);EXPERIMENTAL_WASM_AOT false false + true + true + false spacetime_10.0 https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json - + Exe diff --git a/crates/bindings-csharp/Runtime/build/SpacetimeDB.Runtime.targets b/crates/bindings-csharp/Runtime/build/SpacetimeDB.Runtime.targets index 5f183e0e040..6149c06e45e 100644 --- a/crates/bindings-csharp/Runtime/build/SpacetimeDB.Runtime.targets +++ b/crates/bindings-csharp/Runtime/build/SpacetimeDB.Runtime.targets @@ -4,7 +4,19 @@ Project="$(PkgMicrosoft_DotNet_ILCompiler_LLVM)\build\Microsoft.DotNet.ILCompiler.LLVM.targets" Condition="'$(EXPERIMENTAL_WASM_AOT)' == '1' and '$(ILCompilerTargetsPath)' == '' and '$(PkgMicrosoft_DotNet_ILCompiler_LLVM)' != '' and Exists('$(PkgMicrosoft_DotNet_ILCompiler_LLVM)\build\Microsoft.DotNet.ILCompiler.LLVM.targets')" /> - + + + <_UseNativeAotLlvm Condition="'$(TargetFramework)' == 'net10.0' or $(TargetFramework.StartsWith('net10.'))">true + <_UseNativeAotLlvm Condition="'$(EXPERIMENTAL_WASM_AOT)' == '1'">true + + + + + wasm32-unknown-wasip1 + + + @@ -46,21 +58,53 @@ + + + - + + <_WasmNativeFileForLinking Include="@(NativeFileReference)" /> + + + + <_OriginalIlcSdkPath>$(IlcSdkPath) + <_WasiRuntimeOverlayDir>$(IntermediateOutputPath)native-hidden-no-wit\ + + + + <_WasiRuntimeOverlaySource Include="$(IlcFrameworkNativePath)**\*" Exclude="$(IlcFrameworkNativePath)**\*.wit" /> + + + + + + + + $(_WasiRuntimeOverlayDir) + $(_OriginalIlcSdkPath) + + + - + + - 24 + 29 $([System.IO.Path]::Combine($(IntermediateOutputPath), "wasi-sdk.$(WasiSdkVersion).tar.gz")) @@ -73,23 +117,35 @@ https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$(WasiSdkVersion)/wasi-sdk-$(WasiSdkVersion).0-$(WasiSdkArch)-$(WasiSdkOS).tar.gz - $([System.IO.Path]::Combine($([System.Environment]::GetFolderPath(SpecialFolder.UserProfile)), '.wasi-sdk', "wasi-sdk-$(WasiSdkVersion)")) + + <_WasiSdkFromEnv>$([System.Environment]::GetEnvironmentVariable('WASI_SDK_PATH')) + <_WasiClangFromEnv Condition="'$(_WasiSdkFromEnv)' != ''">$([System.IO.Path]::Combine($(_WasiSdkFromEnv), 'bin', 'clang')) + <_WasiClangFromEnv Condition="'$(_WasiSdkFromEnv)' != '' and $([MSBuild]::IsOSPlatform('Windows'))">$(_WasiClangFromEnv).exe + <_HasValidWasiSdkFromEnv Condition="'$(_WasiSdkFromEnv)' != '' and Exists('$(_WasiClangFromEnv)')">true + + + $([System.IO.Path]::Combine($([System.Environment]::GetFolderPath(SpecialFolder.UserProfile)), '.wasi-sdk', "wasi-sdk-$(WasiSdkVersion)")) + $(_WasiSdkFromEnv) + $(WasiSdkRoot) $([System.IO.Path]::Combine($(WasiSdkRoot), 'share', 'wasi-sysroot')) $([System.IO.Path]::Combine($(WasiSdkRoot), 'bin', 'clang')) $(WasiClang).exe + + Condition="'$(_HasValidWasiSdkFromEnv)' != 'true' and !Exists('$(WasiClang)')" /> - - + + - + + + diff --git a/crates/cli/src/subcommands/build.rs b/crates/cli/src/subcommands/build.rs index 5ef4cea7300..0923b14004f 100644 --- a/crates/cli/src/subcommands/build.rs +++ b/crates/cli/src/subcommands/build.rs @@ -39,6 +39,12 @@ pub fn cli() -> clap::Command { .action(SetTrue) .help("Builds the module using debug instead of release (intended to speed up local iteration, not recommended for CI)"), ) + .arg( + Arg::new("dotnet_version") + .long("dotnet-version") + .value_name("VERSION") + .help("Target .NET SDK major version for C# projects (e.g. 8 or 10). Auto-detected when omitted.") + ) } pub async fn exec(_config: Config, args: &ArgMatches) -> Result<(PathBuf, &'static str), anyhow::Error> { @@ -60,6 +66,16 @@ pub async fn exec(_config: Config, args: &ArgMatches) -> Result<(PathBuf, &'stat }; let build_debug = args.get_flag("debug"); let features = features.cloned(); + let dotnet_version = args.get_one::("dotnet_version"); + + // Set dotnet version env var if explicitly specified + if let Some(version) = dotnet_version { + // SAFETY: We are single-threaded at this point and no other code is reading + // this environment variable concurrently. + unsafe { + std::env::set_var("SPACETIMEDB_DOTNET_VERSION", version); + } + } run_build(module_path, lint_dir, build_debug, features) } diff --git a/crates/cli/src/subcommands/init.rs b/crates/cli/src/subcommands/init.rs index 31d927720f1..ffa5c102ad3 100644 --- a/crates/cli/src/subcommands/init.rs +++ b/crates/cli/src/subcommands/init.rs @@ -134,6 +134,8 @@ pub struct InitOptions { pub skip_next_steps: bool, /// When true, configure C# projects for NativeAOT-LLVM compilation. pub native_aot: bool, + /// Explicit .NET major version override (e.g. 8 or 10). When set, skips auto-detection. + pub dotnet_version: Option, } impl InitOptions { @@ -150,6 +152,9 @@ impl InitOptions { non_interactive: args.get_flag("non-interactive"), skip_next_steps: false, native_aot: args.get_flag("native-aot"), + dotnet_version: args + .get_one::("dotnet-version") + .and_then(|s| s.parse::().ok()), } } } @@ -199,6 +204,12 @@ pub fn cli() -> clap::Command { .action(clap::ArgAction::SetTrue) .help("Configure C# project for NativeAOT-LLVM compilation (experimental, Windows only)"), ) + .arg( + Arg::new("dotnet-version") + .long("dotnet-version") + .value_name("VERSION") + .help("Target .NET SDK major version for C# projects (e.g. 8 or 10). Auto-detected when omitted."), + ) } pub async fn fetch_templates_list() -> anyhow::Result> { @@ -530,21 +541,42 @@ pub async fn exec_with_options(config: &mut Config, options: &InitOptions) -> an template_config.use_local = use_local; + // For C# projects, resolve the target .NET version before scaffolding. + // This may prompt the user interactively if multiple SDKs are installed. + let dotnet_major = if template_config.server_lang == Some(ServerLanguage::Csharp) { + Some(resolve_dotnet_major(options, is_interactive)?) + } else { + None + }; + ensure_empty_directory( &template_config.project_name, &template_config.project_path, is_server_only, )?; - init_from_template(&template_config, &template_config.project_path, is_server_only).await?; - - // Add NativeAOT-LLVM package references to C# projects if --native-aot was specified - if options.native_aot && template_config.server_lang == Some(ServerLanguage::Csharp) { + init_from_template( + &template_config, + &template_config.project_path, + is_server_only, + dotnet_major, + ) + .await?; + + // Add NativeAOT-LLVM project configuration for C# projects when: + // - --native-aot was explicitly specified, OR + // - .NET 10 was selected/detected as the target + let needs_native_aot = if template_config.server_lang == Some(ServerLanguage::Csharp) { + options.native_aot || dotnet_major == Some(10) + } else { + false + }; + if needs_native_aot { let server_dir = template_config.project_path.join("spacetimedb"); - add_native_aot_packages_to_csproj(&server_dir)?; + add_native_aot_packages_to_csproj(&server_dir, dotnet_major)?; } let default_server = config.default_server_name().unwrap_or("maincloud"); - if let Some(path) = create_default_spacetime_config_if_missing(&project_path, options.native_aot, default_server)? { + if let Some(path) = create_default_spacetime_config_if_missing(&project_path, needs_native_aot, default_server)? { println!("{} Created {}", "✓".green(), path.display()); } @@ -1331,13 +1363,14 @@ pub async fn init_from_template( config: &TemplateConfig, project_path: &Path, is_server_only: bool, + dotnet_major: Option, ) -> anyhow::Result<()> { println!("{}", "Initializing project from template...".cyan()); match config.template_type { - TemplateType::Builtin => init_builtin(config, project_path, is_server_only)?, + TemplateType::Builtin => init_builtin(config, project_path, is_server_only, dotnet_major)?, TemplateType::GitHub => init_github_template(config, project_path, is_server_only)?, - TemplateType::Empty => init_empty(config, project_path)?, + TemplateType::Empty => init_empty(config, project_path, dotnet_major)?, } // Install AI assistant rules for multiple editors/tools @@ -1348,7 +1381,12 @@ pub async fn init_from_template( Ok(()) } -fn init_builtin(config: &TemplateConfig, project_path: &Path, is_server_only: bool) -> anyhow::Result<()> { +fn init_builtin( + config: &TemplateConfig, + project_path: &Path, + is_server_only: bool, + dotnet_major: Option, +) -> anyhow::Result<()> { let template_def = config .template_def .as_ref() @@ -1417,6 +1455,16 @@ fn init_builtin(config: &TemplateConfig, project_path: &Path, is_server_only: bo None => {} } + // For C# projects targeting .NET 10, override the template global.json + // (the embedded template ships with 8.0.100 which is wrong for .NET 10). + if config.server_lang == Some(ServerLanguage::Csharp) && dotnet_major == Some(10) { + let global_json_path = server_dir.join("global.json"); + let net10_global_json = + "{\n \"sdk\": {\n \"version\": \"10.0.100\",\n \"rollForward\": \"latestMinor\"\n }\n}\n"; + std::fs::write(&global_json_path, net10_global_json)?; + println!("Updating global.json to use .NET 10 (NativeAOT-LLVM)."); + } + Ok(()) } @@ -1454,7 +1502,7 @@ fn init_github_template(config: &TemplateConfig, project_path: &Path, is_server_ Ok(()) } -fn init_empty(config: &TemplateConfig, project_path: &Path) -> anyhow::Result<()> { +fn init_empty(config: &TemplateConfig, project_path: &Path, dotnet_major: Option) -> anyhow::Result<()> { match config.server_lang { Some(ServerLanguage::Rust) => { println!("Setting up Rust server..."); @@ -1464,7 +1512,7 @@ fn init_empty(config: &TemplateConfig, project_path: &Path) -> anyhow::Result<() Some(ServerLanguage::Csharp) => { println!("Setting up C# server..."); let server_dir = project_path.join("spacetimedb"); - init_empty_csharp_server(&server_dir, &config.project_name)?; + init_empty_csharp_server(&server_dir, &config.project_name, dotnet_major)?; } Some(ServerLanguage::TypeScript) => { println!("Setting up TypeScript server..."); @@ -1488,8 +1536,8 @@ fn init_empty_rust_server(server_dir: &Path, project_name: &str) -> anyhow::Resu Ok(()) } -fn init_empty_csharp_server(server_dir: &Path, _project_name: &str) -> anyhow::Result<()> { - init_csharp_project(server_dir) +fn init_empty_csharp_server(server_dir: &Path, _project_name: &str, dotnet_major: Option) -> anyhow::Result<()> { + init_csharp_project(server_dir, dotnet_major) } fn init_empty_typescript_server(server_dir: &Path, project_name: &str) -> anyhow::Result<()> { @@ -1600,6 +1648,83 @@ fn check_for_cargo() -> bool { false } +/// Returns the set of major .NET SDK versions installed (e.g. {8, 10}). +fn detect_installed_dotnet_majors() -> Vec { + let output = duct::cmd!("dotnet", "--list-sdks").read().unwrap_or_default(); + let mut majors: Vec = output + .lines() + .filter_map(|line| { + // Each line looks like: "8.0.100 [C:\Program Files\dotnet\sdk]" + let version_str = line.split_whitespace().next()?; + crate::tasks::csharp::parse_major_version(version_str) + }) + .collect(); + majors.sort(); + majors.dedup(); + majors +} + +/// Determine the target .NET major version for a C# project. +/// +/// Resolution order: +/// 1. Explicit `--dotnet-version` flag +/// 2. Interactive prompt (if multiple supported versions are installed) +/// 3. Auto-detect from `dotnet --version` (single supported version or non-interactive) +fn resolve_dotnet_major(options: &InitOptions, is_interactive: bool) -> anyhow::Result { + // 1. Explicit flag takes priority. + if let Some(v) = options.dotnet_version { + match v { + 8 | 10 => return Ok(v), + _ => anyhow::bail!("Unsupported --dotnet-version {v}. Supported values: 8, 10."), + } + } + + // --native-aot is for .NET 8 AOT builds (NativeAOT-LLVM with net8.0 TFM). + // .NET 10 always uses NativeAOT-LLVM, no flag needed. + if options.native_aot { + return Ok(8); + } + + let installed = detect_installed_dotnet_majors(); + let supported: Vec = installed.iter().copied().filter(|&v| v == 8 || v == 10).collect(); + + match supported.len() { + 0 => { + // Fall back to whatever `dotnet --version` reports. + let ver = duct::cmd!("dotnet", "--version").read().unwrap_or_default(); + crate::tasks::csharp::parse_major_version(&ver).ok_or_else(|| { + anyhow::anyhow!("Could not detect .NET SDK version. Please install .NET SDK 8.0 or 10.0.") + }) + } + 1 => Ok(supported[0]), + _ => { + // Multiple supported versions — prompt if interactive, else use highest. + if is_interactive { + let theme = ColorfulTheme::default(); + let choices: Vec = supported + .iter() + .map(|v| match v { + 8 => ".NET 8 (JIT — stable, uses wasi-experimental workload)".to_string(), + 10 => ".NET 10 (NativeAOT-LLVM — experimental, better performance)".to_string(), + other => format!(".NET {other}"), + }) + .collect(); + + let selection = Select::with_theme(&theme) + .with_prompt("Multiple .NET SDKs found. Which version should this C# module target?") + .items(&choices) + .default(choices.len() - 1) // Default to highest (typically .NET 10) + .interact()?; + + Ok(supported[selection]) + } else { + // Non-interactive: use the highest supported version. + Ok(*supported.last().unwrap()) + } + } + } +} + fn check_for_dotnet() -> bool { use std::fmt::Write; @@ -1694,6 +1819,19 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> anyhow::Result anyhow::Result<()> { Ok(()) } -pub fn init_csharp_project(project_path: &Path) -> anyhow::Result<()> { +pub fn init_csharp_project(project_path: &Path, dotnet_major: Option) -> anyhow::Result<()> { + check_for_dotnet(); + check_for_git(); + + let global_json = match dotnet_major { + Some(10) => { + println!("Configuring for .NET 10 (NativeAOT-LLVM)."); + "{\n \"sdk\": {\n \"version\": \"10.0.100\",\n \"rollForward\": \"latestMinor\"\n }\n}\n" + } + _ => { + include_str!("../../../../templates/basic-cs/spacetimedb/global.json") + } + }; + let export_files = vec![ ( include_str!("../../../../templates/basic-cs/spacetimedb/StdbModule.csproj"), @@ -1741,15 +1892,9 @@ pub fn init_csharp_project(project_path: &Path) -> anyhow::Result<()> { include_str!("../../../../templates/basic-cs/spacetimedb/Lib.cs"), "Lib.cs", ), - ( - include_str!("../../../../templates/basic-cs/spacetimedb/global.json"), - "global.json", - ), + (global_json, "global.json"), ]; - check_for_dotnet(); - check_for_git(); - for data_file in export_files { let path = project_path.join(data_file.1); create_directory(path.parent().unwrap())?; @@ -1759,9 +1904,19 @@ pub fn init_csharp_project(project_path: &Path) -> anyhow::Result<()> { Ok(()) } -/// Adds NativeAOT-LLVM package references to an existing C# .csproj file and creates NuGet.Config. -/// This is called when `--native-aot` is specified during `spacetime init`. -fn add_native_aot_packages_to_csproj(project_path: &Path) -> anyhow::Result<()> { +/// Adds NativeAOT-LLVM project configuration to an existing C# .csproj file and creates NuGet.Config. +/// +/// The configuration differs depending on the target .NET version: +/// +/// **.NET 8 AOT** (`--native-aot`): Keeps `net8.0` TFM and adds explicit ILCompiler.LLVM 8.0.0-* +/// package references, gated on `EXPERIMENTAL_WASM_AOT=1`. +/// +/// **.NET 10 AOT**: Replaces the TFM with `net10.0` directly (no conditional needed since the +/// project is definitively targeting .NET 10). ILCompiler.LLVM refs are provided transitively +/// by the SpacetimeDB.Runtime NuGet package. +/// +/// Both paths need a NuGet.Config with the dotnet-experimental feed for ILCompiler.LLVM resolution. +fn add_native_aot_packages_to_csproj(project_path: &Path, dotnet_major: Option) -> anyhow::Result<()> { let csproj_path = project_path.join("StdbModule.csproj"); if !csproj_path.exists() { anyhow::bail!("Could not find StdbModule.csproj at {}", csproj_path.display()); @@ -1769,31 +1924,37 @@ fn add_native_aot_packages_to_csproj(project_path: &Path) -> anyhow::Result<()> let content = std::fs::read_to_string(&csproj_path)?; - // The NativeAOT-LLVM ItemGroup to add - let native_aot_item_group = r#" + let new_content = if dotnet_major == Some(8) { + // .NET 8 AOT: keep net8.0 TFM, add explicit ILCompiler.LLVM package references. + let native_aot_config = r#" - "#; - - // Insert the ItemGroup before the closing tag - let new_content = if let Some(pos) = content.rfind("") { - let (before, after) = content.split_at(pos); - format!("{}{}{}", before.trim_end(), native_aot_item_group, after) + if let Some(pos) = content.rfind("") { + let (before, after) = content.split_at(pos); + format!("{}{}{}", before.trim_end(), native_aot_config, after) + } else { + anyhow::bail!("Invalid .csproj file: missing tag"); + } } else { - anyhow::bail!("Invalid .csproj file: missing tag"); + // .NET 10 AOT: directly set TFM to net10.0 (no conditional needed). + // ILCompiler.LLVM comes transitively via the SpacetimeDB.Runtime NuGet package. + content.replace( + "net8.0", + "net10.0", + ) }; std::fs::write(&csproj_path, new_content)?; println!( - "{} Added NativeAOT-LLVM package references to {}", + "{} Added NativeAOT-LLVM project configuration to {}", "✓".green(), csproj_path.display() ); - // Create NuGet.Config with the dotnet-experimental feed required for NativeAOT-LLVM packages + // Create NuGet.Config with the dotnet-experimental feed required for ILCompiler.LLVM packages let nuget_config_path = project_path.join("NuGet.Config"); let nuget_config_content = r#" @@ -1802,6 +1963,17 @@ fn add_native_aot_packages_to_csproj(project_path: &Path) -> anyhow::Result<()> + + + + + + + + + + + "#; @@ -1884,7 +2056,7 @@ pub async fn exec_init_rust(args: &ArgMatches) -> anyhow::Result<()> { pub async fn exec_init_csharp(args: &ArgMatches) -> anyhow::Result<()> { let project_path = args.get_one::("project-path").unwrap(); - init_csharp_project(project_path)?; + init_csharp_project(project_path, None)?; println!( "{}", diff --git a/crates/cli/src/subcommands/publish.rs b/crates/cli/src/subcommands/publish.rs index dd710da2832..078383e4481 100644 --- a/crates/cli/src/subcommands/publish.rs +++ b/crates/cli/src/subcommands/publish.rs @@ -96,6 +96,7 @@ pub fn build_publish_schema(command: &clap::Command) -> Result( let org_opt = command_config.get_one::("organization")?; let org = org_opt.as_deref(); let native_aot = command_config.get_one::("native_aot")?.unwrap_or(false); + let dotnet_version = command_config.get_one::("dotnet_version"); // If the user didn't specify an identity and we didn't specify an anonymous identity, then // we want to use the default identity @@ -558,6 +566,14 @@ async fn execute_publish_configs<'a>( env::set_var("EXPERIMENTAL_WASM_AOT", "1"); } } + // Pass explicit dotnet version to C# build system if specified + if let Ok(Some(version)) = dotnet_version.as_ref() { + // SAFETY: We are single-threaded at this point and no other code is reading + // this environment variable concurrently. + unsafe { + env::set_var("SPACETIMEDB_DOTNET_VERSION", version); + } + } build::exec_with_argstring( path_to_project .as_ref() diff --git a/crates/cli/src/tasks/csharp.rs b/crates/cli/src/tasks/csharp.rs index 5df8b730448..ec245ca2910 100644 --- a/crates/cli/src/tasks/csharp.rs +++ b/crates/cli/src/tasks/csharp.rs @@ -4,10 +4,20 @@ use std::ffi::OsString; use std::fs; use std::path::{Path, PathBuf}; -fn parse_major_version(version: &str) -> Option { +pub(crate) fn parse_major_version(version: &str) -> Option { version.split('.').next()?.parse::().ok() } +/// Describes which C# build path to use. +enum CsharpBuildPath { + /// .NET 8 JIT via the `wasi-experimental` workload (Mono WASM). + Net8Jit, + /// .NET 8 NativeAOT-LLVM (opt-in via `--native-aot`). + Net8Aot, + /// .NET 10 NativeAOT-LLVM (auto-detected, only available path for .NET 10). + Net10Aot, +} + pub(crate) fn build_csharp(project_path: &Path, build_debug: bool) -> anyhow::Result { // All `dotnet` commands must execute in the project directory, otherwise // global.json won't have any effect and wrong .NET SDK might be picked. @@ -17,47 +27,110 @@ pub(crate) fn build_csharp(project_path: &Path, build_debug: bool) -> anyhow::Re }; } - // Check if the `wasi-experimental` workload is installed. Unfortunately, we - // have to do this by inspecting the human-readable output. There is a - // hidden `--machine-readable` flag but it also mixes in human-readable - // output as well as unnecessarily updates various unrelated manifests. - match dotnet!("workload", "list").read() { - Ok(workloads) if workloads.contains("wasi-experimental") => {} - Ok(_) => { - // If wasi-experimental is not found, first check if we're running - // on .NET SDK 8.0. We can't even install that workload on older - // versions, and we don't support .NET 9.0 yet, so this helps to - // provide a nicer message than "Workload ID wasi-experimental is not recognized.". - let version = dotnet!("--version").read().unwrap_or_default(); - if parse_major_version(&version) != Some(8) { - anyhow::bail!(concat!( - ".NET SDK 8.0 is required, but found {version}.\n", - "If you have multiple versions of .NET SDK installed, configure your project using https://learn.microsoft.com/en-us/dotnet/core/tools/global-json." - )); - } + let native_aot_flag = std::env::var_os("EXPERIMENTAL_WASM_AOT").is_some_and(|v| v == "1"); - // Finally, try to install the workload ourselves. On some systems - // this might require elevated privileges, so print a nice error - // message if it fails. - dotnet!( - "workload", - "install", - "wasi-experimental", - "--skip-manifest-update" - ) - .stderr_capture() - .run() - .context(concat!( - "Couldn't install the required wasi-experimental workload.\n", - "You might need to install it manually by running `dotnet workload install wasi-experimental` with privileged rights." - ))?; - } + // Check for explicit dotnet version override from CLI (--dotnet-version flag) + // This takes precedence over auto-detection. + let dotnet_version_override = std::env::var("SPACETIMEDB_DOTNET_VERSION").ok(); + + // Detect the .NET SDK version. Run from project directory only if global.json exists, + // otherwise run from current directory. .NET 10 SDK crashes if global.json is missing. + let global_json_exists = project_path.join("global.json").exists(); + let dotnet_version_result = if global_json_exists { + dotnet!("--version").read() + } else { + duct::cmd!("dotnet", "--version").read() + }; + let dotnet_version_str = match dotnet_version_result { + Ok(v) => v, Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - anyhow::bail!("dotnet not found in PATH. Please install .NET SDK 8.0.") + anyhow::bail!("dotnet not found in PATH. Please install .NET SDK 8.0 or 10.0.") } Err(error) => anyhow::bail!("{error}"), }; + // Use explicit version if provided, otherwise auto-detect from dotnet --version + let dotnet_major = dotnet_version_override + .as_deref() + .and_then(|v| v.parse().ok()) + .or_else(|| parse_major_version(&dotnet_version_str)); + + // Determine the build path based on SDK version and --native-aot flag. + let build_path = match (dotnet_major, native_aot_flag) { + // .NET 10: always use NativeAOT-LLVM, no flag needed. + (Some(10), _) => { + if native_aot_flag { + println!("Note: --native-aot is not needed with .NET 10 (NativeAOT-LLVM is used automatically)."); + } + CsharpBuildPath::Net10Aot + } + // .NET 8 with --native-aot: use NativeAOT-LLVM with .NET 8 ILCompiler packages. + (Some(8), true) => CsharpBuildPath::Net8Aot, + // .NET 8 without flag: use the existing wasi-experimental JIT path. + (Some(8), false) => CsharpBuildPath::Net8Jit, + // Unsupported version. + _ => { + anyhow::bail!( + "Unsupported .NET SDK version: {dotnet_version_str}. SpacetimeDB requires .NET SDK 8.0 or 10.0.\n\ + If you have multiple versions installed, configure your project using \ + https://learn.microsoft.com/en-us/dotnet/core/tools/global-json, \ + or use --dotnet-version to specify the target version explicitly." + ); + } + }; + + // For NativeAOT paths, ensure EXPERIMENTAL_WASM_AOT is set in the environment so MSBuild + // conditionals in .csproj/.props/.targets files activate correctly. + match &build_path { + CsharpBuildPath::Net8Aot | CsharpBuildPath::Net10Aot => { + // SAFETY: We are single-threaded at this point and no other code is reading + // this environment variable concurrently. + unsafe { + std::env::set_var("EXPERIMENTAL_WASM_AOT", "1"); + } + } + CsharpBuildPath::Net8Jit => {} + } + + // .NET 10 SDK crashes if global.json doesn't exist in the working directory. + // Create one in the project directory if using .NET 10 and none exists. + if matches!(build_path, CsharpBuildPath::Net10Aot) { + let global_json_path = project_path.join("global.json"); + if !global_json_path.exists() { + let global_json_content = r#"{"sdk":{"version":"10.0.100","rollForward":"latestMinor"}}"#; + fs::write(&global_json_path, global_json_content)?; + } + } + + // For the JIT path, ensure the wasi-experimental workload is installed. + if matches!(build_path, CsharpBuildPath::Net8Jit) { + // Check if the `wasi-experimental` workload is installed. Unfortunately, we + // have to do this by inspecting the human-readable output. There is a + // hidden `--machine-readable` flag but it also mixes in human-readable + // output as well as unnecessarily updates various unrelated manifests. + match dotnet!("workload", "list").read() { + Ok(workloads) if workloads.contains("wasi-experimental") => {} + Ok(_) => { + // Finally, try to install the workload ourselves. On some systems + // this might require elevated privileges, so print a nice error + // message if it fails. + dotnet!( + "workload", + "install", + "wasi-experimental", + "--skip-manifest-update" + ) + .stderr_capture() + .run() + .context(concat!( + "Couldn't install the required wasi-experimental workload.\n", + "You might need to install it manually by running `dotnet workload install wasi-experimental` with privileged rights." + ))?; + } + Err(error) => anyhow::bail!("{error}"), + }; + } + let config_name = if build_debug { "Debug" } else { "Release" }; // Ensure the project path exists. @@ -68,16 +141,21 @@ pub(crate) fn build_csharp(project_path: &Path, build_debug: bool) -> anyhow::Re ) })?; - // run dotnet publish using cmd macro + // JIT and AOT builds use the same `dotnet publish` command. + // Build-specific configuration (TFM, AOT settings, ILCompiler packages) + // is handled by build_path detection and MSBuild props/targets. dotnet!("publish", "-c", config_name, "-v", "quiet").run()?; - // check if file exists - let subdir = if std::env::var_os("EXPERIMENTAL_WASM_AOT").is_some_and(|v| v == "1") { - "publish" - } else { - "AppBundle" + // Determine output path based on build path. + // Both JIT and AOT builds produce StdbModule.wasm, but in different subdirectories: + // - JIT (wasi-experimental): AppBundle/StdbModule.wasm + // - AOT (NativeAOT-LLVM): publish/StdbModule.wasm + let (target_framework, subdir) = match &build_path { + CsharpBuildPath::Net10Aot => ("net10.0", "publish"), + CsharpBuildPath::Net8Aot => ("net8.0", "publish"), + CsharpBuildPath::Net8Jit => ("net8.0", "AppBundle"), }; - // TODO: This code looks for build outputs in both `bin` and `bin~` as output directories. @bfops feels like we shouldn't have to look for `bin~`, since the `~` suffix is just intended to cause Unity to ignore directories, and that shouldn't be relevant here. We do think we've seen `bin~` appear though, and it's not harmful to do the extra checks, so we're merging for now due to imminent code freeze. At some point, it would be good to figure out if we do actually see `bin~` in module directories, and where that's coming from (which could suggest a bug). + // check for the old .NET 7 path for projects that haven't migrated yet let bad_output_paths = [ project_path.join(format!("bin/{config_name}/net7.0/StdbModule.wasm")), @@ -91,14 +169,36 @@ pub(crate) fn build_csharp(project_path: &Path, build_debug: bool) -> anyhow::Re )); } let possible_output_paths = [ - project_path.join(format!("bin/{config_name}/net8.0/wasi-wasm/{subdir}/StdbModule.wasm")), - project_path.join(format!("bin~/{config_name}/net8.0/wasi-wasm/{subdir}/StdbModule.wasm")), + // Standard publish output paths (JIT and some AOT builds) + project_path.join(format!( + "bin/{config_name}/{target_framework}/wasi-wasm/{subdir}/StdbModule.wasm" + )), + project_path.join(format!( + "bin~/{config_name}/{target_framework}/wasi-wasm/{subdir}/StdbModule.wasm" + )), + // NativeAOT-LLVM outputs to 'native' subdirectory instead of 'publish' + project_path.join(format!( + "bin/{config_name}/{target_framework}/wasi-wasm/native/StdbModule.wasm" + )), + project_path.join(format!( + "bin~/{config_name}/{target_framework}/wasi-wasm/native/StdbModule.wasm" + )), + // Also check for raw wasm output without wasi-wasm RID folder (NativeAOT-LLVM sometimes does this) + project_path.join(format!("bin/{config_name}/{target_framework}/native/StdbModule.wasm")), + project_path.join(format!("bin~/{config_name}/{target_framework}/native/StdbModule.wasm")), ]; - if possible_output_paths.iter().all(|p| p.exists()) { - anyhow::bail!(concat!( - "For some reason, your project has both a `bin` and a `bin~` folder.\n", - "I don't know which to use, so please delete both and rerun this command so that we can see which is up-to-date." - )); + // Check if both bin and bin~ variants exist for the same output path (indicates a conflict) + for i in (0..possible_output_paths.len()).step_by(2) { + if i + 1 < possible_output_paths.len() { + let bin_path = &possible_output_paths[i]; + let bin_tilde_path = &possible_output_paths[i + 1]; + if bin_path.exists() && bin_tilde_path.exists() { + anyhow::bail!(concat!( + "For some reason, your project has both a `bin` and a `bin~` folder.\n", + "I don't know which to use, so please delete both and rerun this command so that we can see which is up-to-date." + )); + } + } } for output_path in possible_output_paths { if output_path.exists() { diff --git a/crates/core/src/host/wasmtime/wasmtime_module.rs b/crates/core/src/host/wasmtime/wasmtime_module.rs index 0690120eb16..060bedc448b 100644 --- a/crates/core/src/host/wasmtime/wasmtime_module.rs +++ b/crates/core/src/host/wasmtime/wasmtime_module.rs @@ -233,6 +233,16 @@ impl module_host_actor::WasmInstancePre for WasmtimeModule { set_store_fuel(&mut store, FunctionBudget::DEFAULT_BUDGET.into()); store.set_epoch_deadline(EPOCH_TICKS_PER_SECOND); + // NativeAOT-LLVM modules are WASI reactors that export `_initialize` + // to set up the native runtime. This must be called before any other exports. + // Traditional .NET 8 WASI modules export `_start` instead (which is not called here). + if let Ok(init) = instance.get_typed_func::<(), ()>(&mut store, "_initialize") { + call_sync_typed_func(&init, &mut store, ()).map_err(|err| InitializationError::RuntimeError { + err, + func: "_initialize".to_owned(), + })?; + } + for preinit in &func_names.preinits { let func = instance.get_typed_func::<(), ()>(&mut store, preinit).unwrap(); call_sync_typed_func(&func, &mut store, ()).map_err(|err| InitializationError::RuntimeError { diff --git a/crates/guard/src/lib.rs b/crates/guard/src/lib.rs index af937910a54..eead324c936 100644 --- a/crates/guard/src/lib.rs +++ b/crates/guard/src/lib.rs @@ -42,7 +42,10 @@ fn target_dir() -> PathBuf { /// Returns the expected CLI binary path. fn cli_binary_path() -> PathBuf { - let profile = "release"; + // Use CARGO_BUILD_PROFILE if set, otherwise default to release for backwards compatibility + let profile = env::var("CARGO_BUILD_PROFILE") + .or_else(|_| env::var("PROFILE")) + .unwrap_or_else(|_| "release".to_string()); let cli_name = if cfg!(windows) { "spacetimedb-cli.exe" } else { diff --git a/crates/smoketests/src/csharp.rs b/crates/smoketests/src/csharp.rs index 5b832f4781f..e4b33adba70 100644 --- a/crates/smoketests/src/csharp.rs +++ b/crates/smoketests/src/csharp.rs @@ -172,12 +172,17 @@ pub(crate) fn prepare_csharp_module(module_path: &Path) -> Result<()> { + + + + + diff --git a/crates/smoketests/tests/smoketests/csharp_aot_module.rs b/crates/smoketests/tests/smoketests/csharp_aot_module.rs index 56df1100861..f0d551c454a 100644 --- a/crates/smoketests/tests/smoketests/csharp_aot_module.rs +++ b/crates/smoketests/tests/smoketests/csharp_aot_module.rs @@ -1,30 +1,54 @@ #![allow(clippy::disallowed_macros)] use spacetimedb_guard::ensure_binaries_built; -use spacetimedb_smoketests::{have_emscripten, require_dotnet, workspace_root}; +use spacetimedb_smoketests::{require_dotnet, workspace_root}; use std::process::Command; +/// Detect the major version of the active .NET SDK. +fn dotnet_major_version() -> Option { + Command::new("dotnet") + .arg("--version") + .output() + .ok() + .filter(|o| o.status.success()) + .and_then(|o| { + let v = String::from_utf8_lossy(&o.stdout); + v.trim().split('.').next()?.parse::().ok() + }) +} + /// Test NativeAOT-LLVM build path for C# modules. -/// Requires emscripten to be installed. -/// Only runs on Windows since runtime.linux-x64.Microsoft.DotNet.ILCompiler.LLVM -/// is not available on the dotnet-experimental NuGet feed. +/// +/// Platform support depends on the .NET SDK version: +/// - .NET 8 AOT: Windows-only (runtime.linux-x64.Microsoft.DotNet.ILCompiler.LLVM +/// 8.0.0-* was never published to the dotnet-experimental NuGet feed). +/// - .NET 10 AOT: Windows and Linux (both runtime packages are available). +/// +/// NativeAOT-LLVM targets WASI and uses WASI SDK (clang), not the wasi-experimental +/// workload or emscripten. WASI SDK is auto-downloaded by SpacetimeDB.Runtime.targets. +/// The user must set EXPERIMENTAL_WASM_AOT=1 to enable the AOT build path. #[test] fn test_build_csharp_module_aot() { require_dotnet!(); - // NativeAOT-LLVM is only available on Windows - if std::env::consts::OS != "windows" { - eprintln!("Skipping AOT test - NativeAOT-LLVM for .NET 8 only available on Windows"); + let major = dotnet_major_version(); + let target_framework = match major { + Some(v) if v >= 10 => "net10.0", + Some(8) => "net8.0", + _ => { + eprintln!("Skipping AOT test - unsupported .NET SDK version: {:?}", major); + return; + } + }; + + // .NET 8 ILCompiler.LLVM packages are only available for Windows. + // .NET 10+ ILCompiler.LLVM packages are available for Windows and Linux. + if target_framework == "net8.0" && std::env::consts::OS != "windows" { + eprintln!("Skipping .NET 8 AOT test - ILCompiler.LLVM 8.0.0-* only available on Windows"); return; } - - // Check for emscripten - fail with helpful message if not available - // Uses have_emscripten() which checks for both `emcc` and `emcc.bat` on Windows - if !have_emscripten() { - panic!( - "NativeAOT-LLVM test requires emscripten but it was not found.\n\ - Install from: https://emscripten.org/docs/getting_started/downloads.html\n\ - Or ensure `emcc` is in your PATH." - ); + if std::env::consts::OS != "windows" && std::env::consts::OS != "linux" { + eprintln!("Skipping AOT test - NativeAOT-LLVM only available on Windows and Linux"); + return; } let workspace = workspace_root(); @@ -57,7 +81,9 @@ fn test_build_csharp_module_aot() { // This ensures subsequent tests can clear NuGet locals without conflicts drop(nuget_packages_dir); - // Verify StdbModule.wasm was produced - let wasm_path = workspace.join("modules/sdk-test-cs/bin/Release/net8.0/wasi-wasm/publish/StdbModule.wasm"); + // Verify StdbModule.wasm was produced at the correct TFM-specific output path + let wasm_path = workspace.join(format!( + "modules/sdk-test-cs/bin/Release/{target_framework}/wasi-wasm/publish/StdbModule.wasm" + )); assert!(wasm_path.exists(), "StdbModule.wasm not found at {:?}", wasm_path); } diff --git a/crates/smoketests/tests/smoketests/csharp_module.rs b/crates/smoketests/tests/smoketests/csharp_module.rs index 6ad79e001be..6a2fc7fe343 100644 --- a/crates/smoketests/tests/smoketests/csharp_module.rs +++ b/crates/smoketests/tests/smoketests/csharp_module.rs @@ -42,12 +42,14 @@ fn test_build_csharp_module() { // Create temp directory for the project let tmpdir = tempfile::tempdir().expect("Failed to create temp directory"); - // Initialize C# project + // Initialize C# project with explicit .NET 8 to test JIT path let output = Command::new(&cli_path) .args([ "init", "--non-interactive", "--lang=csharp", + "--dotnet-version", + "8", "--project-path", tmpdir.path().to_str().unwrap(), "csharp-project", @@ -68,6 +70,8 @@ fn test_build_csharp_module() { let packed_projects = ["BSATN.Runtime", "Runtime"]; let mut sources = String::from(" \n \n"); + // Add experimental NuGet feed for Microsoft.DotNet.ILCompiler.LLVM packages + sources.push_str(" \n"); let mut mappings = String::new(); for project in &packed_projects { @@ -83,6 +87,8 @@ fn test_build_csharp_module() { package_name, package_name )); } + // Add mappings for experimental packages + mappings.push_str(" \n \n \n \n"); // Add fallback for other packages mappings.push_str(" \n \n \n"); diff --git a/crates/smoketests/tests/smoketests/quickstart.rs b/crates/smoketests/tests/smoketests/quickstart.rs index fedf7a53b4b..a1be147ee3b 100644 --- a/crates/smoketests/tests/smoketests/quickstart.rs +++ b/crates/smoketests/tests/smoketests/quickstart.rs @@ -111,11 +111,24 @@ fn create_nuget_config(sources: &[(String, PathBuf)], mappings: &[(String, Strin source_lines.push_str(&format!(" \n", key, path.display())); } + // Group patterns by source while preserving source order (first seen first) + let mut patterns_by_source: std::collections::BTreeMap> = std::collections::BTreeMap::new(); + let mut source_order: Vec = Vec::new(); for (key, pattern) in mappings { - mapping_lines.push_str(&format!( - " \n \n \n", - key, pattern - )); + if !patterns_by_source.contains_key(key) { + source_order.push(key.clone()); + } + patterns_by_source.entry(key.clone()).or_default().push(pattern.clone()); + } + + // Write mappings in insertion order (ensures nuget.org with * comes last) + for key in source_order { + let patterns = patterns_by_source.get(&key).unwrap(); + mapping_lines.push_str(&format!(" \n", key)); + for pattern in patterns { + mapping_lines.push_str(&format!(" \n", pattern)); + } + mapping_lines.push_str(" \n"); } format!( @@ -201,6 +214,44 @@ fn override_nuget_package(project_dir: &Path, package: &str, source_dir: &Path, mappings.push((package.to_string(), package.to_string())); } + // Ensure dotnet-experimental feed exists (needed for NativeAOT-LLVM ILCompiler packages) + if !sources.iter().any(|(k, _)| k == "dotnet-experimental") { + sources.push(( + "dotnet-experimental".to_string(), + PathBuf::from( + "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json", + ), + )); + } + if !mappings.iter().any(|(k, _)| k == "dotnet-experimental") { + mappings.push(( + "dotnet-experimental".to_string(), + "Microsoft.DotNet.ILCompiler.LLVM".to_string(), + )); + mappings.push(("dotnet-experimental".to_string(), "runtime.*".to_string())); + } + + // Add package source mappings for SpacetimeDB packages to local sources + // This must come BEFORE the nuget.org wildcard mapping to ensure local packages are used + let local_runtime_source = sources + .iter() + .find(|(k, _)| k.contains("runtime") || k.contains("Runtime")) + .map(|(k, _)| k.clone()); + if let Some(source_key) = local_runtime_source { + if !mappings + .iter() + .any(|(k, p)| k == &source_key && p == "SpacetimeDB.Runtime") + { + mappings.push((source_key.clone(), "SpacetimeDB.Runtime".to_string())); + } + if !mappings + .iter() + .any(|(k, p)| k == &source_key && p == "SpacetimeDB.BSATN.Runtime") + { + mappings.push((source_key, "SpacetimeDB.BSATN.Runtime".to_string())); + } + } + // Ensure nuget.org fallback exists if !sources.iter().any(|(k, _)| k == "nuget.org") { sources.push(( @@ -214,6 +265,7 @@ fn override_nuget_package(project_dir: &Path, package: &str, source_dir: &Path, // Write config let config = create_nuget_config(&sources, &mappings); + eprintln!("Generated nuget.config at {:?}:\n{}", nuget_config_path, config); fs::write(&nuget_config_path, config)?; let _ = Command::new("dotnet") @@ -237,9 +289,16 @@ fn parse_nuget_config(content: &str) -> (Vec<(String, PathBuf)>, Vec<(String, St sources.push((cap[1].to_string(), PathBuf::from(&cap[2]))); } - let mapping_re = regex::Regex::new(r#"\s*(.*?)<\/packageSource>"#).unwrap(); + let pattern_re = regex::Regex::new(r#" Result<(TempDir, PathBu let project_name = format!("test-{}", template_id); let project_path = tmpdir.path().join(&project_name); - test.spacetime(&[ - "init", - "--template", - template_id, - "--project-path", - project_path.to_str().unwrap(), - "--non-interactive", - &project_name, - ]) - .with_context(|| format!("spacetime init --template {} failed", template_id))?; + // For C# templates, force .NET 8 to match template TFM and avoid + // CLI auto-detecting .NET 10 in CI environments. + let is_csharp = template_id.ends_with("-cs"); + let init_args: Vec<&str> = if is_csharp { + vec![ + "init", + "--template", + template_id, + "--project-path", + project_path.to_str().unwrap(), + "--non-interactive", + "--dotnet-version", + "8", + &project_name, + ] + } else { + vec![ + "init", + "--template", + template_id, + "--project-path", + project_path.to_str().unwrap(), + "--non-interactive", + &project_name, + ] + }; + + test.spacetime(&init_args) + .with_context(|| format!("spacetime init --template {} failed", template_id))?; if !project_path.exists() { bail!("Project directory not created for template {}", template_id); @@ -574,7 +593,17 @@ fn setup_csharp_nuget(project_path: &Path) -> Result { + + + + + + + + + + "#, ) @@ -737,6 +766,8 @@ fn test_csharp_template(test: &Smoketest, template: &Template, project_path: &Pa "--yes", "--module-path", server_path.to_str().unwrap(), + "--dotnet-version", + "8", // Force .NET 8 JIT path to match template TFM &domain, ]) .with_context(|| format!("spacetime publish failed for C# server in template {}", template.id))?; diff --git a/demo/Blackholio/client-unity/Assets/Scripts/autogen/SpacetimeDBClient.g.cs b/demo/Blackholio/client-unity/Assets/Scripts/autogen/SpacetimeDBClient.g.cs index 423b77b5cf4..e7febb6fa13 100644 --- a/demo/Blackholio/client-unity/Assets/Scripts/autogen/SpacetimeDBClient.g.cs +++ b/demo/Blackholio/client-unity/Assets/Scripts/autogen/SpacetimeDBClient.g.cs @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 2.0.0 (commit 6a6b5a6616f0578aa641bc0689691f953b13feb8). +// This was generated using spacetimedb cli version 2.1.0 (commit 6cae7a4ca81a3c90d01d3f3303d46fa7bf7b3d41). #nullable enable diff --git a/demo/Blackholio/server-csharp/StdbModule.csproj b/demo/Blackholio/server-csharp/StdbModule.csproj index c2e9156f933..6e94a06cc46 100644 --- a/demo/Blackholio/server-csharp/StdbModule.csproj +++ b/demo/Blackholio/server-csharp/StdbModule.csproj @@ -12,8 +12,46 @@ $(NoWarn);CS8981;IDE1006 + + net10.0 + $(DefineConstants);EXPERIMENTAL_WASM_AOT + + + + true + true + false + + + + + <_OriginalIlcSdkPath>$(IlcSdkPath) + <_WasiRuntimeOverlayDir>$(IntermediateOutputPath)native-hidden-no-wit\ + + + <_WasiRuntimeOverlaySource Include="$(IlcFrameworkNativePath)**\*" Exclude="$(IlcFrameworkNativePath)**\*.wit" /> + + + + + + + + $(_WasiRuntimeOverlayDir) + $(_OriginalIlcSdkPath) + + + + + + + diff --git a/docs/docs/00300-resources/00200-reference/00100-cli-reference/00100-cli-reference.md b/docs/docs/00300-resources/00200-reference/00100-cli-reference/00100-cli-reference.md index 3365be05535..b9cddcd9243 100644 --- a/docs/docs/00300-resources/00200-reference/00100-cli-reference/00100-cli-reference.md +++ b/docs/docs/00300-resources/00200-reference/00100-cli-reference/00100-cli-reference.md @@ -127,6 +127,7 @@ Run `spacetime help publish` for more detailed information. * `--no-config` — Ignore spacetime.json configuration * `--env ` — Environment name for config file layering (e.g., dev, staging) * `--native-aot` — Use NativeAOT-LLVM compilation for C# modules (experimental, Windows only) +* `--dotnet-version ` — Target .NET SDK major version for C# projects (e.g. 8 or 10). Auto-detected when omitted. @@ -431,6 +432,7 @@ Initializes a new spacetime project. * `--local` — Use local deployment instead of Maincloud * `--non-interactive` — Run in non-interactive mode * `--native-aot` — Configure C# project for NativeAOT-LLVM compilation (experimental, Windows only) +* `--dotnet-version ` — Target .NET SDK major version for C# projects (e.g. 8 or 10). Auto-detected when omitted. @@ -447,6 +449,7 @@ Builds a spacetime module. Default value: `src` * `-d`, `--debug` — Builds the module using debug instead of release (intended to speed up local iteration, not recommended for CI) +* `--dotnet-version ` — Target .NET SDK major version for C# projects (e.g. 8 or 10). Auto-detected when omitted. diff --git a/global.json b/global.json index c19a2e057c7..1e7fdfa95fd 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.100", + "version": "10.0.100", "rollForward": "latestMinor" } } diff --git a/modules/Directory.Build.props b/modules/Directory.Build.props index a03bac5df3a..e66478ad229 100644 --- a/modules/Directory.Build.props +++ b/modules/Directory.Build.props @@ -5,6 +5,7 @@ StdbModule net8.0 + net10.0 wasi-wasm enable enable diff --git a/modules/sdk-test-cs/sdk-test-cs.csproj b/modules/sdk-test-cs/sdk-test-cs.csproj index 09cf1192bb7..19453d0b44f 100644 --- a/modules/sdk-test-cs/sdk-test-cs.csproj +++ b/modules/sdk-test-cs/sdk-test-cs.csproj @@ -10,8 +10,7 @@ - - - + + diff --git a/sdks/csharp/after.SpacetimeDB.ClientSDK.sln.targets b/sdks/csharp/after.SpacetimeDB.ClientSDK.sln.targets index a9197c5098f..bc0f150b4a5 100644 --- a/sdks/csharp/after.SpacetimeDB.ClientSDK.sln.targets +++ b/sdks/csharp/after.SpacetimeDB.ClientSDK.sln.targets @@ -5,6 +5,7 @@ <_UnsupportedDLLs Include="packages/**/net8.0/**" /> + <_UnsupportedDLLs Include="packages/**/net10.0/**" /> diff --git a/sdks/csharp/examples~/regression-tests/client/client.csproj b/sdks/csharp/examples~/regression-tests/client/client.csproj index 540e15ad427..d63b7158f32 100644 --- a/sdks/csharp/examples~/regression-tests/client/client.csproj +++ b/sdks/csharp/examples~/regression-tests/client/client.csproj @@ -8,6 +8,11 @@ true + + net10.0 + $(DefineConstants);EXPERIMENTAL_WASM_AOT + + diff --git a/sdks/csharp/examples~/regression-tests/procedure-client/client.csproj b/sdks/csharp/examples~/regression-tests/procedure-client/client.csproj index 04759b33920..809f2112a87 100644 --- a/sdks/csharp/examples~/regression-tests/procedure-client/client.csproj +++ b/sdks/csharp/examples~/regression-tests/procedure-client/client.csproj @@ -10,6 +10,11 @@ $(NoWarn);CS0067 + + net10.0 + $(DefineConstants);EXPERIMENTAL_WASM_AOT + + diff --git a/sdks/csharp/examples~/regression-tests/republishing/client/client.csproj b/sdks/csharp/examples~/regression-tests/republishing/client/client.csproj index 9c07c1d1c1b..1bbdfd8c02f 100644 --- a/sdks/csharp/examples~/regression-tests/republishing/client/client.csproj +++ b/sdks/csharp/examples~/regression-tests/republishing/client/client.csproj @@ -7,6 +7,11 @@ enable + + net10.0 + $(DefineConstants);EXPERIMENTAL_WASM_AOT + + diff --git a/sdks/csharp/examples~/regression-tests/republishing/server-initial/StdbModule.csproj b/sdks/csharp/examples~/regression-tests/republishing/server-initial/StdbModule.csproj index 3d6a7699986..39a728ccd19 100644 --- a/sdks/csharp/examples~/regression-tests/republishing/server-initial/StdbModule.csproj +++ b/sdks/csharp/examples~/regression-tests/republishing/server-initial/StdbModule.csproj @@ -7,8 +7,46 @@ enable + + net10.0 + $(DefineConstants);EXPERIMENTAL_WASM_AOT + + + + true + true + false + + + + + <_OriginalIlcSdkPath>$(IlcSdkPath) + <_WasiRuntimeOverlayDir>$(IntermediateOutputPath)native-hidden-no-wit\ + + + <_WasiRuntimeOverlaySource Include="$(IlcFrameworkNativePath)**\*" Exclude="$(IlcFrameworkNativePath)**\*.wit" /> + + + + + + + + $(_WasiRuntimeOverlayDir) + $(_OriginalIlcSdkPath) + + + + + + + diff --git a/sdks/csharp/examples~/regression-tests/republishing/server-republish/StdbModule.csproj b/sdks/csharp/examples~/regression-tests/republishing/server-republish/StdbModule.csproj index 3d6a7699986..39a728ccd19 100644 --- a/sdks/csharp/examples~/regression-tests/republishing/server-republish/StdbModule.csproj +++ b/sdks/csharp/examples~/regression-tests/republishing/server-republish/StdbModule.csproj @@ -7,8 +7,46 @@ enable + + net10.0 + $(DefineConstants);EXPERIMENTAL_WASM_AOT + + + + true + true + false + + + + + <_OriginalIlcSdkPath>$(IlcSdkPath) + <_WasiRuntimeOverlayDir>$(IntermediateOutputPath)native-hidden-no-wit\ + + + <_WasiRuntimeOverlaySource Include="$(IlcFrameworkNativePath)**\*" Exclude="$(IlcFrameworkNativePath)**\*.wit" /> + + + + + + + + $(_WasiRuntimeOverlayDir) + $(_OriginalIlcSdkPath) + + + + + + + diff --git a/sdks/csharp/examples~/regression-tests/server/StdbModule.csproj b/sdks/csharp/examples~/regression-tests/server/StdbModule.csproj index 6fe4266c4e8..07779fdaefd 100644 --- a/sdks/csharp/examples~/regression-tests/server/StdbModule.csproj +++ b/sdks/csharp/examples~/regression-tests/server/StdbModule.csproj @@ -7,8 +7,46 @@ enable + + net10.0 + $(DefineConstants);EXPERIMENTAL_WASM_AOT + + + + true + true + false + + + + + <_OriginalIlcSdkPath>$(IlcSdkPath) + <_WasiRuntimeOverlayDir>$(IntermediateOutputPath)native-hidden-no-wit\ + + + <_WasiRuntimeOverlaySource Include="$(IlcFrameworkNativePath)**\*" Exclude="$(IlcFrameworkNativePath)**\*.wit" /> + + + + + + + + $(_WasiRuntimeOverlayDir) + $(_OriginalIlcSdkPath) + + + + + + + diff --git a/sdks/csharp/unity-meta-skeleton~/spacetimedb.bsatn.runtime/lib/net10.0.meta b/sdks/csharp/unity-meta-skeleton~/spacetimedb.bsatn.runtime/lib/net10.0.meta new file mode 100644 index 00000000000..4d0dbab5184 --- /dev/null +++ b/sdks/csharp/unity-meta-skeleton~/spacetimedb.bsatn.runtime/lib/net10.0.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3c6f8e9a2b5d4e7f9a1b2c3d4e5f6a7b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/sdks/csharp/unity-meta-skeleton~/spacetimedb.bsatn.runtime/lib/net10.0/SpacetimeDB.BSATN.Runtime.dll.meta b/sdks/csharp/unity-meta-skeleton~/spacetimedb.bsatn.runtime/lib/net10.0/SpacetimeDB.BSATN.Runtime.dll.meta new file mode 100644 index 00000000000..f502b295917 --- /dev/null +++ b/sdks/csharp/unity-meta-skeleton~/spacetimedb.bsatn.runtime/lib/net10.0/SpacetimeDB.BSATN.Runtime.dll.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: 7d2e8f4c9a3b5e6d8f1a2b3c4d5e6f7a +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 0 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/sdks/csharp/unity-meta-skeleton~/spacetimedb.runtime/lib/net10.0.meta b/sdks/csharp/unity-meta-skeleton~/spacetimedb.runtime/lib/net10.0.meta new file mode 100644 index 00000000000..7246b376f90 --- /dev/null +++ b/sdks/csharp/unity-meta-skeleton~/spacetimedb.runtime/lib/net10.0.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/tools/ci/src/main.rs b/tools/ci/src/main.rs index 146651c1053..d19b911b089 100644 --- a/tools/ci/src/main.rs +++ b/tools/ci/src/main.rs @@ -76,7 +76,10 @@ fn check_global_json_policy() -> Result<()> { } let contents = fs::read_to_string(&p)?; - if contents != root_contents { + // Templates are exempt from content matching to preserve the .NET 8 JIT path for + // module developers importing templates, while the main codebase uses .NET 10 AOT. + // TODO: Remove this exemption once .NET 10 is the default and templates should use it. + if contents != root_contents && !is_template_global_json { eprintln!("Error: {} does not match the root global.json contents", p.display()); ok = false; } else if !is_template_global_json || !is_symlink { @@ -334,6 +337,7 @@ fn run_dlls() -> Result<()> { + @@ -343,6 +347,10 @@ fn run_dlls() -> Result<()> { + + + +