1use std::borrow::Cow;
2use std::fmt::Formatter;
3use std::path::{Component, Path, PathBuf};
4
5use owo_colors::OwoColorize;
6use url::Url;
7
8use uv_configuration::{
9 DependencyGroupsWithDefaults, EditableMode, ExtrasSpecificationWithDefaults, InstallOptions,
10};
11use uv_distribution_filename::{DistExtension, SourceDistExtension};
12use uv_fs::Simplified;
13use uv_git_types::GitReference;
14use uv_normalize::PackageName;
15use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl};
16use uv_redacted::DisplaySafeUrl;
17
18use crate::lock::export::{ExportableRequirement, ExportableRequirements};
19use crate::lock::{Package, PackageId, Source};
20use crate::{Installable, LockError};
21
22#[derive(Debug)]
24pub struct RequirementsTxtExport<'lock> {
25 nodes: Vec<ExportableRequirement<'lock>>,
26 hashes: bool,
27 editable: Option<EditableMode>,
28}
29
30impl<'lock> RequirementsTxtExport<'lock> {
31 pub fn from_lock(
32 target: &impl Installable<'lock>,
33 prune: &[PackageName],
34 extras: &ExtrasSpecificationWithDefaults,
35 dev: &DependencyGroupsWithDefaults,
36 annotate: bool,
37 editable: Option<EditableMode>,
38 hashes: bool,
39 install_options: &'lock InstallOptions,
40 ) -> Result<Self, LockError> {
41 let ExportableRequirements(mut nodes) = ExportableRequirements::from_lock(
43 target,
44 prune,
45 extras,
46 dev,
47 annotate,
48 install_options,
49 )?;
50
51 nodes.sort_unstable_by(|a, b| {
53 RequirementComparator::from(a.package).cmp(&RequirementComparator::from(b.package))
54 });
55
56 Ok(Self {
57 nodes,
58 hashes,
59 editable,
60 })
61 }
62}
63
64impl std::fmt::Display for RequirementsTxtExport<'_> {
65 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
66 for ExportableRequirement {
68 package,
69 marker,
70 dependents,
71 } in &self.nodes
72 {
73 match &package.id.source {
74 Source::Registry(_) => {
75 let version = package
76 .id
77 .version
78 .as_ref()
79 .expect("registry package without version");
80 write!(f, "{}=={}", package.id.name, version)?;
81 }
82 Source::Git(url, git) => {
83 let mut url = url.to_url().map_err(|_| std::fmt::Error)?;
86 url.set_fragment(None);
87 url.set_query(None);
88
89 let git_url = uv_git_types::GitUrl::from_commit(
91 url,
92 GitReference::from(git.kind.clone()),
93 git.precise,
94 git.lfs,
95 )
96 .expect("Internal Git URLs must have supported schemes");
97
98 let url = DisplaySafeUrl::from(ParsedGitUrl {
100 url: git_url.clone(),
101 subdirectory: git.subdirectory.clone(),
102 });
103
104 write!(f, "{} @ {}", package.id.name, url)?;
105 }
106 Source::Direct(url, direct) => {
107 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
108 url: url.to_url().map_err(|_| std::fmt::Error)?,
109 subdirectory: direct.subdirectory.clone(),
110 ext: DistExtension::Source(SourceDistExtension::TarGz),
111 });
112 write!(
113 f,
114 "{} @ {}",
115 package.id.name,
116 url.displayable_with_credentials()
119 )?;
120 }
121 Source::Path(path) | Source::Directory(path) => {
122 if path.is_absolute() {
123 write!(
124 f,
125 "{}",
126 Url::from_file_path(path).map_err(|()| std::fmt::Error)?
127 )?;
128 } else {
129 write!(f, "{}", anchor(path).portable_display())?;
130 }
131 }
132 Source::Editable(path) => match self.editable {
133 None | Some(EditableMode::Editable) => {
134 write!(f, "-e {}", anchor(path).portable_display())?;
135 }
136 Some(EditableMode::NonEditable) => {
137 if path.is_absolute() {
138 write!(
139 f,
140 "{}",
141 Url::from_file_path(path).map_err(|()| std::fmt::Error)?
142 )?;
143 } else {
144 write!(f, "{}", anchor(path).portable_display())?;
145 }
146 }
147 },
148 Source::Virtual(_) => {
149 continue;
150 }
151 }
152
153 if let Some(contents) = marker.contents() {
154 write!(f, " ; {contents}")?;
155 }
156
157 if self.hashes {
158 let mut hashes = package.hashes();
159 hashes.sort_unstable();
160 if !hashes.is_empty() {
161 for hash in hashes.iter() {
162 writeln!(f, " \\")?;
163 write!(f, " --hash=")?;
164 write!(f, "{hash}")?;
165 }
166 }
167 }
168
169 writeln!(f)?;
170
171 match dependents.as_slice() {
173 [] => {}
174 [dependent] => {
175 writeln!(f, "{}", format!(" # via {}", dependent.id.name).green())?;
176 }
177 _ => {
178 writeln!(f, "{}", " # via".green())?;
179 for &dependent in dependents {
180 writeln!(f, "{}", format!(" # {}", dependent.id.name).green())?;
181 }
182 }
183 }
184 }
185
186 Ok(())
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
191enum RequirementComparator<'lock> {
192 Editable(&'lock Path),
193 Path(&'lock Path),
194 Package(&'lock PackageId),
195}
196
197impl<'lock> From<&'lock Package> for RequirementComparator<'lock> {
198 fn from(value: &'lock Package) -> Self {
199 match &value.id.source {
200 Source::Path(path) | Source::Directory(path) => Self::Path(path),
201 Source::Editable(path) => Self::Editable(path),
202 _ => Self::Package(&value.id),
203 }
204 }
205}
206
207fn anchor(path: &Path) -> Cow<'_, Path> {
211 match path.components().next() {
212 None => Cow::Owned(PathBuf::from(".")),
213 Some(Component::CurDir | Component::ParentDir) => Cow::Borrowed(path),
214 _ => Cow::Owned(PathBuf::from("./").join(path)),
215 }
216}