1use std::{collections::HashMap, sync::Arc};
2
3use anyhow::{Context, anyhow};
4use itertools::Itertools;
5
6mod environment;
7mod loader;
8
9use environment::{CandidateWorld, CandidateWorlds, TargetEnvironment, TriggerType};
10pub use environment::{Catalogue, EnvironmentDefinition, load_environment_def};
11pub use loader::ApplicationToValidate;
12use loader::ComponentToValidate;
13use spin_manifest::schema::v2::TargetEnvironmentRef;
14
15use crate::environment::RealisedTargets;
16
17#[derive(Default)]
18pub struct Targets<'a> {
19 pub default: &'a [TargetEnvironmentRef],
20 pub overrides: HashMap<String, &'a [TargetEnvironmentRef]>,
21}
22
23impl<'a> Targets<'a> {
24 fn is_empty(&self) -> bool {
25 self.default.is_empty() && self.overrides.is_empty()
26 }
27
28 fn all_refs(&self) -> Vec<&TargetEnvironmentRef> {
29 self.default
30 .iter()
31 .chain(self.overrides.values().flat_map(|list| list.iter()))
32 .unique()
33 .collect()
34 }
35}
36
37#[derive(Default)]
43pub struct TargetEnvironmentValidation(Vec<anyhow::Error>);
44
45impl TargetEnvironmentValidation {
46 pub fn is_ok(&self) -> bool {
47 self.0.is_empty()
48 }
49
50 pub fn errors(&self) -> &[anyhow::Error] {
51 &self.0
52 }
53}
54
55pub async fn validate_application_against_environment_ids<'a>(
64 application: &ApplicationToValidate,
65 targets: Targets<'a>,
66 cache_root: Option<std::path::PathBuf>,
67 app_dir: &std::path::Path,
68) -> anyhow::Result<TargetEnvironmentValidation> {
69 if targets.is_empty() {
70 return Ok(Default::default());
71 }
72
73 let envs = TargetEnvironment::load_all(targets, cache_root, app_dir).await?;
74 validate_application_against_environments(application, &envs).await
75}
76
77async fn validate_application_against_environments(
82 application: &ApplicationToValidate,
83 envs: &RealisedTargets,
84) -> anyhow::Result<TargetEnvironmentValidation> {
85 for trigger_type in application.trigger_types() {
86 if let Some(env) = envs.iter().find(|e| !e.supports_trigger_type(trigger_type)) {
87 anyhow::bail!(
88 "Environment {} does not support trigger type {trigger_type}",
89 env.name()
90 );
91 }
92 }
93
94 let components_by_trigger_type = application.components_by_trigger_type().await?;
95
96 let mut errs = vec![];
97
98 for (trigger_type, component) in components_by_trigger_type {
99 for component in &component {
100 let envs = envs.get(component.id());
101 errs.extend(
102 validate_component_against_environments(envs, &trigger_type, component).await,
103 );
104 }
105 }
106
107 Ok(TargetEnvironmentValidation(errs))
108}
109
110async fn validate_component_against_environments(
118 envs: &[Arc<TargetEnvironment>],
119 trigger_type: &TriggerType,
120 component: &ComponentToValidate<'_>,
121) -> Vec<anyhow::Error> {
122 let mut errs = vec![];
123
124 for env in envs {
125 let worlds = env.worlds(trigger_type);
126 if let Some(e) = validate_wasm_against_any_world(env, worlds, component)
127 .await
128 .err()
129 {
130 errs.push(e);
131 }
132
133 let host_caps = env.capabilities(trigger_type);
134 if let Some(e) = validate_host_reqs(env, host_caps, component).err() {
135 errs.push(e);
136 }
137 }
138
139 if errs.is_empty() {
140 tracing::info!(
141 "Validated component {} {} against all target worlds",
142 component.id(),
143 component.source_description()
144 );
145 }
146
147 errs
148}
149
150async fn validate_wasm_against_any_world(
154 env: &TargetEnvironment,
155 worlds: &CandidateWorlds,
156 component: &ComponentToValidate<'_>,
157) -> anyhow::Result<()> {
158 let mut result = Ok(());
159 for target_world in worlds {
160 tracing::debug!(
161 "Trying component {} {} against target world {target_world}",
162 component.id(),
163 component.source_description(),
164 );
165 match validate_wasm_against_world(env, target_world, component).await {
166 Ok(()) => {
167 tracing::info!(
168 "Validated component {} {} against target world {target_world}",
169 component.id(),
170 component.source_description(),
171 );
172 return Ok(());
173 }
174 Err(e) => {
175 tracing::info!(
177 "Rejecting component {} {} for target world {target_world} because {e:?}",
178 component.id(),
179 component.source_description(),
180 );
181 result = Err(e);
182 }
183 }
184 }
185 result
186}
187
188async fn validate_wasm_against_world(
189 env: &TargetEnvironment,
190 target_world: &CandidateWorld,
191 component: &ComponentToValidate<'_>,
192) -> anyhow::Result<()> {
193 use wac_types::{ItemKind, Package as WacPackage, Types as WacTypes, WorldId, validate_target};
194
195 fn get_wit_world(
198 types: &WacTypes,
199 top_level_world: WorldId,
200 world_name: &str,
201 ) -> anyhow::Result<WorldId> {
202 let top_level_world = &types[top_level_world];
203 let world = top_level_world
204 .exports
205 .get(world_name)
206 .with_context(|| format!("wit package did not contain a world named '{world_name}'"))?;
207
208 let ItemKind::Type(wac_types::Type::World(world_id)) = world else {
209 anyhow::bail!("wit package was not encoded properly")
211 };
212 let wit_world = &types[*world_id];
213 let world = wit_world.exports.values().next();
214 let Some(ItemKind::Component(w)) = world else {
215 anyhow::bail!("wit package was not encoded properly")
217 };
218 Ok(*w)
219 }
220
221 let mut types = WacTypes::default();
222
223 let target_world_package = WacPackage::from_bytes(
224 &target_world.package_namespaced_name(),
225 target_world.package_version(),
226 target_world.package_bytes(),
227 &mut types,
228 )?;
229
230 let target_world_id =
231 get_wit_world(&types, target_world_package.ty(), target_world.world_name())?;
232
233 let component_package =
234 WacPackage::from_bytes(component.id(), None, component.wasm_bytes(), &mut types)?;
235
236 let target_result = validate_target(&types, target_world_id, component_package.ty());
237
238 match target_result {
239 Ok(_) => Ok(()),
240 Err(report) => Err(format_target_result_error(
241 &types,
242 env.name(),
243 target_world.to_string(),
244 component.id(),
245 component.source_description(),
246 &report,
247 )),
248 }
249}
250
251fn validate_host_reqs(
252 env: &TargetEnvironment,
253 host_caps: &[String],
254 component: &ComponentToValidate,
255) -> anyhow::Result<()> {
256 let unsatisfied: Vec<_> = component
257 .host_requirements()
258 .iter()
259 .filter(|host_req| !satisfies(host_caps, host_req))
260 .cloned()
261 .collect();
262 if unsatisfied.is_empty() {
263 Ok(())
264 } else {
265 Err(anyhow!(
266 "Component {} can't run in environment {} because it requires the feature(s) '{}' which the environment does not support",
267 component.id(),
268 env.name(),
269 unsatisfied.join(", ")
270 ))
271 }
272}
273
274fn satisfies(host_caps: &[String], host_req: &String) -> bool {
275 host_caps.contains(host_req)
276}
277
278fn format_target_result_error(
279 types: &wac_types::Types,
280 env_name: &str,
281 target_world_name: String,
282 component_id: &str,
283 source_description: &str,
284 report: &wac_types::TargetValidationReport,
285) -> anyhow::Error {
286 let mut error_string = format!(
287 "Component {component_id} ({source_description}) can't run in environment {env_name} (world {target_world_name}).\n",
288 );
289
290 for (idx, import) in report.imports_not_in_target().enumerate() {
291 if idx == 0 {
292 error_string.push_str(
293 "The component requires the following imports, which the environment does not provide:\n - ",
294 );
295 } else {
296 error_string.push_str(" - ");
297 }
298 error_string.push_str(import);
299 error_string.push('\n');
300 }
301
302 for (idx, (export, export_kind)) in report.missing_exports().enumerate() {
303 if idx == 0 {
304 error_string.push_str(
305 "The environment requires the following exports, which the component does not provide:\n - ",
306 );
307 } else {
308 error_string.push_str(" - ");
309 }
310 error_string.push_str(export);
311 error_string.push_str(" (");
312 error_string.push_str(export_kind.desc(types));
313 error_string.push_str(")\n");
314 }
315
316 for (name, extern_kind, error) in report.mismatched_types() {
317 error_string.push_str("Found a type mismatch for ");
318 error_string.push_str(&format!("{extern_kind} {name}: {error}"));
319 }
320
321 anyhow!(error_string)
322}