wasm_pkg_core/
resolver.rs

1//! A resolver for resolving dependencies from a component registry.
2// NOTE(thomastaylor312): This is copied and adapted from the `cargo-component` crate: https://github.com/bytecodealliance/cargo-component/blob/f0be1c7d9917aa97e9102e69e3b838dae38d624b/crates/core/src/registry.rs
3
4use std::{
5    collections::{hash_map, HashMap, HashSet},
6    fmt::Debug,
7    ops::{Deref, DerefMut},
8    path::{Path, PathBuf},
9    str::FromStr,
10};
11
12use anyhow::{bail, Context, Result};
13use futures_util::TryStreamExt;
14use indexmap::{IndexMap, IndexSet};
15use semver::{Comparator, Op, Version, VersionReq};
16use tokio::io::{AsyncRead, AsyncReadExt};
17use wasm_pkg_client::{
18    caching::{CachingClient, FileCache},
19    Client, Config, ContentDigest, Error as WasmPkgError, PackageRef, Release, VersionInfo,
20};
21use wit_component::DecodedWasm;
22use wit_parser::{PackageId, PackageName, Resolve, UnresolvedPackageGroup, WorldId};
23
24use crate::{lock::LockFile, wit::get_packages};
25
26/// The name of the default registry.
27pub const DEFAULT_REGISTRY_NAME: &str = "default";
28
29// TODO: functions for resolving dependencies from a lock file
30
31/// Represents a WIT package dependency.
32#[derive(Debug, Clone)]
33pub enum Dependency {
34    /// The dependency is a registry package.
35    Package(RegistryPackage),
36
37    /// The dependency is a path to a local directory or file.
38    Local(PathBuf),
39}
40
41impl FromStr for Dependency {
42    type Err = anyhow::Error;
43
44    fn from_str(s: &str) -> Result<Self> {
45        Ok(Self::Package(s.parse()?))
46    }
47}
48
49/// Represents a reference to a registry package.
50#[derive(Debug, Clone)]
51pub struct RegistryPackage {
52    /// The name of the package.
53    ///
54    /// If not specified, the name from the mapping will be used.
55    pub name: Option<PackageRef>,
56
57    /// The version requirement of the package.
58    pub version: VersionReq,
59
60    /// The name of the component registry containing the package.
61    ///
62    /// If not specified, the default registry is used.
63    pub registry: Option<String>,
64}
65
66impl FromStr for RegistryPackage {
67    type Err = anyhow::Error;
68
69    fn from_str(s: &str) -> Result<Self> {
70        Ok(Self {
71            name: None,
72            version: s
73                .parse()
74                .with_context(|| format!("'{s}' is an invalid registry package version"))?,
75            registry: None,
76        })
77    }
78}
79
80/// Represents information about a resolution of a registry package.
81#[derive(Clone)]
82pub struct RegistryResolution {
83    /// The name of the dependency that was resolved.
84    ///
85    /// This may differ from `package` if the dependency was renamed.
86    pub name: PackageRef,
87    /// The name of the package from the registry that was resolved.
88    pub package: PackageRef,
89    /// The name of the registry used to resolve the package if one was provided
90    pub registry: Option<String>,
91    /// The version requirement that was used to resolve the package.
92    pub requirement: VersionReq,
93    /// The package version that was resolved.
94    pub version: Version,
95    /// The digest of the package contents.
96    pub digest: ContentDigest,
97    /// The client to use for fetching the package contents.
98    client: CachingClient<FileCache>,
99}
100
101impl Debug for RegistryResolution {
102    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
103        f.debug_struct("RegistryResolution")
104            .field("name", &self.name)
105            .field("package", &self.package)
106            .field("registry", &self.registry)
107            .field("requirement", &self.requirement)
108            .field("version", &self.version)
109            .field("digest", &self.digest)
110            .finish()
111    }
112}
113
114impl RegistryResolution {
115    /// Fetches the raw package bytes from the registry. Returns an AsyncRead that will stream the
116    /// package contents
117    pub async fn fetch(&self) -> Result<impl AsyncRead> {
118        let stream = self
119            .client
120            .get_content(
121                &self.package,
122                &Release {
123                    version: self.version.clone(),
124                    content_digest: self.digest.clone(),
125                },
126            )
127            .await?;
128
129        Ok(tokio_util::io::StreamReader::new(
130            stream.map_err(std::io::Error::other),
131        ))
132    }
133}
134
135/// Represents information about a resolution of a local file.
136#[derive(Clone, Debug)]
137pub struct LocalResolution {
138    /// The name of the dependency that was resolved.
139    pub name: PackageRef,
140    /// The path to the resolved dependency.
141    pub path: PathBuf,
142}
143
144/// Represents a resolution of a dependency.
145#[derive(Debug, Clone)]
146#[allow(clippy::large_enum_variant)]
147pub enum DependencyResolution {
148    /// The dependency is resolved from a registry package.
149    Registry(RegistryResolution),
150    /// The dependency is resolved from a local path.
151    Local(LocalResolution),
152}
153
154impl DependencyResolution {
155    /// Gets the name of the dependency that was resolved.
156    pub fn name(&self) -> &PackageRef {
157        match self {
158            Self::Registry(res) => &res.name,
159            Self::Local(res) => &res.name,
160        }
161    }
162
163    /// Gets the resolved version.
164    ///
165    /// Returns `None` if the dependency is not resolved from a registry package.
166    pub fn version(&self) -> Option<&Version> {
167        match self {
168            Self::Registry(res) => Some(&res.version),
169            Self::Local(_) => None,
170        }
171    }
172
173    /// The key used in sorting and searching the lock file package list.
174    ///
175    /// Returns `None` if the dependency is not resolved from a registry package.
176    pub fn key(&self) -> Option<(&PackageRef, Option<&str>)> {
177        match self {
178            DependencyResolution::Registry(pkg) => Some((&pkg.package, pkg.registry.as_deref())),
179            DependencyResolution::Local(_) => None,
180        }
181    }
182
183    /// Decodes the resolved dependency.
184    pub async fn decode(&self) -> Result<DecodedDependency<'_>> {
185        // If the dependency path is a directory, assume it contains wit to parse as a package.
186        let bytes = match self {
187            DependencyResolution::Local(LocalResolution { path, .. })
188                if tokio::fs::metadata(path).await?.is_dir() =>
189            {
190                return Ok(DecodedDependency::Wit {
191                    resolution: self,
192                    package: UnresolvedPackageGroup::parse_dir(path).with_context(|| {
193                        format!("failed to parse dependency `{path}`", path = path.display())
194                    })?,
195                });
196            }
197            DependencyResolution::Local(LocalResolution { path, .. }) => {
198                tokio::fs::read(path).await.with_context(|| {
199                    format!(
200                        "failed to read content of dependency `{name}` at path `{path}`",
201                        name = self.name(),
202                        path = path.display()
203                    )
204                })?
205            }
206            DependencyResolution::Registry(res) => {
207                let mut reader = res.fetch().await?;
208
209                let mut buf = Vec::new();
210                reader.read_to_end(&mut buf).await?;
211                buf
212            }
213        };
214
215        if &bytes[0..4] != b"\0asm" {
216            return Ok(DecodedDependency::Wit {
217                resolution: self,
218                package: UnresolvedPackageGroup::parse(
219                    // This is fake, but it's needed for the parser to work.
220                    self.name().to_string(),
221                    std::str::from_utf8(&bytes).with_context(|| {
222                        format!(
223                            "dependency `{name}` is not UTF-8 encoded",
224                            name = self.name()
225                        )
226                    })?,
227                )?,
228            });
229        }
230
231        Ok(DecodedDependency::Wasm {
232            resolution: self,
233            decoded: wit_component::decode(&bytes).with_context(|| {
234                format!(
235                    "failed to decode content of dependency `{name}`",
236                    name = self.name(),
237                )
238            })?,
239        })
240    }
241}
242
243/// Represents a decoded dependency.
244pub enum DecodedDependency<'a> {
245    /// The dependency decoded from an unresolved WIT package.
246    Wit {
247        /// The resolution related to the decoded dependency.
248        resolution: &'a DependencyResolution,
249        /// The unresolved WIT package.
250        package: UnresolvedPackageGroup,
251    },
252    /// The dependency decoded from a Wasm file.
253    Wasm {
254        /// The resolution related to the decoded dependency.
255        resolution: &'a DependencyResolution,
256        /// The decoded Wasm file.
257        decoded: DecodedWasm,
258    },
259}
260
261impl DecodedDependency<'_> {
262    /// Fully resolves the dependency.
263    ///
264    /// If the dependency is an unresolved WIT package, it will assume that the
265    /// package has no foreign dependencies.
266    pub fn resolve(self) -> Result<(Resolve, PackageId, Vec<PathBuf>)> {
267        match self {
268            Self::Wit { package, .. } => {
269                let mut resolve = Resolve::new();
270                resolve.all_features = true;
271                let source_files = package
272                    .source_map
273                    .source_files()
274                    .map(Path::to_path_buf)
275                    .collect();
276                let pkg = resolve.push_group(package)?;
277                Ok((resolve, pkg, source_files))
278            }
279            Self::Wasm { decoded, .. } => match decoded {
280                DecodedWasm::WitPackage(resolve, pkg) => Ok((resolve, pkg, Vec::new())),
281                DecodedWasm::Component(resolve, world) => {
282                    let pkg = resolve.worlds[world].package.unwrap();
283                    Ok((resolve, pkg, Vec::new()))
284                }
285            },
286        }
287    }
288
289    /// Gets the package name of the decoded dependency.
290    pub fn package_name(&self) -> &PackageName {
291        match self {
292            Self::Wit { package, .. } => &package.main.name,
293            Self::Wasm { decoded, .. } => &decoded.resolve().packages[decoded.package()].name,
294        }
295    }
296
297    /// Converts the decoded dependency into a component world.
298    ///
299    /// Returns an error if the dependency is not a decoded component.
300    pub fn into_component_world(self) -> Result<(Resolve, WorldId)> {
301        match self {
302            Self::Wasm {
303                decoded: DecodedWasm::Component(resolve, world),
304                ..
305            } => Ok((resolve, world)),
306            _ => bail!("dependency is not a WebAssembly component"),
307        }
308    }
309}
310
311/// Used to resolve dependencies for a WIT package.
312pub struct DependencyResolver<'a> {
313    client: CachingClient<FileCache>,
314    lock_file: Option<&'a LockFile>,
315    packages: HashMap<PackageRef, Vec<VersionInfo>>,
316    dependencies: HashMap<PackageRef, RegistryDependency>,
317    resolutions: DependencyResolutionMap,
318}
319
320impl<'a> DependencyResolver<'a> {
321    /// Creates a new dependency resolver. If `config` is `None`, then the resolver will be set to
322    /// offline mode and a lock file must be given as well. Anything that will require network
323    /// access will fail in offline mode.
324    pub fn new(
325        config: Option<Config>,
326        lock_file: Option<&'a LockFile>,
327        cache: FileCache,
328    ) -> anyhow::Result<Self> {
329        if config.is_none() && lock_file.is_none() {
330            anyhow::bail!("lock file must be provided when offline mode is enabled");
331        }
332        let client = CachingClient::new(config.map(Client::new), cache);
333        Ok(DependencyResolver {
334            client,
335            lock_file,
336            resolutions: Default::default(),
337            packages: Default::default(),
338            dependencies: Default::default(),
339        })
340    }
341
342    /// Creates a new dependency resolver with the given client. This is useful when you already
343    /// have a client available. If the client is set to offline mode, then a lock file must be
344    /// given or this will error
345    pub fn new_with_client(
346        client: CachingClient<FileCache>,
347        lock_file: Option<&'a LockFile>,
348    ) -> anyhow::Result<Self> {
349        if client.is_readonly() && lock_file.is_none() {
350            anyhow::bail!("lock file must be provided when offline mode is enabled");
351        }
352        Ok(DependencyResolver {
353            client,
354            lock_file,
355            resolutions: Default::default(),
356            packages: Default::default(),
357            dependencies: Default::default(),
358        })
359    }
360
361    /// Add a dependency to the resolver. If the dependency already exists, then it will be ignored.
362    /// To override an existing dependency, use [`override_dependency`](Self::override_dependency).
363    pub async fn add_dependency(
364        &mut self,
365        name: &PackageRef,
366        dependency: &Dependency,
367    ) -> Result<()> {
368        self.add_dependency_internal(name, dependency, false).await
369    }
370
371    /// Add a dependency to the resolver. If the dependency already exists, then it will be
372    /// overridden.
373    pub async fn override_dependency(
374        &mut self,
375        name: &PackageRef,
376        dependency: &Dependency,
377    ) -> Result<()> {
378        self.add_dependency_internal(name, dependency, true).await
379    }
380
381    async fn add_dependency_internal(
382        &mut self,
383        name: &PackageRef,
384        dependency: &Dependency,
385        force_override: bool,
386    ) -> Result<()> {
387        match dependency {
388            Dependency::Package(package) => {
389                // Dependency comes from a registry, add a dependency to the resolver
390                let registry_name = package.registry.as_deref().or_else(|| {
391                    self.client.client().ok().and_then(|client| {
392                        client
393                            .config()
394                            .resolve_registry(name)
395                            .map(|reg| reg.as_ref())
396                    })
397                });
398                let package_name = package.name.clone().unwrap_or_else(|| name.clone());
399
400                // Resolve the version from the lock file if there is one
401                let locked = match self.lock_file.as_ref().and_then(|resolver| {
402                    resolver
403                        .resolve(registry_name, &package_name, &package.version)
404                        .transpose()
405                }) {
406                    Some(Ok(locked)) => Some(locked),
407                    Some(Err(e)) => return Err(e),
408                    _ => None,
409                };
410
411                // So if it wasn't already fetched first? then we'll try and resolve it later, and the override
412                // is not present there for some reason
413                if !force_override
414                    && (self.resolutions.contains_key(name) || self.dependencies.contains_key(name))
415                {
416                    tracing::debug!(%name, "dependency already exists and override is not set, ignoring");
417                    return Ok(());
418                }
419                self.dependencies.insert(
420                    name.to_owned(),
421                    RegistryDependency {
422                        package: package_name,
423                        version: package.version.clone(),
424                        locked: locked.map(|l| (l.version.clone(), l.digest.clone())),
425                    },
426                );
427            }
428            Dependency::Local(p) => {
429                let res = DependencyResolution::Local(LocalResolution {
430                    name: name.clone(),
431                    path: p.clone(),
432                });
433
434                // This is a bit of a hack, but if there are multiple local dependencies that are
435                // nested and overriden, getting the packages from the local package treats _all_
436                // deps as registry deps. So if we're handling a local path and the dependencies
437                // have a registry package already, override it. Otherwise follow normal overrides.
438                // We should definitely fix this and change where we resolve these things
439                let should_insert = force_override
440                    || self.dependencies.contains_key(name)
441                    || !self.resolutions.contains_key(name);
442                if !should_insert {
443                    tracing::debug!(%name, "dependency already exists and registry override is not set, ignoring");
444                    return Ok(());
445                }
446
447                // Because we got here, we should remove anything from dependencies that is the same
448                // package because we're overriding with the local package. Technically we could be
449                // clever and just do this in the boolean above, but I'm paranoid
450                self.dependencies.remove(name);
451
452                // Now that we check we haven't already inserted this dep, get the packages from the
453                // local dependency and add those to the resolver before adding the dependency
454                let (_, packages) = get_packages(p)
455                    .context("Error getting dependent packages from local dependency")?;
456                Box::pin(self.add_packages(packages))
457                    .await
458                    .context("Error adding packages to resolver for local dependency")?;
459
460                let prev = self.resolutions.insert(name.clone(), res);
461                assert!(prev.is_none());
462            }
463        }
464
465        Ok(())
466    }
467
468    /// A helper function for adding an iterator of package refs and their associated version
469    /// requirements to the resolver
470    pub async fn add_packages(
471        &mut self,
472        packages: impl IntoIterator<Item = (PackageRef, VersionReq)>,
473    ) -> Result<()> {
474        for (package, req) in packages {
475            self.add_dependency(
476                &package,
477                &Dependency::Package(RegistryPackage {
478                    name: Some(package.clone()),
479                    version: req,
480                    registry: None,
481                }),
482            )
483            .await?;
484        }
485        Ok(())
486    }
487
488    /// Resolve all dependencies.
489    ///
490    /// This will download all dependencies that are not already present in client storage.
491    ///
492    /// Returns the dependency resolution map.
493    pub async fn resolve(mut self) -> Result<DependencyResolutionMap> {
494        let mut resolutions = self.resolutions;
495        for (name, dependency) in self.dependencies.into_iter() {
496            // We need to clone a handle to the client because we mutably borrow self below. Might
497            // be worth replacing the mutable borrow with a RwLock down the line.
498            let client = self.client.clone();
499
500            let (selected_version, digest) = if client.is_readonly() {
501                dependency
502                    .locked
503                    .as_ref()
504                    .map(|(ver, digest)| (ver, Some(digest)))
505                    .ok_or_else(|| {
506                        anyhow::anyhow!("Couldn't find locked dependency while in offline mode")
507                    })?
508            } else {
509                let versions =
510                    load_package(&mut self.packages, &self.client, dependency.package.clone())
511                        .await?
512                        .with_context(|| {
513                            format!(
514                                "package `{name}` was not found in component registry",
515                                name = dependency.package
516                            )
517                        })?;
518
519                match &dependency.locked {
520                    Some((version, digest)) => {
521                        // The dependency had a lock file entry, so attempt to do an exact match first
522                        let exact_req = VersionReq {
523                            comparators: vec![Comparator {
524                                op: Op::Exact,
525                                major: version.major,
526                                minor: Some(version.minor),
527                                patch: Some(version.patch),
528                                pre: version.pre.clone(),
529                            }],
530                        };
531
532                        // If an exact match can't be found, fallback to the latest release to satisfy
533                        // the version requirement; this can happen when packages are yanked. If we did
534                        // find an exact match, return the digest for comparison after fetching the
535                        // release
536                        find_latest_release(versions, &exact_req).map(|v| (&v.version, Some(digest))).or_else(|| find_latest_release(versions, &dependency.version).map(|v| (&v.version, None)))
537                    }
538                    None => find_latest_release(versions, &dependency.version).map(|v| (&v.version, None)),
539                }.with_context(|| format!("component registry package `{name}` has no release matching version requirement `{version}`", name = dependency.package, version = dependency.version))?
540            };
541
542            // We need to clone a handle to the client because we mutably borrow self above. Might
543            // be worth replacing the mutable borrow with a RwLock down the line.
544            let release = client
545                .get_release(&dependency.package, selected_version)
546                .await?;
547            if let Some(digest) = digest {
548                if &release.content_digest != digest {
549                    bail!(
550                        "component registry package `{name}` (v`{version}`) has digest `{content}` but the lock file specifies digest `{digest}`",
551                        name = dependency.package,
552                        version = release.version,
553                        content = release.content_digest,
554                    );
555                }
556            }
557            let resolution = RegistryResolution {
558                name: name.clone(),
559                package: dependency.package.clone(),
560                registry: self.client.client().ok().and_then(|client| {
561                    client
562                        .config()
563                        .resolve_registry(&name)
564                        .map(ToString::to_string)
565                }),
566                requirement: dependency.version.clone(),
567                version: release.version.clone(),
568                digest: release.content_digest.clone(),
569                client: self.client.clone(),
570            };
571            resolutions.insert(name, DependencyResolution::Registry(resolution));
572        }
573
574        Ok(resolutions)
575    }
576}
577
578async fn load_package<'b>(
579    packages: &'b mut HashMap<PackageRef, Vec<VersionInfo>>,
580    client: &CachingClient<FileCache>,
581    package: PackageRef,
582) -> Result<Option<&'b Vec<VersionInfo>>> {
583    match packages.entry(package) {
584        hash_map::Entry::Occupied(e) => Ok(Some(e.into_mut())),
585        hash_map::Entry::Vacant(e) => match client.list_all_versions(e.key()).await {
586            Ok(p) => Ok(Some(e.insert(p))),
587            Err(WasmPkgError::PackageNotFound) => Ok(None),
588            Err(err) => Err(err.into()),
589        },
590    }
591}
592
593#[derive(Debug)]
594struct RegistryDependency {
595    /// The canonical package name of the registry package. In most cases, this is the same as the
596    /// name but could be different if the given package name has been remapped
597    package: PackageRef,
598    version: VersionReq,
599    locked: Option<(Version, ContentDigest)>,
600}
601
602fn find_latest_release<'a>(
603    versions: &'a [VersionInfo],
604    req: &VersionReq,
605) -> Option<&'a VersionInfo> {
606    versions
607        .iter()
608        .filter(|info| !info.yanked && req.matches(&info.version))
609        .max_by(|a, b| a.version.cmp(&b.version))
610}
611
612// NOTE(thomastaylor312): This is copied from the old wit package in the cargo-component and broken
613// out for some reuse. I don't know enough about resolvers to know if there is an easier way to
614// write this, so any future people seeing this should feel free to refactor it if they know a
615// better way to do it.
616
617/// Represents a map of dependency resolutions.
618///
619/// The key to the map is the package name of the dependency.
620#[derive(Debug, Clone, Default)]
621pub struct DependencyResolutionMap(HashMap<PackageRef, DependencyResolution>);
622
623impl AsRef<HashMap<PackageRef, DependencyResolution>> for DependencyResolutionMap {
624    fn as_ref(&self) -> &HashMap<PackageRef, DependencyResolution> {
625        &self.0
626    }
627}
628
629impl Deref for DependencyResolutionMap {
630    type Target = HashMap<PackageRef, DependencyResolution>;
631
632    fn deref(&self) -> &Self::Target {
633        &self.0
634    }
635}
636
637impl DerefMut for DependencyResolutionMap {
638    fn deref_mut(&mut self) -> &mut Self::Target {
639        &mut self.0
640    }
641}
642
643impl DependencyResolutionMap {
644    /// Fetch all dependencies and ensure there are no circular dependencies. Returns the decoded
645    /// dependencies (sorted topologically), ready to use for output or adding to a [`Resolve`].
646    pub async fn decode_dependencies(
647        &self,
648    ) -> Result<IndexMap<PackageName, DecodedDependency<'_>>> {
649        // Start by decoding all of the dependencies
650        let mut deps = IndexMap::new();
651        for (name, resolution) in self.0.iter() {
652            let decoded = resolution.decode().await?;
653            if let Some(prev) = deps.insert(decoded.package_name().clone(), decoded) {
654                anyhow::bail!(
655                    "duplicate definitions of package `{prev}` found while decoding dependency `{name}`",
656                    prev = prev.package_name()
657                );
658            }
659        }
660
661        // Do a topological sort of the dependencies
662        let mut order = IndexSet::new();
663        let mut visiting = HashSet::new();
664        for dep in deps.values() {
665            visit(dep, &deps, &mut order, &mut visiting)?;
666        }
667
668        assert!(visiting.is_empty());
669
670        // Now that we have the right order, re-order the dependencies to match
671        deps.sort_by(|name_a, _, name_b, _| {
672            order.get_index_of(name_a).cmp(&order.get_index_of(name_b))
673        });
674
675        Ok(deps)
676    }
677
678    /// Given a path to a component or a directory containing wit, use the given dependencies to
679    /// generate a [`Resolve`] for the root package.
680    pub async fn generate_resolve(&self, dir: impl AsRef<Path>) -> Result<(Resolve, PackageId)> {
681        let mut merged = Resolve {
682            // Retain @unstable features; downstream tooling will process them further
683            all_features: true,
684            ..Resolve::default()
685        };
686
687        let deps = self.decode_dependencies().await?;
688
689        // Parse the root package itself
690        let root = UnresolvedPackageGroup::parse_dir(&dir).with_context(|| {
691            format!(
692                "failed to parse package from directory `{dir}`",
693                dir = dir.as_ref().display()
694            )
695        })?;
696
697        let mut source_files: Vec<_> = root
698            .source_map
699            .source_files()
700            .map(Path::to_path_buf)
701            .collect();
702
703        // Merge all of the dependencies first
704        for decoded in deps.into_values() {
705            match decoded {
706                DecodedDependency::Wit {
707                    resolution,
708                    package,
709                } => {
710                    source_files.extend(package.source_map.source_files().map(Path::to_path_buf));
711                    merged.push_group(package).with_context(|| {
712                        format!(
713                            "failed to merge dependency `{name}`",
714                            name = resolution.name()
715                        )
716                    })?;
717                }
718                DecodedDependency::Wasm {
719                    resolution,
720                    decoded,
721                } => {
722                    let resolve = match decoded {
723                        DecodedWasm::WitPackage(resolve, _) => resolve,
724                        DecodedWasm::Component(resolve, _) => resolve,
725                    };
726
727                    merged.merge(resolve).with_context(|| {
728                        format!(
729                            "failed to merge world of dependency `{name}`",
730                            name = resolution.name()
731                        )
732                    })?;
733                }
734            };
735        }
736
737        let package = merged.push_group(root).with_context(|| {
738            format!(
739                "failed to merge package from directory `{dir}`",
740                dir = dir.as_ref().display()
741            )
742        })?;
743
744        Ok((merged, package))
745    }
746}
747
748fn visit<'a>(
749    dep: &'a DecodedDependency<'a>,
750    deps: &'a IndexMap<PackageName, DecodedDependency>,
751    order: &mut IndexSet<PackageName>,
752    visiting: &mut HashSet<&'a PackageName>,
753) -> Result<()> {
754    if order.contains(dep.package_name()) {
755        return Ok(());
756    }
757
758    // Visit any unresolved foreign dependencies
759    match dep {
760        DecodedDependency::Wit {
761            package,
762            resolution,
763        } => {
764            for name in package.main.foreign_deps.keys() {
765                // Only visit known dependencies
766                // wit-parser will error on unknown foreign dependencies when
767                // the package is resolved
768                if let Some(dep) = deps.get(name) {
769                    if !visiting.insert(name) {
770                        anyhow::bail!("foreign dependency `{name}` forms a dependency cycle while parsing dependency `{other}`", other = resolution.name());
771                    }
772
773                    visit(dep, deps, order, visiting)?;
774                    assert!(visiting.remove(name));
775                }
776            }
777        }
778        DecodedDependency::Wasm {
779            decoded,
780            resolution,
781        } => {
782            // Look for foreign packages in the decoded dependency
783            for (_, package) in &decoded.resolve().packages {
784                if package.name.namespace == dep.package_name().namespace
785                    && package.name.name == dep.package_name().name
786                {
787                    continue;
788                }
789
790                if let Some(dep) = deps.get(&package.name) {
791                    if !visiting.insert(&package.name) {
792                        anyhow::bail!("foreign dependency `{name}` forms a dependency cycle while parsing dependency `{other}`", name = package.name, other = resolution.name());
793                    }
794
795                    visit(dep, deps, order, visiting)?;
796                    assert!(visiting.remove(&package.name));
797                }
798            }
799        }
800    }
801
802    assert!(order.insert(dep.package_name().clone()));
803
804    Ok(())
805}