Skip to content
Merged
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
8 changes: 4 additions & 4 deletions .mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,15 @@ description = "Build the har1 workflow WASM module"
dir = "services/ws-modules/har1"
run = """
wasm-pack build . --target web
cargo run -p module-manifest-to-package-json
cargo run -p et-cli -- module-package-json
"""

[tasks.build-ws-face-detection-module]
description = "Build the face detection workflow WASM module"
dir = "services/ws-modules/face-detection"
run = """
wasm-pack build . --target web
cargo run -p module-manifest-to-package-json
cargo run -p et-cli -- module-package-json
"""

[tasks.build-ws-comm1-module]
Expand Down Expand Up @@ -235,15 +235,15 @@ description = "Build the pydata1 Python workflow module"
dir = "services/ws-modules/pydata1"
run = """
uv build --wheel --out-dir pkg
cargo run -p module-manifest-to-package-json
cargo run -p et-cli -- module-package-json
"""

[tasks.build-ws-pyface1-module]
description = "Build the pyface1 Python face detection workflow module"
dir = "services/ws-modules/pyface1"
run = """
uv build --wheel --out-dir pkg
cargo run -p module-manifest-to-package-json
cargo run -p et-cli -- module-package-json
"""

[tasks.build-ws-java-data1-module]
Expand Down
14 changes: 8 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ Languages:

### Utilities (`utilities/`)

- **cli** (`et-cli`) — Deployment generator: reads scenario YAML, outputs `mise.toml` or `compose.yaml`
- **cli** (`et-cli`) — Scenario and module tooling. Reads scenario YAML and outputs `mise.toml` or `compose.yaml`;
also generates module `pkg/package.json` files with `et-cli module-package-json`.
Deployment-specific generators live under `utilities/cli/src/deployment_types/`.
Module package JSON generation lives under `utilities/cli/src/module_package_json/`.
- **onnx** — ONNX model utilities
- **module-manifest-to-package-json** — Generates `pkg/package.json` from module metadata.
Reads `pyproject.toml` (Python modules, via `[tool.ws-module]`)
or `Cargo.toml` (Rust modules, via `[package.metadata.ws-module]`).

### Verification (`verification/`)

Expand All @@ -91,9 +91,11 @@ and must stay in sync — `mise run check` will fail if they drift. Regenerate w
- Most Rust modules: `wasm-pack build . --target web` from the module directory
- WASM agent (nightly, MVP target): uses `RUSTFLAGS="-C target-cpu=mvp ..."` and `RUSTUP_TOOLCHAIN=nightly`
- `har1` and `face-detection`: after wasm-pack, merge extra `package.json` fields with `yq`
- Python modules: `uv build --wheel` then `cargo run -p module-manifest-to-package-json`
- Rust modules needing dependency injection: `cargo run -p module-manifest-to-package-json`
- Python modules: `uv build --wheel` then `cargo run -p et-cli -- module-package-json`
- Rust modules needing dependency injection: `cargo run -p et-cli -- module-package-json`
merges `[package.metadata.ws-module.dependencies]` from `Cargo.toml` into `pkg/package.json`
- `et-cli module-package-json` reads `pyproject.toml` (Python modules, via `[tool.ws-module]`)
or `Cargo.toml` (Rust modules, via `[package.metadata.ws-module]`).
- Java: `mvn package` from repo root (uses `pom.xml`)

## Observability
Expand Down
9 changes: 0 additions & 9 deletions Cargo.lock

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

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ members = [
"services/ws-wasm-agent",
"utilities/cli",
"utilities/onnx",
"utilities/module-manifest-to-package-json",
]
resolver = "2"

Expand Down
File renamed without changes.
5 changes: 5 additions & 0 deletions utilities/cli/src/deployment_types/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod docker_compose;
mod mise;

pub use docker_compose::{docker_image_module_paths, generate_docker_compose_deployment};
pub use mise::{generate_mise_deployment, scenario_module_paths};
10 changes: 6 additions & 4 deletions utilities/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ use clap::ValueEnum;
use edge_toolkit::input::ClusterInput;
use serde::Deserialize;

