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