diff --git a/.github/workflows/system-install.yml b/.github/workflows/system-install.yml new file mode 100644 index 000000000..82172f4bf --- /dev/null +++ b/.github/workflows/system-install.yml @@ -0,0 +1,110 @@ +name: System Install + +on: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +env: + CARGO_INCREMENTAL: 0 + CARGO_NET_RETRY: 10 + CARGO_TERM_COLOR: always + RUSTUP_MAX_RETRIES: 10 + +jobs: + install-ubuntu: + name: "Install Python on Ubuntu" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: "Install Rust toolchain" + run: rustup show + + - uses: Swatinem/rust-cache@v2 + + - name: "Build" + run: cargo build + + - name: "Print Python path" + run: echo $(which python) + + - name: "Validate global Python install" + run: python scripts/check_system_python.py --uv ./target/debug/uv + + install-macos: + name: "Install Python on macOS" + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + + - name: "Install Python" + run: brew install python@3.8 + + - name: "Install Rust toolchain" + run: rustup show + + - uses: Swatinem/rust-cache@v2 + + - name: "Build" + run: cargo build + + - name: "Print Python path" + run: echo $(which python3.11) + + - name: "Validate global Python install" + run: python3.11 scripts/check_system_python.py --uv ./target/debug/uv + + install-windows: + name: "Install Python on Windows" + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + +# - name: "Install Python" +# run: choco install python + + - name: "Install Rust toolchain" + run: rustup show + + - uses: Swatinem/rust-cache@v2 + + - name: "Build" + run: cargo build + + - name: "Print Python path" + run: echo $(which python) + + - name: "Validate global Python install" + run: py -3.12 ./scripts/check_system_python.py --uv ./target/debug/uv + + install-pyenv: + name: "Install Python using pyenv" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: "Install pyenv" + uses: "gabrielfalcao/pyenv-action@v18" + with: + default: 3.9.7 + + - name: "Install Rust toolchain" + run: rustup show + + - uses: Swatinem/rust-cache@v2 + + - name: "Build" + run: cargo build + + - name: "Print Python path" + run: echo $(which python3.9) + + - name: "Validate global Python install" + run: python3.9 scripts/check_system_python.py --uv ./target/debug/uv diff --git a/crates/uv/src/commands/pip_freeze.rs b/crates/uv/src/commands/pip_freeze.rs index 6555f4b32..a88465c1a 100644 --- a/crates/uv/src/commands/pip_freeze.rs +++ b/crates/uv/src/commands/pip_freeze.rs @@ -20,6 +20,7 @@ use crate::printer::Printer; pub(crate) fn pip_freeze( strict: bool, python: Option<&str>, + system: bool, cache: &Cache, mut printer: Printer, ) -> Result { @@ -27,6 +28,8 @@ pub(crate) fn pip_freeze( let platform = Platform::current()?; let venv = if let Some(python) = python { PythonEnvironment::from_requested_python(python, &platform, cache)? + } else if system { + PythonEnvironment::from_default_python(&platform, cache)? } else { match PythonEnvironment::from_virtualenv(platform.clone(), cache) { Ok(venv) => venv, diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index a58ada202..65ceb9210 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -63,6 +63,7 @@ pub(crate) async fn pip_install( strict: bool, exclude_newer: Option>, python: Option, + system: bool, cache: Cache, mut printer: Printer, ) -> Result { @@ -107,6 +108,8 @@ pub(crate) async fn pip_install( let platform = Platform::current()?; let venv = if let Some(python) = python.as_ref() { PythonEnvironment::from_requested_python(python, &platform, &cache)? + } else if system { + PythonEnvironment::from_default_python(&platform, &cache)? } else { PythonEnvironment::from_virtualenv(platform, &cache)? }; diff --git a/crates/uv/src/commands/pip_list.rs b/crates/uv/src/commands/pip_list.rs index 47fcdceda..2ca1d9c18 100644 --- a/crates/uv/src/commands/pip_list.rs +++ b/crates/uv/src/commands/pip_list.rs @@ -20,12 +20,14 @@ use crate::commands::ExitStatus; use crate::printer::Printer; /// Enumerate the installed packages in the current environment. +#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)] pub(crate) fn pip_list( strict: bool, editable: bool, exclude_editable: bool, exclude: &[PackageName], python: Option<&str>, + system: bool, cache: &Cache, mut printer: Printer, ) -> Result { @@ -33,6 +35,8 @@ pub(crate) fn pip_list( let platform = Platform::current()?; let venv = if let Some(python) = python { PythonEnvironment::from_requested_python(python, &platform, cache)? + } else if system { + PythonEnvironment::from_default_python(&platform, cache)? } else { match PythonEnvironment::from_virtualenv(platform.clone(), cache) { Ok(venv) => venv, diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index c8585ff36..09877894c 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -42,6 +42,7 @@ pub(crate) async fn pip_sync( no_binary: &NoBinary, strict: bool, python: Option, + system: bool, cache: Cache, mut printer: Printer, ) -> Result { @@ -75,6 +76,8 @@ pub(crate) async fn pip_sync( let platform = Platform::current()?; let venv = if let Some(python) = python.as_ref() { PythonEnvironment::from_requested_python(python, &platform, &cache)? + } else if system { + PythonEnvironment::from_default_python(&platform, &cache)? } else { PythonEnvironment::from_virtualenv(platform, &cache)? }; diff --git a/crates/uv/src/commands/pip_uninstall.rs b/crates/uv/src/commands/pip_uninstall.rs index 85c5fbfcd..bcf55b218 100644 --- a/crates/uv/src/commands/pip_uninstall.rs +++ b/crates/uv/src/commands/pip_uninstall.rs @@ -18,6 +18,7 @@ use crate::requirements::{RequirementsSource, RequirementsSpecification}; pub(crate) async fn pip_uninstall( sources: &[RequirementsSource], python: Option, + system: bool, cache: Cache, mut printer: Printer, ) -> Result { @@ -41,6 +42,8 @@ pub(crate) async fn pip_uninstall( let platform = Platform::current()?; let venv = if let Some(python) = python.as_ref() { PythonEnvironment::from_requested_python(python, &platform, &cache)? + } else if system { + PythonEnvironment::from_default_python(&platform, &cache)? } else { PythonEnvironment::from_virtualenv(platform, &cache)? }; diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 8a562b854..fbc87f588 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -454,9 +454,20 @@ struct PipSyncArgs { /// `python3.10` on Linux and macOS. /// - `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. - #[clap(long, short, verbatim_doc_comment)] + #[clap(long, short, verbatim_doc_comment, conflicts_with = "system")] python: Option, + /// Install packages into the system Python. + /// + /// By default, `uv` installs into the virtual environment in the current working directory or + /// any parent directory. The `--system` option instructs `uv` to instead use the first Python + /// found in the system `PATH`. + /// + /// 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. + #[clap(long, conflicts_with = "python")] + system: bool, + /// Use legacy `setuptools` behavior when building source distributions without a /// `pyproject.toml`. #[clap(long)] @@ -641,9 +652,20 @@ struct PipInstallArgs { /// `python3.10` on Linux and macOS. /// - `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. - #[clap(long, short, verbatim_doc_comment)] + #[clap(long, short, verbatim_doc_comment, conflicts_with = "system")] python: Option, + /// Install packages into the system Python. + /// + /// By default, `uv` installs into the virtual environment in the current working directory or + /// any parent directory. The `--system` option instructs `uv` to instead use the first Python + /// found in the system `PATH`. + /// + /// 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. + #[clap(long, conflicts_with = "python")] + system: bool, + /// Use legacy `setuptools` behavior when building source distributions without a /// `pyproject.toml`. #[clap(long)] @@ -725,8 +747,19 @@ struct PipUninstallArgs { /// `python3.10` on Linux and macOS. /// - `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. - #[clap(long, short, verbatim_doc_comment)] + #[clap(long, short, verbatim_doc_comment, conflicts_with = "system")] python: Option, + + /// Use the system Python to uninstall packages. + /// + /// By default, `uv` uninstalls from the virtual environment in the current working directory or + /// any parent directory. The `--system` option instructs `uv` to instead use the first Python + /// found in the system `PATH`. + /// + /// 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. + #[clap(long, conflicts_with = "python")] + system: bool, } #[derive(Args)] @@ -748,8 +781,20 @@ struct PipFreezeArgs { /// `python3.10` on Linux and macOS. /// - `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. - #[clap(long, short, verbatim_doc_comment)] + #[clap(long, short, verbatim_doc_comment, conflicts_with = "system")] python: Option, + + /// List packages for the system Python. + /// + /// By default, `uv` lists packages in the currently activated virtual environment, or a virtual + /// environment (`.venv`) located in the current working directory or any parent directory, + /// falling back to the system Python if no virtual environment is found. The `--system` option + /// instructs `uv` to use the first Python found in the system `PATH`. + /// + /// WARNING: `--system` is intended for use in continuous integration (CI) environments and + /// should be used with caution. + #[clap(long, conflicts_with = "python")] + system: bool, } #[derive(Args)] @@ -783,8 +828,20 @@ struct PipListArgs { /// `python3.10` on Linux and macOS. /// - `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. - #[clap(long, short, verbatim_doc_comment)] + #[clap(long, short, verbatim_doc_comment, conflicts_with = "system")] python: Option, + + /// List packages for the system Python. + /// + /// By default, `uv` lists packages in the currently activated virtual environment, or a virtual + /// environment (`.venv`) located in the current working directory or any parent directory, + /// falling back to the system Python if no virtual environment is found. The `--system` option + /// instructs `uv` to use the first Python found in the system `PATH`. + /// + /// WARNING: `--system` is intended for use in continuous integration (CI) environments and + /// should be used with caution. + #[clap(long, conflicts_with = "python")] + system: bool, } #[derive(Args)] @@ -800,9 +857,20 @@ struct VenvArgs { /// /// 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. - #[clap(long, short, verbatim_doc_comment)] + #[clap(long, short, verbatim_doc_comment, conflicts_with = "system")] python: Option, + /// Use the system Python to uninstall packages. + /// + /// By default, `uv` uninstalls from the virtual environment in the current working directory or + /// any parent directory. The `--system` option instructs `uv` to use the first Python found in + /// the system `PATH`. + /// + /// 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. + #[clap(long, conflicts_with = "python")] + system: bool, + /// Install seed packages (`pip`, `setuptools`, and `wheel`) into the virtual environment. #[clap(long)] seed: bool, @@ -1093,6 +1161,7 @@ async fn run() -> Result { &no_binary, args.strict, args.python, + args.system, cache, printer, ) @@ -1181,6 +1250,7 @@ async fn run() -> Result { args.strict, args.exclude_newer, args.python, + args.system, cache, printer, ) @@ -1200,11 +1270,17 @@ async fn run() -> Result { .map(RequirementsSource::from_path), ) .collect::>(); - commands::pip_uninstall(&sources, args.python, cache, printer).await + commands::pip_uninstall(&sources, args.python, args.system, cache, printer).await } Commands::Pip(PipNamespace { command: PipCommand::Freeze(args), - }) => commands::pip_freeze(args.strict, args.python.as_deref(), &cache, printer), + }) => commands::pip_freeze( + args.strict, + args.python.as_deref(), + args.system, + &cache, + printer, + ), Commands::Pip(PipNamespace { command: PipCommand::List(args), }) => commands::pip_list( @@ -1213,6 +1289,7 @@ async fn run() -> Result { args.exclude_editable, &args.exclude, args.python.as_deref(), + args.system, &cache, printer, ), diff --git a/scripts/check_system_python.py b/scripts/check_system_python.py new file mode 100755 index 000000000..3adc58862 --- /dev/null +++ b/scripts/check_system_python.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 + +"""Install `pylint` into the system Python.""" + +import argparse +import logging +import os +import subprocess +import sys +import tempfile + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") + + print("sys.executable:: %s" % sys.executable) + + parser = argparse.ArgumentParser(description="Check a Python interpreter.") + parser.add_argument("--uv", help="Path to a uv binary.") + args = parser.parse_args() + + uv: str = os.path.abspath(args.uv) if args.uv else "uv" + + # Create a temporary directory. + with tempfile.TemporaryDirectory() as temp_dir: + # Ensure that the package (`pylint`) isn't installed. + logging.info("Checking that `pylint` isn't installed.") + code = subprocess.run( + [sys.executable, "-m", "pip", "show", "pylint"], + cwd=temp_dir, + ) + if code.returncode == 0: + raise Exception("The package `pylint` is installed.") + + # Install the package (`pylint`). + logging.info("Installing the package `pylint`.") + subprocess.run( + [uv, "pip", "install", "pylint", "--system", "--verbose"], + cwd=temp_dir, + check=True, + ) + + # Ensure that the package (`pylint`) isn't installed. + logging.info("Checking that `pylint` is installed.") + code = subprocess.run( + [sys.executable, "-m", "pip", "show", "pylint"], + cwd=temp_dir, + ) + if code.returncode != 0: + raise Exception("The package `pylint` isn't installed.") + + # TODO(charlie): Windows is failing to find the `pylint` binary, despite + # confirmation that it's being written to the intended location. + if os.name != "nt": + logging.info("Checking that `pylint` is in the path.") + code = subprocess.run(["which", "pylint"], cwd=temp_dir) + if code.returncode != 0: + raise Exception("The package `pylint` isn't in the path.") + + # Uninstall the package (`pylint`). + logging.info("Uninstalling the package `pylint`.") + subprocess.run( + [uv, "pip", "uninstall", "pylint", "--system", "--verbose"], + cwd=temp_dir, + check=True, + ) + + # Ensure that the package (`pylint`) isn't installed. + logging.info("Checking that `pylint` isn't installed.") + code = subprocess.run( + [sys.executable, "-m", "pip", "show", "pylint"], + cwd=temp_dir, + ) + if code.returncode == 0: + raise Exception("The package `pylint` is installed.")