Skip to main content

uv_requirements/
source_tree.rs

1use 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    /// Return the [`Path`] to the file representing the source tree (e.g., the `pyproject.toml`).
32    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    /// Return the [`PyProjectToml`] if this is a `pyproject.toml`-based source tree.
41    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    /// The requirements sourced from the source trees.
52    requirements: Box<[Requirement]>,
53    /// The names of the projects that were resolved.
54    project: PackageName,
55    /// The extras used when resolving the requirements.
56    extras: Box<[ExtraName]>,
57}
58
59impl SourceTreeResolution {
60    /// Return the name of the project that was resolved.
61    pub fn project(&self) -> &PackageName {
62        &self.project
63    }
64
65    /// Return the extras used when resolving the requirements.
66    pub fn extras(&self) -> &[ExtraName] {
67        &self.extras
68    }
69
70    /// Return the requirements sourced from the source tree.
71    pub fn into_requirements(self) -> Box<[Requirement]> {
72        self.requirements
73    }
74}
75
76/// A resolver for requirements specified via source trees.
77///
78/// Used, e.g., to determine the input requirements when a user specifies a `pyproject.toml`
79/// file, which may require running PEP 517 build hooks to extract metadata.
80pub struct SourceTreeResolver<'a, Context: BuildContext> {
81    /// The extras to include when resolving requirements.
82    extras: &'a ExtrasSpecification,
83    /// The hash policy to enforce.
84    hasher: &'a HashStrategy,
85    /// The in-memory index for resolving dependencies.
86    index: &'a InMemoryIndex,
87    /// The database for fetching and building distributions.
88    database: DistributionDatabase<'a, Context>,
89}
90
91impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
92    /// Instantiate a new [`SourceTreeResolver`] for a given set of `source_trees`.
93    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    /// Set the [`Reporter`] to use for this resolver.
108    #[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    /// Resolve the requirements from the provided source trees.
117    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    /// Infer the dependencies for a directory dependency.
130    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        // Determine the extras to include when resolving the requirements.
136        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        // Flatten any transitive extras and include dependencies
145        // (unless something like --only-group was passed)
146        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    /// Resolve the [`RequiresDist`] metadata for a given source tree. Attempts to resolve the
168    /// requirements without building the distribution, even if the project contains (e.g.) a
169    /// dynamic version since, critically, we don't need to install the package itself; only its
170    /// dependencies.
171    async fn resolve_requires_dist(&self, source_tree: &SourceTree) -> Result<RequiresDist> {
172        // Convert to a buildable source.
173        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 the path is a `pyproject.toml`, attempt to extract the requirements statically. The
187        // distribution database will do this too, but we can be even more aggressive here since we
188        // _only_ need the requirements. So, for example, even if the version is dynamic, we can
189        // still extract the requirements without performing a build, unlike in the database where
190        // we typically construct a "complete" metadata object.
191        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        // Determine the hash policy. Since we don't have a package name, we perform a
207        // manual match.
208        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        // Fetch the metadata for the distribution.
221        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                // Run the PEP 517 build process to extract metadata from the source distribution.
230                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                // Insert the metadata into the index.
236                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}