diff --git a/Cargo.lock b/Cargo.lock index e0a066de..11072a78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4416,11 +4416,14 @@ dependencies = [ "wadm", "walkdir", "wascap", + "wasm-encoder 0.33.2", "wasmbus-rpc", "wasmcloud-component-adapters", "wasmcloud-control-interface 0.27.0", + "wat", "weld-codegen", "wit-component", + "wit-parser", ] [[package]] @@ -4512,9 +4515,9 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.31.1" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41763f20eafed1399fff1afb466496d3a959f58241436cfdc17e3f5ca954de16" +checksum = "34180c89672b3e4825c3a8db4b61a674f1447afd5fe2445b2d22c3d8b6ea086c" dependencies = [ "leb128", ] @@ -4531,17 +4534,17 @@ dependencies = [ [[package]] name = "wasm-metadata" -version = "0.10.2" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ac8d3bcbbb5081489f35966b86d127596e9cdacfb3824b79f43344662226178" +checksum = "577508d8a45bc54ad97efe77c95ba57bb10e7e5c5bac9c31295ce88b8045cd7d" dependencies = [ "anyhow", "indexmap 2.0.0", "serde", "serde_json", "spdx", - "wasm-encoder 0.31.1", - "wasmparser 0.111.0", + "wasm-encoder 0.33.2", + "wasmparser 0.113.2", ] [[package]] @@ -4726,14 +4729,35 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.111.0" +version = "0.113.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad71036aada3f6b09251546e97e4f4f176dd6b41cf6fa55e7e0f65e86aec319a" +checksum = "1fd0d44fab0bd78404e352f3399324eef76516a4580b52bc9031c60f064e98f3" dependencies = [ "indexmap 2.0.0", "semver", ] +[[package]] +name = "wast" +version = "65.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55a88724cf8c2c0ebbf32c8e8f4ac0d6aa7ba6d73a1cfd94b254aa8f894317e" +dependencies = [ + "leb128", + "memchr", + "unicode-width", + "wasm-encoder 0.33.2", +] + +[[package]] +name = "wat" +version = "1.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83e1a8d86d008adc7bafa5cf4332d448699a08fcf2a715a71fbb75e2c5ca188" +dependencies = [ + "wast", +] + [[package]] name = "web-sys" version = "0.3.64" @@ -4996,25 +5020,27 @@ dependencies = [ [[package]] name = "wit-component" -version = "0.13.2" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9eb6179c5a26adc38fa5a22e263e7a3812c6777ca2e75d1717fd3789f82b64" +checksum = "ee23614740bf871dac9856e3062c7a308506eb3f0a2759939ab8d0aa8436a1c0" dependencies = [ "anyhow", "bitflags 2.3.3", "indexmap 2.0.0", "log", - "wasm-encoder 0.31.1", + "serde", + "serde_json", + "wasm-encoder 0.33.2", "wasm-metadata", - "wasmparser 0.111.0", + "wasmparser 0.113.2", "wit-parser", ] [[package]] name = "wit-parser" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d6926af931f285e206ea71f9b67681f00a65d79097f81da7f9f285de006ba2" +checksum = "a39edca9abb16309def3843af73b58d47d243fe33a9ceee572446bcc57556b9a" dependencies = [ "anyhow", "id-arena", @@ -5022,6 +5048,8 @@ dependencies = [ "log", "pulldown-cmark", "semver", + "serde", + "serde_json", "unicode-xid", "url", ] diff --git a/Cargo.toml b/Cargo.toml index 629f52cf..a4b934aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,10 +149,13 @@ wadm = "0.5.0" walkdir = "2.3" wascap = "0.11.0" wash-lib = { version = "0.9", path = "./crates/wash-lib" } +wasm-encoder = "0.33.2" wasmbus-rpc = "0.14.0" wasmcloud-component-adapters = { version = "0.2.0" } wasmcloud-control-interface = "0.27" wasmcloud-test-util = "0.10.0" weld-codegen = "0.7.0" which = "4.4.0" -wit-component = "0.13.2" +wit-component = "0.14.4" +wit-parser = "0.11.3" +wat = "1.0.74" \ No newline at end of file diff --git a/crates/wash-lib/Cargo.toml b/crates/wash-lib/Cargo.toml index d858a80e..9703add6 100644 --- a/crates/wash-lib/Cargo.toml +++ b/crates/wash-lib/Cargo.toml @@ -49,9 +49,9 @@ reqwest = { workspace = true, features = ["json", "rustls-tls", "stream"] } rmp-serde = "1" semver = { workspace = true, features = ["serde"], optional = true } serde = { workspace = true, features = ["derive"], optional = true } +serde-transcode = "1" serde_cbor = "0.11" serde_json = { workspace = true, optional = true } -serde-transcode = "1" serde_with = { workspace = true } tempfile = { workspace = true } term-table = { workspace = true, optional = true } @@ -65,11 +65,14 @@ toml = { workspace = true } wadm = { workspace = true, optional = true} walkdir = { workspace = true } wascap = { workspace = true } +wasm-encoder = { workspace = true } +wasmbus-rpc = { workspace = true } +wasmcloud-component-adapters = { workspace = true } wasmcloud-control-interface = { workspace = true } +wat = { workspace = true } weld-codegen = { workspace = true } -wasmbus-rpc = { workspace = true } wit-component = { workspace = true } -wasmcloud-component-adapters = { workspace = true } +wit-parser = { workspace = true } [dev-dependencies] claims = { workspace = true } diff --git a/crates/wash-lib/src/build.rs b/crates/wash-lib/src/build.rs index 87b4f3e4..db3e46d1 100644 --- a/crates/wash-lib/src/build.rs +++ b/crates/wash-lib/src/build.rs @@ -1,6 +1,7 @@ //! Build (and sign) a wasmCloud actor, provider, or interface. Depends on the "cli" feature use std::{ + borrow::Cow, fs, io::ErrorKind, path::{Path, PathBuf}, @@ -9,7 +10,8 @@ use std::{ }; use anyhow::{anyhow, bail, Context, Result}; -use wit_component::ComponentEncoder; +use wasm_encoder::{Encode, Section}; +use wit_component::{ComponentEncoder, StringEncoding}; use crate::{ cli::{ @@ -95,14 +97,21 @@ pub fn build_actor( } }?; - if actor_config.wasm_target == WasmTarget::TinyGoWasiPreview1 { - // TODO: run equivalent of `wasm-tools component embed --world [world] ./wit [actor_wasm_path] -o [actor_wasm_embed_path] - } + // Perform embedding, if necessary + if let WasmTarget::WasiPreview1 | WasmTarget::WasiPreview2 = &actor_config.wasm_target { + embed_wasm_component_metadata( + &common_config.path, + actor_config + .wit_world + .as_ref() + .context("`wit_world` must be specified in wasmcloud.toml for creating preview1 or preview2 components")?, + &actor_wasm_path, + &actor_wasm_path, + )?; + }; - // If the actor has been configured as WASI Preview2, adapt it - if actor_config.wasm_target == WasmTarget::WasiPreview2 - || actor_config.wasm_target == WasmTarget::TinyGoWasiPreview1 - { + // If the actor has been configured as WASI Preview2, adapt it from preview1 + if actor_config.wasm_target == WasmTarget::WasiPreview2 { let adapter_wasm_bytes = get_wasi_preview2_adapter_bytes(actor_config)?; // Adapt the component, using the adapter that is available locally let wasm_bytes = adapt_wasi_preview1_component(&actor_wasm_path, adapter_wasm_bytes) @@ -170,7 +179,7 @@ fn build_rust_actor( let metadata = cargo_metadata::MetadataCommand::new().exec()?; let target_path = metadata.target_directory.as_path(); - let build_target = actor_config.build_target(&actor_config.wasm_target); + let build_target = rust_config.build_target(&actor_config.wasm_target); let result = command .args(["build", "--release", "--target", build_target]) @@ -249,7 +258,7 @@ fn build_tinygo_actor( "-o", filename.as_str(), "-target", - actor_config.build_target(&actor_config.wasm_target), + tinygo_config.build_target(&actor_config.wasm_target), "-scheduler", "none", "-no-debug", @@ -319,7 +328,7 @@ fn adapt_wasi_preview1_component( /// if required by project configuration pub(crate) fn get_wasi_preview2_adapter_bytes(config: &ActorConfig) -> Result> { if let ActorConfig { - wasm_target: WasmTarget::WasiPreview2 | WasmTarget::TinyGoWasiPreview1, + wasm_target: WasmTarget::WasiPreview2, wasi_preview2_adapter_path: Some(path), .. } = config @@ -330,6 +339,72 @@ pub(crate) fn get_wasi_preview2_adapter_bytes(config: &ActorConfig) -> Result, + wit_world: impl AsRef, + input_wasm: impl AsRef, + output_wasm: impl AsRef, +) -> Result<()> { + // Find the the WIT directory for the project + let wit_dir = project_path.as_ref().join("wit"); + if !wit_dir.is_dir() { + bail!( + "expected 'wit' directory under project path at [{}] is missing", + wit_dir.display() + ); + }; + + // Resolve the WIT directory packages & worlds + let mut resolve = wit_parser::Resolve::default(); + let (package_id, _paths) = resolve + .push_dir(&wit_dir) + .context("failed to add WIT deps directory")?; + log::info!("successfully loaded WIT @ [{}]", wit_dir.display()); + + // Select the target world that was specified by the user + let world = resolve + .select_world(package_id, wit_world.as_ref().into()) + .context("failed to select world from built resolver")?; + + // Encode the metadata + let encoded_metadata = + wit_component::metadata::encode(&resolve, world, StringEncoding::UTF8, None) + .context("failed to encode WIT metadata for component")?; + + // Load the wasm binary + let mut wasm_bytes = wat::parse_file(input_wasm.as_ref()).with_context(|| { + format!( + "failed to read wasm bytes from [{}]", + input_wasm.as_ref().display() + ) + })?; + + // Build & encode a new custom section at the end of the wasm + let section = wasm_encoder::CustomSection { + name: "component-type".into(), + data: Cow::Borrowed(&encoded_metadata), + }; + wasm_bytes.push(section.id()); + section.encode(&mut wasm_bytes); + log::info!("successfully embedded component metadata in WASM"); + + // Output the WASM to disk (possibly overwriting the original path) + std::fs::write(output_wasm.as_ref(), wasm_bytes).with_context(|| { + format!( + "failed to write updated wasm to disk at [{}]", + output_wasm.as_ref().display() + ) + })?; + + log::info!( + "successfully wrote component w/ metadata to [{}]", + output_wasm.as_ref().display() + ); + + Ok(()) +} + /// Placeholder for future functionality for building providers #[allow(unused)] fn build_provider( diff --git a/crates/wash-lib/src/parser/mod.rs b/crates/wash-lib/src/parser/mod.rs index 7b195b23..961ec85d 100644 --- a/crates/wash-lib/src/parser/mod.rs +++ b/crates/wash-lib/src/parser/mod.rs @@ -48,23 +48,23 @@ pub struct ActorConfig { pub key_directory: PathBuf, /// The filename of the signed wasm actor. pub filename: Option, - /// The target wasm target to build for. Defaults to "wasm32-unknown-unknown". + /// The call alias of the actor. + pub call_alias: Option, + /// The target wasm target to build for. Defaults to "wasm32-unknown-unknown" (a WASM core module). pub wasm_target: WasmTarget, /// Path to a wasm adapter that can be used for preview2 pub wasi_preview2_adapter_path: Option, - /// The call alias of the actor. - pub call_alias: Option, + /// The WIT world that is implemented by the component + pub wit_world: Option, } -impl ActorConfig { +impl RustConfig { pub fn build_target(&self, wasm_target: &WasmTarget) -> &'static str { match wasm_target { WasmTarget::CoreModule => "wasm32-unknown-unknown", // NOTE: eventually "wasm32-wasi" will be renamed to "wasm32-wasi-preview1" // https://github.com/rust-lang/compiler-team/issues/607 WasmTarget::WasiPreview1 | WasmTarget::WasiPreview2 => "wasm32-wasi", - WasmTarget::TinyGoCoreModule => "wasm", - WasmTarget::TinyGoWasiPreview1 => "wasi", } } } @@ -87,6 +87,8 @@ struct RawActorConfig { pub wasi_preview2_adapter_path: Option, /// The call alias of the actor. Defaults to no alias. pub call_alias: Option, + /// The WIT world that is implemented by the componet + pub wit_world: Option, } impl TryFrom for ActorConfig { @@ -107,6 +109,7 @@ impl TryFrom for ActorConfig { .unwrap_or_default(), wasi_preview2_adapter_path: raw_config.wasi_preview2_adapter_path, call_alias: raw_config.call_alias, + wit_world: raw_config.wit_world, }) } } @@ -217,10 +220,6 @@ pub enum WasmTarget { WasiPreview1, #[serde(alias = "wasm32-wasi-preview2")] WasiPreview2, - #[serde(alias = "wasm")] // Shame, shame, TinyGo. Shame. - TinyGoCoreModule, - #[serde(alias = "wasi", alias = "wasip1")] // Shame, shame, TinyGo. Shame. - TinyGoWasiPreview1, } impl From<&str> for WasmTarget { @@ -229,9 +228,6 @@ impl From<&str> for WasmTarget { "wasm32-wasi-preview1" => WasmTarget::WasiPreview1, "wasm32-wasi" => WasmTarget::WasiPreview1, "wasm32-wasi-preview2" => WasmTarget::WasiPreview2, - "wasi" => WasmTarget::TinyGoWasiPreview1, - "wasip1" => WasmTarget::TinyGoWasiPreview1, - "wasm" => WasmTarget::TinyGoCoreModule, _ => WasmTarget::CoreModule, } } @@ -249,8 +245,6 @@ impl Display for WasmTarget { WasmTarget::CoreModule => "wasm32-unknown-unknown", WasmTarget::WasiPreview1 => "wasm32-wasi", WasmTarget::WasiPreview2 => "wasm32-wasi-preview2", - WasmTarget::TinyGoCoreModule => "wasm", - WasmTarget::TinyGoWasiPreview1 => "wasi", }) } } @@ -259,14 +253,18 @@ impl Display for WasmTarget { struct RawProjectConfig { /// The language of the project, e.g. rust, tinygo. This is used to determine which config to parse. pub language: String, + /// The type of project. This is a string that is used to determine which type of config to parse. /// The toml file name is just "type" but is named project_type here to avoid clashing with the type keyword in Rust. #[serde(rename = "type")] pub project_type: String, + /// Name of the project. pub name: Option, + /// Semantic version of the project. pub version: Option, + pub actor: Option, pub provider: Option, pub rust: Option, @@ -280,6 +278,15 @@ pub struct TinyGoConfig { pub tinygo_path: Option, } +impl TinyGoConfig { + pub fn build_target(&self, wasm_target: &WasmTarget) -> &'static str { + match wasm_target { + WasmTarget::CoreModule => "wasm", + WasmTarget::WasiPreview1 | WasmTarget::WasiPreview2 => "wasi", + } + } +} + #[derive(Deserialize, Debug, PartialEq, Default)] struct RawTinyGoConfig { /// The path to the tinygo binary. Optional, will default to `tinygo` if not specified. @@ -289,9 +296,9 @@ struct RawTinyGoConfig { impl TryFrom for TinyGoConfig { type Error = anyhow::Error; - fn try_from(raw_config: RawTinyGoConfig) -> Result { + fn try_from(raw: RawTinyGoConfig) -> Result { Ok(Self { - tinygo_path: raw_config.tinygo_path, + tinygo_path: raw.tinygo_path, }) } } diff --git a/crates/wash-lib/tests/parser/files/tinygo_actor_component.toml b/crates/wash-lib/tests/parser/files/tinygo_actor_component.toml index 76456633..eb0a56bc 100644 --- a/crates/wash-lib/tests/parser/files/tinygo_actor_component.toml +++ b/crates/wash-lib/tests/parser/files/tinygo_actor_component.toml @@ -9,7 +9,7 @@ registry = "localhost:8080" push_insecure = false key_directory = "./keys" filename = "testactor.wasm" -wasm_target = "wasi" +wasm_target = "wasm32-wasi-preview2" call_alias = "testactor" [tinygo] diff --git a/crates/wash-lib/tests/parser/main.rs b/crates/wash-lib/tests/parser/main.rs index 39b74fec..52df9806 100644 --- a/crates/wash-lib/tests/parser/main.rs +++ b/crates/wash-lib/tests/parser/main.rs @@ -33,9 +33,10 @@ fn rust_actor() { push_insecure: false, key_directory: PathBuf::from("./keys"), filename: Some("testactor.wasm".to_string()), - wasm_target: WasmTarget::CoreModule, + call_alias: Some("testactor".to_string()), wasi_preview2_adapter_path: None, - call_alias: Some("testactor".to_string()) + wasm_target: WasmTarget::CoreModule, + wit_world: None, }) ); @@ -78,9 +79,10 @@ fn tinygo_actor_module() { push_insecure: false, key_directory: PathBuf::from("./keys"), filename: Some("testactor.wasm".to_string()), - wasm_target: WasmTarget::TinyGoCoreModule, + call_alias: Some("testactor".to_string()), wasi_preview2_adapter_path: None, - call_alias: Some("testactor".to_string()) + wasm_target: WasmTarget::CoreModule, + wit_world: None, }) ); @@ -98,7 +100,7 @@ fn tinygo_actor_module() { } #[test] -fn tinygo_actor() { +fn tinygo_actor_component() { let result = get_config( Some(PathBuf::from( "./tests/parser/files/tinygo_actor_component.toml", @@ -116,9 +118,10 @@ fn tinygo_actor() { push_insecure: false, key_directory: PathBuf::from("./keys"), filename: Some("testactor.wasm".to_string()), - wasm_target: WasmTarget::TinyGoWasiPreview1, + call_alias: Some("testactor".to_string()), wasi_preview2_adapter_path: None, - call_alias: Some("testactor".to_string()) + wasm_target: WasmTarget::WasiPreview2, + wit_world: None, }) ); } @@ -287,9 +290,10 @@ fn minimal_rust_actor() { push_insecure: false, key_directory: PathBuf::from("./keys"), filename: None, - wasm_target: WasmTarget::CoreModule, + call_alias: None, wasi_preview2_adapter_path: None, - call_alias: None + wasm_target: WasmTarget::CoreModule, + wit_world: None, }) ); @@ -333,9 +337,10 @@ fn cargo_toml_actor() { push_insecure: false, key_directory: PathBuf::from("./keys"), filename: None, - wasm_target: WasmTarget::CoreModule, + call_alias: None, wasi_preview2_adapter_path: None, - call_alias: None + wasm_target: WasmTarget::CoreModule, + wit_world: None, }) );