diff --git a/.mise.toml b/.mise.toml index 1fbb2d0..f7a1dd9 100644 --- a/.mise.toml +++ b/.mise.toml @@ -35,7 +35,7 @@ rust = [ { version = "nightly", components = "rust-src,rustfmt", targets = "wasm32-unknown-unknown" }, ] typos = "latest" -uv = "latest" +uv = "0.11.8" yq = "latest" zig = "latest" @@ -157,7 +157,7 @@ description = "Build the har1 workflow WASM module" dir = "services/ws-modules/har1" run = """ wasm-pack build . --target web -yq eval-all -i 'select(fileIndex == 0) * select(fileIndex == 1)' pkg/package.json package.json +cargo run -p module-manifest-to-package-json """ [tasks.build-ws-face-detection-module] @@ -165,7 +165,7 @@ description = "Build the face detection workflow WASM module" dir = "services/ws-modules/face-detection" run = """ wasm-pack build . --target web -yq eval-all -i 'select(fileIndex == 0) * select(fileIndex == 1)' pkg/package.json package.json +cargo run -p module-manifest-to-package-json """ [tasks.build-ws-comm1-module] @@ -234,7 +234,7 @@ description = "Build the pydata1 Python workflow module" dir = "services/ws-modules/pydata1" run = """ uv build --wheel --out-dir pkg -cargo run -p pyproject-to-package-json +cargo run -p module-manifest-to-package-json """ [tasks.build-ws-pyface1-module] @@ -242,7 +242,7 @@ description = "Build the pyface1 Python face detection workflow module" dir = "services/ws-modules/pyface1" run = """ uv build --wheel --out-dir pkg -cargo run -p pyproject-to-package-json +cargo run -p module-manifest-to-package-json """ [tasks.build-ws-java-data1-module] diff --git a/Cargo.lock b/Cargo.lock index 75a8a40..254194a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2038,6 +2038,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "module-manifest-to-package-json" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "toml", +] + [[package]] name = "multimap" version = "0.10.1" @@ -2481,15 +2490,6 @@ dependencies = [ "prost", ] -[[package]] -name = "pyproject-to-package-json" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "toml", -] - [[package]] name = "qr2term" version = "0.3.3" diff --git a/Cargo.toml b/Cargo.toml index fbc2d2c..2ee9119 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ members = [ "services/ws-wasm-agent", "utilities/cli", "utilities/onnx", - "utilities/pyproject-to-package-json", + "utilities/module-manifest-to-package-json", ] resolver = "2" diff --git a/services/ws-modules/face-detection/Cargo.toml b/services/ws-modules/face-detection/Cargo.toml index 53f0206..952a66c 100644 --- a/services/ws-modules/face-detection/Cargo.toml +++ b/services/ws-modules/face-detection/Cargo.toml @@ -9,6 +9,10 @@ repository.workspace = true [lib] crate-type = ["cdylib", "rlib"] +[package.metadata.ws-module.dependencies] +et-model-face1 = "*" +onnxruntime-web = "*" + [dependencies] et-web.workspace = true et-ws-wasm-agent = { path = "../../ws-wasm-agent" } diff --git a/services/ws-modules/face-detection/package.json b/services/ws-modules/face-detection/package.json deleted file mode 100644 index f272815..0000000 --- a/services/ws-modules/face-detection/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "dependencies": { - "et-model-face1": "*", - "onnxruntime-web": "*" - } -} diff --git a/services/ws-modules/har1/Cargo.toml b/services/ws-modules/har1/Cargo.toml index 259c753..3eeb782 100644 --- a/services/ws-modules/har1/Cargo.toml +++ b/services/ws-modules/har1/Cargo.toml @@ -9,6 +9,9 @@ repository.workspace = true [lib] crate-type = ["cdylib", "rlib"] +[package.metadata.ws-module.dependencies] +et-model-har-motion1 = "*" + [dependencies] et-web.workspace = true et-ws-wasm-agent = { path = "../../ws-wasm-agent" } diff --git a/services/ws-modules/har1/package.json b/services/ws-modules/har1/package.json deleted file mode 100644 index 89a6482..0000000 --- a/services/ws-modules/har1/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "et-model-har-motion1": "*" - } -} diff --git a/utilities/cli/src/lib.rs b/utilities/cli/src/lib.rs index ee7053e..f87aa95 100644 --- a/utilities/cli/src/lib.rs +++ b/utilities/cli/src/lib.rs @@ -81,6 +81,29 @@ struct PyprojectWsModule { dependencies: BTreeMap, } +#[derive(Debug, Default, Deserialize)] +struct CargoPackage { + package: Option, +} + +#[derive(Debug, Default, Deserialize)] +struct CargoPackageMetadata { + name: Option, + metadata: Option, +} + +#[derive(Debug, Default, Deserialize)] +struct CargoMetadata { + #[serde(rename = "ws-module")] + ws_module: Option, +} + +#[derive(Debug, Default, Deserialize)] +struct CargoWsModule { + #[serde(default)] + dependencies: BTreeMap, +} + #[derive(Debug, Clone)] struct ModuleRegistryEntry { mise_path: String, @@ -855,7 +878,8 @@ fn module_package_json(module_path: &Path) -> Option { let pkg_package = read_package_json(&module_path.join("pkg/package.json")); let root_package = read_package_json(&module_path.join("package.json")); let pyproject = read_pyproject_package(&module_path.join("pyproject.toml")); - if pkg_package.is_none() && root_package.is_none() && pyproject.is_none() { + let cargo_package = read_cargo_package(&module_path.join("Cargo.toml")); + if pkg_package.is_none() && root_package.is_none() && pyproject.is_none() && cargo_package.is_none() { return None; } @@ -874,6 +898,14 @@ fn module_package_json(module_path: &Path) -> Option { package.name = pyproject.project.and_then(|project| project.name); } } + if let Some(cargo_package) = cargo_package.and_then(|cargo_package| cargo_package.package) { + if let Some(ws_module) = cargo_package.metadata.and_then(|metadata| metadata.ws_module) { + package.dependencies.extend(ws_module.dependencies); + } + if package.name.is_none() { + package.name = cargo_package.name; + } + } Some(package) } @@ -887,6 +919,11 @@ fn read_pyproject_package(path: &Path) -> Option { toml::from_str(&content).ok() } +fn read_cargo_package(path: &Path) -> Option { + let content = fs::read_to_string(path).ok()?; + toml::from_str(&content).ok() +} + fn resolve_module_paths( registry: &BTreeMap, module_names: &[String], diff --git a/utilities/cli/src/tests.rs b/utilities/cli/src/tests.rs index 03c4c76..e6a9d90 100644 --- a/utilities/cli/src/tests.rs +++ b/utilities/cli/src/tests.rs @@ -103,6 +103,33 @@ onnxruntime-web = "*" ); } +#[test] +fn module_package_json_reads_cargo_ws_module_dependencies() { + let test_root = tempdir().unwrap(); + let module_dir = test_root.path().join("rust-module"); + fs::create_dir_all(&module_dir).unwrap(); + fs::write( + module_dir.join("Cargo.toml"), + r#"[package] +name = "et-ws-rust-module" +version = "0.1.0" +edition = "2024" + +[package.metadata.ws-module.dependencies] +et-model-har-motion1 = "*" +"#, + ) + .unwrap(); + + let package = module_package_json(&module_dir).unwrap(); + + assert_eq!(package.name.as_deref(), Some("et-ws-rust-module")); + assert_eq!( + package.dependencies.get("et-model-har-motion1").map(String::as_str), + Some("*") + ); +} + #[test] fn regenerate_verification_generates_all_deployment_types() { let test_root = tempdir().unwrap(); diff --git a/utilities/pyproject-to-package-json/Cargo.toml b/utilities/module-manifest-to-package-json/Cargo.toml similarity index 59% rename from utilities/pyproject-to-package-json/Cargo.toml rename to utilities/module-manifest-to-package-json/Cargo.toml index dc02c11..00696da 100644 --- a/utilities/pyproject-to-package-json/Cargo.toml +++ b/utilities/module-manifest-to-package-json/Cargo.toml @@ -1,13 +1,13 @@ [package] -name = "pyproject-to-package-json" -description = "Generate pkg/package.json from a Python ws-module's pyproject.toml" +name = "module-manifest-to-package-json" +description = "Generate pkg/package.json from ws-module project metadata" version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true [[bin]] -name = "pyproject-to-package-json" +name = "module-manifest-to-package-json" path = "src/main.rs" [dependencies] diff --git a/utilities/module-manifest-to-package-json/src/main.rs b/utilities/module-manifest-to-package-json/src/main.rs new file mode 100644 index 0000000..b17b937 --- /dev/null +++ b/utilities/module-manifest-to-package-json/src/main.rs @@ -0,0 +1,143 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; + +use serde::Deserialize; +use serde_json::{Map, Value, json}; + +#[derive(Deserialize)] +struct Project { + name: String, + version: String, + description: Option, + license: Option, +} + +#[derive(Deserialize)] +struct WsModule { + #[serde(rename = "js-main")] + js_main: String, + #[serde(default)] + dependencies: BTreeMap, +} + +#[derive(Deserialize)] +struct Tool { + #[serde(rename = "ws-module")] + ws_module: WsModule, +} + +#[derive(Deserialize)] +struct Pyproject { + project: Project, + tool: Tool, +} + +#[derive(Deserialize)] +struct CargoPackage { + package: CargoPackageMetadata, +} + +#[derive(Deserialize)] +struct CargoPackageMetadata { + name: String, + metadata: Option, +} + +#[derive(Deserialize)] +struct CargoMetadata { + #[serde(rename = "ws-module")] + ws_module: Option, +} + +#[derive(Deserialize)] +struct CargoWsModule { + #[serde(default)] + dependencies: BTreeMap, +} + +fn main() { + let out_path = PathBuf::from("pkg/package.json"); + let package_json = if Path::new("pyproject.toml").is_file() { + package_json_from_pyproject() + } else if Path::new("Cargo.toml").is_file() { + package_json_from_cargo(&out_path) + } else { + panic!("Expected pyproject.toml or Cargo.toml in the current directory"); + }; + + fs::create_dir_all(out_path.parent().unwrap()).unwrap(); + let mut out = serde_json::to_string_pretty(&package_json).unwrap(); + out.push('\n'); + fs::write(&out_path, &out).unwrap_or_else(|e| panic!("Failed to write {}: {e}", out_path.display())); + + println!("Wrote {}", out_path.display()); +} + +fn package_json_from_pyproject() -> Value { + let pyproject_path = PathBuf::from("pyproject.toml"); + let pyproject: Pyproject = read_toml(&pyproject_path); + let p = &pyproject.project; + let mut pkg = Map::from_iter([ + ("name".to_string(), json!(p.name)), + ("type".to_string(), json!("module")), + ("description".to_string(), json!(p.description.as_deref().unwrap_or(""))), + ("version".to_string(), json!(p.version)), + ("license".to_string(), json!(p.license.as_deref().unwrap_or(""))), + ("main".to_string(), json!(pyproject.tool.ws_module.js_main)), + ]); + if !pyproject.tool.ws_module.dependencies.is_empty() { + pkg.insert("dependencies".to_string(), json!(pyproject.tool.ws_module.dependencies)); + } + Value::Object(pkg) +} + +fn package_json_from_cargo(out_path: &Path) -> Value { + let cargo_toml: CargoPackage = read_toml(Path::new("Cargo.toml")); + let mut pkg = read_package_json(out_path).unwrap_or_else(|| { + let mut pkg = Map::new(); + pkg.insert("name".to_string(), json!(cargo_toml.package.name)); + pkg.insert("type".to_string(), json!("module")); + pkg + }); + + if !pkg.contains_key("name") { + pkg.insert("name".to_string(), json!(cargo_toml.package.name)); + } + + let Some(ws_module) = cargo_toml.package.metadata.and_then(|metadata| metadata.ws_module) else { + return Value::Object(pkg); + }; + + if !ws_module.dependencies.is_empty() { + let dependencies = pkg + .entry("dependencies".to_string()) + .or_insert_with(|| Value::Object(Map::new())); + let dependency_map = dependencies + .as_object_mut() + .unwrap_or_else(|| panic!("{} contains a non-object dependencies field", out_path.display())); + for (name, version) in ws_module.dependencies { + dependency_map.insert(name, json!(version)); + } + } + + Value::Object(pkg) +} + +fn read_toml(path: &Path) -> T +where + T: for<'de> Deserialize<'de>, +{ + let src = fs::read_to_string(path).unwrap_or_else(|e| panic!("Failed to read {}: {e}", path.display())); + toml::from_str(&src).unwrap_or_else(|e| panic!("Failed to parse {}: {e}", path.display())) +} + +fn read_package_json(path: &Path) -> Option> { + let src = fs::read_to_string(path).ok()?; + let Value::Object(pkg) = + serde_json::from_str(&src).unwrap_or_else(|e| panic!("Failed to parse {}: {e}", path.display())) + else { + panic!("{} must contain a JSON object", path.display()); + }; + Some(pkg) +} diff --git a/utilities/pyproject-to-package-json/src/main.rs b/utilities/pyproject-to-package-json/src/main.rs deleted file mode 100644 index dbebe3a..0000000 --- a/utilities/pyproject-to-package-json/src/main.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::collections::BTreeMap; -use std::fs; -use std::path::PathBuf; - -use serde::Deserialize; -use serde_json::{Map, Value, json}; - -#[derive(Deserialize)] -struct Project { - name: String, - version: String, - description: Option, - license: Option, -} - -#[derive(Deserialize)] -struct WsModule { - #[serde(rename = "js-main")] - js_main: String, - #[serde(default)] - dependencies: BTreeMap, -} - -#[derive(Deserialize)] -struct Tool { - #[serde(rename = "ws-module")] - ws_module: WsModule, -} - -#[derive(Deserialize)] -struct Pyproject { - project: Project, - tool: Tool, -} - -fn main() { - let pyproject_path = PathBuf::from("pyproject.toml"); - let src = fs::read_to_string(&pyproject_path) - .unwrap_or_else(|e| panic!("Failed to read {}: {e}", pyproject_path.display())); - - let pyproject: Pyproject = toml::from_str(&src).unwrap_or_else(|e| panic!("Failed to parse pyproject.toml: {e}")); - - let p = &pyproject.project; - let mut pkg = Map::from_iter([ - ("name".to_string(), json!(p.name)), - ("type".to_string(), json!("module")), - ("description".to_string(), json!(p.description.as_deref().unwrap_or(""))), - ("version".to_string(), json!(p.version)), - ("license".to_string(), json!(p.license.as_deref().unwrap_or(""))), - ("main".to_string(), json!(pyproject.tool.ws_module.js_main)), - ]); - if !pyproject.tool.ws_module.dependencies.is_empty() { - pkg.insert("dependencies".to_string(), json!(pyproject.tool.ws_module.dependencies)); - } - let pkg = Value::Object(pkg); - - let out_path = PathBuf::from("pkg/package.json"); - fs::create_dir_all(out_path.parent().unwrap()).unwrap(); - let mut out = serde_json::to_string_pretty(&pkg).unwrap(); - out.push('\n'); - fs::write(&out_path, &out).unwrap_or_else(|e| panic!("Failed to write {}: {e}", out_path.display())); - - println!("Wrote {}", out_path.display()); -}