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