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