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