diff --git a/docs/guide/config.md b/docs/guide/config.md index bda7499c7c..f21d8578a4 100644 --- a/docs/guide/config.md +++ b/docs/guide/config.md @@ -81,11 +81,6 @@ force-rye-managed = false # virtual environments. 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. -# Learn more about uv here: https://github.com/astral-sh/uv -use-uv = true - # Enable or disable automatic `sync` after `add` and `remove`. This defaults # to `true` when uv is enabled and `false` otherwise. autosync = true diff --git a/docs/guide/docker.md b/docs/guide/docker.md index 56e7dd5ea3..84fa595a63 100644 --- a/docs/guide/docker.md +++ b/docs/guide/docker.md @@ -14,11 +14,26 @@ 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 + RUN pip install uv + + WORKDIR /app + COPY requirements.lock ./ + uv pip install --no-cache -r requirements.lock + + COPY src . + CMD python main.py + ``` + + 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 +42,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 +71,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 uv pip install --no-cache /dist/*.whl CMD python -m my_package ``` @@ -78,8 +94,12 @@ 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 lockfile 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 `--no-cache-dir` and `--no-cache` parameters, passed to `pip` and `uv` respectively, make the image smaller by not +writing any temporary files. While caching can speed up subsequent builds, it's not necessary in a container where the +image is built once and then used many times. + +Similarly, the `PYTHONDONTWRITEBYTECODE=1` environment variable is set to avoid writing `.pyc` files, which are not +needed in a container. (`uv` skips writing `.pyc` files by default.) 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..4e2746e8cc 100644 --- a/docs/guide/sync.md +++ b/docs/guide/sync.md @@ -1,13 +1,9 @@ # 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 uses [`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 +`requirements.lock` and `requirements-dev.lock`). These are not real lockfiles, but they fulfill a similar purpose until a better solution has been implemented. Whenever `rye sync` is called, it will update lockfiles as well as the @@ -66,12 +62,12 @@ rye lock Flask --pre +++ 0.18.0 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 +`pyproject.toml`) lockfiles are not generated with source references. This means that +if custom sources are used the lockfile 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. +when the lockfile is used for Docker image builds. -When this flag is passed then the lock file is generated with references to `--index-url`, +When this flag is passed then the lockfile is generated with references to `--index-url`, `--extra-index-url` or `--find-links`. ``` @@ -100,11 +96,18 @@ lockfile (`requirements-dev.lock`). rye sync --no-dev ``` -## Limitations +## Platform Compatibility -Lockfiles depend on the platform they were generated on. This is a known limitation -in pip-tools. +By default, lockfiles depend on the platform they were generated on. For example, if your project relies on platform-specific packages and you generate lockfiles on Windows, these lockfiles will include Windows-specific projects. Consequently, they won't be compatible with other platforms like Linux or macOS. + +To generate a cross-platform lockfile, you can enable uv's `universal` setting +by adding the following to your `pyproject.toml`: + +```toml +[tool.rye] +universal = true +``` diff --git a/docs/philosophy.md b/docs/philosophy.md index 44481d300e..6022030bd7 100644 --- a/docs/philosophy.md +++ b/docs/philosophy.md @@ -11,14 +11,13 @@ on my mind when I built it: of dependencies. Not even `pip` or `setuptools` are installed into it. Rye manages the virtualenv from outside the virtualenv. -- **No Core Non Standard Stuff:** Rye (with the exception of it's own `tool` section +- **No Core Non-Standard Stuff:** Rye (with the exception of its 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 - `pyproject.toml` only. +- **No Pip:** Rye uses [`uv`](https://github.com/astral-sh/uv) to manage dependencies, + through `pyproject.toml` only. - **No System Python:** I can't deal with any more linux distribution weird Python installations or whatever mess there is on macOS. I used to build my own Pythons @@ -53,10 +52,8 @@ 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 Exposed Pip:** pip is intentionally not exposed. If you install something + into the virtualenv with pip, it disappears next time you sync. - **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 diff --git a/rye/src/bootstrap.rs b/rye/src/bootstrap.rs index 18b1ffb9c5..f84fcf5b4e 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 fc538b5b91..e181c88342 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 @@ -155,7 +114,7 @@ impl ReqExtras { }; } else if let Some(ref path) = self.path { // For hatchling build backend, it use {root:uri} for file relative path, - // but this not supported by pip-tools, + // but this not supported by uv, // and use ${PROJECT_ROOT} will cause error in hatchling, so force absolute path. let is_hatchling = PyProject::discover()?.build_backend() == Some(BuildSystem::Hatchling); @@ -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"); + 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..ee5ece42b4 100644 --- a/rye/src/cli/mod.rs +++ b/rye/src/cli/mod.rs @@ -122,6 +122,13 @@ pub fn execute() -> Result<(), Error> { unreachable!() }; + // Add this to warn about the deprecated use of pip-tools + if !Config::current().use_uv() { + warn!( + "The `use-uv` setting is deprecated, as `pip-tools` support was removed in rye 0.40.0" + ); + } + match cmd { Command::Add(cmd) => add::execute(cmd), Command::Build(cmd) => build::execute(cmd), @@ -181,6 +188,6 @@ fn print_version() -> Result<(), Error> { ); } echo!("symlink support: {}", symlinks_supported()); - echo!("uv enabled: {}", Config::current().use_uv()); + echo!("uv enabled: {}", true); Ok(()) } diff --git a/rye/src/cli/rye.rs b/rye/src/cli/rye.rs index e1891297d7..87299b89a1 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..de0073a086 100644 --- a/rye/src/config.rs +++ b/rye/src/config.rs @@ -258,10 +258,12 @@ impl Config { .get("behavior") .and_then(|x| x.get("autosync")) .and_then(|x| x.as_bool()) - .unwrap_or_else(|| self.use_uv()) + .unwrap_or(true) } - /// Indicates if uv should be used instead of pip-tools. + /// Indicates if uv should be used. + /// + /// This setting is deprecated, as pip-tools support was removed in Rye 0.40. pub fn use_uv(&self) -> bool { self.doc .get("behavior") @@ -409,12 +411,4 @@ author = "John Doe ""#, .iter() .any(|src| src.name == "default" && src.url == "https://pypi.org/simple/")); } - - #[test] - fn test_use_uv() { - let (cfg_path, _temp_dir) = setup_config("[behavior]\nuse-uv = true"); - let cfg = Config::from_path(&cfg_path).expect("Failed to load config"); - // Assuming cfg!(windows) is false in this test environment - assert!(cfg.use_uv()); - } } diff --git a/rye/src/installer.rs b/rye/src/installer.rs index d34fa9d60c..51b66e8220 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 23e2a70da9..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==24.2.0"; -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(()) }