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#[derive(Debug, Clone)]
34pub struct SitePackages {
35 interpreter: Interpreter,
36 distributions: Vec<Option<InstalledDist>>,
40 by_name: FxHashMap<PackageName, Vec<usize>>,
44 by_url: FxHashMap<DisplaySafeUrl, Vec<usize>>,
46}
47
48impl SitePackages {
49 pub fn from_environment(environment: &PythonEnvironment) -> Result<Self> {
51 Self::from_interpreter(environment.interpreter())
52 }
53
54 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 pub fn from_interpreter(interpreter: &Interpreter) -> Result<Self> {
65 Self::from_interpreter_with_filter(interpreter, None)
66 }
67
68 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 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 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 by_name
133 .entry(dist_info.name().clone())
134 .or_default()
135 .push(idx);
136
137 if let InstalledDistKind::Url(dist) = &dist_info.kind {
139 by_url.entry(dist.url.clone()).or_default().push(idx);
140 }
141
142 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 pub fn interpreter(&self) -> &Interpreter {
157 &self.interpreter
158 }
159
160 pub fn iter(&self) -> impl Iterator<Item = &InstalledDist> {
162 self.distributions.iter().flatten()
163 }
164
165 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 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 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 pub(crate) fn any(&self) -> bool {
200 self.distributions.iter().any(Option::is_some)
201 }
202
203 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 let Some(distribution) = distributions.next() else {
217 continue;
218 };
219
220 if let Some(conflict) = distributions.next() {
221 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 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 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 match distribution.read_tags() {
266 Ok(Some(wheel_tags)) => {
267 if !wheel_tags.is_compatible(tags) {
268 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 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 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 }
303 Some(VersionOrUrl::VersionSpecifier(version_specifier)) => {
304 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 }
320 }
321 }
322 }
323 }
324
325 Ok(diagnostics)
326 }
327
328 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 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 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 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 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 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 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 return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()));
501 }
502 [distribution] => {
503 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 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 let metadata = distribution
555 .read_metadata()
556 .with_context(|| format!("Failed to read metadata for: {distribution}"))?;
557
558 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 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 Permissive,
603
604 Strict,
611}
612
613#[derive(Debug)]
615pub enum SatisfiesResult {
616 Fresh {
618 recursive_requirements: FxHashSet<Requirement>,
620 },
621 Unsatisfied(String),
624}
625
626fn 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 _ => 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 package: PackageName,
682 path: PathBuf,
684 },
685 TagsUnavailable {
686 package: PackageName,
688 path: PathBuf,
690 },
691 IncompatiblePythonVersion {
692 package: PackageName,
694 version: Version,
696 requires_python: VersionSpecifiers,
698 },
699 IncompatiblePlatform {
700 package: PackageName,
702 },
703 MissingDependency {
704 package: PackageName,
706 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
708 },
709 IncompatibleDependency {
710 package: PackageName,
712 version: Version,
714 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
716 },
717 DuplicatePackage {
718 package: PackageName,
720 paths: Vec<PathBuf>,
722 },
723}
724
725impl Diagnostic for SitePackagesDiagnostic {
726 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 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 #[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}