1use std::borrow::Cow;
2use std::ffi::OsStr;
3use std::path::{Path, PathBuf};
4use std::str::FromStr;
5use std::sync::Arc;
6
7use jiff::Timestamp;
8use jiff::civil::{Date, DateTime, Time};
9use jiff::tz::{Offset, TimeZone};
10use petgraph::graph::NodeIndex;
11use serde::Deserialize;
12use toml_edit::{Array, ArrayOfTables, Item, Table, value};
13use url::Url;
14
15use uv_cache_key::RepositoryUrl;
16use uv_configuration::{
17 BuildOptions, DependencyGroupsWithDefaults, EditableMode, ExtrasSpecificationWithDefaults,
18 InstallOptions,
19};
20use uv_distribution_filename::{
21 BuildTag, DistExtension, ExtensionError, SourceDistExtension, SourceDistFilename,
22 SourceDistFilenameError, WheelFilename, WheelFilenameError,
23};
24use uv_distribution_types::{
25 BuiltDist, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist, Dist, Edge,
26 FileLocation, GitDirectorySourceDist, IndexUrl, Name, Node, PathBuiltDist, PathSourceDist,
27 RegistryBuiltDist, RegistryBuiltWheel, RegistrySourceDist, RemoteSource, RequiresPython,
28 Resolution, ResolvedDist, SourceDist, ToUrlError, UrlString,
29};
30use uv_fs::{PortablePathBuf, normalize_path, try_relative_to_if};
31use uv_git::{RepositoryReference, ResolvedRepositoryReference};
32use uv_git_types::{GitLfs, GitOid, GitReference, GitUrl, GitUrlParseError};
33use uv_normalize::{ExtraName, GroupName, PackageName};
34use uv_pep440::Version;
35use uv_pep508::{MarkerEnvironment, MarkerTree, VerbatimUrl};
36use uv_platform_tags::{TagCompatibility, TagPriority, Tags};
37use uv_pypi_types::{HashDigests, Hashes, ParsedGitDirectoryUrl, VcsKind};
38use uv_redacted::DisplaySafeUrl;
39use uv_small_str::SmallString;
40
41use crate::lock::export::ExportableRequirements;
42use crate::lock::{Source, WheelTagHint, each_element_on_its_line_array, is_wheel_unreachable};
43use crate::resolution::ResolutionGraphNode;
44use crate::{Installable, LockError, ResolverOutput};
45
46#[derive(Debug, thiserror::Error)]
47pub enum PylockTomlErrorKind {
48 #[error(
49 "Package `{0}` includes both a registry (`packages.wheels`) and a directory source (`packages.directory`)"
50 )]
51 WheelWithDirectory(PackageName),
52 #[error(
53 "Package `{0}` includes both a registry (`packages.wheels`) and a VCS source (`packages.vcs`)"
54 )]
55 WheelWithVcs(PackageName),
56 #[error(
57 "Package `{0}` includes both a registry (`packages.wheels`) and an archive source (`packages.archive`)"
58 )]
59 WheelWithArchive(PackageName),
60 #[error(
61 "Package `{0}` includes both a registry (`packages.sdist`) and a directory source (`packages.directory`)"
62 )]
63 SdistWithDirectory(PackageName),
64 #[error(
65 "Package `{0}` includes both a registry (`packages.sdist`) and a VCS source (`packages.vcs`)"
66 )]
67 SdistWithVcs(PackageName),
68 #[error(
69 "Package `{0}` includes both a registry (`packages.sdist`) and an archive source (`packages.archive`)"
70 )]
71 SdistWithArchive(PackageName),
72 #[error(
73 "Package `{0}` includes both a directory (`packages.directory`) and a VCS source (`packages.vcs`)"
74 )]
75 DirectoryWithVcs(PackageName),
76 #[error(
77 "Package `{0}` includes both a directory (`packages.directory`) and an archive source (`packages.archive`)"
78 )]
79 DirectoryWithArchive(PackageName),
80 #[error(
81 "Package `{0}` includes both a VCS (`packages.vcs`) and an archive source (`packages.archive`)"
82 )]
83 VcsWithArchive(PackageName),
84 #[error(
85 "Package `{0}` must include one of: `wheels`, `directory`, `archive`, `sdist`, or `vcs`"
86 )]
87 MissingSource(PackageName),
88 #[error("Package `{0}` uses a Git archive, which pylock.toml export does not support")]
89 GitArchiveUnsupported(PackageName),
90 #[error("Package `{0}` does not include a compatible wheel for the current platform")]
91 MissingWheel(PackageName),
92 #[error("`packages.wheel` entry for `{0}` must have a `path` or `url`")]
93 WheelMissingPathUrl(PackageName),
94 #[error("`packages.sdist` entry for `{0}` must have a `path` or `url`")]
95 SdistMissingPathUrl(PackageName),
96 #[error("`packages.archive` entry for `{0}` must have a `path` or `url`")]
97 ArchiveMissingPathUrl(PackageName),
98 #[error("`packages.vcs` entry for `{0}` must have a `url` or `path`")]
99 VcsMissingPathUrl(PackageName),
100 #[error("URL must end in a valid wheel filename: `{0}`")]
101 UrlMissingFilename(DisplaySafeUrl),
102 #[error("Path must end in a valid wheel filename: `{0}`")]
103 PathMissingFilename(Box<Path>),
104 #[error("Failed to convert path to URL")]
105 PathToUrl,
106 #[error("Failed to convert URL to path")]
107 UrlToPath,
108 #[error(
109 "Package `{0}` can't be installed because it doesn't have a source distribution or wheel for the current platform"
110 )]
111 NeitherSourceDistNorWheel(PackageName),
112 #[error(
113 "Package `{0}` can't be installed because it is marked as both `--no-binary` and `--no-build`"
114 )]
115 NoBinaryNoBuild(PackageName),
116 #[error(
117 "Package `{0}` can't be installed because it is marked as `--no-binary` but has no source distribution"
118 )]
119 NoBinary(PackageName),
120 #[error(
121 "Package `{0}` can't be installed because it is marked as `--no-build` but has no binary distribution"
122 )]
123 NoBuild(PackageName),
124 #[error(
125 "Package `{0}` can't be installed because the binary distribution is incompatible with the current platform"
126 )]
127 IncompatibleWheelOnly(PackageName),
128 #[error(
129 "Package `{0}` can't be installed because it is marked as `--no-binary` but is itself a binary distribution"
130 )]
131 NoBinaryWheelOnly(PackageName),
132 #[error(transparent)]
133 WheelFilename(#[from] WheelFilenameError),
134 #[error(transparent)]
135 SourceDistFilename(#[from] SourceDistFilenameError),
136 #[error(transparent)]
137 ToUrl(#[from] ToUrlError),
138 #[error(transparent)]
139 GitUrlParse(#[from] GitUrlParseError),
140 #[error(transparent)]
141 LockError(#[from] LockError),
142 #[error(transparent)]
143 Extension(#[from] ExtensionError),
144 #[error(transparent)]
145 Jiff(#[from] jiff::Error),
146 #[error(transparent)]
147 Io(#[from] std::io::Error),
148 #[error(transparent)]
149 Deserialize(#[from] toml::de::Error),
150}
151
152#[derive(Debug)]
153pub struct PylockTomlError {
154 kind: Box<PylockTomlErrorKind>,
155 hint: Option<WheelTagHint>,
156}
157
158impl std::error::Error for PylockTomlError {
159 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
160 self.kind.source()
161 }
162}
163
164impl std::fmt::Display for PylockTomlError {
165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 write!(f, "{}", self.kind)
167 }
168}
169
170impl uv_errors::Hint for PylockTomlError {
171 fn hints(&self) -> uv_errors::Hints<'_> {
172 if let Some(hint) = &self.hint {
173 uv_errors::Hints::from(hint.to_string())
174 } else {
175 uv_errors::Hints::none()
176 }
177 }
178}
179
180impl<E> From<E> for PylockTomlError
181where
182 PylockTomlErrorKind: From<E>,
183{
184 fn from(err: E) -> Self {
185 Self {
186 kind: Box::new(PylockTomlErrorKind::from(err)),
187 hint: None,
188 }
189 }
190}
191
192#[derive(Debug, serde::Serialize, serde::Deserialize)]
193#[serde(rename_all = "kebab-case")]
194pub struct PylockToml {
195 lock_version: Version,
196 created_by: String,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub requires_python: Option<RequiresPython>,
199 #[serde(skip_serializing_if = "Vec::is_empty", default)]
200 pub extras: Vec<ExtraName>,
201 #[serde(skip_serializing_if = "Vec::is_empty", default)]
202 pub dependency_groups: Vec<GroupName>,
203 #[serde(skip_serializing_if = "Vec::is_empty", default)]
204 pub default_groups: Vec<GroupName>,
205 #[serde(skip_serializing_if = "Vec::is_empty", default)]
206 pub packages: Vec<PylockTomlPackage>,
207 #[serde(skip_serializing_if = "Vec::is_empty", default)]
208 attestation_identities: Vec<PylockTomlAttestationIdentity>,
209}
210
211#[derive(Debug, serde::Serialize, serde::Deserialize)]
212#[serde(rename_all = "kebab-case")]
213pub struct PylockTomlPackage {
214 pub name: PackageName,
215 #[serde(skip_serializing_if = "Option::is_none")]
216 pub version: Option<Version>,
217 #[serde(skip_serializing_if = "Option::is_none")]
218 pub index: Option<DisplaySafeUrl>,
219 #[serde(
220 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
221 serialize_with = "uv_pep508::marker::ser::serialize",
222 default
223 )]
224 marker: MarkerTree,
225 #[serde(skip_serializing_if = "Option::is_none")]
226 requires_python: Option<RequiresPython>,
227 #[serde(skip_serializing_if = "Vec::is_empty", default)]
228 dependencies: Vec<PylockTomlDependency>,
229 #[serde(skip_serializing_if = "Option::is_none")]
230 vcs: Option<PylockTomlVcs>,
231 #[serde(skip_serializing_if = "Option::is_none")]
232 directory: Option<PylockTomlDirectory>,
233 #[serde(skip_serializing_if = "Option::is_none")]
234 archive: Option<PylockTomlArchive>,
235 #[serde(skip_serializing_if = "Option::is_none")]
236 sdist: Option<PylockTomlSdist>,
237 #[serde(skip_serializing_if = "Option::is_none")]
238 wheels: Option<Vec<PylockTomlWheel>>,
239}
240
241#[derive(Debug, serde::Serialize, serde::Deserialize)]
242#[serde(rename_all = "kebab-case")]
243#[expect(clippy::empty_structs_with_brackets)]
244struct PylockTomlDependency {}
245
246#[derive(Debug, serde::Serialize, serde::Deserialize)]
247#[serde(rename_all = "kebab-case")]
248struct PylockTomlDirectory {
249 path: PortablePathBuf,
250 #[serde(skip_serializing_if = "Option::is_none")]
251 editable: Option<bool>,
252 #[serde(skip_serializing_if = "Option::is_none")]
253 subdirectory: Option<PortablePathBuf>,
254}
255
256#[derive(Debug, serde::Serialize, serde::Deserialize)]
257#[serde(rename_all = "kebab-case")]
258struct PylockTomlVcs {
259 r#type: VcsKind,
260 #[serde(skip_serializing_if = "Option::is_none")]
261 url: Option<DisplaySafeUrl>,
262 #[serde(skip_serializing_if = "Option::is_none")]
263 path: Option<PortablePathBuf>,
264 #[serde(skip_serializing_if = "Option::is_none")]
265 requested_revision: Option<String>,
266 commit_id: GitOid,
267 #[serde(skip_serializing_if = "Option::is_none")]
268 subdirectory: Option<PortablePathBuf>,
269}
270
271#[derive(Debug, serde::Serialize, serde::Deserialize)]
272#[serde(rename_all = "kebab-case")]
273struct PylockTomlArchive {
274 #[serde(skip_serializing_if = "Option::is_none")]
275 url: Option<DisplaySafeUrl>,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 path: Option<PortablePathBuf>,
278 #[serde(skip_serializing_if = "Option::is_none")]
279 size: Option<u64>,
280 #[serde(
281 skip_serializing_if = "Option::is_none",
282 serialize_with = "timestamp_to_toml_datetime",
283 deserialize_with = "timestamp_from_toml_datetime",
284 default
285 )]
286 upload_time: Option<Timestamp>,
287 #[serde(skip_serializing_if = "Option::is_none")]
288 subdirectory: Option<PortablePathBuf>,
289 hashes: Hashes,
290}
291
292#[derive(Debug, serde::Serialize, serde::Deserialize)]
293#[serde(rename_all = "kebab-case")]
294struct PylockTomlSdist {
295 #[serde(skip_serializing_if = "Option::is_none")]
296 name: Option<SmallString>,
297 #[serde(skip_serializing_if = "Option::is_none")]
298 url: Option<DisplaySafeUrl>,
299 #[serde(skip_serializing_if = "Option::is_none")]
300 path: Option<PortablePathBuf>,
301 #[serde(
302 skip_serializing_if = "Option::is_none",
303 serialize_with = "timestamp_to_toml_datetime",
304 deserialize_with = "timestamp_from_toml_datetime",
305 default
306 )]
307 upload_time: Option<Timestamp>,
308 #[serde(skip_serializing_if = "Option::is_none")]
309 size: Option<u64>,
310 hashes: Hashes,
311}
312
313#[derive(Debug, serde::Serialize, serde::Deserialize)]
314#[serde(rename_all = "kebab-case")]
315struct PylockTomlWheel {
316 #[serde(skip_serializing_if = "Option::is_none")]
317 name: Option<WheelFilename>,
318 #[serde(skip_serializing_if = "Option::is_none")]
319 url: Option<DisplaySafeUrl>,
320 #[serde(skip_serializing_if = "Option::is_none")]
321 path: Option<PortablePathBuf>,
322 #[serde(
323 skip_serializing_if = "Option::is_none",
324 serialize_with = "timestamp_to_toml_datetime",
325 deserialize_with = "timestamp_from_toml_datetime",
326 default
327 )]
328 upload_time: Option<Timestamp>,
329 #[serde(skip_serializing_if = "Option::is_none")]
330 size: Option<u64>,
331 hashes: Hashes,
332}
333
334#[derive(Debug, serde::Serialize, serde::Deserialize)]
335#[serde(rename_all = "kebab-case")]
336struct PylockTomlAttestationIdentity {
337 kind: String,
338}
339
340impl<'lock> PylockToml {
341 pub fn from_resolution(
347 resolution: &ResolverOutput,
348 omit: &[PackageName],
349 install_path: &Path,
350 tags: Option<&Tags>,
351 build_options: &BuildOptions,
352 ) -> Result<Self, PylockTomlErrorKind> {
353 let lock_version = Version::new([1, 0]);
355
356 let created_by = "uv".to_string();
358
359 let requires_python = resolution.requires_python.clone();
361
362 let extras = vec![];
364
365 let dependency_groups = vec![];
367
368 let default_groups = vec![];
370
371 let attestation_identities = vec![];
373
374 let mut packages = Vec::with_capacity(resolution.graph.node_count());
376 for node_index in resolution.graph.node_indices() {
377 let ResolutionGraphNode::Dist(node) = &resolution.graph[node_index] else {
378 continue;
379 };
380 if !node.is_base() {
381 continue;
382 }
383 let ResolvedDist::Installable { dist, version } = &node.dist else {
384 continue;
385 };
386 if omit.contains(dist.name()) {
387 continue;
388 }
389
390 let version = version
392 .as_ref()
393 .filter(|_| !matches!(&**dist, Dist::Source(SourceDist::Directory(..))));
394
395 let mut package = PylockTomlPackage {
397 name: dist.name().clone(),
398 version: version.cloned(),
399 marker: node.marker.pep508(),
400 requires_python: None,
401 dependencies: vec![],
402 index: None,
403 vcs: None,
404 directory: None,
405 archive: None,
406 sdist: None,
407 wheels: None,
408 };
409
410 match &**dist {
411 Dist::Built(BuiltDist::DirectUrl(dist)) => {
412 package.archive = Some(PylockTomlArchive {
413 url: Some((*dist.location).clone()),
414 path: None,
415 size: dist.size(),
416 upload_time: None,
417 subdirectory: None,
418 hashes: Hashes::from(node.hashes.clone()),
419 });
420 }
421 Dist::Built(BuiltDist::Path(dist)) => {
422 let path = try_relative_to_if(
423 &dist.install_path,
424 install_path,
425 !dist.url.was_given_absolute(),
426 )
427 .map(Box::<Path>::from)
428 .unwrap_or_else(|_| dist.install_path.clone());
429 package.archive = Some(PylockTomlArchive {
430 url: None,
431 path: Some(PortablePathBuf::from(path)),
432 size: dist.size(),
433 upload_time: None,
434 subdirectory: None,
435 hashes: Hashes::from(node.hashes.clone()),
436 });
437 }
438 Dist::Built(BuiltDist::GitPath(_)) => {
439 return Err(PylockTomlErrorKind::GitArchiveUnsupported(package.name));
440 }
441 Dist::Built(BuiltDist::Registry(dist)) => {
442 package.wheels = Self::filter_and_convert_wheels(
443 resolution,
444 tags,
445 &requires_python,
446 node_index,
447 &dist.wheels,
448 build_options.no_binary_package(dist.name()),
449 )?;
450
451 let no_build = build_options.no_build_package(dist.name());
453
454 if !no_build {
455 if let Some(sdist) = dist.sdist.as_ref() {
456 let url = sdist
457 .file
458 .url
459 .to_url()
460 .map_err(PylockTomlErrorKind::ToUrl)?;
461 package.sdist = Some(PylockTomlSdist {
462 name: if url
464 .filename()
465 .is_ok_and(|filename| filename == *sdist.file.filename)
466 {
467 None
468 } else {
469 Some(sdist.file.filename.clone())
470 },
471 upload_time: sdist
472 .file
473 .upload_time_utc_ms
474 .map(Timestamp::from_millisecond)
475 .transpose()?,
476 url: Some(url),
477 path: None,
478 size: sdist.file.size,
479 hashes: Hashes::from(sdist.file.hashes.clone()),
480 });
481 }
482 }
483 }
484 Dist::Source(SourceDist::DirectUrl(dist)) => {
485 package.archive = Some(PylockTomlArchive {
486 url: Some((*dist.location).clone()),
487 path: None,
488 size: dist.size(),
489 upload_time: None,
490 subdirectory: dist.subdirectory.clone().map(PortablePathBuf::from),
491 hashes: Hashes::from(node.hashes.clone()),
492 });
493 }
494 Dist::Source(SourceDist::Directory(dist)) => {
495 let path = try_relative_to_if(
496 &dist.install_path,
497 install_path,
498 !dist.url.was_given_absolute(),
499 )
500 .map(Box::<Path>::from)
501 .unwrap_or_else(|_| dist.install_path.clone());
502 package.directory = Some(PylockTomlDirectory {
503 path: PortablePathBuf::from(path),
504 editable: dist.editable,
505 subdirectory: None,
506 });
507 }
508 Dist::Source(SourceDist::GitDirectory(dist)) => {
509 package.vcs = Some(PylockTomlVcs {
510 r#type: VcsKind::Git,
511 url: Some(dist.git.url().clone()),
512 path: None,
513 requested_revision: dist.git.reference().as_str().map(ToString::to_string),
514 commit_id: dist.git.precise().unwrap_or_else(|| {
515 panic!("Git distribution is missing a precise hash: {dist}")
516 }),
517 subdirectory: dist.subdirectory.clone().map(PortablePathBuf::from),
518 });
519 }
520 Dist::Source(SourceDist::GitPath(_)) => {
521 return Err(PylockTomlErrorKind::GitArchiveUnsupported(package.name));
522 }
523 Dist::Source(SourceDist::Path(dist)) => {
524 let path = try_relative_to_if(
525 &dist.install_path,
526 install_path,
527 !dist.url.was_given_absolute(),
528 )
529 .map(Box::<Path>::from)
530 .unwrap_or_else(|_| dist.install_path.clone());
531 package.archive = Some(PylockTomlArchive {
532 url: None,
533 path: Some(PortablePathBuf::from(path)),
534 size: dist.size(),
535 upload_time: None,
536 subdirectory: None,
537 hashes: Hashes::from(node.hashes.clone()),
538 });
539 }
540 Dist::Source(SourceDist::Registry(dist)) => {
541 package.wheels = Self::filter_and_convert_wheels(
542 resolution,
543 tags,
544 &requires_python,
545 node_index,
546 &dist.wheels,
547 build_options.no_binary_package(&dist.name),
548 )?;
549
550 let no_build = build_options.no_build_package(&dist.name);
552
553 if !no_build {
554 let url = dist.file.url.to_url().map_err(PylockTomlErrorKind::ToUrl)?;
555 package.sdist = Some(PylockTomlSdist {
556 name: if url
558 .filename()
559 .is_ok_and(|filename| filename == *dist.file.filename)
560 {
561 None
562 } else {
563 Some(dist.file.filename.clone())
564 },
565 upload_time: dist
566 .file
567 .upload_time_utc_ms
568 .map(Timestamp::from_millisecond)
569 .transpose()?,
570 url: Some(url),
571 path: None,
572 size: dist.file.size,
573 hashes: Hashes::from(dist.file.hashes.clone()),
574 });
575 }
576 }
577 }
578
579 packages.push(package);
581 }
582
583 packages.sort_by(|a, b| a.name.cmp(&b.name).then(a.version.cmp(&b.version)));
585
586 Ok(Self {
588 lock_version,
589 created_by,
590 requires_python: Some(requires_python),
591 extras,
592 dependency_groups,
593 default_groups,
594 packages,
595 attestation_identities,
596 })
597 }
598
599 fn filter_and_convert_wheels(
604 resolution: &ResolverOutput,
605 tags: Option<&Tags>,
606 requires_python: &RequiresPython,
607 node_index: NodeIndex,
608 wheels: &[RegistryBuiltWheel],
609 no_binary: bool,
610 ) -> Result<Option<Vec<PylockTomlWheel>>, PylockTomlErrorKind> {
611 if no_binary {
612 return Ok(None);
613 }
614
615 let wheels: Vec<_> = wheels
617 .iter()
618 .filter(|wheel| {
619 !is_wheel_unreachable(
620 &wheel.filename,
621 resolution,
622 requires_python,
623 node_index,
624 tags,
625 )
626 })
627 .collect();
628
629 if wheels.is_empty() {
630 return Ok(None);
631 }
632
633 let wheels = wheels
634 .into_iter()
635 .map(|wheel| {
636 let url = wheel
637 .file
638 .url
639 .to_url()
640 .map_err(PylockTomlErrorKind::ToUrl)?;
641 Ok(PylockTomlWheel {
642 name: if url
644 .filename()
645 .is_ok_and(|filename| filename == *wheel.file.filename)
646 {
647 None
648 } else {
649 Some(wheel.filename.clone())
650 },
651 upload_time: wheel
652 .file
653 .upload_time_utc_ms
654 .map(Timestamp::from_millisecond)
655 .transpose()?,
656 url: Some(
657 wheel
658 .file
659 .url
660 .to_url()
661 .map_err(PylockTomlErrorKind::ToUrl)?,
662 ),
663 path: None,
664 size: wheel.file.size,
665 hashes: Hashes::from(wheel.file.hashes.clone()),
666 })
667 })
668 .collect::<Result<Vec<_>, PylockTomlErrorKind>>()?;
669 Ok(Some(wheels))
670 }
671
672 pub fn from_lock(
674 target: &impl Installable<'lock>,
675 prune: &[PackageName],
676 extras: &ExtrasSpecificationWithDefaults,
677 dev: &DependencyGroupsWithDefaults,
678 annotate: bool,
679 editable: Option<&EditableMode>,
680 install_options: &'lock InstallOptions,
681 ) -> Result<Self, PylockTomlErrorKind> {
682 let ExportableRequirements(mut nodes) = ExportableRequirements::from_lock(
684 target,
685 prune,
686 extras,
687 dev,
688 annotate,
689 install_options,
690 )?;
691
692 nodes.sort_unstable_by_key(|node| &node.package.id);
694
695 let lock_version = Version::new([1, 0]);
697
698 let created_by = "uv".to_string();
700
701 let requires_python = target.lock().requires_python.clone();
703
704 let extras = vec![];
706
707 let dependency_groups = vec![];
709
710 let default_groups = vec![];
712
713 let attestation_identities = vec![];
715
716 let mut packages = Vec::with_capacity(nodes.len());
718 for node in nodes {
719 let package = node.package;
720
721 let wheels = match &package.id.source {
726 Source::Registry(source) => {
727 let wheels = package
728 .wheels
729 .iter()
730 .map(|wheel| wheel.to_registry_wheel(source, target.install_path()))
731 .collect::<Result<Vec<RegistryBuiltWheel>, LockError>>()?;
732 Some(
733 wheels
734 .into_iter()
735 .map(|wheel| {
736 let url = wheel
737 .file
738 .url
739 .to_url()
740 .map_err(PylockTomlErrorKind::ToUrl)?;
741 Ok(PylockTomlWheel {
742 name: if url
744 .filename()
745 .is_ok_and(|filename| filename == *wheel.file.filename)
746 {
747 None
748 } else {
749 Some(wheel.filename.clone())
750 },
751 upload_time: wheel
752 .file
753 .upload_time_utc_ms
754 .map(Timestamp::from_millisecond)
755 .transpose()?,
756 url: Some(url),
757 path: None,
758 size: wheel.file.size,
759 hashes: Hashes::from(wheel.file.hashes),
760 })
761 })
762 .collect::<Result<Vec<_>, PylockTomlErrorKind>>()?,
763 )
764 }
765 Source::Path(..) => None,
766 Source::Git(..) => None,
767 Source::Direct(..) => None,
768 Source::Directory(..) => None,
769 Source::Editable(..) => None,
770 Source::Virtual(..) => {
771 continue;
773 }
774 };
775
776 let sdist = package.to_source_dist(target.install_path())?;
778
779 let size = package
781 .sdist
782 .as_ref()
783 .and_then(super::super::SourceDist::size);
784 let hash = package.sdist.as_ref().and_then(|sdist| sdist.hash());
785
786 let directory = match &sdist {
788 Some(SourceDist::Directory(sdist)) => Some(PylockTomlDirectory {
789 path: PortablePathBuf::from(
790 sdist
791 .url
792 .given()
793 .map(PathBuf::from)
794 .unwrap_or_else(|| sdist.install_path.to_path_buf())
795 .into_boxed_path(),
796 ),
797 editable: match editable
798 .and_then(|editable| editable.for_package(&package.id.name))
799 {
800 None => sdist.editable,
801 Some(false) => None,
802 Some(true) => Some(true),
803 },
804 subdirectory: None,
805 }),
806 _ => None,
807 };
808
809 let vcs = match &sdist {
811 Some(SourceDist::GitDirectory(sdist)) => Some(PylockTomlVcs {
812 r#type: VcsKind::Git,
813 url: Some(sdist.git.url().clone()),
814 path: None,
815 requested_revision: sdist.git.reference().as_str().map(ToString::to_string),
816 commit_id: sdist.git.precise().unwrap_or_else(|| {
817 panic!("Git distribution is missing a precise hash: {sdist}")
818 }),
819 subdirectory: sdist.subdirectory.clone().map(PortablePathBuf::from),
820 }),
821 _ => None,
822 };
823
824 let archive = match &sdist {
827 Some(SourceDist::DirectUrl(sdist)) => Some(PylockTomlArchive {
828 url: Some(sdist.url.to_url()),
829 path: None,
830 size,
831 upload_time: None,
832 subdirectory: sdist.subdirectory.clone().map(PortablePathBuf::from),
833 hashes: hash.cloned().map(Hashes::from).unwrap_or_default(),
834 }),
835 Some(SourceDist::Path(sdist)) => Some(PylockTomlArchive {
836 url: None,
837 path: Some(PortablePathBuf::from(
838 sdist
839 .url
840 .given()
841 .map(PathBuf::from)
842 .unwrap_or_else(|| sdist.install_path.to_path_buf())
843 .into_boxed_path(),
844 )),
845 size,
846 upload_time: None,
847 subdirectory: None,
848 hashes: hash.cloned().map(Hashes::from).unwrap_or_default(),
849 }),
850 _ => match &package.id.source {
851 Source::Registry(..) => None,
852 Source::Path(source) => package.wheels.first().map(|wheel| PylockTomlArchive {
853 url: None,
854 path: Some(PortablePathBuf::from(source.clone())),
855 size: wheel.size,
856 upload_time: None,
857 subdirectory: None,
858 hashes: wheel.hash.clone().map(Hashes::from).unwrap_or_default(),
859 }),
860 Source::Git(..) => None,
861 Source::Direct(source, ..) => {
862 if let Some(wheel) = package.wheels.first() {
863 Some(PylockTomlArchive {
864 url: Some(source.to_url()?),
865 path: None,
866 size: wheel.size,
867 upload_time: None,
868 subdirectory: None,
869 hashes: wheel.hash.clone().map(Hashes::from).unwrap_or_default(),
870 })
871 } else {
872 None
873 }
874 }
875 Source::Directory(..) => None,
876 Source::Editable(..) => None,
877 Source::Virtual(..) => None,
878 },
879 };
880
881 let sdist = match &sdist {
883 Some(SourceDist::Registry(sdist)) => {
884 let url = sdist
885 .file
886 .url
887 .to_url()
888 .map_err(PylockTomlErrorKind::ToUrl)?;
889 Some(PylockTomlSdist {
890 name: if url
892 .filename()
893 .is_ok_and(|filename| filename == *sdist.file.filename)
894 {
895 None
896 } else {
897 Some(sdist.file.filename.clone())
898 },
899 upload_time: sdist
900 .file
901 .upload_time_utc_ms
902 .map(Timestamp::from_millisecond)
903 .transpose()?,
904 url: Some(url),
905 path: None,
906 size,
907 hashes: hash.cloned().map(Hashes::from).unwrap_or_default(),
908 })
909 }
910 _ => None,
911 };
912
913 let index = package
915 .index(target.install_path())?
916 .map(IndexUrl::into_url);
917
918 let name = package.id.name.clone();
920
921 let version = package
924 .id
925 .version
926 .as_ref()
927 .filter(|_| directory.is_none())
928 .cloned();
929
930 let package = PylockTomlPackage {
931 name,
932 version,
933 marker: node.marker,
934 requires_python: None,
935 dependencies: vec![],
936 index,
937 vcs,
938 directory,
939 archive,
940 sdist,
941 wheels,
942 };
943
944 packages.push(package);
945 }
946
947 Ok(Self {
948 lock_version,
949 created_by,
950 requires_python: Some(requires_python),
951 extras,
952 dependency_groups,
953 default_groups,
954 packages,
955 attestation_identities,
956 })
957 }
958
959 pub fn to_toml(&self) -> Result<String, toml_edit::ser::Error> {
961 let mut doc = toml_edit::DocumentMut::new();
964
965 doc.insert("lock-version", value(self.lock_version.to_string()));
966 doc.insert("created-by", value(self.created_by.as_str()));
967 if let Some(ref requires_python) = self.requires_python {
968 doc.insert("requires-python", value(requires_python.to_string()));
969 }
970 if !self.extras.is_empty() {
971 doc.insert(
972 "extras",
973 value(each_element_on_its_line_array(
974 self.extras.iter().map(ToString::to_string),
975 )),
976 );
977 }
978 if !self.dependency_groups.is_empty() {
979 doc.insert(
980 "dependency-groups",
981 value(each_element_on_its_line_array(
982 self.dependency_groups.iter().map(ToString::to_string),
983 )),
984 );
985 }
986 if !self.default_groups.is_empty() {
987 doc.insert(
988 "default-groups",
989 value(each_element_on_its_line_array(
990 self.default_groups.iter().map(ToString::to_string),
991 )),
992 );
993 }
994 if !self.attestation_identities.is_empty() {
995 let attestation_identities = self
996 .attestation_identities
997 .iter()
998 .map(|attestation_identity| {
999 serde::Serialize::serialize(
1000 &attestation_identity,
1001 toml_edit::ser::ValueSerializer::new(),
1002 )
1003 })
1004 .collect::<Result<Vec<_>, _>>()?;
1005 let attestation_identities = match attestation_identities.as_slice() {
1006 [] => Array::new(),
1007 [attestation_identity] => Array::from_iter([attestation_identity]),
1008 attestation_identities => {
1009 each_element_on_its_line_array(attestation_identities.iter())
1010 }
1011 };
1012 doc.insert("attestation-identities", value(attestation_identities));
1013 }
1014 if !self.packages.is_empty() {
1015 let mut packages = ArrayOfTables::new();
1016 for dist in &self.packages {
1017 packages.push(dist.to_toml()?);
1018 }
1019 doc.insert("packages", Item::ArrayOfTables(packages));
1020 }
1021
1022 Ok(doc.to_string())
1023 }
1024
1025 pub fn to_resolution(
1027 self,
1028 install_path: &Path,
1029 markers: &MarkerEnvironment,
1030 extras: &[ExtraName],
1031 groups: &[GroupName],
1032 tags: &Tags,
1033 build_options: &BuildOptions,
1034 ) -> Result<Resolution, PylockTomlError> {
1035 let mut graph =
1037 petgraph::graph::DiGraph::with_capacity(self.packages.len(), self.packages.len());
1038
1039 let root = graph.add_node(Node::Root);
1041
1042 for package in self.packages {
1043 if !package.marker.evaluate_pep751(markers, extras, groups) {
1045 continue;
1046 }
1047
1048 match (
1049 package.wheels.is_some(),
1050 package.sdist.is_some(),
1051 package.directory.is_some(),
1052 package.vcs.is_some(),
1053 package.archive.is_some(),
1054 ) {
1055 (true, _, true, _, _) => {
1057 return Err(
1058 PylockTomlErrorKind::WheelWithDirectory(package.name.clone()).into(),
1059 );
1060 }
1061 (true, _, _, true, _) => {
1062 return Err(PylockTomlErrorKind::WheelWithVcs(package.name.clone()).into());
1063 }
1064 (true, _, _, _, true) => {
1065 return Err(PylockTomlErrorKind::WheelWithArchive(package.name.clone()).into());
1066 }
1067 (_, true, true, _, _) => {
1069 return Err(
1070 PylockTomlErrorKind::SdistWithDirectory(package.name.clone()).into(),
1071 );
1072 }
1073 (_, true, _, true, _) => {
1074 return Err(PylockTomlErrorKind::SdistWithVcs(package.name.clone()).into());
1075 }
1076 (_, true, _, _, true) => {
1077 return Err(PylockTomlErrorKind::SdistWithArchive(package.name.clone()).into());
1078 }
1079 (_, _, true, true, _) => {
1081 return Err(PylockTomlErrorKind::DirectoryWithVcs(package.name.clone()).into());
1082 }
1083 (_, _, true, _, true) => {
1084 return Err(
1085 PylockTomlErrorKind::DirectoryWithArchive(package.name.clone()).into(),
1086 );
1087 }
1088 (_, _, _, true, true) => {
1090 return Err(PylockTomlErrorKind::VcsWithArchive(package.name.clone()).into());
1091 }
1092 (false, false, false, false, false) => {
1093 return Err(PylockTomlErrorKind::MissingSource(package.name.clone()).into());
1094 }
1095 _ => {}
1096 }
1097
1098 let no_binary = build_options.no_binary_package(&package.name);
1099 let no_build = build_options.no_build_package(&package.name);
1100 let is_wheel = package
1101 .archive
1102 .as_ref()
1103 .map(|archive| archive.is_wheel(&package.name))
1104 .transpose()?
1105 .unwrap_or_default();
1106
1107 let dist = if let Some(best_wheel) =
1109 package.find_best_wheel(tags).filter(|_| !no_binary)
1110 {
1111 let hashes = HashDigests::from(best_wheel.hashes.clone());
1112 let built_dist = Dist::Built(BuiltDist::Registry(RegistryBuiltDist {
1113 wheels: vec![best_wheel.to_registry_wheel(
1114 install_path,
1115 &package.name,
1116 package.index.as_ref(),
1117 )?],
1118 best_wheel_index: 0,
1119 sdist: None,
1120 }));
1121 let dist = ResolvedDist::Installable {
1122 dist: Arc::new(built_dist),
1123 version: package.version,
1124 };
1125 Node::Dist {
1126 dist,
1127 hashes,
1128 install: true,
1129 }
1130 } else if let Some(sdist) = package.sdist.as_ref().filter(|_| !no_build) {
1131 let hashes = HashDigests::from(sdist.hashes.clone());
1132 let sdist = Dist::Source(SourceDist::Registry(sdist.to_sdist(
1133 install_path,
1134 &package.name,
1135 package.version.as_ref(),
1136 package.index.as_ref(),
1137 )?));
1138 let dist = ResolvedDist::Installable {
1139 dist: Arc::new(sdist),
1140 version: package.version,
1141 };
1142 Node::Dist {
1143 dist,
1144 hashes,
1145 install: true,
1146 }
1147 } else if let Some(sdist) = package.directory.as_ref().filter(|_| !no_build) {
1148 let hashes = HashDigests::empty();
1149 let sdist = Dist::Source(SourceDist::Directory(
1150 sdist.to_sdist(install_path, &package.name)?,
1151 ));
1152 let dist = ResolvedDist::Installable {
1153 dist: Arc::new(sdist),
1154 version: package.version,
1155 };
1156 Node::Dist {
1157 dist,
1158 hashes,
1159 install: true,
1160 }
1161 } else if let Some(sdist) = package.vcs.as_ref().filter(|_| !no_build) {
1162 let hashes = HashDigests::empty();
1163 let sdist = Dist::Source(SourceDist::GitDirectory(
1164 sdist.to_sdist(install_path, &package.name)?,
1165 ));
1166 let dist = ResolvedDist::Installable {
1167 dist: Arc::new(sdist),
1168 version: package.version,
1169 };
1170 Node::Dist {
1171 dist,
1172 hashes,
1173 install: true,
1174 }
1175 } else if let Some(dist) = package
1176 .archive
1177 .as_ref()
1178 .filter(|_| if is_wheel { !no_binary } else { !no_build })
1179 {
1180 let hashes = HashDigests::from(dist.hashes.clone());
1181 let dist = dist.to_dist(install_path, &package.name, package.version.as_ref())?;
1182 let dist = ResolvedDist::Installable {
1183 dist: Arc::new(dist),
1184 version: package.version,
1185 };
1186 Node::Dist {
1187 dist,
1188 hashes,
1189 install: true,
1190 }
1191 } else {
1192 return match (no_binary, no_build) {
1193 (true, true) => {
1194 Err(PylockTomlErrorKind::NoBinaryNoBuild(package.name.clone()).into())
1195 }
1196 (true, false) if is_wheel => {
1197 Err(PylockTomlErrorKind::NoBinaryWheelOnly(package.name.clone()).into())
1198 }
1199 (true, false) => {
1200 Err(PylockTomlErrorKind::NoBinary(package.name.clone()).into())
1201 }
1202 (false, true) => Err(PylockTomlErrorKind::NoBuild(package.name.clone()).into()),
1203 (false, false) if is_wheel => Err(PylockTomlError {
1204 kind: Box::new(PylockTomlErrorKind::IncompatibleWheelOnly(
1205 package.name.clone(),
1206 )),
1207 hint: package.tag_hint(tags, markers),
1208 }),
1209 (false, false) => Err(PylockTomlError {
1210 kind: Box::new(PylockTomlErrorKind::NeitherSourceDistNorWheel(
1211 package.name.clone(),
1212 )),
1213 hint: package.tag_hint(tags, markers),
1214 }),
1215 };
1216 };
1217
1218 let index = graph.add_node(dist);
1219 graph.add_edge(root, index, Edge::Prod);
1220 }
1221
1222 Ok(Resolution::new(graph))
1223 }
1224}
1225
1226impl PylockTomlPackage {
1227 fn to_toml(&self) -> Result<Table, toml_edit::ser::Error> {
1229 let mut table = Table::new();
1230 table.insert("name", value(self.name.to_string()));
1231 if let Some(ref version) = self.version {
1232 table.insert("version", value(version.to_string()));
1233 }
1234 if let Some(marker) = self.marker.try_to_string() {
1235 table.insert("marker", value(marker));
1236 }
1237 if let Some(ref requires_python) = self.requires_python {
1238 table.insert("requires-python", value(requires_python.to_string()));
1239 }
1240 if !self.dependencies.is_empty() {
1241 let dependencies = self
1242 .dependencies
1243 .iter()
1244 .map(|dependency| {
1245 serde::Serialize::serialize(&dependency, toml_edit::ser::ValueSerializer::new())
1246 })
1247 .collect::<Result<Vec<_>, _>>()?;
1248 let dependencies = match dependencies.as_slice() {
1249 [] => Array::new(),
1250 [dependency] => Array::from_iter([dependency]),
1251 dependencies => each_element_on_its_line_array(dependencies.iter()),
1252 };
1253 table.insert("dependencies", value(dependencies));
1254 }
1255 if let Some(ref index) = self.index {
1256 table.insert("index", value(index.to_string()));
1257 }
1258 if let Some(ref vcs) = self.vcs {
1259 table.insert(
1260 "vcs",
1261 value(serde::Serialize::serialize(
1262 &vcs,
1263 toml_edit::ser::ValueSerializer::new(),
1264 )?),
1265 );
1266 }
1267 if let Some(ref directory) = self.directory {
1268 table.insert(
1269 "directory",
1270 value(serde::Serialize::serialize(
1271 &directory,
1272 toml_edit::ser::ValueSerializer::new(),
1273 )?),
1274 );
1275 }
1276 if let Some(ref archive) = self.archive {
1277 table.insert(
1278 "archive",
1279 value(serde::Serialize::serialize(
1280 &archive,
1281 toml_edit::ser::ValueSerializer::new(),
1282 )?),
1283 );
1284 }
1285 if let Some(ref sdist) = self.sdist {
1286 table.insert(
1287 "sdist",
1288 value(serde::Serialize::serialize(
1289 &sdist,
1290 toml_edit::ser::ValueSerializer::new(),
1291 )?),
1292 );
1293 }
1294 if let Some(wheels) = self.wheels.as_ref().filter(|wheels| !wheels.is_empty()) {
1295 let wheels = wheels
1296 .iter()
1297 .map(|wheel| {
1298 serde::Serialize::serialize(wheel, toml_edit::ser::ValueSerializer::new())
1299 })
1300 .collect::<Result<Vec<_>, _>>()?;
1301 let wheels = match wheels.as_slice() {
1302 [] => Array::new(),
1303 [wheel] => Array::from_iter([wheel]),
1304 wheels => each_element_on_its_line_array(wheels.iter()),
1305 };
1306 table.insert("wheels", value(wheels));
1307 }
1308
1309 Ok(table)
1310 }
1311
1312 fn find_best_wheel(&self, tags: &Tags) -> Option<&PylockTomlWheel> {
1314 type WheelPriority = (TagPriority, Option<BuildTag>);
1315
1316 let mut best: Option<(WheelPriority, &PylockTomlWheel)> = None;
1317 for wheel in self.wheels.iter().flatten() {
1318 let Ok(filename) = wheel.filename(&self.name) else {
1319 continue;
1320 };
1321 let TagCompatibility::Compatible(tag_priority) = filename.compatibility(tags) else {
1322 continue;
1323 };
1324 let build_tag = filename.build_tag().cloned();
1325 let wheel_priority = (tag_priority, build_tag);
1326 match &best {
1327 None => {
1328 best = Some((wheel_priority, wheel));
1329 }
1330 Some((best_priority, _)) => {
1331 if wheel_priority > *best_priority {
1332 best = Some((wheel_priority, wheel));
1333 }
1334 }
1335 }
1336 }
1337
1338 best.map(|(_, i)| i)
1339 }
1340
1341 fn tag_hint(&self, tags: &Tags, markers: &MarkerEnvironment) -> Option<WheelTagHint> {
1343 let filenames = self
1344 .wheels
1345 .iter()
1346 .flatten()
1347 .filter_map(|wheel| wheel.filename(&self.name).ok())
1348 .collect::<Vec<_>>();
1349 let filenames = filenames.iter().map(Cow::as_ref).collect::<Vec<_>>();
1350 WheelTagHint::from_wheels(&self.name, self.version.as_ref(), &filenames, tags, markers)
1351 }
1352
1353 pub fn as_git_ref(&self) -> Option<ResolvedRepositoryReference> {
1355 let vcs = self.vcs.as_ref()?;
1356 let url = vcs.url.as_ref()?;
1357 let reference = match vcs.requested_revision.as_ref() {
1358 Some(rev) => GitReference::from_rev(rev.clone()),
1359 None => GitReference::DefaultBranch,
1360 };
1361 Some(ResolvedRepositoryReference {
1362 reference: RepositoryReference {
1363 url: RepositoryUrl::new(url),
1364 reference,
1365 },
1366 sha: vcs.commit_id,
1367 })
1368 }
1369}
1370
1371impl PylockTomlWheel {
1372 fn filename(&self, name: &PackageName) -> Result<Cow<'_, WheelFilename>, PylockTomlErrorKind> {
1374 if let Some(name) = self.name.as_ref() {
1375 Ok(Cow::Borrowed(name))
1376 } else if let Some(path) = self.path.as_ref() {
1377 let Some(filename) = path.as_ref().file_name().and_then(OsStr::to_str) else {
1378 return Err(PylockTomlErrorKind::PathMissingFilename(Box::<Path>::from(
1379 path.clone(),
1380 )));
1381 };
1382 let filename = WheelFilename::from_str(filename).map(Cow::Owned)?;
1383 Ok(filename)
1384 } else if let Some(url) = self.url.as_ref() {
1385 let Some(filename) = url.filename().ok() else {
1386 return Err(PylockTomlErrorKind::UrlMissingFilename(url.clone()));
1387 };
1388 let filename = WheelFilename::from_str(&filename).map(Cow::Owned)?;
1389 Ok(filename)
1390 } else {
1391 Err(PylockTomlErrorKind::WheelMissingPathUrl(name.clone()))
1392 }
1393 }
1394
1395 fn to_registry_wheel(
1397 &self,
1398 install_path: &Path,
1399 name: &PackageName,
1400 index: Option<&DisplaySafeUrl>,
1401 ) -> Result<RegistryBuiltWheel, PylockTomlErrorKind> {
1402 let filename = self.filename(name)?.into_owned();
1403
1404 let file_url = if let Some(url) = self.url.as_ref() {
1405 UrlString::from(url)
1406 } else if let Some(path) = self.path.as_ref() {
1407 let path = install_path.join(path);
1408 let url = DisplaySafeUrl::from_file_path(path)
1409 .map_err(|()| PylockTomlErrorKind::PathToUrl)?;
1410 UrlString::from(url)
1411 } else {
1412 return Err(PylockTomlErrorKind::WheelMissingPathUrl(name.clone()));
1413 };
1414
1415 let index = if let Some(index) = index {
1416 IndexUrl::from(VerbatimUrl::from_url(index.clone()))
1417 } else {
1418 let mut index = file_url.to_url().map_err(PylockTomlErrorKind::ToUrl)?;
1423 index.path_segments_mut().unwrap().pop();
1424 IndexUrl::from(VerbatimUrl::from_url(index))
1425 };
1426
1427 let file = Box::new(uv_distribution_types::File {
1428 dist_info_metadata: false,
1429 filename: SmallString::from(filename.to_string()),
1430 hashes: HashDigests::from(self.hashes.clone()),
1431 requires_python: None,
1432 size: self.size,
1433 upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
1434 url: FileLocation::AbsoluteUrl(file_url),
1435 yanked: None,
1436 zstd: None,
1437 });
1438
1439 Ok(RegistryBuiltWheel {
1440 filename,
1441 file,
1442 index,
1443 })
1444 }
1445}
1446
1447impl PylockTomlDirectory {
1448 fn to_sdist(
1450 &self,
1451 install_path: &Path,
1452 name: &PackageName,
1453 ) -> Result<DirectorySourceDist, PylockTomlErrorKind> {
1454 let path = if let Some(subdirectory) = self.subdirectory.as_ref() {
1455 install_path.join(&self.path).join(subdirectory)
1456 } else {
1457 install_path.join(&self.path)
1458 };
1459 let path = normalize_path(path);
1460 let url =
1461 VerbatimUrl::from_normalized_path(&path).map_err(|_| PylockTomlErrorKind::PathToUrl)?;
1462 Ok(DirectorySourceDist {
1463 name: name.clone(),
1464 install_path: path.into_owned().into_boxed_path(),
1465 editable: self.editable,
1466 r#virtual: Some(false),
1467 url,
1468 })
1469 }
1470}
1471
1472impl PylockTomlVcs {
1473 fn to_sdist(
1475 &self,
1476 install_path: &Path,
1477 name: &PackageName,
1478 ) -> Result<GitDirectorySourceDist, PylockTomlErrorKind> {
1479 let subdirectory = self.subdirectory.clone().map(Box::<Path>::from);
1480
1481 let git_url = {
1483 let mut url = if let Some(url) = self.url.as_ref() {
1484 url.clone()
1485 } else if let Some(path) = self.path.as_ref() {
1486 DisplaySafeUrl::from_url(
1487 Url::from_directory_path(install_path.join(path))
1488 .map_err(|()| PylockTomlErrorKind::PathToUrl)?,
1489 )
1490 } else {
1491 return Err(PylockTomlErrorKind::VcsMissingPathUrl(name.clone()));
1492 };
1493 url.set_fragment(None);
1494 url.set_query(None);
1495
1496 let reference = self
1497 .requested_revision
1498 .clone()
1499 .map(GitReference::from_rev)
1500 .unwrap_or_else(|| GitReference::BranchOrTagOrCommit(self.commit_id.to_string()));
1501 let precise = self.commit_id;
1502
1503 GitUrl::from_commit(url, reference, precise, GitLfs::from_env())?
1505 };
1506
1507 let url = DisplaySafeUrl::from(ParsedGitDirectoryUrl {
1509 url: git_url.clone(),
1510 subdirectory: subdirectory.clone(),
1511 });
1512
1513 Ok(GitDirectorySourceDist {
1514 name: name.clone(),
1515 git: Box::new(git_url),
1516 subdirectory: self.subdirectory.clone().map(Box::<Path>::from),
1517 url: VerbatimUrl::from_url(url),
1518 })
1519 }
1520}
1521
1522impl PylockTomlSdist {
1523 fn filename(&self, name: &PackageName) -> Result<Cow<'_, SmallString>, PylockTomlErrorKind> {
1525 if let Some(name) = self.name.as_ref() {
1526 Ok(Cow::Borrowed(name))
1527 } else if let Some(path) = self.path.as_ref() {
1528 let Some(filename) = path.as_ref().file_name().and_then(OsStr::to_str) else {
1529 return Err(PylockTomlErrorKind::PathMissingFilename(Box::<Path>::from(
1530 path.clone(),
1531 )));
1532 };
1533 Ok(Cow::Owned(SmallString::from(filename)))
1534 } else if let Some(url) = self.url.as_ref() {
1535 let Some(filename) = url.filename().ok() else {
1536 return Err(PylockTomlErrorKind::UrlMissingFilename(url.clone()));
1537 };
1538 Ok(Cow::Owned(SmallString::from(filename)))
1539 } else {
1540 Err(PylockTomlErrorKind::SdistMissingPathUrl(name.clone()))
1541 }
1542 }
1543
1544 fn to_sdist(
1546 &self,
1547 install_path: &Path,
1548 name: &PackageName,
1549 version: Option<&Version>,
1550 index: Option<&DisplaySafeUrl>,
1551 ) -> Result<RegistrySourceDist, PylockTomlErrorKind> {
1552 let filename = self.filename(name)?.into_owned();
1553 let ext = SourceDistExtension::from_path(filename.as_ref())?;
1554
1555 let version = if let Some(version) = version {
1556 Cow::Borrowed(version)
1557 } else {
1558 let filename = SourceDistFilename::parse(&filename, ext, name)?;
1559 Cow::Owned(filename.version)
1560 };
1561
1562 let file_url = if let Some(url) = self.url.as_ref() {
1563 UrlString::from(url)
1564 } else if let Some(path) = self.path.as_ref() {
1565 let path = install_path.join(path);
1566 let url = DisplaySafeUrl::from_file_path(path)
1567 .map_err(|()| PylockTomlErrorKind::PathToUrl)?;
1568 UrlString::from(url)
1569 } else {
1570 return Err(PylockTomlErrorKind::SdistMissingPathUrl(name.clone()));
1571 };
1572
1573 let index = if let Some(index) = index {
1574 IndexUrl::from(VerbatimUrl::from_url(index.clone()))
1575 } else {
1576 let mut index = file_url.to_url().map_err(PylockTomlErrorKind::ToUrl)?;
1581 index.path_segments_mut().unwrap().pop();
1582 IndexUrl::from(VerbatimUrl::from_url(index))
1583 };
1584
1585 let file = Box::new(uv_distribution_types::File {
1586 dist_info_metadata: false,
1587 filename,
1588 hashes: HashDigests::from(self.hashes.clone()),
1589 requires_python: None,
1590 size: self.size,
1591 upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
1592 url: FileLocation::AbsoluteUrl(file_url),
1593 yanked: None,
1594 zstd: None,
1595 });
1596
1597 Ok(RegistrySourceDist {
1598 name: name.clone(),
1599 version: version.into_owned(),
1600 file,
1601 ext,
1602 index,
1603 wheels: vec![],
1604 })
1605 }
1606}
1607
1608impl PylockTomlArchive {
1609 fn to_dist(
1610 &self,
1611 install_path: &Path,
1612 name: &PackageName,
1613 version: Option<&Version>,
1614 ) -> Result<Dist, PylockTomlErrorKind> {
1615 if let Some(url) = self.url.as_ref() {
1616 let filename = url
1617 .filename()
1618 .map_err(|_| PylockTomlErrorKind::UrlMissingFilename(url.clone()))?;
1619
1620 let ext = DistExtension::from_path(filename.as_ref())?;
1621 match ext {
1622 DistExtension::Wheel => {
1623 let filename = WheelFilename::from_str(&filename)?;
1624 Ok(Dist::Built(BuiltDist::DirectUrl(DirectUrlBuiltDist {
1625 filename,
1626 location: Box::new(url.clone()),
1627 url: VerbatimUrl::from_url(url.clone()),
1628 })))
1629 }
1630 DistExtension::Source(ext) => {
1631 Ok(Dist::Source(SourceDist::DirectUrl(DirectUrlSourceDist {
1632 name: name.clone(),
1633 location: Box::new(url.clone()),
1634 subdirectory: self.subdirectory.clone().map(Box::<Path>::from),
1635 ext,
1636 url: VerbatimUrl::from_url(url.clone()),
1637 })))
1638 }
1639 }
1640 } else if let Some(path) = self.path.as_ref() {
1641 let filename = path
1642 .as_ref()
1643 .file_name()
1644 .and_then(OsStr::to_str)
1645 .ok_or_else(|| {
1646 PylockTomlErrorKind::PathMissingFilename(Box::<Path>::from(path.clone()))
1647 })?;
1648
1649 let ext = DistExtension::from_path(filename)?;
1650 match ext {
1651 DistExtension::Wheel => {
1652 let filename = WheelFilename::from_str(filename)?;
1653 let install_path = install_path.join(path);
1654 let url = VerbatimUrl::from_absolute_path(&install_path)
1655 .map_err(|_| PylockTomlErrorKind::PathToUrl)?;
1656 Ok(Dist::Built(BuiltDist::Path(PathBuiltDist {
1657 filename,
1658 install_path: install_path.into_boxed_path(),
1659 url,
1660 })))
1661 }
1662 DistExtension::Source(ext) => {
1663 let install_path = install_path.join(path);
1664 let url = VerbatimUrl::from_absolute_path(&install_path)
1665 .map_err(|_| PylockTomlErrorKind::PathToUrl)?;
1666 Ok(Dist::Source(SourceDist::Path(PathSourceDist {
1667 name: name.clone(),
1668 version: version.cloned(),
1669 install_path: install_path.into_boxed_path(),
1670 ext,
1671 url,
1672 })))
1673 }
1674 }
1675 } else {
1676 Err(PylockTomlErrorKind::ArchiveMissingPathUrl(name.clone()))
1677 }
1678 }
1679
1680 fn is_wheel(&self, name: &PackageName) -> Result<bool, PylockTomlErrorKind> {
1682 if let Some(url) = self.url.as_ref() {
1683 let filename = url
1684 .filename()
1685 .map_err(|_| PylockTomlErrorKind::UrlMissingFilename(url.clone()))?;
1686
1687 let ext = DistExtension::from_path(filename.as_ref())?;
1688 Ok(matches!(ext, DistExtension::Wheel))
1689 } else if let Some(path) = self.path.as_ref() {
1690 let filename = path
1691 .as_ref()
1692 .file_name()
1693 .and_then(OsStr::to_str)
1694 .ok_or_else(|| {
1695 PylockTomlErrorKind::PathMissingFilename(Box::<Path>::from(path.clone()))
1696 })?;
1697
1698 let ext = DistExtension::from_path(filename)?;
1699 Ok(matches!(ext, DistExtension::Wheel))
1700 } else {
1701 Err(PylockTomlErrorKind::ArchiveMissingPathUrl(name.clone()))
1702 }
1703 }
1704}
1705
1706#[expect(clippy::ref_option)]
1708fn timestamp_to_toml_datetime<S>(
1709 timestamp: &Option<Timestamp>,
1710 serializer: S,
1711) -> Result<S::Ok, S::Error>
1712where
1713 S: serde::Serializer,
1714{
1715 let Some(timestamp) = timestamp else {
1716 return serializer.serialize_none();
1717 };
1718 let timestamp = timestamp.to_zoned(TimeZone::UTC);
1719 let timestamp = toml_edit::Datetime {
1720 date: Some(toml_edit::Date {
1721 year: u16::try_from(timestamp.year()).map_err(serde::ser::Error::custom)?,
1722 month: u8::try_from(timestamp.month()).map_err(serde::ser::Error::custom)?,
1723 day: u8::try_from(timestamp.day()).map_err(serde::ser::Error::custom)?,
1724 }),
1725 time: Some(toml_edit::Time {
1726 hour: u8::try_from(timestamp.hour()).map_err(serde::ser::Error::custom)?,
1727 minute: u8::try_from(timestamp.minute()).map_err(serde::ser::Error::custom)?,
1728 second: Some(u8::try_from(timestamp.second()).map_err(serde::ser::Error::custom)?),
1729 nanosecond: {
1730 let nanos =
1731 u32::try_from(timestamp.nanosecond()).map_err(serde::ser::Error::custom)?;
1732 if nanos == 0 { None } else { Some(nanos) }
1733 },
1734 }),
1735 offset: Some(toml_edit::Offset::Z),
1736 };
1737 serializer.serialize_some(×tamp)
1738}
1739
1740fn timestamp_from_toml_datetime<'de, D>(deserializer: D) -> Result<Option<Timestamp>, D::Error>
1742where
1743 D: serde::Deserializer<'de>,
1744{
1745 let Some(datetime) = Option::<toml_edit::Datetime>::deserialize(deserializer)? else {
1746 return Ok(None);
1747 };
1748
1749 let Some(date) = datetime.date else {
1751 return Err(serde::de::Error::custom("missing date"));
1752 };
1753
1754 let year = i16::try_from(date.year).map_err(serde::de::Error::custom)?;
1755 let month = i8::try_from(date.month).map_err(serde::de::Error::custom)?;
1756 let day = i8::try_from(date.day).map_err(serde::de::Error::custom)?;
1757 let date = Date::new(year, month, day).map_err(serde::de::Error::custom)?;
1758
1759 let tz = if let Some(offset) = datetime.offset {
1761 match offset {
1762 toml_edit::Offset::Z => TimeZone::UTC,
1763 toml_edit::Offset::Custom { minutes } => {
1764 let hours = i8::try_from(minutes / 60).map_err(serde::de::Error::custom)?;
1765 TimeZone::fixed(Offset::constant(hours))
1766 }
1767 }
1768 } else {
1769 TimeZone::UTC
1770 };
1771
1772 let time = if let Some(time) = datetime.time {
1774 let hour = i8::try_from(time.hour).map_err(serde::de::Error::custom)?;
1775 let minute = i8::try_from(time.minute).map_err(serde::de::Error::custom)?;
1776 let second = time
1777 .second
1778 .map(i8::try_from)
1779 .transpose()
1780 .map_err(serde::de::Error::custom)?
1781 .unwrap_or_default();
1782 let nanosecond = time
1783 .nanosecond
1784 .map(i32::try_from)
1785 .transpose()
1786 .map_err(serde::de::Error::custom)?
1787 .unwrap_or_default();
1788 Time::new(hour, minute, second, nanosecond).map_err(serde::de::Error::custom)?
1789 } else {
1790 Time::midnight()
1791 };
1792
1793 let timestamp = tz
1794 .to_timestamp(DateTime::from_parts(date, time))
1795 .map_err(serde::de::Error::custom)?;
1796 Ok(Some(timestamp))
1797}