docs(package): explain packaging and dependency workflows
This commit is contained in:
@@ -1,4 +1,22 @@
|
|||||||
//! Local build, watch, and deployment loop for Component development.
|
//! Local build, watch, and deployment loop for Component development.
|
||||||
|
//!
|
||||||
|
//! The loop fingerprints Rust/WIT inputs, materializes WIT dependencies,
|
||||||
|
//! compiles a Component, creates a content-addressed development revision,
|
||||||
|
//! registers it, and atomically switches the Console deployment. A failed
|
||||||
|
//! build or deployment never removes the previously active revision.
|
||||||
|
//!
|
||||||
|
//! Build and deployment are separate states. Once a build succeeds,
|
||||||
|
//! `pending_deployment` retains that exact package and retries network or
|
||||||
|
//! Console failures without rebuilding it. A later source change supersedes
|
||||||
|
//! the pending package and starts a new build.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! cargo run -p wasmeld-package --bin wasmeld -- \
|
||||||
|
//! dev components/counter/Cargo.toml \
|
||||||
|
//! --console http://127.0.0.1:8080
|
||||||
|
//! ```
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::BTreeSet,
|
collections::BTreeSet,
|
||||||
@@ -57,6 +75,9 @@ pub(crate) fn run(arguments: Vec<String>) -> Result<(), Box<dyn std::error::Erro
|
|||||||
fs::create_dir_all(&temporary_dir)?;
|
fs::create_dir_all(&temporary_dir)?;
|
||||||
let output = temporary_dir.join("component.wasmpkg");
|
let output = temporary_dir.join("component.wasmpkg");
|
||||||
let mut observed = None;
|
let mut observed = None;
|
||||||
|
// Keeping this separate from `observed` lets transient deployment errors
|
||||||
|
// retry the already-built bytes. Rebuilding on every HTTP failure would
|
||||||
|
// waste time and could create a different revision unexpectedly.
|
||||||
let mut pending_deployment = None;
|
let mut pending_deployment = None;
|
||||||
|
|
||||||
println!("watching:");
|
println!("watching:");
|
||||||
@@ -138,6 +159,10 @@ fn deploy_component(
|
|||||||
console: &str,
|
console: &str,
|
||||||
packed: &PackedComponent,
|
packed: &PackedComponent,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Order is intentional: register makes the immutable revision available,
|
||||||
|
// activate switches new gateway resolutions, and only then may old
|
||||||
|
// development revisions be removed. Cleanup is best-effort because a
|
||||||
|
// successful deployment must not be reported as failed due to stale files.
|
||||||
register(client, console, packed)?;
|
register(client, console, packed)?;
|
||||||
activate(
|
activate(
|
||||||
client,
|
client,
|
||||||
@@ -235,6 +260,9 @@ fn register(
|
|||||||
if response.status().is_success() {
|
if response.status().is_success() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
// Retrying a package whose registration response was lost is safe only
|
||||||
|
// when the exact immutable identity already exists. Do not treat every
|
||||||
|
// conflict as success; it may represent a different control-plane error.
|
||||||
if response.status() == StatusCode::CONFLICT
|
if response.status() == StatusCode::CONFLICT
|
||||||
&& service_exists(
|
&& service_exists(
|
||||||
client,
|
client,
|
||||||
@@ -334,6 +362,9 @@ fn watch_roots(manifest_path: &Path) -> Result<Vec<PathBuf>, Box<dyn std::error:
|
|||||||
let module_path = component_root.join(MODULE_MANIFEST_FILE);
|
let module_path = component_root.join(MODULE_MANIFEST_FILE);
|
||||||
if module_path.is_file() {
|
if module_path.is_file() {
|
||||||
let module = ModuleManifest::read(&module_path)?;
|
let module = ModuleManifest::read(&module_path)?;
|
||||||
|
// Path replacements are source inputs just like the Component itself.
|
||||||
|
// Registry dependencies are immutable and represented by wit.lock, so
|
||||||
|
// they do not need independent watch roots.
|
||||||
for replacement in module.replacements.values() {
|
for replacement in module.replacements.values() {
|
||||||
let path = component_root.join(&replacement.path);
|
let path = component_root.join(&replacement.path);
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
@@ -351,6 +382,8 @@ fn source_fingerprint(roots: &[PathBuf]) -> Result<Vec<u8>, Box<dyn std::error::
|
|||||||
}
|
}
|
||||||
let mut digest = Sha256::new();
|
let mut digest = Sha256::new();
|
||||||
for path in files {
|
for path in files {
|
||||||
|
// Include paths as well as bytes so file renames trigger a rebuild even
|
||||||
|
// when their contents are unchanged.
|
||||||
digest.update(path.to_string_lossy().as_bytes());
|
digest.update(path.to_string_lossy().as_bytes());
|
||||||
match fs::read(&path) {
|
match fs::read(&path) {
|
||||||
Ok(bytes) => digest.update(bytes),
|
Ok(bytes) => digest.update(bytes),
|
||||||
@@ -384,6 +417,8 @@ fn ignored_directory(path: &Path) -> bool {
|
|||||||
name,
|
name,
|
||||||
Some(".git" | ".wasmeld" | "deps" | "dist" | "node_modules" | "target")
|
Some(".git" | ".wasmeld" | "deps" | "dist" | "node_modules" | "target")
|
||||||
) {
|
) {
|
||||||
|
// `wit/deps` is generated by dependency sync. Watching it would make
|
||||||
|
// each build rewrite watched files and trigger an endless rebuild.
|
||||||
return name != Some("deps")
|
return name != Some("deps")
|
||||||
|| path
|
|| path
|
||||||
.parent()
|
.parent()
|
||||||
@@ -395,6 +430,8 @@ fn ignored_directory(path: &Path) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_source_file(path: &Path) -> bool {
|
fn is_source_file(path: &Path) -> bool {
|
||||||
|
// The list is deliberately narrow: fingerprints should represent inputs
|
||||||
|
// to Cargo/WIT resolution, not editor state or generated artifacts.
|
||||||
matches!(
|
matches!(
|
||||||
path.extension().and_then(|extension| extension.to_str()),
|
path.extension().and_then(|extension| extension.to_str()),
|
||||||
Some("rs" | "wit")
|
Some("rs" | "wit")
|
||||||
|
|||||||
@@ -5,6 +5,34 @@
|
|||||||
//! containing exactly [`PACKAGE_MANIFEST_PATH`] and [`COMPONENT_PATH`]. WIT
|
//! containing exactly [`PACKAGE_MANIFEST_PATH`] and [`COMPONENT_PATH`]. WIT
|
||||||
//! packages use the Component Model binary WIT encoding implemented by the
|
//! packages use the Component Model binary WIT encoding implemented by the
|
||||||
//! [`wit_package`] module; they are not stored in `.wasmpkg` containers.
|
//! [`wit_package`] module; they are not stored in `.wasmpkg` containers.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use std::io::Cursor;
|
||||||
|
//! use wasmeld_package::{read_package, write_package};
|
||||||
|
//!
|
||||||
|
//! let component = b"\0asm\x0d\0\x01\0";
|
||||||
|
//! let mut archive = Cursor::new(Vec::new());
|
||||||
|
//! write_package(
|
||||||
|
//! &mut archive,
|
||||||
|
//! "image-resize",
|
||||||
|
//! "1.0.0",
|
||||||
|
//! "example:image-resize/service@1.0.0",
|
||||||
|
//! component,
|
||||||
|
//! )?;
|
||||||
|
//!
|
||||||
|
//! let decoded = read_package(archive.get_ref(), 1024 * 1024)?;
|
||||||
|
//! assert_eq!(decoded.manifest.id, "image-resize");
|
||||||
|
//! assert_eq!(decoded.component, component);
|
||||||
|
//! # Ok::<(), wasmeld_package::PackageError>(())
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! The decoder treats an uploaded archive as untrusted input: it accepts only
|
||||||
|
//! the two canonical entries, limits uncompressed sizes, validates identifiers,
|
||||||
|
//! and verifies the Component digest before returning bytes to the Runtime.
|
||||||
|
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::BTreeSet,
|
collections::BTreeSet,
|
||||||
@@ -35,16 +63,26 @@ const MAX_MANIFEST_BYTES: usize = 64 * 1024;
|
|||||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct PackageManifest {
|
pub struct PackageManifest {
|
||||||
|
/// Manifest schema understood by this version of Wasmeld.
|
||||||
pub schema_version: u32,
|
pub schema_version: u32,
|
||||||
|
/// Stable service identifier used in management and gateway routes.
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
/// Immutable service revision; the same `id@revision` cannot be overwritten.
|
||||||
pub revision: String,
|
pub revision: String,
|
||||||
|
/// Fully versioned WIT world implemented by the Component.
|
||||||
pub world: String,
|
pub world: String,
|
||||||
|
/// Archive path of the executable Component; currently always [`COMPONENT_PATH`].
|
||||||
pub component: String,
|
pub component: String,
|
||||||
|
/// Lowercase SHA-256 digest of the uncompressed Component bytes.
|
||||||
pub sha256: String,
|
pub sha256: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PackageManifest {
|
impl PackageManifest {
|
||||||
/// Creates a manifest and computes the component digest.
|
/// Creates a manifest and computes the component digest.
|
||||||
|
///
|
||||||
|
/// This constructor does not reject invalid identifiers or worlds. Call
|
||||||
|
/// [`Self::validate`] before persisting it; [`write_package`] does this
|
||||||
|
/// automatically.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
id: impl Into<String>,
|
id: impl Into<String>,
|
||||||
revision: impl Into<String>,
|
revision: impl Into<String>,
|
||||||
@@ -94,31 +132,48 @@ impl PackageManifest {
|
|||||||
/// A validated component package decoded from a `.wasmpkg` archive.
|
/// A validated component package decoded from a `.wasmpkg` archive.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ComponentPackage {
|
pub struct ComponentPackage {
|
||||||
|
/// Validated identity, world, and integrity metadata.
|
||||||
pub manifest: PackageManifest,
|
pub manifest: PackageManifest,
|
||||||
|
/// Uncompressed Component bytes whose digest matches the manifest.
|
||||||
pub component: Vec<u8>,
|
pub component: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Errors produced while creating or validating component packages.
|
/// Errors produced while creating or validating component packages.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum PackageError {
|
pub enum PackageError {
|
||||||
|
/// The archive layout or a manifest value violates the package contract.
|
||||||
#[error("invalid component package: {0}")]
|
#[error("invalid component package: {0}")]
|
||||||
InvalidPackage(String),
|
InvalidPackage(String),
|
||||||
|
|
||||||
|
/// The uncompressed Component exceeds the caller-selected upload limit.
|
||||||
#[error("component exceeds the {limit}-byte uncompressed limit")]
|
#[error("component exceeds the {limit}-byte uncompressed limit")]
|
||||||
ComponentTooLarge { limit: usize },
|
ComponentTooLarge {
|
||||||
|
/// Maximum number of accepted uncompressed bytes.
|
||||||
|
limit: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Component bytes do not match the immutable digest in `package.toml`.
|
||||||
#[error("component digest mismatch: expected {expected}, found {actual}")]
|
#[error("component digest mismatch: expected {expected}, found {actual}")]
|
||||||
DigestMismatch { expected: String, actual: String },
|
DigestMismatch {
|
||||||
|
/// Digest declared by the package manifest.
|
||||||
|
expected: String,
|
||||||
|
/// Digest computed from the uploaded Component.
|
||||||
|
actual: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Reading or writing the package stream failed.
|
||||||
#[error("failed to read or write package: {0}")]
|
#[error("failed to read or write package: {0}")]
|
||||||
Io(#[from] io::Error),
|
Io(#[from] io::Error),
|
||||||
|
|
||||||
|
/// The bytes are not a valid constrained ZIP archive.
|
||||||
#[error("invalid ZIP container: {0}")]
|
#[error("invalid ZIP container: {0}")]
|
||||||
Zip(#[from] ZipError),
|
Zip(#[from] ZipError),
|
||||||
|
|
||||||
|
/// `package.toml` could not be decoded.
|
||||||
#[error("invalid package manifest: {0}")]
|
#[error("invalid package manifest: {0}")]
|
||||||
ManifestParse(#[from] toml::de::Error),
|
ManifestParse(#[from] toml::de::Error),
|
||||||
|
|
||||||
|
/// A generated package manifest could not be encoded.
|
||||||
#[error("failed to serialize package manifest: {0}")]
|
#[error("failed to serialize package manifest: {0}")]
|
||||||
ManifestSerialize(#[from] toml::ser::Error),
|
ManifestSerialize(#[from] toml::ser::Error),
|
||||||
}
|
}
|
||||||
@@ -127,6 +182,11 @@ pub enum PackageError {
|
|||||||
///
|
///
|
||||||
/// The caller owns the destination writer. Runtime limits are intentionally not
|
/// The caller owns the destination writer. Runtime limits are intentionally not
|
||||||
/// included because the Console injects platform policy during registration.
|
/// included because the Console injects platform policy during registration.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error when the identity or world is invalid, the Component is
|
||||||
|
/// empty, or the destination cannot be written.
|
||||||
pub fn write_package<W>(
|
pub fn write_package<W>(
|
||||||
writer: W,
|
writer: W,
|
||||||
id: impl Into<String>,
|
id: impl Into<String>,
|
||||||
@@ -162,6 +222,11 @@ where
|
|||||||
///
|
///
|
||||||
/// `max_component_bytes` applies to the uncompressed component so compressed
|
/// `max_component_bytes` applies to the uncompressed component so compressed
|
||||||
/// archives cannot bypass the platform's artifact limit.
|
/// archives cannot bypass the platform's artifact limit.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error for malformed ZIP data, extra or duplicate entries,
|
||||||
|
/// oversized content, invalid metadata, empty Components, or digest mismatch.
|
||||||
pub fn read_package(
|
pub fn read_package(
|
||||||
package_bytes: &[u8],
|
package_bytes: &[u8],
|
||||||
max_component_bytes: usize,
|
max_component_bytes: usize,
|
||||||
|
|||||||
@@ -1,4 +1,18 @@
|
|||||||
//! Command-line entry point for Component packaging and WIT dependency workflows.
|
//! Command-line entry point for Component packaging and WIT dependency workflows.
|
||||||
|
//!
|
||||||
|
//! Typical release packaging:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! wasmeld wit fetch --manifest components/counter/wasmeld.toml --locked
|
||||||
|
//! wasmeld pack components/counter/Cargo.toml --locked
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! `wit fetch` resolves registry or local `replace` sources into `wit/deps`
|
||||||
|
//! and writes `wit.lock`. `--locked` verifies that resolution matches the
|
||||||
|
//! existing lock file and is the appropriate mode for CI. A `replace` changes
|
||||||
|
//! only the development source used for a declared package identity; it does
|
||||||
|
//! not rename that package or make the local path part of the published
|
||||||
|
//! Component contract.
|
||||||
|
|
||||||
mod dev;
|
mod dev;
|
||||||
|
|
||||||
@@ -21,7 +35,9 @@ const TARGET: &str = "wasm32-wasip2";
|
|||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub(crate) enum BuildProfile {
|
pub(crate) enum BuildProfile {
|
||||||
|
/// Fast local build with Cargo's `debug` profile.
|
||||||
Debug,
|
Debug,
|
||||||
|
/// Optimized artifact from Cargo's `release` profile.
|
||||||
Release,
|
Release,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,22 +51,36 @@ impl BuildProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) enum PackageIdentity {
|
pub(crate) enum PackageIdentity {
|
||||||
|
/// Use `[package.metadata.wasmeld].id` and the Cargo package version.
|
||||||
Cargo,
|
Cargo,
|
||||||
|
/// Use a development service ID and a Component-content revision suffix.
|
||||||
Development { id: Option<String> },
|
Development { id: Option<String> },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct PackRequest {
|
pub(crate) struct PackRequest {
|
||||||
|
/// Cargo manifest for the Component crate.
|
||||||
pub manifest_path: PathBuf,
|
pub manifest_path: PathBuf,
|
||||||
|
/// Destination package, or the workspace `dist` default.
|
||||||
pub output: Option<PathBuf>,
|
pub output: Option<PathBuf>,
|
||||||
|
/// Reuse an artifact already present under Cargo's target directory.
|
||||||
|
///
|
||||||
|
/// The caller is responsible for ensuring it matches current sources and
|
||||||
|
/// the requested profile.
|
||||||
pub no_build: bool,
|
pub no_build: bool,
|
||||||
|
/// Require `wit.lock` to match dependency resolution exactly.
|
||||||
pub locked: bool,
|
pub locked: bool,
|
||||||
|
/// Cargo profile used to build and locate the Component.
|
||||||
pub profile: BuildProfile,
|
pub profile: BuildProfile,
|
||||||
|
/// Stable release or content-addressed development identity.
|
||||||
pub identity: PackageIdentity,
|
pub identity: PackageIdentity,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct PackedComponent {
|
pub(crate) struct PackedComponent {
|
||||||
|
/// Validated metadata embedded in the `.wasmpkg`.
|
||||||
pub manifest: PackageManifest,
|
pub manifest: PackageManifest,
|
||||||
|
/// Path to the resulting `.wasmpkg`.
|
||||||
pub output: PathBuf,
|
pub output: PathBuf,
|
||||||
|
/// Path to the raw Component produced or reused by Cargo.
|
||||||
pub artifact: PathBuf,
|
pub artifact: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +171,9 @@ pub(crate) fn pack_component(
|
|||||||
request: PackRequest,
|
request: PackRequest,
|
||||||
) -> Result<PackedComponent, Box<dyn std::error::Error>> {
|
) -> Result<PackedComponent, Box<dyn std::error::Error>> {
|
||||||
let manifest_path = fs::canonicalize(request.manifest_path)?;
|
let manifest_path = fs::canonicalize(request.manifest_path)?;
|
||||||
|
// Materialize WIT before asking Cargo for metadata or compiling: the
|
||||||
|
// component's `wit_bindgen::generate!` reads `wit/world.wit` and
|
||||||
|
// `wit/deps` during macro expansion.
|
||||||
sync_component_dependencies(&manifest_path, request.locked)?;
|
sync_component_dependencies(&manifest_path, request.locked)?;
|
||||||
let metadata = cargo_metadata(&manifest_path)?;
|
let metadata = cargo_metadata(&manifest_path)?;
|
||||||
let package = metadata
|
let package = metadata
|
||||||
@@ -189,6 +222,9 @@ pub(crate) fn pack_component(
|
|||||||
let (id, revision) = match request.identity {
|
let (id, revision) = match request.identity {
|
||||||
PackageIdentity::Cargo => (cargo_id, cargo_revision),
|
PackageIdentity::Cargo => (cargo_id, cargo_revision),
|
||||||
PackageIdentity::Development { id } => {
|
PackageIdentity::Development { id } => {
|
||||||
|
// The revision changes only when compiled Component bytes change.
|
||||||
|
// This makes registration retryable and avoids mutating an
|
||||||
|
// immutable identity while editing.
|
||||||
let digest = format!("{:x}", Sha256::digest(&component));
|
let digest = format!("{:x}", Sha256::digest(&component));
|
||||||
(
|
(
|
||||||
id.unwrap_or_else(|| format!("{cargo_id}-dev")),
|
id.unwrap_or_else(|| format!("{cargo_id}-dev")),
|
||||||
@@ -316,6 +352,8 @@ fn run_wit_replace(
|
|||||||
|
|
||||||
let manifest_path = resolve_module_manifest(manifest_path)?;
|
let manifest_path = resolve_module_manifest(manifest_path)?;
|
||||||
let mut manifest = ModuleManifest::read(&manifest_path)?;
|
let mut manifest = ModuleManifest::read(&manifest_path)?;
|
||||||
|
// Replace is recorded in the source manifest. The next fetch/pack resolves
|
||||||
|
// and locks its contents; generated `wit/deps` must not be hand-edited.
|
||||||
if drop {
|
if drop {
|
||||||
if !manifest.drop_replacement(&package) {
|
if !manifest.drop_replacement(&package) {
|
||||||
return Err(format!("replacement {package:?} does not exist").into());
|
return Err(format!("replacement {package:?} does not exist").into());
|
||||||
@@ -407,6 +445,9 @@ fn run_wit_publish(
|
|||||||
return Err(format!("registry returned HTTP {status}: {}", response.text()?).into());
|
return Err(format!("registry returned HTTP {status}: {}", response.text()?).into());
|
||||||
}
|
}
|
||||||
let published = response.json::<WitPackageMetadata>()?;
|
let published = response.json::<WitPackageMetadata>()?;
|
||||||
|
// A successful status is insufficient: compare the server's canonical
|
||||||
|
// identity and digest to detect a proxy or Registry returning metadata for
|
||||||
|
// different bytes.
|
||||||
if published != package.metadata {
|
if published != package.metadata {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"registry metadata mismatch: expected {}@{} ({})",
|
"registry metadata mismatch: expected {}@{} ({})",
|
||||||
|
|||||||
@@ -4,6 +4,23 @@
|
|||||||
//! prefers root-level path replacements and otherwise downloads immutable
|
//! prefers root-level path replacements and otherwise downloads immutable
|
||||||
//! binary packages from the configured Registry. The complete transitive graph
|
//! binary packages from the configured Registry. The complete transitive graph
|
||||||
//! is materialized under `wit/deps` and recorded in `wit.lock`.
|
//! is materialized under `wit/deps` and recorded in `wit.lock`.
|
||||||
|
//!
|
||||||
|
//! `wasmeld.toml` is the human-authored input; `wit.lock` is the reproducibility
|
||||||
|
//! record; and `wit/deps` is disposable generated output. Commit the first two,
|
||||||
|
//! but regenerate `wit/deps` with [`crate::module::sync_dependencies`] after
|
||||||
|
//! cloning.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! use wasmeld_package::module::sync_dependencies;
|
||||||
|
//!
|
||||||
|
//! // CI uses locked mode so changed Registry content, replacements, or
|
||||||
|
//! // transitive dependencies fail instead of rewriting wit.lock.
|
||||||
|
//! let report = sync_dependencies("components/image-resize/wasmeld.toml", true)?;
|
||||||
|
//! println!("resolved {} packages", report.packages.len());
|
||||||
|
//! # Ok::<(), wasmeld_package::module::ModuleError>(())
|
||||||
|
//! ```
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, BTreeSet},
|
collections::{BTreeMap, BTreeSet},
|
||||||
@@ -156,39 +173,79 @@ pub struct SyncReport {
|
|||||||
/// Errors raised while parsing, resolving, locking, or materializing WIT dependencies.
|
/// Errors raised while parsing, resolving, locking, or materializing WIT dependencies.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ModuleError {
|
pub enum ModuleError {
|
||||||
|
/// A manifest value, dependency identity, version, or path is invalid.
|
||||||
#[error("invalid WIT module: {0}")]
|
#[error("invalid WIT module: {0}")]
|
||||||
InvalidManifest(String),
|
InvalidManifest(String),
|
||||||
|
|
||||||
|
/// A manifest, lock file, or local WIT source could not be read.
|
||||||
#[error("failed to read {path}: {source}")]
|
#[error("failed to read {path}: {source}")]
|
||||||
Read { path: PathBuf, source: io::Error },
|
Read {
|
||||||
|
/// File or directory involved in the failed read.
|
||||||
|
path: PathBuf,
|
||||||
|
/// Underlying filesystem error.
|
||||||
|
source: io::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A lock file or materialized dependency could not be written.
|
||||||
#[error("failed to write {path}: {source}")]
|
#[error("failed to write {path}: {source}")]
|
||||||
Write { path: PathBuf, source: io::Error },
|
Write {
|
||||||
|
/// Destination that could not be updated.
|
||||||
|
path: PathBuf,
|
||||||
|
/// Underlying filesystem error.
|
||||||
|
source: io::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// TOML input could not be decoded.
|
||||||
#[error("failed to parse {path}: {source}")]
|
#[error("failed to parse {path}: {source}")]
|
||||||
ParseManifest {
|
ParseManifest {
|
||||||
|
/// Manifest or lock file containing invalid TOML.
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
/// TOML decoder error.
|
||||||
source: toml::de::Error,
|
source: toml::de::Error,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// A canonical manifest or lock file could not be encoded as TOML.
|
||||||
#[error("failed to serialize {path}: {source}")]
|
#[error("failed to serialize {path}: {source}")]
|
||||||
SerializeManifest {
|
SerializeManifest {
|
||||||
|
/// Destination whose model could not be serialized.
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
/// TOML encoder error.
|
||||||
source: toml::ser::Error,
|
source: toml::ser::Error,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Local or downloaded bytes are not the requested versioned WIT package.
|
||||||
#[error("invalid WIT package at {path}: {message}")]
|
#[error("invalid WIT package at {path}: {message}")]
|
||||||
InvalidWitPackage { path: PathBuf, message: String },
|
InvalidWitPackage {
|
||||||
|
/// Source path used while validating the package.
|
||||||
|
path: PathBuf,
|
||||||
|
/// Identity, version, parsing, or dependency validation detail.
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Locked resolution produced a graph different from `wit.lock`.
|
||||||
#[error("locked dependency graph differs from {path}; run `wasmeld wit tidy`")]
|
#[error("locked dependency graph differs from {path}; run `wasmeld wit tidy`")]
|
||||||
LockMismatch { path: PathBuf },
|
LockMismatch {
|
||||||
|
/// Existing lock file that must be updated outside locked mode.
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Fetching or validating an immutable Registry artifact failed.
|
||||||
#[error("WIT registry request to {url} failed: {message}")]
|
#[error("WIT registry request to {url} failed: {message}")]
|
||||||
Registry { url: String, message: String },
|
Registry {
|
||||||
|
/// Exact Registry endpoint requested by the resolver.
|
||||||
|
url: String,
|
||||||
|
/// HTTP, size-limit, digest, or package-validation detail.
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModuleManifest {
|
impl ModuleManifest {
|
||||||
/// Reads and validates a `wasmeld.toml` manifest.
|
/// Reads and validates a `wasmeld.toml` manifest.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`ModuleError::Read`], [`ModuleError::ParseManifest`], or
|
||||||
|
/// [`ModuleError::InvalidManifest`] without modifying the source file.
|
||||||
pub fn read(path: impl AsRef<Path>) -> Result<Self, ModuleError> {
|
pub fn read(path: impl AsRef<Path>) -> Result<Self, ModuleError> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let source = fs::read_to_string(path).map_err(|source| ModuleError::Read {
|
let source = fs::read_to_string(path).map_err(|source| ModuleError::Read {
|
||||||
@@ -205,6 +262,9 @@ impl ModuleManifest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Validates and writes a canonical TOML manifest.
|
/// Validates and writes a canonical TOML manifest.
|
||||||
|
///
|
||||||
|
/// Callers editing replacements should mutate a parsed value and invoke
|
||||||
|
/// this method once, so invalid intermediate state is never persisted.
|
||||||
pub fn write(&self, path: impl AsRef<Path>) -> Result<(), ModuleError> {
|
pub fn write(&self, path: impl AsRef<Path>) -> Result<(), ModuleError> {
|
||||||
self.validate()?;
|
self.validate()?;
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
@@ -248,6 +308,9 @@ impl ModuleManifest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sets a package-wide or exact-version path replacement.
|
/// Sets a package-wide or exact-version path replacement.
|
||||||
|
///
|
||||||
|
/// An exact key such as `wasmeld:kv@0.1.0` takes precedence over the
|
||||||
|
/// package-wide `wasmeld:kv` key during resolution.
|
||||||
pub fn set_path_replacement(
|
pub fn set_path_replacement(
|
||||||
&mut self,
|
&mut self,
|
||||||
package: impl Into<String>,
|
package: impl Into<String>,
|
||||||
@@ -311,6 +374,16 @@ impl ModuleLock {
|
|||||||
/// dependencies embedded in Registry artifacts. With `locked = true`, the
|
/// dependencies embedded in Registry artifacts. With `locked = true`, the
|
||||||
/// computed graph must exactly match the existing `wit.lock`; the function does
|
/// computed graph must exactly match the existing `wit.lock`; the function does
|
||||||
/// not update the lock file.
|
/// not update the lock file.
|
||||||
|
///
|
||||||
|
/// The dependency directory is assembled in a temporary sibling directory and
|
||||||
|
/// swapped into place only after the complete graph validates. A failed fetch
|
||||||
|
/// therefore leaves both the lock and materialized dependencies intact.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error when the manifest or lock is invalid, a replacement has the
|
||||||
|
/// wrong identity, a Registry package is unavailable or oversized, or two paths
|
||||||
|
/// resolve the same package version to different content.
|
||||||
pub fn sync_dependencies(
|
pub fn sync_dependencies(
|
||||||
manifest_path: impl AsRef<Path>,
|
manifest_path: impl AsRef<Path>,
|
||||||
locked: bool,
|
locked: bool,
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ pub const WIT_PACKAGE_SCHEMA_VERSION: u32 = 1;
|
|||||||
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct WitDependency {
|
pub struct WitDependency {
|
||||||
|
/// Package identity in `namespace:name` form.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
/// Exact semantic version encoded by the dependency package.
|
||||||
pub version: String,
|
pub version: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,32 +31,48 @@ pub struct WitDependency {
|
|||||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct WitPackageMetadata {
|
pub struct WitPackageMetadata {
|
||||||
|
/// Metadata response schema used by the Registry API.
|
||||||
pub schema_version: u32,
|
pub schema_version: u32,
|
||||||
|
/// Package identity derived from binary WIT contents.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
/// Explicit semantic version derived from binary WIT contents.
|
||||||
pub version: String,
|
pub version: String,
|
||||||
|
/// SHA-256 digest of the complete binary WIT artifact.
|
||||||
pub sha256: String,
|
pub sha256: String,
|
||||||
|
/// Exact direct dependencies embedded in the package.
|
||||||
pub dependencies: Vec<WitDependency>,
|
pub dependencies: Vec<WitDependency>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encoded package bytes together with metadata derived from those bytes.
|
/// Encoded package bytes together with metadata derived from those bytes.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct BuiltWitPackage {
|
pub struct BuiltWitPackage {
|
||||||
|
/// Metadata derived by decoding the bytes after encoding.
|
||||||
pub metadata: WitPackageMetadata,
|
pub metadata: WitPackageMetadata,
|
||||||
|
/// Standard Component Model binary WIT package.
|
||||||
pub bytes: Vec<u8>,
|
pub bytes: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Errors raised while parsing WIT source or decoding a binary WIT package.
|
/// Errors raised while parsing WIT source or decoding a binary WIT package.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum WitPackageError {
|
pub enum WitPackageError {
|
||||||
|
/// WIT source could not be parsed or resolved.
|
||||||
#[error("failed to parse WIT package at {path}: {message}")]
|
#[error("failed to parse WIT package at {path}: {message}")]
|
||||||
Source { path: PathBuf, message: String },
|
Source {
|
||||||
|
/// Source file or directory passed to the parser.
|
||||||
|
path: PathBuf,
|
||||||
|
/// Parser or dependency-resolution detail.
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A resolved WIT package could not be encoded.
|
||||||
#[error("failed to encode WIT package: {0}")]
|
#[error("failed to encode WIT package: {0}")]
|
||||||
Encode(String),
|
Encode(String),
|
||||||
|
|
||||||
|
/// Input bytes are not a standard binary WIT package.
|
||||||
#[error("invalid binary WIT package: {0}")]
|
#[error("invalid binary WIT package: {0}")]
|
||||||
Decode(String),
|
Decode(String),
|
||||||
|
|
||||||
|
/// The package identity omits the version required by the Registry.
|
||||||
#[error("WIT package {0} must declare an explicit semantic version")]
|
#[error("WIT package {0} must declare an explicit semantic version")]
|
||||||
MissingVersion(String),
|
MissingVersion(String),
|
||||||
}
|
}
|
||||||
@@ -64,6 +82,11 @@ pub enum WitPackageError {
|
|||||||
/// The package must declare an explicit semantic version. Dependencies
|
/// The package must declare an explicit semantic version. Dependencies
|
||||||
/// available through the source path are encoded as Component Model package
|
/// available through the source path are encoded as Component Model package
|
||||||
/// references and reported in [`WitPackageMetadata::dependencies`].
|
/// references and reported in [`WitPackageMetadata::dependencies`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error when WIT parsing, dependency resolution, binary encoding,
|
||||||
|
/// or post-encode inspection fails.
|
||||||
pub fn build_wit_package(path: impl AsRef<Path>) -> Result<BuiltWitPackage, WitPackageError> {
|
pub fn build_wit_package(path: impl AsRef<Path>) -> Result<BuiltWitPackage, WitPackageError> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let mut resolve = Resolve::default();
|
let mut resolve = Resolve::default();
|
||||||
@@ -83,6 +106,11 @@ pub fn build_wit_package(path: impl AsRef<Path>) -> Result<BuiltWitPackage, WitP
|
|||||||
///
|
///
|
||||||
/// Ordinary WebAssembly Components are rejected even though both formats use a
|
/// Ordinary WebAssembly Components are rejected even though both formats use a
|
||||||
/// Component Model binary container.
|
/// Component Model binary container.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the bytes are malformed, contain a Component instead of
|
||||||
|
/// a WIT package, or any package in the direct dependency set lacks a version.
|
||||||
pub fn inspect_wit_package(bytes: &[u8]) -> Result<WitPackageMetadata, WitPackageError> {
|
pub fn inspect_wit_package(bytes: &[u8]) -> Result<WitPackageMetadata, WitPackageError> {
|
||||||
let decoded = decode(bytes).map_err(|error| WitPackageError::Decode(error.to_string()))?;
|
let decoded = decode(bytes).map_err(|error| WitPackageError::Decode(error.to_string()))?;
|
||||||
let DecodedWasm::WitPackage(resolve, package) = decoded else {
|
let DecodedWasm::WitPackage(resolve, package) = decoded else {
|
||||||
|
|||||||
Reference in New Issue
Block a user