uv_resolver/lock/
installable.rs

1use std::collections::BTreeSet;
2use std::collections::VecDeque;
3use std::collections::hash_map::Entry;
4use std::path::Path;
5use std::sync::Arc;
6
7use either::Either;
8use itertools::Itertools;
9use petgraph::Graph;
10use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
11
12use uv_configuration::ExtrasSpecificationWithDefaults;
13use uv_configuration::{BuildOptions, DependencyGroupsWithDefaults, InstallOptions};
14use uv_distribution_types::{Edge, Node, Resolution, ResolvedDist};
15use uv_normalize::{ExtraName, GroupName, PackageName};
16use uv_platform_tags::Tags;
17use uv_pypi_types::ResolverMarkerEnvironment;
18
19use crate::lock::{LockErrorKind, Package, TagPolicy};
20use crate::{Lock, LockError};
21
22pub trait Installable<'lock> {
23    /// Return the root install path.
24    fn install_path(&self) -> &'lock Path;
25
26    /// Return the [`Lock`] to install.
27    fn lock(&self) -> &'lock Lock;
28
29    /// Return the [`PackageName`] of the root packages in the target.
30    fn roots(&self) -> impl Iterator<Item = &PackageName>;
31
32    /// Return the [`PackageName`] of the target, if available.
33    fn project_name(&self) -> Option<&PackageName>;
34
35    /// Convert the [`Lock`] to a [`Resolution`] using the given marker environment, tags, and root.
36    fn to_resolution(
37        &self,
38        marker_env: &ResolverMarkerEnvironment,
39        tags: &Tags,
40        extras: &ExtrasSpecificationWithDefaults,
41        groups: &DependencyGroupsWithDefaults,
42        build_options: &BuildOptions,
43        install_options: &InstallOptions,
44    ) -> Result<Resolution, LockError> {
45        let size_guess = self.lock().packages.len();
46        let mut petgraph = Graph::with_capacity(size_guess, size_guess);
47        let mut inverse = FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher);
48
49        let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new();
50        let mut seen = FxHashSet::default();
51        let mut activated_projects: Vec<&PackageName> = vec![];
52        let mut activated_extras: Vec<(&PackageName, &ExtraName)> = vec![];
53        let mut activated_groups: Vec<(&PackageName, &GroupName)> = vec![];
54
55        let root = petgraph.add_node(Node::Root);
56
57        // Determine the set of activated extras and groups, from the root.
58        //
59        // TODO(charlie): This isn't quite right. Below, when we add the dependency groups to the
60        // graph, we rely on the activated extras and dependency groups, to evaluate the conflict
61        // marker. But at that point, we don't know the full set of activated extras; this is only
62        // computed below. We somehow need to add the dependency groups _after_ we've computed all
63        // enabled extras, but the groups themselves could depend on the set of enabled extras.
64        if !self.lock().conflicts().is_empty() {
65            for root_name in self.roots() {
66                let dist = self
67                    .lock()
68                    .find_by_name(root_name)
69                    .map_err(|_| LockErrorKind::MultipleRootPackages {
70                        name: root_name.clone(),
71                    })?
72                    .ok_or_else(|| LockErrorKind::MissingRootPackage {
73                        name: root_name.clone(),
74                    })?;
75
76                // Track the activated extras.
77                if groups.prod() {
78                    activated_projects.push(&dist.id.name);
79                    for extra in extras.extra_names(dist.optional_dependencies.keys()) {
80                        activated_extras.push((&dist.id.name, extra));
81                    }
82                }
83
84                // Track the activated groups.
85                for group in dist
86                    .dependency_groups
87                    .keys()
88                    .filter(|group| groups.contains(group))
89                {
90                    activated_groups.push((&dist.id.name, group));
91                }
92            }
93        }
94
95        // Initialize the workspace roots.
96        let mut roots = vec![];
97        for root_name in self.roots() {
98            let dist = self
99                .lock()
100                .find_by_name(root_name)
101                .map_err(|_| LockErrorKind::MultipleRootPackages {
102                    name: root_name.clone(),
103                })?
104                .ok_or_else(|| LockErrorKind::MissingRootPackage {
105                    name: root_name.clone(),
106                })?;
107
108            // Add the workspace package to the graph.
109            let index = petgraph.add_node(if groups.prod() {
110                self.package_to_node(dist, tags, build_options, install_options, marker_env)?
111            } else {
112                self.non_installable_node(dist, tags, marker_env)?
113            });
114            inverse.insert(&dist.id, index);
115
116            // Add an edge from the root.
117            petgraph.add_edge(root, index, Edge::Prod);
118
119            // Push the package onto the queue.
120            roots.push((dist, index));
121        }
122
123        // Add the workspace dependencies to the queue.
124        for (dist, index) in roots {
125            if groups.prod() {
126                // Push its dependencies onto the queue.
127                queue.push_back((dist, None));
128                for extra in extras.extra_names(dist.optional_dependencies.keys()) {
129                    queue.push_back((dist, Some(extra)));
130                }
131            }
132
133            // Add any dev dependencies.
134            for (group, dep) in dist
135                .dependency_groups
136                .iter()
137                .filter_map(|(group, deps)| {
138                    if groups.contains(group) {
139                        Some(deps.iter().map(move |dep| (group, dep)))
140                    } else {
141                        None
142                    }
143                })
144                .flatten()
145            {
146                if !dep.complexified_marker.evaluate(
147                    marker_env,
148                    activated_projects.iter().copied(),
149                    activated_extras.iter().copied(),
150                    activated_groups.iter().copied(),
151                ) {
152                    continue;
153                }
154
155                let dep_dist = self.lock().find_by_id(&dep.package_id);
156
157                // Add the package to the graph.
158                let dep_index = match inverse.entry(&dep.package_id) {
159                    Entry::Vacant(entry) => {
160                        let index = petgraph.add_node(self.package_to_node(
161                            dep_dist,
162                            tags,
163                            build_options,
164                            install_options,
165                            marker_env,
166                        )?);
167                        entry.insert(index);
168                        index
169                    }
170                    Entry::Occupied(entry) => {
171                        // Critically, if the package is already in the graph, then it's a workspace
172                        // member. If it was omitted due to, e.g., `--only-dev`, but is itself
173                        // referenced as a development dependency, then we need to re-enable it.
174                        let index = *entry.get();
175                        let node = &mut petgraph[index];
176                        if !groups.prod() {
177                            *node = self.package_to_node(
178                                dep_dist,
179                                tags,
180                                build_options,
181                                install_options,
182                                marker_env,
183                            )?;
184                        }
185                        index
186                    }
187                };
188
189                petgraph.add_edge(
190                    index,
191                    dep_index,
192                    // This is OK because we are resolving to a resolution for
193                    // a specific marker environment and set of extras/groups.
194                    // So at this point, we know the extras/groups have been
195                    // satisfied, so we can safely drop the conflict marker.
196                    Edge::Dev(group.clone()),
197                );
198
199                // Push its dependencies on the queue.
200                if seen.insert((&dep.package_id, None)) {
201                    queue.push_back((dep_dist, None));
202                }
203                for extra in &dep.extra {
204                    if seen.insert((&dep.package_id, Some(extra))) {
205                        queue.push_back((dep_dist, Some(extra)));
206                    }
207                }
208            }
209        }
210
211        // Add any requirements that are exclusive to the workspace root (e.g., dependencies in
212        // PEP 723 scripts).
213        for dependency in self.lock().requirements() {
214            if !dependency.marker.evaluate(marker_env, &[]) {
215                continue;
216            }
217
218            let root_name = &dependency.name;
219            let dist = self
220                .lock()
221                .find_by_markers(root_name, marker_env)
222                .map_err(|_| LockErrorKind::MultipleRootPackages {
223                    name: root_name.clone(),
224                })?
225                .ok_or_else(|| LockErrorKind::MissingRootPackage {
226                    name: root_name.clone(),
227                })?;
228
229            // Add the package to the graph.
230            let index = petgraph.add_node(if groups.prod() {
231                self.package_to_node(dist, tags, build_options, install_options, marker_env)?
232            } else {
233                self.non_installable_node(dist, tags, marker_env)?
234            });
235            inverse.insert(&dist.id, index);
236
237            // Add the edge.
238            petgraph.add_edge(root, index, Edge::Prod);
239
240            // Push its dependencies on the queue.
241            if seen.insert((&dist.id, None)) {
242                queue.push_back((dist, None));
243            }
244            for extra in &dependency.extras {
245                if seen.insert((&dist.id, Some(extra))) {
246                    queue.push_back((dist, Some(extra)));
247                }
248            }
249        }
250
251        // Add any dependency groups that are exclusive to the workspace root (e.g., dev
252        // dependencies in (legacy) non-project workspace roots).
253        for (group, dependency) in self
254            .lock()
255            .dependency_groups()
256            .iter()
257            .filter_map(|(group, deps)| {
258                if groups.contains(group) {
259                    Some(deps.iter().map(move |dep| (group, dep)))
260                } else {
261                    None
262                }
263            })
264            .flatten()
265        {
266            if !dependency.marker.evaluate(marker_env, &[]) {
267                continue;
268            }
269
270            let root_name = &dependency.name;
271            let dist = self
272                .lock()
273                .find_by_markers(root_name, marker_env)
274                .map_err(|_| LockErrorKind::MultipleRootPackages {
275                    name: root_name.clone(),
276                })?
277                .ok_or_else(|| LockErrorKind::MissingRootPackage {
278                    name: root_name.clone(),
279                })?;
280
281            // Add the package to the graph.
282            let index = match inverse.entry(&dist.id) {
283                Entry::Vacant(entry) => {
284                    let index = petgraph.add_node(self.package_to_node(
285                        dist,
286                        tags,
287                        build_options,
288                        install_options,
289                        marker_env,
290                    )?);
291                    entry.insert(index);
292                    index
293                }
294                Entry::Occupied(entry) => {
295                    // Critically, if the package is already in the graph, then it's a workspace
296                    // member. If it was omitted due to, e.g., `--only-dev`, but is itself
297                    // referenced as a development dependency, then we need to re-enable it.
298                    let index = *entry.get();
299                    let node = &mut petgraph[index];
300                    if !groups.prod() {
301                        *node = self.package_to_node(
302                            dist,
303                            tags,
304                            build_options,
305                            install_options,
306                            marker_env,
307                        )?;
308                    }
309                    index
310                }
311            };
312
313            // Add the edge.
314            petgraph.add_edge(root, index, Edge::Dev(group.clone()));
315
316            // Push its dependencies on the queue.
317            if seen.insert((&dist.id, None)) {
318                queue.push_back((dist, None));
319            }
320            for extra in &dependency.extras {
321                if seen.insert((&dist.id, Some(extra))) {
322                    queue.push_back((dist, Some(extra)));
323                }
324            }
325        }
326
327        // Below, we traverse the dependency graph in a breadth first manner
328        // twice. It's only in the second traversal that we actually build
329        // up our resolution graph. In the first traversal, we accumulate all
330        // activated extras. This includes the extras explicitly enabled on
331        // the CLI (which were gathered above) and the extras enabled via
332        // dependency specifications like `foo[extra]`. We need to do this
333        // to correctly support conflicting extras.
334        //
335        // In particular, the way conflicting extras works is by forking the
336        // resolver based on the extras that are declared as conflicting. But
337        // this forking needs to be made manifest somehow in the lock file to
338        // avoid multiple versions of the same package being installed into the
339        // environment. This is why "conflict markers" were invented. For
340        // example, you might have both `torch` and `torch+cpu` in your
341        // dependency graph, where the latter is only enabled when the `cpu`
342        // extra is enabled, and the former is specifically *not* enabled
343        // when the `cpu` extra is enabled.
344        //
345        // In order to evaluate these conflict markers correctly, we need to
346        // know whether the `cpu` extra is enabled when we visit the `torch`
347        // dependency. If we think it's disabled, then we'll erroneously
348        // include it if the extra is actually enabled. But in order to tell
349        // if it's enabled, we need to traverse the entire dependency graph
350        // first to inspect which extras are enabled!
351        //
352        // Of course, we don't need to do this at all if there aren't any
353        // conflicts. In which case, we skip all of this and just do the one
354        // traversal below.
355        if !self.lock().conflicts().is_empty() {
356            let mut activated_extras_set: BTreeSet<(&PackageName, &ExtraName)> =
357                activated_extras.iter().copied().collect();
358            let mut queue = queue.clone();
359            let mut seen = seen.clone();
360            while let Some((package, extra)) = queue.pop_front() {
361                let deps = if let Some(extra) = extra {
362                    Either::Left(
363                        package
364                            .optional_dependencies
365                            .get(extra)
366                            .into_iter()
367                            .flatten(),
368                    )
369                } else {
370                    Either::Right(package.dependencies.iter())
371                };
372                for dep in deps {
373                    let mut additional_activated_extras = vec![];
374                    for extra in &dep.extra {
375                        let key = (&dep.package_id.name, extra);
376                        if !activated_extras_set.contains(&key) {
377                            additional_activated_extras.push(key);
378                        }
379                    }
380                    if !dep.complexified_marker.evaluate(
381                        marker_env,
382                        activated_projects.iter().copied(),
383                        activated_extras
384                            .iter()
385                            .chain(additional_activated_extras.iter())
386                            .copied(),
387                        activated_groups.iter().copied(),
388                    ) {
389                        continue;
390                    }
391                    // It is, I believe, possible to be here for a dependency that
392                    // will ultimately not be included in the final resolution.
393                    // Specifically, carrying on from the example in the comments
394                    // above, we might visit `torch` first and thus not know if
395                    // the `cpu` feature is enabled or not, and thus, the marker
396                    // evaluation above will pass.
397                    //
398                    // So is this a problem? Well, this is the main reason why we
399                    // do two graph traversals. On the second traversal below, we
400                    // will have seen all of the enabled extras, and so `torch`
401                    // will be excluded.
402                    //
403                    // But could this lead to a bigger list of activated extras
404                    // than we actually have? I believe that is indeed possible,
405                    // but I think it is only a problem if it leads to extras that
406                    // *conflict* with one another being simultaneously enabled.
407                    // However, after this first traversal, we check our set of
408                    // accumulated extras to ensure that there are no conflicts. If
409                    // there are, we raise an error. ---AG
410
411                    for key in additional_activated_extras {
412                        activated_extras_set.insert(key);
413                        activated_extras.push(key);
414                    }
415                    let dep_dist = self.lock().find_by_id(&dep.package_id);
416                    // Push its dependencies on the queue.
417                    if seen.insert((&dep.package_id, None)) {
418                        queue.push_back((dep_dist, None));
419                    }
420                    for extra in &dep.extra {
421                        if seen.insert((&dep.package_id, Some(extra))) {
422                            queue.push_back((dep_dist, Some(extra)));
423                        }
424                    }
425                }
426            }
427            // At time of writing, it's somewhat expected that the set of
428            // conflicting extras is pretty small. With that said, the
429            // time complexity of the following routine is pretty gross.
430            // Namely, `set.contains` is linear in the size of the set,
431            // iteration over all conflicts is also obviously linear in
432            // the number of conflicting sets and then for each of those,
433            // we visit every possible pair of activated extra from above,
434            // which is quadratic in the total number of extras enabled. I
435            // believe the simplest improvement here, if it's necessary, is
436            // to adjust the `Conflicts` internals to own these sorts of
437            // checks. ---AG
438            for set in self.lock().conflicts().iter() {
439                for ((pkg1, extra1), (pkg2, extra2)) in
440                    activated_extras_set.iter().tuple_combinations()
441                {
442                    if set.contains(pkg1, *extra1) && set.contains(pkg2, *extra2) {
443                        return Err(LockErrorKind::ConflictingExtra {
444                            package1: (*pkg1).clone(),
445                            extra1: (*extra1).clone(),
446                            package2: (*pkg2).clone(),
447                            extra2: (*extra2).clone(),
448                        }
449                        .into());
450                    }
451                }
452            }
453        }
454
455        while let Some((package, extra)) = queue.pop_front() {
456            let deps = if let Some(extra) = extra {
457                Either::Left(
458                    package
459                        .optional_dependencies
460                        .get(extra)
461                        .into_iter()
462                        .flatten(),
463                )
464            } else {
465                Either::Right(package.dependencies.iter())
466            };
467            for dep in deps {
468                if !dep.complexified_marker.evaluate(
469                    marker_env,
470                    activated_projects.iter().copied(),
471                    activated_extras.iter().copied(),
472                    activated_groups.iter().copied(),
473                ) {
474                    continue;
475                }
476
477                let dep_dist = self.lock().find_by_id(&dep.package_id);
478
479                // Add the dependency to the graph.
480                let dep_index = match inverse.entry(&dep.package_id) {
481                    Entry::Vacant(entry) => {
482                        let index = petgraph.add_node(self.package_to_node(
483                            dep_dist,
484                            tags,
485                            build_options,
486                            install_options,
487                            marker_env,
488                        )?);
489                        entry.insert(index);
490                        index
491                    }
492                    Entry::Occupied(entry) => *entry.get(),
493                };
494
495                // Add the edge.
496                let index = inverse[&package.id];
497                petgraph.add_edge(
498                    index,
499                    dep_index,
500                    if let Some(extra) = extra {
501                        Edge::Optional(extra.clone())
502                    } else {
503                        Edge::Prod
504                    },
505                );
506
507                // Push its dependencies on the queue.
508                if seen.insert((&dep.package_id, None)) {
509                    queue.push_back((dep_dist, None));
510                }
511                for extra in &dep.extra {
512                    if seen.insert((&dep.package_id, Some(extra))) {
513                        queue.push_back((dep_dist, Some(extra)));
514                    }
515                }
516            }
517        }
518
519        Ok(Resolution::new(petgraph))
520    }
521
522    /// Create an installable [`Node`] from a [`Package`].
523    fn installable_node(
524        &self,
525        package: &Package,
526        tags: &Tags,
527        marker_env: &ResolverMarkerEnvironment,
528        build_options: &BuildOptions,
529    ) -> Result<Node, LockError> {
530        let dist = package.to_dist(
531            self.install_path(),
532            TagPolicy::Required(tags),
533            build_options,
534            marker_env,
535        )?;
536        let version = package.version().cloned();
537        let dist = ResolvedDist::Installable {
538            dist: Arc::new(dist),
539            version,
540        };
541        let hashes = package.hashes();
542        Ok(Node::Dist {
543            dist,
544            hashes,
545            install: true,
546        })
547    }
548
549    /// Create a non-installable [`Node`] from a [`Package`].
550    fn non_installable_node(
551        &self,
552        package: &Package,
553        tags: &Tags,
554        marker_env: &ResolverMarkerEnvironment,
555    ) -> Result<Node, LockError> {
556        let dist = package.to_dist(
557            self.install_path(),
558            TagPolicy::Preferred(tags),
559            &BuildOptions::default(),
560            marker_env,
561        )?;
562        let version = package.version().cloned();
563        let dist = ResolvedDist::Installable {
564            dist: Arc::new(dist),
565            version,
566        };
567        let hashes = package.hashes();
568        Ok(Node::Dist {
569            dist,
570            hashes,
571            install: false,
572        })
573    }
574
575    /// Convert a lockfile entry to a graph [`Node`].
576    fn package_to_node(
577        &self,
578        package: &Package,
579        tags: &Tags,
580        build_options: &BuildOptions,
581        install_options: &InstallOptions,
582        marker_env: &ResolverMarkerEnvironment,
583    ) -> Result<Node, LockError> {
584        if install_options.include_package(
585            package.as_install_target(),
586            self.project_name(),
587            self.lock().members(),
588        ) {
589            self.installable_node(package, tags, marker_env, build_options)
590        } else {
591            self.non_installable_node(package, tags, marker_env)
592        }
593    }
594}