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