docs(package): explain packaging and dependency workflows

This commit is contained in:
Maofeng
2026-07-30 08:13:20 +08:00
parent a5718da1da
commit a1b808013a
5 changed files with 252 additions and 8 deletions
+37
View File
@@ -1,4 +1,22 @@
//! 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::{
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)?;
let output = temporary_dir.join("component.wasmpkg");
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;
println!("watching:");
@@ -138,6 +159,10 @@ fn deploy_component(
console: &str,
packed: &PackedComponent,
) -> 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)?;
activate(
client,
@@ -235,6 +260,9 @@ fn register(
if response.status().is_success() {
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
&& service_exists(
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);
if module_path.is_file() {
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() {
let path = component_root.join(&replacement.path);
if path.exists() {
@@ -351,6 +382,8 @@ fn source_fingerprint(roots: &[PathBuf]) -> Result<Vec<u8>, Box<dyn std::error::
}
let mut digest = Sha256::new();
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());
match fs::read(&path) {
Ok(bytes) => digest.update(bytes),
@@ -384,6 +417,8 @@ fn ignored_directory(path: &Path) -> bool {
name,
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")
|| path
.parent()
@@ -395,6 +430,8 @@ fn ignored_directory(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!(
path.extension().and_then(|extension| extension.to_str()),
Some("rs" | "wit")