this repo has no description
at wasm 1104 lines 36 kB view raw
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}