1use 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
26pub const DEFAULT_REGISTRY_NAME: &str = "default";
28
29#[derive(Debug, Clone)]
33pub enum Dependency {
34 Package(RegistryPackage),
36
37 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#[derive(Debug, Clone)]
51pub struct RegistryPackage {
52 pub name: Option<PackageRef>,
56
57 pub version: VersionReq,
59
60 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#[derive(Clone)]
82pub struct RegistryResolution {
83 pub name: PackageRef,
87 pub package: PackageRef,
89 pub registry: Option<String>,
91 pub requirement: VersionReq,
93 pub version: Version,
95 pub digest: ContentDigest,
97 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 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(stream.map_err(|e| {
130 std::io::Error::new(std::io::ErrorKind::Other, e)
131 })))
132 }
133}
134
135#[derive(Clone, Debug)]
137pub struct LocalResolution {
138 pub name: PackageRef,
140 pub path: PathBuf,
142}
143
144#[derive(Debug, Clone)]
146#[allow(clippy::large_enum_variant)]
147pub enum DependencyResolution {
148 Registry(RegistryResolution),
150 Local(LocalResolution),
152}
153
154impl DependencyResolution {
155 pub fn name(&self) -> &PackageRef {
157 match self {
158 Self::Registry(res) => &res.name,
159 Self::Local(res) => &res.name,
160 }
161 }
162
163 pub fn version(&self) -> Option<&Version> {
167 match self {
168 Self::Registry(res) => Some(&res.version),
169 Self::Local(_) => None,
170 }
171 }
172
173 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 pub async fn decode(&self) -> Result<DecodedDependency> {
185 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 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
243pub enum DecodedDependency<'a> {
245 Wit {
247 resolution: &'a DependencyResolution,
249 package: UnresolvedPackageGroup,
251 },
252 Wasm {
254 resolution: &'a DependencyResolution,
256 decoded: DecodedWasm,
258 },
259}
260
261impl DecodedDependency<'_> {
262 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 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 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
311pub 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 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 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 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 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 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 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 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 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 self.dependencies.remove(name);
451
452 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 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 pub async fn resolve(mut self) -> Result<DependencyResolutionMap> {
494 let mut resolutions = self.resolutions;
495 for (name, dependency) in self.dependencies.into_iter() {
496 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 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 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 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 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#[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 pub async fn decode_dependencies(
647 &self,
648 ) -> Result<IndexMap<PackageName, DecodedDependency<'_>>> {
649 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 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 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 pub async fn generate_resolve(&self, dir: impl AsRef<Path>) -> Result<(Resolve, PackageId)> {
681 let mut merged = Resolve {
682 all_features: true,
684 ..Resolve::default()
685 };
686
687 let deps = self.decode_dependencies().await?;
688
689 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 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 match dep {
760 DecodedDependency::Wit {
761 package,
762 resolution,
763 } => {
764 for name in package.main.foreign_deps.keys() {
765 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 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}