Implement `--break-system-packages` (#2249)

## Summary

Per the
[`EXTERNALLY-MANAGED`](https://packaging.python.org/en/latest/specifications/externally-managed-environments/)
spec, installers SHOULD add a `--break-system-packages` flag to allow
users to override the package manager warnings raised by
`EXTERNALLY-MANAGED`. This PR adds the flag to comply with the spec, and
enable system Python installs on newer versions of certain
distributions.

While this flag feels kind of bad, it's not necessarily a change in
behavior. We _already_ allow installing into these system distributions
-- it's just that `EXTERNALLY-MANAGED` doesn't exist for distributions
that were packaged prior to the spec, so we don't run into this problem.

Closes https://github.com/astral-sh/uv/issues/2234.
This commit is contained in:
Charlie Marsh 2024-03-06 12:37:28 -08:00 committed by GitHub
parent 65e1005bfa
commit a5d5e99496
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 136 additions and 49 deletions

View File

@ -42,7 +42,7 @@ use crate::requirements::{ExtrasSpecification, RequirementsSource, RequirementsS
use super::Upgrade; use super::Upgrade;
/// Install packages into the current environment. /// Install packages into the current environment.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
pub(crate) async fn pip_install( pub(crate) async fn pip_install(
requirements: &[RequirementsSource], requirements: &[RequirementsSource],
constraints: &[RequirementsSource], constraints: &[RequirementsSource],
@ -65,6 +65,7 @@ pub(crate) async fn pip_install(
exclude_newer: Option<DateTime<Utc>>, exclude_newer: Option<DateTime<Utc>>,
python: Option<String>, python: Option<String>,
system: bool, system: bool,
break_system_packages: bool,
cache: Cache, cache: Cache,
printer: Printer, printer: Printer,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
@ -123,18 +124,22 @@ pub(crate) async fn pip_install(
// If the environment is externally managed, abort. // If the environment is externally managed, abort.
if let Some(externally_managed) = venv.interpreter().is_externally_managed() { if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
return if let Some(error) = externally_managed.into_error() { if break_system_packages {
Err(anyhow::anyhow!( debug!("Ignoring externally managed environment due to `--break-system-packages`");
"The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv venv`.",
venv.root().simplified_display().cyan(),
textwrap::indent(&error, " ").green(),
))
} else { } else {
Err(anyhow::anyhow!( return if let Some(error) = externally_managed.into_error() {
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv venv`.", Err(anyhow::anyhow!(
venv.root().simplified_display().cyan() "The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv venv`.",
)) venv.root().simplified_display().cyan(),
}; textwrap::indent(&error, " ").green(),
))
} else {
Err(anyhow::anyhow!(
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv venv`.",
venv.root().simplified_display().cyan()
))
};
}
} }
let _lock = venv.lock()?; let _lock = venv.lock()?;

View File

@ -28,7 +28,7 @@ use crate::printer::Printer;
use crate::requirements::{RequirementsSource, RequirementsSpecification}; use crate::requirements::{RequirementsSource, RequirementsSpecification};
/// Install a set of locked requirements into the current Python environment. /// Install a set of locked requirements into the current Python environment.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
pub(crate) async fn pip_sync( pub(crate) async fn pip_sync(
sources: &[RequirementsSource], sources: &[RequirementsSource],
reinstall: &Reinstall, reinstall: &Reinstall,
@ -43,6 +43,7 @@ pub(crate) async fn pip_sync(
strict: bool, strict: bool,
python: Option<String>, python: Option<String>,
system: bool, system: bool,
break_system_packages: bool,
cache: Cache, cache: Cache,
printer: Printer, printer: Printer,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
@ -90,18 +91,22 @@ pub(crate) async fn pip_sync(
// If the environment is externally managed, abort. // If the environment is externally managed, abort.
if let Some(externally_managed) = venv.interpreter().is_externally_managed() { if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
return if let Some(error) = externally_managed.into_error() { if break_system_packages {
Err(anyhow::anyhow!( debug!("Ignoring externally managed environment due to `--break-system-packages`");
"The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv venv`.",
venv.root().simplified_display().cyan(),
textwrap::indent(&error, " ").green(),
))
} else { } else {
Err(anyhow::anyhow!( return if let Some(error) = externally_managed.into_error() {
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv venv`.", Err(anyhow::anyhow!(
venv.root().simplified_display().cyan() "The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv venv`.",
)) venv.root().simplified_display().cyan(),
}; textwrap::indent(&error, " ").green(),
))
} else {
Err(anyhow::anyhow!(
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv venv`.",
venv.root().simplified_display().cyan()
))
};
}
} }
let _lock = venv.lock()?; let _lock = venv.lock()?;

View File

@ -20,6 +20,7 @@ pub(crate) async fn pip_uninstall(
sources: &[RequirementsSource], sources: &[RequirementsSource],
python: Option<String>, python: Option<String>,
system: bool, system: bool,
break_system_packages: bool,
cache: Cache, cache: Cache,
connectivity: Connectivity, connectivity: Connectivity,
printer: Printer, printer: Printer,
@ -62,18 +63,22 @@ pub(crate) async fn pip_uninstall(
// If the environment is externally managed, abort. // If the environment is externally managed, abort.
if let Some(externally_managed) = venv.interpreter().is_externally_managed() { if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
return if let Some(error) = externally_managed.into_error() { if break_system_packages {
Err(anyhow::anyhow!( debug!("Ignoring externally managed environment due to `--break-system-packages`");
"The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv venv`.",
venv.root().simplified_display().cyan(),
textwrap::indent(&error, " ").green(),
))
} else { } else {
Err(anyhow::anyhow!( return if let Some(error) = externally_managed.into_error() {
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv venv`.", Err(anyhow::anyhow!(
venv.root().simplified_display().cyan() "The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv venv`.",
)) venv.root().simplified_display().cyan(),
}; textwrap::indent(&error, " ").green(),
))
} else {
Err(anyhow::anyhow!(
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv venv`.",
venv.root().simplified_display().cyan()
))
};
}
} }
let _lock = venv.lock()?; let _lock = venv.lock()?;

View File

@ -516,7 +516,13 @@ struct PipSyncArgs {
/// `python3.10` on Linux and macOS. /// `python3.10` on Linux and macOS.
/// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`. /// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`.
/// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path. /// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.
#[clap(long, short, verbatim_doc_comment, conflicts_with = "system")] #[clap(
long,
short,
verbatim_doc_comment,
conflicts_with = "system",
group = "discovery"
)]
python: Option<String>, python: Option<String>,
/// Install packages into the system Python. /// Install packages into the system Python.
@ -527,9 +533,18 @@ struct PipSyncArgs {
/// ///
/// WARNING: `--system` is intended for use in continuous integration (CI) environments and /// WARNING: `--system` is intended for use in continuous integration (CI) environments and
/// should be used with caution, as it can modify the system Python installation. /// should be used with caution, as it can modify the system Python installation.
#[clap(long, conflicts_with = "python")] #[clap(long, conflicts_with = "python", group = "discovery")]
system: bool, system: bool,
/// Allow `uv` to modify an `EXTERNALLY-MANAGED` Python installation.
///
/// WARNING: `--break-system-packages` is intended for use in continuous integration (CI)
/// environments, when installing into Python installations that are managed by an external
/// package manager, like `apt`. It should be used with caution, as such Python installations
/// explicitly recommend against modifications by other package managers (like `uv` or `pip`).
#[clap(long, requires = "discovery")]
break_system_packages: bool,
/// Use legacy `setuptools` behavior when building source distributions without a /// Use legacy `setuptools` behavior when building source distributions without a
/// `pyproject.toml`. /// `pyproject.toml`.
#[clap(long)] #[clap(long)]
@ -741,7 +756,13 @@ struct PipInstallArgs {
/// `python3.10` on Linux and macOS. /// `python3.10` on Linux and macOS.
/// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`. /// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`.
/// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path. /// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.
#[clap(long, short, verbatim_doc_comment, conflicts_with = "system")] #[clap(
long,
short,
verbatim_doc_comment,
conflicts_with = "system",
group = "discovery"
)]
python: Option<String>, python: Option<String>,
/// Install packages into the system Python. /// Install packages into the system Python.
@ -752,9 +773,18 @@ struct PipInstallArgs {
/// ///
/// WARNING: `--system` is intended for use in continuous integration (CI) environments and /// WARNING: `--system` is intended for use in continuous integration (CI) environments and
/// should be used with caution, as it can modify the system Python installation. /// should be used with caution, as it can modify the system Python installation.
#[clap(long, conflicts_with = "python")] #[clap(long, conflicts_with = "python", group = "discovery")]
system: bool, system: bool,
/// Allow `uv` to modify an `EXTERNALLY-MANAGED` Python installation.
///
/// WARNING: `--break-system-packages` is intended for use in continuous integration (CI)
/// environments, when installing into Python installations that are managed by an external
/// package manager, like `apt`. It should be used with caution, as such Python installations
/// explicitly recommend against modifications by other package managers (like `uv` or `pip`).
#[clap(long, requires = "discovery")]
break_system_packages: bool,
/// Use legacy `setuptools` behavior when building source distributions without a /// Use legacy `setuptools` behavior when building source distributions without a
/// `pyproject.toml`. /// `pyproject.toml`.
#[clap(long)] #[clap(long)]
@ -848,7 +878,13 @@ struct PipUninstallArgs {
/// `python3.10` on Linux and macOS. /// `python3.10` on Linux and macOS.
/// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`. /// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`.
/// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path. /// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.
#[clap(long, short, verbatim_doc_comment, conflicts_with = "system")] #[clap(
long,
short,
verbatim_doc_comment,
conflicts_with = "system",
group = "discovery"
)]
python: Option<String>, python: Option<String>,
/// Use the system Python to uninstall packages. /// Use the system Python to uninstall packages.
@ -859,9 +895,18 @@ struct PipUninstallArgs {
/// ///
/// WARNING: `--system` is intended for use in continuous integration (CI) environments and /// WARNING: `--system` is intended for use in continuous integration (CI) environments and
/// should be used with caution, as it can modify the system Python installation. /// should be used with caution, as it can modify the system Python installation.
#[clap(long, conflicts_with = "python")] #[clap(long, conflicts_with = "python", group = "discovery")]
system: bool, system: bool,
/// Allow `uv` to modify an `EXTERNALLY-MANAGED` Python installation.
///
/// WARNING: `--break-system-packages` is intended for use in continuous integration (CI)
/// environments, when installing into Python installations that are managed by an external
/// package manager, like `apt`. It should be used with caution, as such Python installations
/// explicitly recommend against modifications by other package managers (like `uv` or `pip`).
#[clap(long, requires = "discovery")]
break_system_packages: bool,
/// Run offline, i.e., without accessing the network. /// Run offline, i.e., without accessing the network.
#[arg(global = true, long)] #[arg(global = true, long)]
offline: bool, offline: bool,
@ -886,7 +931,13 @@ struct PipFreezeArgs {
/// `python3.10` on Linux and macOS. /// `python3.10` on Linux and macOS.
/// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`. /// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`.
/// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path. /// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.
#[clap(long, short, verbatim_doc_comment, conflicts_with = "system")] #[clap(
long,
short,
verbatim_doc_comment,
conflicts_with = "system",
group = "discovery"
)]
python: Option<String>, python: Option<String>,
/// List packages for the system Python. /// List packages for the system Python.
@ -898,7 +949,7 @@ struct PipFreezeArgs {
/// ///
/// WARNING: `--system` is intended for use in continuous integration (CI) environments and /// WARNING: `--system` is intended for use in continuous integration (CI) environments and
/// should be used with caution. /// should be used with caution.
#[clap(long, conflicts_with = "python")] #[clap(long, conflicts_with = "python", group = "discovery")]
system: bool, system: bool,
} }
@ -937,7 +988,13 @@ struct PipListArgs {
/// `python3.10` on Linux and macOS. /// `python3.10` on Linux and macOS.
/// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`. /// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`.
/// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path. /// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.
#[clap(long, short, verbatim_doc_comment, conflicts_with = "system")] #[clap(
long,
short,
verbatim_doc_comment,
conflicts_with = "system",
group = "discovery"
)]
python: Option<String>, python: Option<String>,
/// List packages for the system Python. /// List packages for the system Python.
@ -949,7 +1006,7 @@ struct PipListArgs {
/// ///
/// WARNING: `--system` is intended for use in continuous integration (CI) environments and /// WARNING: `--system` is intended for use in continuous integration (CI) environments and
/// should be used with caution. /// should be used with caution.
#[clap(long, conflicts_with = "python")] #[clap(long, conflicts_with = "python", group = "discovery")]
system: bool, system: bool,
} }
@ -975,7 +1032,13 @@ struct PipShowArgs {
/// `python3.10` on Linux and macOS. /// `python3.10` on Linux and macOS.
/// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`. /// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`.
/// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path. /// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.
#[clap(long, short, verbatim_doc_comment, conflicts_with = "system")] #[clap(
long,
short,
verbatim_doc_comment,
conflicts_with = "system",
group = "discovery"
)]
python: Option<String>, python: Option<String>,
/// List packages for the system Python. /// List packages for the system Python.
@ -987,7 +1050,7 @@ struct PipShowArgs {
/// ///
/// WARNING: `--system` is intended for use in continuous integration (CI) environments and /// WARNING: `--system` is intended for use in continuous integration (CI) environments and
/// should be used with caution. /// should be used with caution.
#[clap(long, conflicts_with = "python")] #[clap(long, conflicts_with = "python", group = "discovery")]
system: bool, system: bool,
} }
@ -1004,7 +1067,13 @@ struct VenvArgs {
/// ///
/// Note that this is different from `--python-version` in `pip compile`, which takes `3.10` or `3.10.13` and /// Note that this is different from `--python-version` in `pip compile`, which takes `3.10` or `3.10.13` and
/// doesn't look for a Python interpreter on disk. /// doesn't look for a Python interpreter on disk.
#[clap(long, short, verbatim_doc_comment, conflicts_with = "system")] #[clap(
long,
short,
verbatim_doc_comment,
conflicts_with = "system",
group = "discovery"
)]
python: Option<String>, python: Option<String>,
/// Use the system Python to uninstall packages. /// Use the system Python to uninstall packages.
@ -1015,7 +1084,7 @@ struct VenvArgs {
/// ///
/// WARNING: `--system` is intended for use in continuous integration (CI) environments and /// WARNING: `--system` is intended for use in continuous integration (CI) environments and
/// should be used with caution, as it can modify the system Python installation. /// should be used with caution, as it can modify the system Python installation.
#[clap(long, conflicts_with = "python")] #[clap(long, conflicts_with = "python", group = "discovery")]
system: bool, system: bool,
/// Install seed packages (`pip`, `setuptools`, and `wheel`) into the virtual environment. /// Install seed packages (`pip`, `setuptools`, and `wheel`) into the virtual environment.
@ -1347,6 +1416,7 @@ async fn run() -> Result<ExitStatus> {
args.strict, args.strict,
args.python, args.python,
args.system, args.system,
args.break_system_packages,
cache, cache,
printer, printer,
) )
@ -1440,6 +1510,7 @@ async fn run() -> Result<ExitStatus> {
args.exclude_newer, args.exclude_newer,
args.python, args.python,
args.system, args.system,
args.break_system_packages,
cache, cache,
printer, printer,
) )
@ -1463,6 +1534,7 @@ async fn run() -> Result<ExitStatus> {
&sources, &sources,
args.python, args.python,
args.system, args.system,
args.break_system_packages,
cache, cache,
if args.offline { if args.offline {
Connectivity::Offline Connectivity::Offline