Skip to content

Commit fa6fddc

Browse files
committed
Add first version of targets command
Signed-off-by: Ryan Levick <ryan.levick@fermyon.com>
1 parent 05551f6 commit fa6fddc

File tree

6 files changed

+220
-1
lines changed

6 files changed

+220
-1
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ indexmap = { workspace = true }
4040
miette = { workspace = true, features = ["fancy"] }
4141
semver = { workspace = true }
4242
indicatif = { workspace = true, optional = true }
43+
wit-parser = { workspace = true }
44+
wit-component = { workspace = true }
4345

4446
[features]
4547
default = ["wit", "registry"]

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ The `wac` CLI tool has the following commands:
100100

101101
* `wac plug` - Plugs the imports of a component with one or more other components.
102102
* `wac compose` - Compose WebAssembly components using the provided WAC source file.
103+
* `wac targets` - Determines whether a given component conforms to the supplied wit world.
103104
* `wac parse` - Parses a composition into a JSON representation of the AST.
104105
* `wac resolve` - Resolves a composition into a JSON representation.
105106

@@ -117,6 +118,17 @@ Or mixing in packages published to a Warg registry:
117118
wac plug my-namespace:package-name --plug some-namespace:other-package-name -o plugged.wasm
118119
```
119120

121+
### Checking Whether a Component Implements a World
122+
123+
To see whether a given component implements a given world, use the `wac targets` command:
124+
125+
```
126+
wac targets my-component.wasm my-wit.wit
127+
```
128+
129+
If `my-component.wasm` implements the world defined in `my-wit.wit` then the command will succeed. Otherwise, an error will be returned.
130+
131+
If `my-wit.wit` has multiple world definitions, you can disambiguate using the `--world` flag.
120132

121133
### Encoding Compositions
122134

src/bin/wac.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use anyhow::Result;
22
use clap::Parser;
33
use owo_colors::{OwoColorize, Stream, Style};
4-
use wac_cli::commands::{ComposeCommand, ParseCommand, PlugCommand, ResolveCommand};
4+
use wac_cli::commands::{
5+
ComposeCommand, ParseCommand, PlugCommand, ResolveCommand, TargetsCommand,
6+
};
57

68
fn version() -> &'static str {
79
option_env!("CARGO_VERSION_INFO").unwrap_or(env!("CARGO_PKG_VERSION"))
@@ -21,6 +23,7 @@ enum Wac {
2123
Compose(ComposeCommand),
2224
Parse(ParseCommand),
2325
Resolve(ResolveCommand),
26+
Targets(TargetsCommand),
2427
}
2528

2629
#[tokio::main]
@@ -32,6 +35,7 @@ async fn main() -> Result<()> {
3235
Wac::Resolve(cmd) => cmd.exec().await,
3336
Wac::Compose(cmd) => cmd.exec().await,
3437
Wac::Plug(cmd) => cmd.exec().await,
38+
Wac::Targets(cmd) => cmd.exec().await,
3539
} {
3640
eprintln!(
3741
"{error}: {e:?}",

src/commands.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ mod compose;
44
mod parse;
55
mod plug;
66
mod resolve;
7+
mod targets;
78

89
pub use self::compose::*;
910
pub use self::parse::*;
1011
pub use self::plug::*;
1112
pub use self::resolve::*;
13+
pub use self::targets::*;

src/commands/targets.rs

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
use anyhow::{bail, Context, Result};
2+
use clap::Args;
3+
use std::{
4+
fs,
5+
path::{Path, PathBuf},
6+
};
7+
use wac_types::{ExternKind, ItemKind, Package, SubtypeChecker, Types, WorldId};
8+
9+
/// Parses a WAC source file into a JSON AST representation.
10+
#[derive(Args)]
11+
#[clap(disable_version_flag = true)]
12+
pub struct TargetsCommand {
13+
/// The path to the component
14+
#[clap(value_name = "COMPONENT_PATH")]
15+
pub component: PathBuf,
16+
/// The path to the WIT definition
17+
#[clap(value_name = "WIT_PATH")]
18+
pub wit: PathBuf,
19+
/// The name of the world to target
20+
///
21+
/// If the wit package only has one world definition, this does not need to be specified.
22+
#[clap(long)]
23+
pub world: Option<String>,
24+
}
25+
26+
impl TargetsCommand {
27+
/// Executes the command.
28+
pub async fn exec(self) -> Result<()> {
29+
log::debug!("executing targets command");
30+
let mut types = Types::default();
31+
32+
let wit_bytes = encode_wit_as_component(&self.wit)?;
33+
let wit = Package::from_bytes("wit", None, wit_bytes, &mut types)?;
34+
35+
let component_bytes = fs::read(&self.component).with_context(|| {
36+
format!(
37+
"failed to read file `{path}`",
38+
path = self.component.display()
39+
)
40+
})?;
41+
let component = Package::from_bytes("component", None, component_bytes, &mut types)?;
42+
43+
let wit = get_wit_world(&types, wit.ty(), self.world.as_deref())?;
44+
45+
validate_target(&types, wit, component.ty())?;
46+
47+
Ok(())
48+
}
49+
}
50+
51+
/// Gets the selected world from the component encoded WIT package
52+
fn get_wit_world(
53+
types: &Types,
54+
top_level_world: WorldId,
55+
world_name: Option<&str>,
56+
) -> anyhow::Result<WorldId> {
57+
let top_level_world = &types[top_level_world];
58+
let world = match world_name {
59+
Some(world_name) => top_level_world
60+
.exports
61+
.get(world_name)
62+
.with_context(|| format!("wit package did not contain a world name '{world_name}'"))?,
63+
None if top_level_world.exports.len() == 1 => {
64+
top_level_world.exports.values().next().unwrap()
65+
}
66+
None if top_level_world.exports.len() > 1 => {
67+
bail!("wit package has multiple worlds, please specify one with the --world flag")
68+
}
69+
None => {
70+
bail!("wit package did not contain a world")
71+
}
72+
};
73+
let ItemKind::Type(wac_types::Type::World(world_id)) = world else {
74+
// We expect the top-level world to export a world type
75+
bail!("wit package was not encoded properly")
76+
};
77+
let wit_world = &types[*world_id];
78+
let world = wit_world.exports.values().next();
79+
let Some(ItemKind::Component(w)) = world else {
80+
// We expect the nested world type to export a component
81+
bail!("wit package was not encoded properly")
82+
};
83+
Ok(*w)
84+
}
85+
86+
/// Encodes the wit package found at `path` into a component
87+
fn encode_wit_as_component(path: &Path) -> anyhow::Result<Vec<u8>> {
88+
let mut resolve = wit_parser::Resolve::new();
89+
let pkg = if path.is_dir() {
90+
log::debug!(
91+
"loading WIT package from directory `{path}`",
92+
path = path.display()
93+
);
94+
95+
let (pkg, _) = resolve.push_dir(&path)?;
96+
pkg
97+
} else if path.extension().and_then(std::ffi::OsStr::to_str) == Some("wit") {
98+
let unresolved = wit_parser::UnresolvedPackage::parse_file(&path)?;
99+
let pkg = resolve.push(unresolved)?;
100+
pkg
101+
} else {
102+
bail!("expected either a wit directory or wit file")
103+
};
104+
let encoded = wit_component::encode(Some(true), &resolve, pkg).with_context(|| {
105+
format!(
106+
"failed to encode WIT package from `{path}`",
107+
path = path.display()
108+
)
109+
})?;
110+
Ok(encoded)
111+
}
112+
113+
/// An error in target validation
114+
#[derive(thiserror::Error, miette::Diagnostic, Debug)]
115+
#[diagnostic(code("component does not match wit world"))]
116+
pub enum Error {
117+
#[error("the target wit does not have an import named `{import}` but the component does")]
118+
/// The import is not in the target world
119+
ImportNotInTarget {
120+
/// The name of the missing target
121+
import: String,
122+
},
123+
#[error("{kind} `{name}` has a mismatched type for targeted wit world")]
124+
/// An import or export has a mismatched type for the target world.
125+
TargetMismatch {
126+
/// The name of the mismatched item
127+
name: String,
128+
/// The extern kind of the item
129+
kind: ExternKind,
130+
/// The source of the error
131+
#[source]
132+
source: anyhow::Error,
133+
},
134+
#[error("the targeted wit world requires an export named `{name}` but the component did not export one")]
135+
/// Missing an export for the target world.
136+
MissingTargetExport {
137+
/// The export name.
138+
name: String,
139+
/// The expected item kind.
140+
kind: ItemKind,
141+
},
142+
}
143+
144+
/// Validate whether the component conforms to the given world
145+
pub fn validate_target<'a>(
146+
types: &'a Types,
147+
wit_world_id: WorldId,
148+
component_world_id: WorldId,
149+
) -> Result<(), Error> {
150+
let component_world = &types[component_world_id];
151+
let wit_world = &types[wit_world_id];
152+
// The interfaces imported implicitly through uses.
153+
let implicit_imported_interfaces = wit_world.implicit_imported_interfaces(types);
154+
let mut cache = Default::default();
155+
let mut checker = SubtypeChecker::new(&mut cache);
156+
157+
// The output is allowed to import a subset of the world's imports
158+
checker.invert();
159+
for (import, item_kind) in component_world.imports.iter() {
160+
let expected = implicit_imported_interfaces
161+
.get(import.as_str())
162+
.or_else(|| wit_world.imports.get(import))
163+
.ok_or_else(|| Error::ImportNotInTarget {
164+
import: import.to_owned(),
165+
})?;
166+
167+
checker
168+
.is_subtype(expected.promote(), types, *item_kind, types)
169+
.map_err(|e| Error::TargetMismatch {
170+
kind: ExternKind::Import,
171+
name: import.to_owned(),
172+
source: e,
173+
})?;
174+
}
175+
176+
checker.revert();
177+
178+
// The output must export every export in the world
179+
for (name, expected) in &wit_world.exports {
180+
let export = component_world.exports.get(name).copied().ok_or_else(|| {
181+
Error::MissingTargetExport {
182+
name: name.clone(),
183+
kind: *expected,
184+
}
185+
})?;
186+
187+
checker
188+
.is_subtype(export, types, expected.promote(), types)
189+
.map_err(|e| Error::TargetMismatch {
190+
kind: ExternKind::Export,
191+
name: name.clone(),
192+
source: e,
193+
})?;
194+
}
195+
196+
Ok(())
197+
}

0 commit comments

Comments
 (0)