uv_requirements/
source_tree.rs1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use anyhow::{Context, Result};
5use futures::TryStreamExt;
6use futures::stream::FuturesOrdered;
7use url::Url;
8
9use uv_configuration::ExtrasSpecification;
10use uv_distribution::{DistributionDatabase, FlatRequiresDist, Reporter, RequiresDist};
11use uv_distribution_types::Requirement;
12use uv_distribution_types::{
13 BuildableSource, DirectorySourceUrl, HashGeneration, HashPolicy, Identifier, SourceUrl,
14};
15use uv_fs::Simplified;
16use uv_normalize::{ExtraName, PackageName};
17use uv_pep508::RequirementOrigin;
18use uv_pypi_types::PyProjectToml;
19use uv_redacted::DisplaySafeUrl;
20use uv_resolver::{InMemoryIndex, MetadataResponse};
21use uv_types::{BuildContext, HashStrategy};
22
23#[derive(Debug, Clone)]
24pub enum SourceTree {
25 PyProjectToml(PathBuf, PyProjectToml),
26 SetupPy(PathBuf),
27 SetupCfg(PathBuf),
28}
29
30impl SourceTree {
31 fn path(&self) -> &Path {
33 match self {
34 Self::PyProjectToml(path, ..) => path,
35 Self::SetupPy(path) => path,
36 Self::SetupCfg(path) => path,
37 }
38 }
39
40 fn pyproject_toml(&self) -> Option<&PyProjectToml> {
42 match self {
43 Self::PyProjectToml(.., toml) => Some(toml),
44 _ => None,
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
50pub struct SourceTreeResolution {
51 requirements: Box<[Requirement]>,
53 project: PackageName,
55 extras: Box<[ExtraName]>,
57}
58
59impl SourceTreeResolution {
60 pub fn project(&self) -> &PackageName {
62 &self.project
63 }
64
65 pub fn extras(&self) -> &[ExtraName] {
67 &self.extras
68 }
69
70 pub fn into_requirements(self) -> Box<[Requirement]> {
72 self.requirements
73 }
74}
75
76pub struct SourceTreeResolver<'a, Context: BuildContext> {
81 extras: &'a ExtrasSpecification,
83 hasher: &'a HashStrategy,
85 index: &'a InMemoryIndex,
87 database: DistributionDatabase<'a, Context>,
89}
90
91impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
92 pub fn new(
94 extras: &'a ExtrasSpecification,
95 hasher: &'a HashStrategy,
96 index: &'a InMemoryIndex,
97 database: DistributionDatabase<'a, Context>,
98 ) -> Self {
99 Self {
100 extras,
101 hasher,
102 index,
103 database,
104 }
105 }
106
107 #[must_use]
109 pub fn with_reporter(self, reporter: Arc<dyn Reporter>) -> Self {
110 Self {
111 database: self.database.with_reporter(reporter),
112 ..self
113 }
114 }
115
116 pub async fn resolve(
118 self,
119 source_trees: impl Iterator<Item = &SourceTree>,
120 ) -> Result<Vec<SourceTreeResolution>> {
121 let resolutions: Vec<_> = source_trees
122 .map(async |source_tree| self.resolve_source_tree(source_tree).await)
123 .collect::<FuturesOrdered<_>>()
124 .try_collect()
125 .await?;
126 Ok(resolutions)
127 }
128
129 async fn resolve_source_tree(&self, source_tree: &SourceTree) -> Result<SourceTreeResolution> {
131 let metadata = self.resolve_requires_dist(source_tree).await?;
132 let origin =
133 RequirementOrigin::Project(source_tree.path().to_path_buf(), metadata.name.clone());
134
135 let extras = self
137 .extras
138 .extra_names(metadata.provides_extra.iter())
139 .cloned()
140 .collect::<Vec<_>>();
141
142 let mut requirements = Vec::new();
143
144 requirements.extend(
147 FlatRequiresDist::from_requirements(metadata.requires_dist, &metadata.name)
148 .into_iter()
149 .map(|requirement| Requirement {
150 origin: Some(origin.clone()),
151 marker: requirement.marker.simplify_extras(&extras),
152 ..requirement
153 }),
154 );
155
156 let requirements = requirements.into_boxed_slice();
157 let project = metadata.name;
158 let extras = metadata.provides_extra;
159
160 Ok(SourceTreeResolution {
161 requirements,
162 project,
163 extras,
164 })
165 }
166
167 async fn resolve_requires_dist(&self, source_tree: &SourceTree) -> Result<RequiresDist> {
172 let path = fs_err::canonicalize(source_tree.path()).with_context(|| {
174 format!(
175 "Failed to canonicalize path to source tree: {}",
176 source_tree.path().user_display()
177 )
178 })?;
179 let path = path.parent().ok_or_else(|| {
180 anyhow::anyhow!(
181 "The file `{}` appears to be a `pyproject.toml`, `setup.py`, or `setup.cfg` file, which must be in a directory",
182 path.user_display()
183 )
184 })?;
185
186 if let Some(pyproject_toml) = source_tree.pyproject_toml() {
192 if let Some(metadata) = self.database.requires_dist(path, pyproject_toml).await? {
193 return Ok(metadata);
194 }
195 }
196
197 let Ok(url) = Url::from_directory_path(path).map(DisplaySafeUrl::from_url) else {
198 return Err(anyhow::anyhow!("Failed to convert path to URL"));
199 };
200 let source = SourceUrl::Directory(DirectorySourceUrl {
201 url: &url,
202 install_path: path,
203 editable: None,
204 });
205
206 let hashes = match self.hasher {
209 HashStrategy::None => HashPolicy::None,
210 HashStrategy::Generate(mode) => HashPolicy::Generate(*mode),
211 HashStrategy::Verify(_) => HashPolicy::Generate(HashGeneration::All),
212 HashStrategy::Require(_) => {
213 return Err(anyhow::anyhow!(
214 "Hash-checking is not supported for local directories: {}",
215 path.user_display()
216 ));
217 }
218 };
219
220 let metadata = {
222 let id = source.distribution_id();
223 if let Some(response) = self.index.distributions().register_or_wait(&id).await {
224 let MetadataResponse::Found(archive) = &*response else {
225 panic!("Failed to find metadata for: {}", path.user_display());
226 };
227 archive.metadata.clone()
228 } else {
229 let source = BuildableSource::Url(source);
231 let archive = self.database.build_wheel_metadata(&source, hashes).await?;
232
233 let metadata = archive.metadata.clone();
234
235 self.index
237 .distributions()
238 .done(id, Arc::new(MetadataResponse::Found(archive)));
239
240 metadata
241 }
242 };
243
244 Ok(RequiresDist::from(metadata))
245 }
246}