Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

177 changes: 162 additions & 15 deletions crates/cli/src/subcommands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,16 @@ fn get_spacetimedb_typescript_version() -> &'static str {
embedded::get_typescript_bindings_version()
}

fn to_major_minor_patch_wildcard(version: &str) -> String {
let mut parts = version.split('.');
let major = parts.next();
let minor = parts.next();
match (major, minor) {
(Some(major), Some(minor)) if !major.is_empty() && !minor.is_empty() => format!("{major}.{minor}.*"),
_ => version.to_string(),
}
}

fn update_package_json(dir: &Path, package_name: &str) -> anyhow::Result<()> {
let package_path = dir.join("package.json");
if !package_path.exists() {
Expand All @@ -1033,7 +1043,7 @@ fn update_package_json(dir: &Path, package_name: &str) -> anyhow::Result<()> {
if let Some(deps) = package.get_mut("dependencies")
&& deps.get("spacetimedb").is_some()
{
deps["spacetimedb"] = json!(format!("^{}", get_spacetimedb_typescript_version()));
deps["spacetimedb"] = json!(to_major_minor_patch_wildcard(get_spacetimedb_typescript_version()));
}

let updated_content = serde_json::to_string_pretty(&package)?;
Expand All @@ -1042,17 +1052,9 @@ fn update_package_json(dir: &Path, package_name: &str) -> anyhow::Result<()> {
Ok(())
}

fn to_patch_wildcard(ver: &str) -> String {
let mut parts: Vec<&str> = ver.split('.').collect();
if parts.len() >= 3 {
parts[2] = "*";
}
parts.join(".")
}

fn update_cargo_toml_name(dir: &Path, package_name: &str) -> anyhow::Result<()> {
let version = env!("CARGO_PKG_VERSION");
let patch_wildcard = to_patch_wildcard(version);
let patch_wildcard = to_major_minor_patch_wildcard(version);
let cargo_path = dir.join("Cargo.toml");
if !cargo_path.exists() {
return Ok(());
Expand Down Expand Up @@ -1088,7 +1090,8 @@ fn update_cargo_toml_name(dir: &Path, package_name: &str) -> anyhow::Result<()>
if has_path(dep_item) {
if key == "spacetimedb" {
if let Some(version) = embedded::get_workspace_dependency_version(&key) {
set_dependency_version(dep_item, version, true);
let patch_wildcard = to_major_minor_patch_wildcard(version);
set_dependency_version(dep_item, patch_wildcard.as_str(), true);
}
} else if key == "spacetimedb-sdk" {
set_dependency_version(dep_item, patch_wildcard.as_str(), true);
Expand All @@ -1099,7 +1102,12 @@ fn update_cargo_toml_name(dir: &Path, package_name: &str) -> anyhow::Result<()>
if uses_workspace(dep_item)
&& let Some(version) = embedded::get_workspace_dependency_version(&key)
{
set_dependency_version(dep_item, version, key == "spacetimedb");
let version = if key == "spacetimedb" || key == "spacetimedb-sdk" {
to_major_minor_patch_wildcard(version)
} else {
version.to_string()
};
set_dependency_version(dep_item, version.as_str(), key == "spacetimedb");
}
}
}
Expand Down Expand Up @@ -1271,13 +1279,12 @@ fn pretty_format_xml(xml: &str) -> anyhow::Result<String> {
Ok(String::from_utf8(result)?)
}

/// Just do 2.* for now
fn get_spacetimedb_csharp_runtime_version() -> String {
"2.*".to_string()
to_major_minor_patch_wildcard(env!("CARGO_PKG_VERSION"))
}

fn get_spacetimedb_csharp_clientsdk_version() -> String {
"2.*".to_string()
to_major_minor_patch_wildcard(env!("CARGO_PKG_VERSION"))
}

/// Writes a `.env.local` file that includes all common
Expand Down Expand Up @@ -2163,6 +2170,146 @@ fn check_for_emscripten_and_cmake() -> bool {
mod tests {
use super::*;

fn cli_patch_wildcard() -> String {
to_major_minor_patch_wildcard(env!("CARGO_PKG_VERSION"))
}

fn dependency_version<'a>(doc: &'a DocumentMut, name: &str) -> Option<&'a str> {
let dep = doc.get("dependencies")?.get(name)?;
match dep {
Item::Value(value) => value
.as_inline_table()
.and_then(|table| table.get("version"))
.and_then(|value| value.as_str()),
Item::Table(table) => table.get("version").and_then(|value| value.as_str()),
_ => dep.as_str(),
}
}

fn dependency_has_path(doc: &DocumentMut, name: &str) -> bool {
let Some(dep) = doc.get("dependencies").and_then(|deps| deps.get(name)) else {
return false;
};
has_path(dep)
}

#[test]
fn test_to_major_minor_patch_wildcard() {
assert_eq!(to_major_minor_patch_wildcard("2.4.1"), "2.4.*");
assert_eq!(to_major_minor_patch_wildcard("2.4"), "2.4.*");
assert_eq!(to_major_minor_patch_wildcard("not-semver"), "not-semver");
}

#[test]
fn test_update_package_json_uses_major_minor_patch_wildcard() {
let temp = tempfile::TempDir::new().unwrap();
let package_json = temp.path().join("package.json");
std::fs::write(
&package_json,
r#"{
"name": "old-name",
"dependencies": {
"spacetimedb": "workspace:^"
}
}"#,
)
.unwrap();

update_package_json(temp.path(), "new-name").unwrap();

let content = std::fs::read_to_string(package_json).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(parsed["name"], "new-name");
assert_eq!(
parsed["dependencies"]["spacetimedb"],
to_major_minor_patch_wildcard(get_spacetimedb_typescript_version())
);
}

#[test]
fn test_update_cargo_toml_rewrites_spacetimedb_deps_to_patch_wildcards() {
let temp = tempfile::TempDir::new().unwrap();
let cargo_toml = temp.path().join("Cargo.toml");
std::fs::write(
&cargo_toml,
r#"[package]
name = "old-name"
version = "0.1.0"
edition.workspace = true

[dependencies]
spacetimedb = { path = "../../../crates/bindings" }
spacetimedb-sdk = { path = "../../sdks/rust" }
bytes.workspace = true
"#,
)
.unwrap();

update_cargo_toml_name(temp.path(), "new-name").unwrap();

let content = std::fs::read_to_string(cargo_toml).unwrap();
let doc: DocumentMut = content.parse().unwrap();
assert_eq!(doc["package"]["name"].as_str(), Some("new_name"));
assert_eq!(
dependency_version(&doc, "spacetimedb"),
Some(
to_major_minor_patch_wildcard(embedded::get_workspace_dependency_version("spacetimedb").unwrap())
.as_str()
)
);
assert!(!dependency_has_path(&doc, "spacetimedb"));
assert_eq!(
dependency_version(&doc, "spacetimedb-sdk"),
Some(cli_patch_wildcard().as_str())
);
assert!(!dependency_has_path(&doc, "spacetimedb-sdk"));
assert_eq!(
dependency_version(&doc, "bytes"),
embedded::get_workspace_dependency_version("bytes")
);
}

#[test]
fn test_update_csproj_to_nuget_uses_major_minor_patch_wildcard() {
let temp = tempfile::TempDir::new().unwrap();
let csproj = temp.path().join("StdbModule.csproj");
std::fs::write(
&csproj,
r#"<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="SpacetimeDB.Runtime" Version="workspace" />
<ProjectReference Include="..\Runtime\Runtime.csproj" />
</ItemGroup>
</Project>
"#,
)
.unwrap();

update_csproj_server_to_nuget(temp.path()).unwrap();

let content = std::fs::read_to_string(csproj).unwrap();
let root = Element::parse(content.as_bytes()).unwrap();
let runtime_version = root.children.iter().find_map(|node| {
let XMLNode::Element(item_group) = node else {
return None;
};
item_group.children.iter().find_map(|node| {
let XMLNode::Element(package_ref) = node else {
return None;
};
if package_ref.name == "PackageReference"
&& package_ref.attributes.get("Include").map(String::as_str) == Some("SpacetimeDB.Runtime")
{
package_ref.attributes.get("Version").map(String::as_str)
} else {
None
}
})
});
assert_eq!(runtime_version, Some(cli_patch_wildcard().as_str()));
assert!(!content.contains("ProjectReference"));
}

#[test]
fn test_create_default_spacetime_config_if_missing_creates_expected_config() {
let temp = tempfile::TempDir::new().unwrap();
Expand Down
1 change: 1 addition & 0 deletions crates/smoketests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ predicates = "3"
tokio.workspace = true
tokio-postgres.workspace = true
reqwest = { workspace = true, features = ["blocking"] }
xmltree.workspace = true

[lints]
workspace = true
129 changes: 129 additions & 0 deletions crates/smoketests/tests/smoketests/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,97 @@ fn update_package_json_dependency(package_json_path: &Path, package_name: &str,
Ok(())
}

fn assert_major_minor_version(actual: &str, context: impl std::fmt::Display) -> Result<()> {
let re = Regex::new(r"^\d+\.\d+$").unwrap();
if !re.is_match(actual) {
bail!("{context}: expected MAJOR.MINOR, got {actual}");
}
Ok(())
}

fn assert_major_minor_patch_wildcard(actual: &str, context: impl std::fmt::Display) -> Result<()> {
let re = Regex::new(r"^\d+\.\d+\.\*$").unwrap();
if !re.is_match(actual) {
bail!("{context}: expected MAJOR.MINOR.*, got {actual}");
}
Ok(())
}

fn read_cargo_dependency_version(cargo_toml_path: &Path, package_name: &str) -> Result<String> {
let content =
fs::read_to_string(cargo_toml_path).with_context(|| format!("Failed to read {:?}", cargo_toml_path))?;
let data: toml::Value = content
.parse()
.with_context(|| format!("Failed to parse {:?}", cargo_toml_path))?;
let dep = data
.get("dependencies")
.and_then(|deps| deps.get(package_name))
.with_context(|| format!("No dependency `{package_name}` found in {:?}", cargo_toml_path))?;
match dep {
toml::Value::String(version) => Ok(version.clone()),
toml::Value::Table(table) => table
.get("version")
.and_then(|v| v.as_str())
.map(String::from)
.with_context(|| format!("Dependency `{package_name}` in {:?} has no version", cargo_toml_path)),
_ => bail!(
"Unsupported dependency `{package_name}` format in {:?}",
cargo_toml_path
),
}
}

fn read_package_json_dependency_version(package_json_path: &Path, package_name: &str) -> Result<String> {
let content =
fs::read_to_string(package_json_path).with_context(|| format!("Failed to read {:?}", package_json_path))?;
let data: Value =
serde_json::from_str(&content).with_context(|| format!("Failed to parse {:?}", package_json_path))?;
data.get("dependencies")
.and_then(|deps| deps.get(package_name))
.and_then(|v| v.as_str())
.map(String::from)
.with_context(|| format!("No dependency `{package_name}` found in {:?}", package_json_path))
}

fn read_csproj_package_reference_version(csproj_path: &Path, package_name: &str) -> Result<String> {
let content = fs::read_to_string(csproj_path).with_context(|| format!("Failed to read {:?}", csproj_path))?;
let root = xmltree::Element::parse(content.as_bytes())
.with_context(|| format!("Failed to parse XML {:?}", csproj_path))?;

root.children
.iter()
.filter_map(|node| match node {
xmltree::XMLNode::Element(element) if element.name == "ItemGroup" => Some(element),
_ => None,
})
.flat_map(|item_group| item_group.children.iter())
.filter_map(|node| match node {
xmltree::XMLNode::Element(element) if element.name == "PackageReference" => Some(element),
_ => None,
})
.find(|package_ref| package_ref.attributes.get("Include").map(String::as_str) == Some(package_name))
.and_then(|package_ref| package_ref.attributes.get("Version").cloned())
.with_context(|| format!("No PackageReference `{package_name}` found in {:?}", csproj_path))
}

fn find_csproj(dir: &Path) -> Result<PathBuf> {
fs::read_dir(dir)
.with_context(|| format!("Failed to read {:?}", dir))?
.flatten()
.map(|entry| entry.path())
.find(|path| path.extension().is_some_and(|ext| ext == "csproj"))
.with_context(|| format!("No .csproj found in {:?}", dir))
}

fn read_spacetimedb_cpp_version(cmake_path: &Path) -> Result<String> {
let content = fs::read_to_string(cmake_path).with_context(|| format!("Failed to read {:?}", cmake_path))?;
let re = Regex::new(r#"set\(SPACETIMEDB_CPP_VERSION\s+"([^"]+)""#).unwrap();
let caps = re
.captures(&content)
.with_context(|| format!("No SPACETIMEDB_CPP_VERSION found in {:?}", cmake_path))?;
Ok(caps.get(1).unwrap().as_str().to_string())
}

/// Runs pnpm with the given arguments in the given working directory.
fn run_pnpm(args: &[&str], cwd: &Path) -> Result<()> {
pnpm(args, cwd)?;
Expand Down Expand Up @@ -763,6 +854,44 @@ fn test_template(test: &Smoketest, template: &Template) -> Result<()> {
// Test entry point
// ============================================================================

#[test]
fn test_basic_template_dependency_versions() -> Result<()> {
let test = Smoketest::builder().autopublish(false).build();

let (_basic_cpp_tmpdir, basic_cpp_path) = init_template(&test, "basic-cpp")?;
let cpp_server_version = read_spacetimedb_cpp_version(&basic_cpp_path.join("spacetimedb").join("CMakeLists.txt"))?;
assert_major_minor_version(&cpp_server_version, "basic-cpp C++ server SPACETIMEDB_CPP_VERSION")?;
// The current basic C++ template still uses a Rust client; we do not have a C++ client yet.
let cpp_client_manifest = basic_cpp_path.join("Cargo.toml");
if !cpp_client_manifest.exists() {
bail!("basic-cpp expected Rust client manifest at {:?}", cpp_client_manifest);
}

let (_basic_rs_tmpdir, basic_rs_path) = init_template(&test, "basic-rs")?;
let rs_server_version =
read_cargo_dependency_version(&basic_rs_path.join("spacetimedb").join("Cargo.toml"), "spacetimedb")?;
assert_major_minor_patch_wildcard(&rs_server_version, "basic-rs Rust server spacetimedb")?;
let rs_client_version = read_cargo_dependency_version(&basic_rs_path.join("Cargo.toml"), "spacetimedb-sdk")?;
assert_major_minor_patch_wildcard(&rs_client_version, "basic-rs Rust client spacetimedb-sdk")?;

let (_basic_ts_tmpdir, basic_ts_path) = init_template(&test, "basic-ts")?;
let ts_server_version =
read_package_json_dependency_version(&basic_ts_path.join("spacetimedb").join("package.json"), "spacetimedb")?;
assert_major_minor_patch_wildcard(&ts_server_version, "basic-ts TypeScript server spacetimedb")?;
let ts_client_version = read_package_json_dependency_version(&basic_ts_path.join("package.json"), "spacetimedb")?;
assert_major_minor_patch_wildcard(&ts_client_version, "basic-ts TypeScript client spacetimedb")?;

let (_basic_cs_tmpdir, basic_cs_path) = init_template(&test, "basic-cs")?;
let cs_server_project = find_csproj(&basic_cs_path.join("spacetimedb"))?;
let cs_server_version = read_csproj_package_reference_version(&cs_server_project, "SpacetimeDB.Runtime")?;
assert_major_minor_patch_wildcard(&cs_server_version, "basic-cs C# server SpacetimeDB.Runtime")?;
let cs_client_version =
read_csproj_package_reference_version(&basic_cs_path.join("client.csproj"), "SpacetimeDB.ClientSDK")?;
assert_major_minor_patch_wildcard(&cs_client_version, "basic-cs C# client SpacetimeDB.ClientSDK")?;

Ok(())
}

/// Tests all templates discovered in the `templates/` directory.
///
/// For each template the test:
Expand Down
2 changes: 1 addition & 1 deletion templates/basic-cpp/spacetimedb/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ project(spacetime_cpp_module LANGUAGES C CXX)
# The directory should contain the bindings' CMakeLists.txt at its root.
# ------------------------------------------------------------------------------

set(SPACETIMEDB_CPP_VERSION "2.5.0" CACHE STRING "Version selector: MAJOR.MINOR (uses release/MAJOR.MINOR) or MAJOR.MINOR.PATCH (uses tag vMAJOR.MINOR.PATCH)")
set(SPACETIMEDB_CPP_VERSION "2.4" CACHE STRING "Version selector: MAJOR.MINOR (uses release/MAJOR.MINOR) or MAJOR.MINOR.PATCH (uses tag vMAJOR.MINOR.PATCH)")
set(SPACETIMEDB_CPP_REF "" CACHE STRING "Override Git ref directly (e.g. release/1.0, release/latest, v1.0.0)")
set(SPACETIMEDB_CPP_DIR "" CACHE PATH "Path to a local clone of SpacetimeDB C++ bindings (overrides FetchContent)")

Expand Down
Loading
Loading