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