uv_requirements/
source_tree.rs

1use 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    /// Return the [`Path`] to the file representing the source tree (e.g., the `pyproject.toml`).
33    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    /// Return the [`PyProjectToml`] if this is a `pyproject.toml`-based source tree.
42    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    /// The requirements sourced from the source trees.
53    pub requirements: Box<[Requirement]>,
54    /// The names of the projects that were resolved.
55    pub project: PackageName,
56    /// The extras used when resolving the requirements.
57    pub extras: Box<[ExtraName]>,
58}
59
60/// A resolver for requirements specified via source trees.
61///
62/// Used, e.g., to determine the input requirements when a user specifies a `pyproject.toml`
63/// file, which may require running PEP 517 build hooks to extract metadata.
64pub struct SourceTreeResolver<'a, Context: BuildContext> {
65    /// The extras to include when resolving requirements.
66    extras: &'a ExtrasSpecification,
67    /// The hash policy to enforce.
68    hasher: &'a HashStrategy,
69    /// The in-memory index for resolving dependencies.
70    index: &'a InMemoryIndex,
71    /// The database for fetching and building distributions.
72    database: DistributionDatabase<'a, Context>,
73}
74
75impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
76    /// Instantiate a new [`SourceTreeResolver`] for a given set of `source_trees`.
77    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    /// Set the [`Reporter`] to use for this resolver.
92    #[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    /// Resolve the requirements from the provided source trees.
101    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    /// Infer the dependencies for a directory dependency.
114    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        // Determine the extras to include when resolving the requirements.
120        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        // Flatten any transitive extras and include dependencies
129        // (unless something like --only-group was passed)
130        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    /// Resolve the [`RequiresDist`] metadata for a given source tree. Attempts to resolve the
152    /// requirements without building the distribution, even if the project contains (e.g.) a
153    /// dynamic version since, critically, we don't need to install the package itself; only its
154    /// dependencies.
155    async fn resolve_requires_dist(&self, source_tree: &SourceTree) -> Result<RequiresDist> {
156        // Convert to a buildable source.
157        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 the path is a `pyproject.toml`, attempt to extract the requirements statically. The
171        // distribution database will do this too, but we can be even more aggressive here since we
172        // _only_ need the requirements. So, for example, even if the version is dynamic, we can
173        // still extract the requirements without performing a build, unlike in the database where
174        // we typically construct a "complete" metadata object.
175        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        // Determine the hash policy. Since we don't have a package name, we perform a
191        // manual match.
192        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        // Fetch the metadata for the distribution.
205        let metadata = {
206            let id = VersionId::from_url(source.url());
207            if self.index.distributions().register(id.clone()) {
208                // Run the PEP 517 build process to extract metadata from the source distribution.
209                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                // Insert the metadata into the index.
215                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}