Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

suggest unlocking locked pkgs that cause dep resolution failures #3970

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
62 changes: 58 additions & 4 deletions compiler-cli/src/dependencies.rs
Original file line number Diff line number Diff line change
@@ -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<Telem: Telemetry>(
runtime: tokio::runtime::Handle,
mode: Mode,
@@ -691,18 +708,55 @@ fn resolve_versions<Telem: Telemetry>(
}

// Convert provided packages into hex packages for pub-grub resolve
let provided_hex_packages = provided_packages
let provided_hex_packages: HashMap<EcoString, hexpm::Package> = 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<String, Version> = 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(
83 changes: 75 additions & 8 deletions compiler-core/src/dependency.rs
Original file line number Diff line number Diff line change
@@ -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<String, Dependency> = 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<EcoString, hexpm::Package> = 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}"),
}
}

122 changes: 99 additions & 23 deletions compiler-core/src/error.rs
Original file line number Diff line number Diff line change
@@ -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<EcoString>,
},

#[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<EcoString, hexpm::version::Version>,
) -> Error {
fn collect_conflicting_packages<'dt, P: Package, V: Version>(
derivation_tree: &'dt DerivationTree<P, V>,
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<EcoString> = conflicting_packages
.iter()
.map(|pkg| (*pkg).to_string().into())
.collect();

{}
",
conflicting_packages.into_iter().map(|s| format!("- {s}")).join("\n"))
}
let locked_conflicts: Vec<EcoString> = 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") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this logic is correct. That error message is created when a locked version falls outside the requested version constraint, but that's not the situation in which we want to offer to unlock packages. We want to unlock if there's a conflict and any of the packages in the conflict are locked to specific versions.

I don't think such logic should be in the error module as it is only concerned with the definition, construction, and displaying of errors. It doesn't know anything about the wider context of the program or why errors would be emitted.

// 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<E>(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,