Skip to content

Commit c30f53b

Browse files
Allow --constraints and --overrides in uv tool install (#9547)
## Summary Closes #9517.
1 parent 63443f1 commit c30f53b

10 files changed

Lines changed: 386 additions & 28 deletions

File tree

crates/uv-cli/src/lib.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3778,6 +3778,28 @@ pub struct ToolInstallArgs {
37783778
#[arg(long, value_delimiter = ',', value_parser = parse_maybe_file_path)]
37793779
pub with_requirements: Vec<Maybe<PathBuf>>,
37803780

3781+
/// Constrain versions using the given requirements files.
3782+
///
3783+
/// Constraints files are `requirements.txt`-like files that only control the _version_ of a
3784+
/// requirement that's installed. However, including a package in a constraints file will _not_
3785+
/// trigger the installation of that package.
3786+
///
3787+
/// This is equivalent to pip's `--constraint` option.
3788+
#[arg(long, short, alias = "constraint", env = EnvVars::UV_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)]
3789+
pub constraints: Vec<Maybe<PathBuf>>,
3790+
3791+
/// Override versions using the given requirements files.
3792+
///
3793+
/// Overrides files are `requirements.txt`-like files that force a specific version of a
3794+
/// requirement to be installed, regardless of the requirements declared by any constituent
3795+
/// package, and regardless of whether this would be considered an invalid resolution.
3796+
///
3797+
/// While constraints are _additive_, in that they're combined with the requirements of the
3798+
/// constituent packages, overrides are _absolute_, in that they completely replace the
3799+
/// requirements of the constituent packages.
3800+
#[arg(long, alias = "override", env = EnvVars::UV_OVERRIDE, value_delimiter = ' ', value_parser = parse_maybe_file_path)]
3801+
pub overrides: Vec<Maybe<PathBuf>>,
3802+
37813803
#[command(flatten)]
37823804
pub installer: ResolverInstallerArgs,
37833805

crates/uv-tool/src/tool.rs

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ use uv_settings::ToolOptions;
1616
pub struct Tool {
1717
/// The requirements requested by the user during installation.
1818
requirements: Vec<Requirement>,
19+
/// The constraints requested by the user during installation.
20+
constraints: Vec<Requirement>,
21+
/// The overrides requested by the user during installation.
22+
overrides: Vec<Requirement>,
1923
/// The Python requested by the user during installation.
2024
python: Option<String>,
2125
/// A mapping of entry point names to their metadata.
@@ -26,7 +30,12 @@ pub struct Tool {
2630

2731
#[derive(Debug, Clone, Deserialize)]
2832
struct ToolWire {
33+
#[serde(default)]
2934
requirements: Vec<RequirementWire>,
35+
#[serde(default)]
36+
constraints: Vec<Requirement>,
37+
#[serde(default)]
38+
overrides: Vec<Requirement>,
3039
python: Option<String>,
3140
entrypoints: Vec<ToolEntrypoint>,
3241
#[serde(default)]
@@ -51,6 +60,8 @@ impl From<Tool> for ToolWire {
5160
.into_iter()
5261
.map(RequirementWire::Requirement)
5362
.collect(),
63+
constraints: tool.constraints,
64+
overrides: tool.overrides,
5465
python: tool.python,
5566
entrypoints: tool.entrypoints,
5667
options: tool.options,
@@ -71,6 +82,8 @@ impl TryFrom<ToolWire> for Tool {
7182
RequirementWire::Deprecated(requirement) => Requirement::from(requirement),
7283
})
7384
.collect(),
85+
constraints: tool.constraints,
86+
overrides: tool.overrides,
7487
python: tool.python,
7588
entrypoints: tool.entrypoints,
7689
options: tool.options,
@@ -116,6 +129,8 @@ impl Tool {
116129
/// Create a new `Tool`.
117130
pub fn new(
118131
requirements: Vec<Requirement>,
132+
constraints: Vec<Requirement>,
133+
overrides: Vec<Requirement>,
119134
python: Option<String>,
120135
entrypoints: impl Iterator<Item = ToolEntrypoint>,
121136
options: ToolOptions,
@@ -124,6 +139,8 @@ impl Tool {
124139
entrypoints.sort();
125140
Self {
126141
requirements,
142+
constraints,
143+
overrides,
127144
python,
128145
entrypoints,
129146
options,
@@ -140,25 +157,71 @@ impl Tool {
140157
pub(crate) fn to_toml(&self) -> Result<Table, toml_edit::ser::Error> {
141158
let mut table = Table::new();
142159

143-
table.insert("requirements", {
144-
let requirements = self
145-
.requirements
146-
.iter()
147-
.map(|requirement| {
148-
serde::Serialize::serialize(
149-
&requirement,
150-
toml_edit::ser::ValueSerializer::new(),
151-
)
152-
})
153-
.collect::<Result<Vec<_>, _>>()?;
160+
if !self.requirements.is_empty() {
161+
table.insert("requirements", {
162+
let requirements = self
163+
.requirements
164+
.iter()
165+
.map(|requirement| {
166+
serde::Serialize::serialize(
167+
&requirement,
168+
toml_edit::ser::ValueSerializer::new(),
169+
)
170+
})
171+
.collect::<Result<Vec<_>, _>>()?;
154172

155-
let requirements = match requirements.as_slice() {
156-
[] => Array::new(),
157-
[requirement] => Array::from_iter([requirement]),
158-
requirements => each_element_on_its_line_array(requirements.iter()),
159-
};
160-
value(requirements)
161-
});
173+
let requirements = match requirements.as_slice() {
174+
[] => Array::new(),
175+
[requirement] => Array::from_iter([requirement]),
176+
requirements => each_element_on_its_line_array(requirements.iter()),
177+
};
178+
value(requirements)
179+
});
180+
}
181+
182+
if !self.constraints.is_empty() {
183+
table.insert("constraints", {
184+
let constraints = self
185+
.constraints
186+
.iter()
187+
.map(|constraint| {
188+
serde::Serialize::serialize(
189+
&constraint,
190+
toml_edit::ser::ValueSerializer::new(),
191+
)
192+
})
193+
.collect::<Result<Vec<_>, _>>()?;
194+
195+
let constraints = match constraints.as_slice() {
196+
[] => Array::new(),
197+
[constraint] => Array::from_iter([constraint]),
198+
constraints => each_element_on_its_line_array(constraints.iter()),
199+
};
200+
value(constraints)
201+
});
202+
}
203+
204+
if !self.overrides.is_empty() {
205+
table.insert("overrides", {
206+
let overrides = self
207+
.overrides
208+
.iter()
209+
.map(|r#override| {
210+
serde::Serialize::serialize(
211+
&r#override,
212+
toml_edit::ser::ValueSerializer::new(),
213+
)
214+
})
215+
.collect::<Result<Vec<_>, _>>()?;
216+
217+
let overrides = match overrides.as_slice() {
218+
[] => Array::new(),
219+
[r#override] => Array::from_iter([r#override]),
220+
overrides => each_element_on_its_line_array(overrides.iter()),
221+
};
222+
value(overrides)
223+
});
224+
}
162225

163226
if let Some(ref python) = self.python {
164227
table.insert("python", value(python));
@@ -196,6 +259,14 @@ impl Tool {
196259
&self.requirements
197260
}
198261

262+
pub fn constraints(&self) -> &[Requirement] {
263+
&self.constraints
264+
}
265+
266+
pub fn overrides(&self) -> &[Requirement] {
267+
&self.overrides
268+
}
269+
199270
pub fn python(&self) -> &Option<String> {
200271
&self.python
201272
}

crates/uv/src/commands/tool/common.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ pub(crate) fn install_executables(
7070
force: bool,
7171
python: Option<String>,
7272
requirements: Vec<Requirement>,
73+
constraints: Vec<Requirement>,
74+
overrides: Vec<Requirement>,
7375
printer: Printer,
7476
) -> anyhow::Result<ExitStatus> {
7577
let site_packages = SitePackages::from_environment(environment)?;
@@ -183,7 +185,9 @@ pub(crate) fn install_executables(
183185

184186
debug!("Adding receipt for tool `{name}`");
185187
let tool = Tool::new(
186-
requirements.into_iter().collect(),
188+
requirements,
189+
constraints,
190+
overrides,
187191
python,
188192
target_entry_points
189193
.into_iter()

crates/uv/src/commands/tool/install.rs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ use std::str::FromStr;
44
use anyhow::{bail, Result};
55
use owo_colors::OwoColorize;
66
use tracing::{debug, trace};
7+
78
use uv_cache::{Cache, Refresh};
89
use uv_cache_info::Timestamp;
910
use uv_client::{BaseClientBuilder, Connectivity};
1011
use uv_configuration::{Concurrency, Reinstall, TrustedHost, Upgrade};
1112
use uv_dispatch::SharedState;
12-
use uv_distribution_types::UnresolvedRequirementSpecification;
13+
use uv_distribution_types::{NameRequirementSpecification, UnresolvedRequirementSpecification};
1314
use uv_normalize::PackageName;
1415
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
1516
use uv_pep508::MarkerTree;
@@ -43,6 +44,8 @@ pub(crate) async fn install(
4344
editable: bool,
4445
from: Option<String>,
4546
with: &[RequirementsSource],
47+
constraints: &[RequirementsSource],
48+
overrides: &[RequirementsSource],
4649
python: Option<String>,
4750
install_mirrors: PythonInstallMirrors,
4851
force: bool,
@@ -239,7 +242,9 @@ pub(crate) async fn install(
239242
};
240243

241244
// Read the `--with` requirements.
242-
let spec = RequirementsSpecification::from_simple_sources(with, &client_builder).await?;
245+
let spec =
246+
RequirementsSpecification::from_sources(with, constraints, overrides, &client_builder)
247+
.await?;
243248

244249
// Resolve the `--from` and `--with` requirements.
245250
let requirements = {
@@ -263,6 +268,28 @@ pub(crate) async fn install(
263268
requirements
264269
};
265270

271+
// Resolve the constraints.
272+
let constraints = spec
273+
.constraints
274+
.into_iter()
275+
.map(|constraint| constraint.requirement)
276+
.collect::<Vec<_>>();
277+
278+
// Resolve the overrides.
279+
let overrides = resolve_names(
280+
spec.overrides,
281+
&interpreter,
282+
&settings,
283+
&state,
284+
connectivity,
285+
concurrency,
286+
native_tls,
287+
allow_insecure_host,
288+
&cache,
289+
printer,
290+
)
291+
.await?;
292+
266293
// Convert to tool options.
267294
let options = ToolOptions::from(options);
268295

@@ -330,8 +357,10 @@ pub(crate) async fn install(
330357
.is_some()
331358
{
332359
if let Some(tool_receipt) = existing_tool_receipt.as_ref() {
333-
let receipt = tool_receipt.requirements().to_vec();
334-
if requirements == receipt {
360+
if requirements == tool_receipt.requirements()
361+
&& constraints == tool_receipt.constraints()
362+
&& overrides == tool_receipt.overrides()
363+
{
335364
if *tool_receipt.options() != options {
336365
// ...but the options differ, we need to update the receipt.
337366
installed_tools
@@ -357,6 +386,16 @@ pub(crate) async fn install(
357386
.cloned()
358387
.map(UnresolvedRequirementSpecification::from)
359388
.collect(),
389+
constraints: constraints
390+
.iter()
391+
.cloned()
392+
.map(NameRequirementSpecification::from)
393+
.collect(),
394+
overrides: overrides
395+
.iter()
396+
.cloned()
397+
.map(UnresolvedRequirementSpecification::from)
398+
.collect(),
360399
..spec
361400
};
362401

@@ -470,6 +509,8 @@ pub(crate) async fn install(
470509
force || invalid_tool_receipt,
471510
python,
472511
requirements,
512+
constraints,
513+
overrides,
473514
printer,
474515
)
475516
}

crates/uv/src/commands/tool/upgrade.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,16 @@ async fn upgrade_tool(
266266
let settings = ResolverInstallerSettings::from(options.clone());
267267

268268
// Resolve the requirements.
269-
let requirements = existing_tool_receipt.requirements();
270-
let spec =
271-
RequirementsSpecification::from_constraints(requirements.to_vec(), constraints.to_vec());
269+
let spec = RequirementsSpecification::from_overrides(
270+
existing_tool_receipt.requirements().to_vec(),
271+
existing_tool_receipt
272+
.constraints()
273+
.iter()
274+
.chain(constraints)
275+
.cloned()
276+
.collect(),
277+
existing_tool_receipt.overrides().to_vec(),
278+
);
272279

273280
// Initialize any shared state.
274281
let state = SharedState::default();
@@ -362,7 +369,9 @@ async fn upgrade_tool(
362369
ToolOptions::from(options),
363370
true,
364371
existing_tool_receipt.python().to_owned(),
365-
requirements.to_vec(),
372+
existing_tool_receipt.requirements().to_vec(),
373+
existing_tool_receipt.constraints().to_vec(),
374+
existing_tool_receipt.overrides().to_vec(),
366375
printer,
367376
)?;
368377
}

crates/uv/src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -954,12 +954,24 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
954954
.map(RequirementsSource::from_requirements_file),
955955
)
956956
.collect::<Vec<_>>();
957+
let constraints = args
958+
.constraints
959+
.into_iter()
960+
.map(RequirementsSource::from_constraints_txt)
961+
.collect::<Vec<_>>();
962+
let overrides = args
963+
.overrides
964+
.into_iter()
965+
.map(RequirementsSource::from_overrides_txt)
966+
.collect::<Vec<_>>();
957967

958968
Box::pin(commands::tool_install(
959969
args.package,
960970
args.editable,
961971
args.from,
962972
&requirements,
973+
&constraints,
974+
&overrides,
963975
args.python,
964976
args.install_mirrors,
965977
args.force,

0 commit comments

Comments
 (0)