Skip to main content

uv_configuration/
package_options.rs

1use std::path::Path;
2
3use rustc_hash::{FxHashMap, FxHashSet};
4
5use uv_cache::Refresh;
6use uv_cache_info::Timestamp;
7use uv_distribution_types::{Requirement, RequirementSource};
8use uv_normalize::{GroupName, PackageName};
9
10/// Whether to reinstall packages.
11#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
12#[serde(rename_all = "kebab-case", deny_unknown_fields)]
13pub enum Reinstall {
14    /// Don't reinstall any packages; respect the existing installation.
15    #[default]
16    None,
17
18    /// Reinstall all packages in the plan.
19    All,
20
21    /// Reinstall only the specified packages.
22    Packages(Vec<PackageName>, Vec<Box<Path>>),
23}
24
25impl Reinstall {
26    /// Determine the reinstall strategy to use.
27    pub fn from_args(reinstall: Option<bool>, reinstall_package: Vec<PackageName>) -> Option<Self> {
28        match reinstall {
29            Some(true) => Some(Self::All),
30            Some(false) => Some(Self::None),
31            None if reinstall_package.is_empty() => None,
32            None => Some(Self::Packages(reinstall_package, Vec::new())),
33        }
34    }
35
36    /// Returns `true` if no packages should be reinstalled.
37    pub fn is_none(&self) -> bool {
38        matches!(self, Self::None)
39    }
40
41    /// Returns `true` if all packages should be reinstalled.
42    pub fn is_all(&self) -> bool {
43        matches!(self, Self::All)
44    }
45
46    /// Returns `true` if the specified package should be reinstalled.
47    pub fn contains_package(&self, package_name: &PackageName) -> bool {
48        match self {
49            Self::None => false,
50            Self::All => true,
51            Self::Packages(packages, ..) => packages.contains(package_name),
52        }
53    }
54
55    /// Returns `true` if the specified path should be reinstalled.
56    pub fn contains_path(&self, path: &Path) -> bool {
57        match self {
58            Self::None => false,
59            Self::All => true,
60            Self::Packages(.., paths) => paths
61                .iter()
62                .any(|target| same_file::is_same_file(path, target).unwrap_or(false)),
63        }
64    }
65
66    /// Combine a set of [`Reinstall`] values.
67    #[must_use]
68    pub fn combine(self, other: Self) -> Self {
69        match self {
70            // Setting `--reinstall` or `--no-reinstall` should clear previous `--reinstall-package` selections.
71            Self::All | Self::None => self,
72            Self::Packages(self_packages, self_paths) => match other {
73                // If `--reinstall` was enabled previously, `--reinstall-package` is subsumed by reinstalling all packages.
74                Self::All => other,
75                // If `--no-reinstall` was enabled previously, then `--reinstall-package` enables an explicit reinstall of those packages.
76                Self::None => Self::Packages(self_packages, self_paths),
77                // If `--reinstall-package` was included twice, combine the requirements.
78                Self::Packages(other_packages, other_paths) => {
79                    let mut combined_packages = self_packages;
80                    combined_packages.extend(other_packages);
81                    let mut combined_paths = self_paths;
82                    combined_paths.extend(other_paths);
83                    Self::Packages(combined_packages, combined_paths)
84                }
85            },
86        }
87    }
88
89    /// Add a [`Box<Path>`] to the [`Reinstall`] policy.
90    #[must_use]
91    pub fn with_path(self, path: Box<Path>) -> Self {
92        match self {
93            Self::None => Self::Packages(vec![], vec![path]),
94            Self::All => Self::All,
95            Self::Packages(packages, mut paths) => {
96                paths.push(path);
97                Self::Packages(packages, paths)
98            }
99        }
100    }
101
102    /// Add a [`Package`] to the [`Reinstall`] policy.
103    #[must_use]
104    pub fn with_package(self, package_name: PackageName) -> Self {
105        match self {
106            Self::None => Self::Packages(vec![package_name], vec![]),
107            Self::All => Self::All,
108            Self::Packages(mut packages, paths) => {
109                packages.push(package_name);
110                Self::Packages(packages, paths)
111            }
112        }
113    }
114
115    /// Create a [`Reinstall`] strategy to reinstall a single package.
116    pub fn package(package_name: PackageName) -> Self {
117        Self::Packages(vec![package_name], vec![])
118    }
119}
120
121/// Create a [`Refresh`] policy by integrating the [`Reinstall`] policy.
122impl From<Reinstall> for Refresh {
123    fn from(value: Reinstall) -> Self {
124        match value {
125            Reinstall::None => Self::None(Timestamp::now()),
126            Reinstall::All => Self::All(Timestamp::now()),
127            Reinstall::Packages(packages, paths) => {
128                Self::Packages(packages, paths, Timestamp::now())
129            }
130        }
131    }
132}
133
134/// Strategy for determining which packages to consider for upgrade.
135#[derive(Debug, Default, Clone)]
136pub enum UpgradeStrategy {
137    /// Prefer pinned versions from the existing lockfile, if possible.
138    #[default]
139    None,
140
141    /// Allow package upgrades for all packages, ignoring the existing lockfile.
142    All,
143
144    /// Allow package upgrades, but only for the specified packages and/or dependency groups.
145    Some(FxHashSet<PackageName>, FxHashSet<GroupName>),
146}
147
148/// Whether to allow package upgrades.
149#[derive(Debug, Default, Clone)]
150pub struct Upgrade {
151    /// Strategy for picking packages to consider for upgrade.
152    strategy: UpgradeStrategy,
153
154    /// Additional version constraints for specific packages.
155    constraints: FxHashMap<PackageName, Vec<Requirement>>,
156}
157
158impl Upgrade {
159    /// Create a new [`Upgrade`] with no upgrades nor constraints.
160    pub fn none() -> Self {
161        Self {
162            strategy: UpgradeStrategy::None,
163            constraints: FxHashMap::default(),
164        }
165    }
166
167    /// Create a new [`Upgrade`] to consider all packages.
168    pub fn all() -> Self {
169        Self {
170            strategy: UpgradeStrategy::All,
171            constraints: FxHashMap::default(),
172        }
173    }
174
175    /// Determine the upgrade selection strategy from the command-line arguments.
176    pub fn from_args(
177        upgrade: Option<bool>,
178        upgrade_package: Vec<Requirement>,
179        upgrade_group: Vec<GroupName>,
180    ) -> Option<Self> {
181        let groups: FxHashSet<GroupName> = upgrade_group.into_iter().collect();
182
183        let strategy = match upgrade {
184            Some(true) => UpgradeStrategy::All,
185            Some(false) => {
186                if upgrade_package.is_empty() && groups.is_empty() {
187                    return Some(Self::none());
188                }
189                // `--no-upgrade` with `--upgrade-package` allows selecting the specified packages
190                // for upgrade.
191                let packages = upgrade_package.iter().map(|req| req.name.clone()).collect();
192                UpgradeStrategy::Some(packages, groups)
193            }
194            None => {
195                if upgrade_package.is_empty() && groups.is_empty() {
196                    return None;
197                }
198                let packages = upgrade_package.iter().map(|req| req.name.clone()).collect();
199                UpgradeStrategy::Some(packages, groups)
200            }
201        };
202
203        let mut constraints: FxHashMap<PackageName, Vec<Requirement>> = FxHashMap::default();
204        for requirement in upgrade_package {
205            // Skip any "empty" constraints.
206            if let RequirementSource::Registry { specifier, .. } = &requirement.source {
207                if specifier.is_empty() {
208                    continue;
209                }
210            }
211            constraints
212                .entry(requirement.name.clone())
213                .or_default()
214                .push(requirement);
215        }
216
217        Some(Self {
218            strategy,
219            constraints,
220        })
221    }
222
223    /// Create an [`Upgrade`] to upgrade a single package.
224    pub fn package(package_name: PackageName) -> Self {
225        let mut packages = FxHashSet::default();
226        packages.insert(package_name);
227        Self {
228            strategy: UpgradeStrategy::Some(packages, FxHashSet::default()),
229            constraints: FxHashMap::default(),
230        }
231    }
232
233    /// Returns `true` if no packages should be upgraded.
234    pub fn is_none(&self) -> bool {
235        matches!(self.strategy, UpgradeStrategy::None)
236    }
237
238    /// Returns `true` if all packages should be upgraded.
239    pub fn is_all(&self) -> bool {
240        matches!(self.strategy, UpgradeStrategy::All)
241    }
242
243    /// Returns an iterator over the constraints.
244    ///
245    /// When upgrading, users can provide bounds on the upgrade (e.g., `--upgrade-package flask<3`).
246    pub fn constraints(&self) -> impl Iterator<Item = &Requirement> {
247        self.constraints
248            .values()
249            .flat_map(|requirements| requirements.iter())
250    }
251
252    /// Returns the set of explicitly named packages to upgrade (from `--upgrade-package`).
253    pub fn packages(&self) -> Option<&FxHashSet<PackageName>> {
254        match &self.strategy {
255            UpgradeStrategy::Some(packages, _) => Some(packages),
256            _ => None,
257        }
258    }
259
260    /// Returns the set of dependency groups whose packages should be upgraded.
261    pub fn groups(&self) -> Option<&FxHashSet<GroupName>> {
262        match &self.strategy {
263            UpgradeStrategy::Some(_, groups) if !groups.is_empty() => Some(groups),
264            _ => None,
265        }
266    }
267
268    /// Combine a set of [`Upgrade`] values.
269    #[must_use]
270    pub fn combine(self, other: Self) -> Self {
271        // For `strategy`: `other` takes precedence for an explicit `All` or `None`; otherwise,
272        // merge.
273        let strategy = match (self.strategy, other.strategy) {
274            (_, UpgradeStrategy::All) => UpgradeStrategy::All,
275            (_, UpgradeStrategy::None) => UpgradeStrategy::None,
276            (
277                UpgradeStrategy::Some(mut self_packages, mut self_groups),
278                UpgradeStrategy::Some(other_packages, other_groups),
279            ) => {
280                self_packages.extend(other_packages);
281                self_groups.extend(other_groups);
282                UpgradeStrategy::Some(self_packages, self_groups)
283            }
284            (_, UpgradeStrategy::Some(packages, groups)) => UpgradeStrategy::Some(packages, groups),
285        };
286
287        // For `constraints`: always merge the constraints of `self` and `other`.
288        let mut combined_constraints = self.constraints.clone();
289        for (package, requirements) in other.constraints {
290            combined_constraints
291                .entry(package)
292                .or_default()
293                .extend(requirements);
294        }
295
296        Self {
297            strategy,
298            constraints: combined_constraints,
299        }
300    }
301}
302
303/// Create a [`Refresh`] policy by integrating the [`Upgrade`] policy.
304impl From<Upgrade> for Refresh {
305    fn from(value: Upgrade) -> Self {
306        match value.strategy {
307            UpgradeStrategy::None => Self::None(Timestamp::now()),
308            UpgradeStrategy::All => Self::All(Timestamp::now()),
309            UpgradeStrategy::Some(packages, _) => Self::Packages(
310                packages.into_iter().collect::<Vec<_>>(),
311                Vec::new(),
312                Timestamp::now(),
313            ),
314        }
315    }
316}
317
318/// Whether to isolate builds.
319#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
320#[serde(rename_all = "kebab-case", deny_unknown_fields)]
321#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
322pub enum BuildIsolation {
323    /// Isolate all builds.
324    #[default]
325    Isolate,
326
327    /// Do not isolate any builds.
328    Shared,
329
330    /// Do not isolate builds for the specified packages.
331    SharedPackage(Vec<PackageName>),
332}
333
334impl BuildIsolation {
335    /// Determine the build isolation strategy from the command-line arguments.
336    pub fn from_args(
337        no_build_isolation: Option<bool>,
338        no_build_isolation_package: Vec<PackageName>,
339    ) -> Option<Self> {
340        match no_build_isolation {
341            Some(true) => Some(Self::Shared),
342            Some(false) => Some(Self::Isolate),
343            None if no_build_isolation_package.is_empty() => None,
344            None => Some(Self::SharedPackage(no_build_isolation_package)),
345        }
346    }
347
348    /// Combine a set of [`BuildIsolation`] values.
349    #[must_use]
350    pub fn combine(self, other: Self) -> Self {
351        match self {
352            // Setting `--build-isolation` or `--no-build-isolation` should clear previous `--no-build-isolation-package` selections.
353            Self::Isolate | Self::Shared => self,
354            Self::SharedPackage(self_packages) => match other {
355                // If `--no-build-isolation` was enabled previously, `--no-build-isolation-package` is subsumed by sharing all builds.
356                Self::Shared => other,
357                // If `--build-isolation` was enabled previously, then `--no-build-isolation-package` enables specific packages to be shared.
358                Self::Isolate => Self::SharedPackage(self_packages),
359                // If `--no-build-isolation-package` was included twice, combine the packages.
360                Self::SharedPackage(other_packages) => {
361                    let mut combined = self_packages;
362                    combined.extend(other_packages);
363                    Self::SharedPackage(combined)
364                }
365            },
366        }
367    }
368}