1use std::borrow::Cow;
2use std::collections::BTreeSet;
3use std::iter::Flatten;
4use std::path::PathBuf;
5
6use anyhow::{Context, Result};
7use fs_err as fs;
8use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
9
10use uv_distribution_types::{
11 ConfigSettings, Diagnostic, ExtraBuildRequires, ExtraBuildVariables, InstalledDist,
12 InstalledDistKind, Name, NameRequirementSpecification, PackageConfigSettings, Requirement,
13 UnresolvedRequirement, UnresolvedRequirementSpecification,
14};
15use uv_fs::Simplified;
16use uv_normalize::PackageName;
17use uv_pep440::{Version, VersionSpecifiers};
18use uv_pep508::VersionOrUrl;
19use uv_platform_tags::Tags;
20use uv_pypi_types::{ResolverMarkerEnvironment, VerbatimParsedUrl};
21use uv_python::{Interpreter, PythonEnvironment};
22use uv_redacted::DisplaySafeUrl;
23use uv_types::InstalledPackagesProvider;
24use uv_warnings::warn_user;
25
26use crate::satisfies::RequirementSatisfaction;
27
28#[derive(Debug, Clone)]
32pub struct SitePackages {
33 interpreter: Interpreter,
34 distributions: Vec<Option<InstalledDist>>,
38 by_name: FxHashMap<PackageName, Vec<usize>>,
42 by_url: FxHashMap<DisplaySafeUrl, Vec<usize>>,
44}
45
46impl SitePackages {
47 pub fn from_environment(environment: &PythonEnvironment) -> Result<Self> {
49 Self::from_interpreter(environment.interpreter())
50 }
51
52 pub fn from_interpreter(interpreter: &Interpreter) -> Result<Self> {
54 let mut distributions: Vec<Option<InstalledDist>> = Vec::new();
55 let mut by_name = FxHashMap::default();
56 let mut by_url = FxHashMap::default();
57
58 for site_packages in interpreter.site_packages() {
59 let site_packages = match fs::read_dir(site_packages.as_ref()) {
61 Ok(read_dir) => {
62 let dist_likes: BTreeSet<_> = read_dir
64 .filter_map(|read_dir| match read_dir {
65 Ok(entry) => match entry.file_type() {
66 Ok(file_type) => (file_type.is_dir()
67 || entry
68 .path()
69 .extension()
70 .is_some_and(|ext| ext == "egg-link" || ext == "egg-info"))
71 .then_some(Ok(entry.path())),
72 Err(err) => Some(Err(err)),
73 },
74 Err(err) => Some(Err(err)),
75 })
76 .collect::<Result<_, std::io::Error>>()
77 .with_context(|| {
78 format!(
79 "Failed to read site-packages directory contents: {}",
80 site_packages.user_display()
81 )
82 })?;
83 dist_likes
84 }
85 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
86 return Ok(Self {
87 interpreter: interpreter.clone(),
88 distributions,
89 by_name,
90 by_url,
91 });
92 }
93 Err(err) => return Err(err).context("Failed to read site-packages directory"),
94 };
95
96 for path in site_packages {
98 let dist_info = match InstalledDist::try_from_path(&path) {
99 Ok(Some(dist_info)) => dist_info,
100 Ok(None) => continue,
101 Err(_)
102 if path.file_name().is_some_and(|name| {
103 name.to_str().is_some_and(|name| name.starts_with('~'))
104 }) =>
105 {
106 warn_user!(
107 "Ignoring dangling temporary directory: `{}`",
108 path.simplified_display().cyan()
109 );
110 continue;
111 }
112 Err(err) => {
113 return Err(err).context(format!(
114 "Failed to read metadata from: `{}`",
115 path.simplified_display()
116 ));
117 }
118 };
119
120 let idx = distributions.len();
121
122 by_name
124 .entry(dist_info.name().clone())
125 .or_default()
126 .push(idx);
127
128 if let InstalledDistKind::Url(dist) = &dist_info.kind {
130 by_url.entry(dist.url.clone()).or_default().push(idx);
131 }
132
133 distributions.push(Some(dist_info));
135 }
136 }
137
138 Ok(Self {
139 interpreter: interpreter.clone(),
140 distributions,
141 by_name,
142 by_url,
143 })
144 }
145
146 pub fn interpreter(&self) -> &Interpreter {
148 &self.interpreter
149 }
150
151 pub fn iter(&self) -> impl Iterator<Item = &InstalledDist> {
153 self.distributions.iter().flatten()
154 }
155
156 pub fn get_packages(&self, name: &PackageName) -> Vec<&InstalledDist> {
158 let Some(indexes) = self.by_name.get(name) else {
159 return Vec::new();
160 };
161 indexes
162 .iter()
163 .flat_map(|&index| &self.distributions[index])
164 .collect()
165 }
166
167 pub fn remove_packages(&mut self, name: &PackageName) -> Vec<InstalledDist> {
169 let Some(indexes) = self.by_name.get(name) else {
170 return Vec::new();
171 };
172 indexes
173 .iter()
174 .filter_map(|index| std::mem::take(&mut self.distributions[*index]))
175 .collect()
176 }
177
178 pub fn get_urls(&self, url: &DisplaySafeUrl) -> Vec<&InstalledDist> {
180 let Some(indexes) = self.by_url.get(url) else {
181 return Vec::new();
182 };
183 indexes
184 .iter()
185 .flat_map(|&index| &self.distributions[index])
186 .collect()
187 }
188
189 pub fn any(&self) -> bool {
191 self.distributions.iter().any(Option::is_some)
192 }
193
194 pub fn diagnostics(
196 &self,
197 markers: &ResolverMarkerEnvironment,
198 tags: &Tags,
199 ) -> Result<Vec<SitePackagesDiagnostic>> {
200 let mut diagnostics = Vec::new();
201
202 for (package, indexes) in &self.by_name {
203 let mut distributions = indexes.iter().flat_map(|index| &self.distributions[*index]);
204
205 let Some(distribution) = distributions.next() else {
207 continue;
208 };
209
210 if let Some(conflict) = distributions.next() {
211 diagnostics.push(SitePackagesDiagnostic::DuplicatePackage {
213 package: package.clone(),
214 paths: std::iter::once(distribution.install_path().to_owned())
215 .chain(std::iter::once(conflict.install_path().to_owned()))
216 .chain(distributions.map(|dist| dist.install_path().to_owned()))
217 .collect(),
218 });
219 continue;
220 }
221
222 for index in indexes {
223 let Some(distribution) = &self.distributions[*index] else {
224 continue;
225 };
226
227 let Ok(metadata) = distribution.read_metadata() else {
229 diagnostics.push(SitePackagesDiagnostic::MetadataUnavailable {
230 package: package.clone(),
231 path: distribution.install_path().to_owned(),
232 });
233 continue;
234 };
235
236 if let Some(requires_python) = metadata.requires_python.as_ref() {
238 if !requires_python.contains(markers.python_full_version()) {
239 diagnostics.push(SitePackagesDiagnostic::IncompatiblePythonVersion {
240 package: package.clone(),
241 version: self.interpreter.python_version().clone(),
242 requires_python: requires_python.clone(),
243 });
244 }
245 }
246
247 match distribution.read_tags() {
249 Ok(Some(wheel_tags)) => {
250 if !wheel_tags.is_compatible(tags) {
251 diagnostics.push(SitePackagesDiagnostic::IncompatiblePlatform {
253 package: package.clone(),
254 });
255 }
256 }
257 Ok(None) => {}
258 Err(_) => {
259 diagnostics.push(SitePackagesDiagnostic::TagsUnavailable {
260 package: package.clone(),
261 path: distribution.install_path().to_owned(),
262 });
263 }
264 }
265
266 for dependency in &metadata.requires_dist {
268 if !dependency.evaluate_markers(markers, &[]) {
269 continue;
270 }
271
272 let installed = self.get_packages(&dependency.name);
273 match installed.as_slice() {
274 [] => {
275 diagnostics.push(SitePackagesDiagnostic::MissingDependency {
277 package: package.clone(),
278 requirement: dependency.clone(),
279 });
280 }
281 [installed] => {
282 match &dependency.version_or_url {
283 None | Some(VersionOrUrl::Url(_)) => {
284 }
286 Some(VersionOrUrl::VersionSpecifier(version_specifier)) => {
287 if !version_specifier.contains(installed.version()) {
289 diagnostics.push(
290 SitePackagesDiagnostic::IncompatibleDependency {
291 package: package.clone(),
292 version: installed.version().clone(),
293 requirement: dependency.clone(),
294 },
295 );
296 }
297 }
298 }
299 }
300 _ => {
301 }
303 }
304 }
305 }
306 }
307
308 Ok(diagnostics)
309 }
310
311 pub fn satisfies_spec(
313 &self,
314 requirements: &[UnresolvedRequirementSpecification],
315 constraints: &[NameRequirementSpecification],
316 overrides: &[UnresolvedRequirementSpecification],
317 installation: InstallationStrategy,
318 markers: &ResolverMarkerEnvironment,
319 tags: &Tags,
320 config_settings: &ConfigSettings,
321 config_settings_package: &PackageConfigSettings,
322 extra_build_requires: &ExtraBuildRequires,
323 extra_build_variables: &ExtraBuildVariables,
324 ) -> Result<SatisfiesResult> {
325 let requirements = {
327 let mut named = Vec::with_capacity(requirements.len());
328 for requirement in requirements {
329 match &requirement.requirement {
330 UnresolvedRequirement::Named(requirement) => {
331 named.push(Cow::Borrowed(requirement));
332 }
333 UnresolvedRequirement::Unnamed(requirement) => {
334 match self.get_urls(requirement.url.verbatim.raw()).as_slice() {
335 [] => {
336 return Ok(SatisfiesResult::Unsatisfied(
337 requirement.url.verbatim.raw().to_string(),
338 ));
339 }
340 [distribution] => {
341 let requirement = uv_pep508::Requirement {
342 name: distribution.name().clone(),
343 version_or_url: Some(VersionOrUrl::Url(
344 requirement.url.clone(),
345 )),
346 marker: requirement.marker,
347 extras: requirement.extras.clone(),
348 origin: requirement.origin.clone(),
349 };
350 named.push(Cow::Owned(Requirement::from(requirement)));
351 }
352 _ => {
353 return Ok(SatisfiesResult::Unsatisfied(
354 requirement.url.verbatim.raw().to_string(),
355 ));
356 }
357 }
358 }
359 }
360 }
361 named
362 };
363
364 let overrides = {
367 let mut named = Vec::with_capacity(overrides.len());
368 for requirement in overrides {
369 match &requirement.requirement {
370 UnresolvedRequirement::Named(requirement) => {
371 named.push(Cow::Borrowed(requirement));
372 }
373 UnresolvedRequirement::Unnamed(requirement) => {
374 match self.get_urls(requirement.url.verbatim.raw()).as_slice() {
375 [] => {
376 return Ok(SatisfiesResult::Unsatisfied(
377 requirement.url.verbatim.raw().to_string(),
378 ));
379 }
380 [distribution] => {
381 let requirement = uv_pep508::Requirement {
382 name: distribution.name().clone(),
383 version_or_url: Some(VersionOrUrl::Url(
384 requirement.url.clone(),
385 )),
386 marker: requirement.marker,
387 extras: requirement.extras.clone(),
388 origin: requirement.origin.clone(),
389 };
390 named.push(Cow::Owned(Requirement::from(requirement)));
391 }
392 _ => {
393 return Ok(SatisfiesResult::Unsatisfied(
394 requirement.url.verbatim.raw().to_string(),
395 ));
396 }
397 }
398 }
399 }
400 }
401 named
402 };
403
404 self.satisfies_requirements(
405 requirements.iter().map(Cow::as_ref),
406 constraints.iter().map(|constraint| &constraint.requirement),
407 overrides.iter().map(Cow::as_ref),
408 installation,
409 markers,
410 tags,
411 config_settings,
412 config_settings_package,
413 extra_build_requires,
414 extra_build_variables,
415 )
416 }
417
418 pub fn satisfies_requirements<'a>(
420 &self,
421 requirements: impl ExactSizeIterator<Item = &'a Requirement>,
422 constraints: impl Iterator<Item = &'a Requirement>,
423 overrides: impl Iterator<Item = &'a Requirement>,
424 installation: InstallationStrategy,
425 markers: &ResolverMarkerEnvironment,
426 tags: &Tags,
427 config_settings: &ConfigSettings,
428 config_settings_package: &PackageConfigSettings,
429 extra_build_requires: &ExtraBuildRequires,
430 extra_build_variables: &ExtraBuildVariables,
431 ) -> Result<SatisfiesResult> {
432 let constraints: FxHashMap<&PackageName, Vec<&Requirement>> =
434 constraints.fold(FxHashMap::default(), |mut constraints, constraint| {
435 constraints
436 .entry(&constraint.name)
437 .or_default()
438 .push(constraint);
439 constraints
440 });
441 let overrides: FxHashMap<&PackageName, Vec<&Requirement>> =
442 overrides.fold(FxHashMap::default(), |mut overrides, r#override| {
443 overrides
444 .entry(&r#override.name)
445 .or_default()
446 .push(r#override);
447 overrides
448 });
449
450 let mut stack = Vec::with_capacity(requirements.len());
451 let mut seen = FxHashSet::with_capacity_and_hasher(requirements.len(), FxBuildHasher);
452
453 for requirement in requirements {
455 if let Some(r#overrides) = overrides.get(&requirement.name) {
456 for dependency in r#overrides {
457 if dependency.evaluate_markers(Some(markers), &[]) {
458 if seen.insert((*dependency).clone()) {
459 stack.push(Cow::Borrowed(*dependency));
460 }
461 }
462 }
463 } else {
464 if requirement.evaluate_markers(Some(markers), &[]) {
465 if seen.insert(requirement.clone()) {
466 stack.push(Cow::Borrowed(requirement));
467 }
468 }
469 }
470 }
471
472 while let Some(requirement) = stack.pop() {
474 let name = &requirement.name;
475 let installed = self.get_packages(name);
476 match installed.as_slice() {
477 [] => {
478 return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()));
480 }
481 [distribution] => {
482 if requirement.evaluate_markers(Some(markers), &[]) {
484 match RequirementSatisfaction::check(
485 name,
486 distribution,
487 &requirement.source,
488 None,
489 installation,
490 tags,
491 config_settings,
492 config_settings_package,
493 extra_build_requires,
494 extra_build_variables,
495 ) {
496 RequirementSatisfaction::Mismatch
497 | RequirementSatisfaction::OutOfDate
498 | RequirementSatisfaction::CacheInvalid => {
499 return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()));
500 }
501 RequirementSatisfaction::Satisfied => {}
502 }
503 }
504
505 for constraint in constraints.get(name).into_iter().flatten() {
507 if constraint.evaluate_markers(Some(markers), &[]) {
508 match RequirementSatisfaction::check(
509 name,
510 distribution,
511 &constraint.source,
512 None,
513 installation,
514 tags,
515 config_settings,
516 config_settings_package,
517 extra_build_requires,
518 extra_build_variables,
519 ) {
520 RequirementSatisfaction::Mismatch
521 | RequirementSatisfaction::OutOfDate
522 | RequirementSatisfaction::CacheInvalid => {
523 return Ok(SatisfiesResult::Unsatisfied(
524 requirement.to_string(),
525 ));
526 }
527 RequirementSatisfaction::Satisfied => {}
528 }
529 }
530 }
531
532 let metadata = distribution
534 .read_metadata()
535 .with_context(|| format!("Failed to read metadata for: {distribution}"))?;
536
537 for dependency in &metadata.requires_dist {
539 let dependency = Requirement::from(dependency.clone());
540 if let Some(r#overrides) = overrides.get(&dependency.name) {
541 for dependency in r#overrides {
542 if dependency.evaluate_markers(Some(markers), &requirement.extras) {
543 if seen.insert((*dependency).clone()) {
544 stack.push(Cow::Borrowed(*dependency));
545 }
546 }
547 }
548 } else {
549 if dependency.evaluate_markers(Some(markers), &requirement.extras) {
550 if seen.insert(dependency.clone()) {
551 stack.push(Cow::Owned(dependency));
552 }
553 }
554 }
555 }
556 }
557 _ => {
558 return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()));
560 }
561 }
562 }
563
564 Ok(SatisfiesResult::Fresh {
565 recursive_requirements: seen,
566 })
567 }
568}
569
570#[derive(Debug, Clone, Copy, PartialEq, Eq)]
571pub enum InstallationStrategy {
572 Permissive,
581
582 Strict,
589}
590
591#[derive(Debug)]
593pub enum SatisfiesResult {
594 Fresh {
596 recursive_requirements: FxHashSet<Requirement>,
598 },
599 Unsatisfied(String),
602}
603
604impl IntoIterator for SitePackages {
605 type Item = InstalledDist;
606 type IntoIter = Flatten<std::vec::IntoIter<Option<InstalledDist>>>;
607
608 fn into_iter(self) -> Self::IntoIter {
609 self.distributions.into_iter().flatten()
610 }
611}
612
613#[derive(Debug)]
614pub enum SitePackagesDiagnostic {
615 MetadataUnavailable {
616 package: PackageName,
618 path: PathBuf,
620 },
621 TagsUnavailable {
622 package: PackageName,
624 path: PathBuf,
626 },
627 IncompatiblePythonVersion {
628 package: PackageName,
630 version: Version,
632 requires_python: VersionSpecifiers,
634 },
635 IncompatiblePlatform {
636 package: PackageName,
638 },
639 MissingDependency {
640 package: PackageName,
642 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
644 },
645 IncompatibleDependency {
646 package: PackageName,
648 version: Version,
650 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
652 },
653 DuplicatePackage {
654 package: PackageName,
656 paths: Vec<PathBuf>,
658 },
659}
660
661impl Diagnostic for SitePackagesDiagnostic {
662 fn message(&self) -> String {
664 match self {
665 Self::MetadataUnavailable { package, path } => format!(
666 "The package `{package}` is broken or incomplete (unable to read `METADATA`). Consider recreating the virtualenv, or removing the package directory at: {}.",
667 path.display(),
668 ),
669 Self::TagsUnavailable { package, path } => format!(
670 "The package `{package}` is broken or incomplete (unable to read `WHEEL` file). Consider recreating the virtualenv, or removing the package directory at: {}.",
671 path.display(),
672 ),
673 Self::IncompatiblePythonVersion {
674 package,
675 version,
676 requires_python,
677 } => format!(
678 "The package `{package}` requires Python {requires_python}, but `{version}` is installed"
679 ),
680 Self::IncompatiblePlatform { package } => {
681 format!("The package `{package}` was built for a different platform")
682 }
683 Self::MissingDependency {
684 package,
685 requirement,
686 } => {
687 format!("The package `{package}` requires `{requirement}`, but it's not installed")
688 }
689 Self::IncompatibleDependency {
690 package,
691 version,
692 requirement,
693 } => format!(
694 "The package `{package}` requires `{requirement}`, but `{version}` is installed"
695 ),
696 Self::DuplicatePackage { package, paths } => {
697 let mut paths = paths.clone();
698 paths.sort();
699 format!(
700 "The package `{package}` has multiple installed distributions: {}",
701 paths.iter().fold(String::new(), |acc, path| acc
702 + &format!("\n - {}", path.display()))
703 )
704 }
705 }
706 }
707
708 fn includes(&self, name: &PackageName) -> bool {
710 match self {
711 Self::MetadataUnavailable { package, .. } => name == package,
712 Self::TagsUnavailable { package, .. } => name == package,
713 Self::IncompatiblePythonVersion { package, .. } => name == package,
714 Self::IncompatiblePlatform { package } => name == package,
715 Self::MissingDependency { package, .. } => name == package,
716 Self::IncompatibleDependency {
717 package,
718 requirement,
719 ..
720 } => name == package || &requirement.name == name,
721 Self::DuplicatePackage { package, .. } => name == package,
722 }
723 }
724}
725
726impl InstalledPackagesProvider for SitePackages {
727 fn iter(&self) -> impl Iterator<Item = &InstalledDist> {
728 self.iter()
729 }
730
731 fn get_packages(&self, name: &PackageName) -> Vec<&InstalledDist> {
732 self.get_packages(name)
733 }
734}