Skip to content

Conversation

@rekhoff
Copy link
Contributor

@rekhoff rekhoff commented Jan 14, 2026

Description of Changes

  • Added a new cargo ci dlls subcommand to build/pack the in-repo C# NuGet packages and the C# SDK.
  • cargo ci dlls restores sdks/csharp/SpacetimeDB.ClientSDK.csproj using the freshly built local package outputs as to populate sdks/csharp/packages/**.
  • Added a Unity .meta skeleton under sdks/csharp/unity-meta-skeleton~/** and overlays those .meta files onto the latest restored versioned package directory to keep Unity GUIDs stable and import settings consistent.
  • Unity-specific import fixes are captured in the skeleton overlay (notably: preventing Unity from importing incompatible TFMs like net8.0, and marking analyzer DLLs with the RoslynAnalyzer label so Unity can recognize them).

How to use (local)

# Build/pack + restore local packages into sdks/csharp/packages/**
cargo ci dlls

API and ABI breaking changes

N/A

Expected complexity level and risk

2 - Local developer tooling + file overlay into restore output; no runtime/SDK API behavior changes.

Testing

  • cargo check -p ci
  • Ran cargo ci dlls and verified the output under sdks/csharp/packages/** and the various NuGet package locations.
  • Tested a Unity project importing the SpacetimeDB SDK after generating output and confirmed no errors.

@rekhoff rekhoff self-assigned this Jan 14, 2026
@rekhoff rekhoff requested review from bfops and jdetter January 14, 2026 23:29
@rekhoff rekhoff marked this pull request as ready for review January 15, 2026 16:27
"restore",
"SpacetimeDB.ClientSDK.csproj",
"--configfile",
&nuget_config_path_str,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow TIL there's a configfile param

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah but only on restore

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems like by far the cleanest way to do this dance

return Ok(());
}

for entry in fs::read_dir(&pkg_root)? {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: is there a reason we don't remove_dir_all(&pkg_root)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point, latest update should now do exactly that


match best.as_ref() {
None => best = Some((version, entry.path())),
Some((best_v, _)) if version > *best_v => best = Some((version, entry.path())),
Copy link
Collaborator

@bfops bfops Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think it's fine to just pick the first dir (and maybe assert that there is at most 1), especially since we nuke all subdirs of the package's directory beforehand anyway.

(if you make that change, I would add a comment to that effect though)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point. Implemented, and added an error message instead of a comment.

copy_overlay_dir(&src_path, &dst_path)?;
}
} else {
if let Some(file_name) = dst_path.file_name().and_then(|n| n.to_str()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe we should just save entry.file_name() above (or clone it here) since dst_path.file_name is essentially derived from it anyway?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewrote the .meta detection to avoid string conversion entirely.
So if src_path.extension() == "meta", derive the asset path via dst_path.file_stem() (and expect it exists)
Copy .meta only if the corresponding asset exists in the restored dir, otherwise delete stale .meta

copy_overlay_dir(&src_path, &dst_path)?;
}
} else {
if let Some(file_name) = dst_path.file_name().and_then(|n| n.to_str()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but more importantly: should this be an if let Some(...)? shouldn't we unwrap or something? wouldn't we want to know if this failed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. The rewrite fixes this. Now uses path-based .extension()/.file_stem() so we don’t rely on UTF-8 or string suffix logic, and we fail with a message if something unexpected happens.

}
} else {
if let Some(file_name) = dst_path.file_name().and_then(|n| n.to_str()) {
if let Some(asset_name) = file_name.strip_suffix(".meta") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar here: if the strip_suffix failed, wouldn't we want to error out? when would we not expect this to be Some?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, the rewrite should cover this

if let Some(parent) = dst_path.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&src_path, &dst_path)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this must be related to my confusion above, but I don't understand what case this block is for

.run()?;

overlay_unity_meta_skeleton("spacetimedb.bsatn.runtime")?;
overlay_unity_meta_skeleton("spacetimedb.runtime")?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this directory get created for you? for me I only get spacetimedb.bsatn.runtime

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spacetimedb.runtime may or may not exist depending on the restore graph; cargo ci dlls currently tries to overlay both, but overlay_unity_meta_skeleton is already a no-op if the package dir doesn’t exist

@@ -0,0 +1,8 @@
fileFormatVersion: 2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on my system the netstandard2.1 versions get restored, not the net8.0 versions. Is it maybe platform-dependent? Maybe we should copy the net8.0 meta files into a netstandard2.1structure as well, since the code already skips.meta` files for assets that don't exist at runtime?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, SpacetimeDB.ClientSDK.csproj references SpacetimeDB.BSATN.Runtime and that package is multi-targeted (netstandard2.1 and net8.0). I don't recall the specifics on if that was because of platform-dependances, but it's plausible to you have a netstandard2.1 artifacts locally. Luckily the overlay copies .meta only for assets that exist, so we shouldn’t need to duplicate net8.0 metas into a netstandard2.1 structure.

Copy link
Collaborator

@bfops bfops left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks pretty great to me. I left some nits, and I think there may be some remaining meta dance to do.

also, I see more files getting restored such as LICENSE and logo.png, so I think we should possibly do what #3648 did and add a gitignore for those files as well.

@rekhoff
Copy link
Contributor Author

rekhoff commented Jan 16, 2026

This looks pretty great to me. I left some nits, and I think there may be some remaining meta dance to do.

also, I see more files getting restored such as LICENSE and logo.png, so I think we should possibly do what #3648 did and add a gitignore for those files as well.

Updated .gitignore to ignore those restore artifacts and keep only lib/** + analyzers/**

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants