Fork of daniellemaywood.uk/gleam — Wasm codegen work
43 kB
1296 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::LateDeletion
279 | hexpm::ApiError::LateModification => Self::ApiError(api_error),
280 }
281 }
282}
283
284#[derive(Debug)]
285pub struct DependencyProvider<'a, T: PackageFetcher> {
286 packages: RefCell<HashMap<EcoString, hexpm::Package>>,
287 remote: &'a T,
288 locked: &'a HashMap<EcoString, Version>,
289 // Map of packages where an exact version was requested
290 // We need this because by default pubgrub checks exact version by checking
291 // if a version is between the exact and the version 1 bump ahead.
292 // That default breaks on prerelease builds since a bump includes the whole
293 // patch.
294 exact_only: &'a HashMap<String, Version>,
295 optional_dependencies: RefCell<HashMap<EcoString, pubgrub::Range<Version>>>,
296}
297
298impl<'a, T> DependencyProvider<'a, T>
299where
300 T: PackageFetcher,
301{
302 fn new(
303 remote: &'a T,
304 mut packages: HashMap<EcoString, hexpm::Package>,
305 root: hexpm::Package,
306 locked: &'a HashMap<EcoString, Version>,
307 exact_only: &'a HashMap<String, Version>,
308 ) -> Self {
309 let _ = packages.insert(root.name.as_str().into(), root);
310 Self {
311 packages: RefCell::new(packages),
312 locked,
313 remote,
314 exact_only,
315 optional_dependencies: RefCell::new(HashMap::new()),
316 }
317 }
318
319 /// Download information about the package from the registry into the local
320 /// store. Does nothing if the packages are already known.
321 ///
322 /// Package versions are sorted from newest to oldest, with all pre-releases
323 /// at the end to ensure that a non-prerelease version will be picked first
324 /// if there is one.
325 //
326 fn ensure_package_fetched(
327 // We would like to use `&mut self` but the pubgrub library enforces
328 // `&self` with interop mutability.
329 &self,
330 name: &str,
331 ) -> Result<(), PackageFetchError> {
332 let mut packages = self.packages.borrow_mut();
333 if packages.get(name).is_none() {
334 let package = self.remote.get_dependencies(name)?;
335 // mut (therefore clone) is required here in order to sort the releases
336 let mut package = (*package).clone();
337 // Sort the packages from newest to oldest, pres after all others
338 package.releases.sort_by(|a, b| a.version.cmp(&b.version));
339 package.releases.reverse();
340 let (pre, mut norm): (_, Vec<_>) = package
341 .releases
342 .into_iter()
343 .partition(|release| release.version.is_pre());
344 norm.extend(pre);
345 package.releases = norm;
346 let _ = packages.insert(name.into(), package);
347 }
348 Ok(())
349 }
350}
351
352type PackageName = String;
353pub type ResolutionError<'a, T> = pubgrub::PubGrubError<DependencyProvider<'a, T>>;
354
355impl<T> pubgrub::DependencyProvider for DependencyProvider<'_, T>
356where
357 T: PackageFetcher,
358{
359 fn get_dependencies(
360 &self,
361 package: &Self::P,
362 version: &Self::V,
363 ) -> Result<Dependencies<Self::P, Self::VS, Self::M>, Self::Err> {
364 self.ensure_package_fetched(package)?;
365 let packages = self.packages.borrow();
366 let release = match packages
367 .get(package.as_str())
368 .into_iter()
369 .flat_map(|package| package.releases.iter())
370 .find(|release| &release.version == version)
371 {
372 Some(release) => release,
373 None => {
374 return Ok(Dependencies::Unavailable(format!(
375 "{package}@{version} is not available"
376 )));
377 }
378 };
379
380 // Only use retired versions if they have been locked
381 if release.is_retired() && self.locked.get(package.as_str()) != Some(version) {
382 return Ok(Dependencies::Unavailable(format!(
383 "{package}@{version} is retired"
384 )));
385 }
386
387 let mut deps: Map<PackageName, PubgrubRange> = Map::default();
388 for (name, dependency) in &release.requirements {
389 let mut range = dependency.requirement.to_pubgrub().clone();
390 let mut opt_deps = self.optional_dependencies.borrow_mut();
391 // if it's optional and it was not provided yet, store and skip
392 if dependency.optional && !packages.contains_key(name.as_str()) {
393 let _ = opt_deps
394 .entry(name.into())
395 .and_modify(|stored_range| {
396 *stored_range = range.intersection(stored_range);
397 })
398 .or_insert(range);
399 continue;
400 }
401
402 // if a now required dep was optional before, add back the constraints
403 if let Some(other_range) = opt_deps.remove(name.as_str()) {
404 range = range.intersection(&other_range);
405 }
406
407 let _ = deps.insert(name.clone(), range);
408 }
409 Ok(Dependencies::Available(deps))
410 }
411
412 fn prioritize(
413 &self,
414 package: &Self::P,
415 range: &Self::VS,
416 _package_conflicts_counts: &pubgrub::PackageResolutionStatistics,
417 ) -> Self::Priority {
418 Reverse(
419 self.packages
420 .borrow()
421 .get(package.as_str())
422 .cloned()
423 .into_iter()
424 .flat_map(|package| {
425 package
426 .releases
427 .into_iter()
428 .filter(|release| range.contains(&release.version))
429 })
430 .count(),
431 )
432 }
433
434 fn choose_version(
435 &self,
436 package: &Self::P,
437 range: &Self::VS,
438 ) -> std::result::Result<Option<Self::V>, Self::Err> {
439 self.ensure_package_fetched(package)?;
440
441 let exact_package = self.exact_only.get(package);
442 let potential_versions = self
443 .packages
444 .borrow()
445 .get(package.as_str())
446 .cloned()
447 .into_iter()
448 .flat_map(move |package| {
449 package
450 .releases
451 .into_iter()
452 // if an exact version of a package is specified then we
453 // only want to allow that version as available.
454 .filter_map(move |release| match exact_package {
455 Some(version) => (version == &release.version).then_some(release.version),
456 _ => Some(release.version),
457 })
458 })
459 .filter(|version| range.contains(version));
460 match potential_versions
461 .clone()
462 .filter(|version| !version.is_pre())
463 .max()
464 {
465 // Don't resolve to a pre-releaase package unless we *have* to
466 Some(version) => Ok(Some(version)),
467 None => Ok(potential_versions.max()),
468 }
469 }
470
471 type P = PackageName;
472 type V = Version;
473 type VS = PubgrubRange;
474 type Priority = Reverse<usize>;
475 type M = String;
476 type Err = PackageFetchError;
477}
478
479#[cfg(test)]
480mod tests {
481 use hexpm::RetirementStatus;
482
483 use crate::{
484 manifest::{Base16Checksum, ManifestPackage, ManifestPackageSource},
485 requirement,
486 };
487
488 use super::*;
489
490 struct Remote {
491 deps: HashMap<String, Rc<hexpm::Package>>,
492 }
493
494 impl PackageFetcher for Remote {
495 fn get_dependencies(&self, package: &str) -> Result<Rc<hexpm::Package>, PackageFetchError> {
496 self.deps
497 .get(package)
498 .map(Rc::clone)
499 .ok_or(PackageFetchError::NotFoundError(package.to_string()))
500 }
501 }
502
503 fn make_remote() -> Remote {
504 remote(vec![
505 (
506 "gleam_stdlib",
507 vec![
508 release("0.1.0", vec![]),
509 release("0.2.0", vec![]),
510 release("0.2.2", vec![]),
511 release("0.3.0", vec![]),
512 ],
513 ),
514 (
515 "gleam_otp",
516 vec![
517 release("0.1.0", vec![("gleam_stdlib", ">= 0.1.0")]),
518 release("0.2.0", vec![("gleam_stdlib", ">= 0.1.0")]),
519 release("0.3.0-rc1", vec![("gleam_stdlib", ">= 0.1.0")]),
520 release("0.3.0-rc2", vec![("gleam_stdlib", ">= 0.1.0")]),
521 ],
522 ),
523 (
524 "package_with_retired",
525 vec![
526 release("0.1.0", vec![]),
527 retired_release(
528 "0.2.0",
529 vec![],
530 hexpm::RetirementReason::Security,
531 "it's bad",
532 ),
533 ],
534 ),
535 (
536 "package_with_optional",
537 vec![release_with_optional(
538 "0.1.0",
539 vec![],
540 vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")],
541 )],
542 ),
543 (
544 "direct_pkg_with_major_version",
545 vec![
546 release("0.1.0", vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")]),
547 release("1.0.0", vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")]),
548 release("1.1.0", vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")]),
549 ],
550 ),
551 (
552 "depends_on_old_version_of_direct_pkg",
553 vec![release(
554 "0.1.0",
555 vec![("direct_pkg_with_major_version", ">= 0.1.0 and < 0.3.0")],
556 )],
557 ),
558 (
559 "this_pkg_depends_on_indirect_pkg",
560 vec![release(
561 "0.1.0",
562 vec![("indirect_pkg_with_major_version", ">= 0.1.0 and < 1.0.0")],
563 )],
564 ),
565 (
566 "indirect_pkg_with_major_version",
567 vec![
568 release("0.1.0", vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")]),
569 release("1.0.0", vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")]),
570 release("1.1.0", vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")]),
571 ],
572 ),
573 ])
574 }
575
576 #[test]
577 fn resolution_with_locked() {
578 let locked_stdlib = ("gleam_stdlib".into(), Version::parse("0.1.0").unwrap());
579 let result = resolve_versions(
580 &make_remote(),
581 HashMap::new(),
582 "app".into(),
583 vec![("gleam_stdlib".into(), Range::new("~> 0.1".into()).unwrap())].into_iter(),
584 &vec![locked_stdlib].into_iter().collect(),
585 )
586 .unwrap();
587 assert_eq!(
588 result,
589 vec![("gleam_stdlib".into(), Version::parse("0.1.0").unwrap())]
590 .into_iter()
591 .collect()
592 );
593 }
594
595 #[test]
596 fn resolution_without_deps() {
597 let result = resolve_versions(
598 &make_remote(),
599 HashMap::new(),
600 "app".into(),
601 vec![].into_iter(),
602 &vec![].into_iter().collect(),
603 )
604 .unwrap();
605 assert_eq!(result, vec![].into_iter().collect())
606 }
607
608 #[test]
609 fn resolution_1_dep() {
610 let result = resolve_versions(
611 &make_remote(),
612 HashMap::new(),
613 "app".into(),
614 vec![("gleam_stdlib".into(), Range::new("~> 0.1".into()).unwrap())].into_iter(),
615 &vec![].into_iter().collect(),
616 )
617 .unwrap();
618 assert_eq!(
619 result,
620 vec![("gleam_stdlib".into(), Version::try_from("0.3.0").unwrap())]
621 .into_iter()
622 .collect()
623 );
624 }
625
626 #[test]
627 fn resolution_with_nested_deps() {
628 let result = resolve_versions(
629 &make_remote(),
630 HashMap::new(),
631 "app".into(),
632 vec![("gleam_otp".into(), Range::new("~> 0.1".into()).unwrap())].into_iter(),
633 &vec![].into_iter().collect(),
634 )
635 .unwrap();
636 assert_eq!(
637 result,
638 vec![
639 ("gleam_otp".into(), Version::try_from("0.2.0").unwrap()),
640 ("gleam_stdlib".into(), Version::try_from("0.3.0").unwrap())
641 ]
642 .into_iter()
643 .collect()
644 );
645 }
646
647 #[test]
648 fn resolution_with_optional_deps() {
649 let result = resolve_versions(
650 &make_remote(),
651 HashMap::new(),
652 "app".into(),
653 vec![(
654 "package_with_optional".into(),
655 Range::new("~> 0.1".into()).unwrap(),
656 )]
657 .into_iter(),
658 &vec![].into_iter().collect(),
659 )
660 .unwrap();
661 assert_eq!(
662 result,
663 vec![(
664 "package_with_optional".into(),
665 Version::try_from("0.1.0").unwrap()
666 )]
667 .into_iter()
668 .collect()
669 );
670 }
671
672 #[test]
673 fn resolution_with_optional_deps_explicitly_provided() {
674 let result = resolve_versions(
675 &make_remote(),
676 HashMap::new(),
677 "app".into(),
678 vec![
679 (
680 "package_with_optional".into(),
681 Range::new("~> 0.1".into()).unwrap(),
682 ),
683 ("gleam_stdlib".into(), Range::new("~> 0.1".into()).unwrap()),
684 ]
685 .into_iter(),
686 &vec![].into_iter().collect(),
687 )
688 .unwrap();
689 assert_eq!(
690 result,
691 vec![
692 ("gleam_stdlib".into(), Version::try_from("0.2.2").unwrap()),
693 (
694 "package_with_optional".into(),
695 Version::try_from("0.1.0").unwrap()
696 ),
697 ]
698 .into_iter()
699 .collect()
700 );
701 }
702
703 #[test]
704 fn resolution_with_optional_deps_incompatible() {
705 let result = resolve_versions(
706 &make_remote(),
707 HashMap::new(),
708 "app".into(),
709 vec![
710 (
711 "package_with_optional".into(),
712 Range::new("~> 0.1".into()).unwrap(),
713 ),
714 ("gleam_stdlib".into(), Range::new("~> 0.3".into()).unwrap()),
715 ]
716 .into_iter(),
717 &vec![].into_iter().collect(),
718 );
719 assert!(result.is_err());
720 }
721
722 #[test]
723 fn resolution_with_optional_deps_required_by_nested_deps() {
724 let result = resolve_versions(
725 &make_remote(),
726 HashMap::new(),
727 "app".into(),
728 vec![
729 (
730 "package_with_optional".into(),
731 Range::new("~> 0.1".into()).unwrap(),
732 ),
733 ("gleam_otp".into(), Range::new("~> 0.1".into()).unwrap()),
734 ]
735 .into_iter(),
736 &vec![].into_iter().collect(),
737 )
738 .unwrap();
739 assert_eq!(
740 result,
741 vec![
742 ("gleam_stdlib".into(), Version::try_from("0.2.2").unwrap()),
743 ("gleam_otp".into(), Version::try_from("0.2.0").unwrap()),
744 (
745 "package_with_optional".into(),
746 Version::try_from("0.1.0").unwrap()
747 ),
748 ]
749 .into_iter()
750 .collect()
751 );
752 }
753
754 #[test]
755 fn resolution_with_optional_deps_keep_constraints() {}
756
757 #[test]
758 fn resolution_locked_to_older_version() {
759 let result = resolve_versions(
760 &make_remote(),
761 HashMap::new(),
762 "app".into(),
763 vec![("gleam_otp".into(), Range::new("~> 0.1.0".into()).unwrap())].into_iter(),
764 &vec![].into_iter().collect(),
765 )
766 .unwrap();
767 assert_eq!(
768 result,
769 vec![
770 ("gleam_otp".into(), Version::try_from("0.1.0").unwrap()),
771 ("gleam_stdlib".into(), Version::try_from("0.3.0").unwrap())
772 ]
773 .into_iter()
774 .collect()
775 );
776 }
777
778 #[test]
779 fn resolution_retired_versions_not_used_by_default() {
780 let result = resolve_versions(
781 &make_remote(),
782 HashMap::new(),
783 "app".into(),
784 vec![(
785 "package_with_retired".into(),
786 Range::new("> 0.0.0".into()).unwrap(),
787 )]
788 .into_iter(),
789 &vec![].into_iter().collect(),
790 )
791 .unwrap();
792 assert_eq!(
793 result,
794 vec![(
795 "package_with_retired".into(),
796 // Uses the older version that hasn't been retired
797 Version::try_from("0.1.0").unwrap()
798 ),]
799 .into_iter()
800 .collect()
801 );
802 }
803
804 #[test]
805 fn resolution_retired_versions_can_be_used_if_locked() {
806 let result = resolve_versions(
807 &make_remote(),
808 HashMap::new(),
809 "app".into(),
810 vec![(
811 "package_with_retired".into(),
812 Range::new("> 0.0.0".into()).unwrap(),
813 )]
814 .into_iter(),
815 &vec![("package_with_retired".into(), Version::new(0, 2, 0))]
816 .into_iter()
817 .collect(),
818 )
819 .unwrap();
820 assert_eq!(
821 result,
822 vec![(
823 "package_with_retired".into(),
824 // Uses the locked version even though it's retired
825 Version::new(0, 2, 0)
826 ),]
827 .into_iter()
828 .collect()
829 );
830 }
831
832 #[test]
833 fn resolution_prerelease_can_be_selected() {
834 let result = resolve_versions(
835 &make_remote(),
836 HashMap::new(),
837 "app".into(),
838 vec![(
839 "gleam_otp".into(),
840 Range::new("~> 0.3.0-rc1".into()).unwrap(),
841 )]
842 .into_iter(),
843 &vec![].into_iter().collect(),
844 )
845 .unwrap();
846 assert_eq!(
847 result,
848 vec![
849 ("gleam_stdlib".into(), Version::try_from("0.3.0").unwrap()),
850 ("gleam_otp".into(), Version::try_from("0.3.0-rc2").unwrap()),
851 ]
852 .into_iter()
853 .collect(),
854 );
855 }
856
857 #[test]
858 fn resolution_exact_prerelease_can_be_selected() {
859 let result = resolve_versions(
860 &make_remote(),
861 HashMap::new(),
862 "app".into(),
863 vec![("gleam_otp".into(), Range::new("0.3.0-rc1".into()).unwrap())].into_iter(),
864 &vec![].into_iter().collect(),
865 )
866 .unwrap();
867 assert_eq!(
868 result,
869 vec![
870 ("gleam_stdlib".into(), Version::try_from("0.3.0").unwrap()),
871 ("gleam_otp".into(), Version::try_from("0.3.0-rc1").unwrap()),
872 ]
873 .into_iter()
874 .collect(),
875 );
876 }
877
878 #[test]
879 fn resolution_not_found_dep() {
880 let err = resolve_versions(
881 &make_remote(),
882 HashMap::new(),
883 "app".into(),
884 vec![("unknown".into(), Range::new("~> 0.1".into()).unwrap())].into_iter(),
885 &vec![].into_iter().collect(),
886 )
887 .unwrap_err();
888 match err {
889 Error::DependencyResolutionError(error) => assert_eq!(
890 error,
891 "An error occurred while choosing the version of unknown: \
892the package unknown was not found in the package repository"
893 ),
894 _ => panic!("wrong error: {err}"),
895 }
896 }
897
898 #[test]
899 fn resolution_no_matching_version() {
900 let _ = resolve_versions(
901 &make_remote(),
902 HashMap::new(),
903 "app".into(),
904 vec![("gleam_stdlib".into(), Range::new("~> 99.0".into()).unwrap())].into_iter(),
905 &vec![].into_iter().collect(),
906 )
907 .unwrap_err();
908 }
909
910 #[test]
911 fn resolution_locked_version_doesnt_satisfy_requirements() {
912 let err = resolve_versions(
913 &make_remote(),
914 HashMap::new(),
915 "app".into(),
916 vec![(
917 "gleam_stdlib".into(),
918 Range::new("~> 0.1.0".into()).unwrap(),
919 )]
920 .into_iter(),
921 &vec![("gleam_stdlib".into(), Version::new(0, 2, 0))]
922 .into_iter()
923 .collect(),
924 )
925 .unwrap_err();
926
927 match err {
928 Error::IncompatibleLockedVersion { error } => assert_eq!(
929 error,
930 "gleam_stdlib is specified with the requirement `~> 0.1.0`, \
931but it is locked to 0.2.0, which is incompatible."
932 ),
933 _ => panic!("wrong error: {err}"),
934 }
935 }
936
937 #[test]
938 fn resolution_with_exact_dep() {
939 let result = resolve_versions(
940 &make_remote(),
941 HashMap::new(),
942 "app".into(),
943 vec![("gleam_stdlib".into(), Range::new("0.1.0".into()).unwrap())].into_iter(),
944 &vec![].into_iter().collect(),
945 )
946 .unwrap();
947 assert_eq!(
948 result,
949 vec![("gleam_stdlib".into(), Version::try_from("0.1.0").unwrap())]
950 .into_iter()
951 .collect()
952 );
953 }
954
955 #[test]
956 fn parse_exact_version_test() {
957 assert_eq!(
958 parse_exact_version("1.0.0"),
959 Some(Version::parse("1.0.0").unwrap())
960 );
961 assert_eq!(
962 parse_exact_version("==1.0.0"),
963 Some(Version::parse("1.0.0").unwrap())
964 );
965 assert_eq!(
966 parse_exact_version("== 1.0.0"),
967 Some(Version::parse("1.0.0").unwrap())
968 );
969 assert_eq!(parse_exact_version("~> 1.0.0"), None);
970 assert_eq!(parse_exact_version(">= 1.0.0"), None);
971 }
972
973 #[test]
974 fn resolve_major_version_upgrades() {
975 let manifest = manifest::Manifest {
976 requirements: vec![
977 (
978 EcoString::from("package_depends_on_indirect_pkg"),
979 requirement::Requirement::Hex {
980 version: Range::new("> 0.1.0 and <= 1.0.0".into()).unwrap(),
981 },
982 ),
983 (
984 EcoString::from("direct_pkg_with_major_version"),
985 requirement::Requirement::Hex {
986 version: Range::new("> 0.1.0 and <= 2.0.0".into()).unwrap(),
987 },
988 ),
989 (
990 EcoString::from("depends_on_old_version_of_direct_pkg"),
991 requirement::Requirement::Hex {
992 version: Range::new("> 0.1.0 and <= 1.0.0".into()).unwrap(),
993 },
994 ),
995 ]
996 .into_iter()
997 .collect(),
998 packages: vec![
999 ManifestPackage {
1000 name: "direct_pkg_with_major_version".into(),
1001 version: Version::parse("0.1.0").unwrap(),
1002 build_tools: ["gleam".into()].into(),
1003 otp_app: None,
1004 requirements: vec![],
1005 source: ManifestPackageSource::Hex {
1006 outer_checksum: Base16Checksum(vec![1, 2, 3]),
1007 },
1008 },
1009 ManifestPackage {
1010 name: "depends_on_old_version_of_direct_pkg".into(),
1011 version: Version::parse("0.1.0").unwrap(),
1012 build_tools: ["gleam".into()].into(),
1013 otp_app: None,
1014 requirements: vec!["direct_pkg_with_major_version".into()],
1015 source: ManifestPackageSource::Hex {
1016 outer_checksum: Base16Checksum(vec![1, 2, 3]),
1017 },
1018 },
1019 ManifestPackage {
1020 name: "pkg_depends_on_indirect_pkg".into(),
1021 version: Version::parse("0.1.0").unwrap(),
1022 build_tools: ["gleam".into()].into(),
1023 otp_app: None,
1024 requirements: vec!["indirect_pkg_with_major_version".into()],
1025 source: ManifestPackageSource::Hex {
1026 outer_checksum: Base16Checksum(vec![1, 2, 3]),
1027 },
1028 },
1029 ManifestPackage {
1030 name: "indirect_pkg_with_major_version".into(),
1031 version: Version::parse("0.1.0").unwrap(),
1032 build_tools: ["gleam".into()].into(),
1033 otp_app: None,
1034 requirements: vec![],
1035 source: ManifestPackageSource::Hex {
1036 outer_checksum: Base16Checksum(vec![1, 2, 3]),
1037 },
1038 },
1039 ],
1040 };
1041 let result = check_for_major_version_updates(&manifest, &make_remote());
1042
1043 // indirect package with major version will not be in the result even though a major
1044 // version of it is available
1045 assert_eq!(
1046 result,
1047 vec![(
1048 "direct_pkg_with_major_version".into(),
1049 (
1050 Version::try_from("0.1.0").unwrap(),
1051 Version::try_from("1.1.0").unwrap()
1052 )
1053 ),]
1054 .into_iter()
1055 .collect()
1056 );
1057 }
1058
1059 #[test]
1060 fn git_deps_are_not_checked_on_hex_for_major_version_updates() {
1061 let manifest = manifest::Manifest {
1062 requirements: vec![(
1063 EcoString::from("wibble"),
1064 requirement::Requirement::Git {
1065 git: "git".into(),
1066 ref_: "ref".into(),
1067 path: None,
1068 },
1069 )]
1070 .into_iter()
1071 .collect(),
1072 packages: vec![ManifestPackage {
1073 name: "wibble".into(),
1074 version: Version::parse("0.1.0").unwrap(),
1075 build_tools: ["gleam".into()].into(),
1076 otp_app: None,
1077 requirements: vec![],
1078 source: ManifestPackageSource::Git {
1079 repo: "repo".into(),
1080 commit: "commit".into(),
1081 path: None,
1082 },
1083 }],
1084 };
1085
1086 // There's a package on hex with the same name of the git dependency
1087 // and a new major version. Those are not the same package though!!
1088 let remote = remote(vec![("wibble", vec![release("2.0.0", vec![])])]);
1089 let result = check_for_major_version_updates(&manifest, &remote);
1090 assert_eq!(result, HashMap::new());
1091 }
1092
1093 #[test]
1094 fn local_deps_are_not_checked_on_hex_for_major_version_updates() {
1095 let manifest = manifest::Manifest {
1096 requirements: vec![(
1097 EcoString::from("wibble"),
1098 requirement::Requirement::Path {
1099 path: "path".into(),
1100 },
1101 )]
1102 .into_iter()
1103 .collect(),
1104 packages: vec![ManifestPackage {
1105 name: "wibble".into(),
1106 version: Version::parse("0.1.0").unwrap(),
1107 build_tools: ["gleam".into()].into(),
1108 otp_app: None,
1109 requirements: vec![],
1110 source: ManifestPackageSource::Local {
1111 path: "path".into(),
1112 },
1113 }],
1114 };
1115
1116 // There's a package on hex with the same name of the local dependency
1117 // and a new major version. Those are not the same package though!!
1118 let remote = remote(vec![("wibble", vec![release("2.0.0", vec![])])]);
1119 let result = check_for_major_version_updates(&manifest, &remote);
1120 assert_eq!(result, HashMap::new());
1121 }
1122
1123 fn retired_release(
1124 version: &str,
1125 requirements: Vec<(&str, &str)>,
1126 reason: hexpm::RetirementReason,
1127 message: &str,
1128 ) -> Release<()> {
1129 Release {
1130 retirement_status: Some(RetirementStatus {
1131 reason,
1132 message: message.into(),
1133 }),
1134 ..release(version, requirements)
1135 }
1136 }
1137 fn release(version: &str, requirements: Vec<(&str, &str)>) -> Release<()> {
1138 release_with_optional(version, requirements, vec![])
1139 }
1140
1141 fn release_with_optional(
1142 version: &str,
1143 requirements: Vec<(&str, &str)>,
1144 optional_requirements: Vec<(&str, &str)>,
1145 ) -> Release<()> {
1146 let mut all_requirements = HashMap::new();
1147
1148 for (name, range) in requirements {
1149 let requirement = Range::new(range.to_string()).unwrap();
1150 let dependency = Dependency {
1151 requirement,
1152 optional: false,
1153 app: None,
1154 repository: None,
1155 };
1156 let _ = all_requirements.insert(name.to_string(), dependency);
1157 }
1158
1159 for (name, range) in optional_requirements {
1160 let requirement = Range::new(range.to_string()).unwrap();
1161 let dependency = Dependency {
1162 requirement,
1163 optional: true,
1164 app: None,
1165 repository: None,
1166 };
1167 let _ = all_requirements.insert(name.to_string(), dependency);
1168 }
1169
1170 Release {
1171 version: Version::try_from(version).unwrap(),
1172 requirements: all_requirements,
1173 retirement_status: None,
1174 outer_checksum: vec![1, 2, 3],
1175 meta: (),
1176 }
1177 }
1178
1179 fn remote(dependencies: Vec<(&str, Vec<Release<()>>)>) -> Remote {
1180 let mut deps = HashMap::new();
1181 for (package, releases) in dependencies {
1182 let _ = deps.insert(
1183 package.into(),
1184 Rc::new(hexpm::Package {
1185 name: package.into(),
1186 repository: "hexpm".into(),
1187 releases,
1188 }),
1189 );
1190 }
1191 Remote { deps }
1192 }
1193
1194 #[test]
1195 fn resolution_error_message() {
1196 let remote = remote(vec![
1197 (
1198 "wibble",
1199 vec![
1200 release("1.2.0", vec![("wobble", ">= 1.0.0 and < 2.0.0")]),
1201 release("1.3.0", vec![("wobble", ">= 2.0.0 and < 3.0.0")]),
1202 ],
1203 ),
1204 (
1205 "wobble",
1206 vec![
1207 release("1.1.0", vec![("woo", ">= 1.0.0 and < 2.0.0")]),
1208 release("2.0.0", vec![("waa", ">= 1.0.0 and < 2.0.0")]),
1209 ],
1210 ),
1211 (
1212 "woo",
1213 vec![release("1.0.0", vec![]), release("2.0.0", vec![])],
1214 ),
1215 (
1216 "waa",
1217 vec![release("1.0.0", vec![]), release("2.0.0", vec![])],
1218 ),
1219 ]);
1220
1221 let result = resolve_versions(
1222 &remote,
1223 HashMap::new(),
1224 "app".into(),
1225 vec![
1226 (
1227 "wibble".into(),
1228 Range::new(">= 1.0.0 and < 2.0.0".into()).unwrap(),
1229 ),
1230 (
1231 "woo".into(),
1232 Range::new(">= 2.0.0 and < 3.0.0".into()).unwrap(),
1233 ),
1234 (
1235 "waa".into(),
1236 Range::new(">= 2.0.0 and < 3.0.0".into()).unwrap(),
1237 ),
1238 ]
1239 .into_iter(),
1240 &vec![].into_iter().collect(),
1241 );
1242
1243 if let Err(error @ Error::DependencyResolutionNoSolution { .. }) = result {
1244 let message = error.pretty_string();
1245 insta::assert_snapshot!(message)
1246 } else {
1247 panic!("expected a resolution error message")
1248 }
1249 }
1250
1251 #[test]
1252 fn adding_package_version_that_does_not_exist() {
1253 let remote = remote(vec![(
1254 "wibble",
1255 vec![release("1.2.0", vec![]), release("1.3.0", vec![])],
1256 )]);
1257
1258 let result = resolve_versions(
1259 &remote,
1260 HashMap::new(),
1261 "app".into(),
1262 vec![("wibble".into(), Range::new("1.0.0".into()).unwrap())].into_iter(),
1263 &vec![].into_iter().collect(),
1264 );
1265
1266 if let Err(error @ Error::DependencyResolutionNoSolution { .. }) = result {
1267 let message = error.pretty_string();
1268 insta::assert_snapshot!(message)
1269 } else {
1270 panic!("expected a resolution error message")
1271 }
1272 }
1273
1274 #[test]
1275 fn adding_package_version_that_does_not_exist_2() {
1276 let remote = remote(vec![(
1277 "wibble",
1278 vec![release("1.2.0", vec![]), release("1.3.0", vec![])],
1279 )]);
1280
1281 let result = resolve_versions(
1282 &remote,
1283 HashMap::new(),
1284 "app".into(),
1285 vec![("wibble".into(), Range::new("> 2.0.0".into()).unwrap())].into_iter(),
1286 &vec![].into_iter().collect(),
1287 );
1288
1289 if let Err(error @ Error::DependencyResolutionNoSolution { .. }) = result {
1290 let message = error.pretty_string();
1291 insta::assert_snapshot!(message)
1292 } else {
1293 panic!("expected a resolution error message")
1294 }
1295 }
1296}