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