Skip to main content

uv_configuration/
build_options.rs

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