Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3778,6 +3778,28 @@ pub struct ToolInstallArgs {
#[arg(long, value_delimiter = ',', value_parser = parse_maybe_file_path)]
pub with_requirements: Vec<Maybe<PathBuf>>,

/// Constrain versions using the given requirements files.
///
/// Constraints files are `requirements.txt`-like files that only control the _version_ of a
/// requirement that's installed. However, including a package in a constraints file will _not_
/// trigger the installation of that package.
///
/// This is equivalent to pip's `--constraint` option.
#[arg(long, short, alias = "constraint", env = EnvVars::UV_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)]
pub constraints: Vec<Maybe<PathBuf>>,

/// Override versions using the given requirements files.
///
/// Overrides files are `requirements.txt`-like files that force a specific version of a
/// requirement to be installed, regardless of the requirements declared by any constituent
/// package, and regardless of whether this would be considered an invalid resolution.
///
/// While constraints are _additive_, in that they're combined with the requirements of the
/// constituent packages, overrides are _absolute_, in that they completely replace the
/// requirements of the constituent packages.
#[arg(long, alias = "override", env = EnvVars::UV_OVERRIDE, value_delimiter = ' ', value_parser = parse_maybe_file_path)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we want to be able to use --override for non-file overrides? e.g., --override foo==0.1.0

(same for --constraint)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think we allow that elsewhere, but we could? How would we differentiate between file and non-file on the CLI?

Copy link
Member

@zanieb zanieb Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think --override vs --overrides would be the distinguishing factor.

I'm honestly not sure what the status quo is outside uv pip. I think there's a tracking issue about this somewhere, but not sure where...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess in the long-term --override / --overrides vs --override-file / --overrides-file might make more sense? It's sort of awkward since we have the precedents from the pip interface.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be dealt with later :)

pub overrides: Vec<Maybe<PathBuf>>,

#[command(flatten)]
pub installer: ResolverInstallerArgs,

Expand Down
107 changes: 89 additions & 18 deletions crates/uv-tool/src/tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ use uv_settings::ToolOptions;
pub struct Tool {
/// The requirements requested by the user during installation.
requirements: Vec<Requirement>,
/// The constraints requested by the user during installation.
constraints: Vec<Requirement>,
/// The overrides requested by the user during installation.
overrides: Vec<Requirement>,
/// The Python requested by the user during installation.
python: Option<String>,
/// A mapping of entry point names to their metadata.
Expand All @@ -26,7 +30,12 @@ pub struct Tool {

#[derive(Debug, Clone, Deserialize)]
struct ToolWire {
#[serde(default)]
requirements: Vec<RequirementWire>,
#[serde(default)]
constraints: Vec<Requirement>,
#[serde(default)]
overrides: Vec<Requirement>,
python: Option<String>,
entrypoints: Vec<ToolEntrypoint>,
#[serde(default)]
Expand All @@ -51,6 +60,8 @@ impl From<Tool> for ToolWire {
.into_iter()
.map(RequirementWire::Requirement)
.collect(),
constraints: tool.constraints,
overrides: tool.overrides,
python: tool.python,
entrypoints: tool.entrypoints,
options: tool.options,
Expand All @@ -71,6 +82,8 @@ impl TryFrom<ToolWire> for Tool {
RequirementWire::Deprecated(requirement) => Requirement::from(requirement),
})
.collect(),
constraints: tool.constraints,
overrides: tool.overrides,
python: tool.python,
entrypoints: tool.entrypoints,
options: tool.options,
Expand Down Expand Up @@ -116,6 +129,8 @@ impl Tool {
/// Create a new `Tool`.
pub fn new(
requirements: Vec<Requirement>,
constraints: Vec<Requirement>,
overrides: Vec<Requirement>,
python: Option<String>,
entrypoints: impl Iterator<Item = ToolEntrypoint>,
options: ToolOptions,
Expand All @@ -124,6 +139,8 @@ impl Tool {
entrypoints.sort();
Self {
requirements,
constraints,
overrides,
python,
entrypoints,
options,
Expand All @@ -140,25 +157,71 @@ impl Tool {
pub(crate) fn to_toml(&self) -> Result<Table, toml_edit::ser::Error> {
let mut table = Table::new();

table.insert("requirements", {
let requirements = self
.requirements
.iter()
.map(|requirement| {
serde::Serialize::serialize(
&requirement,
toml_edit::ser::ValueSerializer::new(),
)
})
.collect::<Result<Vec<_>, _>>()?;
if !self.requirements.is_empty() {
table.insert("requirements", {
let requirements = self
.requirements
.iter()
.map(|requirement| {
serde::Serialize::serialize(
&requirement,
toml_edit::ser::ValueSerializer::new(),
)
})
.collect::<Result<Vec<_>, _>>()?;

let requirements = match requirements.as_slice() {
[] => Array::new(),
[requirement] => Array::from_iter([requirement]),
requirements => each_element_on_its_line_array(requirements.iter()),
};
value(requirements)
});
let requirements = match requirements.as_slice() {
[] => Array::new(),
[requirement] => Array::from_iter([requirement]),
requirements => each_element_on_its_line_array(requirements.iter()),
};
value(requirements)
});
}

if !self.constraints.is_empty() {
table.insert("constraints", {
let constraints = self
.constraints
.iter()
.map(|constraint| {
serde::Serialize::serialize(
&constraint,
toml_edit::ser::ValueSerializer::new(),
)
})
.collect::<Result<Vec<_>, _>>()?;

let constraints = match constraints.as_slice() {
[] => Array::new(),
[constraint] => Array::from_iter([constraint]),
constraints => each_element_on_its_line_array(constraints.iter()),
};
value(constraints)
});
}

if !self.overrides.is_empty() {
table.insert("overrides", {
let overrides = self
.overrides
.iter()
.map(|r#override| {
serde::Serialize::serialize(
&r#override,
toml_edit::ser::ValueSerializer::new(),
)
})
.collect::<Result<Vec<_>, _>>()?;

let overrides = match overrides.as_slice() {
[] => Array::new(),
[r#override] => Array::from_iter([r#override]),
overrides => each_element_on_its_line_array(overrides.iter()),
};
value(overrides)
});
}

if let Some(ref python) = self.python {
table.insert("python", value(python));
Expand Down Expand Up @@ -196,6 +259,14 @@ impl Tool {
&self.requirements
}

pub fn constraints(&self) -> &[Requirement] {
&self.constraints
}

pub fn overrides(&self) -> &[Requirement] {
&self.overrides
}

pub fn python(&self) -> &Option<String> {
&self.python
}
Expand Down
6 changes: 5 additions & 1 deletion crates/uv/src/commands/tool/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ pub(crate) fn install_executables(
force: bool,
python: Option<String>,
requirements: Vec<Requirement>,
constraints: Vec<Requirement>,
overrides: Vec<Requirement>,
printer: Printer,
) -> anyhow::Result<ExitStatus> {
let site_packages = SitePackages::from_environment(environment)?;
Expand Down Expand Up @@ -183,7 +185,9 @@ pub(crate) fn install_executables(

debug!("Adding receipt for tool `{name}`");
let tool = Tool::new(
requirements.into_iter().collect(),
requirements,
constraints,
overrides,
python,
target_entry_points
.into_iter()
Expand Down
49 changes: 45 additions & 4 deletions crates/uv/src/commands/tool/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ use std::str::FromStr;
use anyhow::{bail, Result};
use owo_colors::OwoColorize;
use tracing::{debug, trace};

use uv_cache::{Cache, Refresh};
use uv_cache_info::Timestamp;
use uv_client::{BaseClientBuilder, Connectivity};
use uv_configuration::{Concurrency, Reinstall, TrustedHost, Upgrade};
use uv_dispatch::SharedState;
use uv_distribution_types::UnresolvedRequirementSpecification;
use uv_distribution_types::{NameRequirementSpecification, UnresolvedRequirementSpecification};
use uv_normalize::PackageName;
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
use uv_pep508::MarkerTree;
Expand Down Expand Up @@ -43,6 +44,8 @@ pub(crate) async fn install(
editable: bool,
from: Option<String>,
with: &[RequirementsSource],
constraints: &[RequirementsSource],
overrides: &[RequirementsSource],
python: Option<String>,
install_mirrors: PythonInstallMirrors,
force: bool,
Expand Down Expand Up @@ -239,7 +242,9 @@ pub(crate) async fn install(
};

// Read the `--with` requirements.
let spec = RequirementsSpecification::from_simple_sources(with, &client_builder).await?;
let spec =
RequirementsSpecification::from_sources(with, constraints, overrides, &client_builder)
.await?;

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

// Resolve the constraints.
let constraints = spec
.constraints
.into_iter()
.map(|constraint| constraint.requirement)
.collect::<Vec<_>>();

// Resolve the overrides.
let overrides = resolve_names(
spec.overrides,
&interpreter,
&settings,
&state,
connectivity,
concurrency,
native_tls,
allow_insecure_host,
&cache,
printer,
)
.await?;

// Convert to tool options.
let options = ToolOptions::from(options);

Expand Down Expand Up @@ -330,8 +357,10 @@ pub(crate) async fn install(
.is_some()
{
if let Some(tool_receipt) = existing_tool_receipt.as_ref() {
let receipt = tool_receipt.requirements().to_vec();
if requirements == receipt {
if requirements == tool_receipt.requirements()
&& constraints == tool_receipt.constraints()
&& overrides == tool_receipt.overrides()
{
if *tool_receipt.options() != options {
// ...but the options differ, we need to update the receipt.
installed_tools
Expand All @@ -357,6 +386,16 @@ pub(crate) async fn install(
.cloned()
.map(UnresolvedRequirementSpecification::from)
.collect(),
constraints: constraints
.iter()
.cloned()
.map(NameRequirementSpecification::from)
.collect(),
overrides: overrides
.iter()
.cloned()
.map(UnresolvedRequirementSpecification::from)
.collect(),
..spec
};

Expand Down Expand Up @@ -470,6 +509,8 @@ pub(crate) async fn install(
force || invalid_tool_receipt,
python,
requirements,
constraints,
overrides,
printer,
)
}
17 changes: 13 additions & 4 deletions crates/uv/src/commands/tool/upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,16 @@ async fn upgrade_tool(
let settings = ResolverInstallerSettings::from(options.clone());

// Resolve the requirements.
let requirements = existing_tool_receipt.requirements();
let spec =
RequirementsSpecification::from_constraints(requirements.to_vec(), constraints.to_vec());
let spec = RequirementsSpecification::from_overrides(
existing_tool_receipt.requirements().to_vec(),
existing_tool_receipt
.constraints()
.iter()
.chain(constraints)
.cloned()
.collect(),
existing_tool_receipt.overrides().to_vec(),
);

// Initialize any shared state.
let state = SharedState::default();
Expand Down Expand Up @@ -362,7 +369,9 @@ async fn upgrade_tool(
ToolOptions::from(options),
true,
existing_tool_receipt.python().to_owned(),
requirements.to_vec(),
existing_tool_receipt.requirements().to_vec(),
existing_tool_receipt.constraints().to_vec(),
existing_tool_receipt.overrides().to_vec(),
printer,
)?;
}
Expand Down
12 changes: 12 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -954,12 +954,24 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
.map(RequirementsSource::from_requirements_file),
)
.collect::<Vec<_>>();
let constraints = args
.constraints
.into_iter()
.map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>();
let overrides = args
.overrides
.into_iter()
.map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>();

Box::pin(commands::tool_install(
args.package,
args.editable,
args.from,
&requirements,
&constraints,
&overrides,
args.python,
args.install_mirrors,
args.force,
Expand Down
Loading