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, ParsedGitDirectoryUrl, ParsedGitPathUrl};
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 = if let Some(install_path) = git.path.as_ref() {
100 let ext =
101 DistExtension::from_path(install_path).map_err(|_| std::fmt::Error)?;
102 DisplaySafeUrl::from(ParsedGitPathUrl {
103 url: git_url.clone(),
104 install_path: install_path.clone(),
105 ext,
106 })
107 } else {
108 DisplaySafeUrl::from(ParsedGitDirectoryUrl {
109 url: git_url.clone(),
110 subdirectory: git.subdirectory.clone(),
111 })
112 };
113
114 write!(f, "{} @ {}", package.id.name, url)?;
115 }
116 Source::Direct(url, direct) => {
117 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
118 url: url.to_url().map_err(|_| std::fmt::Error)?,
119 subdirectory: direct.subdirectory.clone(),
120 ext: DistExtension::Source(SourceDistExtension::TarGz),
121 });
122 write!(
123 f,
124 "{} @ {}",
125 package.id.name,
126 url.displayable_with_credentials()
129 )?;
130 }
131 Source::Path(path) | Source::Directory(path) => {
132 if path.is_absolute() {
133 write!(
134 f,
135 "{}",
136 Url::from_file_path(path).map_err(|()| std::fmt::Error)?
137 )?;
138 } else {
139 write!(f, "{}", anchor(path).portable_display())?;
140 }
141 }
142 Source::Editable(path) => match self
143 .editable
144 .as_ref()
145 .and_then(|editable| editable.for_package(&package.id.name))
146 {
147 None | Some(true) => {
148 write!(f, "-e {}", anchor(path).portable_display())?;
149 }
150 Some(false) => {
151 if path.is_absolute() {
152 write!(
153 f,
154 "{}",
155 Url::from_file_path(path).map_err(|()| std::fmt::Error)?
156 )?;
157 } else {
158 write!(f, "{}", anchor(path).portable_display())?;
159 }
160 }
161 },
162 Source::Virtual(_) => {
163 continue;
164 }
165 }
166
167 if let Some(contents) = marker.contents() {
168 write!(f, " ; {contents}")?;
169 }
170
171 if self.hashes {
172 let mut hashes = package.hashes();
173 hashes.sort_unstable();
174 if !hashes.is_empty() {
175 for hash in hashes.iter() {
176 writeln!(f, " \\")?;
177 write!(f, " --hash=")?;
178 write!(f, "{hash}")?;
179 }
180 }
181 }
182
183 writeln!(f)?;
184
185 match dependents.as_slice() {
187 [] => {}
188 [dependent] => {
189 writeln!(f, "{}", format!(" # via {}", dependent.id.name).green())?;
190 }
191 _ => {
192 writeln!(f, "{}", " # via".green())?;
193 for &dependent in dependents {
194 writeln!(f, "{}", format!(" # {}", dependent.id.name).green())?;
195 }
196 }
197 }
198 }
199
200 Ok(())
201 }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
205enum RequirementComparator<'lock> {
206 Editable(&'lock Path),
207 Path(&'lock Path),
208 Package(&'lock PackageId),
209}
210
211impl<'lock> From<&'lock Package> for RequirementComparator<'lock> {
212 fn from(value: &'lock Package) -> Self {
213 match &value.id.source {
214 Source::Path(path) | Source::Directory(path) => Self::Path(path),
215 Source::Editable(path) => Self::Editable(path),
216 _ => Self::Package(&value.id),
217 }
218 }
219}
220
221fn anchor(path: &Path) -> Cow<'_, Path> {
225 match path.components().next() {
226 None => Cow::Owned(PathBuf::from(".")),
227 Some(Component::CurDir | Component::ParentDir) => Cow::Borrowed(path),
228 _ => Cow::Owned(PathBuf::from("./").join(path)),
229 }
230}