Skip to main content

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::{BTreeSet, HashMap, HashSet, hash_map},
6    fmt::Debug,
7    ops::{Deref, DerefMut},
8    path::{Path, PathBuf},
9    str::FromStr,
10};
11
12use anyhow::{Context, Result, bail};
13use futures_util::TryStreamExt;
14use indexmap::{IndexMap, IndexSet};
15use petgraph::{Direction, acyclic::Acyclic, graph::NodeIndex, stable_graph::StableDiGraph};
16use semver::{Comparator, Op, Version, VersionReq};
17use tokio::io::{AsyncRead, AsyncReadExt};
18use wasm_pkg_client::{
19    Client, Config, ContentDigest, Error as WasmPkgError, PackageRef, Release, VersionInfo,
20    caching::{CachingClient, FileCache},
21};
22use wasm_pkg_common::package::PackageSpec;
23use wit_component::DecodedWasm;
24use wit_parser::{PackageId, PackageName, Resolve, UnresolvedPackageGroup, WorldId};
25
26use crate::{
27    lock::LockFile,
28    wit::{get_local_dependencies, get_packages},
29};
30
31/// The name of the default registry.
32pub const DEFAULT_REGISTRY_NAME: &str = "default";
33
34// TODO: functions for resolving dependencies from a lock file
35
36/// Represents a WIT package dependency.
37#[derive(Debug, Clone)]
38pub enum Dependency {
39    /// The dependency is a registry package.
40    Package(RegistryPackage),
41
42    /// The dependency is a path to a local directory or file.
43    Local(PathBuf),
44}
45
46impl std::fmt::Display for Dependency {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match self {
49            Dependency::Package(RegistryPackage {
50                name,
51                version,
52                registry,
53            }) => {
54                let registry = registry.as_deref().unwrap_or("_");
55                let name = name.as_ref().map(|n| n.to_string());
56
57                write!(
58                    f,
59                    "{{registry=\"{registry}\" package=\"{}@{version}\"}}",
60                    name.as_deref().unwrap_or("_:_"),
61                )
62            }
63            Dependency::Local(path_buf) => write!(f, "{}", path_buf.display()),
64        }
65    }
66}
67
68impl FromStr for Dependency {
69    type Err = anyhow::Error;
70
71    fn from_str(s: &str) -> Result<Self> {
72        Ok(Self::Package(s.parse()?))
73    }
74}
75
76/// Represents a reference to a registry package.
77#[derive(Debug, Clone)]
78pub struct RegistryPackage {
79    /// The name of the package.
80    ///
81    /// If not specified, the name from the mapping will be used.
82    pub name: Option<PackageRef>,
83
84    /// The version requirement of the package.
85    pub version: VersionReq,
86
87    /// The name of the component registry containing the package.
88    ///
89    /// If not specified, the default registry is used.
90    pub registry: Option<String>,
91}
92
93impl FromStr for RegistryPackage {
94    type Err = anyhow::Error;
95
96    fn from_str(s: &str) -> Result<Self> {
97        Ok(Self {
98            name: None,
99            version: s
100                .parse()
101                .with_context(|| format!("'{s}' is an invalid registry package version"))?,
102            registry: None,
103        })
104    }
105}
106
107/// Represents information about a resolution of a registry package.
108#[derive(Clone)]
109pub struct RegistryResolution {
110    /// The name of the dependency that was resolved.
111    ///
112    /// This may differ from `package` if the dependency was renamed.
113    pub name: PackageRef,
114    /// The name of the package from the registry that was resolved.
115    pub package: PackageRef,
116    /// The name of the registry used to resolve the package if one was provided
117    pub registry: Option<String>,
118    /// The version requirement that was used to resolve the package.
119    pub requirement: VersionReq,
120    /// The package version that was resolved.
121    pub version: Version,
122    /// The digest of the package contents.
123    pub digest: ContentDigest,
124    /// The client to use for fetching the package contents.
125    client: CachingClient<FileCache>,
126}
127
128impl Debug for RegistryResolution {
129    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
130        f.debug_struct("RegistryResolution")
131            .field("name", &self.name)
132            .field("package", &self.package)
133            .field("registry", &self.registry)
134            .field("requirement", &self.requirement)
135            .field("version", &self.version)
136            .field("digest", &self.digest)
137            .finish()
138    }
139}
140
141impl RegistryResolution {
142    /// Fetches the raw package bytes from the registry. Returns an AsyncRead that will stream the
143    /// package contents
144    pub async fn fetch(&self) -> Result<impl AsyncRead> {
145        let stream = self
146            .client
147            .get_content(
148                &self.package,
149                &Release {
150                    version: self.version.clone(),
151                    content_digest: self.digest.clone(),
152                },
153            )
154            .await?;
155
156        Ok(tokio_util::io::StreamReader::new(
157            stream.map_err(std::io::Error::other),
158        ))
159    }
160}
161
162/// Represents information about a resolution of a local file.
163#[derive(Clone, Debug)]
164pub struct LocalResolution {
165    /// The name of the dependency that was resolved.
166    pub name: PackageRef,
167    /// The path to the resolved dependency.
168    pub path: PathBuf,
169}
170
171/// Represents a resolution of a dependency.
172#[derive(Debug, Clone)]
173#[allow(clippy::large_enum_variant)]
174pub enum DependencyResolution {
175    /// The dependency is resolved from a registry package.
176    Registry(RegistryResolution),
177    /// The dependency is resolved from a local path.
178    Local(LocalResolution),
179}
180
181impl DependencyResolution {
182    /// Gets the name of the dependency that was resolved.
183    pub fn name(&self) -> &PackageRef {
184        match self {
185            Self::Registry(res) => &res.name,
186            Self::Local(res) => &res.name,
187        }
188    }
189
190    /// Gets the resolved version.
191    ///
192    /// Returns `None` if the dependency is not resolved from a registry package.
193    pub fn version(&self) -> Option<&Version> {
194        match self {
195            Self::Registry(res) => Some(&res.version),
196            Self::Local(_) => None,
197        }
198    }
199
200    /// The key used in sorting and searching the lock file package list.
201    ///
202    /// Returns `None` if the dependency is not resolved from a registry package.
203    pub fn key(&self) -> Option<(&PackageRef, Option<&str>)> {
204        match self {
205            DependencyResolution::Registry(pkg) => Some((&pkg.package, pkg.registry.as_deref())),
206            DependencyResolution::Local(_) => None,
207        }
208    }
209
210    /// Decodes the resolved dependency.
211    pub async fn decode(&self) -> Result<DecodedDependency<'_>> {
212        // If the dependency path is a directory, assume it contains wit to parse as a package.
213        let bytes = match self {
214            DependencyResolution::Local(LocalResolution { path, .. })
215                if tokio::fs::metadata(path).await?.is_dir() =>
216            {
217                return Ok(DecodedDependency::Wit {
218                    resolution: self,
219                    package: UnresolvedPackageGroup::parse_dir(path).with_context(|| {
220                        format!("failed to parse dependency `{path}`", path = path.display())
221                    })?,
222                });
223            }
224            DependencyResolution::Local(LocalResolution { path, .. }) => {
225                tokio::fs::read(path).await.with_context(|| {
226                    format!(
227                        "failed to read content of dependency `{name}` at path `{path}`",
228                        name = self.name(),
229                        path = path.display()
230                    )
231                })?
232            }
233            DependencyResolution::Registry(res) => {
234                let mut reader = res.fetch().await?;
235
236                let mut buf = Vec::new();
237                reader.read_to_end(&mut buf).await?;
238                buf
239            }
240        };
241
242        if &bytes[0..4] != b"\0asm" {
243            let package = UnresolvedPackageGroup::parse(
244                // This is fake, but it's needed for the parser to work.
245                self.name().to_string(),
246                std::str::from_utf8(&bytes).with_context(|| {
247                    format!(
248                        "dependency `{name}` is not UTF-8 encoded",
249                        name = self.name()
250                    )
251                })?,
252            )
253            .map_err(|(src_map, parse_err)| {
254                anyhow::format_err!(
255                    "failed to parse package group: {}",
256                    parse_err.render(&src_map)
257                )
258            })?;
259
260            return Ok(DecodedDependency::Wit {
261                resolution: self,
262                package,
263            });
264        }
265
266        Ok(DecodedDependency::Wasm {
267            resolution: self,
268            decoded: wit_component::decode(&bytes).with_context(|| {
269                format!(
270                    "failed to decode content of dependency `{name}`",
271                    name = self.name(),
272                )
273            })?,
274        })
275    }
276}
277
278/// Represents a decoded dependency.
279pub enum DecodedDependency<'a> {
280    /// The dependency decoded from an unresolved WIT package.
281    Wit {
282        /// The resolution related to the decoded dependency.
283        resolution: &'a DependencyResolution,
284        /// The unresolved WIT package.
285        package: UnresolvedPackageGroup,
286    },
287    /// The dependency decoded from a Wasm file.
288    Wasm {
289        /// The resolution related to the decoded dependency.
290        resolution: &'a DependencyResolution,
291        /// The decoded Wasm file.
292        decoded: DecodedWasm,
293    },
294}
295
296impl DecodedDependency<'_> {
297    /// Fully resolves the dependency.
298    ///
299    /// If the dependency is an unresolved WIT package, it will assume that the
300    /// package has no foreign dependencies.
301    pub fn resolve(self) -> Result<(Resolve, PackageId, Vec<PathBuf>)> {
302        match self {
303            Self::Wit { package, .. } => {
304                let mut resolve = Resolve::new();
305                resolve.all_features = true;
306                let source_files = package
307                    .source_map
308                    .source_files()
309                    .map(Path::to_path_buf)
310                    .collect();
311                let pkg = resolve.push_group(package)?;
312                Ok((resolve, pkg, source_files))
313            }
314            Self::Wasm { decoded, .. } => match decoded {
315                DecodedWasm::WitPackage(resolve, pkg) => Ok((resolve, pkg, Vec::new())),
316                DecodedWasm::Component(resolve, world) => {
317                    let pkg = resolve.worlds[world].package.unwrap();
318                    Ok((resolve, pkg, Vec::new()))
319                }
320            },
321        }
322    }
323
324    /// Gets the package name of the decoded dependency.
325    pub fn package_name(&self) -> &PackageName {
326        match self {
327            Self::Wit { package, .. } => &package.main.name,
328            Self::Wasm { decoded, .. } => &decoded.resolve().packages[decoded.package()].name,
329        }
330    }
331
332    /// Converts the decoded dependency into a component world.
333    ///
334    /// Returns an error if the dependency is not a decoded component.
335    pub fn into_component_world(self) -> Result<(Resolve, WorldId)> {
336        match self {
337            Self::Wasm {
338                decoded: DecodedWasm::Component(resolve, world),
339                ..
340            } => Ok((resolve, world)),
341            _ => bail!("dependency is not a WebAssembly component"),
342        }
343    }
344}
345
346/// Used to resolve dependencies for a WIT package.
347pub struct DependencyResolver<'a> {
348    client: CachingClient<FileCache>,
349    lock_file: Option<&'a LockFile>,
350    packages: HashMap<PackageRef, Vec<VersionInfo>>,
351    dependencies: HashMap<PackageRef, RegistryDependency>,
352    resolutions: DependencyResolutionMap,
353}
354
355impl<'a> DependencyResolver<'a> {
356    /// Creates a new dependency resolver. If [`Config`] is `None`, then the resolver will be set to
357    /// offline mode and a lock file must be given as well. Anything that will require network
358    /// access will fail in offline mode.
359    pub fn new(
360        config: Option<Config>,
361        lock_file: Option<&'a LockFile>,
362        cache: FileCache,
363    ) -> anyhow::Result<Self> {
364        if config.is_none() && lock_file.is_none() {
365            anyhow::bail!("lock file must be provided when offline mode is enabled");
366        }
367        let client = CachingClient::new(config.map(Client::new), cache);
368        Ok(DependencyResolver {
369            client,
370            lock_file,
371            resolutions: Default::default(),
372            packages: Default::default(),
373            dependencies: Default::default(),
374        })
375    }
376
377    /// Creates a new dependency resolver with the given client. This is useful when you already
378    /// have a client available. If the client is set to offline mode, then a lock file must be
379    /// given or this will error
380    pub fn new_with_client(
381        client: CachingClient<FileCache>,
382        lock_file: Option<&'a LockFile>,
383    ) -> anyhow::Result<Self> {
384        if client.is_readonly() && lock_file.is_none() {
385            anyhow::bail!("lock file must be provided when offline mode is enabled");
386        }
387        Ok(DependencyResolver {
388            client,
389            lock_file,
390            resolutions: Default::default(),
391            packages: Default::default(),
392            dependencies: Default::default(),
393        })
394    }
395
396    /// Add a dependency to the resolver. If the dependency already exists, then it will be ignored.
397    /// To override an existing dependency, use [`override_dependency`](Self::override_dependency).
398    pub async fn add_dependency(
399        &mut self,
400        name: &PackageRef,
401        dependency: &Dependency,
402    ) -> Result<()> {
403        self.add_dependency_internal(name, dependency, false).await
404    }
405
406    /// Add a dependency to the resolver. If the dependency already exists, then it will be
407    /// overridden.
408    pub async fn override_dependency(
409        &mut self,
410        name: &PackageRef,
411        dependency: &Dependency,
412    ) -> Result<()> {
413        self.add_dependency_internal(name, dependency, true).await
414    }
415
416    async fn add_dependency_internal(
417        &mut self,
418        name: &PackageRef,
419        dependency: &Dependency,
420        force_override: bool,
421    ) -> Result<()> {
422        match dependency {
423            Dependency::Package(package) => {
424                // Dependency comes from a registry, add a dependency to the resolver
425                let registry_name = package.registry.as_deref().or_else(|| {
426                    self.client.client().ok().and_then(|client| {
427                        client
428                            .config()
429                            .resolve_registry(name)
430                            .map(|reg| reg.as_ref())
431                    })
432                });
433                let package_name = package.name.clone().unwrap_or_else(|| name.clone());
434
435                // Resolve the version from the lock file if there is one
436                let locked = match self.lock_file.as_ref().and_then(|resolver| {
437                    resolver
438                        .resolve(registry_name, &package_name, &package.version)
439                        .transpose()
440                }) {
441                    Some(Ok(locked)) => Some(locked),
442                    Some(Err(e)) => return Err(e),
443                    _ => None,
444                };
445
446                // So if it wasn't already fetched first? then we'll try and resolve it later, and the override
447                // is not present there for some reason
448                if !force_override
449                    && (self.resolutions.contains_key(name) || self.dependencies.contains_key(name))
450                {
451                    tracing::debug!(%name, %dependency, "dependency already exists and override is not set, ignoring");
452                    return Ok(());
453                }
454                self.dependencies.insert(
455                    name.to_owned(),
456                    RegistryDependency {
457                        package: package_name,
458                        version: package.version.clone(),
459                        locked: locked.map(|l| (l.version.clone(), l.digest.clone())),
460                    },
461                );
462            }
463            Dependency::Local(p) => {
464                let res = DependencyResolution::Local(LocalResolution {
465                    name: name.clone(),
466                    path: p.clone(),
467                });
468
469                // This is a bit of a hack, but if there are multiple local dependencies that are
470                // nested and overridden, getting the packages from the local package treats _all_
471                // deps as registry deps. So if we're handling a local path and the dependencies
472                // have a registry package already, override it. Otherwise follow normal overrides.
473                // We should definitely fix this and change where we resolve these things
474                let should_insert = force_override
475                    || self.dependencies.contains_key(name)
476                    || !self.resolutions.contains_key(name);
477                if !should_insert {
478                    tracing::debug!(%name, "dependency already exists and registry override is not set, ignoring");
479                    return Ok(());
480                }
481
482                // Because we got here, we should remove anything from dependencies that is the same
483                // package because we're overriding with the local package. Technically we could be
484                // clever and just do this in the boolean above, but I'm paranoid
485                self.dependencies.remove(name);
486
487                // Now that we check we haven't already inserted this dep, get the packages from the
488                // local dependency and add those to the resolver before adding the dependency
489                let (_, packages) = get_packages(p)
490                    .context("Error getting dependent packages from local dependency")?;
491                Box::pin(self.add_packages(packages))
492                    .await
493                    .context("Error adding packages to resolver for local dependency")?;
494
495                let prev = self.resolutions.insert(name.clone(), res);
496                assert!(prev.is_none());
497            }
498        }
499
500        Ok(())
501    }
502
503    /// A helper function for adding an iterator of package refs and their associated version
504    /// requirements to the resolver
505    pub async fn add_packages(
506        &mut self,
507        packages: impl IntoIterator<Item = (PackageRef, VersionReq)>,
508    ) -> Result<()> {
509        for (package, req) in packages {
510            self.add_dependency(
511                &package,
512                &Dependency::Package(RegistryPackage {
513                    name: Some(package.clone()),
514                    version: req,
515                    registry: None,
516                }),
517            )
518            .await?;
519        }
520        Ok(())
521    }
522
523    /// Resolve all dependencies.
524    ///
525    /// This will download all dependencies that are not already present in client storage.
526    ///
527    /// Returns the dependency resolution map.
528    pub async fn resolve(mut self) -> Result<DependencyResolutionMap> {
529        let mut resolutions = self.resolutions;
530        for (name, dependency) in self.dependencies.into_iter() {
531            // We need to clone a handle to the client because we mutably borrow self below. Might
532            // be worth replacing the mutable borrow with a RwLock down the line.
533            let client = self.client.clone();
534
535            let (selected_version, digest) = if client.is_readonly() {
536                dependency
537                    .locked
538                    .as_ref()
539                    .map(|(ver, digest)| (ver, Some(digest)))
540                    .ok_or_else(|| {
541                        anyhow::anyhow!("Couldn't find locked dependency while in offline mode")
542                    })?
543            } else {
544                let versions =
545                    load_package(&mut self.packages, &self.client, dependency.package.clone())
546                        .await
547                        .with_context(|| format!("package: {}", dependency.package.clone()))?
548                        .with_context(|| {
549                            format!(
550                                "package `{name}` was not found in component registry",
551                                name = dependency.package
552                            )
553                        })?;
554
555                match &dependency.locked {
556                    Some((version, digest)) => {
557                        // The dependency had a lock file entry, so attempt to do an exact match first
558                        let exact_req = VersionReq {
559                            comparators: vec![Comparator {
560                                op: Op::Exact,
561                                major: version.major,
562                                minor: Some(version.minor),
563                                patch: Some(version.patch),
564                                pre: version.pre.clone(),
565                            }],
566                        };
567
568                        // If an exact match can't be found, fallback to the latest release to satisfy
569                        // the version requirement; this can happen when packages are yanked. If we did
570                        // find an exact match, return the digest for comparison after fetching the
571                        // release
572                        find_latest_release(versions, &exact_req)
573                            .map(|v| (&v.version, Some(digest)))
574                            .or_else(|| find_latest_release(versions, &dependency.version).map(|v| (&v.version, None)))
575                        }
576                    None => find_latest_release(versions, &dependency.version).map(|v| (&v.version, None)),
577                }.with_context(||
578                    format!(
579                        "component registry package `{name}` has no release matching version requirement `{version}`",
580                        name = dependency.package,
581                        version = dependency.version
582                    )
583                )?
584            };
585
586            // We need to clone a handle to the client because we mutably borrow self above. Might
587            // be worth replacing the mutable borrow with a RwLock down the line.
588            let release = client
589                .get_release(&dependency.package, selected_version)
590                .await?;
591            if let Some(digest) = digest
592                && &release.content_digest != digest
593            {
594                bail!(
595                    "component registry package `{name}` (v`{version}`) has digest `{content}` but the lock file specifies digest `{digest}`",
596                    name = dependency.package,
597                    version = release.version,
598                    content = release.content_digest,
599                );
600            }
601            let resolution = RegistryResolution {
602                name: name.clone(),
603                package: dependency.package.clone(),
604                registry: self.client.client().ok().and_then(|client| {
605                    client
606                        .config()
607                        .resolve_registry(&name)
608                        .map(ToString::to_string)
609                }),
610                requirement: dependency.version.clone(),
611                version: release.version.clone(),
612                digest: release.content_digest.clone(),
613                client: self.client.clone(),
614            };
615            resolutions.insert(name, DependencyResolution::Registry(resolution));
616        }
617
618        Ok(resolutions)
619    }
620}
621
622async fn load_package<'b>(
623    packages: &'b mut HashMap<PackageRef, Vec<VersionInfo>>,
624    client: &CachingClient<FileCache>,
625    package: PackageRef,
626) -> Result<Option<&'b Vec<VersionInfo>>> {
627    match packages.entry(package) {
628        hash_map::Entry::Occupied(e) => Ok(Some(e.into_mut())),
629        hash_map::Entry::Vacant(e) => match client.list_all_versions(e.key()).await {
630            Ok(p) => Ok(Some(e.insert(p))),
631            Err(WasmPkgError::PackageNotFound) => Ok(None),
632            Err(err) => Err(err.into()),
633        },
634    }
635}
636
637#[derive(Debug)]
638struct RegistryDependency {
639    /// The canonical package name of the registry package. In most cases, this is the same as the
640    /// name but could be different if the given package name has been remapped
641    package: PackageRef,
642    version: VersionReq,
643    locked: Option<(Version, ContentDigest)>,
644}
645
646fn find_latest_release<'a>(
647    versions: &'a [VersionInfo],
648    req: &VersionReq,
649) -> Option<&'a VersionInfo> {
650    versions
651        .iter()
652        .filter(|info| !info.yanked && req.matches(&info.version))
653        .max_by(|a, b| a.version.cmp(&b.version))
654}
655
656// NOTE(thomastaylor312): This is copied from the old wit package in the cargo-component and broken
657// out for some reuse. I don't know enough about resolvers to know if there is an easier way to
658// write this, so any future people seeing this should feel free to refactor it if they know a
659// better way to do it.
660
661/// Represents a map of dependency resolutions.
662///
663/// The key to the map is the package name of the dependency.
664#[derive(Debug, Clone, Default)]
665pub struct DependencyResolutionMap(HashMap<PackageRef, DependencyResolution>);
666
667impl AsRef<HashMap<PackageRef, DependencyResolution>> for DependencyResolutionMap {
668    fn as_ref(&self) -> &HashMap<PackageRef, DependencyResolution> {
669        &self.0
670    }
671}
672
673impl Deref for DependencyResolutionMap {
674    type Target = HashMap<PackageRef, DependencyResolution>;
675
676    fn deref(&self) -> &Self::Target {
677        &self.0
678    }
679}
680
681impl DerefMut for DependencyResolutionMap {
682    fn deref_mut(&mut self) -> &mut Self::Target {
683        &mut self.0
684    }
685}
686
687impl DependencyResolutionMap {
688    /// Fetch all dependencies and ensure there are no circular dependencies. Returns the decoded
689    /// dependencies (sorted topologically), ready to use for output or adding to a [`Resolve`].
690    pub async fn decode_dependencies(
691        &self,
692    ) -> Result<IndexMap<PackageName, DecodedDependency<'_>>> {
693        // Start by decoding all of the dependencies
694        let mut deps = IndexMap::new();
695        for (name, resolution) in self.0.iter() {
696            let decoded = resolution.decode().await?;
697            if let Some(prev) = deps.insert(decoded.package_name().clone(), decoded) {
698                anyhow::bail!(
699                    "duplicate definitions of package `{prev}` found while decoding dependency `{name}`",
700                    prev = prev.package_name()
701                );
702            }
703        }
704
705        // Do a topological sort of the dependencies
706        let mut order = IndexSet::new();
707        let mut visiting = HashSet::new();
708        for dep in deps.values() {
709            visit(dep, &deps, &mut order, &mut visiting)?;
710        }
711
712        assert!(visiting.is_empty());
713
714        // Now that we have the right order, re-order the dependencies to match
715        deps.sort_by(|name_a, _, name_b, _| {
716            order.get_index_of(name_a).cmp(&order.get_index_of(name_b))
717        });
718
719        Ok(deps)
720    }
721
722    /// Given a path to a component or a directory containing wit, use the given dependencies to
723    /// generate a [`Resolve`] for the root package.
724    pub async fn generate_resolve(&self, dir: impl AsRef<Path>) -> Result<(Resolve, PackageId)> {
725        let mut merged = Resolve {
726            // Retain @unstable features; downstream tooling will process them further
727            all_features: true,
728            ..Resolve::default()
729        };
730
731        let deps = self.decode_dependencies().await?;
732
733        // Parse the root package itself
734        let root = UnresolvedPackageGroup::parse_dir(&dir).with_context(|| {
735            format!(
736                "failed to parse package from directory `{dir}`",
737                dir = dir.as_ref().display()
738            )
739        })?;
740
741        let mut source_files: Vec<_> = root
742            .source_map
743            .source_files()
744            .map(Path::to_path_buf)
745            .collect();
746
747        // Merge all of the dependencies first
748        for decoded in deps.into_values() {
749            match decoded {
750                DecodedDependency::Wit {
751                    resolution,
752                    package,
753                } => {
754                    source_files.extend(package.source_map.source_files().map(Path::to_path_buf));
755                    merged.push_group(package).with_context(|| {
756                        format!(
757                            "failed to merge dependency `{name}`",
758                            name = resolution.name()
759                        )
760                    })?;
761                }
762                DecodedDependency::Wasm {
763                    resolution,
764                    decoded,
765                } => {
766                    let resolve = match decoded {
767                        DecodedWasm::WitPackage(resolve, _) => resolve,
768                        DecodedWasm::Component(resolve, _) => resolve,
769                    };
770
771                    merged.merge(resolve).with_context(|| {
772                        format!(
773                            "failed to merge world of dependency `{name}`",
774                            name = resolution.name()
775                        )
776                    })?;
777                }
778            };
779        }
780
781        let package = merged.push_group(root).with_context(|| {
782            format!(
783                "failed to merge package from directory `{dir}`",
784                dir = dir.as_ref().display()
785            )
786        })?;
787
788        Ok((merged, package))
789    }
790}
791
792fn visit<'a>(
793    dep: &'a DecodedDependency<'a>,
794    deps: &'a IndexMap<PackageName, DecodedDependency>,
795    order: &mut IndexSet<PackageName>,
796    visiting: &mut HashSet<&'a PackageName>,
797) -> Result<()> {
798    if order.contains(dep.package_name()) {
799        return Ok(());
800    }
801
802    // Visit any unresolved foreign dependencies
803    match dep {
804        DecodedDependency::Wit {
805            package,
806            resolution,
807        } => {
808            for name in package.main.foreign_deps.keys() {
809                // Only visit known dependencies
810                // wit-parser will error on unknown foreign dependencies when
811                // the package is resolved
812                if let Some(dep) = deps.get(name) {
813                    if !visiting.insert(name) {
814                        anyhow::bail!(
815                            "foreign dependency `{name}` forms a dependency cycle while parsing dependency `{other}`",
816                            other = resolution.name()
817                        );
818                    }
819
820                    visit(dep, deps, order, visiting)?;
821                    assert!(visiting.remove(name));
822                }
823            }
824        }
825        DecodedDependency::Wasm {
826            decoded,
827            resolution,
828        } => {
829            // Look for foreign packages in the decoded dependency
830            for (_, package) in &decoded.resolve().packages {
831                if package.name.namespace == dep.package_name().namespace
832                    && package.name.name == dep.package_name().name
833                {
834                    continue;
835                }
836
837                if let Some(dep) = deps.get(&package.name) {
838                    if !visiting.insert(&package.name) {
839                        anyhow::bail!(
840                            "foreign dependency `{name}` forms a dependency cycle while parsing dependency `{other}`",
841                            name = package.name,
842                            other = resolution.name()
843                        );
844                    }
845
846                    visit(dep, deps, order, visiting)?;
847                    assert!(visiting.remove(&package.name));
848                }
849            }
850        }
851    }
852
853    assert!(order.insert(dep.package_name().clone()));
854
855    Ok(())
856}
857
858/// Graph of publishable packages with the [`petgraph::Direction`] edges describing the dependency direction.
859pub type DependencyGraph<N> = Acyclic<StableDiGraph<N, petgraph::Direction>>;
860
861/// Mapping of [`PackageRef`]s to the respective index inside the dependency graph.
862pub type LocalPackageIndex = HashMap<PackageRef, (NodeIndex, PathBuf)>;
863
864/// State for tracking dependencies during upload.
865pub struct PublishPlan {
866    /// Graph of publishable packages where the edges are `(dependency -DependencyOf->) dependent)`
867    dependents: DependencyGraph<PackageSpec>,
868    // TODO look at using cargo's `InternedString` type for `PackageRef`:
869    // https://docs.rs/cargo/latest/cargo/util/interning/struct.InternedString.html
870    indices: LocalPackageIndex,
871}
872
873impl PublishPlan {
874    /// Generate [`Self`] from a list of WIT package paths (files or directories).
875    pub fn from_paths(paths: &[impl AsRef<Path>]) -> Result<Self> {
876        let (graph, indices) = get_local_dependencies(paths)?;
877        {
878            // collection of local packages that have no version
879            let missing_version = graph
880                .nodes_iter()
881                .map(|f| graph[f].clone())
882                .filter(|pkg| pkg.version.is_none())
883                .collect::<Vec<_>>();
884            if !missing_version.is_empty() {
885                return Err(anyhow::anyhow!(
886                    "Unable to publish packages without a version specified"
887                )
888                .context(format!(
889                    "packages: {}",
890                    package_list(missing_version.iter(), None)
891                )));
892            }
893        }
894        let mut dependents = graph.into_inner();
895
896        dependents.reverse();
897        // graph was already found to be acyclic
898        let dependents = DependencyGraph::try_from(dependents).unwrap();
899
900        Ok(Self {
901            dependents,
902            indices,
903        })
904    }
905
906    pub fn iter<'a>(&'a self) -> impl Iterator<Item = &'a PackageSpec> + 'a {
907        self.dependents
908            .nodes_iter()
909            .map(|id| &(self.dependents[id]))
910    }
911
912    pub fn is_empty(&self) -> bool {
913        self.indices.is_empty()
914    }
915
916    pub fn len(&self) -> usize {
917        self.indices.len()
918    }
919
920    /// Returns the set of packages that are ready for publishing (i.e. have no outstanding dependencies).
921    ///
922    /// These will not be returned in future calls.
923    pub fn take_ready(&self) -> BTreeSet<PackageSpec> {
924        self.dependents
925            .nodes_iter()
926            // there are no dependents on `self.dependents[id]`
927            .filter(|id| {
928                self.dependents
929                    .neighbors_directed(*id, Direction::Incoming)
930                    .count()
931                    == 0
932            })
933            .map(|id| self.dependents[id].clone())
934            .collect()
935    }
936
937    /// Return the path associated with a local package.
938    pub fn get_path(&self, pkg: &PackageRef) -> Option<&Path> {
939        self.indices.get(pkg).map(|(_, p)| p.as_ref())
940    }
941
942    /// Return the [`NodeIndex`] associated with a local package.
943    pub fn get_node_index(&self, pkg: &PackageRef) -> Option<NodeIndex> {
944        self.indices.get(pkg).map(|(id, _)| *id)
945    }
946
947    /// Packages confirmed to be available in the registry, potentially allowing additional
948    /// packages to be "ready".
949    pub fn mark_confirmed(&mut self, published: impl IntoIterator<Item = PackageSpec>) {
950        for spec in published {
951            let (id, _) = self
952                .indices
953                .remove(&spec.package)
954                .expect("PackageSpec has no associated index");
955            // NOTE: nodes without edges will return None here
956            self.dependents.remove_node(id);
957        }
958    }
959}
960
961impl std::fmt::Display for PublishPlan {
962    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
963        // TODO(mkatychev): handle with anstyle and anstream, passing in `anstream::AutoStream` for colour choice
964        for id in self.dependents.nodes_iter() {
965            let dep = &self.dependents[id];
966            // initial dependency graph visualization
967            let mut neighbors = self
968                .dependents
969                .neighbors_directed(id, Direction::Outgoing)
970                .peekable();
971
972            if neighbors.peek().is_none() {
973                // tracing::debug!("{dep} has no dependents");
974                writeln!(f, "[{dep} has no dependents]")?;
975            } else {
976                writeln!(f, "[{dep}]")?;
977            }
978
979            while let Some(id) = neighbors.next() {
980                let pkg = &self.dependents[id];
981                let separator = if neighbors.peek().is_some() {
982                    "├─"
983                } else {
984                    "╰─"
985                };
986
987                writeln!(f, "{separator}─▶ {pkg}")?;
988            }
989        }
990        Ok(())
991    }
992}
993
994/// Format a collection of packages as a list
995///
996/// e.g. "foo:a@0.1.0, bar:b@0.2.0, and baz:c@0.3.0".
997///
998/// Note: the final separator (e.g. "and" in the previous example) can be chosen.
999fn package_list<'a>(
1000    pkgs: impl IntoIterator<Item = &'a PackageSpec>,
1001    final_sep: Option<&str>,
1002) -> String {
1003    let final_sep = final_sep.unwrap_or("and");
1004    let mut names: Vec<_> = pkgs.into_iter().map(|pkg| pkg.to_string()).collect();
1005    names.sort();
1006
1007    match &names[..] {
1008        [] => String::new(),
1009        [a] => a.clone(),
1010        [a, b] => format!("{a} {final_sep} {b}"),
1011        [names @ .., last] => {
1012            format!("{}, {final_sep} {last}", names.join(", "))
1013        }
1014    }
1015}
1016
1017#[cfg(test)]
1018mod tests {
1019
1020    use std::path::PathBuf;
1021
1022    use super::*;
1023    use glob::glob;
1024
1025    fn transitive_local_paths() -> Vec<PathBuf> {
1026        let fixtures_root =
1027            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/transitive-local");
1028        // pass all fixture dirs to a single `wkg publish` invocation
1029        let mut paths: Vec<PathBuf> = glob(fixtures_root.join("**/*.wit").to_str().unwrap())
1030            .unwrap()
1031            .map(|p| p.expect("glob path error"))
1032            .map(|p| p.parent().unwrap().to_path_buf())
1033            .collect();
1034        paths.sort();
1035        paths.dedup();
1036        paths
1037    }
1038
1039    #[test]
1040    fn publish_plan_iter() {
1041        let paths = transitive_local_paths();
1042        let plan = PublishPlan::from_paths(&paths).unwrap();
1043        assert_eq!(
1044            plan.iter().count(),
1045            5,
1046            "unexpected package count\npackages found: {}",
1047            package_list(plan.iter(), None)
1048        );
1049    }
1050
1051    #[test]
1052    fn publish_plan_chunks() {
1053        let paths = transitive_local_paths();
1054        let mut plan = PublishPlan::from_paths(&paths).unwrap();
1055        let mut ready_for_publish = plan.take_ready();
1056        assert_eq!(
1057            ready_for_publish.iter().collect::<Vec<_>>(),
1058            ["example-c:nested@0.1.0", "example-d:foo@0.1.0",],
1059        );
1060        plan.mark_confirmed(ready_for_publish);
1061
1062        ready_for_publish = plan.take_ready();
1063        assert_eq!(
1064            ready_for_publish.iter().collect::<Vec<_>>(),
1065            ["example-c:baz@0.1.0"],
1066        );
1067        plan.mark_confirmed(ready_for_publish);
1068
1069        ready_for_publish = plan.take_ready();
1070        assert_eq!(
1071            ready_for_publish.iter().collect::<Vec<_>>(),
1072            ["example-b:bar@0.1.0"],
1073        );
1074        plan.mark_confirmed(ready_for_publish);
1075
1076        ready_for_publish = plan.take_ready();
1077        assert_eq!(
1078            ready_for_publish.iter().collect::<Vec<_>>(),
1079            ["example-a:foo@0.1.0"],
1080        );
1081        plan.mark_confirmed(ready_for_publish);
1082
1083        assert!(plan.is_empty());
1084    }
1085}