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