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