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