mod docker_compose;
mod mise;
mod deployment_types;
mod module_package_json;

pub use docker_compose::{docker_image_module_paths, generate_docker_compose_deployment};
pub use mise::{generate_mise_deployment, scenario_module_paths};
pub use deployment_types::{
docker_image_module_paths, generate_docker_compose_deployment, generate_mise_deployment, scenario_module_paths,
};
pub use module_package_json::generate_module_package_json;

#[derive(Debug, Clone, Copy, Default, Deserialize, PartialEq, Eq, ValueEnum)]
#[serde(rename_all = "lowercase")]
Expand Down
11 changes: 10 additions & 1 deletion utilities/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::path::PathBuf;

use anyhow::Result;
use clap::{Parser, Subcommand};
use et_cli::{OutputType, generate_deployment, regenerate_verification};
use et_cli::{OutputType, generate_deployment, generate_module_package_json, regenerate_verification};

#[derive(Parser)]
struct Cli {
Expand All @@ -26,6 +26,11 @@ enum Commands {
#[arg(long, default_value = "verification")]
verification_root: PathBuf,
},
/// Generate pkg/package.json from module metadata.
ModulePackageJson {
#[arg(long, default_value = ".")]
module_dir: PathBuf,
},
}

fn main() -> Result<()> {
Expand Down Expand Up @@ -64,6 +69,10 @@ fn main() -> Result<()> {
}
println!("Regenerated {} verification scenario output set(s).", regenerated.len());
}
Commands::ModulePackageJson { module_dir } => {
let output_path = generate_module_package_json(module_dir)?;
println!("Wrote {}", output_path.display());
}
}

Ok(())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result, anyhow};
use serde::Deserialize;
use serde_json::{Map, Value, json};

Expand Down Expand Up @@ -56,27 +57,33 @@ struct CargoWsModule {
dependencies: BTreeMap<String, String>,
}

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)
pub fn generate_module_package_json(module_dir: &Path) -> Result<PathBuf> {
let out_path = module_dir.join("pkg/package.json");
let package_json = if module_dir.join("pyproject.toml").is_file() {
package_json_from_pyproject(module_dir)?
} else if module_dir.join("Cargo.toml").is_file() {
package_json_from_cargo(module_dir, &out_path)?
} else {
panic!("Expected pyproject.toml or Cargo.toml in the current directory");
return Err(anyhow!(
"Expected pyproject.toml or Cargo.toml in module directory {:?}",
module_dir
));
};

fs::create_dir_all(out_path.parent().unwrap()).unwrap();
let mut out = serde_json::to_string_pretty(&package_json).unwrap();
let parent = out_path
.parent()
.ok_or_else(|| anyhow!("Output path {:?} has no parent directory", out_path))?;
fs::create_dir_all(parent).with_context(|| format!("Failed to create output directory: {:?}", parent))?;
let mut out = serde_json::to_string_pretty(&package_json).context("Failed to serialize package JSON")?;
out.push('\n');
fs::write(&out_path, &out).unwrap_or_else(|e| panic!("Failed to write {}: {e}", out_path.display()));
fs::write(&out_path, &out).with_context(|| format!("Failed to write {}", out_path.display()))?;

println!("Wrote {}", out_path.display());
Ok(out_path)
}

