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