From 0d042c0e4b852fa08f245fb4cf4319a979c0312a Mon Sep 17 00:00:00 2001 From: Lisandro Crespo Date: Mon, 20 Jul 2026 10:25:33 -0500 Subject: [PATCH 1/6] Improve handling of unsuported hosts for NativeAOT-LLVM --- crates/cli/src/common_args.rs | 27 ++++++++++++++++++++++++++- crates/cli/src/subcommands/build.rs | 3 +++ crates/cli/src/subcommands/init.rs | 20 ++++++++++++-------- crates/cli/src/tasks/csharp.rs | 8 ++++++++ 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/crates/cli/src/common_args.rs b/crates/cli/src/common_args.rs index 25953a62ce6..f2515b2b1d9 100644 --- a/crates/cli/src/common_args.rs +++ b/crates/cli/src/common_args.rs @@ -76,9 +76,23 @@ pub fn parse_optional_dotnet_version(dotnet_version: Option<&str>) -> anyhow::Re dotnet_version.map(parse_dotnet_version).transpose() } +pub(crate) const NATIVEAOT_UNSUPPORTED_MESSAGE: &str = + "NativeAOT-LLVM in only supported on Windows and Linux (.NET 10)."; + +pub(crate) fn nativeaot_unsupported_on_host(os: &str, dotnet_version: Option) -> bool { + os == "macos" || (os == "linux" && dotnet_version != Some(10)) +} + +pub(crate) fn ensure_nativeaot_supported_on_host(dotnet_version: Option) -> anyhow::Result<()> { + if nativeaot_unsupported_on_host(std::env::consts::OS, dotnet_version) { + anyhow::bail!(NATIVEAOT_UNSUPPORTED_MESSAGE); + } + Ok(()) +} + #[cfg(test)] mod tests { - use super::parse_optional_dotnet_version; + use super::{nativeaot_unsupported_on_host, parse_optional_dotnet_version}; #[test] fn dotnet_version_accepts_supported_sdk_majors() { @@ -92,4 +106,15 @@ mod tests { assert!(parse_optional_dotnet_version(Some("9")).is_err()); assert!(parse_optional_dotnet_version(Some("not-a-number")).is_err()); } + + #[test] + fn dotnet10_nativeaot_is_only_unsupported_on_macos_hosts() { + assert!(nativeaot_unsupported_on_host("macos", Some(10))); + assert!(nativeaot_unsupported_on_host("macos", Some(8))); + assert!(nativeaot_unsupported_on_host("macos", None)); + assert!(nativeaot_unsupported_on_host("linux", Some(8))); + assert!(!nativeaot_unsupported_on_host("linux", Some(10))); + assert!(!nativeaot_unsupported_on_host("windows", Some(8))); + assert!(!nativeaot_unsupported_on_host("windows", Some(10))); + } } diff --git a/crates/cli/src/subcommands/build.rs b/crates/cli/src/subcommands/build.rs index 63cf665150a..cebc38cf144 100644 --- a/crates/cli/src/subcommands/build.rs +++ b/crates/cli/src/subcommands/build.rs @@ -125,6 +125,9 @@ pub async fn exec_with_argstring( }; let build_debug = arg_matches.get_flag("debug"); let dotnet_version = dotnet_version.or_else(|| arg_matches.get_one::("dotnet_version").copied()); + if native_aot { + common_args::ensure_nativeaot_supported_on_host(dotnet_version)?; + } run_build(module_path, lint_dir, build_debug, features, native_aot, dotnet_version) } diff --git a/crates/cli/src/subcommands/init.rs b/crates/cli/src/subcommands/init.rs index 339512fd93b..0e1f6efab63 100644 --- a/crates/cli/src/subcommands/init.rs +++ b/crates/cli/src/subcommands/init.rs @@ -565,6 +565,18 @@ pub async fn exec_with_options(config: &mut Config, options: &InitOptions) -> an None }; + // 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 { + common_args::ensure_nativeaot_supported_on_host(dotnet_major)?; + } + ensure_empty_directory( &template_config.project_name, &template_config.project_path, @@ -578,14 +590,6 @@ pub async fn exec_with_options(config: &mut Config, options: &InitOptions) -> an ) .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, dotnet_major)?; diff --git a/crates/cli/src/tasks/csharp.rs b/crates/cli/src/tasks/csharp.rs index f5f14f5e3db..ca3a4d14081 100644 --- a/crates/cli/src/tasks/csharp.rs +++ b/crates/cli/src/tasks/csharp.rs @@ -5,6 +5,8 @@ use std::ffi::OsString; use std::fs; use std::path::{Path, PathBuf}; +use crate::common_args::{nativeaot_unsupported_on_host, NATIVEAOT_UNSUPPORTED_MESSAGE}; + pub(crate) fn parse_major_version(version: &str) -> Option { version.split('.').next()?.parse::().ok() } @@ -255,6 +257,12 @@ pub(crate) fn build_csharp( CsharpBuildPath::Net8Jit | CsharpBuildPath::Net8Aot => 8, CsharpBuildPath::Net10Aot => 10, }; + if matches!(build_path, CsharpBuildPath::Net8Aot | CsharpBuildPath::Net10Aot) + && nativeaot_unsupported_on_host(std::env::consts::OS, Some(desired_sdk_major)) + { + anyhow::bail!(NATIVEAOT_UNSUPPORTED_MESSAGE); + } + let active_sdk_major = dotnet_version_str.as_deref().and_then(parse_major_version); if active_sdk_major != Some(desired_sdk_major) { let active = dotnet_version_str.as_deref().map(str::trim).unwrap_or(""); From f59b55b76a28c72e184139a9ee9166b39f887119 Mon Sep 17 00:00:00 2001 From: Lisandro Crespo Date: Mon, 20 Jul 2026 12:44:23 -0500 Subject: [PATCH 2/6] Add --native-aot to dev command and handle .net version in host --- crates/cli/src/subcommands/dev.rs | 104 ++++++++++++++++++++++------- crates/cli/src/subcommands/init.rs | 2 +- 2 files changed, 81 insertions(+), 25 deletions(-) diff --git a/crates/cli/src/subcommands/dev.rs b/crates/cli/src/subcommands/dev.rs index a0588b6de50..8b3652de928 100644 --- a/crates/cli/src/subcommands/dev.rs +++ b/crates/cli/src/subcommands/dev.rs @@ -1,5 +1,4 @@ -use crate::common_args::parse_optional_dotnet_version; -use crate::common_args::ClearMode; +use crate::common_args::{parse_optional_dotnet_version, ClearMode}; use crate::config::Config; use crate::generate::Language; use crate::spacetime_config::{ @@ -95,6 +94,12 @@ pub fn cli() -> Command { .help("Template ID or GitHub repository (owner/repo or URL) for project initialization"), ) .arg(common_args::dotnet_version()) + .arg( + Arg::new("native_aot") + .long("native-aot") + .action(clap::ArgAction::SetTrue) + .help("Build C# projects with NativeAOT-LLVM. Ignored with .NET 10 because NativeAOT-LLVM is always used."), + ) .arg( Arg::new("run") .long("run") @@ -191,7 +196,12 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E let no_config = args.get_flag("no_config"); let skip_publish = args.get_flag("skip_publish"); let skip_generate = args.get_flag("skip_generate"); - let dotnet_version = args.get_one::("dotnet_version").map(u8::to_string); + let native_aot = args.get_flag("native_aot"); + let dotnet_major = args + .get_one::("dotnet_version") + .copied() + .unwrap_or_else(init::resolve_default_dotnet_major); + let dotnet_version = dotnet_major.to_string(); // --env defaults to "dev" for spacetime dev let env = args.get_one::("env").map(|s| s.as_str()).unwrap_or("dev"); @@ -407,7 +417,8 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E project_name_default: database_name_from_cli_for_init.clone(), database_name_default: database_name_from_cli_for_init.clone(), skip_next_steps: true, - dotnet_version: parse_optional_dotnet_version(dotnet_version.as_deref())?, + native_aot, + dotnet_version: parse_optional_dotnet_version(Some(dotnet_version.as_str()))?, ..Default::default() }; let created_project_path = init::exec_with_options(&mut config, &init_options).await?; @@ -557,6 +568,8 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E publish_configs = vec![CommandConfig::new(&publish_schema, config_map, &publish_args)?]; } + ensure_nativeaot_supported_for_dev(&publish_configs, dotnet_major, native_aot)?; + if !no_config { let db_to_persist = database_name_from_cli_for_init.as_deref().or_else(|| { publish_configs @@ -750,7 +763,8 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E using_spacetime_config, server_from_cli, force, - dotnet_version.as_deref(), + dotnet_version.as_str(), + native_aot, skip_publish, skip_generate, ) @@ -865,7 +879,8 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E using_spacetime_config, server_from_cli, force, - dotnet_version.as_deref(), + dotnet_version.as_str(), + native_aot, skip_publish, skip_generate, ) @@ -1012,7 +1027,8 @@ async fn generate_build_and_publish( using_spacetime_config: bool, server: Option<&str>, yes: bool, - dotnet_version: Option<&str>, + dotnet_version: &str, + native_aot: bool, skip_publish: bool, skip_generate: bool, ) -> Result<(), anyhow::Error> { @@ -1022,8 +1038,8 @@ async fn generate_build_and_publish( Some(Path::new("src")), false, None, - false, - parse_optional_dotnet_version(dotnet_version)?, + native_aot, + parse_optional_dotnet_version(Some(dotnet_version))?, ) .context("Failed to build project")?; println!("{}", "Build complete!".green()); @@ -1061,7 +1077,7 @@ async fn generate_build_and_publish( ); } else { println!("{}", "Generating module bindings from spacetime.json...".cyan()); - let generate_configs = with_dotnet_version_override(generate_configs.to_vec(), dotnet_version); + let generate_configs = with_dotnet_version_for_dev(generate_configs.to_vec(), dotnet_version); generate::exec_from_entries(generate_configs, crate::generate::extract_descriptions, yes, config_dir) .await?; } @@ -1074,9 +1090,7 @@ async fn generate_build_and_publish( Some(resolved_client_language), Some(module_bindings_dir), ); - if let Some(version) = dotnet_version { - generate_entry.insert("dotnet-version".to_string(), json!(version)); - } + generate_entry.insert("dotnet-version".to_string(), json!(dotnet_version)); generate::exec_from_entries( vec![generate_entry], crate::generate::extract_descriptions, @@ -1131,10 +1145,15 @@ async fn generate_build_and_publish( publish_entry.insert("build-options".to_string(), json!(build_opts)); } - if let Some(version) = - dotnet_version.or_else(|| config_entry.get_config_value("dotnet_version").and_then(|v| v.as_str())) + publish_entry.insert("dotnet-version".to_string(), json!(dotnet_version)); + + if native_aot + || config_entry + .get_config_value("native_aot") + .and_then(|v| v.as_bool()) + .unwrap_or(false) { - publish_entry.insert("dotnet-version".to_string(), json!(version)); + publish_entry.insert("native-aot".to_string(), json!(true)); } // Forward break-clients if set @@ -1155,14 +1174,33 @@ async fn generate_build_and_publish( Ok(()) } -fn with_dotnet_version_override( +fn ensure_nativeaot_supported_for_dev( + publish_configs: &[CommandConfig<'_>], + dotnet_version: u8, + native_aot: bool, +) -> anyhow::Result<()> { + for config_entry in publish_configs { + let uses_native_aot = native_aot + || dotnet_version == 10 + || config_entry + .get_config_value("native_aot") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + if uses_native_aot { + common_args::ensure_nativeaot_supported_on_host(Some(dotnet_version))?; + } + } + + Ok(()) +} + +fn with_dotnet_version_for_dev( mut entries: Vec>, - dotnet_version: Option<&str>, + dotnet_version: &str, ) -> Vec> { - if let Some(version) = dotnet_version { - for entry in &mut entries { - entry.insert("dotnet-version".to_string(), json!(version)); - } + for entry in &mut entries { + entry.insert("dotnet-version".to_string(), json!(dotnet_version)); } entries } @@ -1995,17 +2033,35 @@ mod tests { assert_eq!(matches.get_one::("dotnet_version").copied(), Some(8)); } + #[test] + fn test_dev_cli_parses_native_aot() { + let matches = cli().get_matches_from(vec!["dev", "--native-aot"]); + + assert!(matches.get_flag("native_aot")); + } + #[test] fn test_dev_cli_rejects_unsupported_dotnet_version() { assert!(cli().try_get_matches_from(["dev", "--dotnet-version", "9"]).is_err()); } #[test] - fn test_with_dotnet_version_override_updates_generate_entries() { + fn test_with_dotnet_version_for_dev_updates_generate_entries() { + let mut entry = HashMap::new(); + entry.insert("language".to_string(), json!("csharp")); + + let entries = with_dotnet_version_for_dev(vec![entry], "10"); + + assert_eq!(entries[0].get("dotnet-version"), Some(&json!("10"))); + } + + #[test] + fn test_with_dotnet_version_for_dev_replaces_existing_entry() { let mut entry = HashMap::new(); entry.insert("language".to_string(), json!("csharp")); + entry.insert("dotnet-version".to_string(), json!("8")); - let entries = with_dotnet_version_override(vec![entry], Some("10")); + let entries = with_dotnet_version_for_dev(vec![entry], "10"); assert_eq!(entries[0].get("dotnet-version"), Some(&json!("10"))); } diff --git a/crates/cli/src/subcommands/init.rs b/crates/cli/src/subcommands/init.rs index 0e1f6efab63..9400ab2b3ab 100644 --- a/crates/cli/src/subcommands/init.rs +++ b/crates/cli/src/subcommands/init.rs @@ -1631,7 +1631,7 @@ fn check_for_cargo() -> bool { false } -fn resolve_default_dotnet_major() -> u8 { +pub(crate) fn resolve_default_dotnet_major() -> u8 { let installed_majors = installed_dotnet_sdk_majors(); if let Some(reason) = dotnet8_default_reason(std::env::consts::OS, installed_majors.as_deref()) { match reason { From 68534b1bfc07ed72e38265a169ec22ccff0a19e4 Mon Sep 17 00:00:00 2001 From: Lisandro Crespo Date: Mon, 20 Jul 2026 13:01:41 -0500 Subject: [PATCH 3/6] Clean dev.rs --- crates/cli/src/subcommands/dev.rs | 36 +++++++++++-------------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/crates/cli/src/subcommands/dev.rs b/crates/cli/src/subcommands/dev.rs index 8b3652de928..2a5ede7deaa 100644 --- a/crates/cli/src/subcommands/dev.rs +++ b/crates/cli/src/subcommands/dev.rs @@ -196,7 +196,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E let no_config = args.get_flag("no_config"); let skip_publish = args.get_flag("skip_publish"); let skip_generate = args.get_flag("skip_generate"); - let native_aot = args.get_flag("native_aot"); + let native_aot_from_cli = args.get_flag("native_aot"); let dotnet_major = args .get_one::("dotnet_version") .copied() @@ -417,7 +417,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E project_name_default: database_name_from_cli_for_init.clone(), database_name_default: database_name_from_cli_for_init.clone(), skip_next_steps: true, - native_aot, + native_aot: native_aot_from_cli, dotnet_version: parse_optional_dotnet_version(Some(dotnet_version.as_str()))?, ..Default::default() }; @@ -568,7 +568,16 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E publish_configs = vec![CommandConfig::new(&publish_schema, config_map, &publish_args)?]; } - ensure_nativeaot_supported_for_dev(&publish_configs, dotnet_major, native_aot)?; + let native_aot = native_aot_from_cli + || publish_configs.iter().any(|config_entry| { + config_entry + .get_config_value("native_aot") + .and_then(|v| v.as_bool()) + .unwrap_or(false) + }); + if native_aot { + common_args::ensure_nativeaot_supported_on_host(Some(dotnet_major))?; + } if !no_config { let db_to_persist = database_name_from_cli_for_init.as_deref().or_else(|| { @@ -1174,27 +1183,6 @@ async fn generate_build_and_publish( Ok(()) } -fn ensure_nativeaot_supported_for_dev( - publish_configs: &[CommandConfig<'_>], - dotnet_version: u8, - native_aot: bool, -) -> anyhow::Result<()> { - for config_entry in publish_configs { - let uses_native_aot = native_aot - || dotnet_version == 10 - || config_entry - .get_config_value("native_aot") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - if uses_native_aot { - common_args::ensure_nativeaot_supported_on_host(Some(dotnet_version))?; - } - } - - Ok(()) -} - fn with_dotnet_version_for_dev( mut entries: Vec>, dotnet_version: &str, From af9a1c064100a8c492ca5538698f0209e384c69f Mon Sep 17 00:00:00 2001 From: Lisandro Crespo Date: Mon, 20 Jul 2026 14:50:23 -0500 Subject: [PATCH 4/6] Add missing CLI reference --- .../00200-reference/00100-cli-reference/00100-cli-reference.md | 1 + 1 file changed, 1 insertion(+) 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 b4748e0285f..028652a7ae0 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 @@ -266,6 +266,7 @@ Start development mode with auto-regenerate client module bindings, auto-rebuild * `-t`, `--template