uv_configuration/
package_options.rs

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