Files
wasmeld/crates/wasmeld-package/src/main.rs
T
Maofeng 893895a76c feat(package): add component and WIT package tooling
- define the constrained .wasmpkg archive and integrity checks
- encode and inspect standard binary WIT packages
- resolve exact Registry dependencies with wit.lock and path replace
- provide wasmeld pack and wit build/fetch/publish CLI commands
2026-07-27 04:57:34 +08:00

455 lines
15 KiB
Rust

//! 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<CargoPackage>,
target_directory: PathBuf,
workspace_root: PathBuf,
}
#[derive(Debug, Deserialize)]
struct CargoPackage {
manifest_path: PathBuf,
version: String,
metadata: serde_json::Value,
targets: Vec<CargoTarget>,
}
#[derive(Debug, Deserialize)]
struct CargoTarget {
name: String,
crate_types: Vec<String>,
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("wasmeld: {error}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
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<String>) -> Result<(), Box<dyn std::error::Error>> {
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<String>) -> Result<(), Box<dyn std::error::Error>> {
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<PathBuf>,
locked: bool,
}
fn parse_wit_sync_options(
mut arguments: impl Iterator<Item = String>,
) -> Result<WitSyncOptions, Box<dyn std::error::Error>> {
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<Item = String>,
) -> Result<Option<PathBuf>, Box<dyn std::error::Error>> {
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<Item = String>,
) -> Result<(), Box<dyn std::error::Error>> {
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<Item = String>,
) -> Result<(), Box<dyn std::error::Error>> {
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<Item = String>,
) -> Result<(), Box<dyn std::error::Error>> {
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::<WitPackageMetadata>()?;
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<PathBuf>,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<CargoMetadata, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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 <Cargo.toml> [--output <file.wasmpkg>] [--no-build] [--locked]
wasmeld wit fetch [--manifest <wasmeld.toml>] [--locked]
wasmeld wit tidy [--manifest <wasmeld.toml>] [--locked]
wasmeld wit graph [--manifest <wasmeld.toml>]
wasmeld wit replace <package[@version]> --path <directory> [--manifest <wasmeld.toml>]
wasmeld wit replace <package[@version]> --drop [--manifest <wasmeld.toml>]
wasmeld wit build <wit-path> [--output <package.wasm>]
wasmeld wit publish <wit-path> --registry <url>"
.to_owned()
}