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