Skip to main content

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