Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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

Expand Down
6 changes: 5 additions & 1 deletion src/bin/wac.rs
Original file line number Diff line number Diff line change
@@ -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"))
Expand All @@ -21,6 +23,7 @@ enum Wac {
Compose(ComposeCommand),
Parse(ParseCommand),
Resolve(ResolveCommand),
Targets(TargetsCommand),
}

#[tokio::main]
Expand All @@ -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:?}",
Expand Down
2 changes: 2 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
194 changes: 194 additions & 0 deletions src/commands/targets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
use anyhow::{bail, Context, Result};
use clap::Args;
use std::{
fs,
path::{Path, PathBuf},
};
use wac_types::{ExternKind, ItemKind, Package, SubtypeChecker, Types, WorldId};

/// Verifies that a given WebAssembly component targets a world.
#[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 containing the world to target.
#[clap(long, 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<String>,
}

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<WorldId> {
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 named '{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<Vec<u8>> {
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 {
let unresolved = wit_parser::UnresolvedPackage::parse_file(path)?;
resolve.push(unresolved)?
};
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(
types: &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(())
}