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