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