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