From 151895b24b5107803164d1fbcb37308cfc6da862 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Wed, 6 May 2026 16:50:48 -0700 Subject: [PATCH 1/2] [bfops/regen-dlls]: migrate changes --- .cargo/config.toml | 1 + .github/workflows/ci.yml | 2 +- Cargo.toml | 1 + tools/ci/README.md | 4 +- tools/ci/src/main.rs | 204 +------------------------------ tools/regen/Cargo.toml | 10 ++ tools/regen/src/csharp.rs | 245 ++++++++++++++++++++++++++++++++++++++ tools/regen/src/main.rs | 38 ++++++ 8 files changed, 302 insertions(+), 203 deletions(-) create mode 100644 tools/regen/Cargo.toml create mode 100644 tools/regen/src/csharp.rs create mode 100644 tools/regen/src/main.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 48647676517..cd65da0b165 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -5,6 +5,7 @@ rustflags = ["--cfg", "tokio_unstable"] bump-versions = "run -p upgrade-version --" llm = "run --package xtask-llm-benchmark --bin llm_benchmark --" ci = "run -p ci --" +regen = "run -p regen --" smoketest = "ci smoketests --" smoketests = "smoketest" lint = "ci lint --" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bb00eb6f12..227e995d80b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -689,7 +689,7 @@ jobs: } - name: Hydrate Unity SDK DLLs - run: cargo ci dlls + run: cargo regen csharp dlls - name: Check Unity meta files uses: DeNA/unity-meta-check@v3 diff --git a/Cargo.toml b/Cargo.toml index 120ec24c2cf..ef15a8e2a31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,7 @@ members = [ "tools/replace-spacetimedb", "tools/generate-client-api", "tools/gen-bindings", + "tools/regen", "tools/xtask-llm-benchmark", "crates/bindings-typescript/test-app/server", "crates/bindings-typescript/test-react-router-app/server", diff --git a/tools/ci/README.md b/tools/ci/README.md index 3530ee24b5e..9b71b406fef 100644 --- a/tools/ci/README.md +++ b/tools/ci/README.md @@ -72,9 +72,9 @@ Usage: wasm-bindings ### `dlls` -Builds and packs C# DLLs and NuGet packages for local Unity workflows +Deprecated; use `cargo regen csharp dlls`. -Packs the in-repo C# NuGet packages and restores the C# SDK to populate `sdks/csharp/packages/**`. Then overlays Unity `.meta` skeleton files from `sdks/csharp/unity-meta-skeleton~/**` onto the restored versioned package directory, so Unity can associate stable meta files with the most recently built package. +Builds and packs C# DLLs and NuGet packages for local Unity workflows. **Usage:** ```bash diff --git a/tools/ci/src/main.rs b/tools/ci/src/main.rs index 146651c1053..2454ea3349f 100644 --- a/tools/ci/src/main.rs +++ b/tools/ci/src/main.rs @@ -91,123 +91,6 @@ fn check_global_json_policy() -> Result<()> { Ok(()) } -fn overlay_unity_meta_skeleton(pkg_id: &str) -> Result<()> { - let skeleton_base = Path::new("sdks/csharp/unity-meta-skeleton~"); - let skeleton_root = skeleton_base.join(pkg_id); - if !skeleton_root.exists() { - return Ok(()); - } - - let pkg_root = Path::new("sdks/csharp/packages").join(pkg_id); - if !pkg_root.exists() { - return Ok(()); - } - - // Copy spacetimedb..meta - let pkg_root_meta = skeleton_base.join(format!("{pkg_id}.meta")); - if pkg_root_meta.exists() - && let Some(parent) = pkg_root.parent() - { - let pkg_meta_dst = parent.join(format!("{pkg_id}.meta")); - fs::copy(&pkg_root_meta, &pkg_meta_dst)?; - } - - let versioned_dir = match find_only_subdir(&pkg_root) { - Ok(dir) => dir, - Err(err) => { - log::info!("Skipping Unity meta overlay for {pkg_id}: could not locate restored version dir: {err}"); - return Ok(()); - } - }; - - // If version.meta exists under the skeleton package, rename it to match the restored version dir. - let version_meta_template = skeleton_root.join("version.meta"); - if version_meta_template.exists() - && let Some(parent) = versioned_dir.parent() - { - let version_name = versioned_dir - .file_name() - .expect("versioned directory should have a file name"); - let version_meta_dst = parent.join(format!("{}.meta", version_name.to_string_lossy())); - fs::copy(&version_meta_template, &version_meta_dst)?; - } - - copy_overlay_dir(&skeleton_root, &versioned_dir) -} - -fn clear_restored_package_dirs(pkg_id: &str) -> Result<()> { - let pkg_root = Path::new("sdks/csharp/packages").join(pkg_id); - if !pkg_root.exists() { - return Ok(()); - } - - fs::remove_dir_all(&pkg_root)?; - - Ok(()) -} - -fn find_only_subdir(dir: &Path) -> Result { - let mut subdirs: Vec = vec![]; - - for entry in fs::read_dir(dir)? { - let entry = entry?; - if entry.file_type()?.is_dir() { - subdirs.push(entry.path()); - } - } - - match subdirs.as_slice() { - [] => Err(anyhow::anyhow!( - "Could not find a restored versioned directory under {}", - dir.display() - )), - [only] => Ok(only.clone()), - _ => Err(anyhow::anyhow!( - "Expected exactly one restored versioned directory under {}, found {}", - dir.display(), - subdirs.len() - )), - } -} - -fn copy_overlay_dir(src: &Path, dst: &Path) -> Result<()> { - if !src.exists() { - bail!("Skeleton directory does not exist: {}", src.display()); - } - if !dst.exists() { - bail!("Destination directory does not exist: {}", dst.display()); - } - - for entry in fs::read_dir(src)? { - let entry = entry?; - let src_path = entry.path(); - let dst_path = dst.join(entry.file_name()); - if entry.file_type()?.is_dir() { - if dst_path.exists() { - copy_overlay_dir(&src_path, &dst_path)?; - } - } else { - if src_path.extension() == Some(OsStr::new("meta")) { - let asset_path = dst_path - .parent() - .expect("dst_path should have a parent") - .join(dst_path.file_stem().expect(".meta file should have a file stem")); - - if asset_path.exists() { - fs::copy(&src_path, &dst_path)?; - } else if dst_path.exists() { - fs::remove_file(&dst_path)?; - } - continue; - } - - fs::copy(&src_path, &dst_path)?; - } - } - - Ok(()) -} - #[derive(Subcommand)] enum CiCmd { /// Runs tests @@ -225,11 +108,9 @@ enum CiCmd { /// /// Runs tests for the codegen crate and builds a test module with the wasm bindings. WasmBindings, - /// Builds and packs C# DLLs and NuGet packages for local Unity workflows + /// Deprecated; use `cargo regen csharp dlls`. /// - /// Packs the in-repo C# NuGet packages and restores the C# SDK to populate `sdks/csharp/packages/**`. - /// Then overlays Unity `.meta` skeleton files from `sdks/csharp/unity-meta-skeleton~/**` onto the restored - /// versioned package directory, so Unity can associate stable meta files with the most recently built package. + /// Builds and packs C# DLLs and NuGet packages for local Unity workflows. Dlls, /// Runs smoketests /// @@ -308,84 +189,6 @@ fn tracked_rs_files_under(path: &str) -> Result> { .collect()) } -fn run_dlls() -> Result<()> { - ensure_repo_root()?; - - cmd!( - "dotnet", - "pack", - "crates/bindings-csharp/BSATN.Runtime", - "-c", - "Release" - ) - .run()?; - cmd!("dotnet", "pack", "crates/bindings-csharp/Runtime", "-c", "Release").run()?; - - let repo_root = env::current_dir()?; - let bsatn_source = repo_root.join("crates/bindings-csharp/BSATN.Runtime/bin/Release"); - let runtime_source = repo_root.join("crates/bindings-csharp/Runtime/bin/Release"); - - let nuget_config_dir = tempfile::tempdir()?; - let nuget_config_path = nuget_config_dir.path().join("nuget.config"); - let nuget_config_contents = format!( - r#" - - - - - - - - - - - - - - - - - - - - "#, - bsatn_source.display(), - runtime_source.display(), - ); - fs::write(&nuget_config_path, nuget_config_contents)?; - - let nuget_config_path_str = nuget_config_path.to_string_lossy().to_string(); - - clear_restored_package_dirs("spacetimedb.bsatn.runtime")?; - clear_restored_package_dirs("spacetimedb.runtime")?; - - cmd!( - "dotnet", - "restore", - "SpacetimeDB.ClientSDK.csproj", - "--configfile", - &nuget_config_path_str, - ) - .dir("sdks/csharp") - .run()?; - - overlay_unity_meta_skeleton("spacetimedb.bsatn.runtime")?; - overlay_unity_meta_skeleton("spacetimedb.runtime")?; - - cmd!( - "dotnet", - "pack", - "SpacetimeDB.ClientSDK.csproj", - "-c", - "Release", - "--no-restore" - ) - .dir("sdks/csharp") - .run()?; - - Ok(()) -} - fn run_publish_checks() -> Result<()> { cmd!("bash", "-lc", "test -d venv || python3 -m venv venv").run()?; cmd!("venv/bin/pip3", "install", "argparse", "toml").run()?; @@ -623,7 +426,8 @@ fn main() -> Result<()> { } Some(CiCmd::Dlls) => { - run_dlls()?; + eprintln!("warning: `cargo ci dlls` is deprecated; use `cargo regen csharp dlls` instead"); + cmd!("cargo", "regen", "csharp", "dlls").run()?; } Some(CiCmd::Smoketests(args)) => { diff --git a/tools/regen/Cargo.toml b/tools/regen/Cargo.toml new file mode 100644 index 00000000000..cd3613f267b --- /dev/null +++ b/tools/regen/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "regen" +version = "0.1.0" +edition.workspace = true + +[dependencies] +anyhow.workspace = true +clap.workspace = true +duct.workspace = true +tempfile.workspace = true diff --git a/tools/regen/src/csharp.rs b/tools/regen/src/csharp.rs new file mode 100644 index 00000000000..6092c56ca9c --- /dev/null +++ b/tools/regen/src/csharp.rs @@ -0,0 +1,245 @@ +#![allow(clippy::disallowed_macros)] + +use anyhow::{bail, Result}; +use duct::cmd; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; + +const BSATN_PACKAGE_ID: &str = "spacetimedb.bsatn.runtime"; +const RUNTIME_PACKAGE_ID: &str = "spacetimedb.runtime"; + +fn workspace_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(2) + .expect("tools/regen should be two levels below the workspace root") + .to_path_buf() +} + +fn sdk_dir() -> PathBuf { + workspace_dir().join("sdks/csharp") +} + +fn cli_manifest() -> PathBuf { + workspace_dir().join("crates/cli/Cargo.toml") +} + +fn standalone_manifest() -> PathBuf { + workspace_dir().join("crates/standalone/Cargo.toml") +} + +fn path_arg(path: &Path) -> String { + path.to_string_lossy().into_owned() +} + +fn render_nuget_config(bsatn_source: &Path, runtime_source: &Path) -> String { + format!( + r#" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"#, + bsatn_source.display(), + runtime_source.display(), + ) +} + +fn overlay_unity_meta_skeleton(pkg_id: &str) -> Result<()> { + let sdk = sdk_dir(); + let skeleton_base = sdk.join("unity-meta-skeleton~"); + let skeleton_root = skeleton_base.join(pkg_id); + if !skeleton_root.exists() { + return Ok(()); + } + + let pkg_root = sdk.join("packages").join(pkg_id); + if !pkg_root.exists() { + return Ok(()); + } + + let pkg_root_meta = skeleton_base.join(format!("{pkg_id}.meta")); + if pkg_root_meta.exists() + && let Some(parent) = pkg_root.parent() + { + let pkg_meta_dst = parent.join(format!("{pkg_id}.meta")); + fs::copy(&pkg_root_meta, &pkg_meta_dst)?; + } + + let versioned_dir = match find_only_subdir(&pkg_root) { + Ok(dir) => dir, + Err(err) => { + eprintln!("Skipping Unity meta overlay for {pkg_id}: could not locate restored version dir: {err}"); + return Ok(()); + } + }; + + let version_meta_template = skeleton_root.join("version.meta"); + if version_meta_template.exists() + && let Some(parent) = versioned_dir.parent() + { + let version_name = versioned_dir + .file_name() + .expect("versioned directory should have a file name"); + let version_meta_dst = parent.join(format!("{}.meta", version_name.to_string_lossy())); + fs::copy(&version_meta_template, &version_meta_dst)?; + } + + copy_overlay_dir(&skeleton_root, &versioned_dir) +} + +fn clear_restored_package_dirs(pkg_id: &str) -> Result<()> { + let pkg_root = sdk_dir().join("packages").join(pkg_id); + if pkg_root.exists() { + fs::remove_dir_all(&pkg_root)?; + } + Ok(()) +} + +fn find_only_subdir(dir: &Path) -> Result { + let mut subdirs = vec![]; + + for entry in fs::read_dir(dir)? { + let entry = entry?; + if entry.file_type()?.is_dir() { + subdirs.push(entry.path()); + } + } + + match subdirs.as_slice() { + [] => bail!("Could not find a restored versioned directory under {}", dir.display()), + [only] => Ok(only.clone()), + _ => bail!( + "Expected exactly one restored versioned directory under {}, found {}", + dir.display(), + subdirs.len() + ), + } +} + +fn copy_overlay_dir(src: &Path, dst: &Path) -> Result<()> { + if !src.exists() { + bail!("Skeleton directory does not exist: {}", src.display()); + } + if !dst.exists() { + bail!("Destination directory does not exist: {}", dst.display()); + } + + for entry in fs::read_dir(src)? { + let entry = entry?; + let src_path = entry.path(); + let dst_path = dst.join(entry.file_name()); + if entry.file_type()?.is_dir() { + if dst_path.exists() { + copy_overlay_dir(&src_path, &dst_path)?; + } + } else { + if src_path.extension().is_some_and(|ext| ext == "meta") { + let asset_path = dst_path + .parent() + .expect("dst_path should have a parent") + .join(dst_path.file_stem().expect(".meta file should have a file stem")); + + if asset_path.exists() { + fs::copy(&src_path, &dst_path)?; + } else if dst_path.exists() { + fs::remove_file(&dst_path)?; + } + continue; + } + + fs::copy(&src_path, &dst_path)?; + } + } + + Ok(()) +} + +pub fn regen_dlls() -> Result<()> { + let workspace = workspace_dir(); + let sdk = sdk_dir(); + + cmd!( + "dotnet", + "pack", + workspace.join("crates/bindings-csharp/BSATN.Runtime"), + "-c", + "Release" + ) + .run()?; + cmd!( + "dotnet", + "pack", + workspace.join("crates/bindings-csharp/Runtime"), + "-c", + "Release" + ) + .run()?; + + let nuget_config_dir = tempfile::tempdir()?; + let nuget_config_path = nuget_config_dir.path().join("nuget.config"); + fs::write( + &nuget_config_path, + render_nuget_config( + &workspace.join("crates/bindings-csharp/BSATN.Runtime/bin/Release"), + &workspace.join("crates/bindings-csharp/Runtime/bin/Release"), + ), + )?; + + clear_restored_package_dirs(BSATN_PACKAGE_ID)?; + clear_restored_package_dirs(RUNTIME_PACKAGE_ID)?; + + cmd!( + "dotnet", + "restore", + "SpacetimeDB.ClientSDK.csproj", + "--configfile", + path_arg(&nuget_config_path), + ) + .dir(&sdk) + .run()?; + + overlay_unity_meta_skeleton(BSATN_PACKAGE_ID)?; + overlay_unity_meta_skeleton(RUNTIME_PACKAGE_ID)?; + + cmd!( + "dotnet", + "pack", + "SpacetimeDB.ClientSDK.csproj", + "-c", + "Release", + "--no-restore" + ) + .dir(&sdk) + .run()?; + + Ok(()) +} diff --git a/tools/regen/src/main.rs b/tools/regen/src/main.rs new file mode 100644 index 00000000000..48aa55a305c --- /dev/null +++ b/tools/regen/src/main.rs @@ -0,0 +1,38 @@ +use anyhow::Result; +use clap::{Parser, Subcommand}; + +mod csharp; + +#[derive(Parser)] +#[command(name = "regen", bin_name = "cargo regen", about = "Regenerate checked-in artifacts")] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Regenerate C# SDK artifacts. + Csharp { + #[command(subcommand)] + command: CsharpCommand, + }, +} + +#[derive(Subcommand)] +enum CsharpCommand { + /// Regenerate C# DLL and NuGet package artifacts for Unity workflows. + Dlls, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + + match cli.command { + Command::Csharp { command } => match command { + CsharpCommand::Dlls => csharp::regen_dlls()?, + }, + } + + Ok(()) +} From 5552adbe49a6075c27ddbdec5b14a22959748269 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Wed, 6 May 2026 16:54:02 -0700 Subject: [PATCH 2/2] [bfops/regen-dlls]: lints --- Cargo.lock | 10 ++++++++++ tools/regen/src/csharp.rs | 8 -------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9cc97fceadc..14344b10a0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6237,6 +6237,16 @@ dependencies = [ "smallvec", ] +[[package]] +name = "regen" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 4.5.50", + "duct", + "tempfile", +] + [[package]] name = "regex" version = "1.12.2" diff --git a/tools/regen/src/csharp.rs b/tools/regen/src/csharp.rs index 6092c56ca9c..f8ac31f0335 100644 --- a/tools/regen/src/csharp.rs +++ b/tools/regen/src/csharp.rs @@ -21,14 +21,6 @@ fn sdk_dir() -> PathBuf { workspace_dir().join("sdks/csharp") } -fn cli_manifest() -> PathBuf { - workspace_dir().join("crates/cli/Cargo.toml") -} - -fn standalone_manifest() -> PathBuf { - workspace_dir().join("crates/standalone/Cargo.toml") -} - fn path_arg(path: &Path) -> String { path.to_string_lossy().into_owned() }