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,
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 = 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 = dependency_reachability.and(parent_reachability);
658 let dependency_package = lock.package(dependency.index);
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 = 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}
1105
1106impl MetadataWheel {
1107 fn from_wheel(workspace_root: &PortablePathBuf, wheel: &Wheel) -> Self {
1108 Self {
1109 source: MetadataWheelWireSource::from_wheel(workspace_root, &wheel.url),
1110 hashes: wheel.hash.as_ref().map(hashes_map).unwrap_or_default(),
1111 size: wheel.size,
1112 upload_time: wheel.upload_time,
1113 filename: wheel.filename.clone(),
1114 }
1115 }
1116}
1117
1118#[derive(Clone, Debug, serde::Serialize)]
1119#[serde(untagged, rename_all = "snake_case")]
1120enum MetadataWheelWireSource {
1121 Url { url: UrlString },
1122 Path { path: PortablePathBuf },
1123}
1124
1125impl MetadataWheelWireSource {
1126 fn from_wheel(workspace_root: &PortablePathBuf, wheel: &WheelWireSource) -> Option<Self> {
1127 match wheel {
1128 WheelWireSource::Url { url } => Some(Self::Url { url: url.clone() }),
1129 WheelWireSource::Path { path } => Some(Self::Path {
1130 path: normalize_workspace_relative_path(workspace_root, path),
1131 }),
1132 WheelWireSource::Filename { .. } => None,
1134 }
1135 }
1136}
1137
1138#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
1139struct MetadataExtra {
1140 name: ExtraName,
1141 id: MetadataNodeIdFlat,
1142}
1143
1144#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
1145struct MetadataGroup {
1146 name: GroupName,
1147 id: MetadataNodeIdFlat,
1148}
1149
1150#[derive(Clone, Debug, serde::Serialize)]
1151struct MetadataBuildSystem {
1152 build_backend: String,
1154 id: MetadataNodeIdFlat,
1155}
1156
1157#[derive(Clone, Debug, serde::Serialize)]
1159struct MetadataConflicts {
1160 sets: Vec<MetadataConflictSet>,
1161}
1162
1163impl MetadataConflicts {
1164 fn from_conflicts(
1165 members: &[MetadataWorkspaceMember],
1166 resolve: &BTreeMap<MetadataNodeIdFlat, MetadataNode>,
1167 conflicts: &Conflicts,
1168 ) -> Self {
1169 Self {
1170 sets: conflicts
1171 .iter()
1172 .map(|set| MetadataConflictSet::from_conflicts(members, resolve, set))
1173 .collect(),
1174 }
1175 }
1176}
1177
1178#[derive(Clone, Debug, serde::Serialize)]
1179struct MetadataConflictSet {
1180 items: Vec<MetadataConflictItem>,
1181}
1182
1183impl MetadataConflictSet {
1184 fn from_conflicts(
1185 members: &[MetadataWorkspaceMember],
1186 resolve: &BTreeMap<MetadataNodeIdFlat, MetadataNode>,
1187 set: &ConflictSet,
1188 ) -> Self {
1189 Self {
1190 items: set
1191 .iter()
1192 .map(|item| MetadataConflictItem::from_conflicts(members, resolve, item))
1193 .collect(),
1194 }
1195 }
1196}
1197
1198#[derive(Clone, Debug, serde::Serialize)]
1199struct MetadataConflictItem {
1200 package: PackageName,
1202 kind: MetadataConflictKind,
1203 id: Option<MetadataNodeIdFlat>,
1206}
1207
1208impl MetadataConflictItem {
1209 fn from_conflicts(
1210 members: &[MetadataWorkspaceMember],
1211 resolve: &BTreeMap<MetadataNodeIdFlat, MetadataNode>,
1212 item: &ConflictItem,
1213 ) -> Self {
1214 let kind = MetadataConflictKind::from_conflicts(item.kind());
1215 let id = members
1216 .iter()
1217 .find(|member| &member.name == item.package())
1218 .and_then(|member| {
1219 let package_id = resolve.get(&member.id)?.id.as_package()?;
1220 let id = MetadataNodeId::Package(MetadataPackageNodeId {
1221 kind: kind.to_node_kind(),
1222 ..package_id.clone()
1223 });
1224 Some(id.to_flat())
1225 });
1226 Self {
1227 package: item.package().clone(),
1228 kind,
1229 id,
1230 }
1231 }
1232}
1233
1234#[derive(Clone, Debug, serde::Serialize)]
1235enum MetadataConflictKind {
1236 Group(GroupName),
1237 Extra(ExtraName),
1238 Project,
1239}
1240
1241impl MetadataConflictKind {
1242 fn from_conflicts(item: &ConflictKind) -> Self {
1243 match item {
1244 ConflictKind::Extra(name) => Self::Extra(name.clone()),
1245 ConflictKind::Group(name) => Self::Group(name.clone()),
1246 ConflictKind::Project => Self::Project,
1247 }
1248 }
1249
1250 fn to_node_kind(&self) -> MetadataNodeKind {
1251 match self {
1252 Self::Group(name) => MetadataNodeKind::Group(name.clone()),
1253 Self::Extra(name) => MetadataNodeKind::Extra(name.clone()),
1254 Self::Project => MetadataNodeKind::Package,
1255 }
1256 }
1257}
1258
1259impl Metadata {
1260 pub fn from_lock(workspace: &Workspace, lock: &Lock) -> Result<Self, MetadataError> {
1262 Ok(Self::from_lock_target(
1263 workspace.install_path(),
1264 Some(workspace),
1265 None,
1266 lock,
1267 ))
1268 }
1269
1270 pub fn from_script(script_path: &Path, lock: &Lock) -> Result<Self, MetadataError> {
1272 let workspace_root = script_path.parent().unwrap_or_else(|| Path::new(""));
1273 Ok(Self::from_lock_target(
1274 workspace_root,
1275 None,
1276 Some(script_path),
1277 lock,
1278 ))
1279 }
1280
1281 fn from_lock_target(
1282 workspace_root: &Path,
1283 workspace: Option<&Workspace>,
1284 script_path: Option<&Path>,
1285 lock: &Lock,
1286 ) -> Self {
1287 let mut resolve = BTreeMap::new();
1288 let mut members = Vec::new();
1289 let workspace_root = PortablePathBuf::from(workspace_root);
1290 let reachability = metadata_reachability(&workspace_root, workspace, lock);
1291
1292 for lock_package in lock.packages() {
1293 let mut meta_package = MetadataNode::from_package_id(
1294 &workspace_root,
1295 &lock_package.id,
1296 MetadataNodeKind::Package,
1297 );
1298 let package_reachability = reachability
1299 .get(&meta_package.id.to_flat())
1300 .copied()
1301 .unwrap_or(MarkerTree::FALSE);
1302
1303 for dependency in &lock_package.dependencies {
1305 meta_package.add_dependency(&workspace_root, dependency, package_reachability);
1306 }
1307
1308 for (extra, dependencies) in &lock_package.optional_dependencies {
1310 let mut meta_extra = MetadataNode::from_package_id(
1311 &workspace_root,
1312 &lock_package.id,
1313 MetadataNodeKind::Extra(extra.clone()),
1314 );
1315 let extra_reachability = reachability
1316 .get(&meta_extra.id.to_flat())
1317 .copied()
1318 .unwrap_or(MarkerTree::FALSE);
1319 meta_extra.dependencies.push(MetadataDependency {
1321 id: meta_package.id.to_flat(),
1322 marker: None,
1323 });
1324 for dependency in dependencies {
1325 meta_extra.add_dependency(&workspace_root, dependency, extra_reachability);
1326 }
1327
1328 meta_package.optional_dependencies.push(MetadataExtra {
1329 name: extra.clone(),
1330 id: meta_extra.id.to_flat(),
1331 });
1332
1333 resolve.insert(meta_extra.id.to_flat(), meta_extra);
1334 }
1335
1336 for (group, dependencies) in &lock_package.dependency_groups {
1338 let mut meta_group = MetadataNode::from_package_id(
1339 &workspace_root,
1340 &lock_package.id,
1341 MetadataNodeKind::Group(group.clone()),
1342 );
1343 let group_reachability = reachability
1344 .get(&meta_group.id.to_flat())
1345 .copied()
1346 .unwrap_or(MarkerTree::FALSE);
1347 for dependency in dependencies {
1349 meta_group.add_dependency(&workspace_root, dependency, group_reachability);
1350 }
1351
1352 meta_package.dependency_groups.push(MetadataGroup {
1353 name: group.clone(),
1354 id: meta_group.id.to_flat(),
1355 });
1356
1357 resolve.insert(meta_group.id.to_flat(), meta_group);
1358 }
1359
1360 if let Some(workspace_package) =
1362 workspace.and_then(|workspace| workspace.packages().get(lock_package.name()))
1363 {
1364 let member = MetadataWorkspaceMember {
1365 name: lock_package.name().clone(),
1366 path: normalize_workspace_relative_path(
1367 &workspace_root,
1368 workspace_package.root().as_path(),
1369 ),
1370 id: meta_package.id.to_flat(),
1371 };
1372 members.push(member);
1373 }
1374
1375 if let Some(sdist) = &lock_package.sdist {
1377 meta_package.sdist = Some(MetadataSourceDist::from_sdist(&workspace_root, sdist));
1378 }
1379
1380 meta_package.set_wheels_from_package(&workspace_root, lock_package);
1381
1382 resolve.insert(meta_package.id.to_flat(), meta_package);
1383 }
1384
1385 let script = script_path.map(|path| {
1386 let path = PortablePathBuf::from(path);
1387 let node = MetadataNode::from_script(
1388 path.clone(),
1389 root_dependencies(&workspace_root, lock, lock.requirements()),
1390 );
1391 let id = node.id.to_flat();
1392 resolve.insert(id.clone(), node);
1393 MetadataScript::new(path, id)
1394 });
1395
1396 let workspace_metadata = workspace.map(|_| {
1397 let mut dependency_groups = Vec::new();
1398 for (group, requirements) in lock.dependency_groups() {
1399 let node = MetadataNode::from_workspace_group(
1400 workspace_root.clone(),
1401 group.clone(),
1402 root_dependencies(&workspace_root, lock, requirements),
1403 );
1404 let id = node.id.to_flat();
1405 resolve.insert(id.clone(), node);
1406 dependency_groups.push(MetadataGroup {
1407 name: group.clone(),
1408 id,
1409 });
1410 }
1411
1412 let node = MetadataNode::from_workspace(workspace_root.clone(), dependency_groups);
1413 let id = node.id.to_flat();
1414 resolve.insert(id.clone(), node);
1415 MetadataWorkspace::new(workspace_root.clone(), id)
1416 });
1417 let conflicts = MetadataConflicts::from_conflicts(&members, &resolve, &lock.conflicts);
1418
1419 Self {
1420 schema: SchemaReport {
1421 version: SchemaVersion::Preview,
1422 },
1423 conflicts,
1424 environment: None,
1425 script,
1426 workspace: workspace_metadata,
1427 module_owners: BTreeMap::new(),
1428 workspace_root,
1429 requires_python: lock.requires_python.clone(),
1430 members,
1431 resolution: resolve,
1432 }
1433 }
1434
1435 pub fn package_node_id(
1436 workspace_root: &PortablePathBuf,
1437 dist: &ResolvedDist,
1438 ) -> Result<String, MetadataError> {
1439 let source = Source::from_resolved_dist(dist, workspace_root.as_ref())?;
1440 Ok(MetadataNodeId::Package(MetadataPackageNodeId {
1441 name: dist.name().clone(),
1442 version: dist.version().cloned(),
1443 source: MetadataSource::from_source(workspace_root, source),
1444 kind: MetadataNodeKind::Package,
1445 })
1446 .to_flat())
1447 }
1448
1449 #[must_use]
1450 pub fn with_environment(mut self, environment: &PythonEnvironment) -> Self {
1451 self.environment = Some(MetadataEnvironment {
1452 root: PortablePathBuf::from(environment.root()),
1453 python: PythonReport::from(environment.interpreter()),
1454 });
1455 self
1456 }
1457
1458 #[must_use]
1459 pub fn with_module_owners(mut self, module_owners: BTreeMap<ModuleName, Vec<String>>) -> Self {
1460 self.module_owners = module_owners
1461 .into_iter()
1462 .filter_map(|(module, owners)| {
1463 let owners = owners
1464 .into_iter()
1465 .filter(|package_id| self.resolution.contains_key(package_id))
1466 .map(|package_id| MetadataModuleOwner { package_id })
1467 .collect::<Vec<_>>();
1468 (!owners.is_empty()).then_some((module, owners))
1469 })
1470 .collect();
1471 self
1472 }
1473
1474 pub fn write_json(&self, writer: impl std::io::Write) -> Result<(), MetadataError> {
1475 Ok(serde_json::to_writer_pretty(writer, self)?)
1476 }
1477}