diff --git a/compiler-cli/src/dependencies.rs b/compiler-cli/src/dependencies.rs index 5c0b00c7932..5371a32569d 100644 --- a/compiler-cli/src/dependencies.rs +++ b/compiler-cli/src/dependencies.rs @@ -649,6 +649,23 @@ impl PartialEq for ProvidedPackageSource { } } +// Estimates whether the CLI is ran in a CI environment for use in silencing +// certain CLI dialogues. +fn is_ci_env() -> bool { + let ci_vars = [ + "CI", + "TRAVIS", + "CIRCLECI", + "GITHUB_ACTIONS", + "GITLAB_CI", + "JENKINS_URL", + "TF_BUILD", + "BITBUCKET_COMMIT", + ]; + + ci_vars.iter().any(|var| std::env::var_os(*var).is_some()) +} + fn resolve_versions( runtime: tokio::runtime::Handle, mode: Mode, @@ -691,18 +708,55 @@ fn resolve_versions( } // Convert provided packages into hex packages for pub-grub resolve - let provided_hex_packages = provided_packages + let provided_hex_packages: HashMap = provided_packages .iter() .map(|(name, package)| (name.clone(), package.to_hex_package(name))) .collect(); - let resolved = dependency::resolve_versions( + let root_requirements_clone = root_requirements.clone(); + let resolved: HashMap = match dependency::resolve_versions( PackageFetcher::boxed(runtime.clone()), - provided_hex_packages, + provided_hex_packages.clone(), config.name.clone(), root_requirements.into_iter(), &locked, - )?; + ) { + Ok(it) => it, + Err( + ref e @ Error::DependencyResolutionFailedWithLocked { + error: _, + ref locked_conflicts, + }, + ) => { + if !is_ci_env() { + return Err(e.clone()); + } + + let should_try_unlock = cli::confirm( + "\nSome of these dependencies are locked to specific versions. It may +be possible to find a solution if they are unlocked, would you like +to unlock and try again?", + )?; + + if should_try_unlock { + // unlock pkgs + unlock_packages(&mut locked, locked_conflicts, manifest)?; + + // try again + dependency::resolve_versions( + PackageFetcher::boxed(runtime.clone()), + provided_hex_packages, + config.name.clone(), + root_requirements_clone.into_iter(), + &locked, + )? + } else { + return Err(e.clone()); + } + } + + Err(err) => return Err(err), + }; // Convert the hex packages and local packages into manifest packages let manifest_packages = runtime.block_on(future::try_join_all( diff --git a/compiler-core/src/dependency.rs b/compiler-core/src/dependency.rs index e1e709d7f81..ac9af7119b4 100644 --- a/compiler-core/src/dependency.rs +++ b/compiler-core/src/dependency.rs @@ -28,8 +28,9 @@ where { tracing::info!("resolving_versions"); let root_version = Version::new(0, 0, 0); - let requirements = - root_dependencies(dependencies, locked).map_err(Error::dependency_resolution_failed)?; + + let requirements = root_dependencies(dependencies, locked) + .map_err(|err| Error::dependency_resolution_failed(err, locked))?; // Creating a map of all the required packages that have exact versions specified let exact_deps = &requirements @@ -55,7 +56,7 @@ where root_name.as_str().into(), root_version, ) - .map_err(Error::dependency_resolution_failed)? + .map_err(|err| Error::dependency_resolution_failed(err, locked))? .into_iter() .filter(|(name, _)| name.as_str() != root_name.as_str()) .collect(); @@ -129,6 +130,8 @@ where .map_err(|e| ResolutionError::Failure(format!("Failed to parse range {e}")))? .contains(locked_version); if !compatible { + // see [`crate::error::dependency_resolution_failed`] when + // changing this error's text fmt return Err(ResolutionError::Failure(format!( "{name} is specified with the requirement `{range}`, \ but it is locked to {locked_version}, which is incompatible.", @@ -789,11 +792,75 @@ 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." - ), - _ => panic!("wrong error: {err}"), + Error::DependencyResolutionFailedWithLocked { + error, + locked_conflicts: _, + } => { + assert_eq!( + error, + format!("Unable to find compatible versions due to package versions locked by manifest.toml.\n\ + Consider unlocking the responsible locked package(s) :\n- gleam_stdlib"), + ); + } + _ => panic!("wrong error: {err}"), + } + } + + // These are errors where a locked package version is incompatible with a new package added via gleam add or via a manual gleam.toml update and gleam deps download AND the locked package is not constrained in manifest.toml. + #[test] + fn resolution_locked_version_doesnt_satisfy_requirements_indirect() { + // we're creating a dependency logging v1.4.0 that requires gleam_stdlib v0.40.0 + let mut requirements: HashMap = HashMap::new(); + let _ = requirements.insert( + "gleam_stdlib".to_string(), + Dependency { + requirement: Range::new("~> 0.40.0".to_string()), + optional: false, + app: None, + repository: None, + }, + ); + let mut provided_packages: HashMap = HashMap::new(); + let _ = provided_packages.insert( + "logging".into(), + hexpm::Package { + name: "logging".to_string(), + repository: "test".to_string(), + releases: vec![Release { + version: Version::new(1, 4, 0), + requirements: requirements, + retirement_status: None, + outer_checksum: vec![0], + meta: (), + }], + }, + ); + + // now try and resolve versions with gleam_stdlib v0.20.0 in lock. + let err = resolve_versions( + make_remote(), + provided_packages, + "app".into(), + vec![("logging".into(), Range::new(">= 1.3.0 and < 2.0.0".into()))].into_iter(), + &vec![("gleam_stdlib".into(), Version::new(0, 20, 0))] + .into_iter() + .collect(), + ) + .unwrap_err(); + + // expect failure + match err { + Error::DependencyResolutionFailedWithLocked { + error, + locked_conflicts: _, + } => { + assert_eq!( + error, + format!("Unable to find compatible versions due to package versions locked by manifest.toml.\n\ + Consider unlocking the responsible locked package(s) :\n- gleam_stdlib"), + ); + } + _ => panic!("wrong error: {err}"), } } diff --git a/compiler-core/src/error.rs b/compiler-core/src/error.rs index 4ebc1f71d35..bda9c5a35a4 100644 --- a/compiler-core/src/error.rs +++ b/compiler-core/src/error.rs @@ -17,7 +17,7 @@ use pubgrub::package::Package; use pubgrub::report::DerivationTree; use pubgrub::version::Version; use std::borrow::Cow; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::env; use std::fmt::{Debug, Display}; use std::io::Write; @@ -238,6 +238,13 @@ file_names.iter().map(|x| x.as_str()).join(", "))] #[error("Dependency tree resolution failed: {0}")] DependencyResolutionFailed(String), + #[error("Dependency tree resolution failed due to locked packages: {error}")] + DependencyResolutionFailedWithLocked { + error: String, + // a vec of the names of locked dependencies responsible for the failure + locked_conflicts: Vec, + }, + #[error("The package {0} is listed in dependencies and dev-dependencies")] DuplicateDependency(EcoString), @@ -378,7 +385,10 @@ impl Error { Self::TarFinish(error.to_string()) } - pub fn dependency_resolution_failed(error: ResolutionError) -> Error { + pub fn dependency_resolution_failed( + error: ResolutionError, + locked: &HashMap, + ) -> Error { fn collect_conflicting_packages<'dt, P: Package, V: Version>( derivation_tree: &'dt DerivationTree, conflicting_packages: &mut HashSet<&'dt P>, @@ -406,54 +416,98 @@ impl Error { } } - Self::DependencyResolutionFailed(match error { + match error { ResolutionError::NoSolution(mut derivation_tree) => { derivation_tree.collapse_no_versions(); let mut conflicting_packages = HashSet::new(); collect_conflicting_packages(&derivation_tree, &mut conflicting_packages); - wrap_format!("Unable to find compatible versions for \ -the version constraints in your gleam.toml. \ -The conflicting packages are: + let conflict_names: Vec = conflicting_packages + .iter() + .map(|pkg| (*pkg).to_string().into()) + .collect(); -{} -", - conflicting_packages.into_iter().map(|s| format!("- {s}")).join("\n")) - } + let locked_conflicts: Vec = conflict_names + .iter() + .filter(|name| locked.contains_key(*name)) + .cloned() + .collect(); + + if !locked_conflicts.is_empty() { + Error::DependencyResolutionFailedWithLocked { + error: format!( + "Unable to find compatible versions due to package versions locked by manifest.toml.\n\ + Consider unlocking the responsible locked package(s) :\n{}", + locked_conflicts.iter().map(|s| format!("- {s}")).join("\n") + ), + locked_conflicts, + } + } else { + Error::DependencyResolutionFailed( + format!( + "Unable to find compatible versions for the version constraints in your gleam.toml.\n\ + The conflicting packages are:\n{}", + conflicting_packages.into_iter().map(|s| format!("- {s}")).join("\n") + ) + ) + } + } // end [`ResolutionError::NoSolution`] arm ResolutionError::ErrorRetrievingDependencies { package, version, source, - } => format!( + } => { + let msg = format!( "An error occurred while trying to retrieve dependencies of {package}@{version}: {source}", - ), + ); + Error::DependencyResolutionFailed(msg) + } ResolutionError::DependencyOnTheEmptySet { package, version, dependent, - } => format!( - "{package}@{version} has an impossible dependency on {dependent}", - ), + } => { + let msg = + format!("{package}@{version} has an impossible dependency on {dependent}",); + + Error::DependencyResolutionFailed(msg) + } ResolutionError::SelfDependency { package, version } => { - format!("{package}@{version} somehow depends on itself.") + let msg = format!("{package}@{version} somehow depends on itself."); + Error::DependencyResolutionFailed(msg) } ResolutionError::ErrorChoosingPackageVersion(err) => { - format!("Unable to determine package versions: {err}") + let msg = format!("Unable to determine package versions: {err}"); + Error::DependencyResolutionFailed(msg) } ResolutionError::ErrorInShouldCancel(err) => { - format!("Dependency resolution was cancelled. {err}") + let msg = format!("Dependency resolution was cancelled. {err}"); + Error::DependencyResolutionFailed(msg) } - ResolutionError::Failure(err) => format!( - "An unrecoverable error happened while solving dependencies: {err}" - ), - }) + ResolutionError::Failure(err) => { + let default_msg = format!("Dependency resolution was cancelled. {err}"); + if err.contains(", but it is locked to") { + // first word is package name + match err.split_whitespace().next() { + Some(pkg) => Error::DependencyResolutionFailedWithLocked { + error: format!("Unable to find compatible versions due to package versions locked by manifest.toml.\n\ + Consider unlocking the responsible locked package(s) :\n- {}", pkg), + locked_conflicts: vec![pkg.into()], + }, + None => Error::DependencyResolutionFailed("no pkg".to_string()), + } + } else { + Error::DependencyResolutionFailed(default_msg) + } + } + } } pub fn expand_tar(error: E) -> Error @@ -3570,7 +3624,29 @@ The error from the version resolver library was: }] } - Error::GitDependencyUnsupported => vec![Diagnostic { + // locked_conflicts ignored as the version resolver lib builds the message + // enumerating them + Error::DependencyResolutionFailedWithLocked{error, locked_conflicts: _} => { + let text = format!( +"An error occurred while determining what dependency packages and +versions should be downloaded. +The error from the version resolver library was: + + {} + + ", + wrap(error) + ); + vec![Diagnostic { + title: "Dependency resolution with a locked package".into(), + text, + hint: Some("Try removing locked version(s) in your manifest.toml and re-run the command.".into()), + location: None, + level: Level::Error, + }] + }, + + Error::GitDependencyUnsupported => vec![Diagnostic { title: "Git dependencies are not currently supported".into(), text: "Please remove all git dependencies from the gleam.toml file".into(), hint: None,