Skip to main content

uv_installer/
site_packages.rs

1use std::borrow::Cow;
2use std::iter::Flatten;
3use std::path::{Path, PathBuf};
4use std::str::FromStr;
5
6use anyhow::{Context, Result};
7use fs_err as fs;
8use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
9
10use uv_configuration::{ExcludeDependency, Excludes, Override, Overrides};
11use uv_distribution_filename::EggInfoFilename;
12use uv_distribution_types::{
13    ConfigSettings, DependencyMetadata, Diagnostic, ExtraBuildRequires, ExtraBuildVariables,
14    InstalledDist, InstalledDistKind, Name, NameRequirementSpecification, PackageConfigSettings,
15    Requirement, UnresolvedRequirement, UnresolvedRequirementSpecification,
16};
17use uv_fs::Simplified;
18use uv_normalize::PackageName;
19use uv_pep440::{Version, VersionSpecifiers};
20use uv_pep508::VersionOrUrl;
21use uv_platform_tags::Tags;
22use uv_pypi_types::{ResolverMarkerEnvironment, VerbatimParsedUrl};
23use uv_python::{Interpreter, PythonEnvironment};
24use uv_redacted::DisplaySafeUrl;
25use uv_types::InstalledPackagesProvider;
26use uv_warnings::warn_user;
27
28use crate::satisfies::RequirementSatisfaction;
29
30/// An index over the packages installed in an environment.
31///
32/// Packages are indexed by both name and (for editable installs) URL.
33#[derive(Debug, Clone)]
34pub struct SitePackages {
35    interpreter: Interpreter,
36    /// The vector of all installed distributions. The `by_name` and `by_url` indices index into
37    /// this vector. The vector may contain `None` values, which represent distributions that were
38    /// removed from the virtual environment.
39    distributions: Vec<Option<InstalledDist>>,
40    /// The installed distributions, keyed by name. Although the Python runtime does not support it,
41    /// it is possible to have multiple distributions with the same name to be present in the
42    /// virtual environment, which we handle gracefully.
43    by_name: FxHashMap<PackageName, Vec<usize>>,
44    /// The installed editable distributions, keyed by URL.
45    by_url: FxHashMap<DisplaySafeUrl, Vec<usize>>,
46}
47
48impl SitePackages {
49    /// Build an index of installed packages from the given Python environment.
50    pub fn from_environment(environment: &PythonEnvironment) -> Result<Self> {
51        Self::from_interpreter(environment.interpreter())
52    }
53
54    /// Build an index of the requested installed packages from the given Python environment.
55    pub fn from_environment_for_packages<'a>(
56        environment: &PythonEnvironment,
57        package_names: impl IntoIterator<Item = &'a PackageName>,
58    ) -> Result<Self> {
59        let package_names = package_names.into_iter().collect::<FxHashSet<_>>();
60        Self::from_interpreter_with_filter(environment.interpreter(), Some(&package_names))
61    }
62
63    /// Build an index of installed packages from the given Python executable.
64    pub fn from_interpreter(interpreter: &Interpreter) -> Result<Self> {
65        Self::from_interpreter_with_filter(interpreter, None)
66    }
67
68    /// Build an index of installed packages from the given Python executable.
69    fn from_interpreter_with_filter(
70        interpreter: &Interpreter,
71        package_names: Option<&FxHashSet<&PackageName>>,
72    ) -> Result<Self> {
73        let mut distributions: Vec<Option<InstalledDist>> = Vec::new();
74        let mut by_name: FxHashMap<PackageName, Vec<usize>> = FxHashMap::default();
75        let mut by_url: FxHashMap<DisplaySafeUrl, Vec<usize>> = FxHashMap::default();
76
77        for site_packages in interpreter.site_packages() {
78            // Read the site-packages directory.
79            let site_packages = match fs::read_dir(site_packages.as_ref()) {
80                Ok(read_dir) => sorted_dist_like_paths(read_dir).with_context(|| {
81                    format!(
82                        "Failed to read site-packages directory contents: {}",
83                        site_packages.user_display()
84                    )
85                })?,
86                Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
87                    continue;
88                }
89                Err(err) => return Err(err).context("Failed to read site-packages directory"),
90            };
91
92            // Index all installed packages by name.
93            for path in site_packages {
94                if let Some(package_names) = package_names
95                    && let Some(package_name) = installed_dist_name(&path)
96                    && !package_names.contains(&package_name)
97                {
98                    continue;
99                }
100
101                let dist_info = match InstalledDist::try_from_path(&path) {
102                    Ok(Some(dist_info)) => dist_info,
103                    Ok(None) => continue,
104                    Err(_)
105                        if path.file_name().is_some_and(|name| {
106                            name.to_str().is_some_and(|name| name.starts_with('~'))
107                        }) =>
108                    {
109                        warn_user!(
110                            "Ignoring dangling temporary directory: `{}`",
111                            path.simplified_display().cyan()
112                        );
113                        continue;
114                    }
115                    Err(err) => {
116                        return Err(err).context(format!(
117                            "Failed to read metadata from: `{}`",
118                            path.simplified_display()
119                        ));
120                    }
121                };
122
123                if let Some(package_names) = package_names
124                    && !package_names.contains(dist_info.name())
125                {
126                    continue;
127                }
128
129                let idx = distributions.len();
130
131                // Index the distribution by name.
132                by_name
133                    .entry(dist_info.name().clone())
134                    .or_default()
135                    .push(idx);
136
137                // Index the distribution by URL.
138                if let InstalledDistKind::Url(dist) = &dist_info.kind {
139                    by_url.entry(dist.url.clone()).or_default().push(idx);
140                }
141
142                // Add the distribution to the database.
143                distributions.push(Some(dist_info));
144            }
145        }
146
147        Ok(Self {
148            interpreter: interpreter.clone(),
149            distributions,
150            by_name,
151            by_url,
152        })
153    }
154
155    /// Returns the [`Interpreter`] used to install the packages.
156    pub fn interpreter(&self) -> &Interpreter {
157        &self.interpreter
158    }
159
160    /// Returns an iterator over the installed distributions.
161    pub fn iter(&self) -> impl Iterator<Item = &InstalledDist> {
162        self.distributions.iter().flatten()
163    }
164
165    /// Returns the installed distributions for a given package.
166    pub fn get_packages(&self, name: &PackageName) -> Vec<&InstalledDist> {
167        let Some(indexes) = self.by_name.get(name) else {
168            return Vec::new();
169        };
170        indexes
171            .iter()
172            .flat_map(|&index| &self.distributions[index])
173            .collect()
174    }
175
176    /// Remove the given packages from the index, returning all installed versions, if any.
177    pub(crate) fn remove_packages(&mut self, name: &PackageName) -> Vec<InstalledDist> {
178        let Some(indexes) = self.by_name.get(name) else {
179            return Vec::new();
180        };
181        indexes
182            .iter()
183            .filter_map(|index| std::mem::take(&mut self.distributions[*index]))
184            .collect()
185    }
186
187    /// Returns the distributions installed from the given URL, if any.
188    pub fn get_urls(&self, url: &DisplaySafeUrl) -> Vec<&InstalledDist> {
189        let Some(indexes) = self.by_url.get(url) else {
190            return Vec::new();
191        };
192        indexes
193            .iter()
194            .flat_map(|&index| &self.distributions[index])
195            .collect()
196    }
197
198    /// Returns `true` if there are any installed packages.
199    pub(crate) fn any(&self) -> bool {
200        self.distributions.iter().any(Option::is_some)
201    }
202
203    /// Validate the installed packages in the virtual environment.
204    pub fn diagnostics(
205        &self,
206        markers: &ResolverMarkerEnvironment,
207        tags: &Tags,
208        dependency_metadata: &DependencyMetadata,
209    ) -> Result<Vec<SitePackagesDiagnostic>> {
210        let mut diagnostics = Vec::new();
211
212        for (package, indexes) in &self.by_name {
213            let mut distributions = indexes.iter().flat_map(|index| &self.distributions[*index]);
214
215            // Find the installed distribution for the given package.
216            let Some(distribution) = distributions.next() else {
217                continue;
218            };
219
220            if let Some(conflict) = distributions.next() {
221                // There are multiple installed distributions for the same package.
222                diagnostics.push(SitePackagesDiagnostic::DuplicatePackage {
223                    package: package.clone(),
224                    paths: std::iter::once(distribution.install_path().to_owned())
225                        .chain(std::iter::once(conflict.install_path().to_owned()))
226                        .chain(distributions.map(|dist| dist.install_path().to_owned()))
227                        .collect(),
228                });
229                continue;
230            }
231
232            for index in indexes {
233                let Some(distribution) = &self.distributions[*index] else {
234                    continue;
235                };
236
237                // Determine the dependencies for the given package.
238                let metadata = if let Some(metadata) =
239                    dependency_metadata.get(package, Some(distribution.version()))
240                {
241                    Cow::Owned(metadata)
242                } else {
243                    let Ok(metadata) = distribution.read_metadata() else {
244                        diagnostics.push(SitePackagesDiagnostic::MetadataUnavailable {
245                            package: package.clone(),
246                            path: distribution.install_path().to_owned(),
247                        });
248                        continue;
249                    };
250                    Cow::Borrowed(metadata)
251                };
252
253                // Verify that the package is compatible with the current Python version.
254                if let Some(requires_python) = metadata.requires_python.as_ref() {
255                    if !requires_python.contains(markers.python_full_version()) {
256                        diagnostics.push(SitePackagesDiagnostic::IncompatiblePythonVersion {
257                            package: package.clone(),
258                            version: self.interpreter.python_version().clone(),
259                            requires_python: requires_python.clone(),
260                        });
261                    }
262                }
263
264                // Verify that the package is compatible with the current tags.
265                match distribution.read_tags() {
266                    Ok(Some(wheel_tags)) => {
267                        if !wheel_tags.is_compatible(tags) {
268                            // TODO(charlie): Show the expanded tag hint, that explains _why_ it doesn't match.
269                            diagnostics.push(SitePackagesDiagnostic::IncompatiblePlatform {
270                                package: package.clone(),
271                            });
272                        }
273                    }
274                    Ok(None) => {}
275                    Err(_) => {
276                        diagnostics.push(SitePackagesDiagnostic::TagsUnavailable {
277                            package: package.clone(),
278                            path: distribution.install_path().to_owned(),
279                        });
280                    }
281                }
282
283                // Verify that the dependencies are installed.
284                for dependency in &metadata.requires_dist {
285                    if !dependency.evaluate_markers(markers, &[]) {
286                        continue;
287                    }
288
289                    let installed = self.get_packages(&dependency.name);
290                    match installed.as_slice() {
291                        [] => {
292                            // No version installed.
293                            diagnostics.push(SitePackagesDiagnostic::MissingDependency {
294                                package: package.clone(),
295                                requirement: dependency.clone(),
296                            });
297                        }
298                        [installed] => {
299                            match &dependency.version_or_url {
300                                None | Some(VersionOrUrl::Url(_)) => {
301                                    // Nothing to do (accept any installed version).
302                                }
303                                Some(VersionOrUrl::VersionSpecifier(version_specifier)) => {
304                                    // The installed version doesn't satisfy the requirement.
305                                    if !version_specifier.contains(installed.version()) {
306                                        diagnostics.push(
307                                            SitePackagesDiagnostic::IncompatibleDependency {
308                                                package: package.clone(),
309                                                version: installed.version().clone(),
310                                                requirement: dependency.clone(),
311                                            },
312                                        );
313                                    }
314                                }
315                            }
316                        }
317                        _ => {
318                            // There are multiple installed distributions for the same package.
319                        }
320                    }
321                }
322            }
323        }
324
325        Ok(diagnostics)
326    }
327
328    /// Returns if the installed packages satisfy the given requirements.
329    pub fn satisfies_spec(
330        &self,
331        requirements: &[UnresolvedRequirementSpecification],
332        constraints: &[NameRequirementSpecification],
333        overrides: &[UnresolvedRequirementSpecification],
334        override_dependencies: &[Override<Requirement>],
335        exclude_dependencies: &[ExcludeDependency],
336        installation: InstallationStrategy,
337        markers: &ResolverMarkerEnvironment,
338        tags: &Tags,
339        config_settings: &ConfigSettings,
340        config_settings_package: &PackageConfigSettings,
341        extra_build_requires: &ExtraBuildRequires,
342        extra_build_variables: &ExtraBuildVariables,
343    ) -> Result<SatisfiesResult> {
344        // First, map all unnamed requirements to named requirements.
345        let requirements = {
346            let mut named = Vec::with_capacity(requirements.len());
347            for requirement in requirements {
348                match &requirement.requirement {
349                    UnresolvedRequirement::Named(requirement) => {
350                        named.push(Cow::Borrowed(requirement));
351                    }
352                    UnresolvedRequirement::Unnamed(requirement) => {
353                        match self.get_urls(requirement.url.verbatim.raw()).as_slice() {
354                            [] => {
355                                return Ok(SatisfiesResult::Unsatisfied(
356                                    requirement.url.verbatim.raw().to_string(),
357                                ));
358                            }
359                            [distribution] => {
360                                let requirement = uv_pep508::Requirement {
361                                    name: distribution.name().clone(),
362                                    version_or_url: Some(VersionOrUrl::Url(
363                                        requirement.url.clone(),
364                                    )),
365                                    marker: requirement.marker,
366                                    extras: requirement.extras.clone(),
367                                    origin: requirement.origin.clone(),
368                                };
369                                named.push(Cow::Owned(Requirement::from(requirement)));
370                            }
371                            _ => {
372                                return Ok(SatisfiesResult::Unsatisfied(
373                                    requirement.url.verbatim.raw().to_string(),
374                                ));
375                            }
376                        }
377                    }
378                }
379            }
380            named
381        };
382
383        // Second, map all overrides to named requirements. We assume that all overrides are
384        // relevant.
385        let overrides = {
386            let mut named = Vec::with_capacity(overrides.len());
387            for requirement in overrides {
388                match &requirement.requirement {
389                    UnresolvedRequirement::Named(requirement) => {
390                        named.push(Cow::Borrowed(requirement));
391                    }
392                    UnresolvedRequirement::Unnamed(requirement) => {
393                        match self.get_urls(requirement.url.verbatim.raw()).as_slice() {
394                            [] => {
395                                return Ok(SatisfiesResult::Unsatisfied(
396                                    requirement.url.verbatim.raw().to_string(),
397                                ));
398                            }
399                            [distribution] => {
400                                let requirement = uv_pep508::Requirement {
401                                    name: distribution.name().clone(),
402                                    version_or_url: Some(VersionOrUrl::Url(
403                                        requirement.url.clone(),
404                                    )),
405                                    marker: requirement.marker,
406                                    extras: requirement.extras.clone(),
407                                    origin: requirement.origin.clone(),
408                                };
409                                named.push(Cow::Owned(Requirement::from(requirement)));
410                            }
411                            _ => {
412                                return Ok(SatisfiesResult::Unsatisfied(
413                                    requirement.url.verbatim.raw().to_string(),
414                                ));
415                            }
416                        }
417                    }
418                }
419            }
420            named
421        };
422
423        let overrides = Overrides::from_entries(
424            override_dependencies
425                .iter()
426                .cloned()
427                .chain(
428                    overrides
429                        .iter()
430                        .map(Cow::as_ref)
431                        .cloned()
432                        .map(Override::Requirement),
433                )
434                .collect(),
435        )?;
436        let excludes = Excludes::from_entries(exclude_dependencies.iter().cloned());
437
438        self.satisfies_requirements(
439            requirements.iter().map(Cow::as_ref),
440            constraints.iter().map(|constraint| &constraint.requirement),
441            &overrides,
442            &excludes,
443            installation,
444            markers,
445            tags,
446            config_settings,
447            config_settings_package,
448            extra_build_requires,
449            extra_build_variables,
450        )
451    }
452
453    /// Like [`SitePackages::satisfies_spec`], but with resolved names for all requirements.
454    pub fn satisfies_requirements<'a>(
455        &self,
456        requirements: impl ExactSizeIterator<Item = &'a Requirement>,
457        constraints: impl Iterator<Item = &'a Requirement>,
458        overrides: &'a Overrides,
459        excludes: &'a Excludes,
460        installation: InstallationStrategy,
461        markers: &ResolverMarkerEnvironment,
462        tags: &Tags,
463        config_settings: &ConfigSettings,
464        config_settings_package: &PackageConfigSettings,
465        extra_build_requires: &ExtraBuildRequires,
466        extra_build_variables: &ExtraBuildVariables,
467    ) -> Result<SatisfiesResult> {
468        // Collect the constraints by package name.
469        let constraints: FxHashMap<&PackageName, Vec<&Requirement>> =
470            constraints.fold(FxHashMap::default(), |mut constraints, constraint| {
471                constraints
472                    .entry(&constraint.name)
473                    .or_default()
474                    .push(constraint);
475                constraints
476            });
477        let mut stack = Vec::with_capacity(requirements.len());
478        let mut seen = FxHashSet::with_capacity_and_hasher(requirements.len(), FxBuildHasher);
479
480        // Add the direct requirements to the queue.
481        for requirement in overrides
482            .apply(requirements)
483            .filter(|requirement| !excludes.contains(&requirement.name))
484        {
485            if requirement.evaluate_markers(Some(markers), &[]) {
486                let requirement = requirement.into_owned();
487                if seen.insert(requirement.clone()) {
488                    stack.push(requirement);
489                }
490            }
491        }
492
493        // Verify that all non-editable requirements are met.
494        while let Some(requirement) = stack.pop() {
495            let name = &requirement.name;
496            let installed = self.get_packages(name);
497            match installed.as_slice() {
498                [] => {
499                    // The package isn't installed.
500                    return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()));
501                }
502                [distribution] => {
503                    // Validate that the requirement is satisfied.
504                    if requirement.evaluate_markers(Some(markers), &[]) {
505                        match RequirementSatisfaction::check(
506                            name,
507                            distribution,
508                            &requirement.source,
509                            None,
510                            installation,
511                            tags,
512                            config_settings,
513                            config_settings_package,
514                            extra_build_requires,
515                            extra_build_variables,
516                        ) {
517                            RequirementSatisfaction::Mismatch
518                            | RequirementSatisfaction::OutOfDate
519                            | RequirementSatisfaction::CacheInvalid => {
520                                return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()));
521                            }
522                            RequirementSatisfaction::Satisfied => {}
523                        }
524                    }
525
526                    // Validate that the installed version satisfies the constraints.
527                    for constraint in constraints.get(name).into_iter().flatten() {
528                        if constraint.evaluate_markers(Some(markers), &[]) {
529                            match RequirementSatisfaction::check(
530                                name,
531                                distribution,
532                                &constraint.source,
533                                None,
534                                installation,
535                                tags,
536                                config_settings,
537                                config_settings_package,
538                                extra_build_requires,
539                                extra_build_variables,
540                            ) {
541                                RequirementSatisfaction::Mismatch
542                                | RequirementSatisfaction::OutOfDate
543                                | RequirementSatisfaction::CacheInvalid => {
544                                    return Ok(SatisfiesResult::Unsatisfied(
545                                        requirement.to_string(),
546                                    ));
547                                }
548                                RequirementSatisfaction::Satisfied => {}
549                            }
550                        }
551                    }
552
553                    // Recurse into the dependencies.
554                    let metadata = distribution
555                        .read_metadata()
556                        .with_context(|| format!("Failed to read metadata for: {distribution}"))?;
557
558                    // Add the dependencies to the queue.
559                    let dependencies = metadata
560                        .requires_dist
561                        .iter()
562                        .cloned()
563                        .map(Requirement::from)
564                        .collect::<Vec<_>>();
565                    for dependency in overrides
566                        .apply_for(name, distribution.version(), &dependencies)
567                        .filter(|dependency| {
568                            !excludes.contains_for(name, distribution.version(), &dependency.name)
569                        })
570                    {
571                        if dependency.evaluate_markers(Some(markers), &requirement.extras) {
572                            let dependency = dependency.into_owned();
573                            if seen.insert(dependency.clone()) {
574                                stack.push(dependency);
575                            }
576                        }
577                    }
578                }
579                _ => {
580                    // There are multiple installed distributions for the same package.
581                    return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()));
582                }
583            }
584        }
585
586        Ok(SatisfiesResult::Fresh {
587            recursive_requirements: seen,
588        })
589    }
590}
591
592#[derive(Debug, Clone, Copy, PartialEq, Eq)]
593pub enum InstallationStrategy {
594    /// A permissive installation strategy, which accepts existing installations even if the source
595    /// type differs, as in the `pip` and `uv pip` CLIs.
596    ///
597    /// In this strategy, packages that are already installed in the environment may be reused if
598    /// they implicitly match the requirements. For example, if the user installs `./path/to/idna`,
599    /// then runs `uv pip install anyio` (which depends on `idna`), the existing `idna` installation
600    /// will be reused if its version matches the requirement, even though it was installed from a
601    /// path and is being implicitly requested from a registry.
602    Permissive,
603
604    /// A strict installation strategy, which requires that existing installations match the source
605    /// type, as in the `uv sync` CLI.
606    ///
607    /// This strategy enforces that the installation source must match the requirement source.
608    /// It prevents reusing packages that were installed from different sources, ensuring
609    /// declarative and reproducible environments.
610    Strict,
611}
612
613/// We check if all requirements are already satisfied, recursing through the requirements tree.
614#[derive(Debug)]
615pub enum SatisfiesResult {
616    /// All requirements are recursively satisfied.
617    Fresh {
618        /// The flattened set (transitive closure) of all requirements checked.
619        recursive_requirements: FxHashSet<Requirement>,
620    },
621    /// We found an unsatisfied requirement. Since we exit early, we only know about the first
622    /// unsatisfied requirement.
623    Unsatisfied(String),
624}
625
626/// Infer the package name from an installed distribution path without reading its metadata.
627///
628/// Returns `None` when the name cannot safely be derived from the filename alone.
629fn installed_dist_name(path: &Path) -> Option<PackageName> {
630    let extension = path.extension()?.to_str()?;
631    let file_stem = path.file_stem()?.to_str()?;
632
633    match extension {
634        "dist-info" => {
635            let (name, version) = file_stem.split_once('-')?;
636            Version::from_str(version).ok()?;
637            PackageName::from_str(name).ok()
638        }
639        "egg-info" => {
640            let filename = EggInfoFilename::parse(file_stem).ok()?;
641            filename.version?;
642            Some(filename.name)
643        }
644        // Legacy editables require reading metadata to determine their package name.
645        _ => None,
646    }
647}
648
649impl IntoIterator for SitePackages {
650    type Item = InstalledDist;
651    type IntoIter = Flatten<std::vec::IntoIter<Option<InstalledDist>>>;
652
653    fn into_iter(self) -> Self::IntoIter {
654        self.distributions.into_iter().flatten()
655    }
656}
657
658fn sorted_dist_like_paths(read_dir: fs::ReadDir) -> Result<Vec<PathBuf>, std::io::Error> {
659    let mut paths = read_dir
660        .filter_map(|read_dir| match read_dir {
661            Ok(entry) => match entry.file_type() {
662                Ok(file_type) => (file_type.is_dir()
663                    || entry
664                        .path()
665                        .extension()
666                        .is_some_and(|ext| ext == "egg-link" || ext == "egg-info"))
667                .then_some(Ok(entry.path())),
668                Err(err) => Some(Err(err)),
669            },
670            Err(err) => Some(Err(err)),
671        })
672        .collect::<Result<Vec<_>, std::io::Error>>()?;
673    paths.sort_unstable();
674    Ok(paths)
675}
676
677#[derive(Debug)]
678pub enum SitePackagesDiagnostic {
679    MetadataUnavailable {
680        /// The package that is missing metadata.
681        package: PackageName,
682        /// The path to the package.
683        path: PathBuf,
684    },
685    TagsUnavailable {
686        /// The package that is missing tags.
687        package: PackageName,
688        /// The path to the package.
689        path: PathBuf,
690    },
691    IncompatiblePythonVersion {
692        /// The package that requires a different version of Python.
693        package: PackageName,
694        /// The version of Python that is installed.
695        version: Version,
696        /// The version of Python that is required.
697        requires_python: VersionSpecifiers,
698    },
699    IncompatiblePlatform {
700        /// The package that was built for a different platform.
701        package: PackageName,
702    },
703    MissingDependency {
704        /// The package that is missing a dependency.
705        package: PackageName,
706        /// The dependency that is missing.
707        requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
708    },
709    IncompatibleDependency {
710        /// The package that has an incompatible dependency.
711        package: PackageName,
712        /// The version of the package that is installed.
713        version: Version,
714        /// The dependency that is incompatible.
715        requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
716    },
717    DuplicatePackage {
718        /// The package that has multiple installed distributions.
719        package: PackageName,
720        /// The installed versions of the package.
721        paths: Vec<PathBuf>,
722    },
723}
724
725impl Diagnostic for SitePackagesDiagnostic {
726    /// Convert the diagnostic into a user-facing message.
727    fn message(&self) -> String {
728        match self {
729            Self::MetadataUnavailable { package, path } => format!(
730                "The package `{package}` is broken or incomplete (unable to read `METADATA`). Consider recreating the virtualenv, or removing the package directory at: {}.",
731                path.display(),
732            ),
733            Self::TagsUnavailable { package, path } => format!(
734                "The package `{package}` is broken or incomplete (unable to read `WHEEL` file). Consider recreating the virtualenv, or removing the package directory at: {}.",
735                path.display(),
736            ),
737            Self::IncompatiblePythonVersion {
738                package,
739                version,
740                requires_python,
741            } => format!(
742                "The package `{package}` requires Python {requires_python}, but `{version}` is installed"
743            ),
744            Self::IncompatiblePlatform { package } => {
745                format!("The package `{package}` was built for a different platform")
746            }
747            Self::MissingDependency {
748                package,
749                requirement,
750            } => {
751                format!("The package `{package}` requires `{requirement}`, but it's not installed")
752            }
753            Self::IncompatibleDependency {
754                package,
755                version,
756                requirement,
757            } => format!(
758                "The package `{package}` requires `{requirement}`, but `{version}` is installed"
759            ),
760            Self::DuplicatePackage { package, paths } => {
761                let mut paths = paths.clone();
762                paths.sort();
763                format!(
764                    "The package `{package}` has multiple installed distributions: {}",
765                    paths.iter().fold(String::new(), |acc, path| acc
766                        + &format!("\n  - {}", path.display()))
767                )
768            }
769        }
770    }
771
772    /// Returns `true` if the [`PackageName`] is involved in this diagnostic.
773    fn includes(&self, name: &PackageName) -> bool {
774        match self {
775            Self::MetadataUnavailable { package, .. } => name == package,
776            Self::TagsUnavailable { package, .. } => name == package,
777            Self::IncompatiblePythonVersion { package, .. } => name == package,
778            Self::IncompatiblePlatform { package } => name == package,
779            Self::MissingDependency { package, .. } => name == package,
780            Self::IncompatibleDependency {
781                package,
782                requirement,
783                ..
784            } => name == package || &requirement.name == name,
785            Self::DuplicatePackage { package, .. } => name == package,
786        }
787    }
788}
789
790impl InstalledPackagesProvider for SitePackages {
791    fn iter(&self) -> impl Iterator<Item = &InstalledDist> {
792        self.iter()
793    }
794
795    fn get_packages(&self, name: &PackageName) -> Vec<&InstalledDist> {
796        self.get_packages(name)
797    }
798}
799
800#[cfg(test)]
801mod tests {
802    #[cfg(unix)]
803    use std::os::unix::fs::PermissionsExt;
804
805    use anyhow::Result;
806    #[cfg(unix)]
807    use uv_cache::Cache;
808    #[cfg(unix)]
809    use uv_distribution_types::Name;
810    #[cfg(unix)]
811    use uv_python::Interpreter;
812
813    #[cfg(unix)]
814    use super::SitePackages;
815    use super::sorted_dist_like_paths;
816
817    #[test]
818    fn sorted_dist_like_paths_filters_and_sorts() -> Result<()> {
819        let site_packages = tempfile::tempdir()?;
820        fs_err::create_dir(site_packages.path().join("z_package-1.0.0.dist-info"))?;
821        fs_err::create_dir(site_packages.path().join("a_package"))?;
822        fs_err::write(site_packages.path().join("editable.egg-link"), "")?;
823        fs_err::write(site_packages.path().join("module.py"), "")?;
824        fs_err::write(site_packages.path().join("metadata.egg-info"), "")?;
825
826        let paths = sorted_dist_like_paths(fs_err::read_dir(site_packages.path())?)?;
827        let names = paths
828            .iter()
829            .filter_map(|path| path.file_name())
830            .map(|name| name.to_string_lossy().into_owned())
831            .collect::<Vec<_>>();
832
833        assert_eq!(
834            names,
835            vec![
836                "a_package".to_string(),
837                "editable.egg-link".to_string(),
838                "metadata.egg-info".to_string(),
839                "z_package-1.0.0.dist-info".to_string(),
840            ]
841        );
842
843        Ok(())
844    }
845
846    /// A missing `purelib` directory must not prevent indexing an existing, distinct `platlib`.
847    #[cfg(unix)]
848    #[tokio::test]
849    async fn site_packages_scans_platlib_when_purelib_is_missing() -> Result<()> {
850        let temp_dir = tempfile::tempdir()?;
851        let purelib = temp_dir.path().join("purelib");
852        let platlib = temp_dir.path().join("platlib");
853        let dist_info = platlib.join("demo-1.0.dist-info");
854        fs_err::create_dir_all(&dist_info)?;
855        fs_err::write(
856            dist_info.join("METADATA"),
857            "Metadata-Version: 2.1\nName: demo\nVersion: 1.0\n",
858        )?;
859
860        let executable = temp_dir.path().join("python");
861        let json = r#"{
862            "result": "success",
863            "platform": {"os": {"name": "manylinux", "major": 2, "minor": 38}, "arch": "x86_64"},
864            "manylinux_compatible": true,
865            "standalone": false,
866            "markers": {
867                "implementation_name": "cpython",
868                "implementation_version": "3.12.0",
869                "os_name": "posix",
870                "platform_machine": "x86_64",
871                "platform_python_implementation": "CPython",
872                "platform_release": "6.5.0",
873                "platform_system": "Linux",
874                "platform_version": "test",
875                "python_full_version": "3.12.0",
876                "python_version": "3.12",
877                "sys_platform": "linux"
878            },
879            "sys_base_exec_prefix": "/python",
880            "sys_base_prefix": "/python",
881            "sys_prefix": "/python",
882            "sys_executable": "{EXECUTABLE}",
883            "sys_path": [],
884            "site_packages": [],
885            "stdlib": "/python/lib/python3.12",
886            "extension_suffixes": [".cpython-312-x86_64-linux-gnu.so", ".abi3.so", ".so"],
887            "scheme": {
888                "data": "/python",
889                "include": "/python/include",
890                "platlib": "{PLATLIB}",
891                "purelib": "{PURELIB}",
892                "scripts": "/python/bin"
893            },
894            "virtualenv": {
895                "data": "",
896                "include": "include",
897                "platlib": "lib64/python3.12/site-packages",
898                "purelib": "lib/python3.12/site-packages",
899                "scripts": "bin"
900            },
901            "pointer_size": "64",
902            "gil_disabled": false,
903            "debug_enabled": false
904        }"#
905        .replace("{EXECUTABLE}", &executable.to_string_lossy())
906        .replace("{PLATLIB}", &platlib.to_string_lossy())
907        .replace("{PURELIB}", &purelib.to_string_lossy());
908        fs_err::write(&executable, format!("#!/bin/sh\necho '{json}'\n"))?;
909        fs_err::set_permissions(&executable, PermissionsExt::from_mode(0o770))?;
910
911        let cache = Cache::temp()?.init().await?;
912        let interpreter = Interpreter::query(&executable, &cache)?;
913        let site_packages = SitePackages::from_interpreter(&interpreter)?;
914
915        assert_eq!(
916            site_packages
917                .iter()
918                .map(|distribution| distribution.name().as_ref())
919                .collect::<Vec<_>>(),
920            ["demo"]
921        );
922
923        Ok(())
924    }
925}