1use 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
31pub const DEFAULT_REGISTRY_NAME: &str = "default";
33
34#[derive(Debug, Clone)]
38pub enum Dependency {
39 Package(RegistryPackage),
41
42 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#[derive(Debug, Clone)]
78pub struct RegistryPackage {
79 pub name: Option<PackageRef>,
83
84 pub version: VersionReq,
86
87 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#[derive(Clone)]
109pub struct RegistryResolution {
110 pub name: PackageRef,
114 pub package: PackageRef,
116 pub registry: Option<String>,
118 pub requirement: VersionReq,
120 pub version: Version,
122 pub digest: ContentDigest,
124 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 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#[derive(Clone, Debug)]
164pub struct LocalResolution {
165 pub name: PackageRef,
167 pub path: PathBuf,
169}
170
171#[derive(Debug, Clone)]
173#[allow(clippy::large_enum_variant)]
174pub enum DependencyResolution {
175 Registry(RegistryResolution),
177 Local(LocalResolution),
179}
180
181impl DependencyResolution {
182 pub fn name(&self) -> &PackageRef {
184 match self {
185 Self::Registry(res) => &res.name,
186 Self::Local(res) => &res.name,
187 }
188 }
189
190 pub fn version(&self) -> Option<&Version> {
194 match self {
195 Self::Registry(res) => Some(&res.version),
196 Self::Local(_) => None,
197 }
198 }
199
200 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 pub async fn decode(&self) -> Result<DecodedDependency<'_>> {
212 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 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
278pub enum DecodedDependency<'a> {
280 Wit {
282 resolution: &'a DependencyResolution,
284 package: UnresolvedPackageGroup,
286 },
287 Wasm {
289 resolution: &'a DependencyResolution,
291 decoded: DecodedWasm,
293 },
294}
295
296impl DecodedDependency<'_> {
297 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 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 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
346pub 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 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 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 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 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 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 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 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 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 self.dependencies.remove(name);
486
487 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 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 pub async fn resolve(mut self) -> Result<DependencyResolutionMap> {
529 let mut resolutions = self.resolutions;
530 for (name, dependency) in self.dependencies.into_iter() {
531 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 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 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 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 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#[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 pub async fn decode_dependencies(
691 &self,
692 ) -> Result<IndexMap<PackageName, DecodedDependency<'_>>> {
693 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 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 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 pub async fn generate_resolve(&self, dir: impl AsRef<Path>) -> Result<(Resolve, PackageId)> {
725 let mut merged = Resolve {
726 all_features: true,
728 ..Resolve::default()
729 };
730
731 let deps = self.decode_dependencies().await?;
732
733 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 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 match dep {
804 DecodedDependency::Wit {
805 package,
806 resolution,
807 } => {
808 for name in package.main.foreign_deps.keys() {
809 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 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
858pub type DependencyGraph<N> = Acyclic<StableDiGraph<N, petgraph::Direction>>;
860
861pub type LocalPackageIndex = HashMap<PackageRef, (NodeIndex, PathBuf)>;
863
864pub struct PublishPlan {
866 dependents: DependencyGraph<PackageSpec>,
868 indices: LocalPackageIndex,
871}
872
873impl PublishPlan {
874 pub fn from_paths(paths: &[impl AsRef<Path>]) -> Result<Self> {
876 let (graph, indices) = get_local_dependencies(paths)?;
877 {
878 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 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 pub fn take_ready(&self) -> BTreeSet<PackageSpec> {
924 self.dependents
925 .nodes_iter()
926 .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 pub fn get_path(&self, pkg: &PackageRef) -> Option<&Path> {
939 self.indices.get(pkg).map(|(_, p)| p.as_ref())
940 }
941
942 pub fn get_node_index(&self, pkg: &PackageRef) -> Option<NodeIndex> {
944 self.indices.get(pkg).map(|(id, _)| *id)
945 }
946
947 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 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 for id in self.dependents.nodes_iter() {
965 let dep = &self.dependents[id];
966 let mut neighbors = self
968 .dependents
969 .neighbors_directed(id, Direction::Outgoing)
970 .peekable();
971
972 if neighbors.peek().is_none() {
973 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
994fn 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 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}