1use std::borrow::Cow;
2use std::ffi::OsStr;
3use std::path::Path;
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, relative_to};
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 = relative_to(&dist.install_path, install_path)
415 .map(Box::<Path>::from)
416 .unwrap_or_else(|_| dist.install_path.clone());
417 package.archive = Some(PylockTomlArchive {
418 url: None,
419 path: Some(PortablePathBuf::from(path)),
420 size: dist.size(),
421 upload_time: None,
422 subdirectory: None,
423 hashes: Hashes::from(node.hashes.clone()),
424 });
425 }
426 Dist::Built(BuiltDist::Registry(dist)) => {
427 package.wheels = Self::filter_and_convert_wheels(
428 resolution,
429 tags,
430 &requires_python,
431 node_index,
432 &dist.wheels,
433 build_options.no_binary_package(dist.name()),
434 )?;
435
436 let no_build = build_options.no_build_package(dist.name());
438
439 if !no_build {
440 if let Some(sdist) = dist.sdist.as_ref() {
441 let url = sdist
442 .file
443 .url
444 .to_url()
445 .map_err(PylockTomlErrorKind::ToUrl)?;
446 package.sdist = Some(PylockTomlSdist {
447 name: if url
449 .filename()
450 .is_ok_and(|filename| filename == *sdist.file.filename)
451 {
452 None
453 } else {
454 Some(sdist.file.filename.clone())
455 },
456 upload_time: sdist
457 .file
458 .upload_time_utc_ms
459 .map(Timestamp::from_millisecond)
460 .transpose()?,
461 url: Some(url),
462 path: None,
463 size: sdist.file.size,
464 hashes: Hashes::from(sdist.file.hashes.clone()),
465 });
466 }
467 }
468 }
469 Dist::Source(SourceDist::DirectUrl(dist)) => {
470 package.archive = Some(PylockTomlArchive {
471 url: Some((*dist.location).clone()),
472 path: None,
473 size: dist.size(),
474 upload_time: None,
475 subdirectory: dist.subdirectory.clone().map(PortablePathBuf::from),
476 hashes: Hashes::from(node.hashes.clone()),
477 });
478 }
479 Dist::Source(SourceDist::Directory(dist)) => {
480 let path = relative_to(&dist.install_path, install_path)
481 .map(Box::<Path>::from)
482 .unwrap_or_else(|_| dist.install_path.clone());
483 package.directory = Some(PylockTomlDirectory {
484 path: PortablePathBuf::from(path),
485 editable: dist.editable,
486 subdirectory: None,
487 });
488 }
489 Dist::Source(SourceDist::Git(dist)) => {
490 package.vcs = Some(PylockTomlVcs {
491 r#type: VcsKind::Git,
492 url: Some(dist.git.repository().clone()),
493 path: None,
494 requested_revision: dist.git.reference().as_str().map(ToString::to_string),
495 commit_id: dist.git.precise().unwrap_or_else(|| {
496 panic!("Git distribution is missing a precise hash: {dist}")
497 }),
498 subdirectory: dist.subdirectory.clone().map(PortablePathBuf::from),
499 });
500 }
501 Dist::Source(SourceDist::Path(dist)) => {
502 let path = relative_to(&dist.install_path, install_path)
503 .map(Box::<Path>::from)
504 .unwrap_or_else(|_| dist.install_path.clone());
505 package.archive = Some(PylockTomlArchive {
506 url: None,
507 path: Some(PortablePathBuf::from(path)),
508 size: dist.size(),
509 upload_time: None,
510 subdirectory: None,
511 hashes: Hashes::from(node.hashes.clone()),
512 });
513 }
514 Dist::Source(SourceDist::Registry(dist)) => {
515 package.wheels = Self::filter_and_convert_wheels(
516 resolution,
517 tags,
518 &requires_python,
519 node_index,
520 &dist.wheels,
521 build_options.no_binary_package(&dist.name),
522 )?;
523
524 let no_build = build_options.no_build_package(&dist.name);
526
527 if !no_build {
528 let url = dist.file.url.to_url().map_err(PylockTomlErrorKind::ToUrl)?;
529 package.sdist = Some(PylockTomlSdist {
530 name: if url
532 .filename()
533 .is_ok_and(|filename| filename == *dist.file.filename)
534 {
535 None
536 } else {
537 Some(dist.file.filename.clone())
538 },
539 upload_time: dist
540 .file
541 .upload_time_utc_ms
542 .map(Timestamp::from_millisecond)
543 .transpose()?,
544 url: Some(url),
545 path: None,
546 size: dist.file.size,
547 hashes: Hashes::from(dist.file.hashes.clone()),
548 });
549 }
550 }
551 }
552
553 packages.push(package);
555 }
556
557 packages.sort_by(|a, b| a.name.cmp(&b.name).then(a.version.cmp(&b.version)));
559
560 Ok(Self {
562 lock_version,
563 created_by,
564 requires_python: Some(requires_python),
565 extras,
566 dependency_groups,
567 default_groups,
568 packages,
569 attestation_identities,
570 })
571 }
572
573 fn filter_and_convert_wheels(
578 resolution: &ResolverOutput,
579 tags: Option<&Tags>,
580 requires_python: &RequiresPython,
581 node_index: NodeIndex,
582 wheels: &[RegistryBuiltWheel],
583 no_binary: bool,
584 ) -> Result<Option<Vec<PylockTomlWheel>>, PylockTomlErrorKind> {
585 if no_binary {
586 return Ok(None);
587 }
588
589 let wheels: Vec<_> = wheels
591 .iter()
592 .filter(|wheel| {
593 !is_wheel_unreachable(
594 &wheel.filename,
595 resolution,
596 requires_python,
597 node_index,
598 tags,
599 )
600 })
601 .collect();
602
603 if wheels.is_empty() {
604 return Ok(None);
605 }
606
607 let wheels = wheels
608 .into_iter()
609 .map(|wheel| {
610 let url = wheel
611 .file
612 .url
613 .to_url()
614 .map_err(PylockTomlErrorKind::ToUrl)?;
615 Ok(PylockTomlWheel {
616 name: if url
618 .filename()
619 .is_ok_and(|filename| filename == *wheel.file.filename)
620 {
621 None
622 } else {
623 Some(wheel.filename.clone())
624 },
625 upload_time: wheel
626 .file
627 .upload_time_utc_ms
628 .map(Timestamp::from_millisecond)
629 .transpose()?,
630 url: Some(
631 wheel
632 .file
633 .url
634 .to_url()
635 .map_err(PylockTomlErrorKind::ToUrl)?,
636 ),
637 path: None,
638 size: wheel.file.size,
639 hashes: Hashes::from(wheel.file.hashes.clone()),
640 })
641 })
642 .collect::<Result<Vec<_>, PylockTomlErrorKind>>()?;
643 Ok(Some(wheels))
644 }
645
646 pub fn from_lock(
648 target: &impl Installable<'lock>,
649 prune: &[PackageName],
650 extras: &ExtrasSpecificationWithDefaults,
651 dev: &DependencyGroupsWithDefaults,
652 annotate: bool,
653 editable: Option<EditableMode>,
654 install_options: &'lock InstallOptions,
655 ) -> Result<Self, PylockTomlErrorKind> {
656 let ExportableRequirements(mut nodes) = ExportableRequirements::from_lock(
658 target,
659 prune,
660 extras,
661 dev,
662 annotate,
663 install_options,
664 )?;
665
666 nodes.sort_unstable_by_key(|node| &node.package.id);
668
669 let lock_version = Version::new([1, 0]);
671
672 let created_by = "uv".to_string();
674
675 let requires_python = target.lock().requires_python.clone();
677
678 let extras = vec![];
680
681 let dependency_groups = vec![];
683
684 let default_groups = vec![];
686
687 let attestation_identities = vec![];
689
690 let mut packages = Vec::with_capacity(nodes.len());
692 for node in nodes {
693 let package = node.package;
694
695 let wheels = match &package.id.source {
700 Source::Registry(source) => {
701 let wheels = package
702 .wheels
703 .iter()
704 .map(|wheel| wheel.to_registry_wheel(source, target.install_path()))
705 .collect::<Result<Vec<RegistryBuiltWheel>, LockError>>()?;
706 Some(
707 wheels
708 .into_iter()
709 .map(|wheel| {
710 let url = wheel
711 .file
712 .url
713 .to_url()
714 .map_err(PylockTomlErrorKind::ToUrl)?;
715 Ok(PylockTomlWheel {
716 name: if url
718 .filename()
719 .is_ok_and(|filename| filename == *wheel.file.filename)
720 {
721 None
722 } else {
723 Some(wheel.filename.clone())
724 },
725 upload_time: wheel
726 .file
727 .upload_time_utc_ms
728 .map(Timestamp::from_millisecond)
729 .transpose()?,
730 url: Some(url),
731 path: None,
732 size: wheel.file.size,
733 hashes: Hashes::from(wheel.file.hashes),
734 })
735 })
736 .collect::<Result<Vec<_>, PylockTomlErrorKind>>()?,
737 )
738 }
739 Source::Path(..) => None,
740 Source::Git(..) => None,
741 Source::Direct(..) => None,
742 Source::Directory(..) => None,
743 Source::Editable(..) => None,
744 Source::Virtual(..) => {
745 continue;
747 }
748 };
749
750 let sdist = package.to_source_dist(target.install_path())?;
752
753 let size = package
755 .sdist
756 .as_ref()
757 .and_then(super::super::SourceDist::size);
758 let hash = package.sdist.as_ref().and_then(|sdist| sdist.hash());
759
760 let directory = match &sdist {
762 Some(SourceDist::Directory(sdist)) => Some(PylockTomlDirectory {
763 path: PortablePathBuf::from(
764 relative_to(&sdist.install_path, target.install_path())
765 .unwrap_or_else(|_| sdist.install_path.to_path_buf())
766 .into_boxed_path(),
767 ),
768 editable: match editable {
769 None => sdist.editable,
770 Some(EditableMode::NonEditable) => None,
771 Some(EditableMode::Editable) => Some(true),
772 },
773 subdirectory: None,
774 }),
775 _ => None,
776 };
777
778 let vcs = match &sdist {
780 Some(SourceDist::Git(sdist)) => Some(PylockTomlVcs {
781 r#type: VcsKind::Git,
782 url: Some(sdist.git.repository().clone()),
783 path: None,
784 requested_revision: sdist.git.reference().as_str().map(ToString::to_string),
785 commit_id: sdist.git.precise().unwrap_or_else(|| {
786 panic!("Git distribution is missing a precise hash: {sdist}")
787 }),
788 subdirectory: sdist.subdirectory.clone().map(PortablePathBuf::from),
789 }),
790 _ => None,
791 };
792
793 let archive = match &sdist {
796 Some(SourceDist::DirectUrl(sdist)) => Some(PylockTomlArchive {
797 url: Some(sdist.url.to_url()),
798 path: None,
799 size,
800 upload_time: None,
801 subdirectory: sdist.subdirectory.clone().map(PortablePathBuf::from),
802 hashes: hash.cloned().map(Hashes::from).unwrap_or_default(),
803 }),
804 Some(SourceDist::Path(sdist)) => Some(PylockTomlArchive {
805 url: None,
806 path: Some(PortablePathBuf::from(
807 relative_to(&sdist.install_path, target.install_path())
808 .unwrap_or_else(|_| sdist.install_path.to_path_buf())
809 .into_boxed_path(),
810 )),
811 size,
812 upload_time: None,
813 subdirectory: None,
814 hashes: hash.cloned().map(Hashes::from).unwrap_or_default(),
815 }),
816 _ => match &package.id.source {
817 Source::Registry(..) => None,
818 Source::Path(source) => package.wheels.first().map(|wheel| PylockTomlArchive {
819 url: None,
820 path: Some(PortablePathBuf::from(
821 relative_to(source, target.install_path())
822 .unwrap_or_else(|_| source.to_path_buf())
823 .into_boxed_path(),
824 )),
825 size: wheel.size,
826 upload_time: None,
827 subdirectory: None,
828 hashes: wheel.hash.clone().map(Hashes::from).unwrap_or_default(),
829 }),
830 Source::Git(..) => None,
831 Source::Direct(source, ..) => {
832 if let Some(wheel) = package.wheels.first() {
833 Some(PylockTomlArchive {
834 url: Some(source.to_url()?),
835 path: None,
836 size: wheel.size,
837 upload_time: None,
838 subdirectory: None,
839 hashes: wheel.hash.clone().map(Hashes::from).unwrap_or_default(),
840 })
841 } else {
842 None
843 }
844 }
845 Source::Directory(..) => None,
846 Source::Editable(..) => None,
847 Source::Virtual(..) => None,
848 },
849 };
850
851 let sdist = match &sdist {
853 Some(SourceDist::Registry(sdist)) => {
854 let url = sdist
855 .file
856 .url
857 .to_url()
858 .map_err(PylockTomlErrorKind::ToUrl)?;
859 Some(PylockTomlSdist {
860 name: if url
862 .filename()
863 .is_ok_and(|filename| filename == *sdist.file.filename)
864 {
865 None
866 } else {
867 Some(sdist.file.filename.clone())
868 },
869 upload_time: sdist
870 .file
871 .upload_time_utc_ms
872 .map(Timestamp::from_millisecond)
873 .transpose()?,
874 url: Some(url),
875 path: None,
876 size,
877 hashes: hash.cloned().map(Hashes::from).unwrap_or_default(),
878 })
879 }
880 _ => None,
881 };
882
883 let index = package
885 .index(target.install_path())?
886 .map(IndexUrl::into_url);
887
888 let name = package.id.name.clone();
890
891 let version = package
894 .id
895 .version
896 .as_ref()
897 .filter(|_| directory.is_none())
898 .cloned();
899
900 let package = PylockTomlPackage {
901 name,
902 version,
903 marker: node.marker,
904 requires_python: None,
905 dependencies: vec![],
906 index,
907 vcs,
908 directory,
909 archive,
910 sdist,
911 wheels,
912 };
913
914 packages.push(package);
915 }
916
917 Ok(Self {
918 lock_version,
919 created_by,
920 requires_python: Some(requires_python),
921 extras,
922 dependency_groups,
923 default_groups,
924 packages,
925 attestation_identities,
926 })
927 }
928
929 pub fn to_toml(&self) -> Result<String, toml_edit::ser::Error> {
931 let mut doc = toml_edit::DocumentMut::new();
934
935 doc.insert("lock-version", value(self.lock_version.to_string()));
936 doc.insert("created-by", value(self.created_by.as_str()));
937 if let Some(ref requires_python) = self.requires_python {
938 doc.insert("requires-python", value(requires_python.to_string()));
939 }
940 if !self.extras.is_empty() {
941 doc.insert(
942 "extras",
943 value(each_element_on_its_line_array(
944 self.extras.iter().map(ToString::to_string),
945 )),
946 );
947 }
948 if !self.dependency_groups.is_empty() {
949 doc.insert(
950 "dependency-groups",
951 value(each_element_on_its_line_array(
952 self.dependency_groups.iter().map(ToString::to_string),
953 )),
954 );
955 }
956 if !self.default_groups.is_empty() {
957 doc.insert(
958 "default-groups",
959 value(each_element_on_its_line_array(
960 self.default_groups.iter().map(ToString::to_string),
961 )),
962 );
963 }
964 if !self.attestation_identities.is_empty() {
965 let attestation_identities = self
966 .attestation_identities
967 .iter()
968 .map(|attestation_identity| {
969 serde::Serialize::serialize(
970 &attestation_identity,
971 toml_edit::ser::ValueSerializer::new(),
972 )
973 })
974 .collect::<Result<Vec<_>, _>>()?;
975 let attestation_identities = match attestation_identities.as_slice() {
976 [] => Array::new(),
977 [attestation_identity] => Array::from_iter([attestation_identity]),
978 attestation_identities => {
979 each_element_on_its_line_array(attestation_identities.iter())
980 }
981 };
982 doc.insert("attestation-identities", value(attestation_identities));
983 }
984 if !self.packages.is_empty() {
985 let mut packages = ArrayOfTables::new();
986 for dist in &self.packages {
987 packages.push(dist.to_toml()?);
988 }
989 doc.insert("packages", Item::ArrayOfTables(packages));
990 }
991
992 Ok(doc.to_string())
993 }
994
995 pub fn to_resolution(
997 self,
998 install_path: &Path,
999 markers: &MarkerEnvironment,
1000 extras: &[ExtraName],
1001 groups: &[GroupName],
1002 tags: &Tags,
1003 build_options: &BuildOptions,
1004 ) -> Result<Resolution, PylockTomlError> {
1005 let mut graph =
1007 petgraph::graph::DiGraph::with_capacity(self.packages.len(), self.packages.len());
1008
1009 let root = graph.add_node(Node::Root);
1011
1012 for package in self.packages {
1013 if !package.marker.evaluate_pep751(markers, extras, groups) {
1015 continue;
1016 }
1017
1018 match (
1019 package.wheels.is_some(),
1020 package.sdist.is_some(),
1021 package.directory.is_some(),
1022 package.vcs.is_some(),
1023 package.archive.is_some(),
1024 ) {
1025 (true, _, true, _, _) => {
1027 return Err(
1028 PylockTomlErrorKind::WheelWithDirectory(package.name.clone()).into(),
1029 );
1030 }
1031 (true, _, _, true, _) => {
1032 return Err(PylockTomlErrorKind::WheelWithVcs(package.name.clone()).into());
1033 }
1034 (true, _, _, _, true) => {
1035 return Err(PylockTomlErrorKind::WheelWithArchive(package.name.clone()).into());
1036 }
1037 (_, true, true, _, _) => {
1039 return Err(
1040 PylockTomlErrorKind::SdistWithDirectory(package.name.clone()).into(),
1041 );
1042 }
1043 (_, true, _, true, _) => {
1044 return Err(PylockTomlErrorKind::SdistWithVcs(package.name.clone()).into());
1045 }
1046 (_, true, _, _, true) => {
1047 return Err(PylockTomlErrorKind::SdistWithArchive(package.name.clone()).into());
1048 }
1049 (_, _, true, true, _) => {
1051 return Err(PylockTomlErrorKind::DirectoryWithVcs(package.name.clone()).into());
1052 }
1053 (_, _, true, _, true) => {
1054 return Err(
1055 PylockTomlErrorKind::DirectoryWithArchive(package.name.clone()).into(),
1056 );
1057 }
1058 (_, _, _, true, true) => {
1060 return Err(PylockTomlErrorKind::VcsWithArchive(package.name.clone()).into());
1061 }
1062 (false, false, false, false, false) => {
1063 return Err(PylockTomlErrorKind::MissingSource(package.name.clone()).into());
1064 }
1065 _ => {}
1066 }
1067
1068 let no_binary = build_options.no_binary_package(&package.name);
1069 let no_build = build_options.no_build_package(&package.name);
1070 let is_wheel = package
1071 .archive
1072 .as_ref()
1073 .map(|archive| archive.is_wheel(&package.name))
1074 .transpose()?
1075 .unwrap_or_default();
1076
1077 let dist = if let Some(best_wheel) =
1079 package.find_best_wheel(tags).filter(|_| !no_binary)
1080 {
1081 let hashes = HashDigests::from(best_wheel.hashes.clone());
1082 let built_dist = Dist::Built(BuiltDist::Registry(RegistryBuiltDist {
1083 wheels: vec![best_wheel.to_registry_wheel(
1084 install_path,
1085 &package.name,
1086 package.index.as_ref(),
1087 )?],
1088 best_wheel_index: 0,
1089 sdist: None,
1090 }));
1091 let dist = ResolvedDist::Installable {
1092 dist: Arc::new(built_dist),
1093 version: package.version,
1094 };
1095 Node::Dist {
1096 dist,
1097 hashes,
1098 install: true,
1099 }
1100 } else if let Some(sdist) = package.sdist.as_ref().filter(|_| !no_build) {
1101 let hashes = HashDigests::from(sdist.hashes.clone());
1102 let sdist = Dist::Source(SourceDist::Registry(sdist.to_sdist(
1103 install_path,
1104 &package.name,
1105 package.version.as_ref(),
1106 package.index.as_ref(),
1107 )?));
1108 let dist = ResolvedDist::Installable {
1109 dist: Arc::new(sdist),
1110 version: package.version,
1111 };
1112 Node::Dist {
1113 dist,
1114 hashes,
1115 install: true,
1116 }
1117 } else if let Some(sdist) = package.directory.as_ref().filter(|_| !no_build) {
1118 let hashes = HashDigests::empty();
1119 let sdist = Dist::Source(SourceDist::Directory(
1120 sdist.to_sdist(install_path, &package.name)?,
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.vcs.as_ref().filter(|_| !no_build) {
1132 let hashes = HashDigests::empty();
1133 let sdist = Dist::Source(SourceDist::Git(
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(dist) = package
1146 .archive
1147 .as_ref()
1148 .filter(|_| if is_wheel { !no_binary } else { !no_build })
1149 {
1150 let hashes = HashDigests::from(dist.hashes.clone());
1151 let dist = dist.to_dist(install_path, &package.name, package.version.as_ref())?;
1152 let dist = ResolvedDist::Installable {
1153 dist: Arc::new(dist),
1154 version: package.version,
1155 };
1156 Node::Dist {
1157 dist,
1158 hashes,
1159 install: true,
1160 }
1161 } else {
1162 return match (no_binary, no_build) {
1163 (true, true) => {
1164 Err(PylockTomlErrorKind::NoBinaryNoBuild(package.name.clone()).into())
1165 }
1166 (true, false) if is_wheel => {
1167 Err(PylockTomlErrorKind::NoBinaryWheelOnly(package.name.clone()).into())
1168 }
1169 (true, false) => {
1170 Err(PylockTomlErrorKind::NoBinary(package.name.clone()).into())
1171 }
1172 (false, true) => Err(PylockTomlErrorKind::NoBuild(package.name.clone()).into()),
1173 (false, false) if is_wheel => Err(PylockTomlError {
1174 kind: Box::new(PylockTomlErrorKind::IncompatibleWheelOnly(
1175 package.name.clone(),
1176 )),
1177 hint: package.tag_hint(tags, markers),
1178 }),
1179 (false, false) => Err(PylockTomlError {
1180 kind: Box::new(PylockTomlErrorKind::NeitherSourceDistNorWheel(
1181 package.name.clone(),
1182 )),
1183 hint: package.tag_hint(tags, markers),
1184 }),
1185 };
1186 };
1187
1188 let index = graph.add_node(dist);
1189 graph.add_edge(root, index, Edge::Prod);
1190 }
1191
1192 Ok(Resolution::new(graph))
1193 }
1194}
1195
1196impl PylockTomlPackage {
1197 fn to_toml(&self) -> Result<Table, toml_edit::ser::Error> {
1199 let mut table = Table::new();
1200 table.insert("name", value(self.name.to_string()));
1201 if let Some(ref version) = self.version {
1202 table.insert("version", value(version.to_string()));
1203 }
1204 if let Some(marker) = self.marker.try_to_string() {
1205 table.insert("marker", value(marker));
1206 }
1207 if let Some(ref requires_python) = self.requires_python {
1208 table.insert("requires-python", value(requires_python.to_string()));
1209 }
1210 if !self.dependencies.is_empty() {
1211 let dependencies = self
1212 .dependencies
1213 .iter()
1214 .map(|dependency| {
1215 serde::Serialize::serialize(&dependency, toml_edit::ser::ValueSerializer::new())
1216 })
1217 .collect::<Result<Vec<_>, _>>()?;
1218 let dependencies = match dependencies.as_slice() {
1219 [] => Array::new(),
1220 [dependency] => Array::from_iter([dependency]),
1221 dependencies => each_element_on_its_line_array(dependencies.iter()),
1222 };
1223 table.insert("dependencies", value(dependencies));
1224 }
1225 if let Some(ref index) = self.index {
1226 table.insert("index", value(index.to_string()));
1227 }
1228 if let Some(ref vcs) = self.vcs {
1229 table.insert(
1230 "vcs",
1231 value(serde::Serialize::serialize(
1232 &vcs,
1233 toml_edit::ser::ValueSerializer::new(),
1234 )?),
1235 );
1236 }
1237 if let Some(ref directory) = self.directory {
1238 table.insert(
1239 "directory",
1240 value(serde::Serialize::serialize(
1241 &directory,
1242 toml_edit::ser::ValueSerializer::new(),
1243 )?),
1244 );
1245 }
1246 if let Some(ref archive) = self.archive {
1247 table.insert(
1248 "archive",
1249 value(serde::Serialize::serialize(
1250 &archive,
1251 toml_edit::ser::ValueSerializer::new(),
1252 )?),
1253 );
1254 }
1255 if let Some(ref sdist) = self.sdist {
1256 table.insert(
1257 "sdist",
1258 value(serde::Serialize::serialize(
1259 &sdist,
1260 toml_edit::ser::ValueSerializer::new(),
1261 )?),
1262 );
1263 }
1264 if let Some(wheels) = self.wheels.as_ref().filter(|wheels| !wheels.is_empty()) {
1265 let wheels = wheels
1266 .iter()
1267 .map(|wheel| {
1268 serde::Serialize::serialize(wheel, toml_edit::ser::ValueSerializer::new())
1269 })
1270 .collect::<Result<Vec<_>, _>>()?;
1271 let wheels = match wheels.as_slice() {
1272 [] => Array::new(),
1273 [wheel] => Array::from_iter([wheel]),
1274 wheels => each_element_on_its_line_array(wheels.iter()),
1275 };
1276 table.insert("wheels", value(wheels));
1277 }
1278
1279 Ok(table)
1280 }
1281
1282 fn find_best_wheel(&self, tags: &Tags) -> Option<&PylockTomlWheel> {
1284 type WheelPriority = (TagPriority, Option<BuildTag>);
1285
1286 let mut best: Option<(WheelPriority, &PylockTomlWheel)> = None;
1287 for wheel in self.wheels.iter().flatten() {
1288 let Ok(filename) = wheel.filename(&self.name) else {
1289 continue;
1290 };
1291 let TagCompatibility::Compatible(tag_priority) = filename.compatibility(tags) else {
1292 continue;
1293 };
1294 let build_tag = filename.build_tag().cloned();
1295 let wheel_priority = (tag_priority, build_tag);
1296 match &best {
1297 None => {
1298 best = Some((wheel_priority, wheel));
1299 }
1300 Some((best_priority, _)) => {
1301 if wheel_priority > *best_priority {
1302 best = Some((wheel_priority, wheel));
1303 }
1304 }
1305 }
1306 }
1307
1308 best.map(|(_, i)| i)
1309 }
1310
1311 fn tag_hint(&self, tags: &Tags, markers: &MarkerEnvironment) -> Option<WheelTagHint> {
1313 let filenames = self
1314 .wheels
1315 .iter()
1316 .flatten()
1317 .filter_map(|wheel| wheel.filename(&self.name).ok())
1318 .collect::<Vec<_>>();
1319 let filenames = filenames.iter().map(Cow::as_ref).collect::<Vec<_>>();
1320 WheelTagHint::from_wheels(&self.name, self.version.as_ref(), &filenames, tags, markers)
1321 }
1322
1323 pub fn as_git_ref(&self) -> Option<ResolvedRepositoryReference> {
1325 let vcs = self.vcs.as_ref()?;
1326 let url = vcs.url.as_ref()?;
1327 let reference = match vcs.requested_revision.as_ref() {
1328 Some(rev) => GitReference::from_rev(rev.clone()),
1329 None => GitReference::DefaultBranch,
1330 };
1331 Some(ResolvedRepositoryReference {
1332 reference: RepositoryReference {
1333 url: RepositoryUrl::new(url),
1334 reference,
1335 },
1336 sha: vcs.commit_id,
1337 })
1338 }
1339}
1340
1341impl PylockTomlWheel {
1342 fn filename(&self, name: &PackageName) -> Result<Cow<'_, WheelFilename>, PylockTomlErrorKind> {
1344 if let Some(name) = self.name.as_ref() {
1345 Ok(Cow::Borrowed(name))
1346 } else if let Some(path) = self.path.as_ref() {
1347 let Some(filename) = path.as_ref().file_name().and_then(OsStr::to_str) else {
1348 return Err(PylockTomlErrorKind::PathMissingFilename(Box::<Path>::from(
1349 path.clone(),
1350 )));
1351 };
1352 let filename = WheelFilename::from_str(filename).map(Cow::Owned)?;
1353 Ok(filename)
1354 } else if let Some(url) = self.url.as_ref() {
1355 let Some(filename) = url.filename().ok() else {
1356 return Err(PylockTomlErrorKind::UrlMissingFilename(url.clone()));
1357 };
1358 let filename = WheelFilename::from_str(&filename).map(Cow::Owned)?;
1359 Ok(filename)
1360 } else {
1361 Err(PylockTomlErrorKind::WheelMissingPathUrl(name.clone()))
1362 }
1363 }
1364
1365 fn to_registry_wheel(
1367 &self,
1368 install_path: &Path,
1369 name: &PackageName,
1370 index: Option<&DisplaySafeUrl>,
1371 ) -> Result<RegistryBuiltWheel, PylockTomlErrorKind> {
1372 let filename = self.filename(name)?.into_owned();
1373
1374 let file_url = if let Some(url) = self.url.as_ref() {
1375 UrlString::from(url)
1376 } else if let Some(path) = self.path.as_ref() {
1377 let path = install_path.join(path);
1378 let url = DisplaySafeUrl::from_file_path(path)
1379 .map_err(|()| PylockTomlErrorKind::PathToUrl)?;
1380 UrlString::from(url)
1381 } else {
1382 return Err(PylockTomlErrorKind::WheelMissingPathUrl(name.clone()));
1383 };
1384
1385 let index = if let Some(index) = index {
1386 IndexUrl::from(VerbatimUrl::from_url(index.clone()))
1387 } else {
1388 let mut index = file_url.to_url().map_err(PylockTomlErrorKind::ToUrl)?;
1393 index.path_segments_mut().unwrap().pop();
1394 IndexUrl::from(VerbatimUrl::from_url(index))
1395 };
1396
1397 let file = Box::new(uv_distribution_types::File {
1398 dist_info_metadata: false,
1399 filename: SmallString::from(filename.to_string()),
1400 hashes: HashDigests::from(self.hashes.clone()),
1401 requires_python: None,
1402 size: self.size,
1403 upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
1404 url: FileLocation::AbsoluteUrl(file_url),
1405 yanked: None,
1406 zstd: None,
1407 });
1408
1409 Ok(RegistryBuiltWheel {
1410 filename,
1411 file,
1412 index,
1413 })
1414 }
1415}
1416
1417impl PylockTomlDirectory {
1418 fn to_sdist(
1420 &self,
1421 install_path: &Path,
1422 name: &PackageName,
1423 ) -> Result<DirectorySourceDist, PylockTomlErrorKind> {
1424 let path = if let Some(subdirectory) = self.subdirectory.as_ref() {
1425 install_path.join(&self.path).join(subdirectory)
1426 } else {
1427 install_path.join(&self.path)
1428 };
1429 let path = uv_fs::normalize_path_buf(path);
1430 let url =
1431 VerbatimUrl::from_normalized_path(&path).map_err(|_| PylockTomlErrorKind::PathToUrl)?;
1432 Ok(DirectorySourceDist {
1433 name: name.clone(),
1434 install_path: path.into_boxed_path(),
1435 editable: self.editable,
1436 r#virtual: Some(false),
1437 url,
1438 })
1439 }
1440}
1441
1442impl PylockTomlVcs {
1443 fn to_sdist(
1445 &self,
1446 install_path: &Path,
1447 name: &PackageName,
1448 ) -> Result<GitSourceDist, PylockTomlErrorKind> {
1449 let subdirectory = self.subdirectory.clone().map(Box::<Path>::from);
1450
1451 let git_url = {
1453 let mut url = if let Some(url) = self.url.as_ref() {
1454 url.clone()
1455 } else if let Some(path) = self.path.as_ref() {
1456 DisplaySafeUrl::from_url(
1457 Url::from_directory_path(install_path.join(path))
1458 .map_err(|()| PylockTomlErrorKind::PathToUrl)?,
1459 )
1460 } else {
1461 return Err(PylockTomlErrorKind::VcsMissingPathUrl(name.clone()));
1462 };
1463 url.set_fragment(None);
1464 url.set_query(None);
1465
1466 let reference = self
1467 .requested_revision
1468 .clone()
1469 .map(GitReference::from_rev)
1470 .unwrap_or_else(|| GitReference::BranchOrTagOrCommit(self.commit_id.to_string()));
1471 let precise = self.commit_id;
1472
1473 GitUrl::from_commit(url, reference, precise, GitLfs::from_env())?
1475 };
1476
1477 let url = DisplaySafeUrl::from(ParsedGitUrl {
1479 url: git_url.clone(),
1480 subdirectory: subdirectory.clone(),
1481 });
1482
1483 Ok(GitSourceDist {
1484 name: name.clone(),
1485 git: Box::new(git_url),
1486 subdirectory: self.subdirectory.clone().map(Box::<Path>::from),
1487 url: VerbatimUrl::from_url(url),
1488 })
1489 }
1490}
1491
1492impl PylockTomlSdist {
1493 fn filename(&self, name: &PackageName) -> Result<Cow<'_, SmallString>, PylockTomlErrorKind> {
1495 if let Some(name) = self.name.as_ref() {
1496 Ok(Cow::Borrowed(name))
1497 } else if let Some(path) = self.path.as_ref() {
1498 let Some(filename) = path.as_ref().file_name().and_then(OsStr::to_str) else {
1499 return Err(PylockTomlErrorKind::PathMissingFilename(Box::<Path>::from(
1500 path.clone(),
1501 )));
1502 };
1503 Ok(Cow::Owned(SmallString::from(filename)))
1504 } else if let Some(url) = self.url.as_ref() {
1505 let Some(filename) = url.filename().ok() else {
1506 return Err(PylockTomlErrorKind::UrlMissingFilename(url.clone()));
1507 };
1508 Ok(Cow::Owned(SmallString::from(filename)))
1509 } else {
1510 Err(PylockTomlErrorKind::SdistMissingPathUrl(name.clone()))
1511 }
1512 }
1513
1514 fn to_sdist(
1516 &self,
1517 install_path: &Path,
1518 name: &PackageName,
1519 version: Option<&Version>,
1520 index: Option<&DisplaySafeUrl>,
1521 ) -> Result<RegistrySourceDist, PylockTomlErrorKind> {
1522 let filename = self.filename(name)?.into_owned();
1523 let ext = SourceDistExtension::from_path(filename.as_ref())?;
1524
1525 let version = if let Some(version) = version {
1526 Cow::Borrowed(version)
1527 } else {
1528 let filename = SourceDistFilename::parse(&filename, ext, name)?;
1529 Cow::Owned(filename.version)
1530 };
1531
1532 let file_url = if let Some(url) = self.url.as_ref() {
1533 UrlString::from(url)
1534 } else if let Some(path) = self.path.as_ref() {
1535 let path = install_path.join(path);
1536 let url = DisplaySafeUrl::from_file_path(path)
1537 .map_err(|()| PylockTomlErrorKind::PathToUrl)?;
1538 UrlString::from(url)
1539 } else {
1540 return Err(PylockTomlErrorKind::SdistMissingPathUrl(name.clone()));
1541 };
1542
1543 let index = if let Some(index) = index {
1544 IndexUrl::from(VerbatimUrl::from_url(index.clone()))
1545 } else {
1546 let mut index = file_url.to_url().map_err(PylockTomlErrorKind::ToUrl)?;
1551 index.path_segments_mut().unwrap().pop();
1552 IndexUrl::from(VerbatimUrl::from_url(index))
1553 };
1554
1555 let file = Box::new(uv_distribution_types::File {
1556 dist_info_metadata: false,
1557 filename,
1558 hashes: HashDigests::from(self.hashes.clone()),
1559 requires_python: None,
1560 size: self.size,
1561 upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
1562 url: FileLocation::AbsoluteUrl(file_url),
1563 yanked: None,
1564 zstd: None,
1565 });
1566
1567 Ok(RegistrySourceDist {
1568 name: name.clone(),
1569 version: version.into_owned(),
1570 file,
1571 ext,
1572 index,
1573 wheels: vec![],
1574 })
1575 }
1576}
1577
1578impl PylockTomlArchive {
1579 fn to_dist(
1580 &self,
1581 install_path: &Path,
1582 name: &PackageName,
1583 version: Option<&Version>,
1584 ) -> Result<Dist, PylockTomlErrorKind> {
1585 if let Some(url) = self.url.as_ref() {
1586 let filename = url
1587 .filename()
1588 .map_err(|_| PylockTomlErrorKind::UrlMissingFilename(url.clone()))?;
1589
1590 let ext = DistExtension::from_path(filename.as_ref())?;
1591 match ext {
1592 DistExtension::Wheel => {
1593 let filename = WheelFilename::from_str(&filename)?;
1594 Ok(Dist::Built(BuiltDist::DirectUrl(DirectUrlBuiltDist {
1595 filename,
1596 location: Box::new(url.clone()),
1597 url: VerbatimUrl::from_url(url.clone()),
1598 })))
1599 }
1600 DistExtension::Source(ext) => {
1601 Ok(Dist::Source(SourceDist::DirectUrl(DirectUrlSourceDist {
1602 name: name.clone(),
1603 location: Box::new(url.clone()),
1604 subdirectory: self.subdirectory.clone().map(Box::<Path>::from),
1605 ext,
1606 url: VerbatimUrl::from_url(url.clone()),
1607 })))
1608 }
1609 }
1610 } else if let Some(path) = self.path.as_ref() {
1611 let filename = path
1612 .as_ref()
1613 .file_name()
1614 .and_then(OsStr::to_str)
1615 .ok_or_else(|| {
1616 PylockTomlErrorKind::PathMissingFilename(Box::<Path>::from(path.clone()))
1617 })?;
1618
1619 let ext = DistExtension::from_path(filename)?;
1620 match ext {
1621 DistExtension::Wheel => {
1622 let filename = WheelFilename::from_str(filename)?;
1623 let install_path = install_path.join(path);
1624 let url = VerbatimUrl::from_absolute_path(&install_path)
1625 .map_err(|_| PylockTomlErrorKind::PathToUrl)?;
1626 Ok(Dist::Built(BuiltDist::Path(PathBuiltDist {
1627 filename,
1628 install_path: install_path.into_boxed_path(),
1629 url,
1630 })))
1631 }
1632 DistExtension::Source(ext) => {
1633 let install_path = install_path.join(path);
1634 let url = VerbatimUrl::from_absolute_path(&install_path)
1635 .map_err(|_| PylockTomlErrorKind::PathToUrl)?;
1636 Ok(Dist::Source(SourceDist::Path(PathSourceDist {
1637 name: name.clone(),
1638 version: version.cloned(),
1639 install_path: install_path.into_boxed_path(),
1640 ext,
1641 url,
1642 })))
1643 }
1644 }
1645 } else {
1646 Err(PylockTomlErrorKind::ArchiveMissingPathUrl(name.clone()))
1647 }
1648 }
1649
1650 fn is_wheel(&self, name: &PackageName) -> Result<bool, PylockTomlErrorKind> {
1652 if let Some(url) = self.url.as_ref() {
1653 let filename = url
1654 .filename()
1655 .map_err(|_| PylockTomlErrorKind::UrlMissingFilename(url.clone()))?;
1656
1657 let ext = DistExtension::from_path(filename.as_ref())?;
1658 Ok(matches!(ext, DistExtension::Wheel))
1659 } else if let Some(path) = self.path.as_ref() {
1660 let filename = path
1661 .as_ref()
1662 .file_name()
1663 .and_then(OsStr::to_str)
1664 .ok_or_else(|| {
1665 PylockTomlErrorKind::PathMissingFilename(Box::<Path>::from(path.clone()))
1666 })?;
1667
1668 let ext = DistExtension::from_path(filename)?;
1669 Ok(matches!(ext, DistExtension::Wheel))
1670 } else {
1671 Err(PylockTomlErrorKind::ArchiveMissingPathUrl(name.clone()))
1672 }
1673 }
1674}
1675
1676#[expect(clippy::ref_option)]
1678fn timestamp_to_toml_datetime<S>(
1679 timestamp: &Option<Timestamp>,
1680 serializer: S,
1681) -> Result<S::Ok, S::Error>
1682where
1683 S: serde::Serializer,
1684{
1685 let Some(timestamp) = timestamp else {
1686 return serializer.serialize_none();
1687 };
1688 let timestamp = timestamp.to_zoned(TimeZone::UTC);
1689 let timestamp = toml_edit::Datetime {
1690 date: Some(toml_edit::Date {
1691 year: u16::try_from(timestamp.year()).map_err(serde::ser::Error::custom)?,
1692 month: u8::try_from(timestamp.month()).map_err(serde::ser::Error::custom)?,
1693 day: u8::try_from(timestamp.day()).map_err(serde::ser::Error::custom)?,
1694 }),
1695 time: Some(toml_edit::Time {
1696 hour: u8::try_from(timestamp.hour()).map_err(serde::ser::Error::custom)?,
1697 minute: u8::try_from(timestamp.minute()).map_err(serde::ser::Error::custom)?,
1698 second: u8::try_from(timestamp.second()).map_err(serde::ser::Error::custom)?,
1699 nanosecond: u32::try_from(timestamp.nanosecond()).map_err(serde::ser::Error::custom)?,
1700 }),
1701 offset: Some(toml_edit::Offset::Z),
1702 };
1703 serializer.serialize_some(×tamp)
1704}
1705
1706fn timestamp_from_toml_datetime<'de, D>(deserializer: D) -> Result<Option<Timestamp>, D::Error>
1708where
1709 D: serde::Deserializer<'de>,
1710{
1711 let Some(datetime) = Option::<toml_edit::Datetime>::deserialize(deserializer)? else {
1712 return Ok(None);
1713 };
1714
1715 let Some(date) = datetime.date else {
1717 return Err(serde::de::Error::custom("missing date"));
1718 };
1719
1720 let year = i16::try_from(date.year).map_err(serde::de::Error::custom)?;
1721 let month = i8::try_from(date.month).map_err(serde::de::Error::custom)?;
1722 let day = i8::try_from(date.day).map_err(serde::de::Error::custom)?;
1723 let date = Date::new(year, month, day).map_err(serde::de::Error::custom)?;
1724
1725 let tz = if let Some(offset) = datetime.offset {
1727 match offset {
1728 toml_edit::Offset::Z => TimeZone::UTC,
1729 toml_edit::Offset::Custom { minutes } => {
1730 let hours = i8::try_from(minutes / 60).map_err(serde::de::Error::custom)?;
1731 TimeZone::fixed(Offset::constant(hours))
1732 }
1733 }
1734 } else {
1735 TimeZone::UTC
1736 };
1737
1738 let time = if let Some(time) = datetime.time {
1740 let hour = i8::try_from(time.hour).map_err(serde::de::Error::custom)?;
1741 let minute = i8::try_from(time.minute).map_err(serde::de::Error::custom)?;
1742 let second = i8::try_from(time.second).map_err(serde::de::Error::custom)?;
1743 let nanosecond = i32::try_from(time.nanosecond).map_err(serde::de::Error::custom)?;
1744 Time::new(hour, minute, second, nanosecond).map_err(serde::de::Error::custom)?
1745 } else {
1746 Time::midnight()
1747 };
1748
1749 let timestamp = tz
1750 .to_timestamp(DateTime::from_parts(date, time))
1751 .map_err(serde::de::Error::custom)?;
1752 Ok(Some(timestamp))
1753}