Skip to main content

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::{ConflictKind, ConflictSet, ResolverMarkerEnvironment};
18
19use crate::lock::{Dependency, HashedDist, LockErrorKind, Package, TagPolicy};
20use crate::{Lock, LockError};
21
22fn newly_activated_extras<'lock>(
23    dep: &'lock Dependency,
24    activated_extras: &[(&'lock PackageName, &'lock ExtraName)],
25) -> Vec<(&'lock PackageName, &'lock ExtraName)> {
26    dep.extra
27        .iter()
28        .filter_map(|extra| {
29            let key = (&dep.package_id.name, extra);
30            (!activated_extras.contains(&key)).then_some(key)
31        })
32        .collect()
33}
34
35pub trait Installable<'lock> {
36    /// Return the root install path.
37    fn install_path(&self) -> &'lock Path;
38
39    /// Return the [`Lock`] to install.
40    fn lock(&self) -> &'lock Lock;
41
42    /// Return the [`PackageName`] of the root packages in the target.
43    fn roots(&self) -> impl Iterator<Item = &PackageName>;
44
45    /// Return the [`PackageName`] of the target, if available.
46    fn project_name(&self) -> Option<&PackageName>;
47
48    /// Convert the [`Lock`] to a [`Resolution`] using the given marker environment, tags, and root.
49    fn to_resolution(
50        &self,
51        marker_env: &ResolverMarkerEnvironment,
52        tags: &Tags,
53        extras: &ExtrasSpecificationWithDefaults,
54        groups: &DependencyGroupsWithDefaults,
55        build_options: &BuildOptions,
56        install_options: &InstallOptions,
57    ) -> Result<Resolution, LockError> {
58        let roots = self
59            .roots()
60            .map(|root_name| {
61                self.lock()
62                    .find_by_name(root_name)
63                    .map_err(|_| LockErrorKind::MultipleRootPackages {
64                        name: root_name.clone(),
65                    })?
66                    .ok_or_else(|| {
67                        LockError::from(LockErrorKind::MissingRootPackage {
68                            name: root_name.clone(),
69                        })
70                    })
71            })
72            .collect::<Result<Vec<_>, LockError>>()?;
73
74        InstallableExt::to_resolution_from_packages(
75            self,
76            &roots,
77            true,
78            marker_env,
79            tags,
80            extras,
81            groups,
82            build_options,
83            install_options,
84        )
85    }
86
87    /// Create an installable [`Node`] from a [`Package`].
88    fn installable_node(
89        &self,
90        package: &Package,
91        tags: &Tags,
92        marker_env: &ResolverMarkerEnvironment,
93        build_options: &BuildOptions,
94    ) -> Result<Node, LockError> {
95        let tag_policy = TagPolicy::Required(tags);
96        let HashedDist { dist, hashes } =
97            package.to_dist(self.install_path(), tag_policy, build_options, marker_env)?;
98        let version = package.version().cloned();
99        let dist = ResolvedDist::Installable {
100            dist: Arc::new(dist),
101            version,
102        };
103        Ok(Node::Dist {
104            dist,
105            hashes,
106            install: true,
107        })
108    }
109
110    /// Create a non-installable [`Node`] from a [`Package`].
111    fn non_installable_node(
112        &self,
113        package: &Package,
114        tags: &Tags,
115        marker_env: &ResolverMarkerEnvironment,
116    ) -> Result<Node, LockError> {
117        let HashedDist { dist, .. } = package.to_dist(
118            self.install_path(),
119            TagPolicy::Preferred(tags),
120            &BuildOptions::default(),
121            marker_env,
122        )?;
123        let version = package.version().cloned();
124        let dist = ResolvedDist::Installable {
125            dist: Arc::new(dist),
126            version,
127        };
128        let hashes = package.hashes();
129        Ok(Node::Dist {
130            dist,
131            hashes,
132            install: false,
133        })
134    }
135
136    /// Convert a lockfile entry to a graph [`Node`].
137    fn package_to_node(
138        &self,
139        package: &Package,
140        tags: &Tags,
141        build_options: &BuildOptions,
142        install_options: &InstallOptions,
143        marker_env: &ResolverMarkerEnvironment,
144    ) -> Result<Node, LockError> {
145        if install_options.include_package(
146            package.as_install_target(),
147            self.project_name(),
148            self.lock().members(),
149        ) {
150            self.installable_node(package, tags, marker_env, build_options)
151        } else {
152            self.non_installable_node(package, tags, marker_env)
153        }
154    }
155}
156
157/// Internal lock-to-resolution implementation shared by [`Installable`] and [`Lock`].
158trait InstallableExt<'lock>: Installable<'lock> {
159    /// Convert concrete locked packages to a [`Resolution`].
160    ///
161    /// `include_manifest` controls whether requirements attached directly to the lock target are
162    /// included in addition to `roots`.
163    fn to_resolution_from_packages(
164        &self,
165        roots: &[&Package],
166        include_manifest: bool,
167        marker_env: &ResolverMarkerEnvironment,
168        tags: &Tags,
169        extras: &ExtrasSpecificationWithDefaults,
170        groups: &DependencyGroupsWithDefaults,
171        build_options: &BuildOptions,
172        install_options: &InstallOptions,
173    ) -> Result<Resolution, LockError> {
174        let size_guess = self.lock().packages.len();
175        let mut petgraph = Graph::with_capacity(size_guess, size_guess);
176        let mut inverse = FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher);
177
178        let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new();
179        let mut seen = FxHashSet::default();
180        let mut activated_projects: Vec<&PackageName> = vec![];
181        let mut activated_extras: Vec<(&PackageName, &ExtraName)> = vec![];
182        let mut activated_groups: Vec<(&PackageName, &GroupName)> = vec![];
183        let validate_conflicts = !include_manifest && !self.lock().conflicts().is_empty();
184        let mut dependencies_for_conflict_validation = vec![];
185
186        let root = petgraph.add_node(Node::Root);
187
188        // Determine the set of activated extras and groups, from the root.
189        //
190        // TODO(charlie): This isn't quite right. Below, when we add the dependency groups to the
191        // graph, we rely on the activated extras and dependency groups, to evaluate the conflict
192        // marker. But at that point, we don't know the full set of activated extras; this is only
193        // computed below. We somehow need to add the dependency groups _after_ we've computed all
194        // enabled extras, but the groups themselves could depend on the set of enabled extras.
195        if !self.lock().conflicts().is_empty() {
196            for dist in roots.iter().copied() {
197                // Track the activated extras.
198                if groups.prod() {
199                    activated_projects.push(&dist.id.name);
200                    for extra in extras.extra_names(dist.optional_dependencies.keys()) {
201                        activated_extras.push((&dist.id.name, extra));
202                    }
203                }
204
205                // Track the activated groups.
206                for group in dist
207                    .dependency_groups
208                    .keys()
209                    .filter(|group| groups.contains(group))
210                {
211                    activated_groups.push((&dist.id.name, group));
212                }
213            }
214        }
215
216        // Initialize the workspace roots.
217        let mut initialized_roots = vec![];
218        for dist in roots.iter().copied() {
219            // Add the workspace package to the graph.
220            let index = petgraph.add_node(if groups.prod() {
221                self.package_to_node(dist, tags, build_options, install_options, marker_env)?
222            } else {
223                self.non_installable_node(dist, tags, marker_env)?
224            });
225            inverse.insert(&dist.id, index);
226
227            // Add an edge from the root.
228            petgraph.add_edge(root, index, Edge::Prod);
229
230            // Push the package onto the queue.
231            initialized_roots.push((dist, index));
232        }
233
234        // Add the workspace dependencies to the queue.
235        for (dist, index) in initialized_roots {
236            if groups.prod() {
237                // Push its dependencies onto the queue.
238                queue.push_back((dist, None));
239                for extra in extras.extra_names(dist.optional_dependencies.keys()) {
240                    queue.push_back((dist, Some(extra)));
241                }
242            }
243
244            // Add any dev dependencies.
245            for (group, dep) in dist
246                .dependency_groups
247                .iter()
248                .filter_map(|(group, deps)| {
249                    if groups.contains(group) {
250                        Some(deps.iter().map(move |dep| (group, dep)))
251                    } else {
252                        None
253                    }
254                })
255                .flatten()
256            {
257                if validate_conflicts && dep.complexified_marker.has_conflict_marker() {
258                    dependencies_for_conflict_validation.push((dist, dep));
259                }
260                let additional_activated_extras = newly_activated_extras(dep, &activated_extras);
261                if !dep.complexified_marker.evaluate(
262                    marker_env,
263                    activated_projects.iter().copied(),
264                    activated_extras
265                        .iter()
266                        .chain(additional_activated_extras.iter())
267                        .copied(),
268                    activated_groups.iter().copied(),
269                ) {
270                    continue;
271                }
272
273                let dep_dist = self.lock().find_by_id(&dep.package_id);
274
275                // Add the package to the graph.
276                let dep_index = match inverse.entry(&dep.package_id) {
277                    Entry::Vacant(entry) => {
278                        let index = petgraph.add_node(self.package_to_node(
279                            dep_dist,
280                            tags,
281                            build_options,
282                            install_options,
283                            marker_env,
284                        )?);
285                        entry.insert(index);
286                        index
287                    }
288                    Entry::Occupied(entry) => {
289                        // Critically, if the package is already in the graph, then it's a workspace
290                        // member. If it was omitted due to, e.g., `--only-dev`, but is itself
291                        // referenced as a development dependency, then we need to re-enable it.
292                        let index = *entry.get();
293                        let node = &mut petgraph[index];
294                        if !groups.prod() {
295                            *node = self.package_to_node(
296                                dep_dist,
297                                tags,
298                                build_options,
299                                install_options,
300                                marker_env,
301                            )?;
302                        }
303                        index
304                    }
305                };
306
307                petgraph.add_edge(
308                    index,
309                    dep_index,
310                    // This is OK because we are resolving to a resolution for
311                    // a specific marker environment and set of extras/groups.
312                    // So at this point, we know the extras/groups have been
313                    // satisfied, so we can safely drop the conflict marker.
314                    Edge::Dev(group.clone()),
315                );
316
317                // Push its dependencies on the queue.
318                if seen.insert((&dep.package_id, None)) {
319                    queue.push_back((dep_dist, None));
320                }
321                for extra in &dep.extra {
322                    if seen.insert((&dep.package_id, Some(extra))) {
323                        queue.push_back((dep_dist, Some(extra)));
324                    }
325                }
326            }
327        }
328
329        if include_manifest {
330            // Add any requirements that are exclusive to the workspace root (e.g., dependencies in
331            // PEP 723 scripts).
332            for dependency in self.lock().requirements() {
333                if !dependency.marker.evaluate(marker_env, &[]) {
334                    continue;
335                }
336
337                let root_name = &dependency.name;
338                let dist = self
339                    .lock()
340                    .find_by_markers(root_name, marker_env)
341                    .map_err(|_| LockErrorKind::MultipleRootPackages {
342                        name: root_name.clone(),
343                    })?
344                    .ok_or_else(|| LockErrorKind::MissingRootPackage {
345                        name: root_name.clone(),
346                    })?;
347
348                // Add the package to the graph.
349                let index = petgraph.add_node(if groups.prod() {
350                    self.package_to_node(dist, tags, build_options, install_options, marker_env)?
351                } else {
352                    self.non_installable_node(dist, tags, marker_env)?
353                });
354                inverse.insert(&dist.id, index);
355
356                // Add the edge.
357                petgraph.add_edge(root, index, Edge::Prod);
358
359                // Push its dependencies on the queue.
360                if seen.insert((&dist.id, None)) {
361                    queue.push_back((dist, None));
362                }
363                for extra in &dependency.extras {
364                    if seen.insert((&dist.id, Some(extra))) {
365                        queue.push_back((dist, Some(extra)));
366                    }
367                }
368            }
369
370            // Add any dependency groups that are exclusive to the workspace root (e.g., dev
371            // dependencies in non-project workspace roots).
372            for (group, dependency) in self
373                .lock()
374                .dependency_groups()
375                .iter()
376                .filter_map(|(group, deps)| {
377                    if groups.contains(group) {
378                        Some(deps.iter().map(move |dep| (group, dep)))
379                    } else {
380                        None
381                    }
382                })
383                .flatten()
384            {
385                if !dependency.marker.evaluate(marker_env, &[]) {
386                    continue;
387                }
388
389                let root_name = &dependency.name;
390                let dist = self
391                    .lock()
392                    .find_by_markers(root_name, marker_env)
393                    .map_err(|_| LockErrorKind::MultipleRootPackages {
394                        name: root_name.clone(),
395                    })?
396                    .ok_or_else(|| LockErrorKind::MissingRootPackage {
397                        name: root_name.clone(),
398                    })?;
399
400                // Add the package to the graph.
401                let index = match inverse.entry(&dist.id) {
402                    Entry::Vacant(entry) => {
403                        let index = petgraph.add_node(self.package_to_node(
404                            dist,
405                            tags,
406                            build_options,
407                            install_options,
408                            marker_env,
409                        )?);
410                        entry.insert(index);
411                        index
412                    }
413                    Entry::Occupied(entry) => {
414                        // Critically, if the package is already in the graph, then it's a workspace
415                        // member. If it was omitted due to, e.g., `--only-dev`, but is itself
416                        // referenced as a development dependency, then we need to re-enable it.
417                        let index = *entry.get();
418                        let node = &mut petgraph[index];
419                        if !groups.prod() {
420                            *node = self.package_to_node(
421                                dist,
422                                tags,
423                                build_options,
424                                install_options,
425                                marker_env,
426                            )?;
427                        }
428                        index
429                    }
430                };
431
432                // Add the edge.
433                petgraph.add_edge(root, index, Edge::Dev(group.clone()));
434
435                // Push its dependencies on the queue.
436                if seen.insert((&dist.id, None)) {
437                    queue.push_back((dist, None));
438                }
439                for extra in &dependency.extras {
440                    if seen.insert((&dist.id, Some(extra))) {
441                        queue.push_back((dist, Some(extra)));
442                    }
443                }
444            }
445        }
446
447        // Below, we traverse the dependency graph in a breadth first manner
448        // twice. It's only in the second traversal that we actually build
449        // up our resolution graph. In the first traversal, we accumulate all
450        // activated extras. This includes the extras explicitly enabled on
451        // the CLI (which were gathered above) and the extras enabled via
452        // dependency specifications like `foo[extra]`. We need to do this
453        // to correctly support conflicting extras.
454        //
455        // In particular, the way conflicting extras works is by forking the
456        // resolver based on the extras that are declared as conflicting. But
457        // this forking needs to be made manifest somehow in the lock file to
458        // avoid multiple versions of the same package being installed into the
459        // environment. This is why "conflict markers" were invented. For
460        // example, you might have both `torch` and `torch+cpu` in your
461        // dependency graph, where the latter is only enabled when the `cpu`
462        // extra is enabled, and the former is specifically *not* enabled
463        // when the `cpu` extra is enabled.
464        //
465        // In order to evaluate these conflict markers correctly, we need to
466        // know whether the `cpu` extra is enabled when we visit the `torch`
467        // dependency. If we think it's disabled, then we'll erroneously
468        // include it if the extra is actually enabled. But in order to tell
469        // if it's enabled, we need to traverse the entire dependency graph
470        // first to inspect which extras are enabled!
471        //
472        // Of course, we don't need to do this at all if there aren't any
473        // conflicts. In which case, we skip all of this and just do the one
474        // traversal below.
475        if !self.lock().conflicts().is_empty() {
476            let mut activated_extras_set: BTreeSet<(&PackageName, &ExtraName)> =
477                activated_extras.iter().copied().collect();
478            let mut queue = queue.clone();
479            let mut seen = seen.clone();
480            while let Some((package, extra)) = queue.pop_front() {
481                let deps = if let Some(extra) = extra {
482                    Either::Left(
483                        package
484                            .optional_dependencies
485                            .get(extra)
486                            .into_iter()
487                            .flatten(),
488                    )
489                } else {
490                    Either::Right(package.dependencies.iter())
491                };
492                for dep in deps {
493                    let additional_activated_extras =
494                        newly_activated_extras(dep, &activated_extras);
495                    if !dep.complexified_marker.evaluate(
496                        marker_env,
497                        activated_projects.iter().copied(),
498                        activated_extras
499                            .iter()
500                            .chain(additional_activated_extras.iter())
501                            .copied(),
502                        activated_groups.iter().copied(),
503                    ) {
504                        continue;
505                    }
506                    // It is, I believe, possible to be here for a dependency that
507                    // will ultimately not be included in the final resolution.
508                    // Specifically, carrying on from the example in the comments
509                    // above, we might visit `torch` first and thus not know if
510                    // the `cpu` feature is enabled or not, and thus, the marker
511                    // evaluation above will pass.
512                    //
513                    // So is this a problem? Well, this is the main reason why we
514                    // do two graph traversals. On the second traversal below, we
515                    // will have seen all of the enabled extras, and so `torch`
516                    // will be excluded.
517                    //
518                    // But could this lead to a bigger list of activated extras
519                    // than we actually have? I believe that is indeed possible,
520                    // but I think it is only a problem if it leads to extras that
521                    // *conflict* with one another being simultaneously enabled.
522                    // However, after this first traversal, we check our set of
523                    // accumulated extras to ensure that there are no conflicts. If
524                    // there are, we raise an error. ---AG
525
526                    for key in additional_activated_extras {
527                        activated_extras_set.insert(key);
528                        activated_extras.push(key);
529                    }
530                    let dep_dist = self.lock().find_by_id(&dep.package_id);
531                    // Push its dependencies on the queue.
532                    if seen.insert((&dep.package_id, None)) {
533                        queue.push_back((dep_dist, None));
534                    }
535                    for extra in &dep.extra {
536                        if seen.insert((&dep.package_id, Some(extra))) {
537                            queue.push_back((dep_dist, Some(extra)));
538                        }
539                    }
540                }
541            }
542            // At time of writing, it's somewhat expected that the set of
543            // conflicting extras is pretty small. With that said, the
544            // time complexity of the following routine is pretty gross.
545            // Namely, `set.contains` is linear in the size of the set,
546            // iteration over all conflicts is also obviously linear in
547            // the number of conflicting sets and then for each of those,
548            // we visit every possible pair of activated extra from above,
549            // which is quadratic in the total number of extras enabled. I
550            // believe the simplest improvement here, if it's necessary, is
551            // to adjust the `Conflicts` internals to own these sorts of
552            // checks. ---AG
553            for set in self.lock().conflicts().iter() {
554                for ((pkg1, extra1), (pkg2, extra2)) in
555                    activated_extras_set.iter().tuple_combinations()
556                {
557                    if set.contains(pkg1, *extra1) && set.contains(pkg2, *extra2) {
558                        return Err(LockErrorKind::ConflictingExtra {
559                            package1: (*pkg1).clone(),
560                            extra1: (*extra1).clone(),
561                            package2: (*pkg2).clone(),
562                            extra2: (*extra2).clone(),
563                        }
564                        .into());
565                    }
566                }
567            }
568        }
569
570        while let Some((package, extra)) = queue.pop_front() {
571            let deps = if let Some(extra) = extra {
572                Either::Left(
573                    package
574                        .optional_dependencies
575                        .get(extra)
576                        .into_iter()
577                        .flatten(),
578                )
579            } else {
580                Either::Right(package.dependencies.iter())
581            };
582            for dep in deps {
583                if validate_conflicts && dep.complexified_marker.has_conflict_marker() {
584                    dependencies_for_conflict_validation.push((package, dep));
585                }
586                if !dep.complexified_marker.evaluate(
587                    marker_env,
588                    activated_projects.iter().copied(),
589                    activated_extras.iter().copied(),
590                    activated_groups.iter().copied(),
591                ) {
592                    continue;
593                }
594
595                let dep_dist = self.lock().find_by_id(&dep.package_id);
596
597                // Add the dependency to the graph.
598                let dep_index = match inverse.entry(&dep.package_id) {
599                    Entry::Vacant(entry) => {
600                        let index = petgraph.add_node(self.package_to_node(
601                            dep_dist,
602                            tags,
603                            build_options,
604                            install_options,
605                            marker_env,
606                        )?);
607                        entry.insert(index);
608                        index
609                    }
610                    Entry::Occupied(entry) => *entry.get(),
611                };
612
613                // Add the edge.
614                let index = inverse[&package.id];
615                petgraph.add_edge(
616                    index,
617                    dep_index,
618                    if let Some(extra) = extra {
619                        Edge::Optional(extra.clone())
620                    } else {
621                        Edge::Prod
622                    },
623                );
624
625                // Push its dependencies on the queue.
626                if seen.insert((&dep.package_id, None)) {
627                    queue.push_back((dep_dist, None));
628                }
629                for extra in &dep.extra {
630                    if seen.insert((&dep.package_id, Some(extra))) {
631                        queue.push_back((dep_dist, Some(extra)));
632                    }
633                }
634            }
635        }
636
637        // Evaluate conflict markers from concrete roots, not from workspace members that depend on
638        // them. Reject markers that still depend on conflict items outside the resulting subgraph.
639        if !dependencies_for_conflict_validation.is_empty() {
640            let subgraph_packages = inverse
641                .keys()
642                .map(|package_id| &package_id.name)
643                .collect::<FxHashSet<_>>();
644
645            // The environment and conflict state are shared by every dependency, so repeated
646            // markers have the same result.
647            let mut validated_markers = FxHashSet::default();
648            for (package, dependency) in dependencies_for_conflict_validation {
649                if !validated_markers.insert(dependency.complexified_marker) {
650                    continue;
651                }
652                let mut marker = dependency.complexified_marker;
653                for item in self.lock().conflicts().iter().flat_map(ConflictSet::iter) {
654                    if !subgraph_packages.contains(item.package()) {
655                        continue;
656                    }
657
658                    let active = match item.kind() {
659                        ConflictKind::Project => activated_projects.contains(&item.package()),
660                        ConflictKind::Extra(extra) => {
661                            activated_extras.contains(&(item.package(), extra))
662                        }
663                        ConflictKind::Group(group) => {
664                            activated_groups.contains(&(item.package(), group))
665                        }
666                    };
667                    if active {
668                        marker.assume_conflict_item(item);
669                    } else {
670                        marker.assume_not_conflict_item(item);
671                    }
672                }
673
674                let conflict = marker.conflict_for_environment(marker_env);
675                // All in-subgraph conflict items were resolved above, so a non-constant marker
676                // still depends on a package outside the subgraph.
677                if !conflict.is_constant() {
678                    return Err(LockErrorKind::DependencyConflictOutsideSubgraph {
679                        package: package.id.clone(),
680                        dependency: dependency.package_id.clone(),
681                    }
682                    .into());
683                }
684            }
685        }
686
687        Ok(Resolution::new(petgraph))
688    }
689}
690
691impl<'lock, T> InstallableExt<'lock> for T where T: Installable<'lock> + ?Sized {}
692
693/// An [`Installable`] adapter for materializing concrete packages directly from a [`Lock`].
694struct LockedPackages<'lock> {
695    lock: &'lock Lock,
696    install_path: &'lock Path,
697    project_name: Option<&'lock PackageName>,
698}
699
700impl<'lock> Installable<'lock> for LockedPackages<'lock> {
701    fn install_path(&self) -> &'lock Path {
702        self.install_path
703    }
704
705    fn lock(&self) -> &'lock Lock {
706        self.lock
707    }
708
709    fn roots(&self) -> impl Iterator<Item = &PackageName> {
710        std::iter::empty()
711    }
712
713    fn project_name(&self) -> Option<&PackageName> {
714        self.project_name
715    }
716}
717
718impl Lock {
719    /// Materialize the exact dependency subgraph reachable from concrete locked `roots`.
720    ///
721    /// Each root must be a [`Package`] from this lock. Unlike [`Installable::to_resolution`], this
722    /// method does not include requirements or dependency groups attached directly to the lock
723    /// manifest. Extras and dependency groups on the concrete roots are still included according
724    /// to `extras` and `groups`.
725    ///
726    /// Conflict-marker evaluation starts from `roots` and their requested `extras` and `groups`,
727    /// not from workspace members that depend on those roots. The method returns an error if a
728    /// dependency marker still depends on a conflict item outside the resulting subgraph. Use
729    /// [`Installable::to_resolution`] when materializing an existing lock target.
730    ///
731    /// `project_name` identifies the project for project-specific [`InstallOptions`] filters, if
732    /// applicable. Callers are responsible for selecting roots that apply to `marker_env`.
733    pub fn to_resolution<'lock>(
734        &'lock self,
735        install_path: &'lock Path,
736        roots: impl IntoIterator<Item = &'lock Package>,
737        project_name: Option<&'lock PackageName>,
738        marker_env: &ResolverMarkerEnvironment,
739        tags: &Tags,
740        extras: &ExtrasSpecificationWithDefaults,
741        groups: &DependencyGroupsWithDefaults,
742        build_options: &BuildOptions,
743        install_options: &InstallOptions,
744    ) -> Result<Resolution, LockError> {
745        let mut seen = FxHashSet::default();
746        let mut concrete_roots = Vec::new();
747        for root in roots {
748            let Some(index) = self.by_id.get(&root.id) else {
749                return Err(LockErrorKind::RootPackageMissingFromLock {
750                    id: root.id.clone(),
751                }
752                .into());
753            };
754            if seen.insert(&root.id) {
755                let Some(root) = self.packages.get(*index) else {
756                    return Err(LockErrorKind::RootPackageMissingFromLock {
757                        id: root.id.clone(),
758                    }
759                    .into());
760                };
761                concrete_roots.push(root);
762            }
763        }
764
765        LockedPackages {
766            lock: self,
767            install_path,
768            project_name,
769        }
770        .to_resolution_from_packages(
771            &concrete_roots,
772            false,
773            marker_env,
774            tags,
775            extras,
776            groups,
777            build_options,
778            install_options,
779        )
780    }
781}
782
783#[cfg(test)]
784mod tests {
785    use std::cell::Cell;
786    use std::sync::LazyLock;
787
788    use petgraph::visit::EdgeRef;
789    use uv_configuration::{DependencyGroups, ExtrasSpecification};
790    use uv_distribution_types::Name;
791    use uv_normalize::{DefaultExtras, DefaultGroups};
792    use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder};
793    use uv_platform_tags::{Arch, Os, Platform, TagsOptions};
794    use uv_warnings::anstream;
795
796    use super::*;
797
798    static TAGS: LazyLock<Tags> = LazyLock::new(|| {
799        Tags::from_env(
800            &Platform::new(
801                Os::Macos {
802                    major: 14,
803                    minor: 0,
804                },
805                Arch::Aarch64,
806            ),
807            (3, 11),
808            "cpython",
809            (3, 11),
810            TagsOptions::default(),
811        )
812        .expect("valid tags")
813    });
814
815    static DARWIN_MARKERS: LazyLock<ResolverMarkerEnvironment> =
816        LazyLock::new(|| ResolverMarkerEnvironment::from(marker_environment("darwin", "Darwin")));
817
818    static LINUX_MARKERS: LazyLock<ResolverMarkerEnvironment> =
819        LazyLock::new(|| ResolverMarkerEnvironment::from(marker_environment("linux", "Linux")));
820
821    fn marker_environment(
822        sys_platform: &'static str,
823        platform_system: &'static str,
824    ) -> MarkerEnvironment {
825        MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
826            implementation_name: "cpython",
827            implementation_version: "3.11.5",
828            os_name: "posix",
829            platform_machine: "arm64",
830            platform_python_implementation: "CPython",
831            platform_release: "23.0.0",
832            platform_system,
833            platform_version: "test",
834            python_full_version: "3.11.5",
835            python_version: "3.11",
836            sys_platform,
837        })
838        .expect("valid marker environment")
839    }
840
841    fn lock() -> Lock {
842        toml::from_str(
843            r#"
844version = 1
845revision = 3
846requires-python = ">=3.11"
847resolution-markers = [
848    "sys_platform == 'darwin'",
849    "sys_platform != 'darwin'",
850]
851
852[manifest]
853requirements = [{ name = "unrelated" }]
854
855[[package]]
856name = "dev-dependency"
857version = "1.0.0"
858source = { registry = "https://example.com/simple" }
859sdist = { url = "https://example.com/dev_dependency-1.0.0.tar.gz", hash = "sha256:1111111111111111111111111111111111111111111111111111111111111111" }
860
861[[package]]
862name = "forked"
863version = "1.0.0"
864source = { registry = "https://example.com/simple" }
865resolution-markers = ["sys_platform == 'darwin'"]
866sdist = { url = "https://example.com/forked-1.0.0.tar.gz", hash = "sha256:2222222222222222222222222222222222222222222222222222222222222222" }
867
868[[package]]
869name = "forked"
870version = "2.0.0"
871source = { registry = "https://example.com/simple" }
872resolution-markers = ["sys_platform != 'darwin'"]
873sdist = { url = "https://example.com/forked-2.0.0.tar.gz", hash = "sha256:3333333333333333333333333333333333333333333333333333333333333333" }
874
875[[package]]
876name = "optional-dependency"
877version = "1.0.0"
878source = { registry = "https://example.com/simple" }
879sdist = { url = "https://example.com/optional_dependency-1.0.0.tar.gz", hash = "sha256:4444444444444444444444444444444444444444444444444444444444444444" }
880
881[[package]]
882name = "root-a"
883version = "1.0.0"
884source = { registry = "https://example.com/simple" }
885dependencies = [
886    { name = "forked", version = "1.0.0", source = { registry = "https://example.com/simple" }, marker = "sys_platform == 'darwin'" },
887    { name = "forked", version = "2.0.0", source = { registry = "https://example.com/simple" }, marker = "sys_platform != 'darwin'" },
888    { name = "shared" },
889]
890sdist = { url = "https://example.com/root_a-1.0.0.tar.gz", hash = "sha256:5555555555555555555555555555555555555555555555555555555555555555" }
891
892[package.optional-dependencies]
893feature = [{ name = "optional-dependency" }]
894
895[package.dependency-groups]
896dev = [{ name = "dev-dependency" }]
897
898[package.metadata]
899provides-extras = ["feature"]
900
901[[package]]
902name = "root-b"
903version = "1.0.0"
904source = { registry = "https://example.com/simple" }
905dependencies = [{ name = "shared" }]
906sdist = { url = "https://example.com/root_b-1.0.0.tar.gz", hash = "sha256:6666666666666666666666666666666666666666666666666666666666666666" }
907
908[[package]]
909name = "shared"
910version = "1.0.0"
911source = { registry = "https://example.com/simple" }
912sdist = { url = "https://example.com/shared-1.0.0.tar.gz", hash = "sha256:7777777777777777777777777777777777777777777777777777777777777777" }
913
914[[package]]
915name = "unrelated"
916version = "1.0.0"
917source = { registry = "https://example.com/simple" }
918sdist = { url = "https://example.com/unrelated-1.0.0.tar.gz", hash = "sha256:8888888888888888888888888888888888888888888888888888888888888888" }
919"#,
920        )
921        .expect("valid lock")
922    }
923
924    fn conflict_lock() -> Lock {
925        toml::from_str(
926            r#"
927version = 1
928revision = 3
929requires-python = ">=3.11"
930conflicts = [
931    [
932        { package = "tool", extra = "cpu" },
933        { package = "tool", extra = "gpu" },
934    ],
935    [
936        { package = "project", extra = "foo" },
937        { package = "project", extra = "bar" },
938    ],
939]
940
941[[package]]
942name = "contextual-dependency"
943version = "1.0.0"
944source = { registry = "https://example.com/simple" }
945sdist = { url = "https://example.com/contextual_dependency-1.0.0.tar.gz", hash = "sha256:1111111111111111111111111111111111111111111111111111111111111111" }
946
947[[package]]
948name = "contextual-tool"
949version = "1.0.0"
950source = { registry = "https://example.com/simple" }
951dependencies = [
952    { name = "contextual-dependency", marker = "sys_platform == 'linux' or (sys_platform == 'darwin' and extra == 'extra-7-project-foo')" },
953]
954sdist = { url = "https://example.com/contextual_tool-1.0.0.tar.gz", hash = "sha256:2222222222222222222222222222222222222222222222222222222222222222" }
955
956[[package]]
957name = "cpu-backend"
958version = "1.0.0"
959source = { registry = "https://example.com/simple" }
960sdist = { url = "https://example.com/cpu_backend-1.0.0.tar.gz", hash = "sha256:3333333333333333333333333333333333333333333333333333333333333333" }
961
962[[package]]
963name = "gpu-backend"
964version = "1.0.0"
965source = { registry = "https://example.com/simple" }
966sdist = { url = "https://example.com/gpu_backend-1.0.0.tar.gz", hash = "sha256:4444444444444444444444444444444444444444444444444444444444444444" }
967
968[[package]]
969name = "project"
970version = "1.0.0"
971source = { registry = "https://example.com/simple" }
972sdist = { url = "https://example.com/project-1.0.0.tar.gz", hash = "sha256:5555555555555555555555555555555555555555555555555555555555555555" }
973
974[package.optional-dependencies]
975foo = []
976bar = []
977
978[package.metadata]
979provides-extras = ["foo", "bar"]
980
981[[package]]
982name = "runtime"
983version = "1.0.0"
984source = { registry = "https://example.com/simple" }
985dependencies = [
986    { name = "cpu-backend", marker = "extra == 'extra-4-tool-cpu'" },
987    { name = "gpu-backend", marker = "extra == 'extra-4-tool-gpu'" },
988]
989sdist = { url = "https://example.com/runtime-1.0.0.tar.gz", hash = "sha256:6666666666666666666666666666666666666666666666666666666666666666" }
990
991[[package]]
992name = "tool"
993version = "1.0.0"
994source = { registry = "https://example.com/simple" }
995dependencies = [{ name = "runtime" }]
996sdist = { url = "https://example.com/tool-1.0.0.tar.gz", hash = "sha256:7777777777777777777777777777777777777777777777777777777777777777" }
997
998[package.optional-dependencies]
999cpu = []
1000gpu = []
1001
1002[package.metadata]
1003provides-extras = ["cpu", "gpu"]
1004"#,
1005        )
1006        .expect("valid lock")
1007    }
1008
1009    fn package<'lock>(lock: &'lock Lock, name: &str, version: &str) -> &'lock Package {
1010        lock.packages()
1011            .iter()
1012            .find(|package| {
1013                package.name().as_ref() == name
1014                    && package
1015                        .version()
1016                        .is_some_and(|package_version| package_version.to_string() == version)
1017            })
1018            .expect("locked package")
1019    }
1020
1021    fn materialize(
1022        lock: &Lock,
1023        roots: &[&Package],
1024        marker_env: &ResolverMarkerEnvironment,
1025    ) -> Resolution {
1026        let extras = ExtrasSpecification::from_all_extras().with_defaults(DefaultExtras::default());
1027        let groups = DependencyGroups::from_all_groups().with_defaults(DefaultGroups::default());
1028        lock.to_resolution(
1029            Path::new("."),
1030            roots.iter().copied(),
1031            None,
1032            marker_env,
1033            &TAGS,
1034            &extras,
1035            &groups,
1036            &BuildOptions::default(),
1037            &InstallOptions::default(),
1038        )
1039        .expect("valid resolution")
1040    }
1041
1042    fn materialize_with_extras(
1043        lock: &Lock,
1044        roots: &[&Package],
1045        marker_env: &ResolverMarkerEnvironment,
1046        extras: &ExtrasSpecification,
1047    ) -> Result<Resolution, LockError> {
1048        let extras = extras.with_defaults(DefaultExtras::default());
1049        let groups = DependencyGroupsWithDefaults::none();
1050        lock.to_resolution(
1051            Path::new("."),
1052            roots.iter().copied(),
1053            None,
1054            marker_env,
1055            &TAGS,
1056            &extras,
1057            &groups,
1058            &BuildOptions::default(),
1059            &InstallOptions::default(),
1060        )
1061    }
1062
1063    struct OverridingInstallable<'lock> {
1064        lock: &'lock Lock,
1065        root_name: &'lock PackageName,
1066        package_to_node_calls: Cell<usize>,
1067    }
1068
1069    impl<'lock> Installable<'lock> for OverridingInstallable<'lock> {
1070        fn install_path(&self) -> &'lock Path {
1071            Path::new(".")
1072        }
1073
1074        fn lock(&self) -> &'lock Lock {
1075            self.lock
1076        }
1077
1078        fn roots(&self) -> impl Iterator<Item = &PackageName> {
1079            std::iter::once(self.root_name)
1080        }
1081
1082        fn project_name(&self) -> Option<&PackageName> {
1083            None
1084        }
1085
1086        fn package_to_node(
1087            &self,
1088            _package: &Package,
1089            _tags: &Tags,
1090            _build_options: &BuildOptions,
1091            _install_options: &InstallOptions,
1092            _marker_env: &ResolverMarkerEnvironment,
1093        ) -> Result<Node, LockError> {
1094            self.package_to_node_calls
1095                .set(self.package_to_node_calls.get() + 1);
1096            Ok(Node::Root)
1097        }
1098    }
1099
1100    fn graph_snapshot(resolution: &Resolution) -> (Vec<String>, Vec<String>) {
1101        let graph = resolution.graph();
1102        let labels = graph
1103            .node_weights()
1104            .map(|node| match node {
1105                Node::Root => "root".to_string(),
1106                Node::Dist {
1107                    dist,
1108                    hashes,
1109                    install,
1110                } => format!(
1111                    "{}=={} (install: {install}, hashes: {})",
1112                    dist.name(),
1113                    dist.version()
1114                        .map(ToString::to_string)
1115                        .unwrap_or_else(|| "<dynamic>".to_string()),
1116                    hashes.iter().map(ToString::to_string).join(", ")
1117                ),
1118            })
1119            .collect::<Vec<_>>();
1120        let mut nodes = labels.clone();
1121        nodes.sort_unstable();
1122        let mut edges = graph
1123            .edge_references()
1124            .map(|edge| {
1125                format!(
1126                    "{} --{:?}--> {}",
1127                    labels[edge.source().index()],
1128                    edge.weight(),
1129                    labels[edge.target().index()]
1130                )
1131            })
1132            .collect::<Vec<_>>();
1133        edges.sort_unstable();
1134        (nodes, edges)
1135    }
1136
1137    #[test]
1138    fn materializes_multiple_concrete_roots_with_shared_dependencies() {
1139        let lock = lock();
1140        let resolution = materialize(
1141            &lock,
1142            &[
1143                package(&lock, "root-a", "1.0.0"),
1144                package(&lock, "root-b", "1.0.0"),
1145            ],
1146            &DARWIN_MARKERS,
1147        );
1148
1149        insta::with_settings!({
1150            filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1151        }, {
1152            insta::assert_debug_snapshot!(graph_snapshot(&resolution), @r#"
1153        (
1154            [
1155                "dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1156                "forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1157                "optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1158                "root",
1159                "root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1160                "root-b==1.0.0 (install: true, hashes: sha256:[HASH])",
1161                "shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1162            ],
1163            [
1164                "root --Prod--> root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1165                "root --Prod--> root-b==1.0.0 (install: true, hashes: sha256:[HASH])",
1166                "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Dev(GroupName(\"dev\"))--> dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1167                "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"feature\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1168                "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1169                "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1170                "root-b==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1171            ],
1172        )
1173            "#);
1174        });
1175    }
1176
1177    #[test]
1178    fn materializes_the_selected_universal_lock_fork() {
1179        let lock = lock();
1180        let root = package(&lock, "root-a", "1.0.0");
1181        let darwin = materialize(&lock, &[root], &DARWIN_MARKERS);
1182        let linux = materialize(&lock, &[root], &LINUX_MARKERS);
1183        let concrete_fork =
1184            materialize(&lock, &[package(&lock, "forked", "1.0.0")], &DARWIN_MARKERS);
1185
1186        insta::with_settings!({
1187            filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1188        }, {
1189            insta::assert_debug_snapshot!(graph_snapshot(&darwin), @r#"
1190        (
1191            [
1192                "dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1193                "forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1194                "optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1195                "root",
1196                "root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1197                "shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1198            ],
1199            [
1200                "root --Prod--> root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1201                "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Dev(GroupName(\"dev\"))--> dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1202                "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"feature\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1203                "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1204                "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1205            ],
1206        )
1207        "#);
1208            insta::assert_debug_snapshot!(graph_snapshot(&linux), @r#"
1209        (
1210            [
1211                "dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1212                "forked==2.0.0 (install: true, hashes: sha256:[HASH])",
1213                "optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1214                "root",
1215                "root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1216                "shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1217            ],
1218            [
1219                "root --Prod--> root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1220                "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Dev(GroupName(\"dev\"))--> dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1221                "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"feature\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1222                "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> forked==2.0.0 (install: true, hashes: sha256:[HASH])",
1223                "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1224            ],
1225        )
1226        "#);
1227            insta::assert_debug_snapshot!(graph_snapshot(&concrete_fork), @r#"
1228        (
1229            [
1230                "forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1231                "root",
1232            ],
1233            [
1234                "root --Prod--> forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1235            ],
1236        )
1237            "#);
1238        });
1239    }
1240
1241    #[test]
1242    fn materializes_conflicting_extras_within_the_synthetic_root() {
1243        let lock = conflict_lock();
1244        let extras =
1245            ExtrasSpecification::from_extra(vec!["cpu".parse().expect("valid extra name")]);
1246        let resolution = materialize_with_extras(
1247            &lock,
1248            &[package(&lock, "tool", "1.0.0")],
1249            &DARWIN_MARKERS,
1250            &extras,
1251        )
1252        .expect("conflict markers are resolved within the subgraph");
1253
1254        insta::with_settings!({
1255            filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1256        }, {
1257            insta::assert_debug_snapshot!(graph_snapshot(&resolution), @r#"
1258        (
1259            [
1260                "cpu-backend==1.0.0 (install: true, hashes: sha256:[HASH])",
1261                "root",
1262                "runtime==1.0.0 (install: true, hashes: sha256:[HASH])",
1263                "tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1264            ],
1265            [
1266                "root --Prod--> tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1267                "runtime==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> cpu-backend==1.0.0 (install: true, hashes: sha256:[HASH])",
1268                "tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> runtime==1.0.0 (install: true, hashes: sha256:[HASH])",
1269            ],
1270        )
1271        "#);
1272        });
1273    }
1274
1275    #[test]
1276    fn rejects_conflicts_outside_the_synthetic_root() {
1277        let lock = conflict_lock();
1278        let root = package(&lock, "contextual-tool", "1.0.0");
1279        let extras = ExtrasSpecification::default();
1280
1281        let error = materialize_with_extras(&lock, &[root], &DARWIN_MARKERS, &extras)
1282            .expect_err("Darwin dependency depends on the project extra");
1283        let error = error.to_string();
1284        let error = anstream::adapter::strip_str(&error);
1285        insta::assert_snapshot!(error, @"Cannot materialize dependency `contextual-dependency==1.0.0 @ registry+https://example.com/simple` of `contextual-tool==1.0.0 @ registry+https://example.com/simple` because its conflict marker depends on a package outside the selected subgraph");
1286
1287        let linux = materialize_with_extras(&lock, &[root], &LINUX_MARKERS, &extras)
1288            .expect("the dependency is unconditional on Linux");
1289        insta::with_settings!({
1290            filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1291        }, {
1292            insta::assert_debug_snapshot!(graph_snapshot(&linux), @r#"
1293        (
1294            [
1295                "contextual-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1296                "contextual-tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1297                "root",
1298            ],
1299            [
1300                "contextual-tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> contextual-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1301                "root --Prod--> contextual-tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1302            ],
1303        )
1304        "#);
1305        });
1306    }
1307
1308    #[test]
1309    fn installable_to_resolution_preserves_node_overrides() {
1310        let mut lock = lock();
1311        lock.manifest.requirements.clear();
1312        let target = OverridingInstallable {
1313            root_name: package(&lock, "root-a", "1.0.0").name(),
1314            lock: &lock,
1315            package_to_node_calls: Cell::new(0),
1316        };
1317        let extras = ExtrasSpecification::from_all_extras().with_defaults(DefaultExtras::default());
1318        let groups = DependencyGroups::from_all_groups().with_defaults(DefaultGroups::default());
1319
1320        target
1321            .to_resolution(
1322                &DARWIN_MARKERS,
1323                &TAGS,
1324                &extras,
1325                &groups,
1326                &BuildOptions::default(),
1327                &InstallOptions::default(),
1328            )
1329            .expect("valid resolution");
1330
1331        assert!(target.package_to_node_calls.get() > 0);
1332    }
1333}