//! Command-line entry point for Component packaging and WIT dependency workflows. use std::{ env, fs, path::{Path, PathBuf}, process::{Command, ExitCode}, }; use serde::Deserialize; use wasmeld_package::{ PackageManifest, module::{MODULE_MANIFEST_FILE, ModuleManifest, find_module_manifest, sync_dependencies}, wit_package::{WitPackageMetadata, build_wit_package}, write_package, }; const TARGET: &str = "wasm32-wasip2"; #[derive(Debug, Deserialize)] struct CargoMetadata { packages: Vec, target_directory: PathBuf, workspace_root: PathBuf, } #[derive(Debug, Deserialize)] struct CargoPackage { manifest_path: PathBuf, version: String, metadata: serde_json::Value, targets: Vec, } #[derive(Debug, Deserialize)] struct CargoTarget { name: String, crate_types: Vec, } fn main() -> ExitCode { match run() { Ok(()) => ExitCode::SUCCESS, Err(error) => { eprintln!("wasmeld: {error}"); ExitCode::FAILURE } } } fn run() -> Result<(), Box> { let mut arguments = env::args().skip(1); match arguments.next().as_deref() { Some("pack") => run_pack(arguments.collect()), Some("wit") => run_wit(arguments.collect()), _ => Err(usage().into()), } } fn run_pack(arguments: Vec) -> Result<(), Box> { let mut arguments = arguments.into_iter(); let manifest_path = arguments.next().ok_or_else(usage)?; let mut output = None; let mut no_build = false; let mut locked = false; while let Some(argument) = arguments.next() { match argument.as_str() { "--output" | "-o" => { output = Some( arguments .next() .map(PathBuf::from) .ok_or("--output requires a path")?, ); } "--no-build" => no_build = true, "--locked" => locked = true, _ => return Err(format!("unknown argument {argument:?}\n{}", usage()).into()), } } let manifest_path = fs::canonicalize(manifest_path)?; sync_component_dependencies(&manifest_path, locked)?; let metadata = cargo_metadata(&manifest_path)?; let package = metadata .packages .iter() .find(|package| package.manifest_path == manifest_path) .ok_or("Cargo metadata did not contain the requested package")?; let id = package .metadata .get("wasmeld") .and_then(|metadata| metadata.get("id")) .and_then(serde_json::Value::as_str) .ok_or("[package.metadata.wasmeld].id is required")?; let world = package .metadata .get("wasmeld") .and_then(|metadata| metadata.get("world")) .and_then(serde_json::Value::as_str) .ok_or("[package.metadata.wasmeld].world is required")?; PackageManifest::new(id, &package.version, world, b"identity-validation").validate()?; let target = package .targets .iter() .find(|target| target.crate_types.iter().any(|kind| kind == "cdylib")) .ok_or("component package must expose a cdylib target")?; if !no_build { build_component(&manifest_path)?; } let artifact = metadata .target_directory .join(TARGET) .join("release") .join(format!("{}.wasm", target.name.replace('-', "_"))); let component = fs::read(&artifact).map_err(|error| { format!( "failed to read built component {}: {error}", artifact.display() ) })?; let output = output.unwrap_or_else(|| { metadata .workspace_root .join("dist") .join(format!("{id}-{}.wasmpkg", package.version)) }); if let Some(parent) = output.parent() { fs::create_dir_all(parent)?; } let file = fs::File::create(&output)?; let manifest = write_package(file, id, &package.version, world, &component)?; println!( "packed {}@{} -> {}", manifest.id, manifest.revision, output.display() ); println!("component: {}", artifact.display()); println!("sha256: {}", manifest.sha256); Ok(()) } fn run_wit(arguments: Vec) -> Result<(), Box> { let mut arguments = arguments.into_iter(); match arguments.next().as_deref() { Some("fetch" | "tidy") => { let options = parse_wit_sync_options(arguments)?; let manifest = resolve_module_manifest(options.manifest)?; print_sync_report(&manifest, options.locked) } Some("graph") => { let manifest = resolve_module_manifest(parse_manifest_option(arguments)?)?; print_dependency_graph(&manifest) } Some("replace") => run_wit_replace(arguments), Some("build") => run_wit_build(arguments), Some("publish") => run_wit_publish(arguments), _ => Err(usage().into()), } } struct WitSyncOptions { manifest: Option, locked: bool, } fn parse_wit_sync_options( mut arguments: impl Iterator, ) -> Result> { let mut manifest = None; let mut locked = false; while let Some(argument) = arguments.next() { match argument.as_str() { "--manifest" => { manifest = Some( arguments .next() .map(PathBuf::from) .ok_or("--manifest requires a path")?, ); } "--locked" => locked = true, _ => return Err(format!("unknown argument {argument:?}\n{}", usage()).into()), } } Ok(WitSyncOptions { manifest, locked }) } fn parse_manifest_option( mut arguments: impl Iterator, ) -> Result, Box> { let mut manifest = None; while let Some(argument) = arguments.next() { match argument.as_str() { "--manifest" => { manifest = Some( arguments .next() .map(PathBuf::from) .ok_or("--manifest requires a path")?, ); } _ => return Err(format!("unknown argument {argument:?}\n{}", usage()).into()), } } Ok(manifest) } fn run_wit_replace( mut arguments: impl Iterator, ) -> Result<(), Box> { let package = arguments.next().ok_or("wit replace requires a package")?; let mut manifest_path = None; let mut replacement_path = None; let mut drop = false; while let Some(argument) = arguments.next() { match argument.as_str() { "--manifest" => { manifest_path = Some( arguments .next() .map(PathBuf::from) .ok_or("--manifest requires a path")?, ); } "--path" => { replacement_path = Some( arguments .next() .map(PathBuf::from) .ok_or("--path requires a directory")?, ); } "--drop" => drop = true, _ => return Err(format!("unknown argument {argument:?}\n{}", usage()).into()), } } if drop == replacement_path.is_some() { return Err("wit replace requires exactly one of --path or --drop".into()); } let manifest_path = resolve_module_manifest(manifest_path)?; let mut manifest = ModuleManifest::read(&manifest_path)?; if drop { if !manifest.drop_replacement(&package) { return Err(format!("replacement {package:?} does not exist").into()); } manifest.write(&manifest_path)?; println!("dropped replace {package}"); } else { let replacement_path = replacement_path.expect("validated above"); manifest.set_path_replacement(&package, &replacement_path)?; manifest.write(&manifest_path)?; println!("replaced {package} => path+{}", replacement_path.display()); } println!("manifest: {}", manifest_path.display()); Ok(()) } fn run_wit_build( mut arguments: impl Iterator, ) -> Result<(), Box> { let source = arguments.next().ok_or("wit build requires a source path")?; let mut output = None; while let Some(argument) = arguments.next() { match argument.as_str() { "--output" | "-o" => { output = Some( arguments .next() .map(PathBuf::from) .ok_or("--output requires a path")?, ); } _ => return Err(format!("unknown argument {argument:?}\n{}", usage()).into()), } } let package = build_wit_package(&source)?; let output = output.unwrap_or_else(|| { let basename = package.metadata.name.replace(':', "-"); PathBuf::from("dist/wit").join(format!("{basename}-{}.wasm", package.metadata.version)) }); if let Some(parent) = output.parent() { fs::create_dir_all(parent)?; } fs::write(&output, &package.bytes)?; println!( "built {}@{} -> {}", package.metadata.name, package.metadata.version, output.display() ); println!("sha256: {}", package.metadata.sha256); Ok(()) } fn run_wit_publish( mut arguments: impl Iterator, ) -> Result<(), Box> { let source = arguments .next() .ok_or("wit publish requires a source path")?; let mut registry = env::var("WASMELD_REGISTRY").ok(); while let Some(argument) = arguments.next() { match argument.as_str() { "--registry" => { registry = Some( arguments .next() .ok_or("--registry requires an HTTP(S) URL")?, ); } _ => return Err(format!("unknown argument {argument:?}\n{}", usage()).into()), } } let registry = registry .ok_or("wit publish requires --registry or the WASMELD_REGISTRY environment variable")?; let registry = registry.trim_end_matches('/'); let package = build_wit_package(&source)?; let url = format!("{registry}/api/v1/wit/packages"); let part = reqwest::blocking::multipart::Part::bytes(package.bytes) .file_name("package.wasm") .mime_str("application/wasm")?; let response = reqwest::blocking::Client::builder() .user_agent(format!("wasmeld/{}", env!("CARGO_PKG_VERSION"))) .build()? .post(&url) .multipart(reqwest::blocking::multipart::Form::new().part("package", part)) .send()?; let status = response.status(); if !status.is_success() { return Err(format!("registry returned HTTP {status}: {}", response.text()?).into()); } let published = response.json::()?; if published != package.metadata { return Err(format!( "registry metadata mismatch: expected {}@{} ({})", package.metadata.name, package.metadata.version, package.metadata.sha256 ) .into()); } println!( "published {}@{} -> {registry}", published.name, published.version ); println!("sha256: {}", published.sha256); Ok(()) } fn resolve_module_manifest( manifest: Option, ) -> Result> { match manifest { Some(path) => Ok(fs::canonicalize(path)?), None => Ok(find_module_manifest(env::current_dir()?)?), } } fn sync_component_dependencies( cargo_manifest: &Path, locked: bool, ) -> Result<(), Box> { let module_manifest = cargo_manifest.with_file_name(MODULE_MANIFEST_FILE); if module_manifest.is_file() { print_sync_report(&module_manifest, locked)?; } Ok(()) } fn print_sync_report(manifest: &Path, locked: bool) -> Result<(), Box> { let report = sync_dependencies(manifest, locked)?; let action = if locked { "verified" } else { "resolved" }; println!( "{action} {} WIT package(s) -> {}", report.packages.len(), report.wit_root.join("deps").display() ); for package in report.packages { println!( " {}@{} => {} ({})", package.name, package.version, package.source, package.sha256 ); } println!("lock: {}", report.lock_path.display()); Ok(()) } fn print_dependency_graph(manifest_path: &Path) -> Result<(), Box> { let manifest = ModuleManifest::read(manifest_path)?; println!("{}", manifest_path.display()); for (name, version) in &manifest.dependencies { let exact = format!("{name}@{version}"); let replacement = manifest .replacements .get(&exact) .or_else(|| manifest.replacements.get(name)); match replacement { Some(replacement) => { println!(" {exact} => path+{}", replacement.path.display()); } None => match &manifest.registry.url { Some(registry) => println!(" {exact} => registry+{registry}"), None => println!(" {exact} => registry (unconfigured)"), }, } } Ok(()) } fn cargo_metadata(manifest_path: &Path) -> Result> { let output = Command::new("cargo") .args(["metadata", "--format-version", "1", "--no-deps"]) .arg("--manifest-path") .arg(manifest_path) .output()?; if !output.status.success() { return Err(format!( "cargo metadata failed:\n{}", String::from_utf8_lossy(&output.stderr) ) .into()); } Ok(serde_json::from_slice(&output.stdout)?) } fn build_component(manifest_path: &Path) -> Result<(), Box> { let toolchain = env::var("WASMELD_COMPONENT_TOOLCHAIN").unwrap_or_else(|_| "1.90.0".to_owned()); let status = Command::new("rustup") .args(["run", &toolchain, "cargo", "build"]) .arg("--manifest-path") .arg(manifest_path) .args(["--target", TARGET, "--release"]) .status()?; if !status.success() { return Err(format!("component build failed with status {status}").into()); } Ok(()) } fn usage() -> String { "usage: wasmeld pack [--output ] [--no-build] [--locked] wasmeld wit fetch [--manifest ] [--locked] wasmeld wit tidy [--manifest ] [--locked] wasmeld wit graph [--manifest ] wasmeld wit replace --path [--manifest ] wasmeld wit replace --drop [--manifest ] wasmeld wit build [--output ] wasmeld wit publish --registry " .to_owned() }