Skip to main content

uv_resolver/
prerelease.rs

1use std::borrow::Cow;
2use std::ops::{Deref, DerefMut};
3use std::str::FromStr;
4
5use rustc_hash::FxHashMap;
6use uv_distribution_types::{Requirement, RequirementSource};
7use uv_normalize::PackageName;
8use uv_pep440::{Operator, VersionSpecifiers};
9
10use crate::resolver::ForkSet;
11use crate::{DependencyMode, Manifest, ResolverEnvironment};
12
13#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
14#[serde(deny_unknown_fields, rename_all = "kebab-case")]
15#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
16#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
17pub enum PrereleaseMode {
18    /// Disallow all pre-release versions.
19    Disallow,
20
21    /// Allow all pre-release versions.
22    Allow,
23
24    /// Prefer stable versions, falling back to pre-release versions when necessary.
25    #[default]
26    IfNecessary,
27
28    /// Prefer stable versions for first-party packages with explicit pre-release specifiers,
29    /// falling back to pre-release versions when necessary. Disallow pre-release versions for all
30    /// other packages.
31    Explicit,
32
33    /// Deprecated alias for `if-necessary`.
34    #[deprecated(note = "use `if-necessary` instead")]
35    IfNecessaryOrExplicit,
36}
37
38#[allow(deprecated)]
39impl std::fmt::Display for PrereleaseMode {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self {
42            Self::Disallow => write!(f, "disallow"),
43            Self::Allow => write!(f, "allow"),
44            Self::IfNecessary => write!(f, "if-necessary"),
45            Self::Explicit => write!(f, "explicit"),
46            Self::IfNecessaryOrExplicit => write!(f, "if-necessary-or-explicit"),
47        }
48    }
49}
50
51impl FromStr for PrereleaseMode {
52    type Err = String;
53
54    fn from_str(value: &str) -> Result<Self, Self::Err> {
55        match value {
56            "disallow" => Ok(Self::Disallow),
57            "allow" => Ok(Self::Allow),
58            "if-necessary" => Ok(Self::IfNecessary),
59            "explicit" => Ok(Self::Explicit),
60            #[allow(deprecated)]
61            "if-necessary-or-explicit" => Ok(Self::IfNecessaryOrExplicit),
62            _ => Err(format!(
63                "expected one of `disallow`, `allow`, `if-necessary`, `explicit`, or `if-necessary-or-explicit`, found `{value}`"
64            )),
65        }
66    }
67}
68
69/// A package-specific pre-release selection policy.
70#[derive(Debug, Clone, PartialEq, Eq)]
71#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
72pub struct PrereleasePackageEntry {
73    package: PackageName,
74    mode: PrereleaseMode,
75}
76
77impl FromStr for PrereleasePackageEntry {
78    type Err = String;
79
80    fn from_str(value: &str) -> Result<Self, Self::Err> {
81        let Some((package, mode)) = value.split_once('=') else {
82            return Err(format!(
83                "Invalid `prerelease-package` value `{value}`: expected format `PACKAGE=MODE`"
84            ));
85        };
86
87        let package = PackageName::from_str(package).map_err(|err| {
88            format!("Invalid `prerelease-package` package name `{package}`: {err}")
89        })?;
90        let mode = PrereleaseMode::from_str(mode)
91            .map_err(|err| format!("Invalid `prerelease-package` mode: {err}"))?;
92
93        Ok(Self { package, mode })
94    }
95}
96
97/// Pre-release selection policies that apply to individual packages.
98#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
99#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
100pub struct PrereleasePackage(FxHashMap<PackageName, PrereleaseMode>);
101
102impl Deref for PrereleasePackage {
103    type Target = FxHashMap<PackageName, PrereleaseMode>;
104
105    fn deref(&self) -> &Self::Target {
106        &self.0
107    }
108}
109
110impl DerefMut for PrereleasePackage {
111    fn deref_mut(&mut self) -> &mut Self::Target {
112        &mut self.0
113    }
114}
115
116impl FromIterator<PrereleasePackageEntry> for PrereleasePackage {
117    fn from_iter<T: IntoIterator<Item = PrereleasePackageEntry>>(iter: T) -> Self {
118        Self(
119            iter.into_iter()
120                .map(|entry| (entry.package, entry.mode))
121                .collect(),
122        )
123    }
124}
125
126impl IntoIterator for PrereleasePackage {
127    type Item = (PackageName, PrereleaseMode);
128    type IntoIter = std::collections::hash_map::IntoIter<PackageName, PrereleaseMode>;
129
130    fn into_iter(self) -> Self::IntoIter {
131        self.0.into_iter()
132    }
133}
134
135impl<'a> IntoIterator for &'a PrereleasePackage {
136    type Item = (&'a PackageName, &'a PrereleaseMode);
137    type IntoIter = std::collections::hash_map::Iter<'a, PackageName, PrereleaseMode>;
138
139    fn into_iter(self) -> Self::IntoIter {
140        self.0.iter()
141    }
142}
143
144impl PrereleasePackage {
145    /// Returns whether no package-specific policies are configured.
146    pub(crate) fn is_empty(&self) -> bool {
147        self.0.is_empty()
148    }
149}
150
151/// A pre-release selection policy that applies globally and to individual packages.
152#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
153#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
154pub struct Prerelease {
155    /// Global policy that applies to packages without a package-specific override.
156    pub global: PrereleaseMode,
157    /// Package-specific policies that override the global policy.
158    pub package: PrereleasePackage,
159}
160
161impl Prerelease {
162    /// Returns the effective pre-release selection policy for a package.
163    pub fn mode(&self, package: &PackageName) -> PrereleaseMode {
164        self.package.get(package).copied().unwrap_or(self.global)
165    }
166}
167
168/// Like [`PrereleaseMode`], but with any additional information required to select a candidate,
169/// like the set of direct dependencies.
170#[derive(Debug, Clone)]
171pub(crate) struct PrereleaseStrategy {
172    default: PrereleasePolicy,
173    package: FxHashMap<PackageName, PrereleasePolicy>,
174}
175
176#[derive(Debug, Clone)]
177enum PrereleasePolicy {
178    /// Disallow all pre-release versions.
179    Disallow,
180
181    /// Allow all pre-release versions.
182    Allow,
183
184    /// Prefer stable versions, falling back to pre-release versions when necessary.
185    IfNecessary,
186
187    /// Prefer stable versions for first-party packages with explicit pre-release specifiers,
188    /// falling back to pre-release versions when necessary. Disallow pre-release versions for all
189    /// other packages.
190    Explicit(ForkSet),
191}
192
193impl PrereleaseStrategy {
194    #[allow(deprecated)]
195    pub(crate) fn from_prerelease(
196        prerelease: &Prerelease,
197        manifest: &Manifest,
198        env: &ResolverEnvironment,
199        dependencies: DependencyMode,
200    ) -> Self {
201        Self {
202            default: Self::policy(prerelease.global, manifest, env, dependencies),
203            package: prerelease
204                .package
205                .iter()
206                .map(|(name, mode)| {
207                    (
208                        name.clone(),
209                        Self::policy(*mode, manifest, env, dependencies),
210                    )
211                })
212                .collect(),
213        }
214    }
215
216    #[allow(deprecated)]
217    fn policy(
218        mode: PrereleaseMode,
219        manifest: &Manifest,
220        env: &ResolverEnvironment,
221        dependencies: DependencyMode,
222    ) -> PrereleasePolicy {
223        match mode {
224            PrereleaseMode::Disallow => PrereleasePolicy::Disallow,
225            PrereleaseMode::Allow => PrereleasePolicy::Allow,
226            PrereleaseMode::IfNecessary | PrereleaseMode::IfNecessaryOrExplicit => {
227                PrereleasePolicy::IfNecessary
228            }
229            PrereleaseMode::Explicit => PrereleasePolicy::Explicit(Self::explicit_packages(
230                manifest.candidate_selection_requirements(env, dependencies),
231            )),
232        }
233    }
234
235    fn explicit_packages<'a>(requirements: impl Iterator<Item = Cow<'a, Requirement>>) -> ForkSet {
236        let mut packages = ForkSet::default();
237        for requirement in requirements {
238            let RequirementSource::Registry { specifier, .. } = &requirement.source else {
239                continue;
240            };
241
242            if contains_prerelease(specifier) {
243                packages.add(&requirement, ());
244            }
245        }
246        packages
247    }
248
249    /// Returns the pre-release candidate selection policy for a package.
250    ///
251    /// Pre-releases remain in the candidate universe but, unless they are globally allowed, are
252    /// considered only after stable candidates. Keeping the candidate universe fixed is required
253    /// for PubGrub's learned incompatibilities to remain valid.
254    pub(crate) fn selection(
255        &self,
256        package_name: &PackageName,
257        env: &ResolverEnvironment,
258    ) -> PrereleaseSelection {
259        match self.package.get(package_name).unwrap_or(&self.default) {
260            PrereleasePolicy::Disallow => PrereleaseSelection::Disallow,
261            PrereleasePolicy::Allow => PrereleaseSelection::Allow,
262            PrereleasePolicy::IfNecessary => PrereleaseSelection::PreferStable,
263            PrereleasePolicy::Explicit(packages) => {
264                if packages.contains(package_name, env) {
265                    PrereleaseSelection::PreferStable
266                } else {
267                    PrereleaseSelection::Disallow
268                }
269            }
270        }
271    }
272}
273
274/// Returns `true` if the specifiers explicitly mention a pre-release version.
275///
276/// Exclusions do not opt a package into pre-releases. For example, `!=1.0a1` should not change
277/// which candidate kinds are considered.
278fn contains_prerelease(specifiers: &VersionSpecifiers) -> bool {
279    specifiers
280        .iter()
281        .filter(|specifier| {
282            !matches!(
283                specifier.operator(),
284                Operator::NotEqual | Operator::NotEqualStar
285            )
286        })
287        .any(uv_pep440::VersionSpecifier::any_prerelease)
288}
289
290/// How pre-release candidates participate in version selection.
291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292pub(crate) enum PrereleaseSelection {
293    /// Do not consider pre-release candidates.
294    Disallow,
295    /// Consider stable and pre-release candidates in normal version order.
296    Allow,
297    /// Prefer stable candidates, falling back to pre-releases only after stable candidates are
298    /// exhausted.
299    PreferStable,
300}