diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69bfe7f0e76..b31ec385f36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,6 +94,16 @@ 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: | + wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-29/wasi-sdk-29.0-x86_64-linux.tar.gz + tar -xzf wasi-sdk-29.0-x86_64-linux.tar.gz + sudo mv wasi-sdk-29.0-x86_64-linux /opt/wasi-sdk + echo "WASI_SDK_PATH=/opt/wasi-sdk" >> "$GITHUB_ENV" + - name: Install emscripten (Windows) if: runner.os == 'Windows' shell: pwsh @@ -103,6 +113,18 @@ 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' + $wasiDir = "$env:USERPROFILE\.wasi-sdk" + Invoke-WebRequest -Uri "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-29/wasi-sdk-29.0-x86_64-windows.tar.gz" -OutFile "$env:TEMP\wasi-sdk.tar.gz" + New-Item -ItemType Directory -Force -Path $wasiDir | Out-Null + tar -xzf "$env:TEMP\wasi-sdk.tar.gz" -C $wasiDir --strip-components=1 + echo "WASI_SDK_PATH=$wasiDir" >> $env:GITHUB_ENV + - name: Install psql (Windows) if: runner.os == 'Windows' shell: pwsh diff --git a/crates/bindings-csharp/BSATN.Runtime/BSATN.Runtime.csproj b/crates/bindings-csharp/BSATN.Runtime/BSATN.Runtime.csproj index d58dad4cde4..3bbfd33a32d 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/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 eb28d3481b6..112f5d4fcc8 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..c34516d38dd 100644 --- a/crates/bindings-csharp/Runtime/build/SpacetimeDB.Runtime.props +++ b/crates/bindings-csharp/Runtime/build/SpacetimeDB.Runtime.props @@ -15,6 +15,9 @@ $(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 diff --git a/crates/bindings-csharp/Runtime/build/SpacetimeDB.Runtime.targets b/crates/bindings-csharp/Runtime/build/SpacetimeDB.Runtime.targets index 5f183e0e040..c97678557bc 100644 --- a/crates/bindings-csharp/Runtime/build/SpacetimeDB.Runtime.targets +++ b/crates/bindings-csharp/Runtime/build/SpacetimeDB.Runtime.targets @@ -4,6 +4,12 @@ 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')" /> + + + wasm32-unknown-wasip1 + + @@ -46,6 +52,9 @@ + + + @@ -54,13 +63,40 @@ <_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")) diff --git a/crates/cli/src/subcommands/init.rs b/crates/cli/src/subcommands/init.rs index 31d927720f1..2cfdf4213ac 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!("{} Scaffolding for .NET 10 (NativeAOT-LLVM).", "ℹ".blue(),); + "{\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/tasks/csharp.rs b/crates/cli/src/tasks/csharp.rs index 5df8b730448..dad46f2b210 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,46 +27,82 @@ 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." - ))?; - } + // Detect the .NET SDK version available at the project path (respects global.json). + let dotnet_version_str = match dotnet!("--version").read() { + 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}"), }; + let dotnet_major = 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." + ); + } + }; + + // 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 => {} + } + + // 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" }; @@ -68,16 +114,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,8 +142,12 @@ 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")), + 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" + )), ]; if possible_output_paths.iter().all(|p| p.exists()) { anyhow::bail!(concat!( 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/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..78acfa7b211 100644 --- a/crates/smoketests/tests/smoketests/csharp_module.rs +++ b/crates/smoketests/tests/smoketests/csharp_module.rs @@ -68,6 +68,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 +85,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 39de1f501a8..59c97ff9e63 100644 --- a/crates/smoketests/tests/smoketests/quickstart.rs +++ b/crates/smoketests/tests/smoketests/quickstart.rs @@ -111,11 +111,17 @@ fn create_nuget_config(sources: &[(String, PathBuf)], mappings: &[(String, Strin source_lines.push_str(&format!(" \n", key, path.display())); } + let mut patterns_by_source: std::collections::HashMap> = std::collections::HashMap::new(); for (key, pattern) in mappings { - mapping_lines.push_str(&format!( - " \n \n \n", - key, pattern - )); + patterns_by_source.entry(key.clone()).or_default().push(pattern.clone()); + } + + for (key, patterns) in patterns_by_source { + 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 +207,23 @@ 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())); + } + // Ensure nuget.org fallback exists if !sources.iter().any(|(k, _)| k == "nuget.org") { sources.push(( diff --git a/crates/smoketests/tests/smoketests/templates.rs b/crates/smoketests/tests/smoketests/templates.rs index 8de55871f58..da7716d6f71 100644 --- a/crates/smoketests/tests/smoketests/templates.rs +++ b/crates/smoketests/tests/smoketests/templates.rs @@ -574,7 +574,17 @@ fn setup_csharp_nuget(project_path: &Path) -> Result { + + + + + + + + + + "#, ) 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 b19fb92460c..57a38def25e 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 fe76039a240..38b1ae03a97 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 @@ -416,6 +416,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. 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 af5dc63af32..4d0b06b166f 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 3c31c366324..a538679f42f 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 {