1use anyhow::Context;
2use indexmap::IndexMap;
3use itertools::Itertools;
4use semver::Version;
5use spin_app::locked::InheritConfiguration as LockedInheritConfiguration;
6use spin_common::{ui::quoted_path, url::parse_file_url};
7use spin_serde::{DependencyName, KebabId};
8use std::collections::BTreeMap;
9use thiserror::Error;
10use wac_graph::types::{Package, SubtypeChecker, WorldId};
11use wac_graph::{CompositionGraph, NodeId};
12
13pub use spin_capabilities::InheritConfiguration;
14
15pub async fn compose<
34 L: ComponentSourceLoader,
35 Fut: std::future::Future<Output = Result<Vec<u8>, ComposeError>>,
36>(
37 loader: &L,
38 component: &L::Component,
39 apply_trigger_deps: impl Fn(Vec<u8>) -> Fut,
40) -> Result<Vec<u8>, ComposeError> {
41 Composer::new(loader)
42 .compose(component, apply_trigger_deps)
43 .await
44}
45
46#[async_trait::async_trait]
49pub trait DependencyLike {
50 fn inherit(&self) -> InheritConfiguration;
51 fn export(&self) -> &Option<String>;
52}
53
54#[async_trait::async_trait]
57pub trait ComponentLike {
58 type Dependency: DependencyLike;
59
60 fn dependencies(
61 &self,
62 ) -> impl std::iter::ExactSizeIterator<Item = (&DependencyName, &Self::Dependency)>;
63 fn id(&self) -> &str;
64}
65
66#[async_trait::async_trait]
67impl ComponentLike for spin_app::locked::LockedComponent {
68 type Dependency = spin_app::locked::LockedComponentDependency;
69
70 fn dependencies(
71 &self,
72 ) -> impl std::iter::ExactSizeIterator<Item = (&DependencyName, &Self::Dependency)> {
73 self.dependencies.iter()
74 }
75
76 fn id(&self) -> &str {
77 &self.id
78 }
79}
80
81#[async_trait::async_trait]
82impl DependencyLike for spin_app::locked::LockedComponentDependency {
83 fn inherit(&self) -> InheritConfiguration {
84 match &self.inherit {
85 LockedInheritConfiguration::All => InheritConfiguration::All,
86 LockedInheritConfiguration::Some(cfgs) => InheritConfiguration::Some(cfgs.clone()),
87 }
88 }
89
90 fn export(&self) -> &Option<String> {
91 &self.export
92 }
93}
94
95#[async_trait::async_trait]
97pub trait ComponentSourceLoader {
98 type Component: ComponentLike<Dependency = Self::Dependency>;
99 type Dependency: DependencyLike;
100 type Source;
101 async fn load_component_source(&self, source: &Self::Component) -> anyhow::Result<Vec<u8>>;
102 async fn load_dependency_source(&self, source: &Self::Dependency) -> anyhow::Result<Vec<u8>>;
103 async fn load_source(&self, source: &Self::Source) -> anyhow::Result<Vec<u8>>;
104}
105
106pub struct ComponentSourceLoaderFs;
108
109#[async_trait::async_trait]
110impl ComponentSourceLoader for ComponentSourceLoaderFs {
111 type Component = spin_app::locked::LockedComponent;
112 type Dependency = spin_app::locked::LockedComponentDependency;
113 type Source = spin_app::locked::LockedComponentSource;
114
115 async fn load_component_source(&self, source: &Self::Component) -> anyhow::Result<Vec<u8>> {
116 Self::load_from_locked_source(&source.source).await
117 }
118
119 async fn load_dependency_source(&self, source: &Self::Dependency) -> anyhow::Result<Vec<u8>> {
120 Self::load_from_locked_source(&source.source).await
121 }
122
123 async fn load_source(&self, source: &Self::Source) -> anyhow::Result<Vec<u8>> {
124 Self::load_from_locked_source(source).await
125 }
126}
127
128impl ComponentSourceLoaderFs {
129 async fn load_from_locked_source(
130 source: &spin_app::locked::LockedComponentSource,
131 ) -> anyhow::Result<Vec<u8>> {
132 let source = source
133 .content
134 .source
135 .as_ref()
136 .context("LockedComponentSource missing source field")?;
137
138 let path = parse_file_url(source)?;
139
140 let bytes: Vec<u8> = tokio::fs::read(&path).await.with_context(|| {
141 format!(
142 "failed to read component source from disk at path {}",
143 quoted_path(&path)
144 )
145 })?;
146
147 let component = spin_componentize::componentize_if_necessary(&bytes)
148 .with_context(|| format!("failed to componentize {}", quoted_path(&path)))?;
149
150 Ok(component.into())
151 }
152}
153
154#[derive(Debug, Error)]
156pub enum ComposeError {
157 #[error(
159 "dependency '{dependency_name}' doesn't match any imports of component '{component_id}'"
160 )]
161 UnmatchedDependencyName {
162 component_id: String,
163 dependency_name: DependencyName,
164 },
165 #[error("component '{component_id}' has dependency conflicts: {}", format_conflicts(.conflicts))]
167 DependencyConflicts {
168 component_id: String,
169 conflicts: Vec<(String, Vec<DependencyName>)>,
170 },
171 #[error(
173 "dependency '{dependency_name}' doesn't export '{export_name}' to satisfy import '{import_name}'"
174 )]
175 MissingExport {
176 dependency_name: DependencyName,
177 export_name: String,
178 import_name: String,
179 },
180 #[error("an error occurred when preparing dependencies")]
182 PrepareError(#[source] anyhow::Error),
183 #[error("failed to encode composition graph: {0}")]
185 EncodeError(#[source] anyhow::Error),
186}
187
188fn format_conflicts(conflicts: &[(String, Vec<DependencyName>)]) -> String {
189 conflicts
190 .iter()
191 .map(|(import_name, dependency_names)| {
192 format!(
193 "import '{}' satisfied by dependencies: '{}'",
194 import_name,
195 dependency_names.iter().join(", ")
196 )
197 })
198 .join("; ")
199}
200
201struct Composer<'a, L> {
202 graph: CompositionGraph,
203 loader: &'a L,
204}
205
206impl<'a, L: ComponentSourceLoader> Composer<'a, L> {
207 async fn compose<Fut: std::future::Future<Output = Result<Vec<u8>, ComposeError>>>(
208 mut self,
209 component: &L::Component,
210 apply_trigger_deps: impl Fn(Vec<u8>) -> Fut,
211 ) -> Result<Vec<u8>, ComposeError> {
212 let source = self
213 .loader
214 .load_component_source(component)
215 .await
216 .map_err(ComposeError::PrepareError)?;
217
218 let fulfilled_source = if component.dependencies().len() == 0 {
219 source
220 } else {
221 let (world_id, instantiation_id) = self
222 .register_package(component.id(), None, source)
223 .map_err(ComposeError::PrepareError)?;
224
225 let prepared = self.prepare_dependencies(world_id, component).await?;
226
227 let arguments = self
228 .build_instantiation_arguments(world_id, prepared)
229 .await?;
230
231 for (argument_name, argument) in arguments {
232 self.graph
233 .set_instantiation_argument(instantiation_id, &argument_name, argument)
234 .map_err(|e| ComposeError::PrepareError(e.into()))?;
235 }
236
237 self.export_dependents_exports(world_id, instantiation_id)
238 .map_err(ComposeError::PrepareError)?;
239
240 self.graph
241 .encode(Default::default())
242 .map_err(|e| ComposeError::EncodeError(e.into()))?
243 };
244
245 let with_extras = apply_trigger_deps(fulfilled_source).await?;
246
247 Ok(with_extras)
248 }
249
250 fn new(loader: &'a L) -> Self {
251 Self {
252 graph: CompositionGraph::new(),
253 loader,
254 }
255 }
256
257 async fn prepare_dependencies(
264 &mut self,
265 world_id: WorldId,
266 component: &L::Component,
267 ) -> Result<IndexMap<String, DependencyInfo>, ComposeError> {
268 let imports = self.graph.types()[world_id].imports.clone();
269
270 let import_keys = imports.keys().cloned().collect::<Vec<_>>();
271
272 let mut mappings: BTreeMap<String, Vec<DependencyInfo>> = BTreeMap::new();
273
274 for (dependency_name, dependency) in component.dependencies() {
275 let mut matched = Vec::new();
276
277 for import_name in &import_keys {
278 if matches_import(dependency_name, import_name)
279 .map_err(ComposeError::PrepareError)?
280 {
281 matched.push(import_name.clone());
282 }
283 }
284
285 if matched.is_empty() {
286 return Err(ComposeError::UnmatchedDependencyName {
287 component_id: component.id().to_owned(),
288 dependency_name: dependency_name.clone(),
289 });
290 }
291
292 let info = self
293 .register_dependency(dependency_name.clone(), dependency)
294 .await
295 .map_err(ComposeError::PrepareError)?;
296
297 for import_name in matched {
299 mappings
300 .entry(import_name.to_string())
301 .or_default()
302 .push(info.clone());
303 }
304 }
305
306 let (conflicts, prepared): (Vec<_>, Vec<_>) =
307 mappings.into_iter().partition(|(_, infos)| infos.len() > 1);
308
309 if !conflicts.is_empty() {
310 return Err(ComposeError::DependencyConflicts {
311 component_id: component.id().to_owned(),
312 conflicts: conflicts
313 .into_iter()
314 .map(|(import_name, infos)| {
315 (
316 import_name,
317 infos.into_iter().map(|info| info.manifest_name).collect(),
318 )
319 })
320 .collect(),
321 });
322 }
323
324 Ok(prepared
325 .into_iter()
326 .map(|(import_name, mut infos)| {
327 assert_eq!(infos.len(), 1);
328 (import_name, infos.remove(0))
329 })
330 .collect())
331 }
332
333 async fn build_instantiation_arguments(
338 &mut self,
339 world_id: WorldId,
340 dependencies: IndexMap<String, DependencyInfo>,
341 ) -> Result<IndexMap<String, NodeId>, ComposeError> {
342 let mut cache = Default::default();
343 let mut checker = SubtypeChecker::new(&mut cache);
344
345 let mut arguments = IndexMap::new();
346
347 for (import_name, dependency_info) in dependencies {
348 let (export_name, export_ty) = match dependency_info.export_name {
349 Some(export_name) => {
350 let Some(export_ty) = self.graph.types()[dependency_info.world_id]
351 .exports
352 .get(&export_name)
353 else {
354 return Err(ComposeError::MissingExport {
355 dependency_name: dependency_info.manifest_name,
356 export_name,
357 import_name: import_name.clone(),
358 });
359 };
360
361 (export_name, export_ty)
362 }
363 None => {
364 let Some(export_ty) = self.graph.types()[dependency_info.world_id]
365 .exports
366 .get(&import_name)
367 else {
368 return Err(ComposeError::MissingExport {
369 dependency_name: dependency_info.manifest_name,
370 export_name: import_name.clone(),
371 import_name: import_name.clone(),
372 });
373 };
374
375 (import_name.clone(), export_ty)
376 }
377 };
378
379 let import_ty = self.graph.types()[world_id]
380 .imports
381 .get(&import_name)
382 .unwrap();
383
384 checker.is_subtype(
386 *export_ty,
387 self.graph.types(),
388 *import_ty,
389 self.graph.types(),
390 ).with_context(|| {
391 format!(
392 "dependency '{dependency_name}' exports '{export_name}' which is not compatible with import '{import_name}'",
393 dependency_name = dependency_info.manifest_name,
394 )
395 })
396 .map_err(ComposeError::PrepareError)?;
397
398 let export_id = self
399 .graph
400 .alias_instance_export(dependency_info.instantiation_id, &export_name)
401 .map_err(|e| ComposeError::PrepareError(e.into()))?;
402
403 assert!(arguments.insert(import_name, export_id).is_none());
404 }
405
406 Ok(arguments)
407 }
408
409 async fn register_dependency(
414 &mut self,
415 dependency_name: DependencyName,
416 dependency: &L::Dependency,
417 ) -> anyhow::Result<DependencyInfo> {
418 let mut dependency_source = self.loader.load_dependency_source(dependency).await?;
419
420 let package_name = match &dependency_name {
421 DependencyName::Package(name) => name.package.to_string(),
422 DependencyName::Plain(name) => name.to_string(),
423 };
424
425 dependency_source =
426 spin_capabilities::apply_deny_adapter(&dependency_source, dependency.inherit())?;
427
428 let (world_id, instantiation_id) =
429 self.register_package(&package_name, None, dependency_source)?;
430
431 Ok(DependencyInfo {
432 manifest_name: dependency_name,
433 instantiation_id,
434 world_id,
435 export_name: dependency.export().clone(),
436 })
437 }
438
439 fn register_package(
440 &mut self,
441 name: &str,
442 version: Option<&Version>,
443 source: impl Into<Vec<u8>>,
444 ) -> anyhow::Result<(WorldId, NodeId)> {
445 let package = Package::from_bytes(name, version, source, self.graph.types_mut())?;
446 let world_id = package.ty();
447 let package_id = self.graph.register_package(package)?;
448 let instantiation_id = self.graph.instantiate(package_id);
449
450 Ok((world_id, instantiation_id))
451 }
452
453 fn export_dependents_exports(
454 &mut self,
455 world_id: WorldId,
456 instantiation_id: NodeId,
457 ) -> anyhow::Result<()> {
458 for export_name in self.graph.types()[world_id]
460 .exports
461 .keys()
462 .cloned()
463 .collect::<Vec<_>>()
464 {
465 let export_id = self
466 .graph
467 .alias_instance_export(instantiation_id, &export_name)?;
468
469 self.graph.export(export_id, &export_name)?;
470 }
471
472 Ok(())
473 }
474}
475
476#[derive(Clone)]
477struct DependencyInfo {
478 manifest_name: DependencyName,
482 instantiation_id: NodeId,
484 world_id: WorldId,
486 export_name: Option<String>,
488}
489enum ImportName {
490 Plain(KebabId),
491 Package {
492 package: String,
493 interface: String,
494 version: Option<Version>,
495 },
496}
497
498impl std::str::FromStr for ImportName {
499 type Err = anyhow::Error;
500
501 fn from_str(s: &str) -> Result<Self, Self::Err> {
502 if s.contains([':', '/']) {
503 let (package, rest) = s
504 .split_once('/')
505 .with_context(|| format!("invalid import name: {s}"))?;
506
507 let (interface, version) = match rest.split_once('@') {
508 Some((interface, version)) => {
509 let version = Version::parse(version)
510 .with_context(|| format!("invalid version in import name: {s}"))?;
511
512 (interface, Some(version))
513 }
514 None => (rest, None),
515 };
516
517 Ok(Self::Package {
518 package: package.to_string(),
519 interface: interface.to_string(),
520 version,
521 })
522 } else {
523 Ok(Self::Plain(
524 s.to_string()
525 .try_into()
526 .map_err(|e| anyhow::anyhow!("{e}"))?,
527 ))
528 }
529 }
530}
531
532fn matches_import(dependency_name: &DependencyName, import_name: &str) -> anyhow::Result<bool> {
534 let import_name = import_name.parse::<ImportName>()?;
535
536 match (dependency_name, import_name) {
537 (DependencyName::Plain(dependency_name), ImportName::Plain(import_name)) => {
538 Ok(dependency_name == &import_name)
540 }
541 (
542 DependencyName::Package(dependency_name),
543 ImportName::Package {
544 package: import_package,
545 interface: import_interface,
546 version: import_version,
547 },
548 ) => {
549 if import_package != dependency_name.package.to_string() {
550 return Ok(false);
551 }
552
553 if let Some(interface) = dependency_name.interface.as_ref()
554 && import_interface != interface.as_ref()
555 {
556 return Ok(false);
557 }
558
559 if let Some(version) = dependency_name.version.as_ref()
560 && import_version != Some(version.clone())
561 {
562 return Ok(false);
563 }
564
565 Ok(true)
566 }
567 (_, _) => {
568 Ok(false)
570 }
571 }
572}
573
574#[cfg(test)]
575mod test {
576 use super::*;
577
578 #[test]
579 fn test_matches_import() {
580 for (dep_name, import_names) in [
581 ("foo:bar/baz@0.1.0", vec!["foo:bar/baz@0.1.0"]),
582 ("foo:bar/baz", vec!["foo:bar/baz@0.1.0", "foo:bar/baz"]),
583 ("foo:bar", vec!["foo:bar/baz@0.1.0", "foo:bar/baz"]),
584 ("foo:bar@0.1.0", vec!["foo:bar/baz@0.1.0"]),
585 ("foo-bar", vec!["foo-bar"]),
586 ] {
587 let dep_name: DependencyName = dep_name.parse().unwrap();
588 for import_name in import_names {
589 assert!(matches_import(&dep_name, import_name).unwrap());
590 }
591 }
592
593 for (dep_name, import_names) in [
594 ("foo:bar/baz@0.1.0", vec!["foo:bar/baz"]),
595 ("foo:bar/baz", vec!["foo:bar/bub", "foo:bar/bub@0.1.0"]),
596 ("foo:bar", vec!["foo:bub/bib"]),
597 ("foo:bar@0.1.0", vec!["foo:bar/baz"]),
598 ("foo:bar/baz", vec!["foo:bar/baz-bub", "foo-bar"]),
599 ] {
600 let dep_name: DependencyName = dep_name.parse().unwrap();
601 for import_name in import_names {
602 assert!(!matches_import(&dep_name, import_name).unwrap());
603 }
604 }
605 }
606}