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