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, DependencyMetadata, Diagnostic, ExtraBuildRequires, ExtraBuildVariables,
12 InstalledDist, InstalledDistKind, Name, NameRequirementSpecification, PackageConfigSettings,
13 Requirement, 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 dependency_metadata: &DependencyMetadata,
200 ) -> Result<Vec<SitePackagesDiagnostic>> {
201 let mut diagnostics = Vec::new();
202
203 for (package, indexes) in &self.by_name {
204 let mut distributions = indexes.iter().flat_map(|index| &self.distributions[*index]);
205
206 let Some(distribution) = distributions.next() else {
208 continue;
209 };
210
211 if let Some(conflict) = distributions.next() {
212 diagnostics.push(SitePackagesDiagnostic::DuplicatePackage {
214 package: package.clone(),
215 paths: std::iter::once(distribution.install_path().to_owned())
216 .chain(std::iter::once(conflict.install_path().to_owned()))
217 .chain(distributions.map(|dist| dist.install_path().to_owned()))
218 .collect(),
219 });
220 continue;
221 }
222
223 for index in indexes {
224 let Some(distribution) = &self.distributions[*index] else {
225 continue;
226 };
227
228 let metadata = if let Some(metadata) =
230 dependency_metadata.get(package, Some(distribution.version()))
231 {
232 Cow::Owned(metadata)
233 } else {
234 let Ok(metadata) = distribution.read_metadata() else {
235 diagnostics.push(SitePackagesDiagnostic::MetadataUnavailable {
236 package: package.clone(),
237 path: distribution.install_path().to_owned(),
238 });
239 continue;
240 };
241 Cow::Borrowed(metadata)
242 };
243
244 if let Some(requires_python) = metadata.requires_python.as_ref() {
246 if !requires_python.contains(markers.python_full_version()) {
247 diagnostics.push(SitePackagesDiagnostic::IncompatiblePythonVersion {
248 package: package.clone(),
249 version: self.interpreter.python_version().clone(),
250 requires_python: requires_python.clone(),
251 });
252 }
253 }
254
255 match distribution.read_tags() {
257 Ok(Some(wheel_tags)) => {
258 if !wheel_tags.is_compatible(tags) {
259 diagnostics.push(SitePackagesDiagnostic::IncompatiblePlatform {
261 package: package.clone(),
262 });
263 }
264 }
265 Ok(None) => {}
266 Err(_) => {
267 diagnostics.push(SitePackagesDiagnostic::TagsUnavailable {
268 package: package.clone(),
269 path: distribution.install_path().to_owned(),
270 });
271 }
272 }
273
274 for dependency in &metadata.requires_dist {
276 if !dependency.evaluate_markers(markers, &[]) {
277 continue;
278 }
279
280 let installed = self.get_packages(&dependency.name);
281 match installed.as_slice() {
282 [] => {
283 diagnostics.push(SitePackagesDiagnostic::MissingDependency {
285 package: package.clone(),
286 requirement: dependency.clone(),
287 });
288 }
289 [installed] => {
290 match &dependency.version_or_url {
291 None | Some(VersionOrUrl::Url(_)) => {
292 }
294 Some(VersionOrUrl::VersionSpecifier(version_specifier)) => {
295 if !version_specifier.contains(installed.version()) {
297 diagnostics.push(
298 SitePackagesDiagnostic::IncompatibleDependency {
299 package: package.clone(),
300 version: installed.version().clone(),
301 requirement: dependency.clone(),
302 },
303 );
304 }
305 }
306 }
307 }
308 _ => {
309 }
311 }
312 }
313 }
314 }
315
316 Ok(diagnostics)
317 }
318
319 pub fn satisfies_spec(
321 &self,
322 requirements: &[UnresolvedRequirementSpecification],
323 constraints: &[NameRequirementSpecification],
324 overrides: &[UnresolvedRequirementSpecification],
325 installation: InstallationStrategy,
326 markers: &ResolverMarkerEnvironment,
327 tags: &Tags,
328 config_settings: &ConfigSettings,
329 config_settings_package: &PackageConfigSettings,
330 extra_build_requires: &ExtraBuildRequires,
331 extra_build_variables: &ExtraBuildVariables,
332 ) -> Result<SatisfiesResult> {
333 let requirements = {
335 let mut named = Vec::with_capacity(requirements.len());
336 for requirement in requirements {
337 match &requirement.requirement {
338 UnresolvedRequirement::Named(requirement) => {
339 named.push(Cow::Borrowed(requirement));
340 }
341 UnresolvedRequirement::Unnamed(requirement) => {
342 match self.get_urls(requirement.url.verbatim.raw()).as_slice() {
343 [] => {
344 return Ok(SatisfiesResult::Unsatisfied(
345 requirement.url.verbatim.raw().to_string(),
346 ));
347 }
348 [distribution] => {
349 let requirement = uv_pep508::Requirement {
350 name: distribution.name().clone(),
351 version_or_url: Some(VersionOrUrl::Url(
352 requirement.url.clone(),
353 )),
354 marker: requirement.marker,
355 extras: requirement.extras.clone(),
356 origin: requirement.origin.clone(),
357 };
358 named.push(Cow::Owned(Requirement::from(requirement)));
359 }
360 _ => {
361 return Ok(SatisfiesResult::Unsatisfied(
362 requirement.url.verbatim.raw().to_string(),
363 ));
364 }
365 }
366 }
367 }
368 }
369 named
370 };
371
372 let overrides = {
375 let mut named = Vec::with_capacity(overrides.len());
376 for requirement in overrides {
377 match &requirement.requirement {
378 UnresolvedRequirement::Named(requirement) => {
379 named.push(Cow::Borrowed(requirement));
380 }
381 UnresolvedRequirement::Unnamed(requirement) => {
382 match self.get_urls(requirement.url.verbatim.raw()).as_slice() {
383 [] => {
384 return Ok(SatisfiesResult::Unsatisfied(
385 requirement.url.verbatim.raw().to_string(),
386 ));
387 }
388 [distribution] => {
389 let requirement = uv_pep508::Requirement {
390 name: distribution.name().clone(),
391 version_or_url: Some(VersionOrUrl::Url(
392 requirement.url.clone(),
393 )),
394 marker: requirement.marker,
395 extras: requirement.extras.clone(),
396 origin: requirement.origin.clone(),
397 };
398 named.push(Cow::Owned(Requirement::from(requirement)));
399 }
400 _ => {
401 return Ok(SatisfiesResult::Unsatisfied(
402 requirement.url.verbatim.raw().to_string(),
403 ));
404 }
405 }
406 }
407 }
408 }
409 named
410 };
411
412 self.satisfies_requirements(
413 requirements.iter().map(Cow::as_ref),
414 constraints.iter().map(|constraint| &constraint.requirement),
415 overrides.iter().map(Cow::as_ref),
416 installation,
417 markers,
418 tags,
419 config_settings,
420 config_settings_package,
421 extra_build_requires,
422 extra_build_variables,
423 )
424 }
425
426 pub fn satisfies_requirements<'a>(
428 &self,
429 requirements: impl ExactSizeIterator<Item = &'a Requirement>,
430 constraints: impl Iterator<Item = &'a Requirement>,
431 overrides: impl Iterator<Item = &'a Requirement>,
432 installation: InstallationStrategy,
433 markers: &ResolverMarkerEnvironment,
434 tags: &Tags,
435 config_settings: &ConfigSettings,
436 config_settings_package: &PackageConfigSettings,
437 extra_build_requires: &ExtraBuildRequires,
438 extra_build_variables: &ExtraBuildVariables,
439 ) -> Result<SatisfiesResult> {
440 let constraints: FxHashMap<&PackageName, Vec<&Requirement>> =
442 constraints.fold(FxHashMap::default(), |mut constraints, constraint| {
443 constraints
444 .entry(&constraint.name)
445 .or_default()
446 .push(constraint);
447 constraints
448 });
449 let overrides: FxHashMap<&PackageName, Vec<&Requirement>> =
450 overrides.fold(FxHashMap::default(), |mut overrides, r#override| {
451 overrides
452 .entry(&r#override.name)
453 .or_default()
454 .push(r#override);
455 overrides
456 });
457
458 let mut stack = Vec::with_capacity(requirements.len());
459 let mut seen = FxHashSet::with_capacity_and_hasher(requirements.len(), FxBuildHasher);
460
461 for requirement in requirements {
463 if let Some(r#overrides) = overrides.get(&requirement.name) {
464 for dependency in r#overrides {
465 if dependency.evaluate_markers(Some(markers), &[]) {
466 if seen.insert((*dependency).clone()) {
467 stack.push(Cow::Borrowed(*dependency));
468 }
469 }
470 }
471 } else {
472 if requirement.evaluate_markers(Some(markers), &[]) {
473 if seen.insert(requirement.clone()) {
474 stack.push(Cow::Borrowed(requirement));
475 }
476 }
477 }
478 }
479
480 while let Some(requirement) = stack.pop() {
482 let name = &requirement.name;
483 let installed = self.get_packages(name);
484 match installed.as_slice() {
485 [] => {
486 return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()));
488 }
489 [distribution] => {
490 if requirement.evaluate_markers(Some(markers), &[]) {
492 match RequirementSatisfaction::check(
493 name,
494 distribution,
495 &requirement.source,
496 None,
497 installation,
498 tags,
499 config_settings,
500 config_settings_package,
501 extra_build_requires,
502 extra_build_variables,
503 ) {
504 RequirementSatisfaction::Mismatch
505 | RequirementSatisfaction::OutOfDate
506 | RequirementSatisfaction::CacheInvalid => {
507 return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()));
508 }
509 RequirementSatisfaction::Satisfied => {}
510 }
511 }
512
513 for constraint in constraints.get(name).into_iter().flatten() {
515 if constraint.evaluate_markers(Some(markers), &[]) {
516 match RequirementSatisfaction::check(
517 name,
518 distribution,
519 &constraint.source,
520 None,
521 installation,
522 tags,
523 config_settings,
524 config_settings_package,
525 extra_build_requires,
526 extra_build_variables,
527 ) {
528 RequirementSatisfaction::Mismatch
529 | RequirementSatisfaction::OutOfDate
530 | RequirementSatisfaction::CacheInvalid => {
531 return Ok(SatisfiesResult::Unsatisfied(
532 requirement.to_string(),
533 ));
534 }
535 RequirementSatisfaction::Satisfied => {}
536 }
537 }
538 }
539
540 let metadata = distribution
542 .read_metadata()
543 .with_context(|| format!("Failed to read metadata for: {distribution}"))?;
544
545 for dependency in &metadata.requires_dist {
547 let dependency = Requirement::from(dependency.clone());
548 if let Some(r#overrides) = overrides.get(&dependency.name) {
549 for dependency in r#overrides {
550 if dependency.evaluate_markers(Some(markers), &requirement.extras) {
551 if seen.insert((*dependency).clone()) {
552 stack.push(Cow::Borrowed(*dependency));
553 }
554 }
555 }
556 } else {
557 if dependency.evaluate_markers(Some(markers), &requirement.extras) {
558 if seen.insert(dependency.clone()) {
559 stack.push(Cow::Owned(dependency));
560 }
561 }
562 }
563 }
564 }
565 _ => {
566 return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()));
568 }
569 }
570 }
571
572 Ok(SatisfiesResult::Fresh {
573 recursive_requirements: seen,
574 })
575 }
576}
577
578#[derive(Debug, Clone, Copy, PartialEq, Eq)]
579pub enum InstallationStrategy {
580 Permissive,
589
590 Strict,
597}
598
599#[derive(Debug)]
601pub enum SatisfiesResult {
602 Fresh {
604 recursive_requirements: FxHashSet<Requirement>,
606 },
607 Unsatisfied(String),
610}
611
612impl IntoIterator for SitePackages {
613 type Item = InstalledDist;
614 type IntoIter = Flatten<std::vec::IntoIter<Option<InstalledDist>>>;
615
616 fn into_iter(self) -> Self::IntoIter {
617 self.distributions.into_iter().flatten()
618 }
619}
620
621#[derive(Debug)]
622pub enum SitePackagesDiagnostic {
623 MetadataUnavailable {
624 package: PackageName,
626 path: PathBuf,
628 },
629 TagsUnavailable {
630 package: PackageName,
632 path: PathBuf,
634 },
635 IncompatiblePythonVersion {
636 package: PackageName,
638 version: Version,
640 requires_python: VersionSpecifiers,
642 },
643 IncompatiblePlatform {
644 package: PackageName,
646 },
647 MissingDependency {
648 package: PackageName,
650 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
652 },
653 IncompatibleDependency {
654 package: PackageName,
656 version: Version,
658 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
660 },
661 DuplicatePackage {
662 package: PackageName,
664 paths: Vec<PathBuf>,
666 },
667}
668
669impl Diagnostic for SitePackagesDiagnostic {
670 fn message(&self) -> String {
672 match self {
673 Self::MetadataUnavailable { package, path } => format!(
674 "The package `{package}` is broken or incomplete (unable to read `METADATA`). Consider recreating the virtualenv, or removing the package directory at: {}.",
675 path.display(),
676 ),
677 Self::TagsUnavailable { package, path } => format!(
678 "The package `{package}` is broken or incomplete (unable to read `WHEEL` file). Consider recreating the virtualenv, or removing the package directory at: {}.",
679 path.display(),
680 ),
681 Self::IncompatiblePythonVersion {
682 package,
683 version,
684 requires_python,
685 } => format!(
686 "The package `{package}` requires Python {requires_python}, but `{version}` is installed"
687 ),
688 Self::IncompatiblePlatform { package } => {
689 format!("The package `{package}` was built for a different platform")
690 }
691 Self::MissingDependency {
692 package,
693 requirement,
694 } => {
695 format!("The package `{package}` requires `{requirement}`, but it's not installed")
696 }
697 Self::IncompatibleDependency {
698 package,
699 version,
700 requirement,
701 } => format!(
702 "The package `{package}` requires `{requirement}`, but `{version}` is installed"
703 ),
704 Self::DuplicatePackage { package, paths } => {
705 let mut paths = paths.clone();
706 paths.sort();
707 format!(
708 "The package `{package}` has multiple installed distributions: {}",
709 paths.iter().fold(String::new(), |acc, path| acc
710 + &format!("\n - {}", path.display()))
711 )
712 }
713 }
714 }
715
716 fn includes(&self, name: &PackageName) -> bool {
718 match self {
719 Self::MetadataUnavailable { package, .. } => name == package,
720 Self::TagsUnavailable { package, .. } => name == package,
721 Self::IncompatiblePythonVersion { package, .. } => name == package,
722 Self::IncompatiblePlatform { package } => name == package,
723 Self::MissingDependency { package, .. } => name == package,
724 Self::IncompatibleDependency {
725 package,
726 requirement,
727 ..
728 } => name == package || &requirement.name == name,
729 Self::DuplicatePackage { package, .. } => name == package,
730 }
731 }
732}
733
734impl InstalledPackagesProvider for SitePackages {
735 fn iter(&self) -> impl Iterator<Item = &InstalledDist> {
736 self.iter()
737 }
738
739 fn get_packages(&self, name: &PackageName) -> Vec<&InstalledDist> {
740 self.get_packages(name)
741 }
742}