mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-17 22:07:47 +00:00
uv-resolver: error during installation for conflicting extras
This collects ALL activated extras while traversing the lock file to produce a `Resolution` for installation. If any two extras are activated that are conflicting, then an error is produced. We add a couple of tests to demonstrate the behavior. One case is desirable (where we conditionally depend on `package[extra]`) and the other case is undesirable (where we create an uninstallable lock file). Fixes #9942, Fixes #10590
This commit is contained in:
parent
1676e63603
commit
a8d23da59c
3 changed files with 256 additions and 0 deletions
|
@ -1,9 +1,11 @@
|
|||
use std::borrow::Cow;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::VecDeque;
|
||||
use std::path::Path;
|
||||
|
||||
use either::Either;
|
||||
use itertools::Itertools;
|
||||
use petgraph::Graph;
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
||||
|
||||
|
@ -265,6 +267,8 @@ pub trait Installable<'lock> {
|
|||
}
|
||||
}
|
||||
|
||||
let mut all_activated_extras: BTreeSet<(&PackageName, &ExtraName)> =
|
||||
activated_extras.iter().copied().collect();
|
||||
while let Some((package, extra)) = queue.pop_front() {
|
||||
let deps = if let Some(extra) = extra {
|
||||
Either::Left(
|
||||
|
@ -283,6 +287,7 @@ pub trait Installable<'lock> {
|
|||
let mut extended = activated_extras.to_vec();
|
||||
for extra in &dep.extra {
|
||||
extended.push((&dep.package_id.name, extra));
|
||||
all_activated_extras.insert((&dep.package_id.name, extra));
|
||||
}
|
||||
activated_extras = Cow::Owned(extended);
|
||||
}
|
||||
|
@ -338,6 +343,32 @@ pub trait Installable<'lock> {
|
|||
}
|
||||
}
|
||||
|
||||
// At time of writing, it's somewhat expected that the set of
|
||||
// conflicting extras is pretty small. With that said, the
|
||||
// time complexity of the following routine is pretty gross.
|
||||
// Namely, `set.contains` is linear in the size of the set,
|
||||
// iteration over all conflicts is also obviously linear in
|
||||
// the number of conflicting sets and then for each of those,
|
||||
// we visit every possible pair of activated extra from above,
|
||||
// which is quadratic in the total number of extras enabled. I
|
||||
// believe the simplest improvement here, if it's necessary, is
|
||||
// to adjust the `Conflicts` internals to own these sorts of
|
||||
// checks. ---AG
|
||||
for set in self.lock().conflicts().iter() {
|
||||
for ((pkg1, extra1), (pkg2, extra2)) in all_activated_extras.iter().tuple_combinations()
|
||||
{
|
||||
if set.contains(pkg1, *extra1) && set.contains(pkg2, *extra2) {
|
||||
return Err(LockErrorKind::ConflictingExtra {
|
||||
package1: (*pkg1).clone(),
|
||||
extra1: (*extra1).clone(),
|
||||
package2: (*pkg2).clone(),
|
||||
extra2: (*extra2).clone(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Resolution::new(petgraph))
|
||||
}
|
||||
|
||||
|
|
|
@ -4897,6 +4897,16 @@ enum LockErrorKind {
|
|||
#[source]
|
||||
err: uv_distribution::Error,
|
||||
},
|
||||
#[error(
|
||||
"Found conflicting extras `{package1}[{extra1}]` \
|
||||
and `{package2}[{extra2}]` enabled simultaneously"
|
||||
)]
|
||||
ConflictingExtra {
|
||||
package1: PackageName,
|
||||
extra1: ExtraName,
|
||||
package2: PackageName,
|
||||
extra2: ExtraName,
|
||||
},
|
||||
}
|
||||
|
||||
/// An error that occurs when a source string could not be parsed.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue