diff --git a/Cargo.lock b/Cargo.lock index b58ea3d5fa4..56f91bdef46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -226,7 +226,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror", + "thiserror 1.0.69", "time", ] @@ -687,7 +687,7 @@ checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", "crossbeam-utils", - "hashbrown", + "hashbrown 0.14.3", "lock_api", "once_cell", "parking_lot_core", @@ -927,7 +927,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" dependencies = [ - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1191,7 +1191,7 @@ dependencies = [ "strum", "tar", "termcolor", - "thiserror", + "thiserror 1.0.69", "toml", "tracing", "unicode-segmentation", @@ -1239,6 +1239,12 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" + [[package]] name = "heck" version = "0.5.0" @@ -1253,9 +1259,9 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hexpm" -version = "3.3.0" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4ea53d065cbbcc44f7a5f1814ae2c526d934c5344b97caf2953125aea28e79" +checksum = "4f62182f03cb6894803f78f69dd22720bb9ed5ee6f7e5a64034c67a043e72f81" dependencies = [ "base16", "bytes", @@ -1270,7 +1276,7 @@ dependencies = [ "ring", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", "url", "x509-parser", ] @@ -1428,7 +1434,7 @@ dependencies = [ "log", "serde", "serde_derive", - "thiserror", + "thiserror 1.0.69", "unic-langid", ] @@ -1448,7 +1454,7 @@ dependencies = [ "log", "parking_lot", "rust-embed", - "thiserror", + "thiserror 1.0.69", "unic-langid", "walkdir", ] @@ -1688,12 +1694,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.3", ] [[package]] @@ -2207,6 +2213,17 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "priority-queue" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef08705fa1589a1a59aa924ad77d14722cb0cd97b67dd5004ed5f4a4873fce8d" +dependencies = [ + "autocfg", + "equivalent", + "indexmap", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -2301,12 +2318,16 @@ dependencies = [ [[package]] name = "pubgrub" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd14552ad5f5d743a323c10d576f26822a044355d6601f377d813ece46f38fd" +checksum = "3f5df7e552bc7edd075f5783a87fbfc21d6a546e32c16985679c488c18192d83" dependencies = [ - "rustc-hash 1.1.0", - "thiserror", + "indexmap", + "log", + "priority-queue", + "rustc-hash 2.0.0", + "thiserror 2.0.12", + "version-ranges", ] [[package]] @@ -2339,7 +2360,7 @@ dependencies = [ "quinn-udp", "rustc-hash 1.1.0", "rustls", - "thiserror", + "thiserror 1.0.69", "tokio", "tracing", ] @@ -2356,7 +2377,7 @@ dependencies = [ "rustc-hash 2.0.0", "rustls", "slab", - "thiserror", + "thiserror 1.0.69", "tinyvec", "tracing", ] @@ -2492,7 +2513,7 @@ checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3171,7 +3192,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -3185,6 +3215,17 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -3288,21 +3329,28 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap", "toml_datetime", + "toml_write", "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" + [[package]] name = "tower" version = "0.5.2" @@ -3558,6 +3606,15 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eab68b56840f69efb0fefbe3ab6661499217ffdc58e2eef7c3f6f69835386322" +[[package]] +name = "version-ranges" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8d079415ceb2be83fc355adbadafe401307d5c309c7e6ade6638e6f9f42f42d" +dependencies = [ + "smallvec", +] + [[package]] name = "version_check" version = "0.9.4" @@ -3999,9 +4056,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.26" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" +checksum = "d9fb597c990f03753e08d3c29efbfcf2019a003b4bf4ba19225c158e1549f0f3" dependencies = [ "memchr", ] @@ -4052,7 +4109,7 @@ dependencies = [ "nom", "oid-registry", "rusticata-macros", - "thiserror", + "thiserror 1.0.69", "time", ] diff --git a/Cargo.toml b/Cargo.toml index 5aad3fcbed3..16d751b513f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ toml = "0" # Enum trait impl macros strum = { version = "0", features = ["derive"] } # Hex package manager client -hexpm = "3.3" +hexpm = "4" # Creation of tar file archives tar = "0" # gzip compression @@ -61,4 +61,4 @@ pretty_assertions = "1" insta = { version = "1", features = ["glob"] } # A transitive dependency needed to compile into wasm32-unknown-unknown # See https://docs.rs/getrandom/latest/getrandom/index.html#webassembly-support -getrandom = { version = "0", features = ["js"] } +getrandom = { version = "0.2", features = ["js"] } diff --git a/compiler-cli/Cargo.toml b/compiler-cli/Cargo.toml index 1ee180eb1ef..f7a841deb17 100644 --- a/compiler-cli/Cargo.toml +++ b/compiler-cli/Cargo.toml @@ -36,7 +36,7 @@ same-file = "1" # Open generated docs in browser opener = "0" # Pubgrub dependency resolution algorithm -pubgrub = "0" +pubgrub = "0.3" camino = { workspace = true, features = ["serde1"] } async-trait.workspace = true diff --git a/compiler-cli/src/dependencies.rs b/compiler-cli/src/dependencies.rs index 06fa1afff53..b4f71326542 100644 --- a/compiler-cli/src/dependencies.rs +++ b/compiler-cli/src/dependencies.rs @@ -15,7 +15,7 @@ use gleam_core::{ Error, Result, build::{Mode, Target, Telemetry}, config::PackageConfig, - dependency, + dependency::{self, PackageFetchError}, error::{FileIoAction, FileKind, ShellCommandFailureReason, StandardIoAction}, hex::{self, HEXPM_PUBLIC_KEY}, io::{HttpClient as _, TarUnpacker, WrappedReader}, @@ -314,7 +314,10 @@ fn remove_extra_requirements(config: &PackageConfig, manifest: &mut Manifest) -> pub fn parse_gleam_add_specifier(package: &str) -> Result<(EcoString, Requirement)> { let Some((package, version)) = package.split_once('@') else { // Default to the latest version available. - return Ok((package.into(), Requirement::hex(">= 0.0.0"))); + return Ok(( + package.into(), + Requirement::hex(">= 0.0.0").expect("'>= 0.0.0' should be a valid pubgrub range"), + )); }; // Parse the major and minor from the provided semantic version. @@ -357,7 +360,7 @@ pub fn parse_gleam_add_specifier(package: &str) -> Result<(EcoString, Requiremen ), }); } - }; + }?; Ok((package.into(), requirement)) } @@ -958,7 +961,8 @@ fn provide_package( match provided.get(&package_name) { Some(package) if package.source == package_source => { // This package has already been provided from this source, return the version - let version = hexpm::version::Range::new(format!("== {}", &package.version)); + let version = hexpm::version::Range::new(format!("== {}", &package.version)) + .expect("== {version} should be a valid range"); return Ok(version); } Some(package) => { @@ -1006,7 +1010,8 @@ fn provide_package( } let _ = parents.pop(); // Add the package to the provided packages dictionary - let version = hexpm::version::Range::new(format!("== {}", &config.version)); + let version = hexpm::version::Range::new(format!("== {}", &config.version)) + .expect("== {version} should be a valid range"); let _ = provided.insert( config.name, ProvidedPackage { @@ -1160,10 +1165,7 @@ impl TarUnpacker for Untar { } impl dependency::PackageFetcher for PackageFetcher { - fn get_dependencies( - &self, - package: &str, - ) -> Result, Box> { + fn get_dependencies(&self, package: &str) -> Result, PackageFetchError> { { let runtime_cache = self.runtime_cache.borrow(); let result = runtime_cache.get(package); @@ -1179,21 +1181,13 @@ impl dependency::PackageFetcher for PackageFetcher { let response = self .runtime .block_on(self.http.send(request)) - .map_err(Box::new)?; - - match hexpm::get_package_response(response, HEXPM_PUBLIC_KEY) { - Ok(pkg) => { - let pkg = Rc::new(pkg); - let pkg_ref = Rc::clone(&pkg); - self.cache_package(package, pkg); - Ok(pkg_ref) - } - Err(e) => match e { - hexpm::ApiError::NotFound => { - Err(format!("I couldn't find a package called `{}`", package).into()) - } - _ => Err(e.into()), - }, - } + .map_err(PackageFetchError::fetch_error)?; + + let pkg = hexpm::get_package_response(response, HEXPM_PUBLIC_KEY) + .map_err(PackageFetchError::from)?; + let pkg = Rc::new(pkg); + let pkg_ref = Rc::clone(&pkg); + self.cache_package(package, pkg); + Ok(pkg_ref) } } diff --git a/compiler-cli/src/dependencies/tests.rs b/compiler-cli/src/dependencies/tests.rs index 2f64a7e04b7..d1be9e33c9e 100644 --- a/compiler-cli/src/dependencies/tests.rs +++ b/compiler-cli/src/dependencies/tests.rs @@ -320,60 +320,36 @@ fn parse_gleam_add_specifier_non_numeric_version() { #[test] fn parse_gleam_add_specifier_default() { let provided = "some_package"; - let expected = ">= 0.0.0"; + let expected = Requirement::hex(">= 0.0.0").unwrap(); let (package, version) = parse_gleam_add_specifier(provided).unwrap(); - match &version { - Requirement::Hex { version: v } => { - assert!(v.to_pubgrub().is_ok(), "failed pubgrub parse: {v}"); - } - _ => assert!(false, "failed hexpm version parse: {provided}"), - } - assert_eq!(version, Requirement::hex(expected)); + assert_eq!(version, expected); assert_eq!("some_package", package); } #[test] fn parse_gleam_add_specifier_major_only() { let provided = "wobble@1"; - let expected = ">= 1.0.0 and < 2.0.0"; + let expected = Requirement::hex(">= 1.0.0 and < 2.0.0").unwrap(); let (package, version) = parse_gleam_add_specifier(provided).unwrap(); - match &version { - Requirement::Hex { version: v } => { - assert!(v.to_pubgrub().is_ok(), "failed pubgrub parse: {v}"); - } - _ => assert!(false, "failed hexpm version parse: {provided}"), - } - assert_eq!(version, Requirement::hex(expected)); + assert_eq!(version, expected); assert_eq!("wobble", package); } #[test] fn parse_gleam_add_specifier_major_and_minor() { let provided = "wibble@1.2"; - let expected = ">= 1.2.0 and < 2.0.0"; + let expected = Requirement::hex(">= 1.2.0 and < 2.0.0").unwrap(); let (package, version) = parse_gleam_add_specifier(provided).unwrap(); - match &version { - Requirement::Hex { version: v } => { - assert!(v.to_pubgrub().is_ok(), "failed pubgrub parse: {v}"); - } - _ => assert!(false, "failed hexpm version parse: {provided}"), - } - assert_eq!(version, Requirement::hex(expected)); + assert_eq!(version, expected); assert_eq!("wibble", package); } #[test] fn parse_gleam_add_specifier_major_minor_and_patch() { let provided = "bobble@1.2.3"; - let expected = "1.2.3"; + let expected = Requirement::hex("1.2.3").unwrap(); let (package, version) = parse_gleam_add_specifier(provided).unwrap(); - match &version { - Requirement::Hex { version: v } => { - assert!(v.to_pubgrub().is_ok(), "failed pubgrub parse: {v}"); - } - _ => assert!(false, "failed hexpm version parse: {provided}"), - } - assert_eq!(version, Requirement::hex(expected)); + assert_eq!(version, expected); assert_eq!("bobble", package); } @@ -533,7 +509,10 @@ fn provide_existing_package() { &mut provided, &mut vec!["root".into(), "subpackage".into()], ); - assert_eq!(result, Ok(hexpm::version::Range::new("== 0.1.0".into()))); + assert_eq!( + result, + Ok(hexpm::version::Range::new("== 0.1.0".into()).unwrap()) + ); let result = provide_local_package( "hello_world".into(), @@ -543,7 +522,10 @@ fn provide_existing_package() { &mut provided, &mut vec!["root".into(), "subpackage".into()], ); - assert_eq!(result, Ok(hexpm::version::Range::new("== 0.1.0".into()))); + assert_eq!( + result, + Ok(hexpm::version::Range::new("== 0.1.0".into()).unwrap()) + ); } #[test] @@ -558,7 +540,10 @@ fn provide_conflicting_package() { &mut provided, &mut vec!["root".into(), "subpackage".into()], ); - assert_eq!(result, Ok(hexpm::version::Range::new("== 0.1.0".into()))); + assert_eq!( + result, + Ok(hexpm::version::Range::new("== 0.1.0".into()).unwrap()) + ); let result = provide_package( "hello_world".into(), @@ -592,7 +577,10 @@ fn provided_is_absolute() { &mut provided, &mut vec!["root".into(), "subpackage".into()], ); - assert_eq!(result, Ok(hexpm::version::Range::new("== 0.1.0".into()))); + assert_eq!( + result, + Ok(hexpm::version::Range::new("== 0.1.0".into()).unwrap()) + ); let package = provided.get("hello_world").unwrap().clone(); match package.source { ProvidedPackageSource::Local { path } => { @@ -634,11 +622,11 @@ fn provided_local_to_hex() { requirements: [ ( "req_1".into(), - hexpm::version::Range::new("~> 1.0.0".into()), + hexpm::version::Range::new("~> 1.0.0".into()).unwrap(), ), ( "req_2".into(), - hexpm::version::Range::new("== 1.0.0".into()), + hexpm::version::Range::new("== 1.0.0".into()).unwrap(), ), ] .into(), @@ -656,7 +644,7 @@ fn provided_local_to_hex() { ( "req_1".into(), hexpm::Dependency { - requirement: hexpm::version::Range::new("~> 1.0.0".into()), + requirement: hexpm::version::Range::new("~> 1.0.0".into()).unwrap(), optional: false, app: None, repository: None, @@ -665,7 +653,7 @@ fn provided_local_to_hex() { ( "req_2".into(), hexpm::Dependency { - requirement: hexpm::version::Range::new("== 1.0.0".into()), + requirement: hexpm::version::Range::new("== 1.0.0".into()).unwrap(), optional: false, app: None, repository: None, @@ -693,11 +681,11 @@ fn provided_git_to_hex() { requirements: [ ( "req_1".into(), - hexpm::version::Range::new("~> 1.0.0".into()), + hexpm::version::Range::new("~> 1.0.0".into()).unwrap(), ), ( "req_2".into(), - hexpm::version::Range::new("== 1.0.0".into()), + hexpm::version::Range::new("== 1.0.0".into()).unwrap(), ), ] .into(), @@ -715,7 +703,7 @@ fn provided_git_to_hex() { ( "req_1".into(), hexpm::Dependency { - requirement: hexpm::version::Range::new("~> 1.0.0".into()), + requirement: hexpm::version::Range::new("~> 1.0.0".into()).unwrap(), optional: false, app: None, repository: None, @@ -724,7 +712,7 @@ fn provided_git_to_hex() { ( "req_2".into(), hexpm::Dependency { - requirement: hexpm::version::Range::new("== 1.0.0".into()), + requirement: hexpm::version::Range::new("== 1.0.0".into()).unwrap(), optional: false, app: None, repository: None, @@ -751,11 +739,11 @@ fn provided_local_to_manifest() { requirements: [ ( "req_1".into(), - hexpm::version::Range::new("~> 1.0.0".into()), + hexpm::version::Range::new("~> 1.0.0".into()).unwrap(), ), ( "req_2".into(), - hexpm::version::Range::new("== 1.0.0".into()), + hexpm::version::Range::new("== 1.0.0".into()).unwrap(), ), ] .into(), @@ -789,11 +777,11 @@ fn provided_git_to_manifest() { requirements: [ ( "req_1".into(), - hexpm::version::Range::new("~> 1.0.0".into()), + hexpm::version::Range::new("~> 1.0.0".into()).unwrap(), ), ( "req_2".into(), - hexpm::version::Range::new("== 1.0.0".into()), + hexpm::version::Range::new("== 1.0.0".into()).unwrap(), ), ] .into(), @@ -875,7 +863,7 @@ fn create_testable_unlock_manifest( ( name, Requirement::Hex { - version: hexpm::version::Range::new(range.into()), + version: hexpm::version::Range::new(range.into()).unwrap(), }, ) }) @@ -1219,14 +1207,14 @@ fn package_config( #[test] fn test_remove_do_nothing() { let config = package_config( - HashMap::from([("a".into(), Requirement::hex("~>1"))]), - HashMap::from([("b".into(), Requirement::hex("~>2"))]), + HashMap::from([("a".into(), Requirement::hex("~>1.0").unwrap())]), + HashMap::from([("b".into(), Requirement::hex("~>2.0").unwrap())]), ); let mut manifest = Manifest { requirements: HashMap::from([ - ("a".into(), Requirement::hex("~>1")), - ("b".into(), Requirement::hex("~>2")), + ("a".into(), Requirement::hex("~>1.0").unwrap()), + ("b".into(), Requirement::hex("~>2.0").unwrap()), ]), packages: vec![ manifest_package("a", "1.0.0", vec![]), @@ -1247,7 +1235,7 @@ fn test_remove_simple() { let config = package_config(HashMap::new(), HashMap::new()); let mut manifest = Manifest { - requirements: HashMap::from([("a".into(), Requirement::hex("~>1"))]), + requirements: HashMap::from([("a".into(), Requirement::hex("~>1.0").unwrap())]), packages: vec![manifest_package("a", "1.0.0", vec![])], }; @@ -1262,7 +1250,7 @@ fn test_remove_package_with_transitive_dependencies() { let config = package_config(HashMap::new(), HashMap::new()); let mut manifest = Manifest { - requirements: HashMap::from([("a".into(), Requirement::hex("~>1"))]), + requirements: HashMap::from([("a".into(), Requirement::hex("~>1.0").unwrap())]), packages: vec![ manifest_package("a", "1.0.0", vec!["b".into()]), manifest_package("b", "1.2.3", vec!["c".into()]), @@ -1279,14 +1267,14 @@ fn test_remove_package_with_transitive_dependencies() { #[test] fn test_remove_package_with_shared_transitive_dependencies() { let config = package_config( - HashMap::from([("a".into(), Requirement::hex("~>1"))]), + HashMap::from([("a".into(), Requirement::hex("~>1.0").unwrap())]), HashMap::new(), ); let mut manifest = Manifest { requirements: HashMap::from([ - ("a".into(), Requirement::hex("~>1")), - ("b".into(), Requirement::hex("~>1")), + ("a".into(), Requirement::hex("~>1.0").unwrap()), + ("b".into(), Requirement::hex("~>1.0").unwrap()), ]), packages: vec![ manifest_package("a", "1.0.0", vec!["c".into()]), @@ -1311,14 +1299,14 @@ fn test_remove_package_with_shared_transitive_dependencies() { #[test] fn test_remove_package_that_is_also_a_transitive_dependency() { let config = package_config( - HashMap::from([("a".into(), Requirement::hex("~>1"))]), + HashMap::from([("a".into(), Requirement::hex("~>1.0").unwrap())]), HashMap::new(), ); let mut manifest = Manifest { requirements: HashMap::from([ - ("a".into(), Requirement::hex("~>1")), - ("b".into(), Requirement::hex("~>1")), + ("a".into(), Requirement::hex("~>1.0").unwrap()), + ("b".into(), Requirement::hex("~>1.0").unwrap()), ]), packages: vec![ manifest_package("a", "1.0.0", vec!["b".into(), "c".into()]), diff --git a/compiler-cli/src/publish.rs b/compiler-cli/src/publish.rs index a682374a48c..674ab63ef5d 100644 --- a/compiler-cli/src/publish.rs +++ b/compiler-cli/src/publish.rs @@ -335,8 +335,7 @@ fn do_build_hex_tarball(paths: &ProjectPaths, config: &mut PackageConfig) -> Res // inferred lower bound could be lower. let minimum_required_version = std::cmp::max(minimum_required_version, Version::new(1, 0, 0)); - let inferred_version_range = - pubgrub::range::Range::higher_than(minimum_required_version); + let inferred_version_range = pubgrub::Range::higher_than(minimum_required_version); config.gleam_version = Some(GleamVersion::from_pubgrub(inferred_version_range)); } // Otherwise we need to check that the annotated version range is @@ -699,8 +698,8 @@ fn release_metadata_as_erlang() { let version = "1.2.3".try_into().unwrap(); let homepage = "https://gleam.run".parse().unwrap(); let github = "https://github.com/lpil/myapp".parse().unwrap(); - let req1 = Range::new("~> 1.2.3 or >= 5.0.0".into()); - let req2 = Range::new("~> 1.2".into()); + let req1 = Range::new("~> 1.2.3 or >= 5.0.0".into()).unwrap(); + let req2 = Range::new("~> 1.2".into()).unwrap(); let meta = ReleaseMetadata { name: "myapp", version: &version, diff --git a/compiler-core/Cargo.toml b/compiler-core/Cargo.toml index 9db96fbddb4..e371bbf3be9 100644 --- a/compiler-core/Cargo.toml +++ b/compiler-core/Cargo.toml @@ -31,7 +31,7 @@ globset = { version = "0", features = ["serde1"] } # Checksums xxhash-rust = { version = "0", features = ["xxh3"] } # Pubgrub dependency resolution algorithm -pubgrub = "0" +pubgrub = "0.3" # Used for converting absolute path to relative path pathdiff = { version = "0", features = ["camino"] } # Memory arena using ids rather than references diff --git a/compiler-core/src/analyse.rs b/compiler-core/src/analyse.rs index 7b3c018923a..54c24fb70fa 100644 --- a/compiler-core/src/analyse.rs +++ b/compiler-core/src/analyse.rs @@ -214,7 +214,7 @@ impl<'a, A> ModuleAnalyzer<'a, A> { .package_config .gleam_version .clone() - .map(|version| version.as_pubgrub()), + .map(|version| version.into()), current_module: self.module_name.clone(), target: self.target, importable_modules: self.importable_modules, diff --git a/compiler-core/src/build/project_compiler.rs b/compiler-core/src/build/project_compiler.rs index 60d3c55035d..558ae971141 100644 --- a/compiler-core/src/build/project_compiler.rs +++ b/compiler-core/src/build/project_compiler.rs @@ -24,7 +24,7 @@ use crate::{ use ecow::EcoString; use hexpm::version::Version; use itertools::Itertools; -use pubgrub::range::Range; +use pubgrub::Range; use std::{ cmp, collections::{HashMap, HashSet}, diff --git a/compiler-core/src/config.rs b/compiler-core/src/config.rs index 2edd4c08887..c9e29d7e031 100644 --- a/compiler-core/src/config.rs +++ b/compiler-core/src/config.rs @@ -9,7 +9,7 @@ use crate::{Error, Result}; use camino::{Utf8Path, Utf8PathBuf}; use ecow::EcoString; use globset::{Glob, GlobSetBuilder}; -use hexpm::version::{self, Version}; +use hexpm::version::{self, LowestVersion, Version}; use http::Uri; use serde::ser::SerializeSeq; use serde::{Deserialize, Serialize}; @@ -80,39 +80,50 @@ impl AsRef for SpdxLicense { } #[derive(Debug, PartialEq, Clone)] -pub struct GleamVersion { - pubgrub: pubgrub::range::Range, - hex: version::Range, +pub struct GleamVersion(version::Range); +impl From for GleamVersion { + fn from(range: version::Range) -> Self { + Self(range) + } +} + +impl From for version::Range { + fn from(gleam_version: GleamVersion) -> Self { + gleam_version.0 + } +} + +impl From for pubgrub::Range { + fn from(gleam_version: GleamVersion) -> Self { + gleam_version.0.into() + } } impl GleamVersion { - pub fn from_pubgrub(range: pubgrub::range::Range) -> Self { - Self { - hex: version::Range::new(range.to_string()), - pubgrub: range, - } + pub fn from_pubgrub(range: pubgrub::Range) -> Self { + let range: version::Range = range.into(); + range.into() } - pub fn as_pubgrub(&self) -> pubgrub::range::Range { - self.pubgrub.clone() + pub fn as_pubgrub(&self) -> &pubgrub::Range { + self.0.to_pubgrub() } pub fn new(spec: String) -> Result { - let hex = version::Range::new(spec.to_string()); - let pubgrub = hex.to_pubgrub().map_err(|e| Error::InvalidVersionFormat { - input: spec, - error: e.to_string(), - })?; - - Ok(Self { pubgrub, hex }) + let hex = + version::Range::new(spec.to_string()).map_err(|e| Error::InvalidVersionFormat { + input: spec, + error: e.to_string(), + })?; + Ok(hex.into()) } pub fn lowest_version(&self) -> Option { - self.pubgrub.lowest_version() + self.as_pubgrub().lowest_version() } pub fn hex(&self) -> &version::Range { - &self.hex + &self.0 } } @@ -162,7 +173,7 @@ where S: serde::Serializer, { match gleam_gersion { - Some(version) => serializer.serialize_str(&version.hex.to_string()), + Some(version) => serializer.serialize_str(&version.hex().to_string()), None => serializer.serialize_none(), } } @@ -173,10 +184,8 @@ where { match Deserialize::deserialize(deserialiser)? { Some(range_string) => { - let hex = version::Range::new(range_string); - let pubgrub = hex.clone().to_pubgrub().map_err(serde::de::Error::custom)?; - - Ok(Some(GleamVersion { hex, pubgrub })) + let hex = version::Range::new(range_string).map_err(serde::de::Error::custom)?; + Ok(Some(hex.into())) } None => Ok(None), } @@ -272,7 +281,7 @@ impl PackageConfig { // with the current compiler version pub fn check_gleam_compatibility(&self) -> Result<(), Error> { if let Some(version) = &self.gleam_version { - let range = &version.pubgrub; + let range = version.as_pubgrub(); let compiler_version = Version::parse(COMPILER_VERSION).expect("Parse compiler semantic version"); @@ -296,13 +305,13 @@ impl PackageConfig { fn locked_no_manifest() { let mut config = PackageConfig::default(); config.dependencies = [ - ("prod1".into(), Requirement::hex("~> 1.0")), - ("prod2".into(), Requirement::hex("~> 2.0")), + ("prod1".into(), Requirement::hex("~> 1.0").unwrap()), + ("prod2".into(), Requirement::hex("~> 2.0").unwrap()), ] .into(); config.dev_dependencies = [ - ("dev1".into(), Requirement::hex("~> 1.0")), - ("dev2".into(), Requirement::hex("~> 2.0")), + ("dev1".into(), Requirement::hex("~> 1.0").unwrap()), + ("dev2".into(), Requirement::hex("~> 2.0").unwrap()), ] .into(); assert_eq!(config.locked(None).unwrap(), [].into()); @@ -312,13 +321,13 @@ fn locked_no_manifest() { fn locked_no_changes() { let mut config = PackageConfig::default(); config.dependencies = [ - ("prod1".into(), Requirement::hex("~> 1.0")), - ("prod2".into(), Requirement::hex("~> 2.0")), + ("prod1".into(), Requirement::hex("~> 1.0").unwrap()), + ("prod2".into(), Requirement::hex("~> 2.0").unwrap()), ] .into(); config.dev_dependencies = [ - ("dev1".into(), Requirement::hex("~> 1.0")), - ("dev2".into(), Requirement::hex("~> 2.0")), + ("dev1".into(), Requirement::hex("~> 1.0").unwrap()), + ("dev2".into(), Requirement::hex("~> 2.0").unwrap()), ] .into(); let manifest = Manifest { @@ -345,8 +354,8 @@ fn locked_no_changes() { #[test] fn locked_some_removed() { let mut config = PackageConfig::default(); - config.dependencies = [("prod1".into(), Requirement::hex("~> 1.0"))].into(); - config.dev_dependencies = [("dev2".into(), Requirement::hex("~> 2.0"))].into(); + config.dependencies = [("prod1".into(), Requirement::hex("~> 1.0").unwrap())].into(); + config.dev_dependencies = [("dev2".into(), Requirement::hex("~> 2.0").unwrap())].into(); let manifest = Manifest { requirements: config.all_direct_dependencies().unwrap(), packages: vec![ @@ -372,21 +381,21 @@ fn locked_some_removed() { fn locked_some_changed() { let mut config = PackageConfig::default(); config.dependencies = [ - ("prod1".into(), Requirement::hex("~> 3.0")), // Does not match manifest - ("prod2".into(), Requirement::hex("~> 2.0")), + ("prod1".into(), Requirement::hex("~> 3.0").unwrap()), // Does not match manifest + ("prod2".into(), Requirement::hex("~> 2.0").unwrap()), ] .into(); config.dev_dependencies = [ - ("dev1".into(), Requirement::hex("~> 3.0")), // Does not match manifest - ("dev2".into(), Requirement::hex("~> 2.0")), + ("dev1".into(), Requirement::hex("~> 3.0").unwrap()), // Does not match manifest + ("dev2".into(), Requirement::hex("~> 2.0").unwrap()), ] .into(); let manifest = Manifest { requirements: [ - ("prod1".into(), Requirement::hex("~> 1.0")), - ("prod2".into(), Requirement::hex("~> 2.0")), - ("dev1".into(), Requirement::hex("~> 1.0")), - ("dev2".into(), Requirement::hex("~> 2.0")), + ("prod1".into(), Requirement::hex("~> 1.0").unwrap()), + ("prod2".into(), Requirement::hex("~> 2.0").unwrap()), + ("dev1".into(), Requirement::hex("~> 1.0").unwrap()), + ("dev2".into(), Requirement::hex("~> 2.0").unwrap()), ] .into(), packages: vec![ @@ -412,15 +421,15 @@ fn locked_some_changed() { fn locked_nested_are_removed_too() { let mut config = PackageConfig::default(); config.dependencies = [ - ("1".into(), Requirement::hex("~> 2.0")), // Does not match manifest - ("2".into(), Requirement::hex("~> 1.0")), + ("1".into(), Requirement::hex("~> 2.0").unwrap()), // Does not match manifest + ("2".into(), Requirement::hex("~> 1.0").unwrap()), ] .into(); config.dev_dependencies = [].into(); let manifest = Manifest { requirements: [ - ("1".into(), Requirement::hex("~> 1.0")), - ("2".into(), Requirement::hex("~> 1.0")), + ("1".into(), Requirement::hex("~> 1.0").unwrap()), + ("2".into(), Requirement::hex("~> 1.0").unwrap()), ] .into(), packages: vec![ @@ -463,16 +472,16 @@ fn locked_nested_are_removed_too() { fn locked_unlock_new() { let mut config = PackageConfig::default(); config.dependencies = [ - ("1".into(), Requirement::hex("~> 1.0")), - ("2".into(), Requirement::hex("~> 1.0")), - ("3".into(), Requirement::hex("~> 3.0")), // Does not match manifest + ("1".into(), Requirement::hex("~> 1.0").unwrap()), + ("2".into(), Requirement::hex("~> 1.0").unwrap()), + ("3".into(), Requirement::hex("~> 3.0").unwrap()), // Does not match manifest ] .into(); config.dev_dependencies = [].into(); let manifest = Manifest { requirements: [ - ("1".into(), Requirement::hex("~> 1.0")), - ("2".into(), Requirement::hex("~> 1.0")), + ("1".into(), Requirement::hex("~> 1.0").unwrap()), + ("2".into(), Requirement::hex("~> 1.0").unwrap()), ] .into(), packages: vec![ diff --git a/compiler-core/src/config/stale_package_remover.rs b/compiler-core/src/config/stale_package_remover.rs index 527399432f1..1f86449a51d 100644 --- a/compiler-core/src/config/stale_package_remover.rs +++ b/compiler-core/src/config/stale_package_remover.rs @@ -95,7 +95,7 @@ mod tests { let requirements = HashMap::from_iter([( "required_package".into(), Requirement::Hex { - version: Range::new("1.0.0".into()), + version: Range::new("1.0.0".into()).unwrap(), }, )]); let manifest = Manifest { diff --git a/compiler-core/src/dependency.rs b/compiler-core/src/dependency.rs index 12910eceb55..c5550d34e0b 100644 --- a/compiler-core/src/dependency.rs +++ b/compiler-core/src/dependency.rs @@ -1,20 +1,18 @@ -use std::{borrow::Borrow, cell::RefCell, collections::HashMap, error::Error as StdError, rc::Rc}; +use std::{cell::RefCell, cmp::Reverse, collections::HashMap, rc::Rc}; use crate::{Error, Result, manifest}; use ecow::EcoString; use hexpm::{ Dependency, Release, - version::{Range, ResolutionError, Version}, -}; -use pubgrub::{ - solver::{Dependencies, choose_package_with_fewest_versions}, - type_aliases::Map, + version::{Range, Version}, }; +use pubgrub::{Dependencies, Map}; +use thiserror::Error; pub type PackageVersions = HashMap; -type PubgrubRange = pubgrub::range::Range; +type PubgrubRange = pubgrub::Range; pub fn resolve_versions( package_fetcher: &impl PackageFetcher, @@ -28,8 +26,7 @@ where { tracing::info!("resolving_versions"); let root_version = Version::new(0, 0, 0); - let requirements = root_dependencies(dependencies, locked) - .map_err(|boxed_error| Error::dependency_resolution_failed(*boxed_error))?; + let requirements = root_dependencies(dependencies, locked)?; // Creating a map of all the required packages that have exact versions specified let exact_deps = &requirements @@ -50,7 +47,7 @@ where }], }; - let packages = pubgrub::solver::resolve( + let packages = pubgrub::resolve( &DependencyProvider::new(package_fetcher, provided_packages, root, locked, exact_deps), root_name.as_str().into(), root_version, @@ -136,7 +133,7 @@ fn parse_exact_version(ver: &str) -> Option { fn root_dependencies( base_requirements: Requirements, locked: &HashMap, -) -> Result, Box> +) -> Result, Error> where Requirements: Iterator, { @@ -150,7 +147,7 @@ where app: None, optional: false, repository: None, - requirement: Range::new(version.to_string()), + requirement: version.clone().into(), }, ) }) @@ -175,19 +172,14 @@ where // If the version was locked we verify that the requirement is // compatible with the locked version. Some(locked_version) => { - let compatible = range - .to_pubgrub() - .map_err(|e| { - Box::new(ResolutionError::Failure(format!( - "Failed to parse range {e}" - ))) - })? - .contains(locked_version); + let compatible = range.to_pubgrub().contains(locked_version); if !compatible { - return Err(Box::new(ResolutionError::Failure(format!( - "{name} is specified with the requirement `{range}`, \ + return Err(Error::IncompatibleLockedVersion { + error: format!( + "{name} is specified with the requirement `{range}`, \ but it is locked to {locked_version}, which is incompatible.", - )))); + ), + }); } } }; @@ -197,10 +189,29 @@ but it is locked to {locked_version}, which is incompatible.", } pub trait PackageFetcher { - fn get_dependencies(&self, package: &str) -> Result, Box>; + fn get_dependencies(&self, package: &str) -> Result, PackageFetchError>; } -struct DependencyProvider<'a, T: PackageFetcher> { +#[derive(Debug, Error)] +pub enum PackageFetchError { + #[error("{0}")] + ApiError(hexpm::ApiError), + #[error("{0}")] + FetchError(String), +} +impl From for PackageFetchError { + fn from(api_error: hexpm::ApiError) -> Self { + Self::ApiError(api_error) + } +} +impl PackageFetchError { + pub fn fetch_error(err: T) -> Self { + Self::FetchError(err.to_string()) + } +} + +#[derive(Debug)] +pub struct DependencyProvider<'a, T: PackageFetcher> { packages: RefCell>, remote: &'a T, locked: &'a HashMap, @@ -208,7 +219,7 @@ struct DependencyProvider<'a, T: PackageFetcher> { // We need this because by default pubgrub checks exact version by checking if a version is between the exact // and the version 1 bump ahead. That default breaks on prerelease builds since a bump includes the whole patch exact_only: &'a HashMap, - optional_dependencies: RefCell>>, + optional_dependencies: RefCell>>, } impl<'a, T> DependencyProvider<'a, T> @@ -244,7 +255,7 @@ where // `&self` with interop mutability. &self, name: &str, - ) -> Result<(), Box> { + ) -> Result<(), PackageFetchError> { let mut packages = self.packages.borrow_mut(); if packages.get(name).is_none() { let package = self.remote.get_dependencies(name)?; @@ -266,71 +277,43 @@ where } type PackageName = String; +pub type ResolutionError<'a, T> = pubgrub::PubGrubError>; -impl pubgrub::solver::DependencyProvider for DependencyProvider<'_, T> +impl pubgrub::DependencyProvider for DependencyProvider<'_, T> where T: PackageFetcher, { - fn choose_package_version, Ver: Borrow>( - &self, - potential_packages: impl Iterator, - ) -> Result<(Name, Option), Box> { - let potential_packages: Vec<_> = potential_packages - .map::>, _>(|pair| { - self.ensure_package_fetched(pair.0.borrow())?; - Ok(pair) - }) - .collect::>()?; - let list_available_versions = |name: &PackageName| { - let name = name.as_str(); - let exact_package = self.exact_only.get(name); - self.packages - .borrow() - .get(name) - .cloned() - .into_iter() - .flat_map(move |p| { - p.releases - .into_iter() - // if an exact version of a package is specified then we only want to allow that version as available - .filter(move |release| match exact_package { - Some(ver) => ver == &release.version, - _ => true, - }) - }) - .map(|p| p.version) - }; - Ok(choose_package_with_fewest_versions( - list_available_versions, - potential_packages.into_iter(), - )) - } - fn get_dependencies( &self, - name: &PackageName, - version: &Version, - ) -> Result, Box> { - self.ensure_package_fetched(name)?; + package: &Self::P, + version: &Self::V, + ) -> Result, Self::Err> { + self.ensure_package_fetched(package)?; let packages = self.packages.borrow(); let release = match packages - .get(name.as_str()) + .get(package.as_str()) .into_iter() .flat_map(|p| p.releases.iter()) .find(|r| &r.version == version) { Some(release) => release, - None => return Ok(Dependencies::Unknown), + None => { + return Ok(Dependencies::Unavailable(format!( + "{package}@{version} is not available" + ))); + } }; // Only use retired versions if they have been locked - if release.is_retired() && self.locked.get(name.as_str()) != Some(version) { - return Ok(Dependencies::Unknown); + if release.is_retired() && self.locked.get(package.as_str()) != Some(version) { + return Ok(Dependencies::Unavailable(format!( + "{package}@{version} is retired" + ))); } let mut deps: Map = Default::default(); for (name, d) in &release.requirements { - let mut range = d.requirement.to_pubgrub()?; + let mut range = d.requirement.to_pubgrub().clone(); let mut opt_deps = self.optional_dependencies.borrow_mut(); // if it's optional and it was not provided yet, store and skip if d.optional && !packages.contains_key(name.as_str()) { @@ -350,8 +333,67 @@ where let _ = deps.insert(name.clone(), range); } - Ok(Dependencies::Known(deps)) + Ok(Dependencies::Available(deps)) + } + + fn prioritize( + &self, + package: &Self::P, + range: &Self::VS, + _package_conflicts_counts: &pubgrub::PackageResolutionStatistics, + ) -> Self::Priority { + Reverse( + self.packages + .borrow() + .get(package.as_str()) + .cloned() + .into_iter() + .flat_map(|p| { + p.releases + .into_iter() + .filter(|r| range.contains(&r.version)) + }) + .count(), + ) + } + + fn choose_version( + &self, + package: &Self::P, + range: &Self::VS, + ) -> std::result::Result, Self::Err> { + self.ensure_package_fetched(package)?; + + let exact_package = self.exact_only.get(package); + let potential_versions = self + .packages + .borrow() + .get(package.as_str()) + .cloned() + .into_iter() + .flat_map(move |p| { + p.releases + .into_iter() + // if an exact version of a package is specified then we only want to allow that version as available + .filter_map(move |release| match exact_package { + Some(ver) => (ver == &release.version).then_some(release.version), + _ => Some(release.version), + }) + }) + .filter(|v| range.contains(v)); + match potential_versions.clone().filter(|v| !v.is_pre()).max() { + // Don't resolve to a pre-releaase package unless we *have* to + Some(v) => Ok(Some(v)), + None => Ok(potential_versions.max()), + } } + + type P = PackageName; + type V = Version; + type VS = PubgrubRange; + type Priority = Reverse; + type M = String; + type Err = PackageFetchError; } #[cfg(test)] @@ -368,11 +410,11 @@ mod tests { } impl PackageFetcher for Remote { - fn get_dependencies(&self, package: &str) -> Result, Box> { + fn get_dependencies(&self, package: &str) -> Result, PackageFetchError> { self.deps .get(package) .map(Rc::clone) - .ok_or(Box::new(hexpm::ApiError::NotFound)) + .ok_or(hexpm::ApiError::NotFound.into()) } } @@ -430,7 +472,7 @@ mod tests { app: None, optional: false, repository: None, - requirement: Range::new(">= 0.1.0".into()), + requirement: Range::new(">= 0.1.0".into()).unwrap(), }, )] .into(), @@ -446,7 +488,7 @@ mod tests { app: None, optional: false, repository: None, - requirement: Range::new(">= 0.1.0".into()), + requirement: Range::new(">= 0.1.0".into()).unwrap(), }, )] .into(), @@ -462,7 +504,7 @@ mod tests { app: None, optional: false, repository: None, - requirement: Range::new(">= 0.1.0".into()), + requirement: Range::new(">= 0.1.0".into()).unwrap(), }, )] .into(), @@ -478,7 +520,7 @@ mod tests { app: None, optional: false, repository: None, - requirement: Range::new(">= 0.1.0".into()), + requirement: Range::new(">= 0.1.0".into()).unwrap(), }, )] .into(), @@ -531,7 +573,7 @@ mod tests { app: None, optional: true, repository: None, - requirement: Range::new(">= 0.1.0 and < 0.3.0".into()), + requirement: Range::new(">= 0.1.0 and < 0.3.0".into()).unwrap(), }, )] .into(), @@ -604,7 +646,7 @@ mod tests { app: None, optional: true, repository: None, - requirement: Range::new(range.into()), + requirement: Range::new(range.into()).unwrap(), }, ) }) @@ -627,7 +669,7 @@ mod tests { &make_remote(), HashMap::new(), "app".into(), - vec![("gleam_stdlib".into(), Range::new("~> 0.1".into()))].into_iter(), + vec![("gleam_stdlib".into(), Range::new("~> 0.1".into()).unwrap())].into_iter(), &vec![locked_stdlib].into_iter().collect(), ) .unwrap(); @@ -658,7 +700,7 @@ mod tests { &make_remote(), HashMap::new(), "app".into(), - vec![("gleam_stdlib".into(), Range::new("~> 0.1".into()))].into_iter(), + vec![("gleam_stdlib".into(), Range::new("~> 0.1".into()).unwrap())].into_iter(), &vec![].into_iter().collect(), ) .unwrap(); @@ -676,7 +718,7 @@ mod tests { &make_remote(), HashMap::new(), "app".into(), - vec![("gleam_otp".into(), Range::new("~> 0.1".into()))].into_iter(), + vec![("gleam_otp".into(), Range::new("~> 0.1".into()).unwrap())].into_iter(), &vec![].into_iter().collect(), ) .unwrap(); @@ -697,7 +739,11 @@ mod tests { &make_remote(), HashMap::new(), "app".into(), - vec![("package_with_optional".into(), Range::new("~> 0.1".into()))].into_iter(), + vec![( + "package_with_optional".into(), + Range::new("~> 0.1".into()).unwrap(), + )] + .into_iter(), &vec![].into_iter().collect(), ) .unwrap(); @@ -719,8 +765,11 @@ mod tests { HashMap::new(), "app".into(), vec![ - ("package_with_optional".into(), Range::new("~> 0.1".into())), - ("gleam_stdlib".into(), Range::new("~> 0.1".into())), + ( + "package_with_optional".into(), + Range::new("~> 0.1".into()).unwrap(), + ), + ("gleam_stdlib".into(), Range::new("~> 0.1".into()).unwrap()), ] .into_iter(), &vec![].into_iter().collect(), @@ -747,8 +796,11 @@ mod tests { HashMap::new(), "app".into(), vec![ - ("package_with_optional".into(), Range::new("~> 0.1".into())), - ("gleam_stdlib".into(), Range::new("~> 0.3".into())), + ( + "package_with_optional".into(), + Range::new("~> 0.1".into()).unwrap(), + ), + ("gleam_stdlib".into(), Range::new("~> 0.3".into()).unwrap()), ] .into_iter(), &vec![].into_iter().collect(), @@ -763,8 +815,11 @@ mod tests { HashMap::new(), "app".into(), vec![ - ("package_with_optional".into(), Range::new("~> 0.1".into())), - ("gleam_otp".into(), Range::new("~> 0.1".into())), + ( + "package_with_optional".into(), + Range::new("~> 0.1".into()).unwrap(), + ), + ("gleam_otp".into(), Range::new("~> 0.1".into()).unwrap()), ] .into_iter(), &vec![].into_iter().collect(), @@ -794,7 +849,7 @@ mod tests { &make_remote(), HashMap::new(), "app".into(), - vec![("gleam_otp".into(), Range::new("~> 0.1.0".into()))].into_iter(), + vec![("gleam_otp".into(), Range::new("~> 0.1.0".into()).unwrap())].into_iter(), &vec![].into_iter().collect(), ) .unwrap(); @@ -815,7 +870,11 @@ mod tests { &make_remote(), HashMap::new(), "app".into(), - vec![("package_with_retired".into(), Range::new("> 0.0.0".into()))].into_iter(), + vec![( + "package_with_retired".into(), + Range::new("> 0.0.0".into()).unwrap(), + )] + .into_iter(), &vec![].into_iter().collect(), ) .unwrap(); @@ -837,7 +896,11 @@ mod tests { &make_remote(), HashMap::new(), "app".into(), - vec![("package_with_retired".into(), Range::new("> 0.0.0".into()))].into_iter(), + vec![( + "package_with_retired".into(), + Range::new("> 0.0.0".into()).unwrap(), + )] + .into_iter(), &vec![("package_with_retired".into(), Version::new(0, 2, 0))] .into_iter() .collect(), @@ -861,7 +924,11 @@ mod tests { &make_remote(), HashMap::new(), "app".into(), - vec![("gleam_otp".into(), Range::new("~> 0.3.0-rc1".into()))].into_iter(), + vec![( + "gleam_otp".into(), + Range::new("~> 0.3.0-rc1".into()).unwrap(), + )] + .into_iter(), &vec![].into_iter().collect(), ) .unwrap(); @@ -882,7 +949,7 @@ mod tests { &make_remote(), HashMap::new(), "app".into(), - vec![("gleam_otp".into(), Range::new("0.3.0-rc1".into()))].into_iter(), + vec![("gleam_otp".into(), Range::new("0.3.0-rc1".into()).unwrap())].into_iter(), &vec![].into_iter().collect(), ) .unwrap(); @@ -903,7 +970,7 @@ mod tests { &make_remote(), HashMap::new(), "app".into(), - vec![("unknown".into(), Range::new("~> 0.1".into()))].into_iter(), + vec![("unknown".into(), Range::new("~> 0.1".into()).unwrap())].into_iter(), &vec![].into_iter().collect(), ) .unwrap_err(); @@ -915,7 +982,7 @@ mod tests { &make_remote(), HashMap::new(), "app".into(), - vec![("gleam_stdlib".into(), Range::new("~> 99.0".into()))].into_iter(), + vec![("gleam_stdlib".into(), Range::new("~> 99.0".into()).unwrap())].into_iter(), &vec![].into_iter().collect(), ) .unwrap_err(); @@ -927,7 +994,11 @@ mod tests { &make_remote(), HashMap::new(), "app".into(), - vec![("gleam_stdlib".into(), Range::new("~> 0.1.0".into()))].into_iter(), + vec![( + "gleam_stdlib".into(), + Range::new("~> 0.1.0".into()).unwrap(), + )] + .into_iter(), &vec![("gleam_stdlib".into(), Version::new(0, 2, 0))] .into_iter() .collect(), @@ -935,9 +1006,9 @@ mod tests { .unwrap_err(); match err { - Error::DependencyResolutionFailed(msg) => assert_eq!( - msg, - "An unrecoverable error happened while solving dependencies: gleam_stdlib is specified with the requirement `~> 0.1.0`, but it is locked to 0.2.0, which is incompatible." + Error::IncompatibleLockedVersion { error } => assert_eq!( + error, + "gleam_stdlib is specified with the requirement `~> 0.1.0`, but it is locked to 0.2.0, which is incompatible." ), _ => panic!("wrong error: {err}"), } @@ -949,7 +1020,7 @@ mod tests { &make_remote(), HashMap::new(), "app".into(), - vec![("gleam_stdlib".into(), Range::new("0.1.0".into()))].into_iter(), + vec![("gleam_stdlib".into(), Range::new("0.1.0".into()).unwrap())].into_iter(), &vec![].into_iter().collect(), ) .unwrap(); @@ -986,19 +1057,19 @@ mod tests { ( EcoString::from("package_depends_on_indirect_pkg"), requirement::Requirement::Hex { - version: Range::new("> 0.1.0 and <= 1.0.0".into()), + version: Range::new("> 0.1.0 and <= 1.0.0".into()).unwrap(), }, ), ( EcoString::from("direct_pkg_with_major_version"), requirement::Requirement::Hex { - version: Range::new("> 0.1.0 and <= 2.0.0".into()), + version: Range::new("> 0.1.0 and <= 2.0.0".into()).unwrap(), }, ), ( EcoString::from("depends_on_old_version_of_direct_pkg"), requirement::Requirement::Hex { - version: Range::new("> 0.1.0 and <= 1.0.0".into()), + version: Range::new("> 0.1.0 and <= 1.0.0".into()).unwrap(), }, ), ] diff --git a/compiler-core/src/error.rs b/compiler-core/src/error.rs index f5942d83b6d..fd997a28573 100644 --- a/compiler-core/src/error.rs +++ b/compiler-core/src/error.rs @@ -1,5 +1,6 @@ #![allow(clippy::unwrap_used, clippy::expect_used)] use crate::build::{Origin, Outcome, Runtime, Target}; +use crate::dependency::{PackageFetcher, ResolutionError}; use crate::diagnostic::{Diagnostic, ExtraLabel, Label, Location}; use crate::strings::{to_snake_case, to_upper_camel_case}; use crate::type_::collapse_links; @@ -12,11 +13,9 @@ use crate::type_::{FieldAccessUsage, error::PatternMatchKind}; use crate::{ast::BinOp, parse::error::ParseErrorType, type_::Type}; use crate::{bit_array, diagnostic::Level, javascript, type_::UnifyErrorSituation}; use ecow::EcoString; -use hexpm::version::ResolutionError; use itertools::Itertools; -use pubgrub::package::Package; -use pubgrub::report::DerivationTree; -use pubgrub::version::Version; +use pubgrub::Package; +use pubgrub::{DerivationTree, VersionSet}; use std::borrow::Cow; use std::collections::HashSet; use std::fmt::{Debug, Display}; @@ -191,6 +190,9 @@ pub enum Error { #[error("{input} is not a valid version. {error}")] InvalidVersionFormat { input: String, error: String }, + #[error("incompatible locked version. {error}")] + IncompatibleLockedVersion { error: String }, + #[error("project root already exists")] ProjectRootAlreadyExist { path: String }, @@ -431,23 +433,28 @@ impl Error { Self::TarFinish(error.to_string()) } - pub fn dependency_resolution_failed(error: ResolutionError) -> Error { - fn collect_conflicting_packages<'dt, P: Package, V: Version>( - derivation_tree: &'dt DerivationTree, + pub fn dependency_resolution_failed(error: ResolutionError<'_, T>) -> Error { + fn collect_conflicting_packages< + 'dt, + P: Package, + VS: VersionSet, + M: Clone + Display + Debug + Eq, + >( + derivation_tree: &'dt DerivationTree, conflicting_packages: &mut HashSet<&'dt P>, ) { match derivation_tree { DerivationTree::External(external) => match external { - pubgrub::report::External::NotRoot(package, _) => { + pubgrub::External::NotRoot(package, _) => { let _ = conflicting_packages.insert(package); } - pubgrub::report::External::NoVersions(package, _) => { + pubgrub::External::NoVersions(package, _) => { let _ = conflicting_packages.insert(package); } - pubgrub::report::External::UnavailableDependencies(package, _) => { + pubgrub::External::Custom(package, _, _) => { let _ = conflicting_packages.insert(package); } - pubgrub::report::External::FromDependencyOf(package, _, dep_package, _) => { + pubgrub::External::FromDependencyOf(package, _, dep_package, _) => { let _ = conflicting_packages.insert(package); let _ = conflicting_packages.insert(dep_package); } @@ -488,27 +495,13 @@ The conflicting packages are: "An error occurred while trying to retrieve dependencies of {package}@{version}: {source}", ), - ResolutionError::DependencyOnTheEmptySet { - package, - version, - dependent, - } => format!("{package}@{version} has an impossible dependency on {dependent}",), - - ResolutionError::SelfDependency { package, version } => { - format!("{package}@{version} somehow depends on itself.") - } - - ResolutionError::ErrorChoosingPackageVersion(err) => { - format!("Unable to determine package versions: {err}") + ResolutionError::ErrorChoosingVersion { package, source } => { + format!("An error occured while chosing the version of {package}: {source}",) } ResolutionError::ErrorInShouldCancel(err) => { format!("Dependency resolution was cancelled. {err}") } - - ResolutionError::Failure(err) => { - format!("An unrecoverable error happened while solving dependencies: {err}") - } }) } @@ -3965,6 +3958,22 @@ The error from the parser was: }] } + Error::IncompatibleLockedVersion { error } => { + let text = format!( + "There is an incompatiblity between a version specified in +manifest.toml and a version range specified in gleam.toml: + + {error}" + ); + vec![Diagnostic { + title: "Incompatible locked version".into(), + text, + hint: None, + location: None, + level: Level::Error, + }] + } + Error::DependencyCanonicalizationFailed(package) => { let text = format!("Local package `{package}` has no canonical path"); diff --git a/compiler-core/src/javascript/tests/bit_arrays.rs b/compiler-core/src/javascript/tests/bit_arrays.rs index 71cf823b292..d73b3874a74 100644 --- a/compiler-core/src/javascript/tests/bit_arrays.rs +++ b/compiler-core/src/javascript/tests/bit_arrays.rs @@ -1,5 +1,5 @@ use hexpm::version::Version; -use pubgrub::range::Range; +use pubgrub::Range; use crate::{ assert_js, assert_js_no_warnings_with_gleam_version, assert_js_warnings_with_gleam_version, diff --git a/compiler-core/src/language_server/tests.rs b/compiler-core/src/language_server/tests.rs index 2b485895b69..c3246091b0c 100644 --- a/compiler-core/src/language_server/tests.rs +++ b/compiler-core/src/language_server/tests.rs @@ -311,7 +311,7 @@ fn add_package_from_manifest( package.name.clone(), match package.source { ManifestPackageSource::Hex { .. } => Requirement::Hex { - version: Range::new("1.0.0".into()), + version: Range::new("1.0.0".into()).unwrap(), }, ManifestPackageSource::Local { ref path } => Requirement::Path { path: path.into() }, ManifestPackageSource::Git { @@ -336,7 +336,7 @@ fn add_dev_package_from_manifest( package.name.clone(), match package.source { ManifestPackageSource::Hex { .. } => Requirement::Hex { - version: Range::new("1.0.0".into()), + version: Range::new("1.0.0".into()).unwrap(), }, ManifestPackageSource::Local { ref path } => Requirement::Path { path: path.into() }, ManifestPackageSource::Git { diff --git a/compiler-core/src/manifest.rs b/compiler-core/src/manifest.rs index fd68b652f39..d9c275374c8 100644 --- a/compiler-core/src/manifest.rs +++ b/compiler-core/src/manifest.rs @@ -232,8 +232,8 @@ mod tests { fn manifest_toml_format() { let manifest = Manifest { requirements: [ - ("zzz".into(), Requirement::hex("> 0.0.0")), - ("aaa".into(), Requirement::hex("> 0.0.0")), + ("zzz".into(), Requirement::hex("> 0.0.0").unwrap()), + ("aaa".into(), Requirement::hex("> 0.0.0").unwrap()), ( "awsome_local2".into(), Requirement::git("https://github.com/gleam-lang/gleam.git", "bd9fe02f"), @@ -242,8 +242,8 @@ mod tests { "awsome_local1".into(), Requirement::path("../path/to/package"), ), - ("gleam_stdlib".into(), Requirement::hex("~> 0.17")), - ("gleeunit".into(), Requirement::hex("~> 0.1")), + ("gleam_stdlib".into(), Requirement::hex("~> 0.17").unwrap()), + ("gleeunit".into(), Requirement::hex("~> 0.1").unwrap()), ] .into(), packages: vec![ @@ -342,8 +342,8 @@ zzz = { version = "> 0.0.0" } fn manifest_toml_format_with_unc() { let manifest = Manifest { requirements: [ - ("zzz".into(), Requirement::hex("> 0.0.0")), - ("aaa".into(), Requirement::hex("> 0.0.0")), + ("zzz".into(), Requirement::hex("> 0.0.0").unwrap()), + ("aaa".into(), Requirement::hex("> 0.0.0").unwrap()), ( "awsome_local2".into(), Requirement::git("https://github.com/gleam-lang/gleam.git", "main"), @@ -352,8 +352,8 @@ zzz = { version = "> 0.0.0" } "awsome_local1".into(), Requirement::path("../path/to/package"), ), - ("gleam_stdlib".into(), Requirement::hex("~> 0.17")), - ("gleeunit".into(), Requirement::hex("~> 0.1")), + ("gleam_stdlib".into(), Requirement::hex("~> 0.17").unwrap()), + ("gleeunit".into(), Requirement::hex("~> 0.1").unwrap()), ] .into(), packages: vec![ diff --git a/compiler-core/src/requirement.rs b/compiler-core/src/requirement.rs index 0ee6cc3d3cd..036ae08b682 100644 --- a/compiler-core/src/requirement.rs +++ b/compiler-core/src/requirement.rs @@ -1,6 +1,7 @@ use std::fmt; use std::str::FromStr; +use crate::Error; use crate::error::Result; use crate::io::make_relative; use camino::{Utf8Path, Utf8PathBuf}; @@ -29,10 +30,13 @@ pub enum Requirement { } impl Requirement { - pub fn hex(range: &str) -> Requirement { - Requirement::Hex { - version: Range::new(range.to_string()), - } + pub fn hex(range: &str) -> Result { + Ok(Requirement::Hex { + version: Range::new(range.to_string()).map_err(|e| Error::InvalidVersionFormat { + input: range.to_string(), + error: e.to_string(), + })?, + }) } pub fn path(path: &str) -> Requirement { @@ -91,17 +95,17 @@ where D: Deserializer<'de>, { let version = String::deserialize(deserializer)?; - Ok(Range::new(version)) + Range::new(version).map_err(de::Error::custom) } #[derive(Debug, Copy, Clone)] pub struct Void; impl FromStr for Requirement { - type Err = Void; + type Err = Error; fn from_str(s: &str) -> Result { - Ok(Requirement::hex(s)) + Requirement::hex(s) } } @@ -153,8 +157,8 @@ mod tests { github = { git = "https://github.com/gleam-lang/otp.git", ref = "4d34935" } "#; let deps: HashMap = toml::from_str(toml).unwrap(); - assert_eq!(deps["short"], Requirement::hex("~> 0.5")); - assert_eq!(deps["hex"], Requirement::hex("~> 1.0.0")); + assert_eq!(deps["short"], Requirement::hex("~> 0.5").unwrap()); + assert_eq!(deps["hex"], Requirement::hex("~> 1.0.0").unwrap()); assert_eq!(deps["local"], Requirement::path("/path/to/package")); assert_eq!( deps["github"], diff --git a/compiler-core/src/type_/environment.rs b/compiler-core/src/type_/environment.rs index 03fd664fd52..9aab3eef2c9 100644 --- a/compiler-core/src/type_/environment.rs +++ b/compiler-core/src/type_/environment.rs @@ -1,4 +1,4 @@ -use pubgrub::range::Range; +use pubgrub::Range; use crate::{ analyse::TargetSupport, diff --git a/compiler-core/src/type_/expression.rs b/compiler-core/src/type_/expression.rs index e1116ffbeef..146e6d2ba27 100644 --- a/compiler-core/src/type_/expression.rs +++ b/compiler-core/src/type_/expression.rs @@ -18,7 +18,7 @@ use crate::{ parse::PatternPosition, reference::ReferenceKind, }; -use hexpm::version::Version; +use hexpm::version::{LowestVersion, Version}; use im::hashmap; use itertools::Itertools; use num_bigint::BigInt; diff --git a/compiler-core/src/type_/pattern.rs b/compiler-core/src/type_/pattern.rs index eaeb2683086..dd11aba74f9 100644 --- a/compiler-core/src/type_/pattern.rs +++ b/compiler-core/src/type_/pattern.rs @@ -1,4 +1,4 @@ -use hexpm::version::Version; +use hexpm::version::{LowestVersion, Version}; use im::hashmap; use itertools::Itertools; use num_bigint::BigInt; diff --git a/compiler-core/src/type_/tests.rs b/compiler-core/src/type_/tests.rs index 44c5460125d..8fc33c1d013 100644 --- a/compiler-core/src/type_/tests.rs +++ b/compiler-core/src/type_/tests.rs @@ -11,7 +11,7 @@ use crate::{ }; use ecow::EcoString; use itertools::Itertools; -use pubgrub::range::Range; +use pubgrub::Range; use std::rc::Rc; use vec1::Vec1;