Skip to content

Commit

Permalink
Merge pull request #132 from rylev/targets-cmd
Browse files Browse the repository at this point in the history
Add first version of targets command
  • Loading branch information
rylev authored Aug 12, 2024
2 parents 05551f6 + 7641614 commit 47633c6
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 1 deletion.
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(())
}

0 comments on commit 47633c6

Please sign in to comment.