From 83a83653828a7d8275b5cc4752215940dcb7b9de Mon Sep 17 00:00:00 2001 From: Naz Quadri Date: Sat, 13 Jul 2024 14:28:32 -0400 Subject: [PATCH] Remove pip-tools, use uv for all operations Remove pip even from uv venvs Add back rye/self toolchain in use check Add docs updates --- docs/guide/config.md | 3 +- docs/guide/docker.md | 33 ++++-- docs/guide/rust.md | 8 -- docs/guide/sync.md | 10 +- docs/philosophy.md | 10 +- rye/src/bootstrap.rs | 5 +- rye/src/cli/add.rs | 218 +++------------------------------------ rye/src/cli/build.rs | 24 ++--- rye/src/cli/list.rs | 48 +++------ rye/src/cli/mod.rs | 4 + rye/src/cli/rye.rs | 17 --- rye/src/cli/toolchain.rs | 11 +- rye/src/config.rs | 20 +++- rye/src/installer.rs | 73 ++++--------- rye/src/lock.rs | 116 +++++---------------- rye/src/main.rs | 1 - rye/src/piptools.rs | 104 ------------------- rye/src/sync.rs | 135 +++++------------------- rye/src/utils/mod.rs | 2 +- rye/src/uv.rs | 28 +---- 20 files changed, 171 insertions(+), 699 deletions(-) delete mode 100644 rye/src/piptools.rs diff --git a/docs/guide/config.md b/docs/guide/config.md index bda7499c7c..4c50130680 100644 --- a/docs/guide/config.md +++ b/docs/guide/config.md @@ -82,7 +82,8 @@ force-rye-managed = false global-python = false # When set to `true`, Rye will use `uv` for package resolution and installation. -# Set to `false` to fall back to the `pip-tools` resolver. +# `uv` is the only engine in use at this point and setting this to false will produce a warning +# This flag will be deprecated in a future version. # Learn more about uv here: https://github.com/astral-sh/uv use-uv = true diff --git a/docs/guide/docker.md b/docs/guide/docker.md index 56e7dd5ea3..e9317c5981 100644 --- a/docs/guide/docker.md +++ b/docs/guide/docker.md @@ -14,11 +14,29 @@ This guide requires some familiarity with Docker and Dockerfiles. - Your `pyproject.toml` should contain `virtual = true` under the `[tool.rye]` section. If it's not there, add it and run `rye sync`. - If you're just setting up a project, run `rye init --virtual` instead of `rye init`. -1. Create a `Dockerfile` in your project root with the following content: +2. Create a `Dockerfile` in your project root with the following content, using [`uv`](https://github.com/astral-sh/uv): ```Dockerfile FROM python:slim + # see below for other uv install options + RUN pip install uv + + WORKDIR /app + COPY requirements.lock ./ + RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --no-cache-dir -r requirements.lock + + COPY src . + CMD python main.py + ``` + + Other [`uv`](https://github.com/astral-sh/uv) install options [here](https://github.com/astral-sh/uv?tab=readme-ov-file#getting-started) + + Or using pip: + + ```Dockerfile + FROM python:slim + WORKDIR /app COPY requirements.lock ./ RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -r requirements.lock @@ -27,7 +45,7 @@ This guide requires some familiarity with Docker and Dockerfiles. CMD python main.py ``` -2. You can now build your image like this: +3. You can now build your image like this: ```bash docker build . @@ -56,11 +74,12 @@ The `Dockerfile`s in this guide are examples. Some adjustments you might want to If your code is an installable package, it's recommended that you first build it, then install it inside your Docker image. This way you can be sure that the image is exactly the same as what a user installation would be. -An example `Dockerfile` might look like this: +An example `Dockerfile` might look like this with [`uv`](https://github.com/astral-sh/uv): ```Dockerfile FROM python:slim -RUN --mount=source=dist,target=/dist PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir /dist/*.whl +RUN pip install uv +RUN --mount=source=dist,target=/dist PYTHONDONTWRITEBYTECODE=1 uv pip install --no-cache-dir /dist/*.whl CMD python -m my_package ``` @@ -78,8 +97,8 @@ The [Dockerfile adjustments from the previous section](#dockerfile-adjustments) ## Explanations -Rye's lock file standard is the `requirements.txt` format from `pip`, so you don't actually need `rye` in your container to be able to install dependencies. +Rye's lock file standard is the `requirements.txt` format from `pip` and used by [`uv`](https://github.com/astral-sh/uv), so you don't actually need `rye` in your container to be able to install dependencies. This makes the Dockerfile much simpler and avoids the necessity for multi-stage builds if small images are desired. -The `PYTHONDONTWRITEBYTECODE=1` env var and `--no-cache-dir` parameters when invoking Pip both make the image smaller by not writing any temporary files. -Both Bytecode and Pip caches are speeding things up when running Pip multiple times, but since we are working in a container, we can be pretty sure that we'll never invoke pip again. +The `PYTHONDONTWRITEBYTECODE=1` env var and `--no-cache-dir` parameters when invoking [`uv`](https://github.com/astral-sh/uv) and `pip` both make the image smaller by not writing any temporary files. +Both Bytecode and [`uv`](https://github.com/astral-sh/uv)/`pip` caches are speeding things up when running installs multiple times, but since we are working in a container, we can be pretty sure that we'll never invoke [`uv`](https://github.com/astral-sh/uv) or `pip` again. diff --git a/docs/guide/rust.md b/docs/guide/rust.md index b0b13a5846..55b1ebfdd1 100644 --- a/docs/guide/rust.md +++ b/docs/guide/rust.md @@ -41,14 +41,6 @@ it as a global tool: rye install maturin ``` -Note that `maturin develop` requires `pip` to be installed into the virtualenv. Before -you can use it you need to add it: - -``` -rye add --dev pip -rye sync -``` - Rye recommends mixed python/rust modules. In that case you can save some valuable iteration time by running `maturin develop --skip-install`: diff --git a/docs/guide/sync.md b/docs/guide/sync.md index aa2438a380..082448f9f5 100644 --- a/docs/guide/sync.md +++ b/docs/guide/sync.md @@ -1,10 +1,6 @@ # Syncing and Locking -Rye supports two systems to manage dependencies: -[uv](https://github.com/astral-sh/uv) and -[pip-tools](https://github.com/jazzband/pip-tools). It currently defaults to -`uv` as it offers significantly better performance, but will offer you the -option to use `pip-tools` instead. +Rye supports only supports [`uv`](https://github.com/astral-sh/uv) to manage dependencies. In order to download dependencies rye creates two "lockfiles" (called `requirements.lock` and `requirements-dev.lock`). These are not real lockfiles @@ -67,7 +63,7 @@ rye lock Flask --pre By default (unless the `tool.rye.lock-with-sources` config key is set to `true` in the `pyproject.toml`) lock files are not generated with source references. This means that -if custom sources are used the lock file cannot be installed via `pip` unless also +if custom sources are used the lock file cannot be installed via `uv` or `pip` unless `--find-links` and other parameters are manually passed. This can be particularly useful when the lock file is used for docker image builds. @@ -103,7 +99,7 @@ rye sync --no-dev ## Limitations Lockfiles depend on the platform they were generated on. This is a known limitation -in pip-tools. +in `uv` and `pip-tools`. For example, if your project relies on platform-specific packages and you generate lockfiles on Windows, these lockfiles will include Windows-specific projects. diff --git a/docs/philosophy.md b/docs/philosophy.md index 44481d300e..b0a1811cba 100644 --- a/docs/philosophy.md +++ b/docs/philosophy.md @@ -14,10 +14,9 @@ on my mind when I built it: - **No Core Non Standard Stuff:** Rye (with the exception of it's own `tool` section in the `pyproject.toml`) uses standardized keys. That means it uses regular requirements as you would expect. It also does not use a custom lock file - format and uses [`uv`](https://github.com/astral-sh/uv) and - [`pip-tools`](https://github.com/jazzband/pip-tools) behind the scenes. + format and uses [`uv`](https://github.com/astral-sh/uv). -- **No Pip:** Rye uses pip, but it does not expose it. It manages dependencies in +- **No Pip:** Rye uses [`uv`](https://github.com/astral-sh/uv) only at this point. It manages dependencies in `pyproject.toml` only. - **No System Python:** I can't deal with any more linux distribution weird Python @@ -53,11 +52,6 @@ lack of standardization. Here is what this project ran into over the years: which allows both remote and local references to co-exist and it rewrites them on publish. -- **No Exposed Pip:** pip is intentionally not exposed. If you were to install something - into the virtualenv, it disappears next time you sync. If you symlink `rye` to - `~/.rye/shims/pip` you can get access to pip without installing it into the - virtualenv. There be dragons. - - **No Workspace Spec:** for monorepos and things of that nature, the Python ecosystem would need a definition of workspaces. Today that does not exist which forces every tool to come up with it's own solutions to this problem. diff --git a/rye/src/bootstrap.rs b/rye/src/bootstrap.rs index da001ea343..78ab9aff5a 100644 --- a/rye/src/bootstrap.rs +++ b/rye/src/bootstrap.rs @@ -12,7 +12,6 @@ use once_cell::sync::Lazy; use tempfile::tempdir_in; use crate::config::Config; -use crate::piptools::LATEST_PIP; use crate::platform::{ get_app_dir, get_canonical_py_path, get_python_bin_within, get_toolchain_python_bin, list_known_toolchains, @@ -154,8 +153,8 @@ pub fn ensure_self_venv_with_toolchain( let uv_venv = uv.venv(&venv_dir, &py_bin, &version, None)?; // write our marker uv_venv.write_marker()?; - // update pip and our requirements - uv_venv.update(LATEST_PIP, SELF_REQUIREMENTS)?; + // update our requirements + uv_venv.update_requirements(SELF_REQUIREMENTS)?; // Update the shims let shims = app_dir.join("shims"); diff --git a/rye/src/cli/add.rs b/rye/src/cli/add.rs index 9fa56f2ad2..18ae483626 100644 --- a/rye/src/cli/add.rs +++ b/rye/src/cli/add.rs @@ -1,63 +1,22 @@ use std::env; use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; use std::str::FromStr; use anyhow::{anyhow, bail, Context, Error}; use clap::{Parser, ValueEnum}; -use pep440_rs::{Operator, Version, VersionSpecifier, VersionSpecifiers}; +use pep440_rs::{Operator, VersionSpecifier, VersionSpecifiers}; use pep508_rs::{Requirement, VersionOrUrl}; -use serde::Deserialize; use url::Url; use crate::bootstrap::ensure_self_venv; use crate::config::Config; -use crate::consts::VENV_BIN; use crate::lock::KeyringProvider; use crate::pyproject::{BuildSystem, DependencyKind, ExpandedSources, PyProject}; use crate::sources::py::PythonVersion; use crate::sync::{autosync, sync, SyncOptions}; -use crate::utils::{format_requirement, get_venv_python_bin, set_proxy_variables, CommandOutput}; +use crate::utils::{format_requirement, get_venv_python_bin, CommandOutput}; use crate::uv::UvBuilder; -const PACKAGE_FINDER_SCRIPT: &str = r#" -import sys -import json -from unearth.finder import PackageFinder -from unearth.session import PyPISession -from packaging.version import Version - -py_ver = sys.argv[1] -package = sys.argv[2] -sources = json.loads(sys.argv[3]) -pre = len(sys.argv) > 4 and sys.argv[4] == "--pre" - -finder = PackageFinder( - index_urls=[x[0] for x in sources["index_urls"]], - find_links=sources["find_links"], - trusted_hosts=sources["trusted_hosts"], -) -if py_ver: - finder.target_python.py_ver = tuple(map(int, py_ver.split('.'))) -choices = iter(finder.find_matches(package)) -if not pre: - choices = (m for m in choices if not(m.version and Version(m.version).is_prerelease)) - -print(json.dumps([x.as_json() for x in choices])) -"#; - -#[derive(Deserialize, Debug)] -struct Match { - name: String, - version: Option, - link: Option, -} - -#[derive(Deserialize, Debug)] -struct Link { - requires_python: Option, -} - #[derive(Parser, Debug)] pub struct ReqExtras { /// Install the given package from this git repository @@ -240,8 +199,7 @@ pub struct Args { pub fn execute(cmd: Args) -> Result<(), Error> { let output = CommandOutput::from_quiet_and_verbose(cmd.quiet, cmd.verbose); - let self_venv = ensure_self_venv(output).context("error bootstrapping venv")?; - let python_path = self_venv.join(VENV_BIN).join("python"); + let _ = ensure_self_venv(output).context("error bootstrapping venv")?; let cfg = Config::current(); let mut pyproject_toml = PyProject::discover()?; @@ -272,34 +230,16 @@ pub fn execute(cmd: Args) -> Result<(), Error> { } if !cmd.excluded { - if cfg.use_uv() { - sync(SyncOptions::python_only().pyproject(None)) - .context("failed to sync ahead of add")?; - resolve_requirements_with_uv( - &pyproject_toml, - &py_ver, - &mut requirements, - cmd.pre, - output, - &default_operator, - cmd.keyring_provider, - )?; - } else { - if cmd.keyring_provider != KeyringProvider::Disabled { - bail!("`--keyring-provider` option requires the uv backend"); - } - for requirement in &mut requirements { - resolve_requirements_with_unearth( - &pyproject_toml, - &python_path, - &py_ver, - requirement, - cmd.pre, - output, - &default_operator, - )?; - } - } + sync(SyncOptions::python_only().pyproject(None)).context("failed to sync ahead of add")?; + resolve_requirements_with_uv( + &pyproject_toml, + &py_ver, + &mut requirements, + cmd.pre, + output, + &default_operator, + cmd.keyring_provider, + )?; } for requirement in &requirements { @@ -332,138 +272,6 @@ pub fn execute(cmd: Args) -> Result<(), Error> { Ok(()) } -fn resolve_requirements_with_unearth( - pyproject_toml: &PyProject, - python_path: &PathBuf, - py_ver: &PythonVersion, - requirement: &mut Requirement, - pre: bool, - output: CommandOutput, - default_operator: &Operator, -) -> Result<(), Error> { - let matches = find_best_matches_with_unearth( - pyproject_toml, - python_path, - Some(py_ver), - requirement, - pre, - )?; - if matches.is_empty() { - let all_matches = - find_best_matches_with_unearth(pyproject_toml, python_path, None, requirement, pre) - .unwrap_or_default(); - if all_matches.is_empty() { - // if we did not consider pre-releases, maybe we could find it by doing so. In - // that case give the user a helpful warning before erroring. - if !pre { - let all_pre_matches = find_best_matches_with_unearth( - pyproject_toml, - python_path, - None, - requirement, - true, - ) - .unwrap_or_default(); - if let Some(pre) = all_pre_matches.into_iter().next() { - warn!( - "{} ({}) was found considering pre-releases. Pass --pre to allow use.", - pre.name, - pre.version.unwrap_or_default() - ); - } - bail!( - "did not find package '{}' without using pre-releases.", - format_requirement(requirement) - ); - } else { - bail!("did not find package '{}'", format_requirement(requirement)); - } - } else { - if output != CommandOutput::Quiet { - echo!("Available package versions:"); - for pkg in all_matches { - echo!( - " {} ({}) requires Python {}", - pkg.name, - pkg.version.unwrap_or_default(), - pkg.link - .as_ref() - .and_then(|x| x.requires_python.as_ref()) - .map_or("unknown", |x| x as &str) - ); - } - echo!("A possible solution is to raise the version in `requires-python` in `pyproject.toml`."); - } - bail!( - "did not find a version of package '{}' compatible with this version of Python.", - format_requirement(requirement) - ); - } - } - let m = matches.into_iter().next().unwrap(); - if m.version.is_some() && requirement.version_or_url.is_none() { - let version = Version::from_str(m.version.as_ref().unwrap()) - .map_err(|msg| anyhow!("invalid version: {}", msg))?; - requirement.version_or_url = Some(VersionOrUrl::VersionSpecifier( - VersionSpecifiers::from_iter(Some( - VersionSpecifier::new( - // local versions or versions with only one component cannot - // use ~= but need to use ==. - match *default_operator { - _ if version.is_local() => Operator::Equal, - Operator::TildeEqual if version.release.len() < 2 => { - Operator::GreaterThanEqual - } - ref other => other.clone(), - }, - Version::from_str(m.version.as_ref().unwrap()) - .map_err(|msg| anyhow!("invalid version: {}", msg))?, - false, - ) - .map_err(|msg| anyhow!("invalid version specifier: {}", msg))?, - )), - )); - } - requirement.name = m.name; - Ok(()) -} - -fn find_best_matches_with_unearth( - pyproject: &PyProject, - python_path: &PathBuf, - py_ver: Option<&PythonVersion>, - requirement: &Requirement, - pre: bool, -) -> Result, Error> { - let mut unearth = Command::new(python_path); - let sources = ExpandedSources::from_sources(&pyproject.sources()?)?; - - unearth - .arg("-c") - .arg(PACKAGE_FINDER_SCRIPT) - .arg(match py_ver { - Some(ver) => ver.format_simple(), - None => "".into(), - }) - .arg(&format_requirement(requirement).to_string()) - .arg(serde_json::to_string(&sources)?); - if pre { - unearth.arg("--pre"); - } - set_proxy_variables(&mut unearth); - let unearth = unearth.stdout(Stdio::piped()).output()?; - if unearth.status.success() { - Ok(serde_json::from_slice(&unearth.stdout)?) - } else { - let log = String::from_utf8_lossy(&unearth.stderr); - bail!( - "failed to resolve package {}\n{}", - format_requirement(requirement), - log - ); - } -} - fn resolve_requirements_with_uv( pyproject_toml: &PyProject, py_ver: &PythonVersion, diff --git a/rye/src/cli/build.rs b/rye/src/cli/build.rs index 0d312fde13..3950ca7c7b 100644 --- a/rye/src/cli/build.rs +++ b/rye/src/cli/build.rs @@ -7,7 +7,6 @@ use clap::Parser; use console::style; use crate::bootstrap::{fetch, FetchOptions}; -use crate::config::Config; use crate::platform::get_toolchain_python_bin; use crate::pyproject::{locate_projects, PyProject}; @@ -65,7 +64,6 @@ pub fn execute(cmd: Args) -> Result<(), Error> { } } - let use_uv = Config::current().use_uv(); let projects = locate_projects(project, cmd.all, &cmd.package[..])?; let all_virtual = projects.iter().all(|p| p.is_virtual()); @@ -91,9 +89,6 @@ pub fn execute(cmd: Args) -> Result<(), Error> { uv_venv.write_marker()?; uv_venv.bootstrap()?; - // Respect the output level for the actual builds. - let uv = uv.with_output(output); - for project in projects { // skip over virtual packages on build if project.is_virtual() { @@ -114,14 +109,17 @@ pub fn execute(cmd: Args) -> Result<(), Error> { .arg(&out) .arg(&*project.root_path()); - if use_uv { - let uv_dir = uv - .uv_bin() - .parent() - .ok_or_else(|| anyhow!("Could not find uv binary in self venv: empty path"))?; - build_cmd.env("PATH", prepend_path_to_path_env(uv_dir)?); - build_cmd.arg("--installer=uv"); - } + // we need to ensure uv is available to use without installing it into self_venv + let uv = UvBuilder::new() + .with_output(output) + .ensure_exists()? + .with_output(output); + let uv_dir = uv + .uv_bin() + .parent() + .ok_or_else(|| anyhow!("Could not find uv binary in self venv: empty path"))?; + build_cmd.env("PATH", prepend_path_to_path_env(uv_dir)?); + build_cmd.arg("--installer=uv"); if cmd.wheel { build_cmd.arg("--wheel"); diff --git a/rye/src/cli/list.rs b/rye/src/cli/list.rs index 1620b1e289..bd7c2c46e4 100644 --- a/rye/src/cli/list.rs +++ b/rye/src/cli/list.rs @@ -1,12 +1,9 @@ use std::path::PathBuf; -use std::process::Command; -use anyhow::{bail, Error}; +use anyhow::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}; use crate::uv::{UvBuilder, UvWithVenv}; @@ -25,36 +22,21 @@ pub fn execute(cmd: Args) -> Result<(), Error> { if !python.is_file() { return Ok(()); } - let self_venv = ensure_self_venv(CommandOutput::Normal)?; + let _ = ensure_self_venv(CommandOutput::Normal)?; - if Config::current().use_uv() { - let uv = UvBuilder::new() - .with_output(CommandOutput::Normal) - .ensure_exists()?; - if !project.rye_managed() { - UvWithVenv::new(uv, &project.venv_path(), &project.venv_python_version()?).freeze()?; - } else { - uv.venv( - &project.venv_path(), - &python, - &project.venv_python_version()?, - None, - )? - .freeze()?; - } + let uv = UvBuilder::new() + .with_output(CommandOutput::Normal) + .ensure_exists()?; + if !project.rye_managed() { + UvWithVenv::new(uv, &project.venv_path(), &project.venv_python_version()?).freeze()?; } else { - 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()?; - - if !status.success() { - bail!("failed to print dependencies via pip"); - } - }; - + uv.venv( + &project.venv_path(), + &python, + &project.venv_python_version()?, + None, + )? + .freeze()?; + } Ok(()) } diff --git a/rye/src/cli/mod.rs b/rye/src/cli/mod.rs index 502000da81..3e9237c06e 100644 --- a/rye/src/cli/mod.rs +++ b/rye/src/cli/mod.rs @@ -122,6 +122,9 @@ pub fn execute() -> Result<(), Error> { unreachable!() }; + // Add this to warn about the deprecated use of pip-tools + Config::current().use_uv(); + match cmd { Command::Add(cmd) => add::execute(cmd), Command::Build(cmd) => build::execute(cmd), @@ -181,6 +184,7 @@ fn print_version() -> Result<(), Error> { ); } echo!("symlink support: {}", symlinks_supported()); + // Possibly best to leave this in case others parse the output echo!("uv enabled: {}", Config::current().use_uv()); Ok(()) } diff --git a/rye/src/cli/rye.rs b/rye/src/cli/rye.rs index b744bb0b7c..2f23064292 100644 --- a/rye/src/cli/rye.rs +++ b/rye/src/cli/rye.rs @@ -612,23 +612,6 @@ fn perform_install( return Err(QuietExit(1).into()); } - // Use uv? - if config_doc - .get("behavior") - .and_then(|x| x.get("use-uv")) - .is_none() - { - let use_uv = matches!(mode, InstallMode::NoPrompts) - || dialoguer::Select::with_theme(tui_theme()) - .with_prompt("Select the preferred package installer") - .item("uv (fast, recommended)") - .item("pip-tools (slow, higher compatibility)") - .default(0) - .interact()? - == 0; - toml::ensure_table(config_doc, "behavior")["use-uv"] = toml_edit::value(use_uv); - } - // 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/cli/toolchain.rs b/rye/src/cli/toolchain.rs index e35066ea0d..3e144a2bf7 100644 --- a/rye/src/cli/toolchain.rs +++ b/rye/src/cli/toolchain.rs @@ -13,7 +13,6 @@ use serde::Deserialize; use serde::Serialize; use crate::installer::list_installed_tools; -use crate::piptools::get_pip_tools_venv_path; use crate::platform::{get_app_dir, get_canonical_py_path, list_known_toolchains}; use crate::pyproject::read_venv_marker; use crate::sources::py::{iter_downloadable, PythonVersion}; @@ -113,12 +112,10 @@ fn register(cmd: RegisterCommand) -> Result<(), Error> { fn check_in_use(ver: &PythonVersion) -> Result<(), Error> { // Check if used by rye itself. let app_dir = get_app_dir(); - for venv in &[app_dir.join("self"), get_pip_tools_venv_path(ver)] { - let venv_marker = read_venv_marker(venv); - if let Some(ref venv_marker) = venv_marker { - if &venv_marker.python == ver { - bail!("toolchain {} is still in use by rye itself", ver); - } + let venv_marker = read_venv_marker(app_dir.join("self").as_path()); + if let Some(ref venv_marker) = venv_marker { + if &venv_marker.python == ver { + bail!("toolchain {} is still in use by rye itself", ver); } } diff --git a/rye/src/config.rs b/rye/src/config.rs index dd173268d6..0426512685 100644 --- a/rye/src/config.rs +++ b/rye/src/config.rs @@ -261,13 +261,25 @@ impl Config { .unwrap_or_else(|| self.use_uv()) } - /// Indicates if uv should be used instead of pip-tools. + /// pip-tools is now deprecated in favor uv. This function is used to warn the user about the + /// change and always return true. pub fn use_uv(&self) -> bool { - self.doc + let legacy_use_uv = self + .doc .get("behavior") .and_then(|x| x.get("use-uv")) - .and_then(|x| x.as_bool()) - .unwrap_or(true) + .and_then(|x| x.as_bool()); + + match legacy_use_uv { + Some(use_uv) => { + // warn if the user is using the deprecated behavior.use-uv key, and it's set to false + if !use_uv { + warn!("behavior.use-uv=false is deprecated, uv is always used now"); + } + true + } + None => true, + } } /// Fetches python installations with build info if possible. diff --git a/rye/src/installer.rs b/rye/src/installer.rs index 55fd8f4e51..e4519ab270 100644 --- a/rye/src/installer.rs +++ b/rye/src/installer.rs @@ -143,61 +143,26 @@ pub fn install( requirement.name.as_str(), )?; - if Config::current().use_uv() { - let result = UvBuilder::new() - .with_output(output.quieter()) - .with_sources(sources) - .ensure_exists()? - .venv(&target_venv_path, &py, &py_ver, None)? - .with_output(output) - .install( - &requirement, - UvInstallOptions { - importlib_workaround: py_ver.major == 3 && py_ver.minor == 7, - extras: extra_requirements.to_vec(), - refresh: force, - keyring_provider, - }, - ); - if result.is_err() { - uninstall_helper(&target_venv_path, &shim_dir)?; - return result; - } - } 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"); - - sources.add_as_pip_args(&mut cmd); - if output == CommandOutput::Verbose { - cmd.arg("--verbose"); - } else { - if output == CommandOutput::Quiet { - cmd.arg("-q"); - } - cmd.env("PYTHONWARNINGS", "ignore"); - } - cmd.arg("--").arg(&requirement.to_string()); - - // we don't support versions below 3.7, but for 3.7 we need importlib-metadata - // to be installed - if py_ver.major == 3 && py_ver.minor == 7 { - cmd.arg("importlib-metadata==6.6.0"); - } - - for extra in extra_requirements { - cmd.arg(extra.to_string()); - } + let result = UvBuilder::new() + .with_output(output.quieter()) + .with_sources(sources) + .ensure_exists()? + .venv(&target_venv_path, &py, &py_ver, None)? + .with_output(output) + .install( + &requirement, + UvInstallOptions { + importlib_workaround: py_ver.major == 3 && py_ver.minor == 7, + extras: extra_requirements.to_vec(), + refresh: force, + keyring_provider, + }, + ); + if result.is_err() { + uninstall_helper(&target_venv_path, &shim_dir)?; + return result; + } - let status = cmd.status()?; - if !status.success() { - uninstall_helper(&target_venv_path, &shim_dir)?; - bail!("tool installation failed"); - } - }; let out = Command::new(py) .arg("-c") .arg(FIND_SCRIPT_SCRIPT) diff --git a/rye/src/lock.rs b/rye/src/lock.rs index fc97bc9063..5348ebf524 100644 --- a/rye/src/lock.rs +++ b/rye/src/lock.rs @@ -6,7 +6,7 @@ use std::process::Command; use std::sync::Arc; use std::{env, fmt, fs}; -use anyhow::{anyhow, bail, Context, Error}; +use anyhow::{anyhow, Context, Error}; use clap::ValueEnum; use minijinja::render; use once_cell::sync::Lazy; @@ -16,13 +16,11 @@ use serde::Serialize; use tempfile::NamedTempFile; use url::Url; -use crate::config::Config; -use crate::piptools::{get_pip_compile, get_pip_tools_version, PipToolsVersion}; use crate::pyproject::{ normalize_package_name, DependencyKind, ExpandedSources, PyProject, Workspace, }; use crate::sources::py::PythonVersion; -use crate::utils::{set_proxy_variables, CommandOutput, IoPathContext}; +use crate::utils::{CommandOutput, IoPathContext}; use crate::uv::{UvBuilder, UvPackageUpgrade}; static FILE_EDITABLE_RE: Lazy = Lazy::new(|| Regex::new(r"^-e (file://.*?)\s*$").unwrap()); @@ -409,105 +407,43 @@ fn generate_lockfile( sources: &ExpandedSources, lock_options: &LockOptions, exclusions: &HashSet, - no_deps: bool, + _no_deps: bool, keyring_provider: KeyringProvider, ) -> Result<(), Error> { - let use_uv = Config::current().use_uv(); let scratch = tempfile::tempdir()?; let requirements_file = scratch.path().join("requirements.txt"); if lockfile.is_file() { fs::copy(lockfile, &requirements_file) .path_context(&requirements_file, "unable to restore requirements file")?; - } else if !use_uv { - fs::write(&requirements_file, b"").path_context( - &requirements_file, - "unable to write empty requirements file", - )?; }; - if use_uv { - let upgrade = { - if lock_options.update_all { - UvPackageUpgrade::All - } else if !lock_options.update.is_empty() { - UvPackageUpgrade::Packages(lock_options.update.clone()) - } else { - UvPackageUpgrade::Nothing - } - }; - - UvBuilder::new() - .with_output(output.quieter()) - .with_sources(sources.clone()) - .with_workdir(workspace_path) - .ensure_exists()? - .lockfile( - py_ver, - requirements_file_in, - &requirements_file, - lock_options.pre, - env::var("__RYE_UV_EXCLUDE_NEWER").ok(), - upgrade, - keyring_provider, - lock_options.generate_hashes, - lock_options.universal, - )?; - } else { - if keyring_provider != KeyringProvider::Disabled { - bail!("`--keyring-provider` option requires the uv backend"); - } - 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 { "" } - )); - if lock_options.pre { - cmd.arg("--pre"); - } - if lock_options.generate_hashes { - cmd.arg("--generate-hashes"); - cmd.arg("--reuse-hashes"); - } - - cmd.arg(if output == CommandOutput::Verbose { - "--verbose" - } else { - "-q" - }) - .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)); - - for pkg in &lock_options.update { - cmd.arg("--upgrade-package"); - cmd.arg(pkg); - } + let upgrade = { if lock_options.update_all { - cmd.arg("--upgrade"); + UvPackageUpgrade::All + } else if !lock_options.update.is_empty() { + UvPackageUpgrade::Packages(lock_options.update.clone()) + } else { + UvPackageUpgrade::Nothing } - sources.add_as_pip_args(&mut cmd); - set_proxy_variables(&mut cmd); - let status = cmd.status().context("unable to run pip-compile")?; - if !status.success() { - bail!("failed to generate lockfile"); - }; }; + UvBuilder::new() + .with_output(output.quieter()) + .with_sources(sources.clone()) + .with_workdir(workspace_path) + .ensure_exists()? + .lockfile( + py_ver, + requirements_file_in, + &requirements_file, + lock_options.pre, + env::var("__RYE_UV_EXCLUDE_NEWER").ok(), + upgrade, + keyring_provider, + lock_options.generate_hashes, + lock_options.universal, + )?; + finalize_lockfile( &requirements_file, lockfile, diff --git a/rye/src/main.rs b/rye/src/main.rs index 0a4fb23ee2..6eb2fe754a 100644 --- a/rye/src/main.rs +++ b/rye/src/main.rs @@ -12,7 +12,6 @@ mod config; mod consts; mod installer; mod lock; -mod piptools; mod platform; mod pyproject; mod sources; diff --git a/rye/src/piptools.rs b/rye/src/piptools.rs deleted file mode 100644 index 7764aba168..0000000000 --- a/rye/src/piptools.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::fs; -use std::path::PathBuf; -use std::process::Command; - -use anyhow::{bail, Context, Error}; - -use crate::bootstrap::ensure_self_venv; -use crate::consts::VENV_BIN; -use crate::platform::get_app_dir; -use crate::sources::py::PythonVersion; -use crate::sync::create_virtualenv; -use crate::utils::{get_venv_python_bin, CommandOutput, IoPathContext}; - -// When changing these, also update `SELF_VERSION` in bootstrap.rs to ensure -// that the internals are re-created. -pub const LATEST_PIP: &str = "pip==23.3.2"; -const PIP_TOOLS_LATEST_REQ: &[&str] = &[LATEST_PIP, "pip-tools==7.3.0"]; -const PIP_TOOLS_LEGACY_REQ: &[&str] = &["pip==22.2.0", "pip-tools==6.14.0"]; - -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -/// Which version of piptools are in use? -pub enum PipToolsVersion { - Latest, - Legacy, -} - -impl PipToolsVersion { - fn requirements(&self) -> &'static [&'static str] { - match *self { - PipToolsVersion::Latest => PIP_TOOLS_LATEST_REQ, - PipToolsVersion::Legacy => PIP_TOOLS_LEGACY_REQ, - } - } -} - -fn get_pip_tools_bin(py_ver: &PythonVersion, output: CommandOutput) -> Result { - let self_venv = ensure_self_venv(output)?; - let venv = get_pip_tools_venv_path(py_ver); - - let py = get_venv_python_bin(&venv); - let version = get_pip_tools_version(py_ver); - - // if we have a python interpreter in the given path, let's use it - if venv.join(&py).is_file() { - return Ok(venv); - } - - // if however for some reason the virtualenv itself is already a folder - // it usually means that the symlink to the python is bad now. This can - // happen if someone wiped the toolchain of the pip-tools version. In - // that case wipe it first. - if venv.is_dir() { - fs::remove_dir_all(&venv) - .path_context(&venv, "unable to wipe old virtualenv for pip-tools")?; - } - - echo!(if output, "Creating virtualenv for pip-tools"); - create_virtualenv(output, &self_venv, py_ver, &venv, "pip-tools")?; - - let mut cmd = Command::new(self_venv.join(VENV_BIN).join("pip")); - cmd.arg("--python") - .arg(&py) - .arg("install") - .arg("--upgrade") - .args(version.requirements()) - .arg("-q") - .env("PIP_DISABLE_PIP_VERSION_CHECK", "1"); - if output == CommandOutput::Verbose { - cmd.arg("--verbose"); - } else { - cmd.arg("--quiet"); - cmd.env("PYTHONWARNINGS", "ignore"); - } - let status = cmd.status().context("unable to install pip-tools")?; - if !status.success() { - bail!("failed to initialize pip-tools venv (install dependencies)"); - } - Ok(venv) -} - -pub fn get_pip_tools_version(py_ver: &PythonVersion) -> PipToolsVersion { - if py_ver.major == 3 && py_ver.minor == 7 { - PipToolsVersion::Legacy - } else { - PipToolsVersion::Latest - } -} - -pub fn get_pip_tools_venv_path(py_ver: &PythonVersion) -> PathBuf { - let key = format!("{}@{}.{}", py_ver.name, py_ver.major, py_ver.minor); - get_app_dir().join("pip-tools").join(key) -} - -pub fn get_pip_sync(py_ver: &PythonVersion, output: CommandOutput) -> Result { - Ok(get_pip_tools_bin(py_ver, output)? - .join(VENV_BIN) - .join("pip-sync")) -} - -pub fn get_pip_compile(py_ver: &PythonVersion, output: CommandOutput) -> Result { - Ok(get_pip_tools_bin(py_ver, output)? - .join(VENV_BIN) - .join("pip-compile")) -} diff --git a/rye/src/sync.rs b/rye/src/sync.rs index 2f4ef59d01..d888ac2071 100644 --- a/rye/src/sync.rs +++ b/rye/src/sync.rs @@ -1,28 +1,20 @@ +use std::fs; use std::path::{Path, PathBuf}; -use std::process::Command; -use std::{env, fs}; use anyhow::{bail, Context, Error}; use console::style; use same_file::is_same_file; use serde::{Deserialize, Serialize}; -use tempfile::tempdir; -use crate::bootstrap::{ensure_self_venv, fetch, get_pip_module, FetchOptions}; -use crate::config::Config; -use crate::consts::VENV_BIN; +use crate::bootstrap::{ensure_self_venv, fetch, FetchOptions}; use crate::lock::{ - make_project_root_fragment, update_single_project_lockfile, update_workspace_lockfile, - KeyringProvider, LockMode, LockOptions, + update_single_project_lockfile, update_workspace_lockfile, KeyringProvider, LockMode, + LockOptions, }; -use crate::piptools::{get_pip_sync, get_pip_tools_venv_path}; use crate::platform::get_toolchain_python_bin; -use crate::pyproject::{read_venv_marker, write_venv_marker, ExpandedSources, PyProject}; +use crate::pyproject::{read_venv_marker, ExpandedSources, PyProject}; use crate::sources::py::PythonVersion; -use crate::utils::{ - get_venv_python_bin, set_proxy_variables, symlink_dir, update_venv_sync_marker, CommandOutput, - IoPathContext, -}; +use crate::utils::{get_venv_python_bin, CommandOutput, IoPathContext}; use crate::uv::{UvBuilder, UvSyncOptions}; /// Controls the sync mode @@ -259,65 +251,18 @@ pub fn sync(mut cmd: SyncOptions) -> Result<(), Error> { lockfile }; - let tempdir = tempdir()?; let py_path = get_venv_python_bin(&venv); - if Config::current().use_uv() { - let uv_options = UvSyncOptions { - keyring_provider: cmd.keyring_provider, - }; - UvBuilder::new() - .with_output(output.quieter()) - .with_workdir(&pyproject.workspace_path()) - .with_sources(sources) - .ensure_exists()? - .venv(&venv, &py_path, &py_ver, None)? - .with_output(output) - .sync(&target_lockfile, uv_options)?; - } else { - let mut pip_sync_cmd = Command::new(get_pip_sync(&py_ver, output)?); - let root = pyproject.workspace_path(); - - // 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"); - } - - sources.add_as_pip_args(&mut pip_sync_cmd); - - pip_sync_cmd.arg(&target_lockfile); - - if output == CommandOutput::Verbose { - pip_sync_cmd.arg("--verbose"); - } else if output == CommandOutput::Quiet { - pip_sync_cmd.arg("-q"); - } - set_proxy_variables(&mut pip_sync_cmd); - let status = pip_sync_cmd.status().context("unable to run pip-sync")?; - - if !status.success() { - bail!("Installation of dependencies failed"); - } + let uv_options = UvSyncOptions { + keyring_provider: cmd.keyring_provider, }; + UvBuilder::new() + .with_output(output.quieter()) + .with_workdir(&pyproject.workspace_path()) + .with_sources(sources) + .ensure_exists()? + .venv(&venv, &py_path, &py_ver, None)? + .with_output(output) + .sync(&target_lockfile, uv_options)?; }; } @@ -356,50 +301,22 @@ pub fn autosync( pub fn create_virtualenv( output: CommandOutput, - self_venv: &Path, + _self_venv: &Path, py_ver: &PythonVersion, venv: &Path, prompt: &str, ) -> Result<(), Error> { let py_bin = get_toolchain_python_bin(py_ver)?; - if Config::current().use_uv() { - // try to kill the empty venv if there is one as uv can't work otherwise. - fs::remove_dir(venv).ok(); - let uv = UvBuilder::new() - .with_output(output.quieter()) - .ensure_exists()? - .venv(venv, &py_bin, py_ver, Some(prompt)) - .context("failed to initialize virtualenv")?; - uv.write_marker()?; - uv.sync_marker(); - } else { - // create the venv folder first so we can manipulate some flags on it. - fs::create_dir_all(venv).path_context(venv, "unable to create virtualenv folder")?; - - 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.arg("--").arg(venv); - let status = venv_cmd - .status() - .context("unable to invoke virtualenv command")?; - if !status.success() { - bail!("failed to initialize virtualenv"); - } - - write_venv_marker(venv, py_ver)?; - }; + // try to kill the empty venv if there is one as uv can't work otherwise. + fs::remove_dir(venv).ok(); + let uv = UvBuilder::new() + .with_output(output.quieter()) + .ensure_exists()? + .venv(venv, &py_bin, py_ver, Some(prompt)) + .context("failed to initialize virtualenv")?; + uv.write_marker()?; + uv.sync_marker(); // 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 diff --git a/rye/src/utils/mod.rs b/rye/src/utils/mod.rs index fc431a98c7..9c567212d8 100644 --- a/rye/src/utils/mod.rs +++ b/rye/src/utils/mod.rs @@ -16,7 +16,7 @@ use sha2::{Digest, Sha256}; static ENV_VAR_RE: Lazy = Lazy::new(|| Regex::new(r"\$\{([A-Z0-9_]+)\}").unwrap()); #[cfg(unix)] -pub use std::os::unix::fs::{symlink as symlink_file, symlink as symlink_dir}; +pub use std::os::unix::fs::symlink as symlink_file; #[cfg(windows)] pub use std::os::windows::fs::symlink_file; diff --git a/rye/src/uv.rs b/rye/src/uv.rs index 4523c0dea1..d20d0370af 100644 --- a/rye/src/uv.rs +++ b/rye/src/uv.rs @@ -1,6 +1,5 @@ use crate::bootstrap::{download_url, SELF_REQUIREMENTS}; use crate::lock::{make_project_root_fragment, KeyringProvider}; -use crate::piptools::LATEST_PIP; use crate::platform::get_app_dir; use crate::pyproject::{read_venv_marker, write_venv_marker, ExpandedSources}; use crate::sources::py::PythonVersion; @@ -446,34 +445,9 @@ impl UvWithVenv { } } - /// Updates the venv to the given pip version and requirements. - pub fn update(&self, pip_version: &str, requirements: &str) -> Result<(), Error> { - self.update_pip(pip_version)?; - self.update_requirements(requirements)?; - Ok(()) - } - /// Install the bootstrap requirements in the venv. pub fn bootstrap(&self) -> Result<(), Error> { - self.update(LATEST_PIP, SELF_REQUIREMENTS)?; - Ok(()) - } - - /// Updates the pip version in the venv. - pub fn update_pip(&self, pip_version: &str) -> Result<(), Error> { - self.venv_cmd() - .arg("pip") - .arg("install") - .arg("--upgrade") - .arg(pip_version) - .status() - .with_context(|| { - format!( - "unable to update pip in venv at {}", - self.venv_path.display() - ) - })?; - + self.update_requirements(SELF_REQUIREMENTS)?; Ok(()) }