uv_installer/
plan.rs

1use std::sync::Arc;
2
3use anyhow::{Result, bail};
4use owo_colors::OwoColorize;
5use tracing::{debug, warn};
6
7use uv_cache::{Cache, CacheBucket, WheelCache};
8use uv_cache_info::Timestamp;
9use uv_configuration::{BuildOptions, Reinstall};
10use uv_distribution::{
11    BuiltWheelIndex, HttpArchivePointer, LocalArchivePointer, RegistryWheelIndex,
12};
13use uv_distribution_filename::WheelFilename;
14use uv_distribution_types::{
15    BuiltDist, CachedDirectUrlDist, CachedDist, ConfigSettings, Dist, Error, ExtraBuildRequires,
16    ExtraBuildVariables, Hashed, IndexLocations, InstalledDist, Name, PackageConfigSettings,
17    RequirementSource, Resolution, ResolvedDist, SourceDist,
18};
19use uv_fs::Simplified;
20use uv_normalize::PackageName;
21use uv_platform_tags::{IncompatibleTag, TagCompatibility, Tags};
22use uv_pypi_types::VerbatimParsedUrl;
23use uv_python::PythonEnvironment;
24use uv_types::HashStrategy;
25
26use crate::satisfies::RequirementSatisfaction;
27use crate::{InstallationStrategy, SitePackages};
28
29/// A planner to generate an [`Plan`] based on a set of requirements.
30#[derive(Debug)]
31pub struct Planner<'a> {
32    resolution: &'a Resolution,
33}
34
35impl<'a> Planner<'a> {
36    /// Set the requirements use in the [`Plan`].
37    pub fn new(resolution: &'a Resolution) -> Self {
38        Self { resolution }
39    }
40
41    /// Partition a set of requirements into those that should be linked from the cache, those that
42    /// need to be downloaded, and those that should be removed.
43    ///
44    /// The install plan will respect cache [`Freshness`]. Specifically, if refresh is enabled, the
45    /// plan will respect cache entries created after the current time (as per the [`Refresh`]
46    /// policy). Otherwise, entries will be ignored. The downstream distribution database may still
47    /// read those entries from the cache after revalidating them.
48    ///
49    /// The install plan will also respect the required hashes, such that it will never return a
50    /// cached distribution that does not match the required hash. Like pip, though, it _will_
51    /// return an _installed_ distribution that does not match the required hash.
52    pub fn build(
53        self,
54        mut site_packages: SitePackages,
55        installation: InstallationStrategy,
56        reinstall: &Reinstall,
57        build_options: &BuildOptions,
58        hasher: &HashStrategy,
59        index_locations: &IndexLocations,
60        config_settings: &ConfigSettings,
61        config_settings_package: &PackageConfigSettings,
62        extra_build_requires: &ExtraBuildRequires,
63        extra_build_variables: &ExtraBuildVariables,
64        cache: &Cache,
65        venv: &PythonEnvironment,
66        tags: &Tags,
67    ) -> Result<Plan> {
68        // Index all the already-downloaded wheels in the cache.
69        let mut registry_index = RegistryWheelIndex::new(
70            cache,
71            tags,
72            index_locations,
73            hasher,
74            config_settings,
75            config_settings_package,
76            extra_build_requires,
77            extra_build_variables,
78        );
79        let built_index = BuiltWheelIndex::new(
80            cache,
81            tags,
82            hasher,
83            config_settings,
84            config_settings_package,
85            extra_build_requires,
86            extra_build_variables,
87        );
88
89        let mut cached = vec![];
90        let mut remote = vec![];
91        let mut reinstalls = vec![];
92        let mut extraneous = vec![];
93
94        // TODO(charlie): There are a few assumptions here that are hard to spot:
95        //
96        // 1. Apparently, we never return direct URL distributions as [`ResolvedDist::Installed`].
97        //    If you trace the resolver, we only ever return [`ResolvedDist::Installed`] if you go
98        //    through the [`CandidateSelector`], and we only go through the [`CandidateSelector`]
99        //    for registry distributions.
100        //
101        // 2. We expect any distribution returned as [`ResolvedDist::Installed`] to hit the
102        //    "Requirement already installed" path (hence the `unreachable!`) a few lines below it.
103        //    So, e.g., if a package is marked as `--reinstall`, we _expect_ that it's not passed in
104        //    as [`ResolvedDist::Installed`] here.
105        for dist in self.resolution.distributions() {
106            // Check if the package should be reinstalled.
107            let reinstall = reinstall.contains_package(dist.name())
108                || dist
109                    .source_tree()
110                    .is_some_and(|source_tree| reinstall.contains_path(source_tree));
111
112            // Check if installation of a binary version of the package should be allowed.
113            let no_binary = build_options.no_binary_package(dist.name());
114            let no_build = build_options.no_build_package(dist.name());
115
116            // Determine whether the distribution is already installed.
117            let installed_dists = site_packages.remove_packages(dist.name());
118            if reinstall {
119                reinstalls.extend(installed_dists);
120            } else {
121                match installed_dists.as_slice() {
122                    [] => {}
123                    [installed] => {
124                        let source = RequirementSource::from(dist);
125                        match RequirementSatisfaction::check(
126                            dist.name(),
127                            installed,
128                            &source,
129                            installation,
130                            tags,
131                            config_settings,
132                            config_settings_package,
133                            extra_build_requires,
134                            extra_build_variables,
135                        ) {
136                            RequirementSatisfaction::Mismatch => {
137                                debug!(
138                                    "Requirement installed, but mismatched:\n  Installed: {installed:?}\n  Requested: {source:?}"
139                                );
140                            }
141                            RequirementSatisfaction::Satisfied => {
142                                debug!("Requirement already installed: {installed}");
143                                continue;
144                            }
145                            RequirementSatisfaction::OutOfDate => {
146                                debug!("Requirement installed, but not fresh: {installed}");
147
148                                // If we made it here, something went wrong in the resolver, because it returned an
149                                // already-installed distribution that we "shouldn't" use. Typically, this means the
150                                // distribution was considered out-of-date, but in a way that the resolver didn't
151                                // detect, and is indicative of drift between the resolver's candidate selector and
152                                // the install plan. For example, at present, the resolver doesn't check that an
153                                // installed distribution was built with the expected build settings. Treat it as
154                                // up-to-date for now; it's just means we may not rebuild a package when we otherwise
155                                // should. This is a known issue, but should only affect the `uv pip` CLI, as the
156                                // project APIs never return installed distributions during resolution (i.e., the
157                                // resolver is stateless).
158                                // TODO(charlie): Incorporate these checks into the resolver.
159                                if matches!(dist, ResolvedDist::Installed { .. }) {
160                                    warn!(
161                                        "Installed distribution was considered out-of-date, but returned by the resolver: {dist}"
162                                    );
163                                    continue;
164                                }
165                            }
166                            RequirementSatisfaction::CacheInvalid => {
167                                // Already logged
168                            }
169                        }
170                        reinstalls.push(installed.clone());
171                    }
172                    // We reinstall installed distributions with multiple versions because
173                    // we do not want to keep multiple incompatible versions but removing
174                    // one version is likely to break another.
175                    _ => reinstalls.extend(installed_dists),
176                }
177            }
178
179            let ResolvedDist::Installable { dist, .. } = dist else {
180                unreachable!("Installed distribution could not be found in site-packages: {dist}");
181            };
182
183            if cache.must_revalidate_package(dist.name())
184                || dist
185                    .source_tree()
186                    .is_some_and(|source_tree| cache.must_revalidate_path(source_tree))
187            {
188                debug!("Must revalidate requirement: {}", dist.name());
189                remote.push(dist.clone());
190                continue;
191            }
192
193            // Identify any cached distributions that satisfy the requirement.
194            match dist.as_ref() {
195                Dist::Built(BuiltDist::Registry(wheel)) => {
196                    if let Some(distribution) = registry_index.get(wheel.name()).find_map(|entry| {
197                        if *entry.index.url() != wheel.best_wheel().index {
198                            return None;
199                        }
200                        if entry.dist.filename != wheel.best_wheel().filename {
201                            return None;
202                        }
203                        if entry.built && no_build {
204                            return None;
205                        }
206                        if !entry.built && no_binary {
207                            return None;
208                        }
209                        Some(&entry.dist)
210                    }) {
211                        debug!("Registry requirement already cached: {distribution}");
212                        cached.push(CachedDist::Registry(distribution.clone()));
213                        continue;
214                    }
215                }
216                Dist::Built(BuiltDist::DirectUrl(wheel)) => {
217                    if !wheel.filename.is_compatible(tags) {
218                        let hint = generate_wheel_compatibility_hint(&wheel.filename, tags);
219                        if let Some(hint) = hint {
220                            bail!(
221                                "A URL dependency is incompatible with the current platform: {}\n\n{}{} {}",
222                                wheel.url,
223                                "hint".bold().cyan(),
224                                ":".bold(),
225                                hint
226                            );
227                        }
228                        bail!(
229                            "A URL dependency is incompatible with the current platform: {}",
230                            wheel.url
231                        );
232                    }
233
234                    if no_binary {
235                        bail!(
236                            "A URL dependency points to a wheel which conflicts with `--no-binary`: {}",
237                            wheel.url
238                        );
239                    }
240
241                    // Find the exact wheel from the cache, since we know the filename in
242                    // advance.
243                    let cache_entry = cache
244                        .shard(
245                            CacheBucket::Wheels,
246                            WheelCache::Url(&wheel.url).wheel_dir(wheel.name().as_ref()),
247                        )
248                        .entry(format!("{}.http", wheel.filename.cache_key()));
249
250                    // Read the HTTP pointer.
251                    match HttpArchivePointer::read_from(&cache_entry) {
252                        Ok(Some(pointer)) => {
253                            let cache_info = pointer.to_cache_info();
254                            let build_info = pointer.to_build_info();
255                            let archive = pointer.into_archive();
256                            if archive.satisfies(hasher.get(dist.as_ref())) {
257                                let cached_dist = CachedDirectUrlDist {
258                                    filename: wheel.filename.clone(),
259                                    url: VerbatimParsedUrl {
260                                        parsed_url: wheel.parsed_url(),
261                                        verbatim: wheel.url.clone(),
262                                    },
263                                    hashes: archive.hashes,
264                                    cache_info,
265                                    build_info,
266                                    path: cache.archive(&archive.id).into_boxed_path(),
267                                };
268
269                                debug!("URL wheel requirement already cached: {cached_dist}");
270                                cached.push(CachedDist::Url(cached_dist));
271                                continue;
272                            }
273                            debug!(
274                                "Cached URL wheel requirement does not match expected hash policy for: {wheel}"
275                            );
276                        }
277                        Ok(None) => {}
278                        Err(err) => {
279                            debug!(
280                                "Failed to deserialize cached URL wheel requirement for: {wheel} ({err})"
281                            );
282                        }
283                    }
284                }
285                Dist::Built(BuiltDist::Path(wheel)) => {
286                    // Validate that the path exists.
287                    if !wheel.install_path.exists() {
288                        return Err(Error::NotFound(wheel.url.to_url()).into());
289                    }
290
291                    if !wheel.filename.is_compatible(tags) {
292                        let hint = generate_wheel_compatibility_hint(&wheel.filename, tags);
293                        if let Some(hint) = hint {
294                            bail!(
295                                "A path dependency is incompatible with the current platform: {}\n\n{}{} {}",
296                                wheel.install_path.user_display(),
297                                "hint".bold().cyan(),
298                                ":".bold(),
299                                hint
300                            );
301                        }
302                        bail!(
303                            "A path dependency is incompatible with the current platform: {}",
304                            wheel.install_path.user_display()
305                        );
306                    }
307
308                    if no_binary {
309                        bail!(
310                            "A path dependency points to a wheel which conflicts with `--no-binary`: {}",
311                            wheel.url
312                        );
313                    }
314
315                    // Find the exact wheel from the cache, since we know the filename in
316                    // advance.
317                    let cache_entry = cache
318                        .shard(
319                            CacheBucket::Wheels,
320                            WheelCache::Url(&wheel.url).wheel_dir(wheel.name().as_ref()),
321                        )
322                        .entry(format!("{}.rev", wheel.filename.cache_key()));
323
324                    match LocalArchivePointer::read_from(&cache_entry) {
325                        Ok(Some(pointer)) => match Timestamp::from_path(&wheel.install_path) {
326                            Ok(timestamp) => {
327                                if pointer.is_up_to_date(timestamp) {
328                                    let cache_info = pointer.to_cache_info();
329                                    let build_info = pointer.to_build_info();
330                                    let archive = pointer.into_archive();
331                                    if archive.satisfies(hasher.get(dist.as_ref())) {
332                                        let cached_dist = CachedDirectUrlDist {
333                                            filename: wheel.filename.clone(),
334                                            url: VerbatimParsedUrl {
335                                                parsed_url: wheel.parsed_url(),
336                                                verbatim: wheel.url.clone(),
337                                            },
338                                            hashes: archive.hashes,
339                                            cache_info,
340                                            build_info,
341                                            path: cache.archive(&archive.id).into_boxed_path(),
342                                        };
343
344                                        debug!(
345                                            "Path wheel requirement already cached: {cached_dist}"
346                                        );
347                                        cached.push(CachedDist::Url(cached_dist));
348                                        continue;
349                                    }
350                                    debug!(
351                                        "Cached path wheel requirement does not match expected hash policy for: {wheel}"
352                                    );
353                                }
354                            }
355                            Err(err) => {
356                                debug!("Failed to get timestamp for wheel {wheel} ({err})");
357                            }
358                        },
359                        Ok(None) => {}
360                        Err(err) => {
361                            debug!(
362                                "Failed to deserialize cached path wheel requirement for: {wheel} ({err})"
363                            );
364                        }
365                    }
366                }
367                Dist::Source(SourceDist::Registry(sdist)) => {
368                    if let Some(distribution) = registry_index.get(sdist.name()).find_map(|entry| {
369                        if *entry.index.url() != sdist.index {
370                            return None;
371                        }
372                        if entry.dist.filename.name != sdist.name {
373                            return None;
374                        }
375                        if entry.dist.filename.version != sdist.version {
376                            return None;
377                        }
378                        if entry.built && no_build {
379                            return None;
380                        }
381                        if !entry.built && no_binary {
382                            return None;
383                        }
384                        Some(&entry.dist)
385                    }) {
386                        debug!("Registry requirement already cached: {distribution}");
387                        cached.push(CachedDist::Registry(distribution.clone()));
388                        continue;
389                    }
390                }
391                Dist::Source(SourceDist::DirectUrl(sdist)) => {
392                    // Find the most-compatible wheel from the cache, since we don't know
393                    // the filename in advance.
394                    match built_index.url(sdist) {
395                        Ok(Some(wheel)) => {
396                            if wheel.filename.name == sdist.name {
397                                let cached_dist = wheel.into_url_dist(sdist);
398                                debug!("URL source requirement already cached: {cached_dist}");
399                                cached.push(CachedDist::Url(cached_dist));
400                                continue;
401                            }
402
403                            warn!(
404                                "Cached wheel filename does not match requested distribution for: `{}` (found: `{}`)",
405                                sdist, wheel.filename
406                            );
407                        }
408                        Ok(None) => {}
409                        Err(err) => {
410                            debug!(
411                                "Failed to deserialize cached wheel filename for: {sdist} ({err})"
412                            );
413                        }
414                    }
415                }
416                Dist::Source(SourceDist::Git(sdist)) => {
417                    // Find the most-compatible wheel from the cache, since we don't know
418                    // the filename in advance.
419                    if let Some(wheel) = built_index.git(sdist) {
420                        if wheel.filename.name == sdist.name {
421                            let cached_dist = wheel.into_git_dist(sdist);
422                            debug!("Git source requirement already cached: {cached_dist}");
423                            cached.push(CachedDist::Url(cached_dist));
424                            continue;
425                        }
426
427                        warn!(
428                            "Cached wheel filename does not match requested distribution for: `{}` (found: `{}`)",
429                            sdist, wheel.filename
430                        );
431                    }
432                }
433                Dist::Source(SourceDist::Path(sdist)) => {
434                    // Validate that the path exists.
435                    if !sdist.install_path.exists() {
436                        return Err(Error::NotFound(sdist.url.to_url()).into());
437                    }
438
439                    // Find the most-compatible wheel from the cache, since we don't know
440                    // the filename in advance.
441                    match built_index.path(sdist) {
442                        Ok(Some(wheel)) => {
443                            if wheel.filename.name == sdist.name {
444                                let cached_dist = wheel.into_path_dist(sdist);
445                                debug!("Path source requirement already cached: {cached_dist}");
446                                cached.push(CachedDist::Url(cached_dist));
447                                continue;
448                            }
449
450                            warn!(
451                                "Cached wheel filename does not match requested distribution for: `{}` (found: `{}`)",
452                                sdist, wheel.filename
453                            );
454                        }
455                        Ok(None) => {}
456                        Err(err) => {
457                            debug!(
458                                "Failed to deserialize cached wheel filename for: {sdist} ({err})"
459                            );
460                        }
461                    }
462                }
463                Dist::Source(SourceDist::Directory(sdist)) => {
464                    // Validate that the path exists.
465                    if !sdist.install_path.exists() {
466                        return Err(Error::NotFound(sdist.url.to_url()).into());
467                    }
468
469                    // Find the most-compatible wheel from the cache, since we don't know
470                    // the filename in advance.
471                    match built_index.directory(sdist) {
472                        Ok(Some(wheel)) => {
473                            if wheel.filename.name == sdist.name {
474                                let cached_dist = wheel.into_directory_dist(sdist);
475                                debug!(
476                                    "Directory source requirement already cached: {cached_dist}"
477                                );
478                                cached.push(CachedDist::Url(cached_dist));
479                                continue;
480                            }
481
482                            warn!(
483                                "Cached wheel filename does not match requested distribution for: `{}` (found: `{}`)",
484                                sdist, wheel.filename
485                            );
486                        }
487                        Ok(None) => {}
488                        Err(err) => {
489                            debug!(
490                                "Failed to deserialize cached wheel filename for: {sdist} ({err})"
491                            );
492                        }
493                    }
494                }
495            }
496
497            debug!("Identified uncached distribution: {dist}");
498            remote.push(dist.clone());
499        }
500
501        // Remove any unnecessary packages.
502        if site_packages.any() {
503            // Retain seed packages unless: (1) the virtual environment was created by uv and
504            // (2) the `--seed` argument was not passed to `uv venv`.
505            let seed_packages = !venv.cfg().is_ok_and(|cfg| cfg.is_uv() && !cfg.is_seed());
506            for dist_info in site_packages {
507                if seed_packages && is_seed_package(&dist_info, venv) {
508                    debug!("Preserving seed package: {dist_info}");
509                    continue;
510                }
511
512                debug!("Unnecessary package: {dist_info}");
513                extraneous.push(dist_info);
514            }
515        }
516
517        Ok(Plan {
518            cached,
519            remote,
520            reinstalls,
521            extraneous,
522        })
523    }
524}
525
526/// Returns `true` if the given distribution is a seed package.
527fn is_seed_package(dist_info: &InstalledDist, venv: &PythonEnvironment) -> bool {
528    if venv.interpreter().python_tuple() >= (3, 12) {
529        matches!(dist_info.name().as_ref(), "uv" | "pip")
530    } else {
531        // Include `setuptools` and `wheel` on Python <3.12.
532        matches!(
533            dist_info.name().as_ref(),
534            "pip" | "setuptools" | "wheel" | "uv"
535        )
536    }
537}
538
539/// Generate a hint for explaining wheel compatibility issues.
540fn generate_wheel_compatibility_hint(filename: &WheelFilename, tags: &Tags) -> Option<String> {
541    let TagCompatibility::Incompatible(incompatible_tag) = filename.compatibility(tags) else {
542        return None;
543    };
544
545    match incompatible_tag {
546        IncompatibleTag::Python => {
547            let wheel_tags = filename.python_tags();
548            let current_tag = tags.python_tag();
549
550            if let Some(current) = current_tag {
551                let message = if let Some(pretty) = current.pretty() {
552                    format!("{} (`{}`)", pretty.cyan(), current.cyan())
553                } else {
554                    format!("`{}`", current.cyan())
555                };
556
557                Some(format!(
558                    "The wheel is compatible with {}, but you're using {}",
559                    wheel_tags
560                        .iter()
561                        .map(|tag| if let Some(pretty) = tag.pretty() {
562                            format!("{} (`{}`)", pretty.cyan(), tag.cyan())
563                        } else {
564                            format!("`{}`", tag.cyan())
565                        })
566                        .collect::<Vec<_>>()
567                        .join(", "),
568                    message
569                ))
570            } else {
571                Some(format!(
572                    "The wheel requires {}",
573                    wheel_tags
574                        .iter()
575                        .map(|tag| if let Some(pretty) = tag.pretty() {
576                            format!("{} (`{}`)", pretty.cyan(), tag.cyan())
577                        } else {
578                            format!("`{}`", tag.cyan())
579                        })
580                        .collect::<Vec<_>>()
581                        .join(", ")
582                ))
583            }
584        }
585        IncompatibleTag::Abi => {
586            let wheel_tags = filename.abi_tags();
587            let current_tag = tags.abi_tag();
588
589            if let Some(current) = current_tag {
590                let message = if let Some(pretty) = current.pretty() {
591                    format!("{} (`{}`)", pretty.cyan(), current.cyan())
592                } else {
593                    format!("`{}`", current.cyan())
594                };
595                Some(format!(
596                    "The wheel is compatible with {}, but you're using {}",
597                    wheel_tags
598                        .iter()
599                        .map(|tag| if let Some(pretty) = tag.pretty() {
600                            format!("{} (`{}`)", pretty.cyan(), tag.cyan())
601                        } else {
602                            format!("`{}`", tag.cyan())
603                        })
604                        .collect::<Vec<_>>()
605                        .join(", "),
606                    message
607                ))
608            } else {
609                Some(format!(
610                    "The wheel requires {}",
611                    wheel_tags
612                        .iter()
613                        .map(|tag| if let Some(pretty) = tag.pretty() {
614                            format!("{} (`{}`)", pretty.cyan(), tag.cyan())
615                        } else {
616                            format!("`{}`", tag.cyan())
617                        })
618                        .collect::<Vec<_>>()
619                        .join(", ")
620                ))
621            }
622        }
623        IncompatibleTag::Platform => {
624            let wheel_tags = filename.platform_tags();
625            let current_tag = tags.platform_tag();
626
627            if let Some(current) = current_tag {
628                let message = if let Some(pretty) = current.pretty() {
629                    format!("{} (`{}`)", pretty.cyan(), current.cyan())
630                } else {
631                    format!("`{}`", current.cyan())
632                };
633                Some(format!(
634                    "The wheel is compatible with {}, but you're on {}",
635                    wheel_tags
636                        .iter()
637                        .map(|tag| if let Some(pretty) = tag.pretty() {
638                            format!("{} (`{}`)", pretty.cyan(), tag.cyan())
639                        } else {
640                            format!("`{}`", tag.cyan())
641                        })
642                        .collect::<Vec<_>>()
643                        .join(", "),
644                    message
645                ))
646            } else {
647                Some(format!(
648                    "The wheel requires {}",
649                    wheel_tags
650                        .iter()
651                        .map(|tag| if let Some(pretty) = tag.pretty() {
652                            format!("{} (`{}`)", pretty.cyan(), tag.cyan())
653                        } else {
654                            format!("`{}`", tag.cyan())
655                        })
656                        .collect::<Vec<_>>()
657                        .join(", ")
658                ))
659            }
660        }
661        _ => None,
662    }
663}
664
665#[derive(Debug, Default)]
666pub struct Plan {
667    /// The distributions that are not already installed in the current environment, but are
668    /// available in the local cache.
669    pub cached: Vec<CachedDist>,
670
671    /// The distributions that are not already installed in the current environment, and are
672    /// not available in the local cache.
673    pub remote: Vec<Arc<Dist>>,
674
675    /// Any distributions that are already installed in the current environment, but will be
676    /// re-installed (including upgraded) to satisfy the requirements.
677    pub reinstalls: Vec<InstalledDist>,
678
679    /// Any distributions that are already installed in the current environment, and are
680    /// _not_ necessary to satisfy the requirements.
681    pub extraneous: Vec<InstalledDist>,
682}
683
684impl Plan {
685    /// Returns `true` if the plan is empty.
686    pub fn is_empty(&self) -> bool {
687        self.cached.is_empty()
688            && self.remote.is_empty()
689            && self.reinstalls.is_empty()
690            && self.extraneous.is_empty()
691    }
692
693    /// Partition the remote distributions based on a predicate function.
694    ///
695    /// Returns a tuple of plans, where the first plan contains the remote distributions that match
696    /// the predicate, and the second plan contains those that do not.
697    ///
698    /// Any extraneous and cached distributions will be returned in the first plan, while the second
699    /// plan will contain any `false` matches from the remote distributions, along with any
700    /// reinstalls for those distributions.
701    pub fn partition<F>(self, mut f: F) -> (Self, Self)
702    where
703        F: FnMut(&PackageName) -> bool,
704    {
705        let Self {
706            cached,
707            remote,
708            reinstalls,
709            extraneous,
710        } = self;
711
712        // Partition the remote distributions based on the predicate function.
713        let (left_remote, right_remote) = remote
714            .into_iter()
715            .partition::<Vec<_>, _>(|dist| f(dist.name()));
716
717        // If any remote distributions are not matched, but are already installed, ensure that
718        // they're uninstalled as part of the right plan. (Uninstalling them as part of the left
719        // plan risks uninstalling them from the environment _prior_ to the replacement being built.)
720        let (left_reinstalls, right_reinstalls) = reinstalls
721            .into_iter()
722            .partition::<Vec<_>, _>(|dist| !right_remote.iter().any(|d| d.name() == dist.name()));
723
724        // If the right plan is non-empty, then remove extraneous distributions as part of the
725        // right plan, so they're present until the very end. Otherwise, we risk removing extraneous
726        // packages that are actually build dependencies.
727        let (left_extraneous, right_extraneous) = if right_remote.is_empty() {
728            (extraneous, vec![])
729        } else {
730            (vec![], extraneous)
731        };
732
733        // Always include the cached distributions in the left plan.
734        let (left_cached, right_cached) = (cached, vec![]);
735
736        // Include all cached and extraneous distributions in the left plan.
737        let left_plan = Self {
738            cached: left_cached,
739            remote: left_remote,
740            reinstalls: left_reinstalls,
741            extraneous: left_extraneous,
742        };
743
744        // The right plan will only contain the remote distributions that did not match the predicate,
745        // along with any reinstalls for those distributions.
746        let right_plan = Self {
747            cached: right_cached,
748            remote: right_remote,
749            reinstalls: right_reinstalls,
750            extraneous: right_extraneous,
751        };
752
753        (left_plan, right_plan)
754    }
755}