this repo has no description
1use std::{cell::RefCell, cmp::Reverse, collections::HashMap, rc::Rc};
2
3use crate::{Error, Result, manifest};
4
5use ecow::EcoString;
6use hexpm::{
7 Dependency, Release,
8 version::{Range, Version},
9};
10use pubgrub::{Dependencies, Map};
11use thiserror::Error;
12
13pub type PackageVersions = HashMap<String, Version>;
14
15type PubgrubRange = pubgrub::Range<Version>;
16
17pub fn resolve_versions<Requirements>(
18 package_fetcher: &impl PackageFetcher,
19 provided_packages: HashMap<EcoString, hexpm::Package>,
20 root_name: EcoString,
21 dependencies: Requirements,
22 locked: &HashMap<EcoString, Version>,
23) -> Result<PackageVersions>
24where
25 Requirements: Iterator<Item = (EcoString, Range)>,
26{
27 tracing::info!("resolving_versions");
28 let root_version = Version::new(0, 0, 0);
29 let requirements = root_dependencies(dependencies, locked)?;
30
31 // Creating a map of all the required packages that have exact versions specified
32 let exact_deps = &requirements
33 .iter()
34 .filter_map(|(name, dep)| parse_exact_version(dep.requirement.as_str()).map(|v| (name, v)))
35 .map(|(name, version)| (name.clone(), version))
36 .collect();
37
38 let root = hexpm::Package {
39 name: root_name.as_str().into(),
40 repository: "local".into(),
41 releases: vec![Release {
42 version: root_version.clone(),
43 outer_checksum: vec![],
44 retirement_status: None,
45 requirements,
46 meta: (),
47 }],
48 };
49
50 let packages = pubgrub::resolve(
51 &DependencyProvider::new(package_fetcher, provided_packages, root, locked, exact_deps),
52 root_name.as_str().into(),
53 root_version,
54 )
55 .map_err(|error| Error::dependency_resolution_failed(error, root_name.clone()))?
56 .into_iter()
57 .filter(|(name, _)| name.as_str() != root_name.as_str())
58 .collect();
59
60 Ok(packages)
61}
62
63/**
64* Used to compare 2 versions of a package.
65*/
66pub type PackageVersionDiffs = HashMap<String, (Version, Version)>;
67
68fn resolve_major_versions(
69 package_fetcher: &impl PackageFetcher,
70 versions: PackageVersions,
71) -> PackageVersionDiffs {
72 versions
73 .iter()
74 .filter_map(|(package, version)| {
75 let Ok(hexpackage) = package_fetcher.get_dependencies(package) else {
76 return None;
77 };
78
79 let latest = hexpackage
80 .releases
81 .iter()
82 .map(|release| &release.version)
83 .filter(|version| !version.is_pre())
84 .max()?;
85
86 if latest.major <= version.major {
87 return None;
88 }
89
90 Some((package.to_string(), (version.clone(), latest.clone())))
91 })
92 .collect()
93}
94
95/// Check for major version updates for direct dependencies that are being blocked by some version
96/// constraints.
97pub fn check_for_major_version_updates(
98 manifest: &manifest::Manifest,
99 package_fetcher: &impl PackageFetcher,
100) -> PackageVersionDiffs {
101 // get the resolved versions of the direct dependencies to check for major
102 // version updates.
103 let versions = manifest
104 .packages
105 .iter()
106 .filter(|manifest_package| {
107 manifest
108 .requirements
109 .iter()
110 .any(|(required_pkg, _)| manifest_package.name == *required_pkg)
111 })
112 .map(|manifest_pkg| (manifest_pkg.name.to_string(), manifest_pkg.version.clone()))
113 .collect();
114
115 resolve_major_versions(package_fetcher, versions)
116}
117
118// If the string would parse to an exact version then return the version
119fn parse_exact_version(ver: &str) -> Option<Version> {
120 let version = ver.trim();
121 let first_byte = version.as_bytes().first();
122
123 // Version is exact if it starts with an explicit == or a number
124 if version.starts_with("==") || first_byte.is_some_and(|v| v.is_ascii_digit()) {
125 let version = version.replace("==", "");
126 let version = version.as_str().trim();
127 Version::parse(version).ok()
128 } else {
129 None
130 }
131}
132
133fn root_dependencies<Requirements>(
134 base_requirements: Requirements,
135 locked: &HashMap<EcoString, Version>,
136) -> Result<HashMap<String, Dependency>, Error>
137where
138 Requirements: Iterator<Item = (EcoString, Range)>,
139{
140 // Record all of the already locked versions as hard requirements
141 let mut requirements: HashMap<_, _> = locked
142 .iter()
143 .map(|(name, version)| {
144 (
145 name.to_string(),
146 Dependency {
147 app: None,
148 optional: false,
149 repository: None,
150 requirement: version.clone().into(),
151 },
152 )
153 })
154 .collect();
155
156 for (name, range) in base_requirements {
157 match locked.get(&name) {
158 // If the package was not already locked then we can use the
159 // specified version requirement without modification.
160 None => {
161 let _ = requirements.insert(
162 name.into(),
163 Dependency {
164 app: None,
165 optional: false,
166 repository: None,
167 requirement: range,
168 },
169 );
170 }
171
172 // If the version was locked we verify that the requirement is
173 // compatible with the locked version.
174 Some(locked_version) => {
175 let compatible = range.to_pubgrub().contains(locked_version);
176 if !compatible {
177 return Err(Error::IncompatibleLockedVersion {
178 error: format!(
179 "{name} is specified with the requirement `{range}`, \
180but it is locked to {locked_version}, which is incompatible.",
181 ),
182 });
183 }
184 }
185 };
186 }
187
188 Ok(requirements)
189}
190
191pub trait PackageFetcher {
192 fn get_dependencies(&self, package: &str) -> Result<Rc<hexpm::Package>, PackageFetchError>;
193}
194
195#[derive(Debug, Error)]
196pub enum PackageFetchError {
197 #[error("{0}")]
198 ApiError(hexpm::ApiError),
199 #[error("{0}")]
200 FetchError(String),
201}
202impl From<hexpm::ApiError> for PackageFetchError {
203 fn from(api_error: hexpm::ApiError) -> Self {
204 Self::ApiError(api_error)
205 }
206}
207impl PackageFetchError {
208 pub fn fetch_error<T: std::error::Error>(err: T) -> Self {
209 Self::FetchError(err.to_string())
210 }
211}
212
213#[derive(Debug)]
214pub struct DependencyProvider<'a, T: PackageFetcher> {
215 packages: RefCell<HashMap<EcoString, hexpm::Package>>,
216 remote: &'a T,
217 locked: &'a HashMap<EcoString, Version>,
218 // Map of packages where an exact version was requested
219 // We need this because by default pubgrub checks exact version by checking if a version is between the exact
220 // and the version 1 bump ahead. That default breaks on prerelease builds since a bump includes the whole patch
221 exact_only: &'a HashMap<String, Version>,
222 optional_dependencies: RefCell<HashMap<EcoString, pubgrub::Range<Version>>>,
223}
224
225impl<'a, T> DependencyProvider<'a, T>
226where
227 T: PackageFetcher,
228{
229 fn new(
230 remote: &'a T,
231 mut packages: HashMap<EcoString, hexpm::Package>,
232 root: hexpm::Package,
233 locked: &'a HashMap<EcoString, Version>,
234 exact_only: &'a HashMap<String, Version>,
235 ) -> Self {
236 let _ = packages.insert(root.name.as_str().into(), root);
237 Self {
238 packages: RefCell::new(packages),
239 locked,
240 remote,
241 exact_only,
242 optional_dependencies: RefCell::new(Default::default()),
243 }
244 }
245
246 /// Download information about the package from the registry into the local
247 /// store. Does nothing if the packages are already known.
248 ///
249 /// Package versions are sorted from newest to oldest, with all pre-releases
250 /// at the end to ensure that a non-prerelease version will be picked first
251 /// if there is one.
252 //
253 fn ensure_package_fetched(
254 // We would like to use `&mut self` but the pubgrub library enforces
255 // `&self` with interop mutability.
256 &self,
257 name: &str,
258 ) -> Result<(), PackageFetchError> {
259 let mut packages = self.packages.borrow_mut();
260 if packages.get(name).is_none() {
261 let package = self.remote.get_dependencies(name)?;
262 // mut (therefore clone) is required here in order to sort the releases
263 let mut package = (*package).clone();
264 // Sort the packages from newest to oldest, pres after all others
265 package.releases.sort_by(|a, b| a.version.cmp(&b.version));
266 package.releases.reverse();
267 let (pre, mut norm): (_, Vec<_>) = package
268 .releases
269 .into_iter()
270 .partition(|r| r.version.is_pre());
271 norm.extend(pre);
272 package.releases = norm;
273 let _ = packages.insert(name.into(), package);
274 }
275 Ok(())
276 }
277}
278
279type PackageName = String;
280pub type ResolutionError<'a, T> = pubgrub::PubGrubError<DependencyProvider<'a, T>>;
281
282impl<T> pubgrub::DependencyProvider for DependencyProvider<'_, T>
283where
284 T: PackageFetcher,
285{
286 fn get_dependencies(
287 &self,
288 package: &Self::P,
289 version: &Self::V,
290 ) -> Result<Dependencies<Self::P, Self::VS, Self::M>, Self::Err> {
291 self.ensure_package_fetched(package)?;
292 let packages = self.packages.borrow();
293 let release = match packages
294 .get(package.as_str())
295 .into_iter()
296 .flat_map(|p| p.releases.iter())
297 .find(|r| &r.version == version)
298 {
299 Some(release) => release,
300 None => {
301 return Ok(Dependencies::Unavailable(format!(
302 "{package}@{version} is not available"
303 )));
304 }
305 };
306
307 // Only use retired versions if they have been locked
308 if release.is_retired() && self.locked.get(package.as_str()) != Some(version) {
309 return Ok(Dependencies::Unavailable(format!(
310 "{package}@{version} is retired"
311 )));
312 }
313
314 let mut deps: Map<PackageName, PubgrubRange> = Default::default();
315 for (name, d) in &release.requirements {
316 let mut range = d.requirement.to_pubgrub().clone();
317 let mut opt_deps = self.optional_dependencies.borrow_mut();
318 // if it's optional and it was not provided yet, store and skip
319 if d.optional && !packages.contains_key(name.as_str()) {
320 let _ = opt_deps
321 .entry(name.into())
322 .and_modify(|stored_range| {
323 *stored_range = range.intersection(stored_range);
324 })
325 .or_insert(range);
326 continue;
327 }
328
329 // if a now required dep was optional before, add back the constraints
330 if let Some(other_range) = opt_deps.remove(name.as_str()) {
331 range = range.intersection(&other_range);
332 }
333
334 let _ = deps.insert(name.clone(), range);
335 }
336 Ok(Dependencies::Available(deps))
337 }
338
339 fn prioritize(
340 &self,
341 package: &Self::P,
342 range: &Self::VS,
343 _package_conflicts_counts: &pubgrub::PackageResolutionStatistics,
344 ) -> Self::Priority {
345 Reverse(
346 self.packages
347 .borrow()
348 .get(package.as_str())
349 .cloned()
350 .into_iter()
351 .flat_map(|p| {
352 p.releases
353 .into_iter()
354 .filter(|r| range.contains(&r.version))
355 })
356 .count(),
357 )
358 }
359
360 fn choose_version(
361 &self,
362 package: &Self::P,
363 range: &Self::VS,
364 ) -> std::result::Result<Option<Self::V>, Self::Err> {
365 self.ensure_package_fetched(package)?;
366
367 let exact_package = self.exact_only.get(package);
368 let potential_versions = self
369 .packages
370 .borrow()
371 .get(package.as_str())
372 .cloned()
373 .into_iter()
374 .flat_map(move |p| {
375 p.releases
376 .into_iter()
377 // if an exact version of a package is specified then we only want to allow that version as available
378 .filter_map(move |release| match exact_package {
379 Some(ver) => (ver == &release.version).then_some(release.version),
380 _ => Some(release.version),
381 })
382 })
383 .filter(|v| range.contains(v));
384 match potential_versions.clone().filter(|v| !v.is_pre()).max() {
385 // Don't resolve to a pre-releaase package unless we *have* to
386 Some(v) => Ok(Some(v)),
387 None => Ok(potential_versions.max()),
388 }
389 }
390
391 type P = PackageName;
392 type V = Version;
393 type VS = PubgrubRange;
394 type Priority = Reverse<usize>;
395 type M = String;
396 type Err = PackageFetchError;
397}
398
399#[cfg(test)]
400mod tests {
401 use hexpm::RetirementStatus;
402
403 use crate::{
404 derivation_tree::DerivationTreePrinter,
405 manifest::{Base16Checksum, ManifestPackage, ManifestPackageSource},
406 requirement,
407 };
408
409 use super::*;
410
411 struct Remote {
412 deps: HashMap<String, Rc<hexpm::Package>>,
413 }
414
415 impl PackageFetcher for Remote {
416 fn get_dependencies(&self, package: &str) -> Result<Rc<hexpm::Package>, PackageFetchError> {
417 self.deps
418 .get(package)
419 .map(Rc::clone)
420 .ok_or(hexpm::ApiError::NotFound.into())
421 }
422 }
423
424 fn make_remote() -> Remote {
425 remote(vec![
426 (
427 "gleam_stdlib",
428 vec![
429 release("0.1.0", vec![]),
430 release("0.2.0", vec![]),
431 release("0.2.2", vec![]),
432 release("0.3.0", vec![]),
433 ],
434 ),
435 (
436 "gleam_otp",
437 vec![
438 release("0.1.0", vec![("gleam_stdlib", ">= 0.1.0")]),
439 release("0.2.0", vec![("gleam_stdlib", ">= 0.1.0")]),
440 release("0.3.0-rc1", vec![("gleam_stdlib", ">= 0.1.0")]),
441 release("0.3.0-rc2", vec![("gleam_stdlib", ">= 0.1.0")]),
442 ],
443 ),
444 (
445 "package_with_retired",
446 vec![
447 release("0.1.0", vec![]),
448 retired_release(
449 "0.2.0",
450 vec![],
451 hexpm::RetirementReason::Security,
452 "it's bad",
453 ),
454 ],
455 ),
456 (
457 "package_with_optional",
458 vec![release_with_optional(
459 "0.1.0",
460 vec![],
461 vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")],
462 )],
463 ),
464 (
465 "direct_pkg_with_major_version",
466 vec![
467 release("0.1.0", vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")]),
468 release("1.0.0", vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")]),
469 release("1.1.0", vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")]),
470 ],
471 ),
472 (
473 "depends_on_old_version_of_direct_pkg",
474 vec![release(
475 "0.1.0",
476 vec![("direct_pkg_with_major_version", ">= 0.1.0 and < 0.3.0")],
477 )],
478 ),
479 (
480 "this_pkg_depends_on_indirect_pkg",
481 vec![release(
482 "0.1.0",
483 vec![("indirect_pkg_with_major_version", ">= 0.1.0 and < 1.0.0")],
484 )],
485 ),
486 (
487 "indirect_pkg_with_major_version",
488 vec![
489 release("0.1.0", vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")]),
490 release("1.0.0", vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")]),
491 release("1.1.0", vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")]),
492 ],
493 ),
494 ])
495 }
496
497 #[test]
498 fn resolution_with_locked() {
499 let locked_stdlib = ("gleam_stdlib".into(), Version::parse("0.1.0").unwrap());
500 let result = resolve_versions(
501 &make_remote(),
502 HashMap::new(),
503 "app".into(),
504 vec![("gleam_stdlib".into(), Range::new("~> 0.1".into()).unwrap())].into_iter(),
505 &vec![locked_stdlib].into_iter().collect(),
506 )
507 .unwrap();
508 assert_eq!(
509 result,
510 vec![("gleam_stdlib".into(), Version::parse("0.1.0").unwrap())]
511 .into_iter()
512 .collect()
513 );
514 }
515
516 #[test]
517 fn resolution_without_deps() {
518 let result = resolve_versions(
519 &make_remote(),
520 HashMap::new(),
521 "app".into(),
522 vec![].into_iter(),
523 &vec![].into_iter().collect(),
524 )
525 .unwrap();
526 assert_eq!(result, vec![].into_iter().collect())
527 }
528
529 #[test]
530 fn resolution_1_dep() {
531 let result = resolve_versions(
532 &make_remote(),
533 HashMap::new(),
534 "app".into(),
535 vec![("gleam_stdlib".into(), Range::new("~> 0.1".into()).unwrap())].into_iter(),
536 &vec![].into_iter().collect(),
537 )
538 .unwrap();
539 assert_eq!(
540 result,
541 vec![("gleam_stdlib".into(), Version::try_from("0.3.0").unwrap())]
542 .into_iter()
543 .collect()
544 );
545 }
546
547 #[test]
548 fn resolution_with_nested_deps() {
549 let result = resolve_versions(
550 &make_remote(),
551 HashMap::new(),
552 "app".into(),
553 vec![("gleam_otp".into(), Range::new("~> 0.1".into()).unwrap())].into_iter(),
554 &vec![].into_iter().collect(),
555 )
556 .unwrap();
557 assert_eq!(
558 result,
559 vec![
560 ("gleam_otp".into(), Version::try_from("0.2.0").unwrap()),
561 ("gleam_stdlib".into(), Version::try_from("0.3.0").unwrap())
562 ]
563 .into_iter()
564 .collect()
565 );
566 }
567
568 #[test]
569 fn resolution_with_optional_deps() {
570 let result = resolve_versions(
571 &make_remote(),
572 HashMap::new(),
573 "app".into(),
574 vec![(
575 "package_with_optional".into(),
576 Range::new("~> 0.1".into()).unwrap(),
577 )]
578 .into_iter(),
579 &vec![].into_iter().collect(),
580 )
581 .unwrap();
582 assert_eq!(
583 result,
584 vec![(
585 "package_with_optional".into(),
586 Version::try_from("0.1.0").unwrap()
587 )]
588 .into_iter()
589 .collect()
590 );
591 }
592
593 #[test]
594 fn resolution_with_optional_deps_explicitly_provided() {
595 let result = resolve_versions(
596 &make_remote(),
597 HashMap::new(),
598 "app".into(),
599 vec![
600 (
601 "package_with_optional".into(),
602 Range::new("~> 0.1".into()).unwrap(),
603 ),
604 ("gleam_stdlib".into(), Range::new("~> 0.1".into()).unwrap()),
605 ]
606 .into_iter(),
607 &vec![].into_iter().collect(),
608 )
609 .unwrap();
610 assert_eq!(
611 result,
612 vec![
613 ("gleam_stdlib".into(), Version::try_from("0.2.2").unwrap()),
614 (
615 "package_with_optional".into(),
616 Version::try_from("0.1.0").unwrap()
617 ),
618 ]
619 .into_iter()
620 .collect()
621 );
622 }
623
624 #[test]
625 fn resolution_with_optional_deps_incompatible() {
626 let result = resolve_versions(
627 &make_remote(),
628 HashMap::new(),
629 "app".into(),
630 vec![
631 (
632 "package_with_optional".into(),
633 Range::new("~> 0.1".into()).unwrap(),
634 ),
635 ("gleam_stdlib".into(), Range::new("~> 0.3".into()).unwrap()),
636 ]
637 .into_iter(),
638 &vec![].into_iter().collect(),
639 );
640 assert!(result.is_err());
641 }
642
643 #[test]
644 fn resolution_with_optional_deps_required_by_nested_deps() {
645 let result = resolve_versions(
646 &make_remote(),
647 HashMap::new(),
648 "app".into(),
649 vec![
650 (
651 "package_with_optional".into(),
652 Range::new("~> 0.1".into()).unwrap(),
653 ),
654 ("gleam_otp".into(), Range::new("~> 0.1".into()).unwrap()),
655 ]
656 .into_iter(),
657 &vec![].into_iter().collect(),
658 )
659 .unwrap();
660 assert_eq!(
661 result,
662 vec![
663 ("gleam_stdlib".into(), Version::try_from("0.2.2").unwrap()),
664 ("gleam_otp".into(), Version::try_from("0.2.0").unwrap()),
665 (
666 "package_with_optional".into(),
667 Version::try_from("0.1.0").unwrap()
668 ),
669 ]
670 .into_iter()
671 .collect()
672 );
673 }
674
675 #[test]
676 fn resolution_with_optional_deps_keep_constraints() {}
677
678 #[test]
679 fn resolution_locked_to_older_version() {
680 let result = resolve_versions(
681 &make_remote(),
682 HashMap::new(),
683 "app".into(),
684 vec![("gleam_otp".into(), Range::new("~> 0.1.0".into()).unwrap())].into_iter(),
685 &vec![].into_iter().collect(),
686 )
687 .unwrap();
688 assert_eq!(
689 result,
690 vec![
691 ("gleam_otp".into(), Version::try_from("0.1.0").unwrap()),
692 ("gleam_stdlib".into(), Version::try_from("0.3.0").unwrap())
693 ]
694 .into_iter()
695 .collect()
696 );
697 }
698
699 #[test]
700 fn resolution_retired_versions_not_used_by_default() {
701 let result = resolve_versions(
702 &make_remote(),
703 HashMap::new(),
704 "app".into(),
705 vec![(
706 "package_with_retired".into(),
707 Range::new("> 0.0.0".into()).unwrap(),
708 )]
709 .into_iter(),
710 &vec![].into_iter().collect(),
711 )
712 .unwrap();
713 assert_eq!(
714 result,
715 vec![(
716 "package_with_retired".into(),
717 // Uses the older version that hasn't been retired
718 Version::try_from("0.1.0").unwrap()
719 ),]
720 .into_iter()
721 .collect()
722 );
723 }
724
725 #[test]
726 fn resolution_retired_versions_can_be_used_if_locked() {
727 let result = resolve_versions(
728 &make_remote(),
729 HashMap::new(),
730 "app".into(),
731 vec![(
732 "package_with_retired".into(),
733 Range::new("> 0.0.0".into()).unwrap(),
734 )]
735 .into_iter(),
736 &vec![("package_with_retired".into(), Version::new(0, 2, 0))]
737 .into_iter()
738 .collect(),
739 )
740 .unwrap();
741 assert_eq!(
742 result,
743 vec![(
744 "package_with_retired".into(),
745 // Uses the locked version even though it's retired
746 Version::new(0, 2, 0)
747 ),]
748 .into_iter()
749 .collect()
750 );
751 }
752
753 #[test]
754 fn resolution_prerelease_can_be_selected() {
755 let result = resolve_versions(
756 &make_remote(),
757 HashMap::new(),
758 "app".into(),
759 vec![(
760 "gleam_otp".into(),
761 Range::new("~> 0.3.0-rc1".into()).unwrap(),
762 )]
763 .into_iter(),
764 &vec![].into_iter().collect(),
765 )
766 .unwrap();
767 assert_eq!(
768 result,
769 vec![
770 ("gleam_stdlib".into(), Version::try_from("0.3.0").unwrap()),
771 ("gleam_otp".into(), Version::try_from("0.3.0-rc2").unwrap()),
772 ]
773 .into_iter()
774 .collect(),
775 );
776 }
777
778 #[test]
779 fn resolution_exact_prerelease_can_be_selected() {
780 let result = resolve_versions(
781 &make_remote(),
782 HashMap::new(),
783 "app".into(),
784 vec![("gleam_otp".into(), Range::new("0.3.0-rc1".into()).unwrap())].into_iter(),
785 &vec![].into_iter().collect(),
786 )
787 .unwrap();
788 assert_eq!(
789 result,
790 vec![
791 ("gleam_stdlib".into(), Version::try_from("0.3.0").unwrap()),
792 ("gleam_otp".into(), Version::try_from("0.3.0-rc1").unwrap()),
793 ]
794 .into_iter()
795 .collect(),
796 );
797 }
798
799 #[test]
800 fn resolution_not_found_dep() {
801 let _ = resolve_versions(
802 &make_remote(),
803 HashMap::new(),
804 "app".into(),
805 vec![("unknown".into(), Range::new("~> 0.1".into()).unwrap())].into_iter(),
806 &vec![].into_iter().collect(),
807 )
808 .unwrap_err();
809 }
810
811 #[test]
812 fn resolution_no_matching_version() {
813 let _ = resolve_versions(
814 &make_remote(),
815 HashMap::new(),
816 "app".into(),
817 vec![("gleam_stdlib".into(), Range::new("~> 99.0".into()).unwrap())].into_iter(),
818 &vec![].into_iter().collect(),
819 )
820 .unwrap_err();
821 }
822
823 #[test]
824 fn resolution_locked_version_doesnt_satisfy_requirements() {
825 let err = resolve_versions(
826 &make_remote(),
827 HashMap::new(),
828 "app".into(),
829 vec![(
830 "gleam_stdlib".into(),
831 Range::new("~> 0.1.0".into()).unwrap(),
832 )]
833 .into_iter(),
834 &vec![("gleam_stdlib".into(), Version::new(0, 2, 0))]
835 .into_iter()
836 .collect(),
837 )
838 .unwrap_err();
839
840 match err {
841 Error::IncompatibleLockedVersion { error } => assert_eq!(
842 error,
843 "gleam_stdlib is specified with the requirement `~> 0.1.0`, but it is locked to 0.2.0, which is incompatible."
844 ),
845 _ => panic!("wrong error: {err}"),
846 }
847 }
848
849 #[test]
850 fn resolution_with_exact_dep() {
851 let result = resolve_versions(
852 &make_remote(),
853 HashMap::new(),
854 "app".into(),
855 vec![("gleam_stdlib".into(), Range::new("0.1.0".into()).unwrap())].into_iter(),
856 &vec![].into_iter().collect(),
857 )
858 .unwrap();
859 assert_eq!(
860 result,
861 vec![("gleam_stdlib".into(), Version::try_from("0.1.0").unwrap())]
862 .into_iter()
863 .collect()
864 );
865 }
866
867 #[test]
868 fn parse_exact_version_test() {
869 assert_eq!(
870 parse_exact_version("1.0.0"),
871 Some(Version::parse("1.0.0").unwrap())
872 );
873 assert_eq!(
874 parse_exact_version("==1.0.0"),
875 Some(Version::parse("1.0.0").unwrap())
876 );
877 assert_eq!(
878 parse_exact_version("== 1.0.0"),
879 Some(Version::parse("1.0.0").unwrap())
880 );
881 assert_eq!(parse_exact_version("~> 1.0.0"), None);
882 assert_eq!(parse_exact_version(">= 1.0.0"), None);
883 }
884
885 #[test]
886 fn resolve_major_version_upgrades() {
887 let manifest = manifest::Manifest {
888 requirements: vec![
889 (
890 EcoString::from("package_depends_on_indirect_pkg"),
891 requirement::Requirement::Hex {
892 version: Range::new("> 0.1.0 and <= 1.0.0".into()).unwrap(),
893 },
894 ),
895 (
896 EcoString::from("direct_pkg_with_major_version"),
897 requirement::Requirement::Hex {
898 version: Range::new("> 0.1.0 and <= 2.0.0".into()).unwrap(),
899 },
900 ),
901 (
902 EcoString::from("depends_on_old_version_of_direct_pkg"),
903 requirement::Requirement::Hex {
904 version: Range::new("> 0.1.0 and <= 1.0.0".into()).unwrap(),
905 },
906 ),
907 ]
908 .into_iter()
909 .collect(),
910 packages: vec![
911 ManifestPackage {
912 name: "direct_pkg_with_major_version".into(),
913 version: Version::parse("0.1.0").unwrap(),
914 build_tools: ["gleam".into()].into(),
915 otp_app: None,
916 requirements: vec![],
917 source: ManifestPackageSource::Hex {
918 outer_checksum: Base16Checksum(vec![1, 2, 3]),
919 },
920 },
921 ManifestPackage {
922 name: "depends_on_old_version_of_direct_pkg".into(),
923 version: Version::parse("0.1.0").unwrap(),
924 build_tools: ["gleam".into()].into(),
925 otp_app: None,
926 requirements: vec!["direct_pkg_with_major_version".into()],
927 source: ManifestPackageSource::Hex {
928 outer_checksum: Base16Checksum(vec![1, 2, 3]),
929 },
930 },
931 ManifestPackage {
932 name: "pkg_depends_on_indirect_pkg".into(),
933 version: Version::parse("0.1.0").unwrap(),
934 build_tools: ["gleam".into()].into(),
935 otp_app: None,
936 requirements: vec!["indirect_pkg_with_major_version".into()],
937 source: ManifestPackageSource::Hex {
938 outer_checksum: Base16Checksum(vec![1, 2, 3]),
939 },
940 },
941 ManifestPackage {
942 name: "indirect_pkg_with_major_version".into(),
943 version: Version::parse("0.1.0").unwrap(),
944 build_tools: ["gleam".into()].into(),
945 otp_app: None,
946 requirements: vec![],
947 source: ManifestPackageSource::Hex {
948 outer_checksum: Base16Checksum(vec![1, 2, 3]),
949 },
950 },
951 ],
952 };
953 let result = check_for_major_version_updates(&manifest, &make_remote());
954
955 // indirect package with major version will not be in the result even though a major
956 // version of it is available
957 assert_eq!(
958 result,
959 vec![(
960 "direct_pkg_with_major_version".into(),
961 (
962 Version::try_from("0.1.0").unwrap(),
963 Version::try_from("1.1.0").unwrap()
964 )
965 ),]
966 .into_iter()
967 .collect()
968 );
969 }
970
971 fn retired_release(
972 version: &str,
973 requirements: Vec<(&str, &str)>,
974 reason: hexpm::RetirementReason,
975 message: &str,
976 ) -> Release<()> {
977 Release {
978 retirement_status: Some(RetirementStatus {
979 reason,
980 message: message.into(),
981 }),
982 ..release(version, requirements)
983 }
984 }
985 fn release(version: &str, requirements: Vec<(&str, &str)>) -> Release<()> {
986 release_with_optional(version, requirements, vec![])
987 }
988
989 fn release_with_optional(
990 version: &str,
991 requirements: Vec<(&str, &str)>,
992 optional_requirements: Vec<(&str, &str)>,
993 ) -> Release<()> {
994 let mut all_requirements = HashMap::new();
995
996 for (name, range) in requirements {
997 let requirement = Range::new(range.to_string()).unwrap();
998 let dependency = Dependency {
999 requirement,
1000 optional: false,
1001 app: None,
1002 repository: None,
1003 };
1004 let _ = all_requirements.insert(name.to_string(), dependency);
1005 }
1006
1007 for (name, range) in optional_requirements {
1008 let requirement = Range::new(range.to_string()).unwrap();
1009 let dependency = Dependency {
1010 requirement,
1011 optional: true,
1012 app: None,
1013 repository: None,
1014 };
1015 let _ = all_requirements.insert(name.to_string(), dependency);
1016 }
1017
1018 Release {
1019 version: Version::try_from(version).unwrap(),
1020 requirements: all_requirements,
1021 retirement_status: None,
1022 outer_checksum: vec![1, 2, 3],
1023 meta: (),
1024 }
1025 }
1026
1027 fn remote(dependencies: Vec<(&str, Vec<Release<()>>)>) -> Remote {
1028 let mut deps = HashMap::new();
1029 for (package, releases) in dependencies {
1030 let _ = deps.insert(
1031 package.into(),
1032 Rc::new(hexpm::Package {
1033 name: package.into(),
1034 repository: "hexpm".into(),
1035 releases,
1036 }),
1037 );
1038 }
1039 Remote { deps }
1040 }
1041
1042 #[test]
1043 fn resolution_error_message() {
1044 let remote = remote(vec![
1045 (
1046 "wibble",
1047 vec![
1048 release("1.2.0", vec![("wobble", ">= 1.0.0 and < 2.0.0")]),
1049 release("1.3.0", vec![("wobble", ">= 2.0.0 and < 3.0.0")]),
1050 ],
1051 ),
1052 (
1053 "wobble",
1054 vec![
1055 release("1.1.0", vec![("woo", ">= 1.0.0 and < 2.0.0")]),
1056 release("2.0.0", vec![("waa", ">= 1.0.0 and < 2.0.0")]),
1057 ],
1058 ),
1059 (
1060 "woo",
1061 vec![release("1.0.0", vec![]), release("2.0.0", vec![])],
1062 ),
1063 (
1064 "waa",
1065 vec![release("1.0.0", vec![]), release("2.0.0", vec![])],
1066 ),
1067 ]);
1068
1069 let result = resolve_versions(
1070 &remote,
1071 HashMap::new(),
1072 "app".into(),
1073 vec![
1074 (
1075 "wibble".into(),
1076 Range::new(">= 1.0.0 and < 2.0.0".into()).unwrap(),
1077 ),
1078 (
1079 "woo".into(),
1080 Range::new(">= 2.0.0 and < 3.0.0".into()).unwrap(),
1081 ),
1082 (
1083 "waa".into(),
1084 Range::new(">= 2.0.0 and < 3.0.0".into()).unwrap(),
1085 ),
1086 ]
1087 .into_iter(),
1088 &vec![].into_iter().collect(),
1089 );
1090
1091 if let Err(Error::DependencyResolutionNoSolution {
1092 root_package_name,
1093 derivation_tree,
1094 }) = result
1095 {
1096 let message = crate::error::wrap(
1097 &DerivationTreePrinter::new(root_package_name, derivation_tree.0).print(),
1098 );
1099 insta::assert_snapshot!(message)
1100 } else {
1101 panic!("expected a resolution error message")
1102 }
1103 }
1104}