From fd1d4be372fc2fe3b3f46fb231ed0bec43d8b9be Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Feb 2024 20:42:46 +0100 Subject: [PATCH] Add uv (#657) --- CHANGELOG.md | 2 + rye/src/bootstrap.rs | 3 +- rye/src/cli/list.rs | 23 +++++-- rye/src/cli/rye.rs | 17 +++++ rye/src/config.rs | 16 +++++ rye/src/installer.rs | 24 +++++-- rye/src/lock.rs | 77 ++++++++++++++-------- rye/src/sync.rs | 152 +++++++++++++++++++++++++++---------------- 8 files changed, 217 insertions(+), 97 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c97e8d8ad..0cdb957e5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ _Unreleased_ - Added new `rye list` command and deprecated `rye show --installed-deps` which it replaces. #656 +- Added experimental support for `uv`. + ## 0.23.0 diff --git a/rye/src/bootstrap.rs b/rye/src/bootstrap.rs index e0abce6382..7b75845d67 100644 --- a/rye/src/bootstrap.rs +++ b/rye/src/bootstrap.rs @@ -37,7 +37,7 @@ pub const SELF_PYTHON_TARGET_VERSION: PythonVersionRequest = PythonVersionReques suffix: None, }; -const SELF_VERSION: u64 = 10; +const SELF_VERSION: u64 = 11; const SELF_REQUIREMENTS: &str = r#" build==1.0.3 @@ -57,6 +57,7 @@ unearth==0.14.0 urllib3==2.0.7 virtualenv==20.25.0 ruff==0.1.14 +uv==0.1.0 "#; static FORCED_TO_UPDATE: AtomicBool = AtomicBool::new(false); diff --git a/rye/src/cli/list.rs b/rye/src/cli/list.rs index afeb6fc49c..fe87ed51e4 100644 --- a/rye/src/cli/list.rs +++ b/rye/src/cli/list.rs @@ -5,6 +5,7 @@ use anyhow::{bail, Error}; use clap::Parser; use crate::bootstrap::ensure_self_venv; +use crate::config::Config; use crate::consts::VENV_BIN; use crate::pyproject::PyProject; use crate::utils::{get_venv_python_bin, CommandOutput}; @@ -25,13 +26,21 @@ pub fn execute(cmd: Args) -> Result<(), Error> { } let self_venv = ensure_self_venv(CommandOutput::Normal)?; - let status = Command::new(self_venv.join(VENV_BIN).join("pip")) - .arg("--python") - .arg(&python) - .arg("freeze") - .env("PYTHONWARNINGS", "ignore") - .env("PIP_DISABLE_PIP_VERSION_CHECK", "1") - .status()?; + let status = if Config::current().use_uf() { + Command::new(self_venv.join(VENV_BIN).join("uv")) + .arg("pip") + .arg("freeze") + .env("VIRTUAL_ENV", project.venv_path().as_os_str()) + .status()? + } else { + Command::new(self_venv.join(VENV_BIN).join("pip")) + .arg("--python") + .arg(&python) + .arg("freeze") + .env("PYTHONWARNINGS", "ignore") + .env("PIP_DISABLE_PIP_VERSION_CHECK", "1") + .status()? + }; if !status.success() { bail!("failed to print dependencies via pip"); diff --git a/rye/src/cli/rye.rs b/rye/src/cli/rye.rs index c429b3a97f..db783694e9 100644 --- a/rye/src/cli/rye.rs +++ b/rye/src/cli/rye.rs @@ -441,6 +441,23 @@ fn perform_install( return Err(QuietExit(1).into()); } + // Use uv? + if config_doc + .get("behavior") + .and_then(|x| x.get("use-uv")) + .is_none() + && (matches!(mode, InstallMode::NoPrompts) + || dialoguer::Select::with_theme(tui_theme()) + .with_prompt("Select the preferred package installer") + .item("pip-tools (slow but stable)") + .item("uv (quick but experimental)") + .default(0) + .interact()? + == 1) + { + toml::ensure_table(config_doc, "behavior")["use-uv"] = toml_edit::value(true); + } + // If the global-python flag is not in the settings, ask the user if they want to turn // on global shims upon installation. if config_doc diff --git a/rye/src/config.rs b/rye/src/config.rs index d64a681247..856490ddb2 100644 --- a/rye/src/config.rs +++ b/rye/src/config.rs @@ -246,4 +246,20 @@ impl Config { Ok(rv) } + + /// Indicates if the experimental uv support should be used. + pub fn use_uf(&self) -> bool { + let yes = self + .doc + .get("behavior") + .and_then(|x| x.get("use-uv")) + .and_then(|x| x.as_bool()) + .unwrap_or(false); + if yes && cfg!(windows) { + warn!("uv enabled in config but not supported on windows"); + false + } else { + yes + } + } } diff --git a/rye/src/installer.rs b/rye/src/installer.rs index f7cf233f3f..25a529001a 100644 --- a/rye/src/installer.rs +++ b/rye/src/installer.rs @@ -127,14 +127,24 @@ pub fn install( requirement.name.as_str(), )?; - let mut cmd = Command::new(self_venv.join(VENV_BIN).join("pip")); - cmd.arg("--python") - .arg(&py) - .arg("install") - .env("PYTHONWARNINGS", "ignore") - .env("PIP_DISABLE_PIP_VERSION_CHECK", "1"); - sources.add_as_pip_args(&mut cmd); + let mut cmd = if Config::current().use_uf() { + let mut cmd = Command::new(self_venv.join(VENV_BIN).join("uv")); + cmd.arg("pip") + .arg("install") + .env("VIRTUAL_ENV", &target_venv_path) + .env("PYTHONWARNINGS", "ignore"); + cmd + } else { + let mut cmd = Command::new(self_venv.join(VENV_BIN).join("pip")); + cmd.arg("--python") + .arg(&py) + .arg("install") + .env("PYTHONWARNINGS", "ignore") + .env("PIP_DISABLE_PIP_VERSION_CHECK", "1"); + cmd + }; + sources.add_as_pip_args(&mut cmd); if output == CommandOutput::Verbose { cmd.arg("--verbose"); } else { diff --git a/rye/src/lock.rs b/rye/src/lock.rs index 34900a1ab1..2629c4e1af 100644 --- a/rye/src/lock.rs +++ b/rye/src/lock.rs @@ -15,6 +15,9 @@ use serde::Serialize; use tempfile::NamedTempFile; use url::Url; +use crate::bootstrap::ensure_self_venv; +use crate::config::Config; +use crate::consts::VENV_BIN; use crate::piptools::{get_pip_compile, get_pip_tools_version, PipToolsVersion}; use crate::pyproject::{ normalize_package_name, DependencyKind, ExpandedSources, PyProject, Workspace, @@ -134,7 +137,7 @@ pub fn update_workspace_lockfile( sources, lock_options, &exclusions, - &["--pip-args=--no-deps"], + true, )?; Ok(()) @@ -287,7 +290,7 @@ pub fn update_single_project_lockfile( sources, lock_options, &exclusions, - &[], + false, )?; Ok(()) @@ -303,7 +306,7 @@ fn generate_lockfile( sources: &ExpandedSources, lock_options: &LockOptions, exclusions: &HashSet, - extra_args: &[&str], + no_deps: bool, ) -> Result<(), Error> { let scratch = tempfile::tempdir()?; let requirements_file = scratch.path().join("requirements.txt"); @@ -313,36 +316,55 @@ fn generate_lockfile( fs::write(&requirements_file, b"")?; } - let pip_compile = get_pip_compile(py_ver, output)?; - let mut cmd = Command::new(pip_compile); - - // legacy pip tools requires some extra parameters - if get_pip_tools_version(py_ver) == PipToolsVersion::Legacy { - cmd.arg("--resolver=backtracking"); - } + let mut cmd = if Config::current().use_uf() { + let self_venv = ensure_self_venv(output)?; + let mut cmd = Command::new(self_venv.join(VENV_BIN).join("uv")); + cmd.arg("pip") + .arg("compile") + .arg("--no-header") + .arg(format!( + "--python-version={}.{}.{}", + py_ver.major, py_ver.minor, py_ver.patch + )); + if output == CommandOutput::Verbose { + cmd.arg("--verbose"); + } else if output == CommandOutput::Quiet { + cmd.arg("-q"); + } + cmd + } else { + let mut cmd = Command::new(get_pip_compile(py_ver, output)?); + // legacy pip tools requires some extra parameters + if get_pip_tools_version(py_ver) == PipToolsVersion::Legacy { + cmd.arg("--resolver=backtracking"); + } + cmd.arg("--strip-extras") + .arg("--allow-unsafe") + .arg("--no-header") + .arg("--annotate") + .arg("--pip-args") + .arg(format!( + "--python-version=\"{}.{}.{}\"{}", + py_ver.major, + py_ver.minor, + py_ver.patch, + if no_deps { " --no-deps" } else { "" } + )) + .arg(if output == CommandOutput::Verbose { + "--verbose" + } else { + "-q" + }); + cmd + }; - cmd.arg("--no-annotate") - .arg("--strip-extras") - .arg("--allow-unsafe") - .arg("--no-header") - .arg("--annotate") - .arg("--pip-args") - .arg(format!( - "--python-version=\"{}.{}\"", - py_ver.major, py_ver.minor - )) - .arg("-o") + cmd.arg("-o") .arg(&requirements_file) .arg(requirements_file_in) .current_dir(workspace_path) .env("PYTHONWARNINGS", "ignore") .env("PROJECT_ROOT", make_project_root_fragment(workspace_path)); - if output == CommandOutput::Verbose { - cmd.arg("--verbose"); - } else { - cmd.arg("-q"); - } for pkg in &lock_options.update { cmd.arg("--upgrade-package"); cmd.arg(pkg); @@ -354,7 +376,6 @@ fn generate_lockfile( cmd.arg("--pre"); } sources.add_as_pip_args(&mut cmd); - cmd.args(extra_args); set_proxy_variables(&mut cmd); let status = cmd.status().context("unable to run pip-compile")?; if !status.success() { @@ -427,6 +448,8 @@ fn finalize_lockfile( } }; continue; + } else if line.starts_with('#') { + continue; } writeln!(rv, "{}", line)?; } diff --git a/rye/src/sync.rs b/rye/src/sync.rs index 0874e4bc9d..732916858a 100644 --- a/rye/src/sync.rs +++ b/rye/src/sync.rs @@ -245,50 +245,66 @@ pub fn sync(mut cmd: SyncOptions) -> Result<(), Error> { if output != CommandOutput::Quiet { echo!("Installing dependencies"); } - let tempdir = tempdir()?; - let mut pip_sync_cmd = Command::new(get_pip_sync(&py_ver, output)?); - let root = pyproject.workspace_path(); - let py_path = get_venv_python_bin(&venv); - - // we need to run this after we have run the `get_pip_sync` command - // as this is what bootstraps or updates the pip tools installation. - // This is needed as on unix platforms we need to search the module path. - symlink_dir( - get_pip_module(&get_pip_tools_venv_path(&py_ver)) - .context("could not locate pip")?, - tempdir.path().join("pip"), - ) - .context("failed linking pip module into for pip-sync")?; - pip_sync_cmd - .env("PROJECT_ROOT", make_project_root_fragment(&root)) - .env("PYTHONPATH", tempdir.path()) - .current_dir(&root) - .arg("--python-executable") - .arg(&py_path) - .arg("--pip-args") - .arg("--no-deps"); + let tempdir = tempdir()?; + let mut sync_cmd = if Config::current().use_uf() { + let mut uv_sync_cmd = Command::new(self_venv.join(VENV_BIN).join("uv")); + uv_sync_cmd.arg("pip").arg("sync"); + let root = pyproject.workspace_path(); + + uv_sync_cmd + .env("PROJECT_ROOT", make_project_root_fragment(&root)) + .env("VIRTUAL_ENV", pyproject.venv_path().as_os_str()) + .current_dir(&root); + uv_sync_cmd + } else { + let mut pip_sync_cmd = Command::new(get_pip_sync(&py_ver, output)?); + let root = pyproject.workspace_path(); + let py_path = get_venv_python_bin(&venv); + + // we need to run this after we have run the `get_pip_sync` command + // as this is what bootstraps or updates the pip tools installation. + // This is needed as on unix platforms we need to search the module path. + symlink_dir( + get_pip_module(&get_pip_tools_venv_path(&py_ver)) + .context("could not locate pip")?, + tempdir.path().join("pip"), + ) + .context("failed linking pip module into for pip-sync")?; + + pip_sync_cmd + .env("PROJECT_ROOT", make_project_root_fragment(&root)) + .env("PYTHONPATH", tempdir.path()) + .current_dir(&root) + .arg("--python-executable") + .arg(&py_path) + .arg("--pip-args") + .arg("--no-deps"); + + if output != CommandOutput::Quiet { + pip_sync_cmd.env("PYTHONWARNINGS", "ignore"); + } else if output == CommandOutput::Verbose && env::var("PIP_VERBOSE").is_err() { + pip_sync_cmd.env("PIP_VERBOSE", "2"); + } + pip_sync_cmd + }; - sources.add_as_pip_args(&mut pip_sync_cmd); + sources.add_as_pip_args(&mut sync_cmd); if cmd.dev && dev_lockfile.is_file() { - pip_sync_cmd.arg(&dev_lockfile); + sync_cmd.arg(&dev_lockfile); } else { - pip_sync_cmd.arg(&lockfile); + sync_cmd.arg(&lockfile); } if output == CommandOutput::Verbose { - pip_sync_cmd.arg("--verbose"); - if env::var("PIP_VERBOSE").is_err() { - pip_sync_cmd.env("PIP_VERBOSE", "2"); - } - } else if output != CommandOutput::Quiet { - pip_sync_cmd.env("PYTHONWARNINGS", "ignore"); - } else { - pip_sync_cmd.arg("-q"); + sync_cmd.arg("--verbose"); + } else if output == CommandOutput::Quiet { + sync_cmd.arg("-q"); } - set_proxy_variables(&mut pip_sync_cmd); - let status = pip_sync_cmd.status().context("unable to run pip-sync")?; + set_proxy_variables(&mut sync_cmd); + let status = sync_cmd.status().context("unable to run pip-sync")?; + if !status.success() { bail!("Installation of dependencies failed"); } @@ -309,27 +325,44 @@ pub fn create_virtualenv( venv: &Path, prompt: &str, ) -> Result<(), Error> { - // create the venv folder first so we can manipulate some flags on it. - fs::create_dir_all(venv) - .with_context(|| format!("unable to create virtualenv folder '{}'", venv.display()))?; - - update_venv_sync_marker(output, venv); - let py_bin = get_toolchain_python_bin(py_ver)?; - let mut venv_cmd = Command::new(self_venv.join(VENV_BIN).join("virtualenv")); - if output == CommandOutput::Verbose { - venv_cmd.arg("--verbose"); + + let mut venv_cmd = if Config::current().use_uf() { + // try to kill the empty venv if there is one as uv can't work otherwise. + fs::remove_dir(venv).ok(); + let mut venv_cmd = Command::new(self_venv.join(VENV_BIN).join("uv")); + venv_cmd.arg("venv"); + if output == CommandOutput::Verbose { + venv_cmd.arg("--verbose"); + } else { + venv_cmd.arg("-q"); + } + venv_cmd.arg("-p"); + venv_cmd.arg(&py_bin); + venv_cmd } else { - venv_cmd.arg("-q"); - venv_cmd.env("PYTHONWARNINGS", "ignore"); - } - venv_cmd.arg("-p"); - venv_cmd.arg(&py_bin); - venv_cmd.arg("--no-seed"); - venv_cmd.arg("--prompt"); - venv_cmd.arg(prompt); - venv_cmd.arg("--"); - venv_cmd.arg(venv); + // create the venv folder first so we can manipulate some flags on it. + fs::create_dir_all(venv) + .with_context(|| format!("unable to create virtualenv folder '{}'", venv.display()))?; + + update_venv_sync_marker(output, venv); + let mut venv_cmd = Command::new(self_venv.join(VENV_BIN).join("virtualenv")); + if output == CommandOutput::Verbose { + venv_cmd.arg("--verbose"); + } else { + venv_cmd.arg("-q"); + venv_cmd.env("PYTHONWARNINGS", "ignore"); + } + venv_cmd.arg("-p"); + venv_cmd.arg(&py_bin); + venv_cmd.arg("--no-seed"); + venv_cmd.arg("--prompt"); + venv_cmd.arg(prompt); + venv_cmd + }; + + venv_cmd.arg("--").arg(venv); + let status = venv_cmd .status() .context("unable to invoke virtualenv command")?; @@ -337,6 +370,11 @@ pub fn create_virtualenv( bail!("failed to initialize virtualenv"); } + // uv can only do it now + if Config::current().use_uf() { + update_venv_sync_marker(output, venv); + } + // On UNIX systems Python is unable to find the tcl config that is placed // outside of the virtualenv. It also sometimes is entirely unable to find // the tcl config that comes from the standalone python builds. @@ -353,7 +391,11 @@ pub fn create_virtualenv( fn update_venv_sync_marker(output: CommandOutput, venv_path: &Path) { if let Err(err) = mark_path_sync_ignore(venv_path, Config::current().venv_mark_sync_ignore()) { if output != CommandOutput::Quiet && Config::current().venv_mark_sync_ignore() { - warn!("unable to mark virtualenv ignored for cloud sync: {}", err); + warn!( + "unable to mark virtualenv {} ignored for cloud sync: {}", + venv_path.display(), + err + ); } } }