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