1use std::collections::{BTreeMap, VecDeque};
2use std::fmt::Display;
3use std::path::Path;
4
5use uv_distribution_filename::WheelFilename;
6use uv_distribution_types::{Name, Requirement, RequiresPython, ResolvedDist, UrlString};
7use uv_fs::PortablePathBuf;
8use uv_normalize::{ExtraName, GroupName, PackageName};
9use uv_pep440::Version;
10use uv_pep508::{MarkerTree, StringVersion};
11use uv_pypi_types::{ConflictItem, ConflictKind, ConflictSet, Conflicts, ModuleName};
12use uv_python::{Interpreter, LenientImplementationName, PythonEnvironment};
13use uv_workspace::Workspace;
14
15use crate::lock::{
16 Dependency, DirectSource, Package, PackageId, RegistrySource, Source, SourceDist,
17 SourceDistMetadata, Wheel, WheelWireSource, ZstdWheel,
18};
19use crate::{Lock, LockError};
20
21#[derive(Debug, thiserror::Error)]
22enum MetadataErrorKind {
23 #[error(transparent)]
24 Serialize(#[from] serde_json::error::Error),
25 #[error(transparent)]
26 Lock(#[from] LockError),
27}
28
29#[derive(Debug)]
30pub struct MetadataError {
31 kind: Box<MetadataErrorKind>,
32}
33
34impl std::error::Error for MetadataError {
35 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
36 self.kind.source()
37 }
38}
39
40impl std::fmt::Display for MetadataError {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 write!(f, "{}", self.kind)?;
43 Ok(())
44 }
45}
46
47impl<E> From<E> for MetadataError
48where
49 MetadataErrorKind: From<E>,
50{
51 fn from(err: E) -> Self {
52 Self {
53 kind: Box::new(MetadataErrorKind::from(err)),
54 }
55 }
56}
57
58#[derive(Debug, serde::Serialize)]
60pub struct Metadata {
61 schema: SchemaReport,
63 workspace_root: PortablePathBuf,
68 #[serde(skip_serializing_if = "Option::is_none", default)]
70 environment: Option<MetadataEnvironment>,
71 #[serde(skip_serializing_if = "Option::is_none", default)]
73 script: Option<MetadataScript>,
74 #[serde(skip_serializing_if = "Option::is_none", default)]
76 workspace: Option<MetadataWorkspace>,
77 requires_python: RequiresPython,
81 conflicts: MetadataConflicts,
83 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
85 module_owners: BTreeMap<ModuleName, Vec<MetadataModuleOwner>>,
86 #[serde(skip_serializing_if = "Vec::is_empty", default)]
90 members: Vec<MetadataWorkspaceMember>,
91 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
93 resolution: BTreeMap<MetadataNodeIdFlat, MetadataNode>,
94}
95
96#[derive(serde::Serialize, Debug, Default)]
98#[serde(rename_all = "snake_case")]
99enum SchemaVersion {
100 #[default]
102 Preview,
103}
104
105#[derive(serde::Serialize, Debug, Default)]
107struct SchemaReport {
108 version: SchemaVersion,
110}
111
112#[derive(Debug, serde::Serialize)]
114struct MetadataEnvironment {
115 root: PortablePathBuf,
117 python: PythonReport,
119}
120
121#[derive(Debug, serde::Serialize)]
123pub struct PythonReport {
124 path: PortablePathBuf,
126 version: StringVersion,
128 implementation: LenientImplementationName,
130}
131
132impl From<&Interpreter> for PythonReport {
133 fn from(interpreter: &Interpreter) -> Self {
134 Self {
135 path: PortablePathBuf::from(interpreter.sys_executable()),
136 version: interpreter.python_full_version().clone(),
137 implementation: LenientImplementationName::from(interpreter.implementation_name()),
138 }
139 }
140}
141
142impl PythonReport {
143 pub fn path(&self) -> &Path {
145 self.path.as_ref()
146 }
147
148 #[must_use]
150 pub fn with_path(mut self, path: PortablePathBuf) -> Self {
151 self.path = path;
152 self
153 }
154}
155
156#[derive(Debug, serde::Serialize)]
158pub(crate) struct MetadataScript {
159 path: PortablePathBuf,
161 id: MetadataNodeIdFlat,
163}
164
165impl MetadataScript {
166 pub(crate) fn new(path: PortablePathBuf, id: MetadataNodeIdFlat) -> Self {
167 Self { path, id }
168 }
169}
170
171#[derive(Debug, serde::Serialize)]
173pub(crate) struct MetadataWorkspace {
174 path: PortablePathBuf,
176 id: MetadataNodeIdFlat,
178}
179
180impl MetadataWorkspace {
181 pub(crate) fn new(path: PortablePathBuf, id: MetadataNodeIdFlat) -> Self {
182 Self { path, id }
183 }
184}
185
186#[derive(Debug, serde::Serialize)]
188pub(crate) struct MetadataWorkspaceMember {
189 name: PackageName,
191 path: PortablePathBuf,
193 id: MetadataNodeIdFlat,
195}
196
197impl MetadataWorkspaceMember {
198 pub(crate) fn from_locked_package(
200 workspace_root: &PortablePathBuf,
201 package_id: &PackageId,
202 ) -> Option<Self> {
203 let path = match &package_id.source {
204 Source::Directory(path) | Source::Editable(path) | Source::Virtual(path) => path,
205 Source::Registry(_) | Source::Git(..) | Source::Direct(..) | Source::Path(_) => {
206 return None;
207 }
208 };
209 Some(Self {
210 name: package_id.name.clone(),
211 path: normalize_workspace_relative_path(workspace_root, path),
212 id: MetadataNodeId::from_package_id(
213 workspace_root,
214 package_id,
215 MetadataNodeKind::Package,
216 )
217 .to_flat(),
218 })
219 }
220}
221
222#[derive(Debug, serde::Serialize)]
224struct MetadataModuleOwner {
225 package_id: MetadataNodeIdFlat,
227}
228
229#[derive(Debug, Clone, serde::Serialize)]
312pub(crate) struct MetadataNode {
313 #[serde(flatten)]
315 id: MetadataNodeId,
316 dependencies: Vec<MetadataDependency>,
318 #[serde(skip_serializing_if = "Vec::is_empty", default)]
320 optional_dependencies: Vec<MetadataExtra>,
321 #[serde(skip_serializing_if = "Vec::is_empty", default)]
323 dependency_groups: Vec<MetadataGroup>,
324 #[serde(skip_serializing_if = "Option::is_none", default)]
326 latest_version: Option<Version>,
327 #[serde(skip_serializing_if = "Option::is_none", default)]
329 build_system: Option<MetadataBuildSystem>,
330 #[serde(skip_serializing_if = "Option::is_none", default)]
332 sdist: Option<MetadataSourceDist>,
333 #[serde(skip_serializing_if = "Vec::is_empty", default)]
335 wheels: Vec<MetadataWheel>,
336}
337
338impl MetadataNode {
339 pub(crate) fn new(id: MetadataNodeId) -> Self {
340 Self {
341 id,
342 dependencies: Vec::new(),
343 dependency_groups: Vec::new(),
344 optional_dependencies: Vec::new(),
345 latest_version: None,
346 wheels: Vec::new(),
347 build_system: None,
348 sdist: None,
349 }
350 }
351
352 pub(crate) fn from_package_id(
353 workspace_root: &PortablePathBuf,
354 id: &PackageId,
355 kind: MetadataNodeKind,
356 ) -> Self {
357 Self::new(MetadataNodeId::from_package_id(workspace_root, id, kind))
358 }
359
360 fn from_script(path: PortablePathBuf, dependencies: Vec<MetadataDependency>) -> Self {
361 let mut node = Self::new(MetadataNodeId::from_script(path));
362 node.dependencies = dependencies;
363 node
364 }
365
366 fn from_workspace(path: PortablePathBuf, dependency_groups: Vec<MetadataGroup>) -> Self {
367 let mut node = Self::new(MetadataNodeId::from_workspace(path));
368 node.dependency_groups = dependency_groups;
369 node
370 }
371
372 fn from_workspace_group(
373 path: PortablePathBuf,
374 group: GroupName,
375 dependencies: Vec<MetadataDependency>,
376 ) -> Self {
377 let mut node = Self::new(MetadataNodeId::from_workspace_group(path, group));
378 node.dependencies = dependencies;
379 node
380 }
381
382 fn add_dependency(
383 &mut self,
384 workspace_root: &PortablePathBuf,
385 dependency: &Dependency,
386 parent_reachability: MarkerTree,
387 ) {
388 let mut marker = dependency.simplified_marker.as_simplified_marker_tree();
389 marker.and(parent_reachability);
390 let marker = marker.try_to_string();
391 let extras = dependency.extra();
392 if extras.is_empty() {
393 let id = MetadataNodeId::from_package_id(
394 workspace_root,
395 &dependency.package_id,
396 MetadataNodeKind::Package,
397 );
398 self.dependencies.push(MetadataDependency {
399 id: id.to_flat(),
400 marker,
401 });
402 return;
403 }
404 for extra in extras {
405 let id = MetadataNodeId::from_package_id(
406 workspace_root,
407 &dependency.package_id,
408 MetadataNodeKind::Extra(extra.clone()),
409 );
410 self.dependencies.push(MetadataDependency {
411 id: id.to_flat(),
412 marker: marker.clone(),
413 });
414 }
415 }
416
417 pub(crate) fn add_resolution_dependency(
418 &mut self,
419 id: MetadataNodeIdFlat,
420 marker: Option<MetadataMarker>,
421 ) {
422 self.dependencies.push(MetadataDependency { id, marker });
423 }
424
425 pub(crate) fn add_optional_dependency(&mut self, name: ExtraName, id: MetadataNodeIdFlat) {
426 self.optional_dependencies.push(MetadataExtra { name, id });
427 }
428
429 pub(crate) fn add_dependency_group(&mut self, name: GroupName, id: MetadataNodeIdFlat) {
430 self.dependency_groups.push(MetadataGroup { name, id });
431 }
432
433 pub(crate) fn set_latest_version(&mut self, version: Option<Version>) {
434 self.latest_version = version;
435 }
436
437 pub(crate) fn set_wheels_from_package(
438 &mut self,
439 workspace_root: &PortablePathBuf,
440 package: &Package,
441 ) {
442 self.wheels = package
443 .wheels
444 .iter()
445 .map(|wheel| MetadataWheel::from_wheel(workspace_root, wheel))
446 .collect();
447 }
448
449 pub(crate) fn normalize_resolution(&mut self) {
450 self.dependencies.sort();
451 self.dependencies.dedup();
452 self.optional_dependencies.sort();
453 self.optional_dependencies.dedup();
454 self.dependency_groups.sort();
455 self.dependency_groups.dedup();
456 }
457}
458
459#[derive(Debug, Clone, serde::Serialize)]
460#[serde(rename_all = "snake_case")]
461enum MetadataWorkspaceNodeKind {
462 Workspace,
463}
464
465#[derive(Debug, Clone, serde::Serialize)]
466#[serde(rename_all = "snake_case")]
467enum MetadataWorkspaceGroupNodeKind {
468 Group(GroupName),
469}
470#[derive(Debug, Clone, serde::Serialize)]
471#[serde(rename_all = "snake_case")]
472enum MetadataScriptNodeKind {
473 Script,
474}
475
476fn root_dependencies<'lock>(
477 workspace_root: &PortablePathBuf,
478 lock: &'lock Lock,
479 requirements: impl IntoIterator<Item = &'lock Requirement>,
480) -> Vec<MetadataDependency> {
481 let mut dependencies = Vec::new();
482
483 for requirement in requirements {
486 for package in lock
487 .packages()
488 .iter()
489 .filter(|package| package.name() == &requirement.name)
490 {
491 let Some(marker) = lock.root_requirement_marker(requirement, package) else {
492 continue;
493 };
494
495 let marker = marker.try_to_string();
496 let mut has_extra_node = false;
497 for extra in requirement
498 .extras
499 .iter()
500 .filter(|extra| package.optional_dependencies.contains_key(*extra))
501 {
502 let id = MetadataNodeId::from_package_id(
503 workspace_root,
504 &package.id,
505 MetadataNodeKind::Extra(extra.clone()),
506 );
507 dependencies.push(MetadataDependency {
508 id: id.to_flat(),
509 marker: marker.clone(),
510 });
511 has_extra_node = true;
512 }
513
514 if !has_extra_node {
515 let id = MetadataNodeId::from_package_id(
516 workspace_root,
517 &package.id,
518 MetadataNodeKind::Package,
519 );
520 dependencies.push(MetadataDependency {
521 id: id.to_flat(),
522 marker,
523 });
524 }
525 }
526 }
527
528 dependencies
529}
530
531fn metadata_reachability(
537 workspace_root: &PortablePathBuf,
538 workspace: Option<&Workspace>,
539 lock: &Lock,
540) -> BTreeMap<MetadataNodeIdFlat, MarkerTree> {
541 let mut reachability = BTreeMap::new();
542 let mut queue = VecDeque::new();
543 let always = MarkerTree::TRUE;
544
545 if let Some(workspace) = workspace {
546 for package in lock
547 .packages()
548 .iter()
549 .filter(|package| workspace.packages().contains_key(package.name()))
550 {
551 add_metadata_reachability(
552 workspace_root,
553 &mut reachability,
554 &mut queue,
555 package,
556 MetadataNodeKind::Package,
557 always,
558 );
559 for extra in package.optional_dependencies.keys() {
560 add_metadata_reachability(
561 workspace_root,
562 &mut reachability,
563 &mut queue,
564 package,
565 MetadataNodeKind::Extra(extra.clone()),
566 always,
567 );
568 }
569 for group in package.dependency_groups.keys() {
570 add_metadata_reachability(
571 workspace_root,
572 &mut reachability,
573 &mut queue,
574 package,
575 MetadataNodeKind::Group(group.clone()),
576 always,
577 );
578 }
579 }
580 }
581
582 for requirement in lock
583 .requirements()
584 .iter()
585 .chain(lock.dependency_groups().values().flatten())
586 {
587 for package in lock
588 .packages()
589 .iter()
590 .filter(|package| package.name() == &requirement.name)
591 {
592 let Some(marker) = lock.root_requirement_marker(requirement, package) else {
593 continue;
594 };
595 let mut has_extra_node = false;
596 for extra in requirement
597 .extras
598 .iter()
599 .filter(|extra| package.optional_dependencies.contains_key(*extra))
600 {
601 add_metadata_reachability(
602 workspace_root,
603 &mut reachability,
604 &mut queue,
605 package,
606 MetadataNodeKind::Extra(extra.clone()),
607 marker,
608 );
609 has_extra_node = true;
610 }
611 if !has_extra_node {
612 add_metadata_reachability(
613 workspace_root,
614 &mut reachability,
615 &mut queue,
616 package,
617 MetadataNodeKind::Package,
618 marker,
619 );
620 }
621 }
622 }
623
624 while let Some((package, kind)) = queue.pop_front() {
625 let id =
626 MetadataNodeId::from_package_id(workspace_root, &package.id, kind.clone()).to_flat();
627 let Some(parent_reachability) = reachability.get(&id).copied() else {
628 continue;
629 };
630
631 if matches!(kind, MetadataNodeKind::Extra(_)) {
632 add_metadata_reachability(
633 workspace_root,
634 &mut reachability,
635 &mut queue,
636 package,
637 MetadataNodeKind::Package,
638 parent_reachability,
639 );
640 }
641
642 let dependencies: &[Dependency] = match &kind {
643 MetadataNodeKind::Package => package.dependencies.as_slice(),
644 MetadataNodeKind::Extra(extra) => package
645 .optional_dependencies
646 .get(extra)
647 .map_or(&[], Vec::as_slice),
648 MetadataNodeKind::Group(group) => package
649 .dependency_groups
650 .get(group)
651 .map_or(&[], Vec::as_slice),
652 MetadataNodeKind::Build => &[],
653 };
654 for dependency in dependencies {
655 let mut dependency_reachability =
656 dependency.simplified_marker.as_simplified_marker_tree();
657 dependency_reachability.and(parent_reachability);
658 let dependency_package = lock.find_by_id(&dependency.package_id);
659 if dependency.extra.is_empty() {
660 add_metadata_reachability(
661 workspace_root,
662 &mut reachability,
663 &mut queue,
664 dependency_package,
665 MetadataNodeKind::Package,
666 dependency_reachability,
667 );
668 } else {
669 for extra in &dependency.extra {
670 add_metadata_reachability(
671 workspace_root,
672 &mut reachability,
673 &mut queue,
674 dependency_package,
675 MetadataNodeKind::Extra(extra.clone()),
676 dependency_reachability,
677 );
678 }
679 }
680 }
681 }
682
683 reachability
684}
685
686fn add_metadata_reachability<'lock>(
687 workspace_root: &PortablePathBuf,
688 reachability: &mut BTreeMap<MetadataNodeIdFlat, MarkerTree>,
689 queue: &mut VecDeque<(&'lock Package, MetadataNodeKind)>,
690 package: &'lock Package,
691 kind: MetadataNodeKind,
692 marker: MarkerTree,
693) {
694 let id = MetadataNodeId::from_package_id(workspace_root, &package.id, kind.clone()).to_flat();
695 let changed = if let Some(existing) = reachability.get_mut(&id) {
696 let previous = *existing;
697 existing.or(marker);
698 *existing != previous
699 } else {
700 reachability.insert(id, marker);
701 true
702 };
703 if changed {
704 queue.push_back((package, kind));
705 }
706}
707
708#[derive(Debug, Clone, serde::Serialize)]
710#[serde(untagged)]
711pub(crate) enum MetadataNodeId {
712 Package(MetadataPackageNodeId),
713 Script(MetadataScriptNodeId),
714 Workspace(MetadataWorkspaceNodeId),
715 WorkspaceGroup(MetadataWorkspaceGroupNodeId),
716}
717
718#[derive(Debug, Clone, serde::Serialize)]
722pub(crate) struct MetadataPackageNodeId {
723 name: PackageName,
725 #[serde(skip_serializing_if = "Option::is_none", default)]
727 version: Option<Version>,
728 source: MetadataSource,
730 kind: MetadataNodeKind,
732}
733
734#[derive(Debug, Clone, serde::Serialize)]
736pub(crate) struct MetadataScriptNodeId {
737 kind: MetadataScriptNodeKind,
738 path: PortablePathBuf,
740}
741
742#[derive(Debug, Clone, serde::Serialize)]
744pub(crate) struct MetadataWorkspaceNodeId {
745 kind: MetadataWorkspaceNodeKind,
746 path: PortablePathBuf,
748}
749
750#[derive(Debug, Clone, serde::Serialize)]
752pub(crate) struct MetadataWorkspaceGroupNodeId {
753 kind: MetadataWorkspaceGroupNodeKind,
754 path: PortablePathBuf,
756}
757
758type MetadataNodeIdFlat = String;
762
763impl MetadataNodeId {
764 pub(crate) fn from_script(path: PortablePathBuf) -> Self {
765 Self::Script(MetadataScriptNodeId {
766 kind: MetadataScriptNodeKind::Script,
767 path,
768 })
769 }
770
771 pub(crate) fn from_workspace(path: PortablePathBuf) -> Self {
772 Self::Workspace(MetadataWorkspaceNodeId {
773 kind: MetadataWorkspaceNodeKind::Workspace,
774 path,
775 })
776 }
777
778 pub(crate) fn from_workspace_group(path: PortablePathBuf, group: GroupName) -> Self {
779 Self::WorkspaceGroup(MetadataWorkspaceGroupNodeId {
780 kind: MetadataWorkspaceGroupNodeKind::Group(group),
781 path,
782 })
783 }
784
785 pub(crate) fn from_package_id(
786 workspace_root: &PortablePathBuf,
787 id: &PackageId,
788 kind: MetadataNodeKind,
789 ) -> Self {
790 let name = id.name.clone();
791 let version = id.version.clone();
792 let source = MetadataSource::from_source(workspace_root, id.source.clone());
793
794 Self::Package(MetadataPackageNodeId {
795 name,
796 version,
797 source,
798 kind,
799 })
800 }
801
802 fn as_package(&self) -> Option<&MetadataPackageNodeId> {
803 match self {
804 Self::Package(package) => Some(package),
805 Self::Script(_) | Self::Workspace(_) | Self::WorkspaceGroup(_) => None,
806 }
807 }
808
809 pub(crate) fn to_flat(&self) -> MetadataNodeIdFlat {
810 self.to_string()
811 }
812}
813
814impl Display for MetadataNodeId {
815 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
816 match self {
817 Self::Package(package) => match &package.version {
818 Some(version) => write!(
819 f,
820 "{}{}=={version}@{}",
821 package.name, package.kind, package.source
822 ),
823 None => write!(f, "{}{}@{}", package.name, package.kind, package.source),
824 },
825 Self::Script(script) => write!(f, "script+{}", script.path),
826 Self::Workspace(workspace) => write!(f, "workspace+{}", workspace.path),
827 Self::WorkspaceGroup(group) => {
828 let MetadataWorkspaceGroupNodeKind::Group(name) = &group.kind;
829 write!(f, "workspace+{}:{name}", group.path)
830 }
831 }
832 }
833}
834
835#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
836struct MetadataDependency {
837 id: MetadataNodeIdFlat,
838 #[serde(skip_serializing_if = "Option::is_none", default)]
839 marker: Option<MetadataMarker>,
840}
841
842type MetadataMarker = String;
843
844#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
846#[serde(rename_all = "snake_case")]
847pub(crate) enum MetadataNodeKind {
848 Package,
851 #[expect(dead_code)]
854 Build,
855 Extra(ExtraName),
858 Group(GroupName),
861}
862
863impl Display for MetadataNodeKind {
864 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
865 match self {
866 Self::Package => Ok(()),
868 Self::Build => f.write_str("(build)"),
869 Self::Extra(extra_name) => write!(f, "[{extra_name}]"),
870 Self::Group(group_name) => write!(f, ":{group_name}"),
871 }
872 }
873}
874
875#[derive(Clone, Debug, serde::Serialize)]
876#[serde(untagged, rename_all = "snake_case")]
877enum MetadataSource {
878 Registry {
879 registry: MetadataRegistrySource,
880 },
881 Git {
882 git: UrlString,
883 },
884 Direct {
885 url: UrlString,
886 subdirectory: Option<PortablePathBuf>,
887 },
888 Path {
889 path: PortablePathBuf,
890 },
891 Directory {
892 directory: PortablePathBuf,
893 },
894 Editable {
895 editable: PortablePathBuf,
896 },
897 Virtual {
898 r#virtual: PortablePathBuf,
899 },
900}
901
902impl Display for MetadataSource {
903 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
904 match self {
905 Self::Registry {
906 registry: MetadataRegistrySource::Url(url),
907 }
908 | Self::Git { git: url }
909 | Self::Direct { url, .. } => {
910 write!(f, "{}+{}", self.name(), url)
911 }
912 Self::Registry {
913 registry: MetadataRegistrySource::Path(path),
914 }
915 | Self::Path { path }
916 | Self::Directory { directory: path }
917 | Self::Editable { editable: path }
918 | Self::Virtual { r#virtual: path } => {
919 write!(f, "{}+{}", self.name(), path)
920 }
921 }
922 }
923}
924
925impl MetadataSource {
926 fn name(&self) -> &str {
927 match self {
928 Self::Registry { .. } => "registry",
929 Self::Git { .. } => "git",
930 Self::Direct { .. } => "direct",
931 Self::Path { .. } => "path",
932 Self::Directory { .. } => "directory",
933 Self::Editable { .. } => "editable",
934 Self::Virtual { .. } => "virtual",
935 }
936 }
937}
938
939impl MetadataSource {
940 fn from_source(workspace_root: &PortablePathBuf, source: Source) -> Self {
941 match source {
942 Source::Registry(source) => match source {
943 RegistrySource::Url(url) => Self::Registry {
944 registry: MetadataRegistrySource::Url(url),
945 },
946 RegistrySource::Path(path) => Self::Registry {
947 registry: MetadataRegistrySource::Path(normalize_workspace_relative_path(
948 workspace_root,
949 &path,
950 )),
951 },
952 },
953 Source::Git(url, _) => Self::Git { git: url },
954 Source::Direct(url, DirectSource { subdirectory }) => Self::Direct {
955 url,
956 subdirectory: subdirectory
957 .map(|path| normalize_workspace_relative_path(workspace_root, &path)),
958 },
959 Source::Path(path) => Self::Path {
960 path: normalize_workspace_relative_path(workspace_root, &path),
961 },
962 Source::Directory(path) => Self::Directory {
963 directory: normalize_workspace_relative_path(workspace_root, &path),
964 },
965 Source::Editable(path) => Self::Editable {
966 editable: normalize_workspace_relative_path(workspace_root, &path),
967 },
968 Source::Virtual(path) => Self::Virtual {
969 r#virtual: normalize_workspace_relative_path(workspace_root, &path),
970 },
971 }
972 }
973}
974
975fn normalize_workspace_relative_path(
976 workspace_root: &PortablePathBuf,
977 maybe_rel: &std::path::Path,
978) -> PortablePathBuf {
979 if maybe_rel.is_absolute() {
980 PortablePathBuf::from(maybe_rel)
981 } else {
982 PortablePathBuf::from(workspace_root.as_ref().join(maybe_rel).as_path())
983 }
984}
985
986#[derive(Clone, Debug, serde::Serialize)]
987#[serde(rename_all = "snake_case")]
988enum MetadataRegistrySource {
989 Url(UrlString),
991 Path(PortablePathBuf),
993}
994
995#[derive(Clone, Debug, serde::Serialize)]
996#[serde(untagged, rename_all = "snake_case")]
997enum MetadataSourceDist {
998 Url {
999 url: UrlString,
1000 #[serde(flatten)]
1001 metadata: MetadataSourceDistMetadata,
1002 },
1003 Path {
1004 path: PortablePathBuf,
1005 #[serde(flatten)]
1006 metadata: MetadataSourceDistMetadata,
1007 },
1008 Metadata {
1009 #[serde(flatten)]
1010 metadata: MetadataSourceDistMetadata,
1011 },
1012}
1013
1014impl MetadataSourceDist {
1015 fn from_sdist(workspace_root: &PortablePathBuf, sdist: &SourceDist) -> Self {
1016 match sdist {
1017 SourceDist::Url { url, metadata } => Self::Url {
1018 url: url.clone(),
1019 metadata: MetadataSourceDistMetadata::from_sdist(metadata),
1020 },
1021 SourceDist::Path { path, metadata } => Self::Path {
1022 path: normalize_workspace_relative_path(workspace_root, path),
1023 metadata: MetadataSourceDistMetadata::from_sdist(metadata),
1024 },
1025 SourceDist::Metadata { metadata } => Self::Metadata {
1026 metadata: MetadataSourceDistMetadata::from_sdist(metadata),
1027 },
1028 }
1029 }
1030}
1031
1032#[derive(Clone, Debug, serde::Serialize)]
1033#[serde(rename_all = "snake_case")]
1034struct MetadataSourceDistMetadata {
1035 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
1037 hashes: BTreeMap<HashAlgorithm, Hash>,
1038 #[serde(skip_serializing_if = "Option::is_none", default)]
1042 size: Option<u64>,
1043 #[serde(skip_serializing_if = "Option::is_none", default)]
1045 upload_time: Option<jiff::Timestamp>,
1046}
1047
1048type HashAlgorithm = String;
1050type Hash = String;
1052
1053fn hashes_map(hash: &crate::lock::Hash) -> BTreeMap<HashAlgorithm, Hash> {
1058 Some((hash.0.algorithm.to_string(), hash.0.digest.to_string()))
1059 .into_iter()
1060 .collect()
1061}
1062
1063impl MetadataSourceDistMetadata {
1064 fn from_sdist(sdist: &SourceDistMetadata) -> Self {
1065 Self {
1066 hashes: sdist.hash.as_ref().map(hashes_map).unwrap_or_default(),
1067 size: sdist.size,
1068 upload_time: sdist.upload_time,
1069 }
1070 }
1071}
1072#[derive(Clone, Debug, serde::Serialize)]
1073struct MetadataWheel {
1074 #[serde(flatten)]
1079 source: Option<MetadataWheelWireSource>,
1080 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
1086 hashes: BTreeMap<HashAlgorithm, Hash>,
1087 #[serde(skip_serializing_if = "Option::is_none", default)]
1091 size: Option<u64>,
1092 #[serde(skip_serializing_if = "Option::is_none", default)]
1096 upload_time: Option<jiff::Timestamp>,
1097 filename: WheelFilename,
1104 #[serde(skip_serializing_if = "Option::is_none", default)]
1106 zstd: Option<MetadataZstdWheel>,
1107}
1108
1109impl MetadataWheel {
1110 fn from_wheel(workspace_root: &PortablePathBuf, wheel: &Wheel) -> Self {
1111 Self {
1112 source: MetadataWheelWireSource::from_wheel(workspace_root, &wheel.url),
1113 hashes: wheel.hash.as_ref().map(hashes_map).unwrap_or_default(),
1114 size: wheel.size,
1115 upload_time: wheel.upload_time,
1116 filename: wheel.filename.clone(),
1117 zstd: wheel.zstd.as_ref().map(MetadataZstdWheel::from_wheel),
1118 }
1119 }
1120}
1121
1122#[derive(Clone, Debug, serde::Serialize)]
1123#[serde(untagged, rename_all = "snake_case")]
1124enum MetadataWheelWireSource {
1125 Url { url: UrlString },
1126 Path { path: PortablePathBuf },
1127}
1128
1129impl MetadataWheelWireSource {
1130 fn from_wheel(workspace_root: &PortablePathBuf, wheel: &WheelWireSource) -> Option<Self> {
1131 match wheel {
1132 WheelWireSource::Url { url } => Some(Self::Url { url: url.clone() }),
1133 WheelWireSource::Path { path } => Some(Self::Path {
1134 path: normalize_workspace_relative_path(workspace_root, path),
1135 }),
1136 WheelWireSource::Filename { .. } => None,
1138 }
1139 }
1140}
1141
1142#[derive(Clone, Debug, serde::Serialize)]
1143struct MetadataZstdWheel {
1144 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
1145 hashes: BTreeMap<HashAlgorithm, Hash>,
1146 #[serde(skip_serializing_if = "Option::is_none", default)]
1147 size: Option<u64>,
1148}
1149
1150impl MetadataZstdWheel {
1151 fn from_wheel(wheel: &ZstdWheel) -> Self {
1152 Self {
1153 hashes: wheel.hash.as_ref().map(hashes_map).unwrap_or_default(),
1154 size: wheel.size,
1155 }
1156 }
1157}
1158
1159#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
1160struct MetadataExtra {
1161 name: ExtraName,
1162 id: MetadataNodeIdFlat,
1163}
1164
1165#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
1166struct MetadataGroup {
1167 name: GroupName,
1168 id: MetadataNodeIdFlat,
1169}
1170
1171#[derive(Clone, Debug, serde::Serialize)]
1172struct MetadataBuildSystem {
1173 build_backend: String,
1175 id: MetadataNodeIdFlat,
1176}
1177
1178#[derive(Clone, Debug, serde::Serialize)]
1180struct MetadataConflicts {
1181 sets: Vec<MetadataConflictSet>,
1182}
1183
1184impl MetadataConflicts {
1185 fn from_conflicts(
1186 members: &[MetadataWorkspaceMember],
1187 resolve: &BTreeMap<MetadataNodeIdFlat, MetadataNode>,
1188 conflicts: &Conflicts,
1189 ) -> Self {
1190 Self {
1191 sets: conflicts
1192 .iter()
1193 .map(|set| MetadataConflictSet::from_conflicts(members, resolve, set))
1194 .collect(),
1195 }
1196 }
1197}
1198
1199#[derive(Clone, Debug, serde::Serialize)]
1200struct MetadataConflictSet {
1201 items: Vec<MetadataConflictItem>,
1202}
1203
1204impl MetadataConflictSet {
1205 fn from_conflicts(
1206 members: &[MetadataWorkspaceMember],
1207 resolve: &BTreeMap<MetadataNodeIdFlat, MetadataNode>,
1208 set: &ConflictSet,
1209 ) -> Self {
1210 Self {
1211 items: set
1212 .iter()
1213 .map(|item| MetadataConflictItem::from_conflicts(members, resolve, item))
1214 .collect(),
1215 }
1216 }
1217}
1218
1219#[derive(Clone, Debug, serde::Serialize)]
1220struct MetadataConflictItem {
1221 package: PackageName,
1223 kind: MetadataConflictKind,
1224 id: Option<MetadataNodeIdFlat>,
1227}
1228
1229impl MetadataConflictItem {
1230 fn from_conflicts(
1231 members: &[MetadataWorkspaceMember],
1232 resolve: &BTreeMap<MetadataNodeIdFlat, MetadataNode>,
1233 item: &ConflictItem,
1234 ) -> Self {
1235 let kind = MetadataConflictKind::from_conflicts(item.kind());
1236 let id = members
1237 .iter()
1238 .find(|member| &member.name == item.package())
1239 .and_then(|member| {
1240 let package_id = resolve.get(&member.id)?.id.as_package()?;
1241 let id = MetadataNodeId::Package(MetadataPackageNodeId {
1242 kind: kind.to_node_kind(),
1243 ..package_id.clone()
1244 });
1245 Some(id.to_flat())
1246 });
1247 Self {
1248 package: item.package().clone(),
1249 kind,
1250 id,
1251 }
1252 }
1253}
1254
1255#[derive(Clone, Debug, serde::Serialize)]
1256enum MetadataConflictKind {
1257 Group(GroupName),
1258 Extra(ExtraName),
1259 Project,
1260}
1261
1262impl MetadataConflictKind {
1263 fn from_conflicts(item: &ConflictKind) -> Self {
1264 match item {
1265 ConflictKind::Extra(name) => Self::Extra(name.clone()),
1266 ConflictKind::Group(name) => Self::Group(name.clone()),
1267 ConflictKind::Project => Self::Project,
1268 }
1269 }
1270
1271 fn to_node_kind(&self) -> MetadataNodeKind {
1272 match self {
1273 Self::Group(name) => MetadataNodeKind::Group(name.clone()),
1274 Self::Extra(name) => MetadataNodeKind::Extra(name.clone()),
1275 Self::Project => MetadataNodeKind::Package,
1276 }
1277 }
1278}
1279
1280impl Metadata {
1281 pub fn from_lock(workspace: &Workspace, lock: &Lock) -> Result<Self, MetadataError> {
1283 Ok(Self::from_lock_target(
1284 workspace.install_path(),
1285 Some(workspace),
1286 None,
1287 lock,
1288 ))
1289 }
1290
1291 pub fn from_script(script_path: &Path, lock: &Lock) -> Result<Self, MetadataError> {
1293 let workspace_root = script_path.parent().unwrap_or_else(|| Path::new(""));
1294 Ok(Self::from_lock_target(
1295 workspace_root,
1296 None,
1297 Some(script_path),
1298 lock,
1299 ))
1300 }
1301
1302 fn from_lock_target(
1303 workspace_root: &Path,
1304 workspace: Option<&Workspace>,
1305 script_path: Option<&Path>,
1306 lock: &Lock,
1307 ) -> Self {
1308 let mut resolve = BTreeMap::new();
1309 let mut members = Vec::new();
1310 let workspace_root = PortablePathBuf::from(workspace_root);
1311 let reachability = metadata_reachability(&workspace_root, workspace, lock);
1312
1313 for lock_package in lock.packages() {
1314 let mut meta_package = MetadataNode::from_package_id(
1315 &workspace_root,
1316 &lock_package.id,
1317 MetadataNodeKind::Package,
1318 );
1319 let package_reachability = reachability
1320 .get(&meta_package.id.to_flat())
1321 .copied()
1322 .unwrap_or(MarkerTree::FALSE);
1323
1324 for dependency in &lock_package.dependencies {
1326 meta_package.add_dependency(&workspace_root, dependency, package_reachability);
1327 }
1328
1329 for (extra, dependencies) in &lock_package.optional_dependencies {
1331 let mut meta_extra = MetadataNode::from_package_id(
1332 &workspace_root,
1333 &lock_package.id,
1334 MetadataNodeKind::Extra(extra.clone()),
1335 );
1336 let extra_reachability = reachability
1337 .get(&meta_extra.id.to_flat())
1338 .copied()
1339 .unwrap_or(MarkerTree::FALSE);
1340 meta_extra.dependencies.push(MetadataDependency {
1342 id: meta_package.id.to_flat(),
1343 marker: None,
1344 });
1345 for dependency in dependencies {
1346 meta_extra.add_dependency(&workspace_root, dependency, extra_reachability);
1347 }
1348
1349 meta_package.optional_dependencies.push(MetadataExtra {
1350 name: extra.clone(),
1351 id: meta_extra.id.to_flat(),
1352 });
1353
1354 resolve.insert(meta_extra.id.to_flat(), meta_extra);
1355 }
1356
1357 for (group, dependencies) in &lock_package.dependency_groups {
1359 let mut meta_group = MetadataNode::from_package_id(
1360 &workspace_root,
1361 &lock_package.id,
1362 MetadataNodeKind::Group(group.clone()),
1363 );
1364 let group_reachability = reachability
1365 .get(&meta_group.id.to_flat())
1366 .copied()
1367 .unwrap_or(MarkerTree::FALSE);
1368 for dependency in dependencies {
1370 meta_group.add_dependency(&workspace_root, dependency, group_reachability);
1371 }
1372
1373 meta_package.dependency_groups.push(MetadataGroup {
1374 name: group.clone(),
1375 id: meta_group.id.to_flat(),
1376 });
1377
1378 resolve.insert(meta_group.id.to_flat(), meta_group);
1379 }
1380
1381 if let Some(workspace_package) =
1383 workspace.and_then(|workspace| workspace.packages().get(lock_package.name()))
1384 {
1385 let member = MetadataWorkspaceMember {
1386 name: lock_package.name().clone(),
1387 path: normalize_workspace_relative_path(
1388 &workspace_root,
1389 workspace_package.root().as_path(),
1390 ),
1391 id: meta_package.id.to_flat(),
1392 };
1393 members.push(member);
1394 }
1395
1396 if let Some(sdist) = &lock_package.sdist {
1398 meta_package.sdist = Some(MetadataSourceDist::from_sdist(&workspace_root, sdist));
1399 }
1400
1401 meta_package.set_wheels_from_package(&workspace_root, lock_package);
1402
1403 resolve.insert(meta_package.id.to_flat(), meta_package);
1404 }
1405
1406 let script = script_path.map(|path| {
1407 let path = PortablePathBuf::from(path);
1408 let node = MetadataNode::from_script(
1409 path.clone(),
1410 root_dependencies(&workspace_root, lock, lock.requirements()),
1411 );
1412 let id = node.id.to_flat();
1413 resolve.insert(id.clone(), node);
1414 MetadataScript::new(path, id)
1415 });
1416
1417 let workspace_metadata = workspace.map(|_| {
1418 let mut dependency_groups = Vec::new();
1419 for (group, requirements) in lock.dependency_groups() {
1420 let node = MetadataNode::from_workspace_group(
1421 workspace_root.clone(),
1422 group.clone(),
1423 root_dependencies(&workspace_root, lock, requirements),
1424 );
1425 let id = node.id.to_flat();
1426 resolve.insert(id.clone(), node);
1427 dependency_groups.push(MetadataGroup {
1428 name: group.clone(),
1429 id,
1430 });
1431 }
1432
1433 let node = MetadataNode::from_workspace(workspace_root.clone(), dependency_groups);
1434 let id = node.id.to_flat();
1435 resolve.insert(id.clone(), node);
1436 MetadataWorkspace::new(workspace_root.clone(), id)
1437 });
1438 let conflicts = MetadataConflicts::from_conflicts(&members, &resolve, &lock.conflicts);
1439
1440 Self {
1441 schema: SchemaReport {
1442 version: SchemaVersion::Preview,
1443 },
1444 conflicts,
1445 environment: None,
1446 script,
1447 workspace: workspace_metadata,
1448 module_owners: BTreeMap::new(),
1449 workspace_root,
1450 requires_python: lock.requires_python.clone(),
1451 members,
1452 resolution: resolve,
1453 }
1454 }
1455
1456 pub fn package_node_id(
1457 workspace_root: &PortablePathBuf,
1458 dist: &ResolvedDist,
1459 ) -> Result<String, MetadataError> {
1460 let source = Source::from_resolved_dist(dist, workspace_root.as_ref())?;
1461 Ok(MetadataNodeId::Package(MetadataPackageNodeId {
1462 name: dist.name().clone(),
1463 version: dist.version().cloned(),
1464 source: MetadataSource::from_source(workspace_root, source),
1465 kind: MetadataNodeKind::Package,
1466 })
1467 .to_flat())
1468 }
1469
1470 #[must_use]
1471 pub fn with_environment(mut self, environment: &PythonEnvironment) -> Self {
1472 self.environment = Some(MetadataEnvironment {
1473 root: PortablePathBuf::from(environment.root()),
1474 python: PythonReport::from(environment.interpreter()),
1475 });
1476 self
1477 }
1478
1479 #[must_use]
1480 pub fn with_module_owners(mut self, module_owners: BTreeMap<ModuleName, Vec<String>>) -> Self {
1481 self.module_owners = module_owners
1482 .into_iter()
1483 .filter_map(|(module, owners)| {
1484 let owners = owners
1485 .into_iter()
1486 .filter(|package_id| self.resolution.contains_key(package_id))
1487 .map(|package_id| MetadataModuleOwner { package_id })
1488 .collect::<Vec<_>>();
1489 (!owners.is_empty()).then_some((module, owners))
1490 })
1491 .collect();
1492 self
1493 }
1494
1495 pub fn to_json(&self) -> Result<String, MetadataError> {
1496 Ok(serde_json::to_string_pretty(self)?)
1497 }
1498}