wac_cli/commands/
targets.rs1use 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#[derive(Args)]
11#[clap(disable_version_flag = true)]
12pub struct TargetsCommand {
13 #[clap(value_name = "COMPONENT_PATH")]
15 pub component: PathBuf,
16 #[clap(long, value_name = "WIT_PATH")]
18 pub wit: PathBuf,
19 #[clap(long)]
23 pub world: Option<String>,
24}
25
26impl TargetsCommand {
27 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
51fn 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 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 bail!("wit package was not encoded properly")
82 };
83 Ok(*w)
84}
85
86fn 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#[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 ImportNotInTarget {
117 import: String,
119 },
120 #[error("{kind} `{name}` has a mismatched type for targeted wit world")]
121 TargetMismatch {
123 name: String,
125 kind: ExternKind,
127 #[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 MissingTargetExport {
134 name: String,
136 kind: ItemKind,
138 },
139}
140
141pub 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 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 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 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}