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,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