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:
Maofeng
2026-07-27 04:57:34 +08:00
commit 893895a76c
8 changed files with 4047 additions and 0 deletions
+173
View File
@@ -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(_)));
}
}