wac_cli/commands/
targets.rs

1use anyhow::{bail, Context, Result};
2use clap::Args;
3use std::{
4    fs,
5    path::{Path, PathBuf},
6};
7use wac_types::{ExternKind, ItemKind, Package, SubtypeChecker, Types, WorldId};
8
9/// Verifies that a given WebAssembly component targets a world.
10#[derive(Args)]
11#[clap(disable_version_flag = true)]
12pub 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 containing the world to target.
17    #[clap(long, 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
26impl 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
52fn 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 named '{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
87fn 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 {
98        let unresolved = wit_parser::UnresolvedPackage::parse_file(path)?;
99        resolve.push(unresolved)?
100    };
101    let encoded = wit_component::encode(Some(true), &resolve, pkg).with_context(|| {
102        format!(
103            "failed to encode WIT package from `{path}`",
104            path = path.display()
105        )
106    })?;
107    Ok(encoded)
108}
109
110/// An error in target validation
111#[derive(thiserror::Error, miette::Diagnostic, Debug)]
112#[diagnostic(code("component does not match wit world"))]
113pub enum Error {
114    #[error("the target wit does not have an import named `{import}` but the component does")]
115    /// The import is not in the target world
116    ImportNotInTarget {
117        /// The name of the missing target
118        import: String,
119    },
120    #[error("{kind} `{name}` has a mismatched type for targeted wit world")]
121    /// An import or export has a mismatched type for the target world.
122    TargetMismatch {
123        /// The name of the mismatched item
124        name: String,
125        /// The extern kind of the item
126        kind: ExternKind,
127        /// The source of the error
128        #[source]
129        source: anyhow::Error,
130    },
131    #[error("the targeted wit world requires an export named `{name}` but the component did not export one")]
132    /// Missing an export for the target world.
133    MissingTargetExport {
134        /// The export name.
135        name: String,
136        /// The expected item kind.
137        kind: ItemKind,
138    },
139}
140
141/// Validate whether the component conforms to the given world
142pub fn validate_target(
143    types: &Types,
144    wit_world_id: WorldId,
145    component_world_id: WorldId,
146) -> Result<(), Error> {
147    let component_world = &types[component_world_id];
148    let wit_world = &types[wit_world_id];
149    // The interfaces imported implicitly through uses.
150    let implicit_imported_interfaces = wit_world.implicit_imported_interfaces(types);
151    let mut cache = Default::default();
152    let mut checker = SubtypeChecker::new(&mut cache);
153
154    // The output is allowed to import a subset of the world's imports
155    checker.invert();
156    for (import, item_kind) in component_world.imports.iter() {
157        let expected = implicit_imported_interfaces
158            .get(import.as_str())
159            .or_else(|| wit_world.imports.get(import))
160            .ok_or_else(|| Error::ImportNotInTarget {
161                import: import.to_owned(),
162            })?;
163
164        checker
165            .is_subtype(expected.promote(), types, *item_kind, types)
166            .map_err(|e| Error::TargetMismatch {
167                kind: ExternKind::Import,
168                name: import.to_owned(),
169                source: e,
170            })?;
171    }
172
173    checker.revert();
174
175    // The output must export every export in the world
176    for (name, expected) in &wit_world.exports {
177        let export = component_world.exports.get(name).copied().ok_or_else(|| {
178            Error::MissingTargetExport {
179                name: name.clone(),
180                kind: *expected,
181            }
182        })?;
183
184        checker
185            .is_subtype(export, types, expected.promote(), types)
186            .map_err(|e| Error::TargetMismatch {
187                kind: ExternKind::Export,
188                name: name.clone(),
189                source: e,
190            })?;
191    }
192
193    Ok(())
194}