From fa6fddc32ef1f46b2f204b2b383dd2a85113cdc5 Mon Sep 17 00:00:00 2001 From: Ryan Levick Date: Tue, 9 Jul 2024 20:54:18 +0200 Subject: [PATCH 1/3] Add first version of targets command Signed-off-by: Ryan Levick --- Cargo.lock | 2 + Cargo.toml | 2 + README.md | 12 +++ src/bin/wac.rs | 6 +- src/commands.rs | 2 + src/commands/targets.rs | 197 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 src/commands/targets.rs diff --git a/Cargo.lock b/Cargo.lock index c7a9b44..459454a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3850,6 +3850,8 @@ dependencies = [ "warg-protocol", "wasmprinter 0.202.0", "wat", + "wit-component", + "wit-parser", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bc1a23a..12c4a2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,8 @@ indexmap = { workspace = true } miette = { workspace = true, features = ["fancy"] } semver = { workspace = true } indicatif = { workspace = true, optional = true } +wit-parser = { workspace = true } +wit-component = { workspace = true } [features] default = ["wit", "registry"] diff --git a/README.md b/README.md index c5386ed..da1c22a 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ The `wac` CLI tool has the following commands: * `wac plug` - Plugs the imports of a component with one or more other components. * `wac compose` - Compose WebAssembly components using the provided WAC source file. +* `wac targets` - Determines whether a given component conforms to the supplied wit world. * `wac parse` - Parses a composition into a JSON representation of the AST. * `wac resolve` - Resolves a composition into a JSON representation. @@ -117,6 +118,17 @@ Or mixing in packages published to a Warg registry: wac plug my-namespace:package-name --plug some-namespace:other-package-name -o plugged.wasm ``` +### Checking Whether a Component Implements a World + +To see whether a given component implements a given world, use the `wac targets` command: + +``` +wac targets my-component.wasm my-wit.wit +``` + +If `my-component.wasm` implements the world defined in `my-wit.wit` then the command will succeed. Otherwise, an error will be returned. + +If `my-wit.wit` has multiple world definitions, you can disambiguate using the `--world` flag. ### Encoding Compositions diff --git a/src/bin/wac.rs b/src/bin/wac.rs index c121795..7a9d167 100644 --- a/src/bin/wac.rs +++ b/src/bin/wac.rs @@ -1,7 +1,9 @@ use anyhow::Result; use clap::Parser; use owo_colors::{OwoColorize, Stream, Style}; -use wac_cli::commands::{ComposeCommand, ParseCommand, PlugCommand, ResolveCommand}; +use wac_cli::commands::{ + ComposeCommand, ParseCommand, PlugCommand, ResolveCommand, TargetsCommand, +}; fn version() -> &'static str { option_env!("CARGO_VERSION_INFO").unwrap_or(env!("CARGO_PKG_VERSION")) @@ -21,6 +23,7 @@ enum Wac { Compose(ComposeCommand), Parse(ParseCommand), Resolve(ResolveCommand), + Targets(TargetsCommand), } #[tokio::main] @@ -32,6 +35,7 @@ async fn main() -> Result<()> { Wac::Resolve(cmd) => cmd.exec().await, Wac::Compose(cmd) => cmd.exec().await, Wac::Plug(cmd) => cmd.exec().await, + Wac::Targets(cmd) => cmd.exec().await, } { eprintln!( "{error}: {e:?}", diff --git a/src/commands.rs b/src/commands.rs index dc579e0..e9b9ffa 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -4,8 +4,10 @@ mod compose; mod parse; mod plug; mod resolve; +mod targets; pub use self::compose::*; pub use self::parse::*; pub use self::plug::*; pub use self::resolve::*; +pub use self::targets::*; diff --git a/src/commands/targets.rs b/src/commands/targets.rs new file mode 100644 index 0000000..7ea8aec --- /dev/null +++ b/src/commands/targets.rs @@ -0,0 +1,197 @@ +use anyhow::{bail, Context, Result}; +use clap::Args; +use std::{ + fs, + path::{Path, PathBuf}, +}; +use wac_types::{ExternKind, ItemKind, Package, SubtypeChecker, Types, WorldId}; + +/// Parses a WAC source file into a JSON AST representation. +#[derive(Args)] +#[clap(disable_version_flag = true)] +pub struct TargetsCommand { + /// The path to the component + #[clap(value_name = "COMPONENT_PATH")] + pub component: PathBuf, + /// The path to the WIT definition + #[clap(value_name = "WIT_PATH")] + pub wit: PathBuf, + /// The name of the world to target + /// + /// If the wit package only has one world definition, this does not need to be specified. + #[clap(long)] + pub world: Option, +} + +impl TargetsCommand { + /// Executes the command. + pub async fn exec(self) -> Result<()> { + log::debug!("executing targets command"); + let mut types = Types::default(); + + let wit_bytes = encode_wit_as_component(&self.wit)?; + let wit = Package::from_bytes("wit", None, wit_bytes, &mut types)?; + + let component_bytes = fs::read(&self.component).with_context(|| { + format!( + "failed to read file `{path}`", + path = self.component.display() + ) + })?; + let component = Package::from_bytes("component", None, component_bytes, &mut types)?; + + let wit = get_wit_world(&types, wit.ty(), self.world.as_deref())?; + + validate_target(&types, wit, component.ty())?; + + Ok(()) + } +} + +/// Gets the selected world from the component encoded WIT package +fn get_wit_world( + types: &Types, + top_level_world: WorldId, + world_name: Option<&str>, +) -> anyhow::Result { + let top_level_world = &types[top_level_world]; + let world = match world_name { + Some(world_name) => top_level_world + .exports + .get(world_name) + .with_context(|| format!("wit package did not contain a world name '{world_name}'"))?, + None if top_level_world.exports.len() == 1 => { + top_level_world.exports.values().next().unwrap() + } + None if top_level_world.exports.len() > 1 => { + bail!("wit package has multiple worlds, please specify one with the --world flag") + } + None => { + bail!("wit package did not contain a world") + } + }; + let ItemKind::Type(wac_types::Type::World(world_id)) = world else { + // We expect the top-level world to export a world type + bail!("wit package was not encoded properly") + }; + let wit_world = &types[*world_id]; + let world = wit_world.exports.values().next(); + let Some(ItemKind::Component(w)) = world else { + // We expect the nested world type to export a component + bail!("wit package was not encoded properly") + }; + Ok(*w) +} + +/// Encodes the wit package found at `path` into a component +fn encode_wit_as_component(path: &Path) -> anyhow::Result> { + let mut resolve = wit_parser::Resolve::new(); + let pkg = if path.is_dir() { + log::debug!( + "loading WIT package from directory `{path}`", + path = path.display() + ); + + let (pkg, _) = resolve.push_dir(&path)?; + pkg + } else if path.extension().and_then(std::ffi::OsStr::to_str) == Some("wit") { + let unresolved = wit_parser::UnresolvedPackage::parse_file(&path)?; + let pkg = resolve.push(unresolved)?; + pkg + } else { + bail!("expected either a wit directory or wit file") + }; + let encoded = wit_component::encode(Some(true), &resolve, pkg).with_context(|| { + format!( + "failed to encode WIT package from `{path}`", + path = path.display() + ) + })?; + Ok(encoded) +} + +/// An error in target validation +#[derive(thiserror::Error, miette::Diagnostic, Debug)] +#[diagnostic(code("component does not match wit world"))] +pub enum Error { + #[error("the target wit does not have an import named `{import}` but the component does")] + /// The import is not in the target world + ImportNotInTarget { + /// The name of the missing target + import: String, + }, + #[error("{kind} `{name}` has a mismatched type for targeted wit world")] + /// An import or export has a mismatched type for the target world. + TargetMismatch { + /// The name of the mismatched item + name: String, + /// The extern kind of the item + kind: ExternKind, + /// The source of the error + #[source] + source: anyhow::Error, + }, + #[error("the targeted wit world requires an export named `{name}` but the component did not export one")] + /// Missing an export for the target world. + MissingTargetExport { + /// The export name. + name: String, + /// The expected item kind. + kind: ItemKind, + }, +} + +/// Validate whether the component conforms to the given world +pub fn validate_target<'a>( + types: &'a Types, + wit_world_id: WorldId, + component_world_id: WorldId, +) -> Result<(), Error> { + let component_world = &types[component_world_id]; + let wit_world = &types[wit_world_id]; + // The interfaces imported implicitly through uses. + let implicit_imported_interfaces = wit_world.implicit_imported_interfaces(types); + let mut cache = Default::default(); + let mut checker = SubtypeChecker::new(&mut cache); + + // The output is allowed to import a subset of the world's imports + checker.invert(); + for (import, item_kind) in component_world.imports.iter() { + let expected = implicit_imported_interfaces + .get(import.as_str()) + .or_else(|| wit_world.imports.get(import)) + .ok_or_else(|| Error::ImportNotInTarget { + import: import.to_owned(), + })?; + + checker + .is_subtype(expected.promote(), types, *item_kind, types) + .map_err(|e| Error::TargetMismatch { + kind: ExternKind::Import, + name: import.to_owned(), + source: e, + })?; + } + + checker.revert(); + + // The output must export every export in the world + for (name, expected) in &wit_world.exports { + let export = component_world.exports.get(name).copied().ok_or_else(|| { + Error::MissingTargetExport { + name: name.clone(), + kind: *expected, + } + })?; + + checker + .is_subtype(export, types, expected.promote(), types) + .map_err(|e| Error::TargetMismatch { + kind: ExternKind::Export, + name: name.clone(), + source: e, + })?; + } + + Ok(()) +} From bd2a8dc9fc482bf9d9f51bd64332f067ca640bd8 Mon Sep 17 00:00:00 2001 From: Ryan Levick Date: Thu, 1 Aug 2024 14:02:57 +0200 Subject: [PATCH 2/3] PR feedback Signed-off-by: Ryan Levick --- src/commands/targets.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/commands/targets.rs b/src/commands/targets.rs index 7ea8aec..8fac0f8 100644 --- a/src/commands/targets.rs +++ b/src/commands/targets.rs @@ -6,7 +6,7 @@ use std::{ }; use wac_types::{ExternKind, ItemKind, Package, SubtypeChecker, Types, WorldId}; -/// Parses a WAC source file into a JSON AST representation. +/// Verifies that a given WebAssembly component targets a world. #[derive(Args)] #[clap(disable_version_flag = true)] pub struct TargetsCommand { @@ -59,7 +59,7 @@ fn get_wit_world( Some(world_name) => top_level_world .exports .get(world_name) - .with_context(|| format!("wit package did not contain a world name '{world_name}'"))?, + .with_context(|| format!("wit package did not contain a world named '{world_name}'"))?, None if top_level_world.exports.len() == 1 => { top_level_world.exports.values().next().unwrap() } @@ -92,14 +92,11 @@ fn encode_wit_as_component(path: &Path) -> anyhow::Result> { path = path.display() ); - let (pkg, _) = resolve.push_dir(&path)?; - pkg - } else if path.extension().and_then(std::ffi::OsStr::to_str) == Some("wit") { - let unresolved = wit_parser::UnresolvedPackage::parse_file(&path)?; - let pkg = resolve.push(unresolved)?; + let (pkg, _) = resolve.push_dir(path)?; pkg } else { - bail!("expected either a wit directory or wit file") + let unresolved = wit_parser::UnresolvedPackage::parse_file(path)?; + resolve.push(unresolved)? }; let encoded = wit_component::encode(Some(true), &resolve, pkg).with_context(|| { format!( @@ -142,8 +139,8 @@ pub enum Error { } /// Validate whether the component conforms to the given world -pub fn validate_target<'a>( - types: &'a Types, +pub fn validate_target( + types: &Types, wit_world_id: WorldId, component_world_id: WorldId, ) -> Result<(), Error> { From 76416141bf4ac022e75a24ce1c8086f030bc9ff0 Mon Sep 17 00:00:00 2001 From: Ryan Levick Date: Mon, 12 Aug 2024 11:36:53 +0200 Subject: [PATCH 3/3] Require --wit flag Signed-off-by: Ryan Levick --- src/commands/targets.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/targets.rs b/src/commands/targets.rs index 8fac0f8..df3f04f 100644 --- a/src/commands/targets.rs +++ b/src/commands/targets.rs @@ -10,11 +10,11 @@ use wac_types::{ExternKind, ItemKind, Package, SubtypeChecker, Types, WorldId}; #[derive(Args)] #[clap(disable_version_flag = true)] pub struct TargetsCommand { - /// The path to the component + /// The path to the component. #[clap(value_name = "COMPONENT_PATH")] pub component: PathBuf, - /// The path to the WIT definition - #[clap(value_name = "WIT_PATH")] + /// The path to the WIT definition containing the world to target. + #[clap(long, value_name = "WIT_PATH")] pub wit: PathBuf, /// The name of the world to target ///