Skip to main content

uv_configuration/
build_options.rs

1use uv_normalize::PackageName;
2pub use uv_pypi_types::BuildKind;
3
4use crate::{PackageNameSpecifier, PackageNameSpecifiers};
5
6#[derive(Debug, Copy, Clone, PartialEq, Eq)]
7pub enum BuildOutput {
8    /// Send the build backend output to `stderr`.
9    Stderr,
10    /// Send the build backend output to `tracing`.
11    Debug,
12    /// Do not display the build backend output.
13    Quiet,
14}
15
16#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
17#[serde(rename_all = "kebab-case", deny_unknown_fields)]
18pub struct BuildOptions {
19    no_binary: NoBinary,
20    no_build: NoBuild,
21}
22
23impl BuildOptions {
24    pub fn new(no_binary: NoBinary, no_build: NoBuild) -> Self {
25        Self {
26            no_binary,
27            no_build,
28        }
29    }
30
31    #[must_use]
32    pub fn combine(self, no_binary: NoBinary, no_build: NoBuild) -> Self {
33        Self {
34            no_binary: self.no_binary.combine(no_binary),
35            no_build: self.no_build.combine(no_build),
36        }
37    }
38
39    pub fn no_binary_package(&self, package_name: &PackageName) -> bool {
40        match &self.no_binary {
41            NoBinary::None => false,
42            NoBinary::All => match &self.no_build {
43                // Allow `all` to be overridden by specific build exclusions
44                NoBuild::Packages(packages) => !packages.contains(package_name),
45                _ => true,
46            },
47            NoBinary::Packages(packages) => packages.contains(package_name),
48        }
49    }
50
51    pub fn no_build_package(&self, package_name: &PackageName) -> bool {
52        match &self.no_build {
53            NoBuild::All => match &self.no_binary {
54                // Allow `all` to be overridden by specific binary exclusions
55                NoBinary::Packages(packages) => !packages.contains(package_name),
56                _ => true,
57            },
58            NoBuild::None => false,
59            NoBuild::Packages(packages) => packages.contains(package_name),
60        }
61    }
62
63    pub fn no_build_requirement(&self, package_name: Option<&PackageName>) -> bool {
64        match package_name {
65            Some(name) => self.no_build_package(name),
66            None => self.no_build_all(),
67        }
68    }
69
70    fn no_build_all(&self) -> bool {
71        matches!(self.no_build, NoBuild::All)
72    }
73
74    /// Return the [`NoBuild`] strategy to use.
75    pub fn no_build(&self) -> &NoBuild {
76        &self.no_build
77    }
78
79    /// Return the [`NoBinary`] strategy to use.
80    pub fn no_binary(&self) -> &NoBinary {
81        &self.no_binary
82    }
83}
84
85#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
86#[serde(rename_all = "kebab-case", deny_unknown_fields)]
87pub enum NoBinary {
88    /// Allow installation of any wheel.
89    #[default]
90    None,
91
92    /// Do not allow installation from any wheels.
93    All,
94
95    /// Do not allow installation from the specific wheels.
96    Packages(Vec<PackageName>),
97}
98
99impl NoBinary {
100    /// Determine the binary installation strategy to use for the given arguments.
101    pub fn from_args(no_binary: Option<bool>, no_binary_package: Vec<PackageName>) -> Self {
102        match no_binary {
103            Some(true) => Self::All,
104            Some(false) => Self::None,
105            None => {
106                if no_binary_package.is_empty() {
107                    Self::None
108                } else {
109                    Self::Packages(no_binary_package)
110                }
111            }
112        }
113    }
114
115    /// Determine the binary installation strategy to use for the given arguments from the pip CLI.
116    pub fn from_pip_args(no_binary: Vec<PackageNameSpecifier>) -> Self {
117        let combined = PackageNameSpecifiers::from_iter(no_binary.into_iter());
118        match combined {
119            PackageNameSpecifiers::All => Self::All,
120            PackageNameSpecifiers::None => Self::None,
121            PackageNameSpecifiers::Packages(packages) => Self::Packages(packages),
122        }
123    }
124
125    /// Determine the binary installation strategy to use for the given argument from the pip CLI.
126    pub fn from_pip_arg(no_binary: PackageNameSpecifier) -> Self {
127        Self::from_pip_args(vec![no_binary])
128    }
129
130    /// Combine a set of [`NoBinary`] values.
131    #[must_use]
132    pub fn combine(self, other: Self) -> Self {
133        match (self, other) {
134            // If both are `None`, the result is `None`.
135            (Self::None, Self::None) => Self::None,
136            // If either is `All`, the result is `All`.
137            (Self::All, _) | (_, Self::All) => Self::All,
138            // If one is `None`, the result is the other.
139            (Self::Packages(a), Self::None) => Self::Packages(a),
140            (Self::None, Self::Packages(b)) => Self::Packages(b),
141            // If both are `Packages`, the result is the union of the two.
142            (Self::Packages(mut a), Self::Packages(b)) => {
143                a.extend(b);
144                Self::Packages(a)
145            }
146        }
147    }
148
149    /// Extend a [`NoBinary`] value with another.
150    pub fn extend(&mut self, other: Self) {
151        match (&mut *self, other) {
152            // If either is `All`, the result is `All`.
153            (Self::All, _) | (_, Self::All) => *self = Self::All,
154            // If both are `None`, the result is `None`.
155            (Self::None, Self::None) => {
156                // Nothing to do.
157            }
158            // If one is `None`, the result is the other.
159            (Self::Packages(_), Self::None) => {
160                // Nothing to do.
161            }
162            (Self::None, Self::Packages(b)) => {
163                // Take ownership of `b`.
164                *self = Self::Packages(b);
165            }
166            // If both are `Packages`, the result is the union of the two.
167            (Self::Packages(a), Self::Packages(b)) => {
168                a.extend(b);
169            }
170        }
171    }
172}
173
174impl NoBinary {
175    /// Returns `true` if all wheels are allowed.
176    pub fn is_none(&self) -> bool {
177        matches!(self, Self::None)
178    }
179}
180
181#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
182#[serde(rename_all = "kebab-case", deny_unknown_fields)]
183pub enum NoBuild {
184    /// Allow building wheels from any source distribution.
185    #[default]
186    None,
187
188    /// Do not allow building wheels from any source distribution.
189    All,
190
191    /// Do not allow building wheels from the given package's source distributions.
192    Packages(Vec<PackageName>),
193}
194
195impl NoBuild {
196    /// Determine the build strategy to use for the given arguments.
197    pub fn from_args(no_build: Option<bool>, no_build_package: Vec<PackageName>) -> Self {
198        match no_build {
199            Some(true) => Self::All,
200            Some(false) => Self::None,
201            None => {
202                if no_build_package.is_empty() {
203                    Self::None
204                } else {
205                    Self::Packages(no_build_package)
206                }
207            }
208        }
209    }
210
211    /// Determine the build strategy to use for the given arguments from the pip CLI.
212    pub fn from_pip_args(only_binary: Vec<PackageNameSpecifier>, no_build: bool) -> Self {
213        if no_build {
214            Self::All
215        } else {
216            let combined = PackageNameSpecifiers::from_iter(only_binary.into_iter());
217            match combined {
218                PackageNameSpecifiers::All => Self::All,
219                PackageNameSpecifiers::None => Self::None,
220                PackageNameSpecifiers::Packages(packages) => Self::Packages(packages),
221            }
222        }
223    }
224
225    /// Determine the build strategy to use for the given argument from the pip CLI.
226    pub fn from_pip_arg(no_build: PackageNameSpecifier) -> Self {
227        Self::from_pip_args(vec![no_build], false)
228    }
229
230    /// Combine a set of [`NoBuild`] values.
231    #[must_use]
232    pub fn combine(self, other: Self) -> Self {
233        match (self, other) {
234            // If both are `None`, the result is `None`.
235            (Self::None, Self::None) => Self::None,
236            // If either is `All`, the result is `All`.
237            (Self::All, _) | (_, Self::All) => Self::All,
238            // If one is `None`, the result is the other.
239            (Self::Packages(a), Self::None) => Self::Packages(a),
240            (Self::None, Self::Packages(b)) => Self::Packages(b),
241            // If both are `Packages`, the result is the union of the two.
242            (Self::Packages(mut a), Self::Packages(b)) => {
243                a.extend(b);
244                Self::Packages(a)
245            }
246        }
247    }
248
249    /// Extend a [`NoBuild`] value with another.
250    pub fn extend(&mut self, other: Self) {
251        match (&mut *self, other) {
252            // If either is `All`, the result is `All`.
253            (Self::All, _) | (_, Self::All) => *self = Self::All,
254            // If both are `None`, the result is `None`.
255            (Self::None, Self::None) => {
256                // Nothing to do.
257            }
258            // If one is `None`, the result is the other.
259            (Self::Packages(_), Self::None) => {
260                // Nothing to do.
261            }
262            (Self::None, Self::Packages(b)) => {
263                // Take ownership of `b`.
264                *self = Self::Packages(b);
265            }
266            // If both are `Packages`, the result is the union of the two.
267            (Self::Packages(a), Self::Packages(b)) => {
268                a.extend(b);
269            }
270        }
271    }
272}
273
274impl NoBuild {
275    /// Returns `true` if all builds are allowed.
276    pub fn is_none(&self) -> bool {
277        matches!(self, Self::None)
278    }
279}
280
281#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
282#[serde(deny_unknown_fields, rename_all = "kebab-case")]
283#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
284#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
285pub enum IndexStrategy {
286    /// Only use results from the first index that returns a match for a given package name.
287    ///
288    /// While this differs from pip's behavior, it's the default index strategy as it's the most
289    /// secure.
290    #[default]
291    #[cfg_attr(feature = "clap", clap(alias = "first-match"))]
292    FirstIndex,
293    /// Search for every package name across all indexes, exhausting the versions from the first
294    /// index before moving on to the next.
295    ///
296    /// In this strategy, we look for every package across all indexes. When resolving, we attempt
297    /// to use versions from the indexes in order, such that we exhaust all available versions from
298    /// the first index before moving on to the next. Further, if a version is found to be
299    /// incompatible in the first index, we do not reconsider that version in subsequent indexes,
300    /// even if the secondary index might contain compatible versions (e.g., variants of the same
301    /// versions with different ABI tags or Python version constraints).
302    ///
303    /// See: <https://peps.python.org/pep-0708/>
304    #[cfg_attr(feature = "clap", clap(alias = "unsafe-any-match"))]
305    #[serde(alias = "unsafe-any-match")]
306    UnsafeFirstMatch,
307    /// Search for every package name across all indexes, preferring the "best" version found. If a
308    /// package version is in multiple indexes, only look at the entry for the first index.
309    ///
310    /// In this strategy, we look for every package across all indexes. When resolving, we consider
311    /// all versions from all indexes, choosing the "best" version found (typically, the highest
312    /// compatible version).
313    ///
314    /// This most closely matches pip's behavior, but exposes the resolver to "dependency confusion"
315    /// attacks whereby malicious actors can publish packages to public indexes with the same name
316    /// as internal packages, causing the resolver to install the malicious package in lieu of
317    /// the intended internal package.
318    ///
319    /// See: <https://peps.python.org/pep-0708/>
320    UnsafeBestMatch,
321}
322
323#[cfg(test)]
324mod tests {
325    use std::str::FromStr;
326
327    use anyhow::Error;
328
329    use super::*;
330
331    #[test]
332    fn no_build_from_args() -> Result<(), Error> {
333        assert_eq!(
334            NoBuild::from_pip_args(vec![PackageNameSpecifier::from_str(":all:")?], false),
335            NoBuild::All,
336        );
337        assert_eq!(
338            NoBuild::from_pip_args(vec![PackageNameSpecifier::from_str(":all:")?], true),
339            NoBuild::All,
340        );
341        assert_eq!(
342            NoBuild::from_pip_args(vec![PackageNameSpecifier::from_str(":none:")?], true),
343            NoBuild::All,
344        );
345        assert_eq!(
346            NoBuild::from_pip_args(vec![PackageNameSpecifier::from_str(":none:")?], false),
347            NoBuild::None,
348        );
349        assert_eq!(
350            NoBuild::from_pip_args(
351                vec![
352                    PackageNameSpecifier::from_str("foo")?,
353                    PackageNameSpecifier::from_str("bar")?
354                ],
355                false
356            ),
357            NoBuild::Packages(vec![
358                PackageName::from_str("foo")?,
359                PackageName::from_str("bar")?
360            ]),
361        );
362        assert_eq!(
363            NoBuild::from_pip_args(
364                vec![
365                    PackageNameSpecifier::from_str("test")?,
366                    PackageNameSpecifier::All
367                ],
368                false
369            ),
370            NoBuild::All,
371        );
372        assert_eq!(
373            NoBuild::from_pip_args(
374                vec![
375                    PackageNameSpecifier::from_str("foo")?,
376                    PackageNameSpecifier::from_str(":none:")?,
377                    PackageNameSpecifier::from_str("bar")?
378                ],
379                false
380            ),
381            NoBuild::Packages(vec![PackageName::from_str("bar")?]),
382        );
383
384        Ok(())
385    }
386}