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