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 installation,
489 tags,
490 config_settings,
491 config_settings_package,
492 extra_build_requires,
493 extra_build_variables,
494 ) {
495 RequirementSatisfaction::Mismatch
496 | RequirementSatisfaction::OutOfDate
497 | RequirementSatisfaction::CacheInvalid => {
498 return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()));
499 }
500 RequirementSatisfaction::Satisfied => {}
501 }
502 }
503
504 for constraint in constraints.get(name).into_iter().flatten() {
506 if constraint.evaluate_markers(Some(markers), &[]) {
507 match RequirementSatisfaction::check(
508 name,
509 distribution,
510 &constraint.source,
511 installation,
512 tags,
513 config_settings,
514 config_settings_package,
515 extra_build_requires,
516 extra_build_variables,
517 ) {
518 RequirementSatisfaction::Mismatch
519 | RequirementSatisfaction::OutOfDate
520 | RequirementSatisfaction::CacheInvalid => {
521 return Ok(SatisfiesResult::Unsatisfied(
522 requirement.to_string(),
523 ));
524 }
525 RequirementSatisfaction::Satisfied => {}
526 }
527 }
528 }
529
530 let metadata = distribution
532 .read_metadata()
533 .with_context(|| format!("Failed to read metadata for: {distribution}"))?;
534
535 for dependency in &metadata.requires_dist {
537 let dependency = Requirement::from(dependency.clone());
538 if let Some(r#overrides) = overrides.get(&dependency.name) {
539 for dependency in r#overrides {
540 if dependency.evaluate_markers(Some(markers), &requirement.extras) {
541 if seen.insert((*dependency).clone()) {
542 stack.push(Cow::Borrowed(*dependency));
543 }
544 }
545 }
546 } else {
547 if dependency.evaluate_markers(Some(markers), &requirement.extras) {
548 if seen.insert(dependency.clone()) {
549 stack.push(Cow::Owned(dependency));
550 }
551 }
552 }
553 }
554 }
555 _ => {
556 return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()));
558 }
559 }
560 }
561
562 Ok(SatisfiesResult::Fresh {
563 recursive_requirements: seen,
564 })
565 }
566}
567
568#[derive(Debug, Clone, Copy, PartialEq, Eq)]
569pub enum InstallationStrategy {
570 Permissive,
579
580 Strict,
587}
588
589#[derive(Debug)]
591pub enum SatisfiesResult {
592 Fresh {
594 recursive_requirements: FxHashSet<Requirement>,
596 },
597 Unsatisfied(String),
600}
601
602impl IntoIterator for SitePackages {
603 type Item = InstalledDist;
604 type IntoIter = Flatten<std::vec::IntoIter<Option<InstalledDist>>>;
605
606 fn into_iter(self) -> Self::IntoIter {
607 self.distributions.into_iter().flatten()
608 }
609}
610
611#[derive(Debug)]
612pub enum SitePackagesDiagnostic {
613 MetadataUnavailable {
614 package: PackageName,
616 path: PathBuf,
618 },
619 TagsUnavailable {
620 package: PackageName,
622 path: PathBuf,
624 },
625 IncompatiblePythonVersion {
626 package: PackageName,
628 version: Version,
630 requires_python: VersionSpecifiers,
632 },
633 IncompatiblePlatform {
634 package: PackageName,
636 },
637 MissingDependency {
638 package: PackageName,
640 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
642 },
643 IncompatibleDependency {
644 package: PackageName,
646 version: Version,
648 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
650 },
651 DuplicatePackage {
652 package: PackageName,
654 paths: Vec<PathBuf>,
656 },
657}
658
659impl Diagnostic for SitePackagesDiagnostic {
660 fn message(&self) -> String {
662 match self {
663 Self::MetadataUnavailable { package, path } => format!(
664 "The package `{package}` is broken or incomplete (unable to read `METADATA`). Consider recreating the virtualenv, or removing the package directory at: {}.",
665 path.display(),
666 ),
667 Self::TagsUnavailable { package, path } => format!(
668 "The package `{package}` is broken or incomplete (unable to read `WHEEL` file). Consider recreating the virtualenv, or removing the package directory at: {}.",
669 path.display(),
670 ),
671 Self::IncompatiblePythonVersion {
672 package,
673 version,
674 requires_python,
675 } => format!(
676 "The package `{package}` requires Python {requires_python}, but `{version}` is installed"
677 ),
678 Self::IncompatiblePlatform { package } => {
679 format!("The package `{package}` was built for a different platform")
680 }
681 Self::MissingDependency {
682 package,
683 requirement,
684 } => {
685 format!("The package `{package}` requires `{requirement}`, but it's not installed")
686 }
687 Self::IncompatibleDependency {
688 package,
689 version,
690 requirement,
691 } => format!(
692 "The package `{package}` requires `{requirement}`, but `{version}` is installed"
693 ),
694 Self::DuplicatePackage { package, paths } => {
695 let mut paths = paths.clone();
696 paths.sort();
697 format!(
698 "The package `{package}` has multiple installed distributions: {}",
699 paths.iter().fold(String::new(), |acc, path| acc
700 + &format!("\n - {}", path.display()))
701 )
702 }
703 }
704 }
705
706 fn includes(&self, name: &PackageName) -> bool {
708 match self {
709 Self::MetadataUnavailable { package, .. } => name == package,
710 Self::TagsUnavailable { package, .. } => name == package,
711 Self::IncompatiblePythonVersion { package, .. } => name == package,
712 Self::IncompatiblePlatform { package } => name == package,
713 Self::MissingDependency { package, .. } => name == package,
714 Self::IncompatibleDependency {
715 package,
716 requirement,
717 ..
718 } => name == package || &requirement.name == name,
719 Self::DuplicatePackage { package, .. } => name == package,
720 }
721 }
722}
723
724impl InstalledPackagesProvider for SitePackages {
725 fn iter(&self) -> impl Iterator<Item = &InstalledDist> {
726 self.iter()
727 }
728
729 fn get_packages(&self, name: &PackageName) -> Vec<&InstalledDist> {
730 self.get_packages(name)
731 }
732}