Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

gleam / compiler-core / src / dependency.rs
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}