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 std::fmt::Display for Dependency {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 Dependency::Package(RegistryPackage {
45 name,
46 version,
47 registry,
48 }) => {
49 let registry = registry.as_deref().unwrap_or("_");
50 let name = name.as_ref().map(|n| n.to_string());
51
52 write!(
53 f,
54 "{{registry=\"{registry}\" package=\"{}@{version}\"}}",
55 name.as_deref().unwrap_or("_:_"),
56 )
57 }
58 Dependency::Local(path_buf) => write!(f, "{}", path_buf.display()),
59 }
60 }
61}
62
63impl FromStr for Dependency {
64 type Err = anyhow::Error;
65
66 fn from_str(s: &str) -> Result<Self> {
67 Ok(Self::Package(s.parse()?))
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct RegistryPackage {
74 pub name: Option<PackageRef>,
78
79 pub version: VersionReq,
81
82 pub registry: Option<String>,
86}
87
88impl FromStr for RegistryPackage {
89 type Err = anyhow::Error;
90
91 fn from_str(s: &str) -> Result<Self> {
92 Ok(Self {
93 name: None,
94 version: s
95 .parse()
96 .with_context(|| format!("'{s}' is an invalid registry package version"))?,
97 registry: None,
98 })
99 }
100}
101
102#[derive(Clone)]
104pub struct RegistryResolution {
105 pub name: PackageRef,
109 pub package: PackageRef,
111 pub registry: Option<String>,
113 pub requirement: VersionReq,
115 pub version: Version,
117 pub digest: ContentDigest,
119 client: CachingClient<FileCache>,
121}
122
123impl Debug for RegistryResolution {
124 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
125 f.debug_struct("RegistryResolution")
126 .field("name", &self.name)
127 .field("package", &self.package)
128 .field("registry", &self.registry)
129 .field("requirement", &self.requirement)
130 .field("version", &self.version)
131 .field("digest", &self.digest)
132 .finish()
133 }
134}
135
136impl RegistryResolution {
137 pub async fn fetch(&self) -> Result<impl AsyncRead> {
140 let stream = self
141 .client
142 .get_content(
143 &self.package,
144 &Release {
145 version: self.version.clone(),
146 content_digest: self.digest.clone(),
147 },
148 )
149 .await?;
150
151 Ok(tokio_util::io::StreamReader::new(
152 stream.map_err(std::io::Error::other),
153 ))
154 }
155}
156
157#[derive(Clone, Debug)]
159pub struct LocalResolution {
160 pub name: PackageRef,
162 pub path: PathBuf,
164}
165
166#[derive(Debug, Clone)]
168#[allow(clippy::large_enum_variant)]
169pub enum DependencyResolution {
170 Registry(RegistryResolution),
172 Local(LocalResolution),
174}
175
176impl DependencyResolution {
177 pub fn name(&self) -> &PackageRef {
179 match self {
180 Self::Registry(res) => &res.name,
181 Self::Local(res) => &res.name,
182 }
183 }
184
185 pub fn version(&self) -> Option<&Version> {
189 match self {
190 Self::Registry(res) => Some(&res.version),
191 Self::Local(_) => None,
192 }
193 }
194
195 pub fn key(&self) -> Option<(&PackageRef, Option<&str>)> {
199 match self {
200 DependencyResolution::Registry(pkg) => Some((&pkg.package, pkg.registry.as_deref())),
201 DependencyResolution::Local(_) => None,
202 }
203 }
204
205 pub async fn decode(&self) -> Result<DecodedDependency<'_>> {
207 let bytes = match self {
209 DependencyResolution::Local(LocalResolution { path, .. })
210 if tokio::fs::metadata(path).await?.is_dir() =>
211 {
212 return Ok(DecodedDependency::Wit {
213 resolution: self,
214 package: UnresolvedPackageGroup::parse_dir(path).with_context(|| {
215 format!("failed to parse dependency `{path}`", path = path.display())
216 })?,
217 });
218 }
219 DependencyResolution::Local(LocalResolution { path, .. }) => {
220 tokio::fs::read(path).await.with_context(|| {
221 format!(
222 "failed to read content of dependency `{name}` at path `{path}`",
223 name = self.name(),
224 path = path.display()
225 )
226 })?
227 }
228 DependencyResolution::Registry(res) => {
229 let mut reader = res.fetch().await?;
230
231 let mut buf = Vec::new();
232 reader.read_to_end(&mut buf).await?;
233 buf
234 }
235 };
236
237 if &bytes[0..4] != b"\0asm" {
238 return Ok(DecodedDependency::Wit {
239 resolution: self,
240 package: UnresolvedPackageGroup::parse(
241 self.name().to_string(),
243 std::str::from_utf8(&bytes).with_context(|| {
244 format!(
245 "dependency `{name}` is not UTF-8 encoded",
246 name = self.name()
247 )
248 })?,
249 )?,
250 });
251 }
252
253 Ok(DecodedDependency::Wasm {
254 resolution: self,
255 decoded: wit_component::decode(&bytes).with_context(|| {
256 format!(
257 "failed to decode content of dependency `{name}`",
258 name = self.name(),
259 )
260 })?,
261 })
262 }
263}
264
265pub enum DecodedDependency<'a> {
267 Wit {
269 resolution: &'a DependencyResolution,
271 package: UnresolvedPackageGroup,
273 },
274 Wasm {
276 resolution: &'a DependencyResolution,
278 decoded: DecodedWasm,
280 },
281}
282
283impl DecodedDependency<'_> {
284 pub fn resolve(self) -> Result<(Resolve, PackageId, Vec<PathBuf>)> {
289 match self {
290 Self::Wit { package, .. } => {
291 let mut resolve = Resolve::new();
292 resolve.all_features = true;
293 let source_files = package
294 .source_map
295 .source_files()
296 .map(Path::to_path_buf)
297 .collect();
298 let pkg = resolve.push_group(package)?;
299 Ok((resolve, pkg, source_files))
300 }
301 Self::Wasm { decoded, .. } => match decoded {
302 DecodedWasm::WitPackage(resolve, pkg) => Ok((resolve, pkg, Vec::new())),
303 DecodedWasm::Component(resolve, world) => {
304 let pkg = resolve.worlds[world].package.unwrap();
305 Ok((resolve, pkg, Vec::new()))
306 }
307 },
308 }
309 }
310
311 pub fn package_name(&self) -> &PackageName {
313 match self {
314 Self::Wit { package, .. } => &package.main.name,
315 Self::Wasm { decoded, .. } => &decoded.resolve().packages[decoded.package()].name,
316 }
317 }
318
319 pub fn into_component_world(self) -> Result<(Resolve, WorldId)> {
323 match self {
324 Self::Wasm {
325 decoded: DecodedWasm::Component(resolve, world),
326 ..
327 } => Ok((resolve, world)),
328 _ => bail!("dependency is not a WebAssembly component"),
329 }
330 }
331}
332
333pub struct DependencyResolver<'a> {
335 client: CachingClient<FileCache>,
336 lock_file: Option<&'a LockFile>,
337 packages: HashMap<PackageRef, Vec<VersionInfo>>,
338 dependencies: HashMap<PackageRef, RegistryDependency>,
339 resolutions: DependencyResolutionMap,
340}
341
342impl<'a> DependencyResolver<'a> {
343 pub fn new(
347 config: Option<Config>,
348 lock_file: Option<&'a LockFile>,
349 cache: FileCache,
350 ) -> anyhow::Result<Self> {
351 if config.is_none() && lock_file.is_none() {
352 anyhow::bail!("lock file must be provided when offline mode is enabled");
353 }
354 let client = CachingClient::new(config.map(Client::new), cache);
355 Ok(DependencyResolver {
356 client,
357 lock_file,
358 resolutions: Default::default(),
359 packages: Default::default(),
360 dependencies: Default::default(),
361 })
362 }
363
364 pub fn new_with_client(
368 client: CachingClient<FileCache>,
369 lock_file: Option<&'a LockFile>,
370 ) -> anyhow::Result<Self> {
371 if client.is_readonly() && lock_file.is_none() {
372 anyhow::bail!("lock file must be provided when offline mode is enabled");
373 }
374 Ok(DependencyResolver {
375 client,
376 lock_file,
377 resolutions: Default::default(),
378 packages: Default::default(),
379 dependencies: Default::default(),
380 })
381 }
382
383 pub async fn add_dependency(
386 &mut self,
387 name: &PackageRef,
388 dependency: &Dependency,
389 ) -> Result<()> {
390 self.add_dependency_internal(name, dependency, false).await
391 }
392
393 pub async fn override_dependency(
396 &mut self,
397 name: &PackageRef,
398 dependency: &Dependency,
399 ) -> Result<()> {
400 self.add_dependency_internal(name, dependency, true).await
401 }
402
403 async fn add_dependency_internal(
404 &mut self,
405 name: &PackageRef,
406 dependency: &Dependency,
407 force_override: bool,
408 ) -> Result<()> {
409 match dependency {
410 Dependency::Package(package) => {
411 let registry_name = package.registry.as_deref().or_else(|| {
413 self.client.client().ok().and_then(|client| {
414 client
415 .config()
416 .resolve_registry(name)
417 .map(|reg| reg.as_ref())
418 })
419 });
420 let package_name = package.name.clone().unwrap_or_else(|| name.clone());
421
422 let locked = match self.lock_file.as_ref().and_then(|resolver| {
424 resolver
425 .resolve(registry_name, &package_name, &package.version)
426 .transpose()
427 }) {
428 Some(Ok(locked)) => Some(locked),
429 Some(Err(e)) => return Err(e),
430 _ => None,
431 };
432
433 if !force_override
436 && (self.resolutions.contains_key(name) || self.dependencies.contains_key(name))
437 {
438 tracing::debug!(%name, %dependency, "dependency already exists and override is not set, ignoring");
439 return Ok(());
440 }
441 self.dependencies.insert(
442 name.to_owned(),
443 RegistryDependency {
444 package: package_name,
445 version: package.version.clone(),
446 locked: locked.map(|l| (l.version.clone(), l.digest.clone())),
447 },
448 );
449 }
450 Dependency::Local(p) => {
451 let res = DependencyResolution::Local(LocalResolution {
452 name: name.clone(),
453 path: p.clone(),
454 });
455
456 let should_insert = force_override
462 || self.dependencies.contains_key(name)
463 || !self.resolutions.contains_key(name);
464 if !should_insert {
465 tracing::debug!(%name, "dependency already exists and registry override is not set, ignoring");
466 return Ok(());
467 }
468
469 self.dependencies.remove(name);
473
474 let (_, packages) = get_packages(p)
477 .context("Error getting dependent packages from local dependency")?;
478 Box::pin(self.add_packages(packages))
479 .await
480 .context("Error adding packages to resolver for local dependency")?;
481
482 let prev = self.resolutions.insert(name.clone(), res);
483 assert!(prev.is_none());
484 }
485 }
486
487 Ok(())
488 }
489
490 pub async fn add_packages(
493 &mut self,
494 packages: impl IntoIterator<Item = (PackageRef, VersionReq)>,
495 ) -> Result<()> {
496 for (package, req) in packages {
497 self.add_dependency(
498 &package,
499 &Dependency::Package(RegistryPackage {
500 name: Some(package.clone()),
501 version: req,
502 registry: None,
503 }),
504 )
505 .await?;
506 }
507 Ok(())
508 }
509
510 pub async fn resolve(mut self) -> Result<DependencyResolutionMap> {
516 let mut resolutions = self.resolutions;
517 for (name, dependency) in self.dependencies.into_iter() {
518 let client = self.client.clone();
521
522 let (selected_version, digest) = if client.is_readonly() {
523 dependency
524 .locked
525 .as_ref()
526 .map(|(ver, digest)| (ver, Some(digest)))
527 .ok_or_else(|| {
528 anyhow::anyhow!("Couldn't find locked dependency while in offline mode")
529 })?
530 } else {
531 let versions =
532 load_package(&mut self.packages, &self.client, dependency.package.clone())
533 .await?
534 .with_context(|| {
535 format!(
536 "package `{name}` was not found in component registry",
537 name = dependency.package
538 )
539 })?;
540
541 match &dependency.locked {
542 Some((version, digest)) => {
543 let exact_req = VersionReq {
545 comparators: vec![Comparator {
546 op: Op::Exact,
547 major: version.major,
548 minor: Some(version.minor),
549 patch: Some(version.patch),
550 pre: version.pre.clone(),
551 }],
552 };
553
554 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)))
559 }
560 None => find_latest_release(versions, &dependency.version).map(|v| (&v.version, None)),
561 }.with_context(|| format!("component registry package `{name}` has no release matching version requirement `{version}`", name = dependency.package, version = dependency.version))?
562 };
563
564 let release = client
567 .get_release(&dependency.package, selected_version)
568 .await?;
569 if let Some(digest) = digest {
570 if &release.content_digest != digest {
571 bail!(
572 "component registry package `{name}` (v`{version}`) has digest `{content}` but the lock file specifies digest `{digest}`",
573 name = dependency.package,
574 version = release.version,
575 content = release.content_digest,
576 );
577 }
578 }
579 let resolution = RegistryResolution {
580 name: name.clone(),
581 package: dependency.package.clone(),
582 registry: self.client.client().ok().and_then(|client| {
583 client
584 .config()
585 .resolve_registry(&name)
586 .map(ToString::to_string)
587 }),
588 requirement: dependency.version.clone(),
589 version: release.version.clone(),
590 digest: release.content_digest.clone(),
591 client: self.client.clone(),
592 };
593 resolutions.insert(name, DependencyResolution::Registry(resolution));
594 }
595
596 Ok(resolutions)
597 }
598}
599
600async fn load_package<'b>(
601 packages: &'b mut HashMap<PackageRef, Vec<VersionInfo>>,
602 client: &CachingClient<FileCache>,
603 package: PackageRef,
604) -> Result<Option<&'b Vec<VersionInfo>>> {
605 match packages.entry(package) {
606 hash_map::Entry::Occupied(e) => Ok(Some(e.into_mut())),
607 hash_map::Entry::Vacant(e) => match client.list_all_versions(e.key()).await {
608 Ok(p) => Ok(Some(e.insert(p))),
609 Err(WasmPkgError::PackageNotFound) => Ok(None),
610 Err(err) => Err(err.into()),
611 },
612 }
613}
614
615#[derive(Debug)]
616struct RegistryDependency {
617 package: PackageRef,
620 version: VersionReq,
621 locked: Option<(Version, ContentDigest)>,
622}
623
624fn find_latest_release<'a>(
625 versions: &'a [VersionInfo],
626 req: &VersionReq,
627) -> Option<&'a VersionInfo> {
628 versions
629 .iter()
630 .filter(|info| !info.yanked && req.matches(&info.version))
631 .max_by(|a, b| a.version.cmp(&b.version))
632}
633
634#[derive(Debug, Clone, Default)]
643pub struct DependencyResolutionMap(HashMap<PackageRef, DependencyResolution>);
644
645impl AsRef<HashMap<PackageRef, DependencyResolution>> for DependencyResolutionMap {
646 fn as_ref(&self) -> &HashMap<PackageRef, DependencyResolution> {
647 &self.0
648 }
649}
650
651impl Deref for DependencyResolutionMap {
652 type Target = HashMap<PackageRef, DependencyResolution>;
653
654 fn deref(&self) -> &Self::Target {
655 &self.0
656 }
657}
658
659impl DerefMut for DependencyResolutionMap {
660 fn deref_mut(&mut self) -> &mut Self::Target {
661 &mut self.0
662 }
663}
664
665impl DependencyResolutionMap {
666 pub async fn decode_dependencies(
669 &self,
670 ) -> Result<IndexMap<PackageName, DecodedDependency<'_>>> {
671 let mut deps = IndexMap::new();
673 for (name, resolution) in self.0.iter() {
674 let decoded = resolution.decode().await?;
675 if let Some(prev) = deps.insert(decoded.package_name().clone(), decoded) {
676 anyhow::bail!(
677 "duplicate definitions of package `{prev}` found while decoding dependency `{name}`",
678 prev = prev.package_name()
679 );
680 }
681 }
682
683 let mut order = IndexSet::new();
685 let mut visiting = HashSet::new();
686 for dep in deps.values() {
687 visit(dep, &deps, &mut order, &mut visiting)?;
688 }
689
690 assert!(visiting.is_empty());
691
692 deps.sort_by(|name_a, _, name_b, _| {
694 order.get_index_of(name_a).cmp(&order.get_index_of(name_b))
695 });
696
697 Ok(deps)
698 }
699
700 pub async fn generate_resolve(&self, dir: impl AsRef<Path>) -> Result<(Resolve, PackageId)> {
703 let mut merged = Resolve {
704 all_features: true,
706 ..Resolve::default()
707 };
708
709 let deps = self.decode_dependencies().await?;
710
711 let root = UnresolvedPackageGroup::parse_dir(&dir).with_context(|| {
713 format!(
714 "failed to parse package from directory `{dir}`",
715 dir = dir.as_ref().display()
716 )
717 })?;
718
719 let mut source_files: Vec<_> = root
720 .source_map
721 .source_files()
722 .map(Path::to_path_buf)
723 .collect();
724
725 for decoded in deps.into_values() {
727 match decoded {
728 DecodedDependency::Wit {
729 resolution,
730 package,
731 } => {
732 source_files.extend(package.source_map.source_files().map(Path::to_path_buf));
733 merged.push_group(package).with_context(|| {
734 format!(
735 "failed to merge dependency `{name}`",
736 name = resolution.name()
737 )
738 })?;
739 }
740 DecodedDependency::Wasm {
741 resolution,
742 decoded,
743 } => {
744 let resolve = match decoded {
745 DecodedWasm::WitPackage(resolve, _) => resolve,
746 DecodedWasm::Component(resolve, _) => resolve,
747 };
748
749 merged.merge(resolve).with_context(|| {
750 format!(
751 "failed to merge world of dependency `{name}`",
752 name = resolution.name()
753 )
754 })?;
755 }
756 };
757 }
758
759 let package = merged.push_group(root).with_context(|| {
760 format!(
761 "failed to merge package from directory `{dir}`",
762 dir = dir.as_ref().display()
763 )
764 })?;
765
766 Ok((merged, package))
767 }
768}
769
770fn visit<'a>(
771 dep: &'a DecodedDependency<'a>,
772 deps: &'a IndexMap<PackageName, DecodedDependency>,
773 order: &mut IndexSet<PackageName>,
774 visiting: &mut HashSet<&'a PackageName>,
775) -> Result<()> {
776 if order.contains(dep.package_name()) {
777 return Ok(());
778 }
779
780 match dep {
782 DecodedDependency::Wit {
783 package,
784 resolution,
785 } => {
786 for name in package.main.foreign_deps.keys() {
787 if let Some(dep) = deps.get(name) {
791 if !visiting.insert(name) {
792 anyhow::bail!("foreign dependency `{name}` forms a dependency cycle while parsing dependency `{other}`", other = resolution.name());
793 }
794
795 visit(dep, deps, order, visiting)?;
796 assert!(visiting.remove(name));
797 }
798 }
799 }
800 DecodedDependency::Wasm {
801 decoded,
802 resolution,
803 } => {
804 for (_, package) in &decoded.resolve().packages {
806 if package.name.namespace == dep.package_name().namespace
807 && package.name.name == dep.package_name().name
808 {
809 continue;
810 }
811
812 if let Some(dep) = deps.get(&package.name) {
813 if !visiting.insert(&package.name) {
814 anyhow::bail!("foreign dependency `{name}` forms a dependency cycle while parsing dependency `{other}`", name = package.name, other = resolution.name());
815 }
816
817 visit(dep, deps, order, visiting)?;
818 assert!(visiting.remove(&package.name));
819 }
820 }
821 }
822 }
823
824 assert!(order.insert(dep.package_name().clone()));
825
826 Ok(())
827}