diff --git a/README.md b/README.md index ab2b0813..2730b8b2 100644 --- a/README.md +++ b/README.md @@ -145,5 +145,28 @@ The command composes a caller component's WASM (which uses the generated stub to generated stub WASM, writing out a composed WASM which no longer depends on the stub interface, ready to use. - `source-wasm`: The WASM file of the caller component -- `stub-wasm`: The WASM file of the generated stub +- `stub-wasm`: The WASM file of the generated stub. Multiple stubs can be listed. - `dest-wasm`: The name of the composed WASM file to be generated + +## Initialize cargo make tasks for a workspace + +```shell +Usage: wasm-rpc-stubgen initialize-workspace [OPTIONS] --targets --callers + +Options: + --targets + List of subprojects to be called via RPC + --callers + List of subprojects using the generated stubs for calling remote workers + --wasm-rpc-path-override +``` + +When both the target and the caller components are in the same Cargo workspace, this command can initialize a `cargo-make` file with dependent tasks +performing the stub generation, WIT merging and WASM composition. + +Once the workspace is initialized, the following two commands become available: + +```shell +cargo make build-flow +cargo make release-build-flow +``` diff --git a/wasm-rpc-stubgen/README.md b/wasm-rpc-stubgen/README.md index 28326faa..fa03622c 100644 --- a/wasm-rpc-stubgen/README.md +++ b/wasm-rpc-stubgen/README.md @@ -100,5 +100,29 @@ The command composes a caller component's WASM (which uses the generated stub to generated stub WASM, writing out a composed WASM which no longer depends on the stub interface, ready to use. - `source-wasm`: The WASM file of the caller component -- `stub-wasm`: The WASM file of the generated stub +- `stub-wasm`: The WASM file of the generated stub. Multiple stubs can be listed. - `dest-wasm`: The name of the composed WASM file to be generated + + +## Initialize cargo make tasks for a workspace + +```shell +Usage: wasm-rpc-stubgen initialize-workspace [OPTIONS] --targets --callers + +Options: + --targets + List of subprojects to be called via RPC + --callers + List of subprojects using the generated stubs for calling remote workers + --wasm-rpc-path-override +``` + +When both the target and the caller components are in the same Cargo workspace, this command can initialize a `cargo-make` file with dependent tasks +performing the stub generation, WIT merging and WASM composition. + +Once the workspace is initialized, the following two commands become available: + +```shell +cargo make build-flow +cargo make release-build-flow +``` diff --git a/wasm-rpc-stubgen/src/cargo.rs b/wasm-rpc-stubgen/src/cargo.rs index dda2eefc..366c3876 100644 --- a/wasm-rpc-stubgen/src/cargo.rs +++ b/wasm-rpc-stubgen/src/cargo.rs @@ -185,6 +185,19 @@ pub fn is_cargo_component_toml(path: &Path) -> anyhow::Result { Ok(false) } +pub fn is_cargo_workspace_toml(path: &Path) -> anyhow::Result { + let manifest = Manifest::from_path(path)?; + if let Some(workspace) = manifest.workspace { + if !workspace.members.is_empty() { + Ok(true) + } else { + Ok(false) + } + } else { + Ok(false) + } +} + pub fn add_dependencies_to_cargo_toml(cargo_path: &Path, names: &[String]) -> anyhow::Result<()> { let mut manifest: Manifest = Manifest::from_path_with_metadata(cargo_path)?; if let Some(ref mut package) = manifest.package { diff --git a/wasm-rpc-stubgen/src/main.rs b/wasm-rpc-stubgen/src/main.rs index c077eb70..ea225a3b 100644 --- a/wasm-rpc-stubgen/src/main.rs +++ b/wasm-rpc-stubgen/src/main.rs @@ -14,6 +14,7 @@ mod cargo; mod compilation; +mod make; mod rust; mod stub; mod wit; @@ -43,62 +44,83 @@ enum Command { Build(BuildArgs), AddStubDependency(AddStubDependencyArgs), Compose(ComposeArgs), + InitializeWorkspace(InitializeWorkspaceArgs), } +/// Generate a Rust RPC stub crate for a WASM component #[derive(clap::Args)] #[command(version, about, long_about = None)] -struct GenerateArgs { +pub struct GenerateArgs { #[clap(short, long)] - source_wit_root: PathBuf, + pub source_wit_root: PathBuf, #[clap(short, long)] - dest_crate_root: PathBuf, + pub dest_crate_root: PathBuf, #[clap(short, long)] - world: Option, + pub world: Option, #[clap(long, default_value = "0.0.1")] - stub_crate_version: String, + pub stub_crate_version: String, #[clap(long)] - wasm_rpc_path_override: Option, + pub wasm_rpc_path_override: Option, } +/// Build an RPC stub for a WASM component #[derive(clap::Args)] #[command(version, about, long_about = None)] -struct BuildArgs { +pub struct BuildArgs { #[clap(short, long)] - source_wit_root: PathBuf, + pub source_wit_root: PathBuf, #[clap(long)] - dest_wasm: PathBuf, + pub dest_wasm: PathBuf, #[clap(long)] - dest_wit_root: PathBuf, + pub dest_wit_root: PathBuf, #[clap(short, long)] - world: Option, + pub world: Option, #[clap(long, default_value = "0.0.1")] - stub_crate_version: String, + pub stub_crate_version: String, #[clap(long)] - wasm_rpc_path_override: Option, + pub wasm_rpc_path_override: Option, } +/// Adds a generated stub as a dependency to another WASM component #[derive(clap::Args)] #[command(version, about, long_about = None)] -struct AddStubDependencyArgs { +pub struct AddStubDependencyArgs { #[clap(short, long)] - stub_wit_root: PathBuf, + pub stub_wit_root: PathBuf, #[clap(short, long)] - dest_wit_root: PathBuf, + pub dest_wit_root: PathBuf, #[clap(short, long)] - overwrite: bool, + pub overwrite: bool, #[clap(short, long)] - update_cargo_toml: bool, + pub update_cargo_toml: bool, } +/// Compose a WASM component with a generated stub WASM #[derive(clap::Args)] #[command(version, about, long_about = None)] -struct ComposeArgs { +pub struct ComposeArgs { #[clap(long)] - source_wasm: PathBuf, + pub source_wasm: PathBuf, + #[clap(long, required = true)] + pub stub_wasm: Vec, #[clap(long)] - stub_wasm: PathBuf, + pub dest_wasm: PathBuf, +} + +/// Initializes a Golem-specific cargo-make configuration in a Cargo workspace for automatically +/// generating stubs and composing results. +#[derive(clap::Args)] +#[command(version, about, long_about = None)] +pub struct InitializeWorkspaceArgs { + /// List of subprojects to be called via RPC + #[clap(long, required = true)] + pub targets: Vec, + /// List of subprojects using the generated stubs for calling remote workers + #[clap(long, required = true)] + pub callers: Vec, + #[clap(long)] - dest_wasm: PathBuf, + pub wasm_rpc_path_override: Option, } #[tokio::main] @@ -118,6 +140,9 @@ async fn main() { Command::Compose(compose_args) => { let _ = render_error(compose(compose_args)); } + Command::InitializeWorkspace(init_workspace_args) => { + let _ = render_error(initialize_workspace(init_workspace_args)); + } } } @@ -131,7 +156,7 @@ fn render_error(result: anyhow::Result) -> Option { } } -fn generate(args: GenerateArgs) -> anyhow::Result<()> { +pub fn generate(args: GenerateArgs) -> anyhow::Result<()> { let stub_def = StubDefinition::new( &args.source_wit_root, &args.dest_crate_root, @@ -151,7 +176,7 @@ fn generate(args: GenerateArgs) -> anyhow::Result<()> { Ok(()) } -async fn build(args: BuildArgs) -> anyhow::Result<()> { +pub async fn build(args: BuildArgs) -> anyhow::Result<()> { let target_root = TempDir::new("wasm-rpc-stubgen")?; let stub_def = StubDefinition::new( @@ -204,7 +229,7 @@ async fn build(args: BuildArgs) -> anyhow::Result<()> { Ok(()) } -fn add_stub_dependency(args: AddStubDependencyArgs) -> anyhow::Result<()> { +pub fn add_stub_dependency(args: AddStubDependencyArgs) -> anyhow::Result<()> { let source_deps = wit::get_dep_dirs(&args.stub_wit_root)?; let main_wit = args.stub_wit_root.join("_stub.wit"); @@ -264,26 +289,28 @@ fn add_stub_dependency(args: AddStubDependencyArgs) -> anyhow::Result<()> { Ok(()) } -fn compose(args: ComposeArgs) -> anyhow::Result<()> { +pub fn compose(args: ComposeArgs) -> anyhow::Result<()> { let mut config = wasm_compose::config::Config::default(); - let stub_bytes = fs::read(&args.stub_wasm)?; - let stub_component = - Component::::from_bytes(&stub_bytes).map_err(|err| anyhow!(err))?; - - let state = AnalysisContext::new(stub_component); - let stub_exports = state.get_top_level_exports().map_err(|err| match err { - AnalysisFailure::Failed(msg) => anyhow!(msg), - })?; - - for export in stub_exports { - if let AnalysedExport::Instance(instance) = export { - config.dependencies.insert( - instance.name.clone(), - Dependency { - path: args.stub_wasm.clone(), - }, - ); + for stub_wasm in &args.stub_wasm { + let stub_bytes = fs::read(stub_wasm)?; + let stub_component = Component::::from_bytes(&stub_bytes) + .map_err(|err| anyhow!(err))?; + + let state = AnalysisContext::new(stub_component); + let stub_exports = state.get_top_level_exports().map_err(|err| match err { + AnalysisFailure::Failed(msg) => anyhow!(msg), + })?; + + for export in stub_exports { + if let AnalysedExport::Instance(instance) = export { + config.dependencies.insert( + instance.name.clone(), + Dependency { + path: stub_wasm.clone(), + }, + ); + } } } @@ -293,3 +320,13 @@ fn compose(args: ComposeArgs) -> anyhow::Result<()> { fs::write(&args.dest_wasm, result).context("Failed to write the composed component")?; Ok(()) } + +pub fn initialize_workspace(args: InitializeWorkspaceArgs) -> anyhow::Result<()> { + make::initialize_workspace( + &args.targets, + &args.callers, + args.wasm_rpc_path_override, + "golem-wasm-rpc-stubgen", + &[], + ) +} diff --git a/wasm-rpc-stubgen/src/make.rs b/wasm-rpc-stubgen/src/make.rs new file mode 100644 index 00000000..44b7a585 --- /dev/null +++ b/wasm-rpc-stubgen/src/make.rs @@ -0,0 +1,342 @@ +use crate::{cargo, GenerateArgs}; +use std::fs; +use std::process::Command; +use toml::map::Map; +use toml::Value; + +pub fn initialize_workspace( + targets: &[String], + callers: &[String], + wasm_rpc_path_override: Option, + stubgen_command: &str, + stubgen_prefix: &[&str], +) -> anyhow::Result<()> { + let cwd = std::env::current_dir()?; + if cargo::is_cargo_workspace_toml(&cwd.join("Cargo.toml"))? { + let makefile_path = cwd.join("Makefile.toml"); + if makefile_path.exists() && makefile_path.is_file() { + Err(anyhow::anyhow!("Makefile.toml already exists. Modifying existing cargo-make projects is currently not supported.")) + } else if has_cargo_make() { + let makefile = generate_makefile( + targets, + callers, + wasm_rpc_path_override.clone(), + stubgen_command, + stubgen_prefix, + )?; + println!("Writing cargo-make Makefile to {:?}", makefile_path); + fs::write(makefile_path, makefile)?; + + for target in targets { + println!("Generating initial stub for {target}"); + + crate::generate(GenerateArgs { + source_wit_root: cwd.join(format!("{target}/wit")), + dest_crate_root: cwd.join(format!("{target}-stub")), + world: None, + stub_crate_version: "0.0.1".to_string(), + wasm_rpc_path_override: wasm_rpc_path_override.clone(), + })?; + } + + Ok(()) + } else { + Err(anyhow::anyhow!( + "cargo-make is not installed. Please install it with `cargo install cargo-make`" + )) + } + } else { + Err(anyhow::anyhow!("Not in a cargo workspace")) + } +} + +fn generate_makefile( + targets: &[String], + callers: &[String], + wasm_rpc_path_override: Option, + stubgen_command: &str, + stubgen_prefix: &[&str], +) -> anyhow::Result { + let mut root = Map::default(); + + let mut config = Map::default(); + config.insert("default_to_workspace".to_string(), Value::Boolean(false)); + root.insert("config".to_string(), Value::Table(config)); + + let mut tasks = Map::default(); + + let mut default = Map::default(); + default.insert("alias".to_string(), Value::String("build".to_string())); + tasks.insert("default".to_string(), Value::Table(default)); + + let mut clean = Map::default(); + clean.insert( + "command".to_string(), + Value::String("cargo-component".to_string()), + ); + clean.insert( + "args".to_string(), + Value::Array(vec![Value::String("clean".to_string())]), + ); + tasks.insert("clean".to_string(), Value::Table(clean)); + + let mut build = Map::default(); + build.insert( + "command".to_string(), + Value::String("cargo-component".to_string()), + ); + build.insert( + "args".to_string(), + Value::Array(vec![Value::String("build".to_string())]), + ); + build.insert( + "dependencies".to_string(), + Value::Array(vec![ + Value::String("clean".to_string()), + Value::String("regenerate-stubs".to_string()), + ]), + ); + tasks.insert("build".to_string(), Value::Table(build)); + + let mut build_release = Map::default(); + build_release.insert( + "command".to_string(), + Value::String("cargo-component".to_string()), + ); + build_release.insert( + "args".to_string(), + Value::Array(vec![ + Value::String("build".to_string()), + Value::String("--release".to_string()), + ]), + ); + build_release.insert( + "dependencies".to_string(), + Value::Array(vec![ + Value::String("clean".to_string()), + Value::String("regenerate-stubs".to_string()), + ]), + ); + tasks.insert("build-release".to_string(), Value::Table(build_release)); + + let mut test = Map::default(); + test.insert( + "command".to_string(), + Value::String("cargo-component".to_string()), + ); + test.insert( + "args".to_string(), + Value::Array(vec![Value::String("test".to_string())]), + ); + test.insert( + "dependencies".to_string(), + Value::Array(vec![Value::String("clean".to_string())]), + ); + tasks.insert("test".to_string(), Value::Table(test)); + + let mut regenerate_stub_tasks = Vec::new(); + for target in targets { + let mut generate_stub = Map::default(); + generate_stub.insert("cwd".to_string(), Value::String(".".to_string())); + generate_stub.insert( + "command".to_string(), + Value::String(stubgen_command.to_string()), + ); + let mut args = Vec::new(); + args.extend(stubgen_prefix.iter().map(|s| Value::String(s.to_string()))); + args.push(Value::String("generate".to_string())); + args.push(Value::String("-s".to_string())); + args.push(Value::String(format!("{}/wit", target))); + args.push(Value::String("-d".to_string())); + args.push(Value::String(format!("{}-stub", target))); + if let Some(wasm_rpc_path_override) = wasm_rpc_path_override.as_ref() { + args.push(Value::String("--wasm-rpc-path-override".to_string())); + args.push(Value::String(wasm_rpc_path_override.to_string())); + } + generate_stub.insert("args".to_string(), Value::Array(args)); + + let generate_stub_task_name = format!("generate-{}-stub", target); + tasks.insert(generate_stub_task_name.clone(), Value::Table(generate_stub)); + + for caller in callers { + let mut add_stub_dependency = Map::default(); + add_stub_dependency.insert("cwd".to_string(), Value::String(".".to_string())); + add_stub_dependency.insert( + "command".to_string(), + Value::String(stubgen_command.to_string()), + ); + + let mut args = Vec::new(); + args.extend(stubgen_prefix.iter().map(|s| Value::String(s.to_string()))); + args.extend(vec![ + Value::String("add-stub-dependency".to_string()), + Value::String("--stub-wit-root".to_string()), + Value::String(format!("{}-stub/wit", target)), + Value::String("--dest-wit-root".to_string()), + Value::String(format!("{}/wit", caller)), + Value::String("--overwrite".to_string()), + Value::String("--update-cargo-toml".to_string()), + ]); + + add_stub_dependency.insert("args".to_string(), Value::Array(args)); + add_stub_dependency.insert( + "dependencies".to_string(), + Value::Array(vec![Value::String(generate_stub_task_name.clone())]), + ); + + let regenerate_task_name = format!("add-stub-dependency-{}-{}", target, caller); + tasks.insert( + regenerate_task_name.clone(), + Value::Table(add_stub_dependency), + ); + regenerate_stub_tasks.push(regenerate_task_name); + } + } + + let mut regenerate_stubs = Map::default(); + regenerate_stubs.insert( + "dependencies".to_string(), + Value::Array( + regenerate_stub_tasks + .iter() + .map(|n| Value::String(n.to_string())) + .collect::>(), + ), + ); + tasks.insert( + "regenerate-stubs".to_string(), + Value::Table(regenerate_stubs), + ); + + let mut compose_tasks = Vec::new(); + let mut compose_release_tasks = Vec::new(); + for caller in callers { + let mut compose = Map::default(); + compose.insert("cwd".to_string(), Value::String(".".to_string())); + compose.insert( + "command".to_string(), + Value::String(stubgen_command.to_string()), + ); + let mut args = Vec::new(); + args.extend(stubgen_prefix.iter().map(|s| Value::String(s.to_string()))); + args.push(Value::String("compose".to_string())); + args.push(Value::String("--source-wasm".to_string())); + args.push(Value::String(format!( + "target/wasm32-wasi/debug/{}.wasm", + caller + ))); + for target in targets { + args.push(Value::String("--stub-wasm".to_string())); + args.push(Value::String(format!( + "target/wasm32-wasi/debug/{}_stub.wasm", + target + ))); + } + args.push(Value::String("--dest-wasm".to_string())); + args.push(Value::String(format!( + "target/wasm32-wasi/debug/{}-composed.wasm", + caller + ))); + + compose.insert("args".to_string(), Value::Array(args)); + + let task_name = format!("compose-{}", caller); + tasks.insert(task_name.clone(), Value::Table(compose)); + compose_tasks.push(task_name); + + let mut compose_release = Map::default(); + compose_release.insert("cwd".to_string(), Value::String(".".to_string())); + compose_release.insert( + "command".to_string(), + Value::String(stubgen_command.to_string()), + ); + let mut args = Vec::new(); + args.extend(stubgen_prefix.iter().map(|s| Value::String(s.to_string()))); + args.push(Value::String("compose".to_string())); + args.push(Value::String("--source-wasm".to_string())); + args.push(Value::String(format!( + "target/wasm32-wasi/release/{}.wasm", + caller + ))); + for target in targets { + args.push(Value::String("--stub-wasm".to_string())); + args.push(Value::String(format!( + "target/wasm32-wasi/release/{}_stub.wasm", + target + ))); + } + args.push(Value::String("--dest-wasm".to_string())); + args.push(Value::String(format!( + "target/wasm32-wasi/release/{}-composed.wasm", + caller + ))); + + compose_release.insert("args".to_string(), Value::Array(args)); + + let task_name = format!("compose-release-{}", caller); + tasks.insert(task_name.clone(), Value::Table(compose_release)); + compose_release_tasks.push(task_name); + } + + let mut post_build = Map::default(); + post_build.insert( + "dependencies".to_string(), + Value::Array( + compose_tasks + .iter() + .map(|n| Value::String(n.to_string())) + .collect::>(), + ), + ); + tasks.insert("post-build".to_string(), Value::Table(post_build)); + + let mut post_build_release = Map::default(); + post_build_release.insert( + "dependencies".to_string(), + Value::Array( + compose_release_tasks + .iter() + .map(|n| Value::String(n.to_string())) + .collect::>(), + ), + ); + tasks.insert( + "post-build-release".to_string(), + Value::Table(post_build_release), + ); + + let mut build_flow = Map::default(); + build_flow.insert( + "dependencies".to_string(), + Value::Array(vec![ + Value::String("build".to_string()), + Value::String("post-build".to_string()), + ]), + ); + tasks.insert("build-flow".to_string(), Value::Table(build_flow)); + + let mut release_build_flow = Map::default(); + release_build_flow.insert( + "dependencies".to_string(), + Value::Array(vec![ + Value::String("build-release".to_string()), + Value::String("post-build-release".to_string()), + ]), + ); + tasks.insert( + "release-build-flow".to_string(), + Value::Table(release_build_flow), + ); + + root.insert("tasks".to_string(), Value::Table(tasks)); + + let result = toml::to_string(&root)?; + Ok(result) +} + +fn has_cargo_make() -> bool { + Command::new("cargo-make") + .args(["--version"]) + .output() + .is_ok() +}