diff --git a/Cargo.lock b/Cargo.lock index fd045d8..855c801 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -386,6 +386,20 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "oci-loader" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "path-loader-router" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -558,6 +572,13 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "test-loader-harness" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "thiserror" version = "1.0.69" diff --git a/Cargo.toml b/Cargo.toml index 3a87500..f5e3e8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,9 @@ members = [ "components/extract-wit", "components/filesystem-loader", "components/http-loader", + "components/oci-loader", + "components/path-loader-router", + "components/test-loader-harness", "components/wac-loader", ] diff --git a/Makefile b/Makefile index 1f92290..8e35ec1 100644 --- a/Makefile +++ b/Makefile @@ -66,11 +66,6 @@ endef $(foreach name,$(TOOLS),$(eval $(call INSTALL_TOOL,$(name)))) -# the target a cargo component is built for, the package's `default-target`, e.g. `wasm32-wasip3`. -# wasm32-unknown-unknown builds a core module that is wrapped as a component, a wasi target builds -# a component. -cargo_target = $(or $(shell sed -n 's/^default-target = "\(.*\)"$$/\1/p' components/$(1)/Cargo.toml),wasm32-unknown-unknown) - .PHONY: components components: ${COMPONENTS_DIR}/interface.wasm $(foreach component,$(COMPONENTS),${COMPONENTS_DIR}/$(component)/$(component).wasm ${COMPONENTS_DIR}/$(component)/$(component).debug.wasm) @@ -118,20 +113,12 @@ ${COMPONENTS_DIR}/$1/$1.debug.wasm: components/$1/$1.wkg ${COMPONENTS_DIR}/$1/RE else ifneq ($(wildcard components/$1/Cargo.toml),) ${COMPONENTS_DIR}/$1/$1.wasm: Cargo.toml Cargo.lock components/wit/deps $(shell find components/$1 -type f) $(shell find crates -type f 2> /dev/null) ${COMPONENTS_DIR}/$1/README.md | $(call tool,wasm-tools) - cargo build -p $1 --target $(call cargo_target,$1) --release -ifeq ($(call cargo_target,$1),wasm32-unknown-unknown) + cargo build -p $1 --target wasm32-unknown-unknown --release wasm-tools component new target/wasm32-unknown-unknown/release/$(subst -,_,$1).wasm -o ${COMPONENTS_DIR}/$1/$1.wasm -else - cp target/$(call cargo_target,$1)/release/$(subst -,_,$1).wasm ${COMPONENTS_DIR}/$1/$1.wasm -endif ${COMPONENTS_DIR}/$1/$1.debug.wasm: Cargo.toml Cargo.lock components/wit/deps $(shell find components/$1 -type f) $(shell find crates -type f 2> /dev/null) ${COMPONENTS_DIR}/$1/README.md | $(call tool,wasm-tools) - cargo build --target $(call cargo_target,$1) -p $1 -ifeq ($(call cargo_target,$1),wasm32-unknown-unknown) + cargo build --target wasm32-unknown-unknown -p $1 wasm-tools component new target/wasm32-unknown-unknown/debug/$(subst -,_,$1).wasm -o ${COMPONENTS_DIR}/$1/$1.debug.wasm -else - cp target/$(call cargo_target,$1)/debug/$(subst -,_,$1).wasm ${COMPONENTS_DIR}/$1/$1.debug.wasm -endif endif diff --git a/README.md b/README.md index 593c717..172f296 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,9 @@ The cli tools the build uses, [`wasm-tools`](https://github.com/bytecodealliance - [`extract-wit`](./components/extract-wit/) - [`filesystem-loader`](./components/filesystem-loader/) - [`http-loader`](./components/http-loader/) +- [`oci-loader`](./components/oci-loader/) +- [`path-loader`](./components/path-loader/) +- [`path-loader-router`](./components/path-loader-router/) - [`wac-loader`](./components/wac-loader/) ## Community diff --git a/components/dep-componentized-http-client/README.md b/components/dep-componentized-http-client/README.md new file mode 100644 index 0000000..ef9fc65 --- /dev/null +++ b/components/dep-componentized-http-client/README.md @@ -0,0 +1,3 @@ +# `dep-componentized-http-client` + +from https://github.com/componentized/http/tree/main/components/http-client diff --git a/components/dep-componentized-http-client/dep-componentized-http-client.wkg b/components/dep-componentized-http-client/dep-componentized-http-client.wkg new file mode 100644 index 0000000..afa4788 --- /dev/null +++ b/components/dep-componentized-http-client/dep-componentized-http-client.wkg @@ -0,0 +1,2 @@ +ghcr.io/componentized/http/client:0.1.0-dev@sha256:cec0f0e8cf10ae936ff8b45a78ac199c0f7a8e2c0a3cb1b3e86ec6cac6ad1646 +ghcr.io/componentized/http/client:0.1.0-dev_debug@sha256:bfe41f3aa3fb3329fb12c2fa8fe1d6048b95bd4fcb0d87f102f5a8d012a2a51d diff --git a/components/dep-componentized-oci-client/README.md b/components/dep-componentized-oci-client/README.md new file mode 100644 index 0000000..e7d9359 --- /dev/null +++ b/components/dep-componentized-oci-client/README.md @@ -0,0 +1,3 @@ +# `dep-componentized-oci-client` + +from https://github.com/componentized/oci/tree/main/components/client diff --git a/components/dep-componentized-oci-client/dep-componentized-oci-client.wkg b/components/dep-componentized-oci-client/dep-componentized-oci-client.wkg new file mode 100644 index 0000000..7e80648 --- /dev/null +++ b/components/dep-componentized-oci-client/dep-componentized-oci-client.wkg @@ -0,0 +1,2 @@ +ghcr.io/componentized/oci/client:0.1.0-dev@sha256:a5c60dc24dea1883e347f67a7f3929694b620c8096fb4afff54cb8b9fbc95004 +ghcr.io/componentized/oci/client:0.1.0-dev_debug@sha256:075eecc73333b82ce0854907e06015296f7c3a839539d7b5457bec31d95e7a00 diff --git a/components/filesystem-loader/Cargo.toml b/components/filesystem-loader/Cargo.toml index ce7f056..8600b09 100644 --- a/components/filesystem-loader/Cargo.toml +++ b/components/filesystem-loader/Cargo.toml @@ -1,13 +1,9 @@ -cargo-features = ["per-package-target"] - [package] name = "filesystem-loader" version = "0.1.0" edition = "2024" license = "Apache-2.0" -default-target = "wasm32-wasip3" - [lib] crate-type = ["cdylib"] # built for the default-target, a wasm test binary is unable to run on the host diff --git a/components/filesystem-loader/src/lib.rs b/components/filesystem-loader/src/lib.rs index dc46eb2..a1f3470 100644 --- a/components/filesystem-loader/src/lib.rs +++ b/components/filesystem-loader/src/lib.rs @@ -1,8 +1,11 @@ -use std::{fs, io}; +use std::path::Path; use crate::{ - exports::componentized::component::path_loader::Guest, - exports::componentized::component::types::{Component, Error}, + exports::componentized::component::{ + path_loader::Guest, + types::{Component, Error}, + }, + wasi::filesystem::{preopens, types as filesystem}, }; pub(crate) struct FilesystemLoader; @@ -10,14 +13,144 @@ pub(crate) struct FilesystemLoader; impl Guest for FilesystemLoader { #[allow(async_fn_in_trait)] async fn load(path: String) -> Result { - let component = fs::read(path)?; + let (dir, path) = resolve(&path)?; + let file = dir + .open_at( + filesystem::PathFlags::SYMLINK_FOLLOW, + path, + filesystem::OpenFlags::empty(), + filesystem::DescriptorFlags::READ, + ) + .await?; + let (data, result) = file.read_via_stream(0); + let component = data.collect().await; + result.await?; Ok(component) } } -impl From for Error { - fn from(value: io::Error) -> Self { - Self::Other(Some(value.to_string())) +/// Resolve a path to the preopened directory that contains it and the path relative to that +/// directory. When multiple preopens match, the most specific one wins. +fn resolve(path: &str) -> Result<(filesystem::Descriptor, String), Error> { + let path = Path::new(path); + let mut resolved: Option<(usize, filesystem::Descriptor, String)> = None; + for (dir, name) in preopens::get_directories() { + let prefix = Path::new(&name); + let relative = if path.is_relative() && prefix == Path::new(".") { + Some(path) + } else { + path.strip_prefix(prefix).ok() + }; + let Some(relative) = relative.and_then(Path::to_str) else { + continue; + }; + let specificity = prefix.components().count(); + if resolved.as_ref().is_none_or(|(s, _, _)| specificity > *s) { + resolved = Some((specificity, dir, relative.to_string())); + } + } + resolved.map(|(_, dir, path)| (dir, path)).ok_or_else(|| { + Error::Other(Some(format!( + "filesystem: no preopen for path {}", + path.display() + ))) + }) +} + +impl From for Error { + fn from(value: filesystem::ErrorCode) -> Self { + match value { + filesystem::ErrorCode::Access => Self::Other(Some("filesystem: access".to_string())), + filesystem::ErrorCode::Already => Self::Other(Some("filesystem: already".to_string())), + filesystem::ErrorCode::BadDescriptor => { + Self::Other(Some("filesystem: bad-descriptor".to_string())) + } + filesystem::ErrorCode::Busy => Self::Other(Some("filesystem: busy".to_string())), + filesystem::ErrorCode::Deadlock => { + Self::Other(Some("filesystem: deadlock".to_string())) + } + filesystem::ErrorCode::Quota => Self::Other(Some("filesystem: quota".to_string())), + filesystem::ErrorCode::Exist => Self::Other(Some("filesystem: exist".to_string())), + filesystem::ErrorCode::FileTooLarge => { + Self::Other(Some("filesystem: file-too-large".to_string())) + } + filesystem::ErrorCode::IllegalByteSequence => { + Self::Other(Some("filesystem: illegal-byte-sequence".to_string())) + } + filesystem::ErrorCode::InProgress => { + Self::Other(Some("filesystem: in-progress".to_string())) + } + filesystem::ErrorCode::Interrupted => { + Self::Other(Some("filesystem: interrupted".to_string())) + } + filesystem::ErrorCode::Invalid => Self::Other(Some("filesystem: invalid".to_string())), + filesystem::ErrorCode::Io => Self::Other(Some("filesystem: io".to_string())), + filesystem::ErrorCode::IsDirectory => { + Self::Other(Some("filesystem: is-directory".to_string())) + } + filesystem::ErrorCode::Loop => Self::Other(Some("filesystem: loop".to_string())), + filesystem::ErrorCode::TooManyLinks => { + Self::Other(Some("filesystem: too-many-links".to_string())) + } + filesystem::ErrorCode::MessageSize => { + Self::Other(Some("filesystem: message-size".to_string())) + } + filesystem::ErrorCode::NameTooLong => { + Self::Other(Some("filesystem: name-too-long".to_string())) + } + filesystem::ErrorCode::NoDevice => { + Self::Other(Some("filesystem: no-device".to_string())) + } + filesystem::ErrorCode::NoEntry => Self::Other(Some("filesystem: no-entry".to_string())), + filesystem::ErrorCode::NoLock => Self::Other(Some("filesystem: no-lock".to_string())), + filesystem::ErrorCode::InsufficientMemory => { + Self::Other(Some("filesystem: insufficient-memory".to_string())) + } + filesystem::ErrorCode::InsufficientSpace => { + Self::Other(Some("filesystem: insufficient-space".to_string())) + } + filesystem::ErrorCode::NotDirectory => { + Self::Other(Some("filesystem: not-directory".to_string())) + } + filesystem::ErrorCode::NotEmpty => { + Self::Other(Some("filesystem: not-empty".to_string())) + } + filesystem::ErrorCode::NotRecoverable => { + Self::Other(Some("filesystem: not-recoverable".to_string())) + } + filesystem::ErrorCode::Unsupported => { + Self::Other(Some("filesystem: unsupported".to_string())) + } + filesystem::ErrorCode::NoTty => Self::Other(Some("filesystem: no-tty".to_string())), + filesystem::ErrorCode::NoSuchDevice => { + Self::Other(Some("filesystem: no-such-device".to_string())) + } + filesystem::ErrorCode::Overflow => { + Self::Other(Some("filesystem: overflow".to_string())) + } + filesystem::ErrorCode::NotPermitted => { + Self::Other(Some("filesystem: not-permitted".to_string())) + } + filesystem::ErrorCode::Pipe => Self::Other(Some("filesystem: pipe".to_string())), + filesystem::ErrorCode::ReadOnly => { + Self::Other(Some("filesystem: read-only".to_string())) + } + filesystem::ErrorCode::InvalidSeek => { + Self::Other(Some("filesystem: invalid-seek".to_string())) + } + filesystem::ErrorCode::TextFileBusy => { + Self::Other(Some("filesystem: text-file-busy".to_string())) + } + filesystem::ErrorCode::CrossDevice => { + Self::Other(Some("filesystem: cross-device".to_string())) + } + filesystem::ErrorCode::Other(None) => { + Self::Other(Some("filesystem: other".to_string())) + } + filesystem::ErrorCode::Other(Some(message)) => { + Self::Other(Some(format!("filesystem: other: {message}"))) + } + } } } diff --git a/components/oci-loader/Cargo.toml b/components/oci-loader/Cargo.toml new file mode 100644 index 0000000..cfb2534 --- /dev/null +++ b/components/oci-loader/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "oci-loader" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/components/oci-loader/README.md b/components/oci-loader/README.md new file mode 100644 index 0000000..9408ca5 --- /dev/null +++ b/components/oci-loader/README.md @@ -0,0 +1,3 @@ +# `oci-loader` + +Loads a component from an OCI registry by reference. diff --git a/components/oci-loader/src/lib.rs b/components/oci-loader/src/lib.rs new file mode 100644 index 0000000..3b9c07b --- /dev/null +++ b/components/oci-loader/src/lib.rs @@ -0,0 +1,135 @@ +use crate::{ + componentized::oci::client::{self as oci, Digest, Reference}, + exports::componentized::component::{ + path_loader::Guest, + types::{Component, Error}, + }, +}; + +pub(crate) struct OCILoader; + +impl Guest for OCILoader { + #[allow(async_fn_in_trait)] + async fn load(path: String) -> Result { + let manifest_reference = oci::parse_reference(&path)?; + let manifest = match oci::get_manifest(manifest_reference.clone()).await? { + oci::Manifest::OciImageV1(oci_image_manifest_v1) => oci_image_manifest_v1, + _ => Err(Error::Other(Some("unexpected manifest".to_string())))?, + }; + match manifest.config.media_type { + oci::MediaType::ApplicationVndWasmConfigV0(oci::MediaTypeSuffix::Json) => {} + _ => Err(Error::Other(Some( + "unexpected config media type".to_string(), + )))?, + }; + if manifest.layers.len() != 1 { + Err(Error::Other(Some("unknown artifact layout".to_string())))? + } + let component_descriptor = manifest.layers.first().unwrap(); + match component_descriptor.media_type { + oci::MediaType::ApplicationWasm => {} + _ => Err(Error::Other(Some( + "unknown artifact media type".to_string(), + )))?, + } + + let config_reference = manifest_reference.with_digest(&manifest.config.digest); + let config = + match oci::get_config(config_reference, Some(manifest.config.media_type.clone())) + .await? + { + oci::Config::WasmV0(wasm_config_v0) => wasm_config_v0, + _ => Err(Error::Other(Some("unexpected config".to_string())))?, + }; + if config.component.is_none() { + Err(Error::Other(Some("not a component".to_string())))? + } + + let component_reference = + manifest_reference.with_digest(&manifest.layers.first().unwrap().digest); + let component = oci::get_blob(component_reference).await?; + if component_descriptor.size != component.len() as u64 { + Err(Error::Other(Some(format!( + "component size mismatch: expected {expected}, actual {actual}", + expected = component_descriptor.size, + actual = component.len() + ))))? + } + + Ok(component) + } +} + +impl Reference { + fn with_digest(&self, digest: &Digest) -> Self { + Self { + registry: self.registry.clone(), + repository: self.repository.clone(), + tag: None, + digest: Some(digest.clone()), + } + } +} + +impl From for Error { + fn from(value: oci::ErrorCode) -> Self { + match value { + oci::ErrorCode::BlobUnknown(message) => { + Self::Other(Some(format!("OCI error blob-unknown: {message}"))) + } + oci::ErrorCode::BlobUploadInvalid(message) => { + Self::Other(Some(format!("OCI error blob-upload-invalid: {message}"))) + } + oci::ErrorCode::BlobUploadUnknown(message) => { + Self::Other(Some(format!("OCI error blob-upload-unknown: {message}"))) + } + oci::ErrorCode::DigestInvalid(message) => { + Self::Other(Some(format!("OCI error digest-invalid: {message}"))) + } + oci::ErrorCode::ManifestBlobUnknown(message) => { + Self::Other(Some(format!("OCI error manifest-blob-unknown: {message}"))) + } + oci::ErrorCode::ManifestInvalid(message) => { + Self::Other(Some(format!("OCI error manifest-invalid: {message}"))) + } + oci::ErrorCode::ManifestUnknown(message) => { + Self::Other(Some(format!("OCI error manifest-unknown: {message}"))) + } + oci::ErrorCode::NameInvalid(message) => { + Self::Other(Some(format!("OCI error name-invalid: {message}"))) + } + oci::ErrorCode::NameUnknown(message) => { + Self::Other(Some(format!("OCI error name-unknown: {message}"))) + } + oci::ErrorCode::SizeInvalid(message) => { + Self::Other(Some(format!("OCI error size-invalid: {message}"))) + } + oci::ErrorCode::Unauthorized(message) => { + Self::Other(Some(format!("OCI error unauthorized: {message}"))) + } + oci::ErrorCode::Denied(message) => { + Self::Other(Some(format!("OCI error denied: {message}"))) + } + oci::ErrorCode::Unsupported(message) => { + Self::Other(Some(format!("OCI error unsupported: {message}"))) + } + oci::ErrorCode::Toomanyrequests(_) => { + // TODO detect and retry request if within a grace period + Self::Other(Some(format!("OCI error toomanyrequests"))) + } + oci::ErrorCode::Other(Some(message)) => { + Self::Other(Some(format!("OCI error other: {message}"))) + } + oci::ErrorCode::Other(None) => Self::Other(Some(format!("OCI error other"))), + } + } +} + +wit_bindgen::generate!({ + path: "../wit", + world: "oci-loader", + merge_structurally_equal_types: true, + generate_all +}); + +export!(OCILoader); diff --git a/components/path-loader-router/Cargo.toml b/components/path-loader-router/Cargo.toml new file mode 100644 index 0000000..ae29e06 --- /dev/null +++ b/components/path-loader-router/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "path-loader-router" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/components/path-loader-router/README.md b/components/path-loader-router/README.md new file mode 100644 index 0000000..34410ea --- /dev/null +++ b/components/path-loader-router/README.md @@ -0,0 +1,14 @@ +# `path-loader-router` + +Routes a component loader based on the path. + +See [`path-loader`](../path-loader/) for the router with the loader components installed. + +Based on the path prefix the request is routed to an appropriate loader: + +- `http://` -> http-loader: scheme is preserved +- `https://` -> http-loader: scheme is preserved +- `file:///` -> filesystem-loader: scheme is dropped, last '/' is preserved with path +- `file://` -> invalid, other hosts are not supported +- `file:` -> filesystem-loader: scheme is dropped +- `oci://` -> oci-loader: scheme is dropped diff --git a/components/path-loader-router/src/lib.rs b/components/path-loader-router/src/lib.rs new file mode 100644 index 0000000..0b2d13c --- /dev/null +++ b/components/path-loader-router/src/lib.rs @@ -0,0 +1,38 @@ +use crate::exports::componentized::component::{ + path_loader::Guest, + types::{Component, Error}, +}; + +pub(crate) struct PathLoader; + +impl Guest for PathLoader { + #[allow(async_fn_in_trait)] + async fn load(path: String) -> Result { + if path.starts_with("http:") { + http_loader::load(path).await + } else if path.starts_with("https:") { + http_loader::load(path).await + } else if path.starts_with("file:///") { + filesystem_loader::load(path[7..].to_string()).await + } else if path.starts_with("file://") { + Err(Error::Other(Some(format!( + "invalid filesystem-path loader: {path}" + )))) + } else if path.starts_with("file:") { + filesystem_loader::load(path[5..].to_string()).await + } else if path.starts_with("oci://") { + oci_loader::load(path[6..].to_string()).await + } else { + Err(Error::Other(Some(format!("unknown path loader: {path}")))) + } + } +} + +wit_bindgen::generate!({ + path: "../wit", + world: "path-loader", + merge_structurally_equal_types: true, + generate_all +}); + +export!(PathLoader); diff --git a/components/path-loader/README.md b/components/path-loader/README.md new file mode 100644 index 0000000..894afd3 --- /dev/null +++ b/components/path-loader/README.md @@ -0,0 +1,14 @@ +# `path-loader` + +Loads a component from a path. + +See [`path-loader-router`](../path-loader-router/) for the router without the loader components installed. + +Based on the path prefix the request is routed to an appropriate loader: + +- `http://` -> http-loader: scheme is preserved +- `https://` -> http-loader: scheme is preserved +- `file:///` -> filesystem-loader: scheme is dropped, last '/' is preserved with path +- `file://` -> invalid, other hosts are not supported +- `file:` -> filesystem-loader: scheme is dropped +- `oci://` -> oci-loader: scheme is dropped diff --git a/components/path-loader/path-loader.wac b/components/path-loader/path-loader.wac new file mode 100644 index 0000000..d5210aa --- /dev/null +++ b/components/path-loader/path-loader.wac @@ -0,0 +1,8 @@ +package componentized:component; + +export new local:path-loader-router{ + filesystem-loader: new local:filesystem-loader{ ... }.path-loader, + http-loader: new local:http-loader{ ... }.path-loader, + oci-loader: new local:oci-loader{ ... }.path-loader, + ... +}...; diff --git a/components/test-loader-harness/Cargo.toml b/components/test-loader-harness/Cargo.toml new file mode 100644 index 0000000..ed8bd7b --- /dev/null +++ b/components/test-loader-harness/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "test-loader-harness" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/components/test-loader-harness/README.md b/components/test-loader-harness/README.md new file mode 100644 index 0000000..7e343ec --- /dev/null +++ b/components/test-loader-harness/README.md @@ -0,0 +1,3 @@ +# `test-loader-core` + +Test harness for a path loader. diff --git a/components/test-loader-harness/src/lib.rs b/components/test-loader-harness/src/lib.rs new file mode 100644 index 0000000..217b9ef --- /dev/null +++ b/components/test-loader-harness/src/lib.rs @@ -0,0 +1,61 @@ +use crate::{ + componentized::component::{path_loader, types::Error}, + exports::wasi::cli::run::Guest, + wasi::cli::{environment, stderr, stdout}, +}; + +/// Writes `$bytes` (a `Vec`) to a wasi:cli output stream (`stdout` or `stderr`). +/// +/// Must be used within an async context. Evaluates to `Result<(), ()>`, which is `Err` +/// if the host reports an error or stops reading before all bytes are written. +macro_rules! write_to { + ($stream:ident, $bytes:expr) => { + async { + let (mut tx, rx) = wit_stream::new(); + // hand the reader to the host before writing, otherwise the write never completes + let result = $stream::write_via_stream(rx); + let remaining = tx.write_all($bytes).await; + // close the stream so the host sees EOF and resolves `result` + drop(tx); + match result.await { + Ok(()) if remaining.is_empty() => Ok(()), + _ => Err(()), + } + } + .await + }; +} + +struct LoaderHarness; + +impl Guest for LoaderHarness { + #[doc = " Run the program."] + #[allow(async_fn_in_trait)] + async fn run() -> Result<(), ()> { + let args = environment::get_arguments(); + let path = args.get(1).expect("path argument required"); + + match path_loader::load(path.to_string()).await { + Ok(component) => { + write_to!(stdout, component) + } + Err(e) => { + let message = match e { + Error::Other(Some(message)) => message, + Error::Other(None) => "--unknown error--".to_string(), + }; + let _ = write_to!(stderr, format!("{}\n", message).into_bytes()); + Err(()) + } + } + } +} + +wit_bindgen::generate!({ + path: "../wit", + world: "test-loader-harness", + merge_structurally_equal_types: true, + generate_all +}); + +export!(LoaderHarness); diff --git a/components/test-loader/README.md b/components/test-loader/README.md new file mode 100644 index 0000000..35f19b2 --- /dev/null +++ b/components/test-loader/README.md @@ -0,0 +1,14 @@ +# `test-loader` + +Test harness for a path loader. + +```sh +make components/test-loader && \ + wasmtime run \ + -Wcomponent-model-map \ + -Shttp \ + --dir=. \ + ./lib/test-loader.wasm \ + oci://ghcr.io/componentized/config/empty:0.2.1 \ + | wasm-tools component wit +``` diff --git a/components/test-loader/test-loader.wac b/components/test-loader/test-loader.wac new file mode 100644 index 0000000..e7dfad3 --- /dev/null +++ b/components/test-loader/test-loader.wac @@ -0,0 +1,17 @@ +package componentized:component; + +let http-client = new local:dep-componentized-http-client { ... }; + +let oci-client = new local:dep-componentized-oci-client { + client: http-client.client, + ... +}; + +export new local:test-loader-harness { + path-loader: new local:path-loader { + "componentized:http/client@0.1.0-dev": http-client.client, + "componentized:oci/client@0.0.0-dev": oci-client.client, + ... + }.path-loader, + ... +}...; diff --git a/components/wit/worlds.wit b/components/wit/worlds.wit index 6b1cacb..81abea2 100644 --- a/components/wit/worlds.wit +++ b/components/wit/worlds.wit @@ -17,7 +17,39 @@ world http-loader { export componentized:component/path-loader@0.0.0-dev; } +world oci-loader { + import componentized:oci/client@0.0.0-dev; + export componentized:component/types@0.0.0-dev; + export componentized:component/path-loader@0.0.0-dev; +} + +world path-loader { + import filesystem-loader: interface { + use componentized:component/types@0.0.0-dev.{component, error}; + load: async func(path: string) -> result; + } + import http-loader: interface { + use componentized:component/types@0.0.0-dev.{component, error}; + load: async func(path: string) -> result; + } + import oci-loader: interface { + use componentized:component/types@0.0.0-dev.{component, error}; + load: async func(path: string) -> result; + } + + export componentized:component/types@0.0.0-dev; + export componentized:component/path-loader@0.0.0-dev; +} + world wac-loader { export componentized:component/types@0.0.0-dev; export componentized:component/wac-loader@0.0.0-dev; } + +world test-loader-harness { + import componentized:component/path-loader@0.0.0-dev; + import wasi:cli/environment@0.3.0; + import wasi:cli/stdout@0.3.0; + import wasi:cli/stderr@0.3.0; + export wasi:cli/run@0.3.0; +} diff --git a/components/wkg.lock b/components/wkg.lock index 3de086b..2f1fc11 100644 --- a/components/wkg.lock +++ b/components/wkg.lock @@ -11,6 +11,24 @@ requirement = "=0.1.0-dev" version = "0.1.0-dev" digest = "sha256:f74b53af4b5ecb4c77e8bd47887920e0626efb2e45e8a9386f2519910faf5e5c" +[[packages]] +name = "componentized:oci" +registry = "componentized.dev" + +[[packages.versions]] +requirement = "=0.0.0-dev" +version = "0.0.0-dev" +digest = "sha256:fe33e6ac5f37a1a5f37b249c382b28e5224d4f2b28a7b6f8499a8bb11762e400" + +[[packages]] +name = "wasi:cli" +registry = "wasi.dev" + +[[packages.versions]] +requirement = "=0.3.0" +version = "0.3.0" +digest = "sha256:3a2d58743b67a057f7210b9a73d3b21b34ec7895ffe34d1cb092927307156e40" + [[packages]] name = "wasi:filesystem" registry = "wasi.dev" diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 90bb313..9dbbb5c 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "nightly-2026-09-18" -targets = [ "wasm32-unknown-unknown", "wasm32-wasip3" ] +channel = "1.98" +targets = [ "wasm32-unknown-unknown" ]