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