455 lines
15 KiB
Rust
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()
|
||
|
|
}
|