fn package_json_from_pyproject() -> Value {
let pyproject_path = PathBuf::from("pyproject.toml");
let pyproject: Pyproject = read_toml(&pyproject_path);
fn package_json_from_pyproject(module_dir: &Path) -> Result<Value> {
let pyproject_path = module_dir.join("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)),
Expand All @@ -89,12 +96,12 @@ fn package_json_from_pyproject() -> Value {
if !pyproject.tool.ws_module.dependencies.is_empty() {
pkg.insert("dependencies".to_string(), json!(pyproject.tool.ws_module.dependencies));
}
Value::Object(pkg)
Ok(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(|| {
fn package_json_from_cargo(module_dir: &Path, out_path: &Path) -> Result<Value> {
let cargo_toml: CargoPackage = read_toml(&module_dir.join("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"));
Expand All @@ -106,7 +113,7 @@ fn package_json_from_cargo(out_path: &Path) -> Value {
}

let Some(ws_module) = cargo_toml.package.metadata.and_then(|metadata| metadata.ws_module) else {
return Value::Object(pkg);
return Ok(Value::Object(pkg));
};

if !ws_module.dependencies.is_empty() {
Expand All @@ -115,29 +122,33 @@ fn package_json_from_cargo(out_path: &Path) -> Value {
.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()));
.ok_or_else(|| anyhow!("{} 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)
Ok(Value::Object(pkg))
}

fn read_toml<T>(path: &Path) -> T
fn read_toml<T>(path: &Path) -> Result<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()))
let src = fs::read_to_string(path).with_context(|| format!("Failed to read {}", path.display()))?;
toml::from_str(&src).with_context(|| format!("Failed to parse {}", path.display()))
}

fn read_package_json(path: &Path) -> Option<Map<String, Value>> {
let src = fs::read_to_string(path).ok()?;
fn read_package_json(path: &Path) -> Result<Option<Map<String, Value>>> {
let src = match fs::read_to_string(path) {
Ok(src) => src,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error).with_context(|| format!("Failed to read {}", path.display())),
};
let Value::Object(pkg) =
serde_json::from_str(&src).unwrap_or_else(|e| panic!("Failed to parse {}: {e}", path.display()))
serde_json::from_str(&src).with_context(|| format!("Failed to parse {}", path.display()))?
else {
panic!("{} must contain a JSON object", path.display());
return Err(anyhow!("{} must contain a JSON object", path.display()));
};
Some(pkg)
Ok(Some(pkg))
}
79 changes: 79 additions & 0 deletions utilities/cli/tests/module_package_json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#![cfg(test)]

use std::fs;

use et_cli::generate_module_package_json;
use serde_json::Value;
use tempfile::tempdir;

#[test]
fn module_package_json_generates_from_pyproject_metadata() {
let test_root = tempdir().unwrap();
let module_dir = test_root.path();
fs::write(
module_dir.join("pyproject.toml"),
r#"[project]
name = "et-ws-python-module"
version = "0.1.0"
description = "Python module"
license = "Apache-2.0"

[tool.ws-module]
js-main = "python_module.js"

[tool.ws-module.dependencies]
et-model-face1 = "*"
"#,
)
.unwrap();

let output_path = generate_module_package_json(module_dir).unwrap();
let package: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap();

assert_eq!(package["name"], "et-ws-python-module");
assert_eq!(package["type"], "module");
assert_eq!(package["description"], "Python module");
assert_eq!(package["version"], "0.1.0");
assert_eq!(package["license"], "Apache-2.0");
assert_eq!(package["main"], "python_module.js");
assert_eq!(package["dependencies"]["et-model-face1"], "*");
}

#[test]
fn module_package_json_merges_cargo_ws_module_dependencies() {
let test_root = tempdir().unwrap();
let module_dir = test_root.path();
let package_dir = module_dir.join("pkg");
fs::create_dir_all(&package_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();
fs::write(
package_dir.join("package.json"),
r#"{
"type": "module",
"dependencies": {
"existing-package": "1.0.0"
}
}
"#,
)
.unwrap();

let output_path = generate_module_package_json(module_dir).unwrap();
let package: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap();

assert_eq!(package["name"], "et-ws-rust-module");
assert_eq!(package["type"], "module");
assert_eq!(package["dependencies"]["existing-package"], "1.0.0");
assert_eq!(package["dependencies"]["et-model-har-motion1"], "*");
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![cfg(test)]

use std::fs;

use et_cli::{
Expand Down
16 changes: 0 additions & 16 deletions utilities/module-manifest-to-package-json/Cargo.toml

This file was deleted.

Loading