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)]
120struct MetadataScript {
121 path: PortablePathBuf,
123 id: MetadataNodeIdFlat,
125}
126
127#[derive(Debug, serde::Serialize)]
129struct MetadataWorkspace {
130 path: PortablePathBuf,
132 id: MetadataNodeIdFlat,
134}
135
136#[derive(Debug, serde::Serialize)]
138struct MetadataWorkspaceMember {
139 name: PackageName,
141 path: PortablePathBuf,
143 id: MetadataNodeIdFlat,
145}
146
147#[derive(Debug, serde::Serialize)]
149struct MetadataModuleOwner {
150 package_id: MetadataNodeIdFlat,
152}
153
154#[derive(Debug, Clone, serde::Serialize)]
201struct MetadataNode {
202 #[serde(flatten)]
204 id: MetadataNodeId,
205 dependencies: Vec<MetadataDependency>,
207 #[serde(skip_serializing_if = "Vec::is_empty", default)]
209 optional_dependencies: Vec<MetadataExtra>,
210 #[serde(skip_serializing_if = "Vec::is_empty", default)]
212 dependency_groups: Vec<MetadataGroup>,
213 #[serde(skip_serializing_if = "Option::is_none", default)]
215 build_system: Option<MetadataBuildSystem>,
216 #[serde(skip_serializing_if = "Option::is_none", default)]
218 sdist: Option<MetadataSourceDist>,
219 #[serde(skip_serializing_if = "Vec::is_empty", default)]
221 wheels: Vec<MetadataWheel>,
222}
223
224impl MetadataNode {
225 fn new(id: MetadataNodeId) -> Self {
226 Self {
227 id,
228 dependencies: Vec::new(),
229 dependency_groups: Vec::new(),
230 optional_dependencies: Vec::new(),
231 wheels: Vec::new(),
232 build_system: None,
233 sdist: None,
234 }
235 }
236
237 fn from_package_id(
238 workspace_root: &PortablePathBuf,
239 id: &PackageId,
240 kind: MetadataNodeKind,
241 ) -> Self {
242 Self::new(MetadataNodeId::from_package_id(workspace_root, id, kind))
243 }
244
245 fn from_script(path: PortablePathBuf, dependencies: Vec<MetadataDependency>) -> Self {
246 let mut node = Self::new(MetadataNodeId::Script(MetadataScriptNodeId {
247 kind: MetadataScriptNodeKind::Script,
248 path,
249 }));
250 node.dependencies = dependencies;
251 node
252 }
253
254 fn from_workspace(path: PortablePathBuf, dependency_groups: Vec<MetadataGroup>) -> Self {
255 let mut node = Self::new(MetadataNodeId::Workspace(MetadataWorkspaceNodeId {
256 kind: MetadataWorkspaceNodeKind::Workspace,
257 path,
258 }));
259 node.dependency_groups = dependency_groups;
260 node
261 }
262
263 fn from_workspace_group(
264 path: PortablePathBuf,
265 group: GroupName,
266 dependencies: Vec<MetadataDependency>,
267 ) -> Self {
268 let mut node = Self::new(MetadataNodeId::WorkspaceGroup(
269 MetadataWorkspaceGroupNodeId {
270 kind: MetadataWorkspaceGroupNodeKind::Group(group),
271 path,
272 },
273 ));
274 node.dependencies = dependencies;
275 node
276 }
277
278 fn add_dependency(
279 &mut self,
280 workspace_root: &PortablePathBuf,
281 dependency: &Dependency,
282 parent_reachability: MarkerTree,
283 ) {
284 let mut marker = dependency.simplified_marker.as_simplified_marker_tree();
285 marker.and(parent_reachability);
286 let marker = marker.try_to_string();
287 let extras = dependency.extra();
288 if extras.is_empty() {
289 let id = MetadataNodeId::from_package_id(
290 workspace_root,
291 &dependency.package_id,
292 MetadataNodeKind::Package,
293 );
294 self.dependencies.push(MetadataDependency {
295 id: id.to_flat(),
296 marker,
297 });
298 return;
299 }
300 for extra in extras {
301 let id = MetadataNodeId::from_package_id(
302 workspace_root,
303 &dependency.package_id,
304 MetadataNodeKind::Extra(extra.clone()),
305 );
306 self.dependencies.push(MetadataDependency {
307 id: id.to_flat(),
308 marker: marker.clone(),
309 });
310 }
311 }
312}
313
314#[derive(Debug, Clone, serde::Serialize)]
315#[serde(rename_all = "snake_case")]
316enum MetadataWorkspaceNodeKind {
317 Workspace,
318}
319
320#[derive(Debug, Clone, serde::Serialize)]
321#[serde(rename_all = "snake_case")]
322enum MetadataWorkspaceGroupNodeKind {
323 Group(GroupName),
324}
325#[derive(Debug, Clone, serde::Serialize)]
326#[serde(rename_all = "snake_case")]
327enum MetadataScriptNodeKind {
328 Script,
329}
330
331fn root_dependencies<'lock>(
332 workspace_root: &PortablePathBuf,
333 lock: &'lock Lock,
334 requirements: impl IntoIterator<Item = &'lock Requirement>,
335) -> Vec<MetadataDependency> {
336 let mut dependencies = Vec::new();
337
338 for requirement in requirements {
341 for package in lock
342 .packages()
343 .iter()
344 .filter(|package| package.name() == &requirement.name)
345 {
346 let Some(marker) = lock.root_requirement_marker(requirement, package) else {
347 continue;
348 };
349
350 let marker = marker.try_to_string();
351 let mut has_extra_node = false;
352 for extra in requirement
353 .extras
354 .iter()
355 .filter(|extra| package.optional_dependencies.contains_key(*extra))
356 {
357 let id = MetadataNodeId::from_package_id(
358 workspace_root,
359 &package.id,
360 MetadataNodeKind::Extra(extra.clone()),
361 );
362 dependencies.push(MetadataDependency {
363 id: id.to_flat(),
364 marker: marker.clone(),
365 });
366 has_extra_node = true;
367 }
368
369 if !has_extra_node {
370 let id = MetadataNodeId::from_package_id(
371 workspace_root,
372 &package.id,
373 MetadataNodeKind::Package,
374 );
375 dependencies.push(MetadataDependency {
376 id: id.to_flat(),
377 marker,
378 });
379 }
380 }
381 }
382
383 dependencies
384}
385
386fn metadata_reachability(
392 workspace_root: &PortablePathBuf,
393 workspace: Option<&Workspace>,
394 lock: &Lock,
395) -> BTreeMap<MetadataNodeIdFlat, MarkerTree> {
396 let mut reachability = BTreeMap::new();
397 let mut queue = VecDeque::new();
398 let always = MarkerTree::TRUE;
399
400 if let Some(workspace) = workspace {
401 for package in lock
402 .packages()
403 .iter()
404 .filter(|package| workspace.packages().contains_key(package.name()))
405 {
406 add_metadata_reachability(
407 workspace_root,
408 &mut reachability,
409 &mut queue,
410 package,
411 MetadataNodeKind::Package,
412 always,
413 );
414 for extra in package.optional_dependencies.keys() {
415 add_metadata_reachability(
416 workspace_root,
417 &mut reachability,
418 &mut queue,
419 package,
420 MetadataNodeKind::Extra(extra.clone()),
421 always,
422 );
423 }
424 for group in package.dependency_groups.keys() {
425 add_metadata_reachability(
426 workspace_root,
427 &mut reachability,
428 &mut queue,
429 package,
430 MetadataNodeKind::Group(group.clone()),
431 always,
432 );
433 }
434 }
435 }
436
437 for requirement in lock
438 .requirements()
439 .iter()
440 .chain(lock.dependency_groups().values().flatten())
441 {
442 for package in lock
443 .packages()
444 .iter()
445 .filter(|package| package.name() == &requirement.name)
446 {
447 let Some(marker) = lock.root_requirement_marker(requirement, package) else {
448 continue;
449 };
450 let mut has_extra_node = false;
451 for extra in requirement
452 .extras
453 .iter()
454 .filter(|extra| package.optional_dependencies.contains_key(*extra))
455 {
456 add_metadata_reachability(
457 workspace_root,
458 &mut reachability,
459 &mut queue,
460 package,
461 MetadataNodeKind::Extra(extra.clone()),
462 marker,
463 );
464 has_extra_node = true;
465 }
466 if !has_extra_node {
467 add_metadata_reachability(
468 workspace_root,
469 &mut reachability,
470 &mut queue,
471 package,
472 MetadataNodeKind::Package,
473 marker,
474 );
475 }
476 }
477 }
478
479 while let Some((package, kind)) = queue.pop_front() {
480 let id =
481 MetadataNodeId::from_package_id(workspace_root, &package.id, kind.clone()).to_flat();
482 let Some(parent_reachability) = reachability.get(&id).copied() else {
483 continue;
484 };
485
486 if matches!(kind, MetadataNodeKind::Extra(_)) {
487 add_metadata_reachability(
488 workspace_root,
489 &mut reachability,
490 &mut queue,
491 package,
492 MetadataNodeKind::Package,
493 parent_reachability,
494 );
495 }
496
497 let dependencies: &[Dependency] = match &kind {
498 MetadataNodeKind::Package => package.dependencies.as_slice(),
499 MetadataNodeKind::Extra(extra) => package
500 .optional_dependencies
501 .get(extra)
502 .map_or(&[], Vec::as_slice),
503 MetadataNodeKind::Group(group) => package
504 .dependency_groups
505 .get(group)
506 .map_or(&[], Vec::as_slice),
507 MetadataNodeKind::Build => &[],
508 };
509 for dependency in dependencies {
510 let mut dependency_reachability =
511 dependency.simplified_marker.as_simplified_marker_tree();
512 dependency_reachability.and(parent_reachability);
513 let dependency_package = lock.find_by_id(&dependency.package_id);
514 if dependency.extra.is_empty() {
515 add_metadata_reachability(
516 workspace_root,
517 &mut reachability,
518 &mut queue,
519 dependency_package,
520 MetadataNodeKind::Package,
521 dependency_reachability,
522 );
523 } else {
524 for extra in &dependency.extra {
525 add_metadata_reachability(
526 workspace_root,
527 &mut reachability,
528 &mut queue,
529 dependency_package,
530 MetadataNodeKind::Extra(extra.clone()),
531 dependency_reachability,
532 );
533 }
534 }
535 }
536 }
537
538 reachability
539}
540
541fn add_metadata_reachability<'lock>(
542 workspace_root: &PortablePathBuf,
543 reachability: &mut BTreeMap<MetadataNodeIdFlat, MarkerTree>,
544 queue: &mut VecDeque<(&'lock Package, MetadataNodeKind)>,
545 package: &'lock Package,
546 kind: MetadataNodeKind,
547 marker: MarkerTree,
548) {
549 let id = MetadataNodeId::from_package_id(workspace_root, &package.id, kind.clone()).to_flat();
550 let changed = if let Some(existing) = reachability.get_mut(&id) {
551 let previous = *existing;
552 existing.or(marker);
553 *existing != previous
554 } else {
555 reachability.insert(id, marker);
556 true
557 };
558 if changed {
559 queue.push_back((package, kind));
560 }
561}
562
563#[derive(Debug, Clone, serde::Serialize)]
565#[serde(untagged)]
566enum MetadataNodeId {
567 Package(MetadataPackageNodeId),
568 Script(MetadataScriptNodeId),
569 Workspace(MetadataWorkspaceNodeId),
570 WorkspaceGroup(MetadataWorkspaceGroupNodeId),
571}
572
573#[derive(Debug, Clone, serde::Serialize)]
577struct MetadataPackageNodeId {
578 name: PackageName,
580 #[serde(skip_serializing_if = "Option::is_none", default)]
582 version: Option<Version>,
583 source: MetadataSource,
585 kind: MetadataNodeKind,
587}
588
589#[derive(Debug, Clone, serde::Serialize)]
591struct MetadataScriptNodeId {
592 kind: MetadataScriptNodeKind,
593 path: PortablePathBuf,
595}
596
597#[derive(Debug, Clone, serde::Serialize)]
599struct MetadataWorkspaceNodeId {
600 kind: MetadataWorkspaceNodeKind,
601 path: PortablePathBuf,
603}
604
605#[derive(Debug, Clone, serde::Serialize)]
607struct MetadataWorkspaceGroupNodeId {
608 kind: MetadataWorkspaceGroupNodeKind,
609 path: PortablePathBuf,
611}
612
613type MetadataNodeIdFlat = String;
617
618impl MetadataNodeId {
619 fn from_package_id(
620 workspace_root: &PortablePathBuf,
621 id: &PackageId,
622 kind: MetadataNodeKind,
623 ) -> Self {
624 let name = id.name.clone();
625 let version = id.version.clone();
626 let source = MetadataSource::from_source(workspace_root, id.source.clone());
627
628 Self::Package(MetadataPackageNodeId {
629 name,
630 version,
631 source,
632 kind,
633 })
634 }
635
636 fn as_package(&self) -> Option<&MetadataPackageNodeId> {
637 match self {
638 Self::Package(package) => Some(package),
639 Self::Script(_) | Self::Workspace(_) | Self::WorkspaceGroup(_) => None,
640 }
641 }
642
643 fn to_flat(&self) -> MetadataNodeIdFlat {
644 self.to_string()
645 }
646}
647
648impl Display for MetadataNodeId {
649 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
650 match self {
651 Self::Package(package) => match &package.version {
652 Some(version) => write!(
653 f,
654 "{}{}=={version}@{}",
655 package.name, package.kind, package.source
656 ),
657 None => write!(f, "{}{}@{}", package.name, package.kind, package.source),
658 },
659 Self::Script(script) => write!(f, "script+{}", script.path),
660 Self::Workspace(workspace) => write!(f, "workspace+{}", workspace.path),
661 Self::WorkspaceGroup(group) => {
662 let MetadataWorkspaceGroupNodeKind::Group(name) = &group.kind;
663 write!(f, "workspace+{}:{name}", group.path)
664 }
665 }
666 }
667}
668
669#[derive(Debug, Clone, serde::Serialize)]
670struct MetadataDependency {
671 id: MetadataNodeIdFlat,
672 #[serde(skip_serializing_if = "Option::is_none", default)]
673 marker: Option<MetadataMarker>,
674}
675
676type MetadataMarker = String;
677
678#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
680#[serde(rename_all = "snake_case")]
681enum MetadataNodeKind {
682 Package,
685 #[expect(dead_code)]
688 Build,
689 Extra(ExtraName),
692 Group(GroupName),
695}
696
697impl Display for MetadataNodeKind {
698 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
699 match self {
700 Self::Package => Ok(()),
702 Self::Build => f.write_str("(build)"),
703 Self::Extra(extra_name) => write!(f, "[{extra_name}]"),
704 Self::Group(group_name) => write!(f, ":{group_name}"),
705 }
706 }
707}
708
709#[derive(Clone, Debug, serde::Serialize)]
710#[serde(untagged, rename_all = "snake_case")]
711enum MetadataSource {
712 Registry {
713 registry: MetadataRegistrySource,
714 },
715 Git {
716 git: UrlString,
717 },
718 Direct {
719 url: UrlString,
720 subdirectory: Option<PortablePathBuf>,
721 },
722 Path {
723 path: PortablePathBuf,
724 },
725 Directory {
726 directory: PortablePathBuf,
727 },
728 Editable {
729 editable: PortablePathBuf,
730 },
731 Virtual {
732 r#virtual: PortablePathBuf,
733 },
734}
735
736impl Display for MetadataSource {
737 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
738 match self {
739 Self::Registry {
740 registry: MetadataRegistrySource::Url(url),
741 }
742 | Self::Git { git: url }
743 | Self::Direct { url, .. } => {
744 write!(f, "{}+{}", self.name(), url)
745 }
746 Self::Registry {
747 registry: MetadataRegistrySource::Path(path),
748 }
749 | Self::Path { path }
750 | Self::Directory { directory: path }
751 | Self::Editable { editable: path }
752 | Self::Virtual { r#virtual: path } => {
753 write!(f, "{}+{}", self.name(), path)
754 }
755 }
756 }
757}
758
759impl MetadataSource {
760 fn name(&self) -> &str {
761 match self {
762 Self::Registry { .. } => "registry",
763 Self::Git { .. } => "git",
764 Self::Direct { .. } => "direct",
765 Self::Path { .. } => "path",
766 Self::Directory { .. } => "directory",
767 Self::Editable { .. } => "editable",
768 Self::Virtual { .. } => "virtual",
769 }
770 }
771}
772
773impl MetadataSource {
774 fn from_source(workspace_root: &PortablePathBuf, source: Source) -> Self {
775 match source {
776 Source::Registry(source) => match source {
777 RegistrySource::Url(url) => Self::Registry {
778 registry: MetadataRegistrySource::Url(url),
779 },
780 RegistrySource::Path(path) => Self::Registry {
781 registry: MetadataRegistrySource::Path(normalize_workspace_relative_path(
782 workspace_root,
783 &path,
784 )),
785 },
786 },
787 Source::Git(url, _) => Self::Git { git: url },
788 Source::Direct(url, DirectSource { subdirectory }) => Self::Direct {
789 url,
790 subdirectory: subdirectory
791 .map(|path| normalize_workspace_relative_path(workspace_root, &path)),
792 },
793 Source::Path(path) => Self::Path {
794 path: normalize_workspace_relative_path(workspace_root, &path),
795 },
796 Source::Directory(path) => Self::Directory {
797 directory: normalize_workspace_relative_path(workspace_root, &path),
798 },
799 Source::Editable(path) => Self::Editable {
800 editable: normalize_workspace_relative_path(workspace_root, &path),
801 },
802 Source::Virtual(path) => Self::Virtual {
803 r#virtual: normalize_workspace_relative_path(workspace_root, &path),
804 },
805 }
806 }
807}
808
809fn normalize_workspace_relative_path(
810 workspace_root: &PortablePathBuf,
811 maybe_rel: &std::path::Path,
812) -> PortablePathBuf {
813 if maybe_rel.is_absolute() {
814 PortablePathBuf::from(maybe_rel)
815 } else {
816 PortablePathBuf::from(workspace_root.as_ref().join(maybe_rel).as_path())
817 }
818}
819
820#[derive(Clone, Debug, serde::Serialize)]
821#[serde(rename_all = "snake_case")]
822enum MetadataRegistrySource {
823 Url(UrlString),
825 Path(PortablePathBuf),
827}
828
829#[derive(Clone, Debug, serde::Serialize)]
830#[serde(untagged, rename_all = "snake_case")]
831enum MetadataSourceDist {
832 Url {
833 url: UrlString,
834 #[serde(flatten)]
835 metadata: MetadataSourceDistMetadata,
836 },
837 Path {
838 path: PortablePathBuf,
839 #[serde(flatten)]
840 metadata: MetadataSourceDistMetadata,
841 },
842 Metadata {
843 #[serde(flatten)]
844 metadata: MetadataSourceDistMetadata,
845 },
846}
847
848impl MetadataSourceDist {
849 fn from_sdist(workspace_root: &PortablePathBuf, sdist: &SourceDist) -> Self {
850 match sdist {
851 SourceDist::Url { url, metadata } => Self::Url {
852 url: url.clone(),
853 metadata: MetadataSourceDistMetadata::from_sdist(metadata),
854 },
855 SourceDist::Path { path, metadata } => Self::Path {
856 path: normalize_workspace_relative_path(workspace_root, path),
857 metadata: MetadataSourceDistMetadata::from_sdist(metadata),
858 },
859 SourceDist::Metadata { metadata } => Self::Metadata {
860 metadata: MetadataSourceDistMetadata::from_sdist(metadata),
861 },
862 }
863 }
864}
865
866#[derive(Clone, Debug, serde::Serialize)]
867#[serde(rename_all = "snake_case")]
868struct MetadataSourceDistMetadata {
869 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
871 hashes: BTreeMap<HashAlgorithm, Hash>,
872 #[serde(skip_serializing_if = "Option::is_none", default)]
876 size: Option<u64>,
877 #[serde(skip_serializing_if = "Option::is_none", default)]
879 upload_time: Option<jiff::Timestamp>,
880}
881
882type HashAlgorithm = String;
884type Hash = String;
886
887fn hashes_map(hash: &crate::lock::Hash) -> BTreeMap<HashAlgorithm, Hash> {
892 Some((hash.0.algorithm.to_string(), hash.0.digest.to_string()))
893 .into_iter()
894 .collect()
895}
896
897impl MetadataSourceDistMetadata {
898 fn from_sdist(sdist: &SourceDistMetadata) -> Self {
899 Self {
900 hashes: sdist.hash.as_ref().map(hashes_map).unwrap_or_default(),
901 size: sdist.size,
902 upload_time: sdist.upload_time,
903 }
904 }
905}
906#[derive(Clone, Debug, serde::Serialize)]
907struct MetadataWheel {
908 #[serde(flatten)]
913 source: Option<MetadataWheelWireSource>,
914 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
920 hashes: BTreeMap<HashAlgorithm, Hash>,
921 #[serde(skip_serializing_if = "Option::is_none", default)]
925 size: Option<u64>,
926 #[serde(skip_serializing_if = "Option::is_none", default)]
930 upload_time: Option<jiff::Timestamp>,
931 filename: WheelFilename,
938 #[serde(skip_serializing_if = "Option::is_none", default)]
940 zstd: Option<MetadataZstdWheel>,
941}
942
943impl MetadataWheel {
944 fn from_wheel(workspace_root: &PortablePathBuf, wheel: &Wheel) -> Self {
945 Self {
946 source: MetadataWheelWireSource::from_wheel(workspace_root, &wheel.url),
947 hashes: wheel.hash.as_ref().map(hashes_map).unwrap_or_default(),
948 size: wheel.size,
949 upload_time: wheel.upload_time,
950 filename: wheel.filename.clone(),
951 zstd: wheel.zstd.as_ref().map(MetadataZstdWheel::from_wheel),
952 }
953 }
954}
955
956#[derive(Clone, Debug, serde::Serialize)]
957#[serde(untagged, rename_all = "snake_case")]
958enum MetadataWheelWireSource {
959 Url { url: UrlString },
960 Path { path: PortablePathBuf },
961}
962
963impl MetadataWheelWireSource {
964 fn from_wheel(workspace_root: &PortablePathBuf, wheel: &WheelWireSource) -> Option<Self> {
965 match wheel {
966 WheelWireSource::Url { url } => Some(Self::Url { url: url.clone() }),
967 WheelWireSource::Path { path } => Some(Self::Path {
968 path: normalize_workspace_relative_path(workspace_root, path),
969 }),
970 WheelWireSource::Filename { .. } => None,
972 }
973 }
974}
975
976#[derive(Clone, Debug, serde::Serialize)]
977struct MetadataZstdWheel {
978 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
979 hashes: BTreeMap<HashAlgorithm, Hash>,
980 #[serde(skip_serializing_if = "Option::is_none", default)]
981 size: Option<u64>,
982}
983
984impl MetadataZstdWheel {
985 fn from_wheel(wheel: &ZstdWheel) -> Self {
986 Self {
987 hashes: wheel.hash.as_ref().map(hashes_map).unwrap_or_default(),
988 size: wheel.size,
989 }
990 }
991}
992
993#[derive(Clone, Debug, serde::Serialize)]
994struct MetadataExtra {
995 name: ExtraName,
996 id: MetadataNodeIdFlat,
997}
998
999#[derive(Clone, Debug, serde::Serialize)]
1000struct MetadataGroup {
1001 name: GroupName,
1002 id: MetadataNodeIdFlat,
1003}
1004
1005#[derive(Clone, Debug, serde::Serialize)]
1006struct MetadataBuildSystem {
1007 build_backend: String,
1009 id: MetadataNodeIdFlat,
1010}
1011
1012#[derive(Clone, Debug, serde::Serialize)]
1014struct MetadataConflicts {
1015 sets: Vec<MetadataConflictSet>,
1016}
1017
1018impl MetadataConflicts {
1019 fn from_conflicts(
1020 members: &[MetadataWorkspaceMember],
1021 resolve: &BTreeMap<MetadataNodeIdFlat, MetadataNode>,
1022 conflicts: &Conflicts,
1023 ) -> Self {
1024 Self {
1025 sets: conflicts
1026 .iter()
1027 .map(|set| MetadataConflictSet::from_conflicts(members, resolve, set))
1028 .collect(),
1029 }
1030 }
1031}
1032
1033#[derive(Clone, Debug, serde::Serialize)]
1034struct MetadataConflictSet {
1035 items: Vec<MetadataConflictItem>,
1036}
1037
1038impl MetadataConflictSet {
1039 fn from_conflicts(
1040 members: &[MetadataWorkspaceMember],
1041 resolve: &BTreeMap<MetadataNodeIdFlat, MetadataNode>,
1042 set: &ConflictSet,
1043 ) -> Self {
1044 Self {
1045 items: set
1046 .iter()
1047 .map(|item| MetadataConflictItem::from_conflicts(members, resolve, item))
1048 .collect(),
1049 }
1050 }
1051}
1052
1053#[derive(Clone, Debug, serde::Serialize)]
1054struct MetadataConflictItem {
1055 package: PackageName,
1057 kind: MetadataConflictKind,
1058 id: Option<MetadataNodeIdFlat>,
1061}
1062
1063impl MetadataConflictItem {
1064 fn from_conflicts(
1065 members: &[MetadataWorkspaceMember],
1066 resolve: &BTreeMap<MetadataNodeIdFlat, MetadataNode>,
1067 item: &ConflictItem,
1068 ) -> Self {
1069 let kind = MetadataConflictKind::from_conflicts(item.kind());
1070 let id = members
1071 .iter()
1072 .find(|member| &member.name == item.package())
1073 .and_then(|member| {
1074 let package_id = resolve.get(&member.id)?.id.as_package()?;
1075 let id = MetadataNodeId::Package(MetadataPackageNodeId {
1076 kind: kind.to_node_kind(),
1077 ..package_id.clone()
1078 });
1079 Some(id.to_flat())
1080 });
1081 Self {
1082 package: item.package().clone(),
1083 kind,
1084 id,
1085 }
1086 }
1087}
1088
1089#[derive(Clone, Debug, serde::Serialize)]
1090enum MetadataConflictKind {
1091 Group(GroupName),
1092 Extra(ExtraName),
1093 Project,
1094}
1095
1096impl MetadataConflictKind {
1097 fn from_conflicts(item: &ConflictKind) -> Self {
1098 match item {
1099 ConflictKind::Extra(name) => Self::Extra(name.clone()),
1100 ConflictKind::Group(name) => Self::Group(name.clone()),
1101 ConflictKind::Project => Self::Project,
1102 }
1103 }
1104
1105 fn to_node_kind(&self) -> MetadataNodeKind {
1106 match self {
1107 Self::Group(name) => MetadataNodeKind::Group(name.clone()),
1108 Self::Extra(name) => MetadataNodeKind::Extra(name.clone()),
1109 Self::Project => MetadataNodeKind::Package,
1110 }
1111 }
1112}
1113
1114impl Metadata {
1115 pub fn from_lock(workspace: &Workspace, lock: &Lock) -> Result<Self, MetadataError> {
1117 Ok(Self::from_lock_target(
1118 workspace.install_path(),
1119 Some(workspace),
1120 None,
1121 lock,
1122 ))
1123 }
1124
1125 pub fn from_script(script_path: &Path, lock: &Lock) -> Result<Self, MetadataError> {
1127 let workspace_root = script_path.parent().unwrap_or_else(|| Path::new(""));
1128 Ok(Self::from_lock_target(
1129 workspace_root,
1130 None,
1131 Some(script_path),
1132 lock,
1133 ))
1134 }
1135
1136 fn from_lock_target(
1137 workspace_root: &Path,
1138 workspace: Option<&Workspace>,
1139 script_path: Option<&Path>,
1140 lock: &Lock,
1141 ) -> Self {
1142 let mut resolve = BTreeMap::new();
1143 let mut members = Vec::new();
1144 let workspace_root = PortablePathBuf::from(workspace_root);
1145 let reachability = metadata_reachability(&workspace_root, workspace, lock);
1146
1147 for lock_package in lock.packages() {
1148 let mut meta_package = MetadataNode::from_package_id(
1149 &workspace_root,
1150 &lock_package.id,
1151 MetadataNodeKind::Package,
1152 );
1153 let package_reachability = reachability
1154 .get(&meta_package.id.to_flat())
1155 .copied()
1156 .unwrap_or(MarkerTree::FALSE);
1157
1158 for dependency in &lock_package.dependencies {
1160 meta_package.add_dependency(&workspace_root, dependency, package_reachability);
1161 }
1162
1163 for (extra, dependencies) in &lock_package.optional_dependencies {
1165 let mut meta_extra = MetadataNode::from_package_id(
1166 &workspace_root,
1167 &lock_package.id,
1168 MetadataNodeKind::Extra(extra.clone()),
1169 );
1170 let extra_reachability = reachability
1171 .get(&meta_extra.id.to_flat())
1172 .copied()
1173 .unwrap_or(MarkerTree::FALSE);
1174 meta_extra.dependencies.push(MetadataDependency {
1176 id: meta_package.id.to_flat(),
1177 marker: None,
1178 });
1179 for dependency in dependencies {
1180 meta_extra.add_dependency(&workspace_root, dependency, extra_reachability);
1181 }
1182
1183 meta_package.optional_dependencies.push(MetadataExtra {
1184 name: extra.clone(),
1185 id: meta_extra.id.to_flat(),
1186 });
1187
1188 resolve.insert(meta_extra.id.to_flat(), meta_extra);
1189 }
1190
1191 for (group, dependencies) in &lock_package.dependency_groups {
1193 let mut meta_group = MetadataNode::from_package_id(
1194 &workspace_root,
1195 &lock_package.id,
1196 MetadataNodeKind::Group(group.clone()),
1197 );
1198 let group_reachability = reachability
1199 .get(&meta_group.id.to_flat())
1200 .copied()
1201 .unwrap_or(MarkerTree::FALSE);
1202 for dependency in dependencies {
1204 meta_group.add_dependency(&workspace_root, dependency, group_reachability);
1205 }
1206
1207 meta_package.dependency_groups.push(MetadataGroup {
1208 name: group.clone(),
1209 id: meta_group.id.to_flat(),
1210 });
1211
1212 resolve.insert(meta_group.id.to_flat(), meta_group);
1213 }
1214
1215 if let Some(workspace_package) =
1217 workspace.and_then(|workspace| workspace.packages().get(lock_package.name()))
1218 {
1219 let member = MetadataWorkspaceMember {
1220 name: lock_package.name().clone(),
1221 path: normalize_workspace_relative_path(
1222 &workspace_root,
1223 workspace_package.root().as_path(),
1224 ),
1225 id: meta_package.id.to_flat(),
1226 };
1227 members.push(member);
1228 }
1229
1230 if let Some(sdist) = &lock_package.sdist {
1232 meta_package.sdist = Some(MetadataSourceDist::from_sdist(&workspace_root, sdist));
1233 }
1234
1235 for wheel in &lock_package.wheels {
1236 meta_package
1237 .wheels
1238 .push(MetadataWheel::from_wheel(&workspace_root, wheel));
1239 }
1240
1241 resolve.insert(meta_package.id.to_flat(), meta_package);
1242 }
1243
1244 let script = script_path.map(|path| {
1245 let path = PortablePathBuf::from(path);
1246 let node = MetadataNode::from_script(
1247 path.clone(),
1248 root_dependencies(&workspace_root, lock, lock.requirements()),
1249 );
1250 let id = node.id.to_flat();
1251 resolve.insert(id.clone(), node);
1252 MetadataScript { path, id }
1253 });
1254
1255 let workspace_metadata = workspace.map(|_| {
1256 let mut dependency_groups = Vec::new();
1257 for (group, requirements) in lock.dependency_groups() {
1258 let node = MetadataNode::from_workspace_group(
1259 workspace_root.clone(),
1260 group.clone(),
1261 root_dependencies(&workspace_root, lock, requirements),
1262 );
1263 let id = node.id.to_flat();
1264 resolve.insert(id.clone(), node);
1265 dependency_groups.push(MetadataGroup {
1266 name: group.clone(),
1267 id,
1268 });
1269 }
1270
1271 let node = MetadataNode::from_workspace(workspace_root.clone(), dependency_groups);
1272 let id = node.id.to_flat();
1273 resolve.insert(id.clone(), node);
1274 MetadataWorkspace {
1275 path: workspace_root.clone(),
1276 id,
1277 }
1278 });
1279 let conflicts = MetadataConflicts::from_conflicts(&members, &resolve, &lock.conflicts);
1280
1281 Self {
1282 schema: SchemaReport {
1283 version: SchemaVersion::Preview,
1284 },
1285 conflicts,
1286 environment: None,
1287 script,
1288 workspace: workspace_metadata,
1289 module_owners: BTreeMap::new(),
1290 workspace_root,
1291 requires_python: lock.requires_python.clone(),
1292 members,
1293 resolution: resolve,
1294 }
1295 }
1296
1297 pub fn package_node_id(
1298 workspace_root: &PortablePathBuf,
1299 dist: &ResolvedDist,
1300 ) -> Result<String, MetadataError> {
1301 let source = Source::from_resolved_dist(dist, workspace_root.as_ref())?;
1302 Ok(MetadataNodeId::Package(MetadataPackageNodeId {
1303 name: dist.name().clone(),
1304 version: dist.version().cloned(),
1305 source: MetadataSource::from_source(workspace_root, source),
1306 kind: MetadataNodeKind::Package,
1307 })
1308 .to_flat())
1309 }
1310
1311 #[must_use]
1312 pub fn with_environment_root(mut self, environment_root: &Path) -> Self {
1313 self.environment = Some(MetadataEnvironment {
1314 root: PortablePathBuf::from(environment_root),
1315 });
1316 self
1317 }
1318
1319 #[must_use]
1320 pub fn with_module_owners(mut self, module_owners: BTreeMap<ModuleName, Vec<String>>) -> Self {
1321 self.module_owners = module_owners
1322 .into_iter()
1323 .filter_map(|(module, owners)| {
1324 let owners = owners
1325 .into_iter()
1326 .filter(|package_id| self.resolution.contains_key(package_id))
1327 .map(|package_id| MetadataModuleOwner { package_id })
1328 .collect::<Vec<_>>();
1329 (!owners.is_empty()).then_some((module, owners))
1330 })
1331 .collect();
1332 self
1333 }
1334
1335 pub fn to_json(&self) -> Result<String, MetadataError> {
1336 Ok(serde_json::to_string_pretty(self)?)
1337 }
1338}