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