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
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
/target/
|
||||||
|
/dist/
|
||||||
|
/var/
|
||||||
|
/.idea/
|
||||||
|
/components/*/wit/deps/
|
||||||
|
/components/*/wit/.deps.*
|
||||||
Generated
+1974
File diff suppressed because it is too large
Load Diff
+23
@@ -0,0 +1,23 @@
|
|||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"crates/wasmeld-package",
|
||||||
|
]
|
||||||
|
default-members = ["crates/wasmeld-package"]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
edition = "2024"
|
||||||
|
rust-version = "1.90"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
reqwest = { version = "0.13.4", default-features = false, features = ["blocking", "json", "multipart", "rustls"] }
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
serde_json = "1.0.149"
|
||||||
|
semver = "1.0.28"
|
||||||
|
sha2 = "0.10.9"
|
||||||
|
thiserror = "2.0.17"
|
||||||
|
toml = "0.9.8"
|
||||||
|
wit-component = "=0.243.0"
|
||||||
|
wit-parser = "0.243.0"
|
||||||
|
zip = { version = "2.4.2", default-features = false, features = ["deflate"] }
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "wasmeld-package"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
reqwest.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
semver.workspace = true
|
||||||
|
sha2.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
toml.workspace = true
|
||||||
|
wit-component.workspace = true
|
||||||
|
wit-parser.workspace = true
|
||||||
|
zip.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3.23.0"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "wasmeld"
|
||||||
|
path = "src/main.rs"
|
||||||
@@ -0,0 +1,356 @@
|
|||||||
|
//! Component and WIT packaging primitives shared by the Wasmeld CLI, Console,
|
||||||
|
//! and Runtime.
|
||||||
|
//!
|
||||||
|
//! A runnable component is distributed as a constrained `.wasmpkg` ZIP
|
||||||
|
//! containing exactly [`PACKAGE_MANIFEST_PATH`] and [`COMPONENT_PATH`]. WIT
|
||||||
|
//! packages use the Component Model binary WIT encoding implemented by the
|
||||||
|
//! [`wit_package`] module; they are not stored in `.wasmpkg` containers.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::BTreeSet,
|
||||||
|
io::{self, Cursor, Read, Seek, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use thiserror::Error;
|
||||||
|
use zip::{CompressionMethod, ZipArchive, ZipWriter, result::ZipError, write::SimpleFileOptions};
|
||||||
|
|
||||||
|
/// Resolves `wasmeld.toml`, `wit.lock`, Registry dependencies, and path replacements.
|
||||||
|
pub mod module;
|
||||||
|
/// Builds and inspects standard binary WIT packages.
|
||||||
|
pub mod wit_package;
|
||||||
|
|
||||||
|
/// Current `.wasmpkg` manifest schema.
|
||||||
|
pub const PACKAGE_SCHEMA_VERSION: u32 = 1;
|
||||||
|
/// Fixed manifest entry name inside a `.wasmpkg` archive.
|
||||||
|
pub const PACKAGE_MANIFEST_PATH: &str = "package.toml";
|
||||||
|
/// Fixed component entry name inside a `.wasmpkg` archive.
|
||||||
|
pub const COMPONENT_PATH: &str = "component.wasm";
|
||||||
|
/// Baseline service world implemented by Wasmeld components.
|
||||||
|
pub const SERVICE_WORLD: &str = "wasmeld:service/service-component@0.1.0";
|
||||||
|
const MAX_MANIFEST_BYTES: usize = 64 * 1024;
|
||||||
|
|
||||||
|
/// Identity and integrity metadata embedded in a `.wasmpkg` archive.
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct PackageManifest {
|
||||||
|
pub schema_version: u32,
|
||||||
|
pub id: String,
|
||||||
|
pub revision: String,
|
||||||
|
pub world: String,
|
||||||
|
pub component: String,
|
||||||
|
pub sha256: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PackageManifest {
|
||||||
|
/// Creates a manifest and computes the component digest.
|
||||||
|
pub fn new(
|
||||||
|
id: impl Into<String>,
|
||||||
|
revision: impl Into<String>,
|
||||||
|
world: impl Into<String>,
|
||||||
|
component: &[u8],
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
schema_version: PACKAGE_SCHEMA_VERSION,
|
||||||
|
id: id.into(),
|
||||||
|
revision: revision.into(),
|
||||||
|
world: world.into(),
|
||||||
|
component: COMPONENT_PATH.to_owned(),
|
||||||
|
sha256: component_sha256(component),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates schema compatibility, identifiers, world syntax, and digest format.
|
||||||
|
pub fn validate(&self) -> Result<(), PackageError> {
|
||||||
|
if self.schema_version != PACKAGE_SCHEMA_VERSION {
|
||||||
|
return Err(PackageError::InvalidPackage(format!(
|
||||||
|
"unsupported package schema version {}, expected {PACKAGE_SCHEMA_VERSION}",
|
||||||
|
self.schema_version
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
validate_identifier("id", &self.id)?;
|
||||||
|
validate_identifier("revision", &self.revision)?;
|
||||||
|
validate_world(&self.world)?;
|
||||||
|
if self.component != COMPONENT_PATH {
|
||||||
|
return Err(PackageError::InvalidPackage(format!(
|
||||||
|
"component must be {COMPONENT_PATH:?}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if self.sha256.len() != 64
|
||||||
|
|| !self
|
||||||
|
.sha256
|
||||||
|
.bytes()
|
||||||
|
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
|
||||||
|
{
|
||||||
|
return Err(PackageError::InvalidPackage(
|
||||||
|
"sha256 must be a lowercase SHA-256 digest".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A validated component package decoded from a `.wasmpkg` archive.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ComponentPackage {
|
||||||
|
pub manifest: PackageManifest,
|
||||||
|
pub component: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors produced while creating or validating component packages.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum PackageError {
|
||||||
|
#[error("invalid component package: {0}")]
|
||||||
|
InvalidPackage(String),
|
||||||
|
|
||||||
|
#[error("component exceeds the {limit}-byte uncompressed limit")]
|
||||||
|
ComponentTooLarge { limit: usize },
|
||||||
|
|
||||||
|
#[error("component digest mismatch: expected {expected}, found {actual}")]
|
||||||
|
DigestMismatch { expected: String, actual: String },
|
||||||
|
|
||||||
|
#[error("failed to read or write package: {0}")]
|
||||||
|
Io(#[from] io::Error),
|
||||||
|
|
||||||
|
#[error("invalid ZIP container: {0}")]
|
||||||
|
Zip(#[from] ZipError),
|
||||||
|
|
||||||
|
#[error("invalid package manifest: {0}")]
|
||||||
|
ManifestParse(#[from] toml::de::Error),
|
||||||
|
|
||||||
|
#[error("failed to serialize package manifest: {0}")]
|
||||||
|
ManifestSerialize(#[from] toml::ser::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a deterministic two-entry `.wasmpkg` archive.
|
||||||
|
///
|
||||||
|
/// The caller owns the destination writer. Runtime limits are intentionally not
|
||||||
|
/// included because the Console injects platform policy during registration.
|
||||||
|
pub fn write_package<W>(
|
||||||
|
writer: W,
|
||||||
|
id: impl Into<String>,
|
||||||
|
revision: impl Into<String>,
|
||||||
|
world: impl Into<String>,
|
||||||
|
component: &[u8],
|
||||||
|
) -> Result<PackageManifest, PackageError>
|
||||||
|
where
|
||||||
|
W: Write + Seek,
|
||||||
|
{
|
||||||
|
if component.is_empty() {
|
||||||
|
return Err(PackageError::InvalidPackage(
|
||||||
|
"component must not be empty".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let manifest = PackageManifest::new(id, revision, world, component);
|
||||||
|
manifest.validate()?;
|
||||||
|
let manifest_toml = toml::to_string_pretty(&manifest)?;
|
||||||
|
let options = SimpleFileOptions::default()
|
||||||
|
.compression_method(CompressionMethod::Deflated)
|
||||||
|
.unix_permissions(0o644);
|
||||||
|
let mut archive = ZipWriter::new(writer);
|
||||||
|
archive.start_file(PACKAGE_MANIFEST_PATH, options)?;
|
||||||
|
archive.write_all(manifest_toml.as_bytes())?;
|
||||||
|
archive.start_file(COMPONENT_PATH, options)?;
|
||||||
|
archive.write_all(component)?;
|
||||||
|
archive.finish()?;
|
||||||
|
Ok(manifest)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads and validates a `.wasmpkg` archive.
|
||||||
|
///
|
||||||
|
/// `max_component_bytes` applies to the uncompressed component so compressed
|
||||||
|
/// archives cannot bypass the platform's artifact limit.
|
||||||
|
pub fn read_package(
|
||||||
|
package_bytes: &[u8],
|
||||||
|
max_component_bytes: usize,
|
||||||
|
) -> Result<ComponentPackage, PackageError> {
|
||||||
|
let mut archive = ZipArchive::new(Cursor::new(package_bytes))?;
|
||||||
|
if archive.len() != 2 {
|
||||||
|
return Err(PackageError::InvalidPackage(format!(
|
||||||
|
"archive must contain exactly {PACKAGE_MANIFEST_PATH} and {COMPONENT_PATH}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut names = BTreeSet::new();
|
||||||
|
for index in 0..archive.len() {
|
||||||
|
let entry = archive.by_index(index)?;
|
||||||
|
let name = entry.name().to_owned();
|
||||||
|
if name != PACKAGE_MANIFEST_PATH && name != COMPONENT_PATH {
|
||||||
|
return Err(PackageError::InvalidPackage(format!(
|
||||||
|
"unexpected archive entry {name:?}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if !names.insert(name.clone()) {
|
||||||
|
return Err(PackageError::InvalidPackage(format!(
|
||||||
|
"duplicate archive entry {name:?}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !names.contains(PACKAGE_MANIFEST_PATH) || !names.contains(COMPONENT_PATH) {
|
||||||
|
return Err(PackageError::InvalidPackage(format!(
|
||||||
|
"archive must contain {PACKAGE_MANIFEST_PATH} and {COMPONENT_PATH}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let manifest_bytes = read_entry(&mut archive, PACKAGE_MANIFEST_PATH, MAX_MANIFEST_BYTES)
|
||||||
|
.map_err(|error| match error {
|
||||||
|
PackageError::ComponentTooLarge { .. } => PackageError::InvalidPackage(format!(
|
||||||
|
"{PACKAGE_MANIFEST_PATH} exceeds the {MAX_MANIFEST_BYTES}-byte limit"
|
||||||
|
)),
|
||||||
|
other => other,
|
||||||
|
})?;
|
||||||
|
let manifest = toml::from_slice::<PackageManifest>(&manifest_bytes)?;
|
||||||
|
manifest.validate()?;
|
||||||
|
|
||||||
|
let component = read_entry(&mut archive, COMPONENT_PATH, max_component_bytes)?;
|
||||||
|
if component.is_empty() {
|
||||||
|
return Err(PackageError::InvalidPackage(
|
||||||
|
"component must not be empty".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let actual = component_sha256(&component);
|
||||||
|
if actual != manifest.sha256 {
|
||||||
|
return Err(PackageError::DigestMismatch {
|
||||||
|
expected: manifest.sha256,
|
||||||
|
actual,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ComponentPackage {
|
||||||
|
manifest,
|
||||||
|
component,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_entry<R: Read + Seek>(
|
||||||
|
archive: &mut ZipArchive<R>,
|
||||||
|
name: &str,
|
||||||
|
limit: usize,
|
||||||
|
) -> Result<Vec<u8>, PackageError> {
|
||||||
|
let entry = archive.by_name(name)?;
|
||||||
|
if entry.size() > limit as u64 {
|
||||||
|
return Err(PackageError::ComponentTooLarge { limit });
|
||||||
|
}
|
||||||
|
let mut bytes = Vec::with_capacity(entry.size() as usize);
|
||||||
|
entry
|
||||||
|
.take(limit.saturating_add(1) as u64)
|
||||||
|
.read_to_end(&mut bytes)?;
|
||||||
|
if bytes.len() > limit {
|
||||||
|
return Err(PackageError::ComponentTooLarge { limit });
|
||||||
|
}
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn component_sha256(component: &[u8]) -> String {
|
||||||
|
format!("{:x}", Sha256::digest(component))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_identifier(name: &str, value: &str) -> Result<(), PackageError> {
|
||||||
|
let valid = !value.is_empty()
|
||||||
|
&& value.len() <= 128
|
||||||
|
&& value
|
||||||
|
.bytes()
|
||||||
|
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'));
|
||||||
|
if valid {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(PackageError::InvalidPackage(format!(
|
||||||
|
"{name} must contain only ASCII letters, digits, '.', '_' or '-' and be at most 128 bytes"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates the canonical `namespace:package/world@version` world form.
|
||||||
|
pub fn validate_world(world: &str) -> Result<(), PackageError> {
|
||||||
|
let valid = !world.is_empty()
|
||||||
|
&& world.len() <= 256
|
||||||
|
&& world.contains(':')
|
||||||
|
&& world.contains('/')
|
||||||
|
&& world.contains('@')
|
||||||
|
&& world.bytes().all(|byte| {
|
||||||
|
byte.is_ascii_alphanumeric() || matches!(byte, b':' | b'/' | b'@' | b'-' | b'_' | b'.')
|
||||||
|
});
|
||||||
|
if valid {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(PackageError::InvalidPackage(
|
||||||
|
"world must be a versioned WIT world path and be at most 256 bytes".to_owned(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::io::{Cursor, Write};
|
||||||
|
|
||||||
|
use zip::{ZipWriter, write::SimpleFileOptions};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn package_round_trip() {
|
||||||
|
let mut bytes = Cursor::new(Vec::new());
|
||||||
|
let written =
|
||||||
|
write_package(&mut bytes, "echo", "0.1.0", SERVICE_WORLD, b"component").unwrap();
|
||||||
|
let package = read_package(bytes.get_ref(), 1024).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(package.manifest, written);
|
||||||
|
assert_eq!(package.component, b"component");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_unexpected_archive_entries() {
|
||||||
|
let mut bytes = Cursor::new(Vec::new());
|
||||||
|
let mut archive = ZipWriter::new(&mut bytes);
|
||||||
|
let options = SimpleFileOptions::default();
|
||||||
|
archive.start_file(PACKAGE_MANIFEST_PATH, options).unwrap();
|
||||||
|
archive.write_all(b"invalid").unwrap();
|
||||||
|
archive.start_file(COMPONENT_PATH, options).unwrap();
|
||||||
|
archive.write_all(b"component").unwrap();
|
||||||
|
archive.start_file("../escape", options).unwrap();
|
||||||
|
archive.write_all(b"escape").unwrap();
|
||||||
|
archive.finish().unwrap();
|
||||||
|
|
||||||
|
let error = read_package(bytes.get_ref(), 1024).unwrap_err();
|
||||||
|
assert!(error.to_string().contains("exactly"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enforces_the_uncompressed_component_limit() {
|
||||||
|
let mut bytes = Cursor::new(Vec::new());
|
||||||
|
write_package(&mut bytes, "echo", "0.1.0", SERVICE_WORLD, b"component").unwrap();
|
||||||
|
|
||||||
|
let error = read_package(bytes.get_ref(), 4).unwrap_err();
|
||||||
|
assert!(matches!(
|
||||||
|
error,
|
||||||
|
PackageError::ComponentTooLarge { limit: 4 }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_unsafe_service_identifiers() {
|
||||||
|
let mut bytes = Cursor::new(Vec::new());
|
||||||
|
let error = write_package(
|
||||||
|
&mut bytes,
|
||||||
|
"../escape",
|
||||||
|
"0.1.0",
|
||||||
|
SERVICE_WORLD,
|
||||||
|
b"component",
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(error.to_string().contains("ASCII letters"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepts_component_specific_worlds() {
|
||||||
|
let manifest = PackageManifest::new(
|
||||||
|
"clock-probe",
|
||||||
|
"0.1.0",
|
||||||
|
"component:clock-probe/clock-probe-component@0.1.0",
|
||||||
|
b"component",
|
||||||
|
);
|
||||||
|
manifest.validate().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,454 @@
|
|||||||
|
//! 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()
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,173 @@
|
|||||||
|
//! Standard binary WIT package encoding and inspection.
|
||||||
|
//!
|
||||||
|
//! Package identity is derived from the encoded WIT itself. Registry clients
|
||||||
|
//! never submit a separate package name, version, or dependency manifest that
|
||||||
|
//! could disagree with the artifact.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use thiserror::Error;
|
||||||
|
use wit_parser::{
|
||||||
|
Resolve,
|
||||||
|
decoding::{DecodedWasm, decode},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Metadata schema returned by the Wasmeld WIT Registry API.
|
||||||
|
pub const WIT_PACKAGE_SCHEMA_VERSION: u32 = 1;
|
||||||
|
|
||||||
|
/// One exact, direct WIT package dependency.
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WitDependency {
|
||||||
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Metadata derived from a binary WIT package.
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WitPackageMetadata {
|
||||||
|
pub schema_version: u32,
|
||||||
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
pub sha256: String,
|
||||||
|
pub dependencies: Vec<WitDependency>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encoded package bytes together with metadata derived from those bytes.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct BuiltWitPackage {
|
||||||
|
pub metadata: WitPackageMetadata,
|
||||||
|
pub bytes: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors raised while parsing WIT source or decoding a binary WIT package.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum WitPackageError {
|
||||||
|
#[error("failed to parse WIT package at {path}: {message}")]
|
||||||
|
Source { path: PathBuf, message: String },
|
||||||
|
|
||||||
|
#[error("failed to encode WIT package: {0}")]
|
||||||
|
Encode(String),
|
||||||
|
|
||||||
|
#[error("invalid binary WIT package: {0}")]
|
||||||
|
Decode(String),
|
||||||
|
|
||||||
|
#[error("WIT package {0} must declare an explicit semantic version")]
|
||||||
|
MissingVersion(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a WIT source path and encodes its main package as binary WIT.
|
||||||
|
///
|
||||||
|
/// The package must declare an explicit semantic version. Dependencies
|
||||||
|
/// available through the source path are encoded as Component Model package
|
||||||
|
/// references and reported in [`WitPackageMetadata::dependencies`].
|
||||||
|
pub fn build_wit_package(path: impl AsRef<Path>) -> Result<BuiltWitPackage, WitPackageError> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
let mut resolve = Resolve::default();
|
||||||
|
let (package, _) = resolve
|
||||||
|
.push_path(path)
|
||||||
|
.map_err(|error| WitPackageError::Source {
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
message: error.to_string(),
|
||||||
|
})?;
|
||||||
|
let bytes = wit_component::encode(&resolve, package)
|
||||||
|
.map_err(|error| WitPackageError::Encode(error.to_string()))?;
|
||||||
|
let metadata = metadata_from_resolve(&resolve, package, &bytes)?;
|
||||||
|
Ok(BuiltWitPackage { metadata, bytes })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes binary WIT and derives its identity, direct dependencies, and digest.
|
||||||
|
///
|
||||||
|
/// Ordinary WebAssembly Components are rejected even though both formats use a
|
||||||
|
/// Component Model binary container.
|
||||||
|
pub fn inspect_wit_package(bytes: &[u8]) -> Result<WitPackageMetadata, WitPackageError> {
|
||||||
|
let decoded = decode(bytes).map_err(|error| WitPackageError::Decode(error.to_string()))?;
|
||||||
|
let DecodedWasm::WitPackage(resolve, package) = decoded else {
|
||||||
|
return Err(WitPackageError::Decode(
|
||||||
|
"artifact is a WebAssembly Component, not a binary WIT package".to_owned(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
metadata_from_resolve(&resolve, package, bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the lowercase SHA-256 digest used as the Registry artifact identity.
|
||||||
|
pub fn wit_package_sha256(bytes: &[u8]) -> String {
|
||||||
|
format!("{:x}", Sha256::digest(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata_from_resolve(
|
||||||
|
resolve: &Resolve,
|
||||||
|
package: wit_parser::PackageId,
|
||||||
|
bytes: &[u8],
|
||||||
|
) -> Result<WitPackageMetadata, WitPackageError> {
|
||||||
|
let package_name = &resolve.packages[package].name;
|
||||||
|
let name = format!("{}:{}", package_name.namespace, package_name.name);
|
||||||
|
let version = package_name
|
||||||
|
.version
|
||||||
|
.as_ref()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.ok_or_else(|| WitPackageError::MissingVersion(name.clone()))?;
|
||||||
|
let mut dependencies = resolve
|
||||||
|
.package_direct_deps(package)
|
||||||
|
.map(|dependency| {
|
||||||
|
let dependency = &resolve.packages[dependency].name;
|
||||||
|
let dependency_name = format!("{}:{}", dependency.namespace, dependency.name);
|
||||||
|
let dependency_version = dependency
|
||||||
|
.version
|
||||||
|
.as_ref()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.ok_or_else(|| WitPackageError::MissingVersion(dependency_name.clone()))?;
|
||||||
|
Ok(WitDependency {
|
||||||
|
name: dependency_name,
|
||||||
|
version: dependency_version,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, WitPackageError>>()?;
|
||||||
|
dependencies.sort();
|
||||||
|
dependencies.dedup();
|
||||||
|
|
||||||
|
Ok(WitPackageMetadata {
|
||||||
|
schema_version: WIT_PACKAGE_SCHEMA_VERSION,
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
sha256: wit_package_sha256(bytes),
|
||||||
|
dependencies,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builds_and_inspects_a_binary_wit_package() {
|
||||||
|
let source = tempdir().unwrap();
|
||||||
|
fs::write(
|
||||||
|
source.path().join("package.wit"),
|
||||||
|
"package wasmeld:clock@1.2.3;\ninterface clock { now: func() -> u64; }\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let package = build_wit_package(source.path()).unwrap();
|
||||||
|
let inspected = inspect_wit_package(&package.bytes).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(package.metadata, inspected);
|
||||||
|
assert_eq!(inspected.name, "wasmeld:clock");
|
||||||
|
assert_eq!(inspected.version, "1.2.3");
|
||||||
|
assert!(inspected.dependencies.is_empty());
|
||||||
|
assert_eq!(inspected.sha256.len(), 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_a_component_as_a_wit_package() {
|
||||||
|
let error = inspect_wit_package(b"not wasm").unwrap_err();
|
||||||
|
assert!(matches!(error, WitPackageError::Decode(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user