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