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