diff --git a/Cargo.lock b/Cargo.lock index a5c7fd56..ba35ed1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5561,6 +5561,7 @@ version = "0.1.0" dependencies = [ "serde", "serde_json", + "tempfile", "thiserror 2.0.18", "toml", "wit-bindgen", diff --git a/crates/plugin-pdk/Cargo.toml b/crates/plugin-pdk/Cargo.toml index 025aa81e..99202a5b 100644 --- a/crates/plugin-pdk/Cargo.toml +++ b/crates/plugin-pdk/Cargo.toml @@ -13,4 +13,5 @@ thiserror.workspace = true wit-bindgen = "0.51.0" [dev-dependencies] +tempfile.workspace = true toml.workspace = true diff --git a/crates/plugin-pdk/src/lib.rs b/crates/plugin-pdk/src/lib.rs index 898f953d..69f9edd7 100644 --- a/crates/plugin-pdk/src/lib.rs +++ b/crates/plugin-pdk/src/lib.rs @@ -10,9 +10,13 @@ //! [`export_component_tool!`] macro: //! //! ```ignore -//! yoi_plugin_pdk::wit_bindgen::generate!({ +//! use yoi_plugin_pdk::wit_bindgen; +//! +//! wit_bindgen::generate!({ //! world: "tool", //! path: "../../../../resources/plugin/wit", +//! generate_all, +//! runtime_path: "yoi_plugin_pdk::wit_bindgen::rt", //! }); //! //! fn echo( @@ -42,7 +46,8 @@ pub const TOOL_WORLD: &str = "yoi:plugin/tool@1.0.0"; pub const TOOL_WIT: &str = include_str!("../../../resources/plugin/wit/yoi-plugin-tool-v1.wit"); /// Repository WIT for the grant-bound host APIs importable by Tool components. -pub const HOST_WIT: &str = include_str!("../../../resources/plugin/wit/yoi-host-v1.wit"); +pub const HOST_WIT: &str = + include_str!("../../../resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit"); /// Maximum serialized ToolOutput JSON accepted by Yoi's current Plugin runtime. pub const MAX_TOOL_OUTPUT_BYTES: usize = 64 * 1024; @@ -321,9 +326,11 @@ where /// Implement the generated Component Model `Guest` trait for a typed JSON /// handler and export it with the `wit-bindgen` generated `export!` macro. /// -/// The caller must invoke `yoi_plugin_pdk::wit_bindgen::generate!` for the -/// `tool` world first, which defines the `Guest` trait and `export!` macro in -/// the current module. The generated component still imports only WIT-declared +/// The caller must import the PDK's `wit_bindgen` re-export and invoke +/// `wit_bindgen::generate!` for the `tool` world first, with +/// `runtime_path: "yoi_plugin_pdk::wit_bindgen::rt"`. That defines the +/// `Guest` trait and `export!` macro in the current module. The generated +/// component still imports only WIT-declared /// host APIs; this macro does not grant filesystem, network, or environment /// authority. #[macro_export] @@ -464,5 +471,6 @@ mod tests { assert_eq!(TOOL_WORLD, "yoi:plugin/tool@1.0.0"); assert!(HOST_WIT.contains("interface https")); assert!(HOST_WIT.contains("interface fs")); + assert!(HOST_WIT.contains("%list: func")); } } diff --git a/crates/plugin-pdk/tests/template.rs b/crates/plugin-pdk/tests/template.rs index 009cea58..a5e2239d 100644 --- a/crates/plugin-pdk/tests/template.rs +++ b/crates/plugin-pdk/tests/template.rs @@ -1,3 +1,7 @@ +use std::fs; +use std::path::Path; +use std::process::Command; + use toml::Value; const TEMPLATE_CARGO: &str = @@ -15,6 +19,7 @@ const PDK_CARGO: &str = include_str!("../Cargo.toml"); fn rust_component_tool_template_has_expected_files() { let cargo: Value = toml::from_str(TEMPLATE_CARGO).expect("template Cargo.toml parses"); assert_eq!(cargo["package"]["edition"].as_str(), Some("2024")); + assert!(cargo["workspace"].is_table()); assert_eq!(cargo["lib"]["crate-type"][0].as_str(), Some("cdylib")); assert_eq!( cargo["dependencies"]["yoi-plugin-pdk"]["path"].as_str(), @@ -31,7 +36,10 @@ fn rust_component_tool_template_has_expected_files() { ); assert!(plugin["tools"].as_array().expect("tools array").len() == 1); - assert!(TEMPLATE_LIB.contains("yoi_plugin_pdk::wit_bindgen::generate!")); + assert!(TEMPLATE_LIB.contains("use yoi_plugin_pdk::wit_bindgen")); + assert!(TEMPLATE_LIB.contains("wit_bindgen::generate!")); + assert!(TEMPLATE_LIB.contains("generate_all")); + assert!(TEMPLATE_LIB.contains("runtime_path: \"yoi_plugin_pdk::wit_bindgen::rt\"")); assert!(TEMPLATE_LIB.contains("yoi_plugin_pdk::export_component_tool!")); assert!(TEMPLATE_LIB.contains("ToolOutput::json")); assert!(TEMPLATE_README.contains("Component Model Tool Plugin")); @@ -39,11 +47,47 @@ fn rust_component_tool_template_has_expected_files() { #[test] fn documented_sample_uses_pdk_component_path() { - assert!(SAMPLE_LIB.contains("yoi_plugin_pdk::wit_bindgen::generate!")); + assert!(SAMPLE_LIB.contains("use yoi_plugin_pdk::wit_bindgen")); + assert!(SAMPLE_LIB.contains("wit_bindgen::generate!")); + assert!(SAMPLE_LIB.contains("generate_all")); + assert!(SAMPLE_LIB.contains("runtime_path: \"yoi_plugin_pdk::wit_bindgen::rt\"")); assert!(SAMPLE_LIB.contains("yoi_plugin_pdk::export_component_tool!")); assert!(!SAMPLE_LIB.contains("export_name")); } +#[test] +fn embedded_template_cargo_checks_for_wasm_target() { + let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let template_dir = crate_dir.join("../../resources/plugin/templates/rust-component-tool"); + let manifest_path = template_dir.join("Cargo.toml"); + let lock_path = template_dir.join("Cargo.lock"); + let _ = fs::remove_file(&lock_path); + + let target_dir = tempfile::tempdir().expect("temporary cargo target dir"); + let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); + let output = Command::new(cargo) + .arg("check") + .arg("--manifest-path") + .arg(&manifest_path) + .arg("--target") + .arg("wasm32-unknown-unknown") + .arg("--offline") + .arg("--target-dir") + .arg(target_dir.path()) + .env("CARGO_TERM_COLOR", "never") + .output() + .expect("spawn cargo check for embedded template"); + + let _ = fs::remove_file(&lock_path); + assert!( + output.status.success(), + "template cargo check failed\nstatus: {}\nstdout:\n{}\nstderr:\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + #[test] fn pdk_runtime_dependencies_are_guest_side_only() { let cargo: Value = toml::from_str(PDK_CARGO).expect("PDK Cargo.toml parses"); diff --git a/crates/plugin-pdk/tests/wit_bindgen_probe.rs b/crates/plugin-pdk/tests/wit_bindgen_probe.rs new file mode 100644 index 00000000..041e3a29 --- /dev/null +++ b/crates/plugin-pdk/tests/wit_bindgen_probe.rs @@ -0,0 +1,25 @@ +use yoi_plugin_pdk::wit_bindgen; + +wit_bindgen::generate!({ + world: "tool", + path: "../../resources/plugin/wit", + generate_all, + runtime_path: "yoi_plugin_pdk::wit_bindgen::rt", +}); + +struct Probe; + +impl Guest for Probe { + fn call(tool_name: String, input_json: String) -> String { + yoi_plugin_pdk::run_json_tool(&tool_name, &input_json, |_ctx, input: serde_json::Value| { + yoi_plugin_pdk::ToolOutput::json("probe ok", input) + }) + } +} + +#[test] +fn wit_bindgen_generates_current_tool_world() { + let output = ::call("probe".to_string(), r#"{"ok":true}"#.to_string()); + let value: serde_json::Value = serde_json::from_str(&output).unwrap(); + assert_eq!(value["summary"], "probe ok"); +} diff --git a/docs/development/plugin-development.md b/docs/development/plugin-development.md index fc46081f..25edef62 100644 --- a/docs/development/plugin-development.md +++ b/docs/development/plugin-development.md @@ -119,11 +119,14 @@ The important authoring shape is: ```rust use serde::{Deserialize, Serialize}; +use yoi_plugin_pdk::wit_bindgen; use yoi_plugin_pdk::{ToolContext, ToolError, ToolOutput}; -yoi_plugin_pdk::wit_bindgen::generate!({ +wit_bindgen::generate!({ world: "tool", path: "../../../resources/plugin/wit", + generate_all, + runtime_path: "yoi_plugin_pdk::wit_bindgen::rt", }); #[derive(Deserialize)] diff --git a/docs/examples/plugin-component-tool/lib.rs b/docs/examples/plugin-component-tool/lib.rs index 94afd294..4311cc2f 100644 --- a/docs/examples/plugin-component-tool/lib.rs +++ b/docs/examples/plugin-component-tool/lib.rs @@ -4,11 +4,14 @@ //! and package the adapted component as `plugin.component.wasm`. use serde::{Deserialize, Serialize}; +use yoi_plugin_pdk::wit_bindgen; use yoi_plugin_pdk::{ToolContext, ToolError, ToolOutput}; -yoi_plugin_pdk::wit_bindgen::generate!({ +wit_bindgen::generate!({ world: "tool", path: "../../../resources/plugin/wit", + generate_all, + runtime_path: "yoi_plugin_pdk::wit_bindgen::rt", }); #[derive(Debug, Deserialize)] diff --git a/package.nix b/package.nix index f1238c7a..fdf427fe 100644 --- a/package.nix +++ b/package.nix @@ -40,7 +40,7 @@ rustPlatform.buildRustPackage rec { filter = sourceFilter; }; - cargoHash = "sha256-gMDU496wWn3LYhlXwxczHW/tT3IJxclwJtIY6ZjomtQ="; + cargoHash = "sha256-ci9h0U83YQQBeT3xlsGuKULnl1Aphgpg3pR4n0se16I="; depsExtraArgs = { # Older fetchCargoVendor utilities used crates.io's API download endpoint, diff --git a/resources/plugin/templates/rust-component-tool/Cargo.toml b/resources/plugin/templates/rust-component-tool/Cargo.toml index e852c364..a2013985 100644 --- a/resources/plugin/templates/rust-component-tool/Cargo.toml +++ b/resources/plugin/templates/rust-component-tool/Cargo.toml @@ -5,6 +5,10 @@ edition = "2024" license = "MIT" publish = false +# Keep the embedded template checkable in-place without making it a member of +# Yoi's root workspace. A copied starter remains a normal standalone package. +[workspace] + [lib] crate-type = ["cdylib"] diff --git a/resources/plugin/templates/rust-component-tool/README.md b/resources/plugin/templates/rust-component-tool/README.md index aa07b2bf..9208c75f 100644 --- a/resources/plugin/templates/rust-component-tool/README.md +++ b/resources/plugin/templates/rust-component-tool/README.md @@ -14,7 +14,7 @@ The PDK is guest-side only. It does not grant filesystem, network, or environmen ## Checkout/development dependency -Inside the Yoi checkout this template uses a local path dependency: +Inside the Yoi checkout this template uses a local path dependency and declares an empty `[workspace]` so it can be checked in place without becoming a member of Yoi's root workspace: ```toml yoi-plugin-pdk = { path = "../../../../crates/plugin-pdk" } diff --git a/resources/plugin/templates/rust-component-tool/src/lib.rs b/resources/plugin/templates/rust-component-tool/src/lib.rs index ab8301c7..c954aedc 100644 --- a/resources/plugin/templates/rust-component-tool/src/lib.rs +++ b/resources/plugin/templates/rust-component-tool/src/lib.rs @@ -1,9 +1,12 @@ use serde::{Deserialize, Serialize}; +use yoi_plugin_pdk::wit_bindgen; use yoi_plugin_pdk::{ToolContext, ToolError, ToolOutput}; -yoi_plugin_pdk::wit_bindgen::generate!({ +wit_bindgen::generate!({ world: "tool", path: "../../../../resources/plugin/wit", + generate_all, + runtime_path: "yoi_plugin_pdk::wit_bindgen::rt", }); #[derive(Debug, Deserialize)] diff --git a/resources/plugin/wit/yoi-host-v1.wit b/resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit similarity index 90% rename from resources/plugin/wit/yoi-host-v1.wit rename to resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit index ae66da92..e0544a0a 100644 --- a/resources/plugin/wit/yoi-host-v1.wit +++ b/resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit @@ -10,6 +10,6 @@ interface https { /// Grant-bound filesystem host API. No ambient WASI filesystem is exposed. interface fs { read: func(request-json: string) -> string; - list: func(request-json: string) -> string; + %list: func(request-json: string) -> string; write: func(request-json: string) -> string; }