1use std::collections::BTreeMap;
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_pypi_types::{ConflictItem, ConflictKind, ConflictSet, Conflicts, ModuleName};
11use uv_workspace::Workspace;
12
13use crate::lock::{
14 Dependency, DirectSource, PackageId, RegistrySource, Source, SourceDist, SourceDistMetadata,
15 Wheel, WheelWireSource, ZstdWheel,
16};
17use crate::{Lock, LockError};
18
19#[derive(Debug, thiserror::Error)]
20enum MetadataErrorKind {
21 #[error(transparent)]
22 Serialize(#[from] serde_json::error::Error),
23 #[error(transparent)]
24 Lock(#[from] LockError),
25}
26
27#[derive(Debug)]
28pub struct MetadataError {
29 kind: Box<MetadataErrorKind>,
30}
31
32impl std::error::Error for MetadataError {
33 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
34 self.kind.source()
35 }
36}
37
38impl std::fmt::Display for MetadataError {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 write!(f, "{}", self.kind)?;
41 Ok(())
42 }
43}
44
45impl<E> From<E> for MetadataError
46where
47 MetadataErrorKind: From<E>,
48{
49 fn from(err: E) -> Self {
50 Self {
51 kind: Box::new(MetadataErrorKind::from(err)),
52 }
53 }
54}
55
56#[derive(Debug, serde::Serialize)]
58pub struct Metadata {
59 schema: SchemaReport,
61 workspace_root: PortablePathBuf,
66 #[serde(skip_serializing_if = "Option::is_none", default)]
68 environment: Option<MetadataEnvironment>,
69 #[serde(skip_serializing_if = "Option::is_none", default)]
71 script: Option<MetadataScript>,
72 #[serde(skip_serializing_if = "Option::is_none", default)]
74 workspace: Option<MetadataWorkspace>,
75 requires_python: RequiresPython,
79 conflicts: MetadataConflicts,
81 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
83 module_owners: BTreeMap<ModuleName, Vec<MetadataModuleOwner>>,
84 #[serde(skip_serializing_if = "Vec::is_empty", default)]
88 members: Vec<MetadataWorkspaceMember>,
89 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
91 resolution: BTreeMap<MetadataNodeIdFlat, MetadataNode>,
92}
93
94#[derive(serde::Serialize, Debug, Default)]
96#[serde(rename_all = "snake_case")]
97enum SchemaVersion {
98 #[default]
100 Preview,
101}
102
103#[derive(serde::Serialize, Debug, Default)]
105struct SchemaReport {
106 version: SchemaVersion,
108}
109
110#[derive(Debug, serde::Serialize)]
112struct MetadataEnvironment {
113 root: PortablePathBuf,
115}
116
117#[derive(Debug, serde::Serialize)]
119struct MetadataScript {
120 path: PortablePathBuf,
122 id: MetadataNodeIdFlat,
124}
125
126#[derive(Debug, serde::Serialize)]
128struct MetadataWorkspace {
129 path: PortablePathBuf,
131 id: MetadataNodeIdFlat,
133}
134
135#[derive(Debug, serde::Serialize)]
137struct MetadataWorkspaceMember {
138 name: PackageName,
140 path: PortablePathBuf,
142 id: MetadataNodeIdFlat,
144}
145
146#[derive(Debug, serde::Serialize)]
148struct MetadataModuleOwner {
149 package_id: MetadataNodeIdFlat,
151}
152
153#[derive(Debug, Clone, serde::Serialize)]
200struct MetadataNode {
201 #[serde(flatten)]
203 id: MetadataNodeId,
204 dependencies: Vec<MetadataDependency>,
206 #[serde(skip_serializing_if = "Vec::is_empty", default)]
208 optional_dependencies: Vec<MetadataExtra>,
209 #[serde(skip_serializing_if = "Vec::is_empty", default)]
211 dependency_groups: Vec<MetadataGroup>,
212 #[serde(skip_serializing_if = "Option::is_none", default)]
214 build_system: Option<MetadataBuildSystem>,
215 #[serde(skip_serializing_if = "Option::is_none", default)]
217 sdist: Option<MetadataSourceDist>,
218 #[serde(skip_serializing_if = "Vec::is_empty", default)]
220 wheels: Vec<MetadataWheel>,
221}
222
223impl MetadataNode {
224 fn new(id: MetadataNodeId) -> Self {
225 Self {
226 id,
227 dependencies: Vec::new(),
228 dependency_groups: Vec::new(),
229 optional_dependencies: Vec::new(),
230 wheels: Vec::new(),
231 build_system: None,
232 sdist: None,
233 }
234 }
235
236 fn from_package_id(
237 workspace_root: &PortablePathBuf,
238 id: &PackageId,
239 kind: MetadataNodeKind,
240 ) -> Self {
241 Self::new(MetadataNodeId::from_package_id(workspace_root, id, kind))
242 }
243
244 fn from_script(path: PortablePathBuf, dependencies: Vec<MetadataDependency>) -> Self {
245 let mut node = Self::new(MetadataNodeId::Script(MetadataScriptNodeId {
246 kind: MetadataScriptNodeKind::Script,
247 path,
248 }));
249 node.dependencies = dependencies;
250 node
251 }
252
253 fn from_workspace(path: PortablePathBuf, dependency_groups: Vec<MetadataGroup>) -> Self {
254 let mut node = Self::new(MetadataNodeId::Workspace(MetadataWorkspaceNodeId {
255 kind: MetadataWorkspaceNodeKind::Workspace,
256 path,
257 }));
258 node.dependency_groups = dependency_groups;
259 node
260 }
261
262 fn from_workspace_group(
263 path: PortablePathBuf,
264 group: GroupName,
265 dependencies: Vec<MetadataDependency>,
266 ) -> Self {
267 let mut node = Self::new(MetadataNodeId::WorkspaceGroup(
268 MetadataWorkspaceGroupNodeId {
269 kind: MetadataWorkspaceGroupNodeKind::Group(group),
270 path,
271 },
272 ));
273 node.dependencies = dependencies;
274 node
275 }
276
277 fn add_dependency(&mut self, workspace_root: &PortablePathBuf, dependency: &Dependency) {
278 let extras = dependency.extra();
279 if extras.is_empty() {
280 let id = MetadataNodeId::from_package_id(
281 workspace_root,
282 &dependency.package_id,
283 MetadataNodeKind::Package,
284 );
285 self.dependencies.push(MetadataDependency {
286 id: id.to_flat(),
287 marker: dependency.simplified_marker.try_to_string(),
288 });
289 return;
290 }
291 for extra in extras {
292 let id = MetadataNodeId::from_package_id(
293 workspace_root,
294 &dependency.package_id,
295 MetadataNodeKind::Extra(extra.clone()),
296 );
297 self.dependencies.push(MetadataDependency {
298 id: id.to_flat(),
299 marker: dependency.simplified_marker.try_to_string(),
300 });
301 }
302 }
303}
304
305#[derive(Debug, Clone, serde::Serialize)]
306#[serde(rename_all = "snake_case")]
307enum MetadataWorkspaceNodeKind {
308 Workspace,
309}
310
311#[derive(Debug, Clone, serde::Serialize)]
312#[serde(rename_all = "snake_case")]
313enum MetadataWorkspaceGroupNodeKind {
314 Group(GroupName),
315}
316#[derive(Debug, Clone, serde::Serialize)]
317#[serde(rename_all = "snake_case")]
318enum MetadataScriptNodeKind {
319 Script,
320}
321
322fn root_dependencies<'lock>(
323 workspace_root: &PortablePathBuf,
324 lock: &'lock Lock,
325 requirements: impl IntoIterator<Item = &'lock Requirement>,
326) -> Vec<MetadataDependency> {
327 let mut dependencies = Vec::new();
328
329 for requirement in requirements {
332 for package in lock
333 .packages()
334 .iter()
335 .filter(|package| package.name() == &requirement.name)
336 {
337 let Some(marker) = lock.root_requirement_marker(requirement, package) else {
338 continue;
339 };
340
341 let marker = marker.try_to_string();
342 let mut has_extra_node = false;
343 for extra in requirement
344 .extras
345 .iter()
346 .filter(|extra| package.optional_dependencies.contains_key(*extra))
347 {
348 let id = MetadataNodeId::from_package_id(
349 workspace_root,
350 &package.id,
351 MetadataNodeKind::Extra(extra.clone()),
352 );
353 dependencies.push(MetadataDependency {
354 id: id.to_flat(),
355 marker: marker.clone(),
356 });
357 has_extra_node = true;
358 }
359
360 if !has_extra_node {
361 let id = MetadataNodeId::from_package_id(
362 workspace_root,
363 &package.id,
364 MetadataNodeKind::Package,
365 );
366 dependencies.push(MetadataDependency {
367 id: id.to_flat(),
368 marker,
369 });
370 }
371 }
372 }
373
374 dependencies
375}
376
377#[derive(Debug, Clone, serde::Serialize)]
379#[serde(untagged)]
380enum MetadataNodeId {
381 Package(MetadataPackageNodeId),
382 Script(MetadataScriptNodeId),
383 Workspace(MetadataWorkspaceNodeId),
384 WorkspaceGroup(MetadataWorkspaceGroupNodeId),
385}
386
387#[derive(Debug, Clone, serde::Serialize)]
391struct MetadataPackageNodeId {
392 name: PackageName,
394 #[serde(skip_serializing_if = "Option::is_none", default)]
396 version: Option<Version>,
397 source: MetadataSource,
399 kind: MetadataNodeKind,
401}
402
403#[derive(Debug, Clone, serde::Serialize)]
405struct MetadataScriptNodeId {
406 kind: MetadataScriptNodeKind,
407 path: PortablePathBuf,
409}
410
411#[derive(Debug, Clone, serde::Serialize)]
413struct MetadataWorkspaceNodeId {
414 kind: MetadataWorkspaceNodeKind,
415 path: PortablePathBuf,
417}
418
419#[derive(Debug, Clone, serde::Serialize)]
421struct MetadataWorkspaceGroupNodeId {
422 kind: MetadataWorkspaceGroupNodeKind,
423 path: PortablePathBuf,
425}
426
427type MetadataNodeIdFlat = String;
431
432impl MetadataNodeId {
433 fn from_package_id(
434 workspace_root: &PortablePathBuf,
435 id: &PackageId,
436 kind: MetadataNodeKind,
437 ) -> Self {
438 let name = id.name.clone();
439 let version = id.version.clone();
440 let source = MetadataSource::from_source(workspace_root, id.source.clone());
441
442 Self::Package(MetadataPackageNodeId {
443 name,
444 version,
445 source,
446 kind,
447 })
448 }
449
450 fn as_package(&self) -> Option<&MetadataPackageNodeId> {
451 match self {
452 Self::Package(package) => Some(package),
453 Self::Script(_) | Self::Workspace(_) | Self::WorkspaceGroup(_) => None,
454 }
455 }
456
457 fn to_flat(&self) -> MetadataNodeIdFlat {
458 self.to_string()
459 }
460}
461
462impl Display for MetadataNodeId {
463 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464 match self {
465 Self::Package(package) => match &package.version {
466 Some(version) => write!(
467 f,
468 "{}{}=={version}@{}",
469 package.name, package.kind, package.source
470 ),
471 None => write!(f, "{}{}@{}", package.name, package.kind, package.source),
472 },
473 Self::Script(script) => write!(f, "script+{}", script.path),
474 Self::Workspace(workspace) => write!(f, "workspace+{}", workspace.path),
475 Self::WorkspaceGroup(group) => {
476 let MetadataWorkspaceGroupNodeKind::Group(name) = &group.kind;
477 write!(f, "workspace+{}:{name}", group.path)
478 }
479 }
480 }
481}
482
483#[derive(Debug, Clone, serde::Serialize)]
484struct MetadataDependency {
485 id: MetadataNodeIdFlat,
486 #[serde(skip_serializing_if = "Option::is_none", default)]
487 marker: Option<MetadataMarker>,
488}
489
490type MetadataMarker = String;
491
492#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
494#[serde(rename_all = "snake_case")]
495enum MetadataNodeKind {
496 Package,
499 #[expect(dead_code)]
502 Build,
503 Extra(ExtraName),
506 Group(GroupName),
509}
510
511impl Display for MetadataNodeKind {
512 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
513 match self {
514 Self::Package => Ok(()),
516 Self::Build => f.write_str("(build)"),
517 Self::Extra(extra_name) => write!(f, "[{extra_name}]"),
518 Self::Group(group_name) => write!(f, ":{group_name}"),
519 }
520 }
521}
522
523#[derive(Clone, Debug, serde::Serialize)]
524#[serde(untagged, rename_all = "snake_case")]
525enum MetadataSource {
526 Registry {
527 registry: MetadataRegistrySource,
528 },
529 Git {
530 git: UrlString,
531 },
532 Direct {
533 url: UrlString,
534 subdirectory: Option<PortablePathBuf>,
535 },
536 Path {
537 path: PortablePathBuf,
538 },
539 Directory {
540 directory: PortablePathBuf,
541 },
542 Editable {
543 editable: PortablePathBuf,
544 },
545 Virtual {
546 r#virtual: PortablePathBuf,
547 },
548}
549
550impl Display for MetadataSource {
551 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
552 match self {
553 Self::Registry {
554 registry: MetadataRegistrySource::Url(url),
555 }
556 | Self::Git { git: url }
557 | Self::Direct { url, .. } => {
558 write!(f, "{}+{}", self.name(), url)
559 }
560 Self::Registry {
561 registry: MetadataRegistrySource::Path(path),
562 }
563 | Self::Path { path }
564 | Self::Directory { directory: path }
565 | Self::Editable { editable: path }
566 | Self::Virtual { r#virtual: path } => {
567 write!(f, "{}+{}", self.name(), path)
568 }
569 }
570 }
571}
572
573impl MetadataSource {
574 fn name(&self) -> &str {
575 match self {
576 Self::Registry { .. } => "registry",
577 Self::Git { .. } => "git",
578 Self::Direct { .. } => "direct",
579 Self::Path { .. } => "path",
580 Self::Directory { .. } => "directory",
581 Self::Editable { .. } => "editable",
582 Self::Virtual { .. } => "virtual",
583 }
584 }
585}
586
587impl MetadataSource {
588 fn from_source(workspace_root: &PortablePathBuf, source: Source) -> Self {
589 match source {
590 Source::Registry(source) => match source {
591 RegistrySource::Url(url) => Self::Registry {
592 registry: MetadataRegistrySource::Url(url),
593 },
594 RegistrySource::Path(path) => Self::Registry {
595 registry: MetadataRegistrySource::Path(normalize_workspace_relative_path(
596 workspace_root,
597 &path,
598 )),
599 },
600 },
601 Source::Git(url, _) => Self::Git { git: url },
602 Source::Direct(url, DirectSource { subdirectory }) => Self::Direct {
603 url,
604 subdirectory: subdirectory
605 .map(|path| normalize_workspace_relative_path(workspace_root, &path)),
606 },
607 Source::Path(path) => Self::Path {
608 path: normalize_workspace_relative_path(workspace_root, &path),
609 },
610 Source::Directory(path) => Self::Directory {
611 directory: normalize_workspace_relative_path(workspace_root, &path),
612 },
613 Source::Editable(path) => Self::Editable {
614 editable: normalize_workspace_relative_path(workspace_root, &path),
615 },
616 Source::Virtual(path) => Self::Virtual {
617 r#virtual: normalize_workspace_relative_path(workspace_root, &path),
618 },
619 }
620 }
621}
622
623fn normalize_workspace_relative_path(
624 workspace_root: &PortablePathBuf,
625 maybe_rel: &std::path::Path,
626) -> PortablePathBuf {
627 if maybe_rel.is_absolute() {
628 PortablePathBuf::from(maybe_rel)
629 } else {
630 PortablePathBuf::from(workspace_root.as_ref().join(maybe_rel).as_path())
631 }
632}
633
634#[derive(Clone, Debug, serde::Serialize)]
635#[serde(rename_all = "snake_case")]
636enum MetadataRegistrySource {
637 Url(UrlString),
639 Path(PortablePathBuf),
641}
642
643#[derive(Clone, Debug, serde::Serialize)]
644#[serde(untagged, rename_all = "snake_case")]
645enum MetadataSourceDist {
646 Url {
647 url: UrlString,
648 #[serde(flatten)]
649 metadata: MetadataSourceDistMetadata,
650 },
651 Path {
652 path: PortablePathBuf,
653 #[serde(flatten)]
654 metadata: MetadataSourceDistMetadata,
655 },
656 Metadata {
657 #[serde(flatten)]
658 metadata: MetadataSourceDistMetadata,
659 },
660}
661
662impl MetadataSourceDist {
663 fn from_sdist(workspace_root: &PortablePathBuf, sdist: &SourceDist) -> Self {
664 match sdist {
665 SourceDist::Url { url, metadata } => Self::Url {
666 url: url.clone(),
667 metadata: MetadataSourceDistMetadata::from_sdist(metadata),
668 },
669 SourceDist::Path { path, metadata } => Self::Path {
670 path: normalize_workspace_relative_path(workspace_root, path),
671 metadata: MetadataSourceDistMetadata::from_sdist(metadata),
672 },
673 SourceDist::Metadata { metadata } => Self::Metadata {
674 metadata: MetadataSourceDistMetadata::from_sdist(metadata),
675 },
676 }
677 }
678}
679
680#[derive(Clone, Debug, serde::Serialize)]
681#[serde(rename_all = "snake_case")]
682struct MetadataSourceDistMetadata {
683 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
685 hashes: BTreeMap<HashAlgorithm, Hash>,
686 #[serde(skip_serializing_if = "Option::is_none", default)]
690 size: Option<u64>,
691 #[serde(skip_serializing_if = "Option::is_none", default)]
693 upload_time: Option<jiff::Timestamp>,
694}
695
696type HashAlgorithm = String;
698type Hash = String;
700
701fn hashes_map(hash: &crate::lock::Hash) -> BTreeMap<HashAlgorithm, Hash> {
706 Some((hash.0.algorithm.to_string(), hash.0.digest.to_string()))
707 .into_iter()
708 .collect()
709}
710
711impl MetadataSourceDistMetadata {
712 fn from_sdist(sdist: &SourceDistMetadata) -> Self {
713 Self {
714 hashes: sdist.hash.as_ref().map(hashes_map).unwrap_or_default(),
715 size: sdist.size,
716 upload_time: sdist.upload_time,
717 }
718 }
719}
720#[derive(Clone, Debug, serde::Serialize)]
721struct MetadataWheel {
722 #[serde(flatten)]
727 source: Option<MetadataWheelWireSource>,
728 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
734 hashes: BTreeMap<HashAlgorithm, Hash>,
735 #[serde(skip_serializing_if = "Option::is_none", default)]
739 size: Option<u64>,
740 #[serde(skip_serializing_if = "Option::is_none", default)]
744 upload_time: Option<jiff::Timestamp>,
745 filename: WheelFilename,
752 #[serde(skip_serializing_if = "Option::is_none", default)]
754 zstd: Option<MetadataZstdWheel>,
755}
756
757impl MetadataWheel {
758 fn from_wheel(workspace_root: &PortablePathBuf, wheel: &Wheel) -> Self {
759 Self {
760 source: MetadataWheelWireSource::from_wheel(workspace_root, &wheel.url),
761 hashes: wheel.hash.as_ref().map(hashes_map).unwrap_or_default(),
762 size: wheel.size,
763 upload_time: wheel.upload_time,
764 filename: wheel.filename.clone(),
765 zstd: wheel.zstd.as_ref().map(MetadataZstdWheel::from_wheel),
766 }
767 }
768}
769
770#[derive(Clone, Debug, serde::Serialize)]
771#[serde(untagged, rename_all = "snake_case")]
772enum MetadataWheelWireSource {
773 Url { url: UrlString },
774 Path { path: PortablePathBuf },
775}
776
777impl MetadataWheelWireSource {
778 fn from_wheel(workspace_root: &PortablePathBuf, wheel: &WheelWireSource) -> Option<Self> {
779 match wheel {
780 WheelWireSource::Url { url } => Some(Self::Url { url: url.clone() }),
781 WheelWireSource::Path { path } => Some(Self::Path {
782 path: normalize_workspace_relative_path(workspace_root, path),
783 }),
784 WheelWireSource::Filename { .. } => None,
786 }
787 }
788}
789
790#[derive(Clone, Debug, serde::Serialize)]
791struct MetadataZstdWheel {
792 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
793 hashes: BTreeMap<HashAlgorithm, Hash>,
794 #[serde(skip_serializing_if = "Option::is_none", default)]
795 size: Option<u64>,
796}
797
798impl MetadataZstdWheel {
799 fn from_wheel(wheel: &ZstdWheel) -> Self {
800 Self {
801 hashes: wheel.hash.as_ref().map(hashes_map).unwrap_or_default(),
802 size: wheel.size,
803 }
804 }
805}
806
807#[derive(Clone, Debug, serde::Serialize)]
808struct MetadataExtra {
809 name: ExtraName,
810 id: MetadataNodeIdFlat,
811}
812
813#[derive(Clone, Debug, serde::Serialize)]
814struct MetadataGroup {
815 name: GroupName,
816 id: MetadataNodeIdFlat,
817}
818
819#[derive(Clone, Debug, serde::Serialize)]
820struct MetadataBuildSystem {
821 build_backend: String,
823 id: MetadataNodeIdFlat,
824}
825
826#[derive(Clone, Debug, serde::Serialize)]
828struct MetadataConflicts {
829 sets: Vec<MetadataConflictSet>,
830}
831
832impl MetadataConflicts {
833 fn from_conflicts(
834 members: &[MetadataWorkspaceMember],
835 resolve: &BTreeMap<MetadataNodeIdFlat, MetadataNode>,
836 conflicts: &Conflicts,
837 ) -> Self {
838 Self {
839 sets: conflicts
840 .iter()
841 .map(|set| MetadataConflictSet::from_conflicts(members, resolve, set))
842 .collect(),
843 }
844 }
845}
846
847#[derive(Clone, Debug, serde::Serialize)]
848struct MetadataConflictSet {
849 items: Vec<MetadataConflictItem>,
850}
851
852impl MetadataConflictSet {
853 fn from_conflicts(
854 members: &[MetadataWorkspaceMember],
855 resolve: &BTreeMap<MetadataNodeIdFlat, MetadataNode>,
856 set: &ConflictSet,
857 ) -> Self {
858 Self {
859 items: set
860 .iter()
861 .map(|item| MetadataConflictItem::from_conflicts(members, resolve, item))
862 .collect(),
863 }
864 }
865}
866
867#[derive(Clone, Debug, serde::Serialize)]
868struct MetadataConflictItem {
869 package: PackageName,
871 kind: MetadataConflictKind,
872 id: Option<MetadataNodeIdFlat>,
875}
876
877impl MetadataConflictItem {
878 fn from_conflicts(
879 members: &[MetadataWorkspaceMember],
880 resolve: &BTreeMap<MetadataNodeIdFlat, MetadataNode>,
881 item: &ConflictItem,
882 ) -> Self {
883 let kind = MetadataConflictKind::from_conflicts(item.kind());
884 let id = members
885 .iter()
886 .find(|member| &member.name == item.package())
887 .and_then(|member| {
888 let package_id = resolve.get(&member.id)?.id.as_package()?;
889 let id = MetadataNodeId::Package(MetadataPackageNodeId {
890 kind: kind.to_node_kind(),
891 ..package_id.clone()
892 });
893 Some(id.to_flat())
894 });
895 Self {
896 package: item.package().clone(),
897 kind,
898 id,
899 }
900 }
901}
902
903#[derive(Clone, Debug, serde::Serialize)]
904enum MetadataConflictKind {
905 Group(GroupName),
906 Extra(ExtraName),
907 Project,
908}
909
910impl MetadataConflictKind {
911 fn from_conflicts(item: &ConflictKind) -> Self {
912 match item {
913 ConflictKind::Extra(name) => Self::Extra(name.clone()),
914 ConflictKind::Group(name) => Self::Group(name.clone()),
915 ConflictKind::Project => Self::Project,
916 }
917 }
918
919 fn to_node_kind(&self) -> MetadataNodeKind {
920 match self {
921 Self::Group(name) => MetadataNodeKind::Group(name.clone()),
922 Self::Extra(name) => MetadataNodeKind::Extra(name.clone()),
923 Self::Project => MetadataNodeKind::Package,
924 }
925 }
926}
927
928impl Metadata {
929 pub fn from_lock(workspace: &Workspace, lock: &Lock) -> Result<Self, MetadataError> {
931 Ok(Self::from_lock_target(
932 workspace.install_path(),
933 Some(workspace),
934 None,
935 lock,
936 ))
937 }
938
939 pub fn from_script(script_path: &Path, lock: &Lock) -> Result<Self, MetadataError> {
941 let workspace_root = script_path.parent().unwrap_or_else(|| Path::new(""));
942 Ok(Self::from_lock_target(
943 workspace_root,
944 None,
945 Some(script_path),
946 lock,
947 ))
948 }
949
950 fn from_lock_target(
951 workspace_root: &Path,
952 workspace: Option<&Workspace>,
953 script_path: Option<&Path>,
954 lock: &Lock,
955 ) -> Self {
956 let mut resolve = BTreeMap::new();
957 let mut members = Vec::new();
958 let workspace_root = PortablePathBuf::from(workspace_root);
959
960 for lock_package in lock.packages() {
961 let mut meta_package = MetadataNode::from_package_id(
962 &workspace_root,
963 &lock_package.id,
964 MetadataNodeKind::Package,
965 );
966
967 for dependency in &lock_package.dependencies {
969 meta_package.add_dependency(&workspace_root, dependency);
970 }
971
972 for (extra, dependencies) in &lock_package.optional_dependencies {
974 let mut meta_extra = MetadataNode::from_package_id(
975 &workspace_root,
976 &lock_package.id,
977 MetadataNodeKind::Extra(extra.clone()),
978 );
979 meta_extra.dependencies.push(MetadataDependency {
981 id: meta_package.id.to_flat(),
982 marker: None,
983 });
984 for dependency in dependencies {
985 meta_extra.add_dependency(&workspace_root, dependency);
986 }
987
988 meta_package.optional_dependencies.push(MetadataExtra {
989 name: extra.clone(),
990 id: meta_extra.id.to_flat(),
991 });
992
993 resolve.insert(meta_extra.id.to_flat(), meta_extra);
994 }
995
996 for (group, dependencies) in &lock_package.dependency_groups {
998 let mut meta_group = MetadataNode::from_package_id(
999 &workspace_root,
1000 &lock_package.id,
1001 MetadataNodeKind::Group(group.clone()),
1002 );
1003 for dependency in dependencies {
1005 meta_group.add_dependency(&workspace_root, dependency);
1006 }
1007
1008 meta_package.dependency_groups.push(MetadataGroup {
1009 name: group.clone(),
1010 id: meta_group.id.to_flat(),
1011 });
1012
1013 resolve.insert(meta_group.id.to_flat(), meta_group);
1014 }
1015
1016 if let Some(workspace_package) =
1018 workspace.and_then(|workspace| workspace.packages().get(lock_package.name()))
1019 {
1020 let member = MetadataWorkspaceMember {
1021 name: lock_package.name().clone(),
1022 path: normalize_workspace_relative_path(
1023 &workspace_root,
1024 workspace_package.root().as_path(),
1025 ),
1026 id: meta_package.id.to_flat(),
1027 };
1028 members.push(member);
1029 }
1030
1031 if let Some(sdist) = &lock_package.sdist {
1033 meta_package.sdist = Some(MetadataSourceDist::from_sdist(&workspace_root, sdist));
1034 }
1035
1036 for wheel in &lock_package.wheels {
1037 meta_package
1038 .wheels
1039 .push(MetadataWheel::from_wheel(&workspace_root, wheel));
1040 }
1041
1042 resolve.insert(meta_package.id.to_flat(), meta_package);
1043 }
1044
1045 let script = script_path.map(|path| {
1046 let path = PortablePathBuf::from(path);
1047 let node = MetadataNode::from_script(
1048 path.clone(),
1049 root_dependencies(&workspace_root, lock, lock.requirements()),
1050 );
1051 let id = node.id.to_flat();
1052 resolve.insert(id.clone(), node);
1053 MetadataScript { path, id }
1054 });
1055
1056 let workspace_metadata = workspace.map(|_| {
1057 let mut dependency_groups = Vec::new();
1058 for (group, requirements) in lock.dependency_groups() {
1059 let node = MetadataNode::from_workspace_group(
1060 workspace_root.clone(),
1061 group.clone(),
1062 root_dependencies(&workspace_root, lock, requirements),
1063 );
1064 let id = node.id.to_flat();
1065 resolve.insert(id.clone(), node);
1066 dependency_groups.push(MetadataGroup {
1067 name: group.clone(),
1068 id,
1069 });
1070 }
1071
1072 let node = MetadataNode::from_workspace(workspace_root.clone(), dependency_groups);
1073 let id = node.id.to_flat();
1074 resolve.insert(id.clone(), node);
1075 MetadataWorkspace {
1076 path: workspace_root.clone(),
1077 id,
1078 }
1079 });
1080 let conflicts = MetadataConflicts::from_conflicts(&members, &resolve, &lock.conflicts);
1081
1082 Self {
1083 schema: SchemaReport {
1084 version: SchemaVersion::Preview,
1085 },
1086 conflicts,
1087 environment: None,
1088 script,
1089 workspace: workspace_metadata,
1090 module_owners: BTreeMap::new(),
1091 workspace_root,
1092 requires_python: lock.requires_python.clone(),
1093 members,
1094 resolution: resolve,
1095 }
1096 }
1097
1098 pub fn package_node_id(
1099 workspace_root: &PortablePathBuf,
1100 dist: &ResolvedDist,
1101 ) -> Result<String, MetadataError> {
1102 let source = Source::from_resolved_dist(dist, workspace_root.as_ref())?;
1103 Ok(MetadataNodeId::Package(MetadataPackageNodeId {
1104 name: dist.name().clone(),
1105 version: dist.version().cloned(),
1106 source: MetadataSource::from_source(workspace_root, source),
1107 kind: MetadataNodeKind::Package,
1108 })
1109 .to_flat())
1110 }
1111
1112 #[must_use]
1113 pub fn with_environment_root(mut self, environment_root: &Path) -> Self {
1114 self.environment = Some(MetadataEnvironment {
1115 root: PortablePathBuf::from(environment_root),
1116 });
1117 self
1118 }
1119
1120 #[must_use]
1121 pub fn with_module_owners(mut self, module_owners: BTreeMap<ModuleName, Vec<String>>) -> Self {
1122 self.module_owners = module_owners
1123 .into_iter()
1124 .filter_map(|(module, owners)| {
1125 let owners = owners
1126 .into_iter()
1127 .filter(|package_id| self.resolution.contains_key(package_id))
1128 .map(|package_id| MetadataModuleOwner { package_id })
1129 .collect::<Vec<_>>();
1130 (!owners.is_empty()).then_some((module, owners))
1131 })
1132 .collect();
1133 self
1134 }
1135
1136 pub fn to_json(&self) -> Result<String, MetadataError> {
1137 Ok(serde_json::to_string_pretty(self)?)
1138 }
1139}