+> [!NOTE]
+>
+> If you're getting started with Rye, consider [uv](https://github.com/astral-sh/uv), the
+> [successor project](https://lucumr.pocoo.org/2024/2/15/rye-grows-with-uv/) from the same maintainers.
+>
+> While Rye is actively maintained, uv offers a more stable and feature-complete experience, and is the recommended
+> choice for new projects.
+>
+> Having trouble migrating? [Let us know what's missing.](https://github.com/astral-sh/rye/discussions/1342)
+
Rye is a comprehensive project and package management solution for Python.
Born from [its creator's](https://github.com/mitsuhiko) desire to establish a
one-stop-shop for all Python users, Rye provides a unified experience to install and manage Python
@@ -47,21 +57,21 @@ The installation takes just a minute:
* **Linux and macOS:**
```
- curl -sSf https://rye-up.com/get | bash
+ curl -sSf https://rye.astral.sh/get | bash
```
* **Windows:**
- Download and run the installer ([64bit Intel](https://github.com/astral-sh/rye/releases/latest/download/rye-x86_64-windows.exe) or [32bit Intel](https://github.com/astral-sh/rye/releases/latest/download/rye-x86-windows.exe)).
+ Download and run the installer ([64-bit (x86-64)](https://github.com/astral-sh/rye/releases/latest/download/rye-x86_64-windows.exe) or [32-bit (x86)](https://github.com/astral-sh/rye/releases/latest/download/rye-x86-windows.exe)).
-For more details and other options, refer to the [installation instructions](https://rye-up.com/guide/installation/).
+For more details and other options, refer to the [installation instructions](https://rye.astral.sh/guide/installation/).
## Learn More
Did I spark your interest?
-* [Visit the Website](https://rye-up.com/)
-* [Read the Documentation](https://rye-up.com/guide/)
+* [Visit the Website](https://rye.astral.sh/)
+* [Read the Documentation](https://rye.astral.sh/guide/)
* [Report Problems in the Issue Tracker](https://github.com/astral-sh/rye/issues)
## More
@@ -70,5 +80,5 @@ Did I spark your interest?
on GitHub
* [Discord](https://discord.gg/drbkcdtSbg), for conversations with other developers in text form
* [Issue Tracker](https://github.com/astral-sh/rye/issues), if you run into bugs or have suggestions
-* [Badges](https://rye-up.com/community/#badges), if you want to show that you use Rye
+* [Badges](https://rye.astral.sh/community/#badges), if you want to show that you use Rye
* License: MIT
diff --git a/docs/.includes/curl-to-bash-options.md b/docs/.includes/curl-to-bash-options.md
index 9fec62be05..9c67a60039 100644
--- a/docs/.includes/curl-to-bash-options.md
+++ b/docs/.includes/curl-to-bash-options.md
@@ -14,5 +14,5 @@ variables:
This for instance installs a specific version of Rye without asking questions:
```bash
-curl -sSf https://rye-up.com/get | RYE_VERSION="0.4.0" RYE_INSTALL_OPTION="--yes" bash
+curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.4.0" RYE_INSTALL_OPTION="--yes" bash
```
\ No newline at end of file
diff --git a/docs/.includes/quick-install.md b/docs/.includes/quick-install.md
index 1ae5b317b0..383c9116b6 100644
--- a/docs/.includes/quick-install.md
+++ b/docs/.includes/quick-install.md
@@ -4,14 +4,14 @@
operating system and CPU architecture and install it:
```bash
- curl -sSf https://rye-up.com/get | bash
+ curl -sSf https://rye.astral.sh/get | bash
```
Alternatively if you don't trust this approach, you can download the latest release
binary. On first run it will install itself.
- * [rye-x86_64-linux.gz](https://github.com/astral-sh/rye/releases/latest/download/rye-x86_64-linux.gz) for 64bit Intel computers
- * [rye-aarch64-linux.gz](https://github.com/astral-sh/rye/releases/latest/download/rye-aarch64-linux.gz) for 64bit ARM computers
+ * [rye-x86_64-linux.gz](https://github.com/astral-sh/rye/releases/latest/download/rye-x86_64-linux.gz) Intel/AMD (x86-64).
+ * [rye-aarch64-linux.gz](https://github.com/astral-sh/rye/releases/latest/download/rye-aarch64-linux.gz) for ARM64.
```bash
gunzip rye-x86_64-linux.gz
@@ -25,14 +25,14 @@
operating system and CPU architecture and install it:
```bash
- curl -sSf https://rye-up.com/get | bash
+ curl -sSf https://rye.astral.sh/get | bash
```
Alternatively if you don't trust this approach, you can download the latest release
binary. On first run it will install itself.
- * [rye-aarch64-macos.gz](https://github.com/astral-sh/rye/releases/latest/download/rye-aarch64-macos.gz) for M1/M2 Macs
- * [rye-x86_64-macos.gz](https://github.com/astral-sh/rye/releases/latest/download/rye-x86_64-macos.gz) for Intel Macs
+ * [rye-aarch64-macos.gz](https://github.com/astral-sh/rye/releases/latest/download/rye-aarch64-macos.gz) for Apple Silicon (M1/M2/M3) (ARM64).
+ * [rye-x86_64-macos.gz](https://github.com/astral-sh/rye/releases/latest/download/rye-x86_64-macos.gz) for Intel processors (x86-64).
```bash
gunzip rye-aarch64-macos.gz
@@ -47,8 +47,8 @@
to have "Developer Mode" activated when using Rye and before starting the
installation. [Learn more](../guide/faq.md).
- * [rye-x86_64-windows.exe](https://github.com/astral-sh/rye/releases/latest/download/rye-x86_64-windows.exe) for 64bit Intel Windows
- * [rye-x86-windows.exe](https://github.com/astral-sh/rye/releases/latest/download/rye-x86-windows.exe) for 32bit Intel Windows
+ * [rye-x86_64-windows.exe](https://github.com/astral-sh/rye/releases/latest/download/rye-x86_64-windows.exe) for 64-bit (x86-64).
+ * [rye-x86-windows.exe](https://github.com/astral-sh/rye/releases/latest/download/rye-x86-windows.exe) for 32-bit (x86).
!!!Note
diff --git a/docs/CNAME b/docs/CNAME
index ced6063925..0c6d7c3fb4 100644
--- a/docs/CNAME
+++ b/docs/CNAME
@@ -1 +1 @@
-rye-up.com
+rye.astral.sh
diff --git a/docs/community.md b/docs/community.md
index b82ef2bc8a..ec394f8ce0 100644
--- a/docs/community.md
+++ b/docs/community.md
@@ -22,21 +22,21 @@ You can also reach out [via Twitter](https://twitter.com/mitsuhiko) or
Want to show that you are using Rye? Why not throw a badge into your project's `README.md`:
```md
-[![Rye](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/rye/main/artwork/badge.json)](https://rye-up.com)
+[![Rye](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/rye/main/artwork/badge.json)](https://rye.astral.sh)
```
... or `README.rst`:
```rst
.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/rye/main/artwork/badge.json
- :target: https://rye-up.com
+ :target: https://rye.astral.sh
:alt: Rye
```
... or, as HTML:
```html
-
+
```
-[![Rye](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/rye/main/artwork/badge.json)](https://rye-up.com)
+[![Rye](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/rye/main/artwork/badge.json)](https://rye.astral.sh)
diff --git a/docs/guide/basics.md b/docs/guide/basics.md
index b807af57d9..bd8eacc4b8 100644
--- a/docs/guide/basics.md
+++ b/docs/guide/basics.md
@@ -74,8 +74,8 @@ Use the `add` command to add dependencies to your project.
rye add "flask>=2.0"
```
-Note that after `add` you need to run `sync` again to actually install it. If you
-want to add packages from custom indexes, you have to [configure the source](sources.md)
+Followed by `rye sync` to install the dependency into the virtual environment.
+If you want to add packages from custom indexes, you have to [configure the source](sources.md)
first.
## Listing Dependencies
@@ -97,13 +97,14 @@ rye remove flask
## Working with the Project
-To run executables in the context of the virtualenv you can use the `run` command. For
-instance if you want to use `black` you can add and run it like this:
+To run executables in the context of the virtualenv you can use the `run` command. For
+instance, to use `black`, add it to the project, sync the virtual environment, and run it
+over the current directory like this:
```
rye add black
rye sync
-rye run black
+rye run black .
```
If you want to have the commands available directly you will need to activate the
@@ -163,11 +164,11 @@ The following structure will be created:
```
The [`pyproject.toml`](pyproject.md) will be generated with a
-[`[project.scripts]`](pyproject.md#projectscripts) section named `hello`
-that points to the `main()` function of `__init__.py`. After you
-synchronized your changes, you can run the script with `rye run hello`.
+[`[project.scripts]`](pyproject.md#projectscripts) section containing a
+`my-project` script that points to the `main()` function of `__init__.py`. After
+you synchronized your changes, you can run the script with `rye run my-project`.
```shell
rye sync
-rye run hello
+rye run my-project
```
diff --git a/docs/guide/commands/add.md b/docs/guide/commands/add.md
index 816cd154a3..6875900c8b 100644
--- a/docs/guide/commands/add.md
+++ b/docs/guide/commands/add.md
@@ -2,7 +2,7 @@
Adds a Python package to this project. The command takes a PEP 508 requirement string
but provides additional helper arguments to make this process more user friendly. For
-instance instead of passing git references within the requiement string, the `--git`
+instance instead of passing git references within the requirement string, the `--git`
parameter can be used.
If auto sync is disabled, after a dependency is added it's not automatically
@@ -73,7 +73,7 @@ Added packagename @ file:///path/to/packagename as regular dependency
* `--pre`: Include pre-releases when finding a package version
-* `--pin `: Overrides the pin operator [possible values: `equal`, `tilde-equal``, `greater-than-equal``]
+* `--pin `: Overrides the pin operator [possible values: `equal`, `tilde-equal`, `greater-than-equal`]
* `--sync`: Runs `sync` automatically even if auto-sync is disabled.
@@ -83,4 +83,4 @@ Added packagename @ file:///path/to/packagename as regular dependency
* `-q, --quiet`: Turns off all output
-* `-h, --help`: Print help (see a summary with '-h')
\ No newline at end of file
+* `-h, --help`: Print help (see a summary with `-h`)
diff --git a/docs/guide/commands/build.md b/docs/guide/commands/build.md
index a8dff8a1d9..46d7498031 100644
--- a/docs/guide/commands/build.md
+++ b/docs/guide/commands/build.md
@@ -46,7 +46,7 @@ By default you will find the artifacts in the `dist` folder.
* `-o, --out `: An output directory (defaults to `workspace/dist`)
-* `--pyproject `: Use this `pyproject.toml`` file
+* `--pyproject `: Use this `pyproject.toml` file
* `-c, --clean`: Clean the output directory first
@@ -54,4 +54,4 @@ By default you will find the artifacts in the `dist` folder.
* `-q, --quiet`: Turns off all output
-* `-h, --help`: Print help
\ No newline at end of file
+* `-h, --help`: Print help
diff --git a/docs/guide/commands/fetch.md b/docs/guide/commands/fetch.md
index d85be0980d..66ee918f8a 100644
--- a/docs/guide/commands/fetch.md
+++ b/docs/guide/commands/fetch.md
@@ -3,6 +3,10 @@
Fetches a Python interpreter for the local machine. This command is
available under the aliases `rye fetch` and `rye toolchain fetch`.
+As of Rye 0.31.0 toolchains are always fetched without build info. This
+means that in the folder where toolchains are stored only the interpreter
+is found. For more information see [Fetching Toolchains](../toolchains/index.md#build-info).
+
## Example
Fetch a specific version of Python:
@@ -25,6 +29,13 @@ Unpacking
Downloaded cpython@3.8.17
```
+To fetch a version of Python into a specific location rather than rye's
+interpreter cache:
+
+```
+$ rye fetch cpython@3.9.1 --target-path=my-interpreter
+```
+
## Arguments
* `[VERSION]`: The version of Python to fetch.
@@ -35,6 +46,12 @@ Downloaded cpython@3.8.17
* `-f, --force`: Fetch the Python toolchain even if it is already installed.
+* `--target-path` ``: Fetches the Python toolchain into an explicit location rather
+
+* `--build-info`: Fetches with build info
+
+* `--no-build-info`: Fetches without build info
+
* `-v, --verbose`: Enables verbose diagnostics
* `-q, --quiet`: Turns off all output
diff --git a/docs/guide/commands/index.md b/docs/guide/commands/index.md
index 3fedf90ee8..07c99679df 100644
--- a/docs/guide/commands/index.md
+++ b/docs/guide/commands/index.md
@@ -18,8 +18,17 @@ This is a list of all the commands that rye provides:
* [run](run.md): Runs a command installed into this package
* [show](show.md): Prints the current state of the project
* [sync](sync.md): Updates the virtualenv based on the pyproject.toml
+* [test](test.md): Runs the project's tests
* [toolchain](toolchain/index.md): Helper utility to manage Python toolchains
* [tools](tools/index.md): Helper utility to manage global tools.
* [self](self/index.md): Rye self management
* [uninstall](uninstall.md): Uninstalls a global tool (alias)
-* [version](version.md): Get or set project version
\ No newline at end of file
+* [version](version.md): Get or set project version
+
+## Options
+
+The toplevel `rye` command accepts the following options:
+
+* `--env-file` ``: This can be supplied multiple times to make rye load
+ a given `.env` file. Note that this file is not referenced to handle the
+ `RYE_HOME` variable which must be supplied as environment variable always.
\ No newline at end of file
diff --git a/docs/guide/commands/lock.md b/docs/guide/commands/lock.md
index 374e0744b6..079f69363d 100644
--- a/docs/guide/commands/lock.md
+++ b/docs/guide/commands/lock.md
@@ -3,7 +3,7 @@
Updates the lockfiles without installing dependencies. Usually one would use
the [`sync`](sync.md) command instead which both locks and installs dependencies.
-For more information see [Synching and Locking](../sync.md).
+For more information see [Syncing and Locking](../sync.md).
## Example
@@ -30,6 +30,8 @@ Done!
* `--all-features`: Enables all features
+* `--generate-hashes`: Set to true to lock with hashes in the lockfile
+
* `--with-sources`: Set to true to lock with sources in the lockfile
* `--pyproject `: Use this pyproject.toml file
diff --git a/docs/guide/commands/publish.md b/docs/guide/commands/publish.md
index 0baab43003..7baeb6bf11 100644
--- a/docs/guide/commands/publish.md
+++ b/docs/guide/commands/publish.md
@@ -40,6 +40,8 @@ $ rye publish dist/example-0.1.0.tar.gz
* `--cert `: Path to alternate CA bundle
+* `--skip-existing`: Skip files already published (repository must support this feature)
+
* `-y, --yes`: Skip prompts
* `-v, --verbose`: Enables verbose diagnostics
diff --git a/docs/guide/commands/self/completion.md b/docs/guide/commands/self/completion.md
index 5631ae0e5e..2b3c56cf42 100644
--- a/docs/guide/commands/self/completion.md
+++ b/docs/guide/commands/self/completion.md
@@ -7,7 +7,7 @@ Generates a completion script for a shell
Generate a completion script for zsh and load it:
```
-$ eval (rye self completion -s zsh)
+$ eval "$(rye self completion -s zsh)"
```
## Arguments
@@ -18,6 +18,6 @@ _no arguments_
* `-s, --shell `: The shell to generate a completion script for (defaults to 'bash')
- [possible values: `bash`, `elvish`, `fish`, `powershell`, `zsh`]
+ [possible values: `bash`, `elvish`, `fish`, `powershell`, `zsh`, `nushell`]
* `-h, --help`: Print help (see a summary with '-h')
diff --git a/docs/guide/commands/self/update.md b/docs/guide/commands/self/update.md
index 629c4ca352..9eb16bfd80 100644
--- a/docs/guide/commands/self/update.md
+++ b/docs/guide/commands/self/update.md
@@ -25,6 +25,12 @@ Compile a specific revision:
$ rye self update --rev 08910bc9b3b7c72a3d3ac694c4f3412259161477
```
+Compile latest development version:
+
+```
+$ rye self update --branch main
+```
+
## Arguments
_no arguments_
@@ -37,6 +43,8 @@ _no arguments_
* `--rev `: Update to a specific git rev
+* `--branch `: Update to a specific git branch
+
* `--force`: Force reinstallation
* `-h, --help`: Print help (see a summary with '-h')
diff --git a/docs/guide/commands/sync.md b/docs/guide/commands/sync.md
index f1306264cb..df85869325 100644
--- a/docs/guide/commands/sync.md
+++ b/docs/guide/commands/sync.md
@@ -2,7 +2,7 @@
Updates the lockfiles and installs the dependencies into the virtualenv.
-For more information see [Synching and Locking](../sync.md).
+For more information see [Syncing and Locking](../sync.md).
## Example
@@ -53,6 +53,8 @@ To exit the sub shell run `exit`.
* `--all-features`: Enables all features
+* `--generate-hashes`: Set to true to lock with hashes in the lockfile
+
* `--with-sources`: Set to true to lock with sources in the lockfile
* `--pyproject `: Use this pyproject.toml file
diff --git a/docs/guide/commands/test.md b/docs/guide/commands/test.md
new file mode 100644
index 0000000000..8a4d6671f2
--- /dev/null
+++ b/docs/guide/commands/test.md
@@ -0,0 +1,58 @@
+# `test`
+
++++ 0.28.0
+
+Run the test suites of the project. At the moment this always runs `pytest`.
+Note that `pytest` must be installed into the virtual env unlike `ruff`
+which is used behind the scenes automatically for linting and formatting.
+Thus in order to use this, you need to declare `pytest` as dev dependency.
+
+```
+$ rye add --dev pytest
+```
+
+It's recommended to place tests in a folder called `tests` adjacent to the
+`src` folder of your project.
+
+For more information about how to use pytest, have a look at the
+[Pytest Documentation](https://docs.pytest.org/en/8.0.x/).
+
+## Example
+
+Run the test suite:
+
+```
+$ rye test
+platform win32 -- Python 3.11.1, pytest-8.0.2, pluggy-1.4.0
+rootdir: /Users/john/Development/stuff
+plugins: anyio-4.3.0
+collected 1 item
+
+stuff/tests/test_batch.py . [100%]
+```
+
+## Arguments
+
+* `[EXTRA_ARGS]...` Extra arguments to the test runner.
+
+ These arguments are forwarded directly to the underlying test runner (currently
+ always `pytest`). Note that extra arguments must be separated from other arguments
+ with the `--` marker.
+
+## Options
+
+* `-a, --all`: Test all packages in the workspace
+
+* `-p, --package `: Run the test suite of a specific package
+
+* `--pyproject `: Use this `pyproject.toml` file
+
+* `-v, --verbose`: Enables verbose diagnostics
+
+* `-q, --quiet`: Turns off all output
+
+* `-i, --ignore`: Ignore the specified directory
+
+* `-s`, `--no-capture`: Disable stdout/stderr capture for the test runner
+
+* `-h, --help`: Print help (see a summary with '-h')
diff --git a/docs/guide/config.md b/docs/guide/config.md
index 93ad8aae0b..f21d8578a4 100644
--- a/docs/guide/config.md
+++ b/docs/guide/config.md
@@ -5,16 +5,16 @@ also a bit of global configuration to influence how it works.
## Changing Home Folder
-By default Rye places all it's configuration in `~/.rye` on Unix and `%USERPROFILE%\.rye` on
+By default Rye places all its configuration in `~/.rye` on Unix and `%USERPROFILE%\.rye` on
Windows. This behavior can be changed via the `RYE_HOME` environment variable. This is useful
-if you do not like the default location of where Rye places it's configuration or if you need
+if you do not like the default location where Rye places its configuration or if you need
to isolate it.
## Home Folder Structure
-The `.rye` home folder contains both user configuration as well as Rye managed state such
+The `.rye` home folder contains both user configuration as well as Rye-managed state such
as [installed toolchains](toolchains/index.md). The following files and folders are placed within the
-`.rye` folder. Note that not all are there always.
+`.rye` folder. Note that not all are always there.
### `config.toml`
@@ -39,7 +39,7 @@ which automatically proxies to the current virtualenv or globally installed [too
## Config File
-The config file `config.toml` in the `.rye` folder today only is used to manage defaults. This
+The config file `config.toml` in the `.rye` folder today is only used to manage defaults. This
is a fully annotated config file:
```toml
@@ -73,32 +73,33 @@ http = "http://127.0.0.1:4000"
https = "http://127.0.0.1:4000"
[behavior]
-# When set to true the `managed` flag is always assumed to be true.
+# When set to `true` the `managed` flag is always assumed to be `true`.
force-rye-managed = false
# Enables global shims when set to `true`. This means that the installed
-# `python` shim will resolve to a Rye managed toolchain even outside of
+# `python` shim will resolve to a Rye-managed toolchain even outside of
# virtual environments.
global-python = false
-# When set to `true` enables experimental support of uv as a replacement
-# for pip-tools. Learn more about uv here: https://github.com/astral-sh/uv
-use-uv = false
-
# Enable or disable automatic `sync` after `add` and `remove`. This defaults
# to `true` when uv is enabled and `false` otherwise.
autosync = true
-# Marks the managed .venv in a way that cloud based synchronization systems
-# like Dropbox and iCloud Files will not upload it. This defaults to true
+# Marks the managed .venv in a way that cloud-based synchronization systems
+# like Dropbox and iCloud Files will not upload it. This defaults to `true`
# as a .venv in cloud storage typically does not make sense. Set this to
# `false` to disable this behavior.
venv-mark-sync-ignore = true
-# a array of tables with optional sources. Same format as in pyproject.toml
+# When set to `true` Rye will fetch certain interpreters with build information.
+# This will increase the space requirements, will put the interpreter into an
+# extra folder called `./install/` and place build artifacts adjacent in `./build`.
+fetch-with-build-info = false
+
+# An array of tables with optional sources. Same format as in pyproject.toml
[[sources]]
name = "default"
-url = "http://pypi.org/simple/"
+url = "https://pypi.org/simple/"
```
## Manipulating Config
@@ -107,7 +108,7 @@ url = "http://pypi.org/simple/"
The configuration can be read and modified with `rye config`. The
keys are in dotted notation. `--get` reads a key, `--set`, `--set-int`,
-`--set-bool`, or `--unset` modify one.
+`--set-bool`, and `--unset` modify one.
```bash
rye config --set proxy.http=http://127.0.0.1:4000
@@ -119,4 +120,4 @@ For more information see [`config`](commands/config.md).
## Per Project Config
-For the project specific `pyproject.toml` config see [pyproject.toml](pyproject.md).
+For the project-specific `pyproject.toml` config see [pyproject.toml](pyproject.md).
diff --git a/docs/guide/docker.md b/docs/guide/docker.md
new file mode 100644
index 0000000000..3f1a8447b8
--- /dev/null
+++ b/docs/guide/docker.md
@@ -0,0 +1,105 @@
+# Building a Container with Docker
+
+If you want to put your Python code into a container, you probably have some server code that you don't submit to PyPI or another registry.
+If that's the case, read on. Else, skip to [the next section](#container-from-a-python-package).
+
+This guide requires some familiarity with Docker and Dockerfiles.
+
+## Container from Source
+
+1. Make sure that your project is set up as a [virtual project](./virtual.md).
+ This means that you can't install it, and it won't mark itself as a dependency.
+ If you need your project to be installable, go to [the next section](#container-from-a-python-package).
+
+ - 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`.
+
+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 ./
+ RUN uv pip install --no-cache --system -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
+
+ COPY src .
+ CMD python main.py
+ ```
+
+3. You can now build your image like this:
+
+ ```bash
+ docker build .
+ ```
+
+### Dockerfile Adjustments
+
+The `Dockerfile`s in this guide are examples. Some adjustments you might want to make:
+
+- The command (`CMD python src/main.py`) should point to your script.
+- Adjust the base image (`FROM python:slim`):
+ - Prefer a tagged version that matches the one from your `.python-version` file, e.g. `FROM python:3.12.0-slim`.
+ - The `-slim` variants are generally a good tradeoff between image size and compatibility and should work fine for most workloads.
+ But you can also use `-alpine` for smaller images (but potential compatibility issues) or no suffix for ones that contain more system tools.
+- If you need additional system packages, install them before copying your source code, i.e. before the line `COPY src .`.
+ When using Debian-based images (i.e. `-slim` or no-suffix variants), that could look like this:
+
+ ```Dockerfile
+ RUN apt-get update \
+ && apt-get install -y --no-install-recommends some-dependency another-dependency \
+ && rm -rf /var/lib/apt/lists/*
+ ```
+
+## Container from a Python Package
+
+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 with [`uv`](https://github.com/astral-sh/uv):
+
+```Dockerfile
+FROM python:slim
+RUN pip install uv
+RUN --mount=source=dist,target=/dist uv pip install --no-cache /dist/*.whl
+CMD python -m my_package
+```
+
+To build your docker image, you'll have to first build your wheel, like this:
+
+```bash
+rye build --wheel --clean
+docker build . --tag your-image-name
+```
+
+Note that this approach bundles your dependencies and code in a single layer.
+This might be nice for performance, but it also means that all dependencies are re-installed during every image build, and different versions won't share the disk space for the dependencies.
+
+The [Dockerfile adjustments from the previous section](#dockerfile-adjustments) apply.
+
+## Explanations
+
+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 `--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/faq.md b/docs/guide/faq.md
index 9e72463b2d..c2c2d00ee5 100644
--- a/docs/guide/faq.md
+++ b/docs/guide/faq.md
@@ -92,7 +92,7 @@ the default one higher priority in the `PATH:
```
export PATH="/usr/bin:$PATH"
-curl -sSf https://rye-up.com/get | bash
+curl -sSf https://rye.astral.sh/get | bash
```
## References to Build-Time Paths
@@ -102,7 +102,7 @@ accommodating to portable builds there are various limitations still with this
approach. One of them is that built Python distributions capture some absolute
paths and other build-time configuration. These file paths are then often used
by build tools to invoke C compilers. For instance you might run into a compiler
-error like ``error: stdio.h: No such file or directory`` when building C
+error like `error: stdio.h: No such file or directory` when building C
extensions. There is no known solution to this problem today other than
[registering a non portable toolchain](toolchains/index.md#registering-toolchains).
@@ -133,12 +133,12 @@ due to limitations in `libedit`. In some cases though you might also discover t
the backspace key does not work or arrow keys don't work as expected. This can be
because the _terminfo_ database cannot be found.
-For solutions to this issue, read the [behavior quirks guide](https://python-build-standalone.readthedocs.io/en/latest/quirks.html) in the
+For solutions to this issue, read the [behavior quirks guide](https://gregoryszorc.com/docs/python-build-standalone/main/quirks.html) in the
Standalone Python Builds documentation for solutions.
## Can I use Rye Alongside Other Python Installations?
-Rye given it's experimental nature does not want to disrupt already existing Python
+Rye given its experimental nature does not want to disrupt already existing Python
workflows. As such using it alongside other Python installations is intentionally
supported. Even if the Rye shims come first on the `PATH`, Rye will automatically
resolve to a different Python installation on the search path when invoked in a
diff --git a/docs/guide/installation.md b/docs/guide/installation.md
index 25b62b8d51..1f883ee211 100644
--- a/docs/guide/installation.md
+++ b/docs/guide/installation.md
@@ -137,7 +137,7 @@ to learn more.
## Shell Completion
-Rye supports generating completion scripts for Bash, Zsh, Fish or Powershell. Here are some common locations for each shell:
+Rye supports generating completion scripts for Bash, Zsh, Fish, Powershell and Nushell. Here are some common locations for each shell:
=== "Bash"
@@ -183,6 +183,12 @@ Rye supports generating completion scripts for Bash, Zsh, Fish or Powershell. He
rye self completion -s powershell | Out-File -Encoding utf8 $PROFILE\..\Completions\rye_completion.ps1
```
+=== "NuShell"
+
+ ```nushell
+ rye self completion -s nushell | save --append $nu.env-path
+ ```
+
## Updating Rye
To update rye to the latest version you can use `rye` itself:
diff --git a/docs/guide/publish.md b/docs/guide/publish.md
index eb5b9c276a..b08ff7e8a7 100644
--- a/docs/guide/publish.md
+++ b/docs/guide/publish.md
@@ -61,3 +61,7 @@ rye publish --token --yes
```
Rye will store your repository info in `$HOME/.rye/credentials` for future use.
+
+### --skip-existing
+
+You can use `--skip-existing` to skip any distribution files that have already been published to the repository. Note that some repositories may not support this feature.
diff --git a/docs/guide/pyproject.md b/docs/guide/pyproject.md
index 832b6b0309..7c5a6bcc72 100644
--- a/docs/guide/pyproject.md
+++ b/docs/guide/pyproject.md
@@ -60,6 +60,33 @@ pulled in as indirect dependencies. These are added here automatically with `ry
excluded-dependencies = ["cffi"]
```
+## `tool.rye.universal`
+
++++ 0.36.0
+
+When this flag is enabled all `lock` and `sync` operations in the project or workspace
+operate as if `--universal` is passed. This means that the dependency resolver will
+attempt to generate a resolution that's valid on all platforms, operating systems, and
+architectures, rather than a resolution that's specific to the current platform.
+
+```toml
+[tool.rye]
+universal = true
+```
+
+## `tool.rye.generate-hashes`
+
++++ 0.35.0
+
+When this flag is enabled all `lock` and `sync` operations in the project or workspace
+operate as if `--generate-hashes` is passed. This means that all dependencies in all
+lock files will include a hash.
+
+```toml
+[tool.rye]
+generate-hashes = true
+```
+
## `tool.rye.lock-with-sources`
+++ 0.18.0
@@ -158,6 +185,18 @@ This key can be used to provide environment variables with a script:
devserver = { cmd = "flask run --debug", env = { FLASK_APP = "./hello.py" } }
```
+### `env-file`
+
++++ 0.30.0
+
+This is similar to `env` but rather than setting environment variables directly, it instead
+points to a file that should be loaded (relative to the `pyproject.toml`):
+
+```toml
+[tool.rye.scripts]
+devserver = { cmd = "flask run --debug", env-file = ".dev.env" }
+```
+
### `chain`
This is a special key that can be set instead of `cmd` to make a command invoke multiple
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/sources.md b/docs/guide/sources.md
index cae24beb75..4371da8942 100644
--- a/docs/guide/sources.md
+++ b/docs/guide/sources.md
@@ -85,6 +85,9 @@ This is a [PEP 503](https://www.python.org/dev/peps/pep-0503/) type index as pro
by tools such as PyPI or [devpi](https://github.com/devpi/devpi). It corresponds to
the arguments `--index-url` or `--extra-index-url` in pip.
+Note: see the [`uv` documentation](https://github.com/astral-sh/uv/blob/main/PIP_COMPATIBILITY.md#packages-that-exist-on-multiple-indexes)
+for more on the use of multiple indexes.
+
### `find-links`
This is a source that can be of a variety of types and has to point to a file path
diff --git a/docs/guide/sync.md b/docs/guide/sync.md
index 6d100253ce..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
-`pip-tools` but will offer you the option to use `uv` instead. `uv` will become
-the default choice once it stabilzes as it offers significantly better performance.
+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/guide/toolchains/cpython.md b/docs/guide/toolchains/cpython.md
index b676b6d609..d42dcaca1f 100644
--- a/docs/guide/toolchains/cpython.md
+++ b/docs/guide/toolchains/cpython.md
@@ -28,7 +28,7 @@ different from a regular Python build.
The following changes to a regular Python versions you should be aware of:
-* `libedit` instead of `readline`: unfortunately `readline` is GPL2 licensed
+* `libedit` instead of `readline`: unfortunately `readline` is GPLv3 licensed
and this is a hazard for redistributions. As such, the portable Python
builds link against the more freely licensed `libedit` instead.
@@ -37,8 +37,8 @@ The following changes to a regular Python versions you should be aware of:
Additionally due to how these builds are created, there are some other quirks
you might run into related to terminal support or TKinter. Some of these
-issues are collected in the [FAQ](../faq.md). Additionally the Python
-Standalone Builds have a [Behavior Quirks](https://python-build-standalone.readthedocs.io/en/latest/quirks.html)
+issues are collected in the [FAQ](../faq.md). Additionally, the Python
+Standalone Builds have a [Behavior Quirks](https://gregoryszorc.com/docs/python-build-standalone/main/quirks.html)
page.
## Sources
diff --git a/docs/guide/toolchains/index.md b/docs/guide/toolchains/index.md
index 9281cd0cf9..955c11d882 100644
--- a/docs/guide/toolchains/index.md
+++ b/docs/guide/toolchains/index.md
@@ -95,6 +95,19 @@ Toolchains are fetched from two sources:
* [Indygreg's Portable Python Builds](https://github.com/indygreg/python-build-standalone) for CPython
* [PyPy.org](https://www.pypy.org/) for PyPy
+You can also fetch toolchains into a specific location. In this case the interpreter is not
+stored where Rye normally consults it, but in a specific location. Rye will then not be able
+to use it unless it's manually registered. This however can be useful for debugging or advanced
+setups:
+
+```
+rye toolchain fetch cpython@3.8.5 --target-path=my-interpreter
+```
+
+If you want to use rye interpreter fetching without installing rye, you might want to export the
+`RYE_NO_AUTO_INSTALL` environment variable and set it to `1` as otherwise the installer will kick
+in.
+
## Registering Toolchains
Additionally, it's possible to register an external toolchain with the `rye toolchain register`
@@ -126,3 +139,19 @@ rye toolchain remove cpython@3.8.5
!!! Warning
Removing an actively used toolchain will render the virtualenvs that refer to use broken.
+
+## Build Info
+
++++ 0.31.0
+
+Prior to Rye 0.31.0 the Python installations were fetched with build infos. You can see
+this because the folder structure in `~/.rye/py/INTERPRETER` is a bit different. Rather than
+finding `cpython@3.8.5/bin/python3` there you will instead have an extra `install` folder
+(`cpython@3.8.5/install/bin/python3`) alongside a `build` folder containing the intermediate
+build outputs. Starting with 0.31.0 the build info is removed by default. If
+you want to get it back, you can explicitly fetch with `--build-info` or you can
+set the `behavior.fetch-with-build-info` config flag to true:
+
+```
+rye config --set-bool behavior.fetch-with-build-info=true
+```
diff --git a/docs/guide/virtual.md b/docs/guide/virtual.md
index ee0533db13..133ae885c9 100644
--- a/docs/guide/virtual.md
+++ b/docs/guide/virtual.md
@@ -18,11 +18,11 @@ rye run mkdocs
```
This will create a `pyproject.toml` but does not actually declare any python code itself.
-Yet when synching you will end up with mkdocs in your project.
+Yet when syncing you will end up with mkdocs in your project.
## Behavior Changes
-When synching the project itself is never installed into the virtualenv as it's not
+When syncing the project itself is never installed into the virtualenv as it's not
considered to be a valid package. Likewise you cannot publish virtual packages to
PyPI or another index.
diff --git a/docs/guide/workspaces.md b/docs/guide/workspaces.md
index 67a3fd0b2b..fbd33c4a1f 100644
--- a/docs/guide/workspaces.md
+++ b/docs/guide/workspaces.md
@@ -1,15 +1,15 @@
# Workspaces
Workspaces are a feature that allows you to work with multiple packages that
-have dependencies to each other. A workspace is declared by setting the
-`tool.rye.workspace` key a `pyproject.toml`. Afterwards all projects within
+have dependencies on each other. A workspace is declared by setting the
+`tool.rye.workspace` key in `pyproject.toml`. Afterwards, all projects within
that workspace share a singular virtualenv.
## Declaring Workspaces
-A workspace is declared by the "toplevel" `pyproject.toml`. At the very least
-the key `tool.rye.workspace` needs to be added. It's recommended that a glob
-pattern is also set in the `members` key to prevent accidentally including
+A workspace is declared in the "toplevel" `pyproject.toml`. At the very least
+the key `tool.rye.workspace` needs to be added. It's also recommended to
+set a glob pattern in the `members` key to prevent accidentally including
unintended folders as projects.
```toml
@@ -18,7 +18,7 @@ members = ["myname-*"]
```
This declares a workspace where all folders starting with the name `myname-`
-are considered. If the toplevel workspace in itself should not be a project,
+are considered. If the toplevel workspace itself should not be a project,
then it should be declared as a virtual package:
```toml
@@ -29,12 +29,12 @@ virtual = true
members = ["myname-*"]
```
-For more information on that see [Virtual Packages](../virtual/).
+For more information on that, see [Virtual Packages](../virtual/).
## Syncing
-In a workspace it does not matter which project you are working with, the entire
-workspace is synchronized at all times. This has some untypical consequences but
+In a workspace, it does not matter which project you are working with, the entire
+workspace is synchronized at all times. This has some atypical consequences but
simplifies the general development workflow.
When a package depends on another package it's first located in the workspace locally
diff --git a/docs/index.md b/docs/index.md
index 866283c269..ccfdcbcf34 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -8,6 +8,16 @@ hide:
Rye: a Hassle-Free Python Experience
+!!! note
+
+ If you're getting started with Rye, consider [uv](https://github.com/astral-sh/uv), the
+ [successor project](https://lucumr.pocoo.org/2024/2/15/rye-grows-with-uv/) from the same maintainers.
+
+ While Rye is actively maintained, uv offers a more stable and feature-complete experience, and is the recommended
+ choice for new projects.
+
+ Having trouble migrating? [Let us know what's missing.](https://github.com/astral-sh/rye/discussions/1342)
+
Rye is a comprehensive project and package management solution for Python.
Born from [its creator's](https://github.com/mitsuhiko) desire to establish a
one-stop-shop for all Python users, Rye provides a unified experience to install and manages Python
diff --git a/docs/philosophy.md b/docs/philosophy.md
index 9850d45cbd..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 manage 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/mkdocs.yml b/mkdocs.yml
index a4b97d19d0..3f9e29a608 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -21,6 +21,7 @@ nav:
- Dependencies: guide/deps.md
- Workspaces: guide/workspaces.md
- Virtual Projects: guide/virtual.md
+ - Docker: guide/docker.md
- Commands:
- Overview: guide/commands/index.md
- add: guide/commands/add.md
@@ -39,6 +40,7 @@ nav:
- run: guide/commands/run.md
- show: guide/commands/show.md
- sync: guide/commands/sync.md
+ - test: guide/commands/test.md
- toolchain:
- Overview: guide/commands/toolchain/index.md
- fetch: guide/commands/toolchain/fetch.md
diff --git a/notes/pep508.md b/notes/pep508.md
index f2cf2b1ede..0238c8efb8 100644
--- a/notes/pep508.md
+++ b/notes/pep508.md
@@ -96,7 +96,7 @@ dependencies = [
]
[project.dependencies_meta.0]
-exta_information = 42
+extra_information = 42
```
Unfortunately that would still cause issues for tools that locally interpret `pyproject.toml`
diff --git a/pyproject.toml b/pyproject.toml
index 43067e34af..c136f82907 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,18 +15,15 @@ dependencies = [
readme = "README.md"
requires-python = ">= 3.8"
-[build-system]
-requires = ["hatchling"]
-build-backend = "hatchling.build"
-
-[tool.hatch.build.targets.wheel]
-packages = ["dummy"]
-
[tool.rye]
managed = true
+virtual = true
[tool.rye.scripts]
serve-docs = "mkdocs serve"
[tool.rye.workspace]
members = ["rye-devtools"]
+
+[tool.pytest.ini_options]
+addopts = "--ignore target"
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 9fd084536d..70c818ffe0 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -6,18 +6,18 @@
# features: []
# all-features: false
# with-sources: false
+# generate-hashes: false
--e file:.
-e file:rye-devtools
-anyio==4.2.0
+anyio==4.4.0
# via httpx
-certifi==2023.5.7
+certifi==2024.6.2
# via httpcore
# via httpx
# via requests
-charset-normalizer==3.1.0
+charset-normalizer==3.3.2
# via requests
-click==8.1.3
+click==8.1.7
# via mkdocs
colorama==0.4.6
# via mkdocs-material
@@ -25,16 +25,17 @@ ghp-import==2.1.0
# via mkdocs
h11==0.14.0
# via httpcore
-httpcore==1.0.2
+httpcore==1.0.5
# via httpx
-httpx==0.26.0
+httpx==0.27.0
# via rye-devtools
-idna==3.4
+idna==3.7
# via anyio
# via httpx
# via requests
-isort==5.13.2
-jinja2==3.1.2
+iniconfig==2.0.0
+ # via pytest
+jinja2==3.1.4
# via mkdocs
# via mkdocs-material
markdown==3.3.7
@@ -42,53 +43,50 @@ markdown==3.3.7
# via mkdocs
# via mkdocs-material
# via pymdown-extensions
-markupsafe==2.1.2
+markupsafe==2.1.5
# via jinja2
-mdx-gh-links==0.3
- # via rye-dev
+mdx-gh-links==0.4
mergedeep==1.3.4
# via mkdocs
mkdocs==1.4.3
# via mkdocs-material
# via mkdocs-simple-hooks
- # via rye-dev
mkdocs-include-markdown-plugin==4.0.4
- # via rye-dev
-mkdocs-material==9.1.12
- # via rye-dev
-mkdocs-material-extensions==1.1.1
+mkdocs-material==9.1.20
+mkdocs-material-extensions==1.3.1
# via mkdocs-material
mkdocs-simple-hooks==0.1.5
- # via rye-dev
mkdocs-version-annotations==1.0.0
- # via rye-dev
-packaging==23.1
+packaging==24.1
# via mkdocs
-pygments==2.15.1
+ # via pytest
+pluggy==1.5.0
+ # via pytest
+pygments==2.18.0
# via mkdocs-material
pymdown-extensions==9.11
# via mkdocs-material
- # via rye-dev
-python-dateutil==2.8.2
+pytest==8.0.2
+python-dateutil==2.9.0.post0
# via ghp-import
-pyyaml==6.0
+pyyaml==6.0.1
# via mkdocs
# via pymdown-extensions
# via pyyaml-env-tag
pyyaml-env-tag==0.1
# via mkdocs
-regex==2023.5.5
+regex==2024.5.15
# via mkdocs-material
-requests==2.31.0
+requests==2.32.3
# via mkdocs-material
six==1.16.0
# via python-dateutil
-sniffio==1.3.0
+sniffio==1.3.1
# via anyio
# via httpx
socksio==1.0.0
# via httpx
-urllib3==2.0.2
+urllib3==2.2.2
# via requests
-watchdog==3.0.0
+watchdog==4.0.1
# via mkdocs
diff --git a/requirements.lock b/requirements.lock
index 4bf6f467c2..c2cd892a2d 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -6,18 +6,18 @@
# features: []
# all-features: false
# with-sources: false
+# generate-hashes: false
--e file:.
-e file:rye-devtools
-anyio==4.2.0
+anyio==4.4.0
# via httpx
-certifi==2023.5.7
+certifi==2024.6.2
# via httpcore
# via httpx
# via requests
-charset-normalizer==3.1.0
+charset-normalizer==3.3.2
# via requests
-click==8.1.3
+click==8.1.7
# via mkdocs
colorama==0.4.6
# via mkdocs-material
@@ -25,15 +25,15 @@ ghp-import==2.1.0
# via mkdocs
h11==0.14.0
# via httpcore
-httpcore==1.0.2
+httpcore==1.0.5
# via httpx
-httpx==0.26.0
+httpx==0.27.0
# via rye-devtools
-idna==3.4
+idna==3.7
# via anyio
# via httpx
# via requests
-jinja2==3.1.2
+jinja2==3.1.4
# via mkdocs
# via mkdocs-material
markdown==3.3.7
@@ -41,53 +41,46 @@ markdown==3.3.7
# via mkdocs
# via mkdocs-material
# via pymdown-extensions
-markupsafe==2.1.2
+markupsafe==2.1.5
# via jinja2
-mdx-gh-links==0.3
- # via rye-dev
+mdx-gh-links==0.4
mergedeep==1.3.4
# via mkdocs
mkdocs==1.4.3
# via mkdocs-material
# via mkdocs-simple-hooks
- # via rye-dev
mkdocs-include-markdown-plugin==4.0.4
- # via rye-dev
-mkdocs-material==9.1.12
- # via rye-dev
-mkdocs-material-extensions==1.1.1
+mkdocs-material==9.1.20
+mkdocs-material-extensions==1.3.1
# via mkdocs-material
mkdocs-simple-hooks==0.1.5
- # via rye-dev
mkdocs-version-annotations==1.0.0
- # via rye-dev
-packaging==23.1
+packaging==24.1
# via mkdocs
-pygments==2.15.1
+pygments==2.18.0
# via mkdocs-material
pymdown-extensions==9.11
# via mkdocs-material
- # via rye-dev
-python-dateutil==2.8.2
+python-dateutil==2.9.0.post0
# via ghp-import
-pyyaml==6.0
+pyyaml==6.0.1
# via mkdocs
# via pymdown-extensions
# via pyyaml-env-tag
pyyaml-env-tag==0.1
# via mkdocs
-regex==2023.5.5
+regex==2024.5.15
# via mkdocs-material
-requests==2.31.0
+requests==2.32.3
# via mkdocs-material
six==1.16.0
# via python-dateutil
-sniffio==1.3.0
+sniffio==1.3.1
# via anyio
# via httpx
socksio==1.0.0
# via httpx
-urllib3==2.0.2
+urllib3==2.2.2
# via requests
-watchdog==3.0.0
+watchdog==4.0.1
# via mkdocs
diff --git a/rye-devtools/pyproject.toml b/rye-devtools/pyproject.toml
index 9d7b27a75d..81e65bff86 100644
--- a/rye-devtools/pyproject.toml
+++ b/rye-devtools/pyproject.toml
@@ -19,7 +19,7 @@ build-backend = "hatchling.build"
[tool.rye]
managed = true
dev-dependencies = [
- "isort>=5.13.2",
+ "pytest==8.0.2",
]
[tool.hatch.metadata]
diff --git a/rye-devtools/src/rye_devtools/__init__.py b/rye-devtools/src/rye_devtools/__init__.py
index 0f1844592b..e69de29bb2 100644
--- a/rye-devtools/src/rye_devtools/__init__.py
+++ b/rye-devtools/src/rye_devtools/__init__.py
@@ -1,2 +0,0 @@
-from . import find_downloads
-from . import find_uv_downloads
diff --git a/rye-devtools/src/rye_devtools/common.py b/rye-devtools/src/rye_devtools/common.py
index 254f7cb004..1433a54408 100644
--- a/rye-devtools/src/rye_devtools/common.py
+++ b/rye-devtools/src/rye_devtools/common.py
@@ -1,9 +1,8 @@
-from typing import Self
import itertools
import sys
import time
from datetime import datetime, timezone
-from typing import NamedTuple
+from typing import NamedTuple, Self
import httpx
diff --git a/rye-devtools/src/rye_devtools/find_downloads.py b/rye-devtools/src/rye_devtools/find_downloads.py
index 2d4db55a35..13e92f6ddb 100644
--- a/rye-devtools/src/rye_devtools/find_downloads.py
+++ b/rye-devtools/src/rye_devtools/find_downloads.py
@@ -1,16 +1,15 @@
-"""This script is used to generate rye/src/downloads.inc.
+"""This script is used to generate rye/src/sources/generated/python_downloads.inc.
It finds the latest Python releases, sorts them by
various factors (arch, platform, flavor) and generates download
links to be included into rye at build time.
"""
+
import abc
import asyncio
-import itertools
import os
import re
import sys
-import unittest
from dataclasses import dataclass
from enum import StrEnum
from urllib.parse import unquote
@@ -18,21 +17,7 @@
import httpx
from httpx import HTTPStatusError
-from .common import Version, fetch, PlatformTriple
-
-
-def log(*args, **kwargs):
- print(*args, file=sys.stderr, **kwargs)
-
-
-def batched(iterable, n):
- "Batch data into tuples of length n. The last batch may be shorter."
- # batched('ABCDEFG', 3) --> ABC DEF G
- if n < 1:
- raise ValueError("n must be at least one")
- it = iter(iterable)
- while batch := tuple(itertools.islice(it, n)):
- yield batch
+from .common import PlatformTriple, Version, batched, fetch, log
class PythonImplementation(StrEnum):
@@ -209,6 +194,11 @@ def match_mapping(
# Map, old, special triplets to proper triples for parsing, or
# return the triple if it's not a special one
triple = cls.SPECIAL_TRIPLES.get(triple, triple)
+
+ # freethreaded builds are experimental, ignore them for now
+ if "freethreaded" in triple:
+ return
+
pieces = triple.split("-")
flavor = match_flavor(triple)
env, pieces = match_mapping(pieces, cls.ENV_MAPPING)
@@ -434,31 +424,3 @@ def main():
if __name__ == "__main__":
main()
-
-
-class Tests(unittest.TestCase):
- def test_parse_triplets(self):
- expected = {
- "aarch64-apple-darwin-lto": PlatformTriple("aarch64", "macos", None, "lto"),
- "aarch64-unknown-linux-gnu-pgo+lto": PlatformTriple(
- "aarch64", "linux", "gnu", "pgo+lto"
- ),
- # "x86_64-unknown-linux-musl-debug": PlatformTriple(
- # "x86_64", "linux", "musl", "debug"
- # ),
- "aarch64-unknown-linux-gnu-debug-full": PlatformTriple(
- "aarch64", "linux", "gnu", "debug"
- ),
- "x86_64-unknown-linux-gnu-debug": PlatformTriple(
- "x86_64", "linux", "gnu", "debug"
- ),
- "linux64": PlatformTriple("x86_64", "linux", "gnu", None),
- "ppc64le-unknown-linux-gnu-noopt-full": None,
- "x86_64_v3-unknown-linux-gnu-lto": None,
- "x86_64-pc-windows-msvc-shared-pgo": PlatformTriple(
- "x86_64", "windows", None, "shared-pgo"
- ),
- }
-
- for input, expected in expected.items():
- self.assertEqual(CPythonFinder.parse_triple(input), expected, input)
diff --git a/rye-devtools/src/rye_devtools/find_uv_downloads.py b/rye-devtools/src/rye_devtools/find_uv_downloads.py
index b677ecef0d..f27d0e5fcb 100644
--- a/rye-devtools/src/rye_devtools/find_uv_downloads.py
+++ b/rye-devtools/src/rye_devtools/find_uv_downloads.py
@@ -1,17 +1,19 @@
-"""This script is used to generate rye/src/generated/uv_downloads.inc.
+"""This script is used to generate rye/src/sources/generated/uv_downloads.inc.
It finds the latest UV releases and generates rust code that can be included
into rye at build time.
"""
-from .common import Version, PlatformTriple, fetch, log
-import re
-from typing import AsyncIterator
import asyncio
import os
-import httpx
+import re
import sys
from dataclasses import dataclass
+from typing import AsyncIterator
+
+import httpx
+
+from .common import PlatformTriple, Version, fetch, log
@dataclass
@@ -33,14 +35,20 @@ class UvDownloads:
"aarch64": "aarch64",
}
+ GLIBC = {
+ "x86_64": "gnu",
+ "i686": "gnu",
+ "aarch64": "musl",
+ }
+
PLATFORM_ENV = {
"unknown-linux-gnu": ("linux", "gnu"),
- # "unknown-linux-musl": ("linux", "musl"),
+ "unknown-linux-musl": ("linux", "musl"),
"apple-darwin": ("macos", None),
"pc-windows-msvc": ("windows", None),
}
- RE = re.compile(r"uv-(?P[^\-]+)-(?P.+)(\.tar\.gz|\.zip)$")
+ RE = re.compile(r"uv-(?P[^-]+)-(?P.+)(\.tar\.gz|\.zip)$")
def __init__(self, client: httpx.Client) -> None:
self.client = client
@@ -62,7 +70,7 @@ async def most_recent_downloads(
url = asset["browser_download_url"]
if (triple := self.parse_triple(url)) is not None:
sha_resp = await fetch(self.client, url + ".sha256")
- sha256 = sha_resp.text.split(' ')[0].strip()
+ sha256 = sha_resp.text.split(" ")[0].strip()
yield UvDownload(
triple=triple,
version=version,
@@ -79,9 +87,10 @@ def parse_triple(cls, url: str) -> PlatformTriple | None:
if arch_str in cls.ARCH and plat_env_str in cls.PLATFORM_ENV:
arch = cls.ARCH[arch_str]
plat, env = cls.PLATFORM_ENV[plat_env_str]
- return PlatformTriple(
- arch=arch, platform=plat, environment=env, flavor=None
- )
+ if env is None or env == cls.GLIBC[arch_str]:
+ return PlatformTriple(
+ arch=arch, platform=plat, environment=env, flavor=None
+ )
return None
@@ -89,7 +98,7 @@ def parse_triple(cls, url: str) -> PlatformTriple | None:
def render(downloads: list[UvDownload]):
print("// Generated by rye-devtools. DO NOT EDIT.")
print(
- "// To regenerate, run `rye run uv-downloads > rye/src/generated/uv_downloads.inc` from the root of the repository."
+ "// To regenerate, run `rye run uv-downloads > rye/src/sources/generated/uv_downloads.inc` from the root of the repository."
)
print("use std::borrow::Cow;")
print("pub const UV_DOWNLOADS: &[UvDownload] = &[")
@@ -123,8 +132,6 @@ async def async_main():
"Authorization": "Bearer " + token,
}
- downloads = []
-
log("Fetching all uv downloads.")
async with httpx.AsyncClient(follow_redirects=True, headers=headers) as client:
finder = UvDownloads(client)
diff --git a/rye-devtools/tests/test_basic.py b/rye-devtools/tests/test_basic.py
new file mode 100644
index 0000000000..9aa20fadcc
--- /dev/null
+++ b/rye-devtools/tests/test_basic.py
@@ -0,0 +1,41 @@
+import pytest
+from rye_devtools.common import batched
+from rye_devtools.find_downloads import CPythonFinder, PlatformTriple
+
+
+def test_batched():
+ assert list(batched("ABCDEFG", 3)) == [tuple("ABC"), tuple("DEF"), tuple("G")]
+
+
+@pytest.mark.parametrize(
+ "input, expected",
+ [
+ ("aarch64-apple-darwin-lto", PlatformTriple("aarch64", "macos", None, "lto")),
+ (
+ "aarch64-unknown-linux-gnu-pgo+lto",
+ PlatformTriple("aarch64", "linux", "gnu", "pgo+lto"),
+ ),
+ # (
+ # "x86_64-unknown-linux-musl-debug",
+ # PlatformTriple("x86_64", "linux", "musl", "debug"),
+ # ),
+ (
+ "aarch64-unknown-linux-gnu-debug-full",
+ PlatformTriple("aarch64", "linux", "gnu", "debug"),
+ ),
+ (
+ "x86_64-unknown-linux-gnu-debug",
+ PlatformTriple("x86_64", "linux", "gnu", "debug"),
+ ),
+ ("linux64", PlatformTriple("x86_64", "linux", "gnu", None)),
+ ("ppc64le-unknown-linux-gnu-noopt-full", None),
+ ("x86_64_v3-unknown-linux-gnu-lto", None),
+ (
+ "x86_64-pc-windows-msvc-shared-pgo",
+ PlatformTriple("x86_64", "windows", None, "shared-pgo"),
+ ),
+ ("aarch64-apple-darwin-freethreaded+pgo-full", None),
+ ],
+)
+def test_parse_triplets(input, expected):
+ assert CPythonFinder.parse_triple(input) == expected
diff --git a/rye/Cargo.toml b/rye/Cargo.toml
index 538a1eb5f2..2664716695 100644
--- a/rye/Cargo.toml
+++ b/rye/Cargo.toml
@@ -1,13 +1,13 @@
[package]
name = "rye"
-version = "0.28.0"
+version = "0.43.0"
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-age = "0.9.1"
+age = "0.10.0"
anyhow = { version = "1.0.70", features = ["backtrace"] }
clap = { version = "4.3.5", default-features = false, features = [
"derive",
@@ -16,20 +16,15 @@ clap = { version = "4.3.5", default-features = false, features = [
"std",
] }
clap_complete = "4.2.1"
+clap_complete_nushell = "4.5.1"
console = "0.15.7"
curl = { version = "0.4.44", features = ["ssl", "static-curl", "static-ssl"] }
-decompress = { version = "0.6.0", default-features = false, features = [
- "tarzst",
- "targz",
-] }
flate2 = "1.0.25"
git-testament = "0.2.4"
globset = "0.4.10"
indicatif = "0.17.3"
-memchr = "2.5.0"
license = { version = "3.1.1", features = ["offline"] }
-minijinja = { version = "1.0.0", features = ["json"] }
-nix = { version = "0.27.1", default-features = false, features = ["process"] }
+minijinja = { version = "2.0.1", features = ["json"] }
once_cell = "1.17.1"
pathdiff = "0.2.1"
pep440_rs = "0.4.0"
@@ -42,7 +37,7 @@ shlex = "1.3.0"
slug = "0.1.4"
tar = "0.4.38"
tempfile = "3.5.0"
-toml_edit = "0.21.0"
+toml_edit = "0.22.9"
url = "2.3.1"
walkdir = "2.3.3"
which = "6.0.0"
@@ -57,15 +52,15 @@ self-replace = "1.3.5"
configparser = "3.0.2"
monotrail-utils = { git = "https://github.com/konstin/poc-monotrail", rev = "e0251f68c254f834180198b8677fcf85d4b6a844" }
python-pkginfo = { version = "0.6.0", features = ["serde"] }
-sysinfo = { version = "0.29.4", default-features = false, features = [] }
home = "0.5.9"
ctrlc = "3.4.2"
+dotenvy = "0.15.7"
[target."cfg(unix)".dependencies]
xattr = "1.3.1"
[target."cfg(windows)".dependencies]
-winapi = { version = "0.3.9", default-features = false, features = [] }
+winapi = { version = "0.3.9", default-features = false, features = ["winuser", "winioctl", "ioapiset"] }
winreg = "0.52.0"
[target."cfg(windows)".build-dependencies]
diff --git a/rye/src/bootstrap.rs b/rye/src/bootstrap.rs
index 3a55016cee..e548775a3e 100644
--- a/rye/src/bootstrap.rs
+++ b/rye/src/bootstrap.rs
@@ -9,16 +9,17 @@ use anyhow::{anyhow, bail, Context, Error};
use console::style;
use indicatif::{ProgressBar, ProgressStyle};
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_toolchain_python_bin, list_known_toolchains,
+ get_app_dir, get_canonical_py_path, get_python_bin_within, get_toolchain_python_bin,
+ list_known_toolchains,
};
use crate::pyproject::latest_available_python_version;
use crate::sources::py::{get_download_url, PythonVersion, PythonVersionRequest};
use crate::utils::{check_checksum, symlink_file, unpack_archive, CommandOutput, IoPathContext};
-use crate::uv::Uv;
+use crate::uv::UvBuilder;
/// this is the target version that we want to fetch
pub const SELF_PYTHON_TARGET_VERSION: PythonVersionRequest = PythonVersionRequest {
@@ -31,11 +32,11 @@ pub const SELF_PYTHON_TARGET_VERSION: PythonVersionRequest = PythonVersionReques
suffix: None,
};
-const SELF_VERSION: u64 = 14;
+const SELF_VERSION: u64 = 26;
-const SELF_REQUIREMENTS: &str = r#"
-build==1.0.3
-certifi==2023.11.17
+pub const SELF_REQUIREMENTS: &str = r#"
+build==1.2.1
+certifi==2024.2.2
charset-normalizer==3.3.2
click==8.1.7
distlib==0.3.8
@@ -46,11 +47,11 @@ platformdirs==4.0.0
pyproject_hooks==1.0.0
requests==2.31.0
tomli==2.0.1
-twine==4.0.2
+twine==5.1.1
unearth==0.14.0
urllib3==2.0.7
virtualenv==20.25.0
-ruff==0.2.2
+ruff==0.8.2
"#;
static FORCED_TO_UPDATE: AtomicBool = AtomicBool::new(false);
@@ -64,6 +65,28 @@ fn is_up_to_date() -> bool {
*UP_TO_UPDATE || FORCED_TO_UPDATE.load(atomic::Ordering::Relaxed)
}
+#[derive(Debug, Clone)]
+pub(crate) enum SelfVenvStatus {
+ NotUpToDate,
+ DoesNotExist,
+}
+
+/// Get self venv path and check if it exists and is up to date
+pub fn get_self_venv_status() -> Result {
+ let app_dir = get_app_dir();
+ let venv_dir = app_dir.join("self");
+
+ if venv_dir.is_dir() {
+ if is_up_to_date() {
+ Ok(venv_dir)
+ } else {
+ Err((venv_dir, SelfVenvStatus::NotUpToDate))
+ }
+ } else {
+ Err((venv_dir, SelfVenvStatus::DoesNotExist))
+ }
+}
+
/// Bootstraps the venv for rye itself
pub fn ensure_self_venv(output: CommandOutput) -> Result {
ensure_self_venv_with_toolchain(output, None)
@@ -75,26 +98,31 @@ pub fn ensure_self_venv_with_toolchain(
toolchain_version_request: Option,
) -> Result {
let app_dir = get_app_dir();
- let venv_dir = app_dir.join("self");
- if venv_dir.is_dir() {
- if is_up_to_date() {
- return Ok(venv_dir);
- } else {
- if output != CommandOutput::Quiet {
- echo!("Detected outdated rye internals. Refreshing");
- }
+ let venv_dir = match get_self_venv_status() {
+ Ok(venv_dir) => return Ok(venv_dir),
+ Err((venv_dir, SelfVenvStatus::DoesNotExist)) => venv_dir,
+ Err((venv_dir, SelfVenvStatus::NotUpToDate)) => {
+ echo!(if output, "Detected outdated rye internals. Refreshing");
fs::remove_dir_all(&venv_dir)
.path_context(&venv_dir, "could not remove self-venv for update")?;
+
+ let pip_tools_dir = app_dir.join("pip-tools");
+ if pip_tools_dir.is_dir() {
+ fs::remove_dir_all(&pip_tools_dir)
+ .context("could not remove pip-tools for update")?;
+ }
+
+ venv_dir
}
- }
+ };
- if output != CommandOutput::Quiet {
- echo!("Bootstrapping rye internals");
- }
+ echo!(if output, "Bootstrapping rye internals");
// Ensure we have uv
- let uv = Uv::ensure_exists(CommandOutput::Quiet)?;
+ let uv = UvBuilder::new()
+ .with_output(CommandOutput::Quiet)
+ .ensure_exists()?;
let version = match toolchain_version_request {
Some(ref version_request) => ensure_specific_self_toolchain(output, version_request)
@@ -125,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");
@@ -254,9 +282,9 @@ pub fn get_pip_module(venv: &Path) -> Result {
Ok(rv)
}
-/// we only support cpython 3.9 to 3.12
+/// we only support cpython 3.9 to 3.13
pub fn is_self_compatible_toolchain(version: &PythonVersion) -> bool {
- version.name == "cpython" && version.major == 3 && version.minor >= 9 && version.minor <= 12
+ version.name == "cpython" && version.major == 3 && version.minor >= 9 && version.minor <= 13
}
/// Ensure that the toolchain for the self environment is available.
@@ -269,15 +297,17 @@ fn ensure_latest_self_toolchain(output: CommandOutput) -> Result,
+ /// Include build info (overrides configured default).
+ pub build_info: Option,
+}
+
+impl FetchOptions {
+ /// Basic fetch options.
+ pub fn with_output(output: CommandOutput) -> FetchOptions {
+ FetchOptions {
+ output,
+ ..Default::default()
+ }
+ }
+}
+
+impl Default for FetchOptions {
+ fn default() -> Self {
+ Self {
+ output: CommandOutput::Normal,
+ force: false,
+ target_path: None,
+ build_info: None,
+ }
+ }
+}
+
/// Fetches a version if missing.
pub fn fetch(
version: &PythonVersionRequest,
- output: CommandOutput,
- force: bool,
+ options: FetchOptions,
) -> Result {
- if let Ok(version) = PythonVersion::try_from(version.clone()) {
- let py_bin = get_toolchain_python_bin(&version)?;
- if !force && py_bin.is_file() {
- if output == CommandOutput::Verbose {
- echo!("Python version already downloaded. Skipping.");
+ // Check if there is registered toolchain that matches the request
+ if options.target_path.is_none() {
+ if let Ok(version) = PythonVersion::try_from(version.clone()) {
+ let py_bin = get_toolchain_python_bin(&version)?;
+ if !options.force && py_bin.is_file() {
+ echo!(if verbose options.output, "Python version already downloaded. Skipping.");
+ return Ok(version);
}
- return Ok(version);
}
}
-
let (version, url, sha256) = match get_download_url(version) {
Some(result) => result,
None => bail!("unknown version {}", version),
};
- let target_dir = get_canonical_py_path(&version)?;
- let target_py_bin = get_toolchain_python_bin(&version)?;
- if output == CommandOutput::Verbose {
- echo!("target dir: {}", target_dir.display());
- }
- if target_dir.is_dir() && target_py_bin.is_file() {
- if !force {
- if output == CommandOutput::Verbose {
- echo!("Python version already downloaded. Skipping.");
+ let target_dir = match options.target_path {
+ Some(ref target_dir) => {
+ if target_dir.is_file() {
+ bail!("target directory '{}' is a file", target_dir.display());
+ }
+ echo!(if options.output, "Downloading to '{}'", target_dir.display());
+ if target_dir.is_dir() {
+ if options.force {
+ // Refuse to remove the target directory if it's not empty and not a python installation
+ if target_dir.read_dir()?.next().is_some()
+ && !get_python_bin_within(target_dir).exists()
+ {
+ bail!(
+ "target directory '{}' exists and is not a Python installation",
+ target_dir.display()
+ );
+ }
+ fs::remove_dir_all(target_dir)
+ .path_context(target_dir, "could not remove target directory")?;
+ } else {
+ bail!("target directory '{}' exists", target_dir.display());
+ }
}
- return Ok(version);
+ Cow::Borrowed(target_dir.as_path())
}
- if output != CommandOutput::Quiet {
- echo!("Removing the existing Python version");
+ None => {
+ let target_dir = get_canonical_py_path(&version)?;
+ let target_py_bin = get_toolchain_python_bin(&version)?;
+ if target_py_bin.is_file() {
+ if !options.force {
+ echo!(if verbose options.output, "Python version already downloaded. Skipping.");
+ return Ok(version);
+ }
+ echo!(if options.output, "Removing the existing Python version");
+ fs::remove_dir_all(&target_dir).with_context(|| {
+ format!("failed to remove target folder {}", target_dir.display())
+ })?;
+ }
+ echo!(if verbose options.output, "target dir: {}", target_dir.display());
+ Cow::Owned(target_dir)
}
- fs::remove_dir_all(&target_dir)
- .with_context(|| format!("failed to remove target folder {}", target_dir.display()))?;
- }
+ };
- fs::create_dir_all(&target_dir).path_context(&target_dir, "failed to create target folder")?;
-
- if output == CommandOutput::Verbose {
- echo!("download url: {}", url);
- }
- if output != CommandOutput::Quiet {
- echo!("{} {}", style("Downloading").cyan(), version);
- }
- let archive_buffer = download_url(url, output)?;
+ echo!(if verbose options.output, "download url: {}", url);
+ echo!(if options.output, "{} {}", style("Downloading").cyan(), version);
+ let archive_buffer = download_url(url, options.output)?;
if let Some(sha256) = sha256 {
- if output != CommandOutput::Quiet {
- echo!("{} {}", style("Checking").cyan(), "checksum");
- }
+ echo!(if options.output, "{} {}", style("Checking").cyan(), "checksum");
check_checksum(&archive_buffer, sha256)
.with_context(|| format!("Checksum check of {} failed", &url))?;
- } else if output != CommandOutput::Quiet {
- echo!("Checksum check skipped (no hash available)");
+ } else {
+ echo!(if options.output, "Checksum check skipped (no hash available)");
}
- if output != CommandOutput::Quiet {
- echo!("{}", style("Unpacking").cyan());
+ echo!(if options.output, "{}", style("Unpacking").cyan());
+
+ let parent = target_dir
+ .parent()
+ .ok_or_else(|| anyhow!("cannot unpack to root"))?;
+ if !parent.exists() {
+ fs::create_dir_all(parent).path_context(&target_dir, "failed to create target folder")?;
}
- unpack_archive(&archive_buffer, &target_dir, 1).with_context(|| {
+
+ let with_build_info = options
+ .build_info
+ .unwrap_or_else(|| Config::current().fetch_with_build_info());
+ let temp_dir = tempdir_in(parent).context("temporary unpack location")?;
+
+ unpack_archive(&archive_buffer, temp_dir.path(), 1).with_context(|| {
format!(
"unpacking of downloaded tarball {} to '{}' failed",
&url,
- target_dir.display()
+ temp_dir.path().display(),
)
})?;
- if output != CommandOutput::Quiet {
- echo!("{} {}", style("Downloaded").green(), version);
+ // if we want to retain build infos or the installation has no build infos, then move
+ // the folder into the permanent location
+ if with_build_info || !installation_has_build_info(temp_dir.path()) {
+ let temp_dir = temp_dir.into_path();
+ fs::rename(&temp_dir, &target_dir).inspect_err(|_| {
+ fs::remove_dir_all(&temp_dir).ok();
+ })
+
+ // otherwise move the contents of the `install` folder over.
+ } else {
+ fs::rename(temp_dir.path().join("install"), &target_dir)
}
+ .path_context(&target_dir, "unable to persist download")?;
+
+ echo!(if options.output, "{} {}", style("Downloaded").green(), version);
Ok(version)
}
+fn installation_has_build_info(p: &Path) -> bool {
+ let mut has_install = false;
+ let mut has_build = false;
+ if let Ok(dir) = p.read_dir() {
+ for entry in dir.flatten() {
+ match entry.file_name().to_str() {
+ Some("install") => has_install = true,
+ Some("build") => has_build = true,
+ _ => {}
+ }
+ }
+ }
+ has_install && has_build
+}
+
pub fn download_url(url: &str, output: CommandOutput) -> Result, Error> {
match download_url_ignore_404(url, output)? {
Some(result) => Ok(result),
@@ -502,6 +616,6 @@ fn validate_shared_libraries(py: &Path) -> Result<(), Error> {
}
bail!(
"Python installation is unable to run on this machine due to missing libraries.\n\
- Visit https://rye-up.com/guide/faq/#missing-shared-libraries-on-linux for next steps."
+ Visit https://rye.astral.sh/guide/faq/#missing-shared-libraries-on-linux for next steps."
);
}
diff --git a/rye/src/cli/add.rs b/rye/src/cli/add.rs
index a3dd4269db..e181c88342 100644
--- a/rye/src/cli/add.rs
+++ b/rye/src/cli/add.rs
@@ -1,62 +1,21 @@
use std::env;
-use std::io::Write;
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, set_proxy_variables, CommandOutput};
-use crate::uv::Uv;
-
-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,
-}
+use crate::utils::{format_requirement, get_venv_python_bin, CommandOutput};
+use crate::uv::UvBuilder;
#[derive(Parser, Debug)]
pub struct ReqExtras {
@@ -138,7 +97,7 @@ impl ReqExtras {
};
req.version_or_url = match req.version_or_url {
Some(_) => bail!("requirement already has a version marker"),
- None => Some(pep508_rs::VersionOrUrl::Url(
+ None => Some(VersionOrUrl::Url(
format!("git+{}{}", git, suffix).parse().with_context(|| {
format!("unable to interpret '{}{}' as git reference", git, suffix)
})?,
@@ -147,14 +106,15 @@ impl ReqExtras {
} else if let Some(ref url) = self.url {
req.version_or_url = match req.version_or_url {
Some(_) => bail!("requirement already has a version marker"),
- None => Some(pep508_rs::VersionOrUrl::Url(
- url.parse()
- .with_context(|| format!("unable to parse '{}' as url", url))?,
- )),
+ None => {
+ Some(VersionOrUrl::Url(url.parse().with_context(|| {
+ format!("unable to parse '{}' as url", url)
+ })?))
+ }
};
} 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);
@@ -170,11 +130,13 @@ impl ReqExtras {
path.display()
)
})?;
- Url::from_file_path(Path::new("/${PROJECT_ROOT}").join(rv)).unwrap()
+ let mut url = Url::parse("file://")?;
+ url.set_path(&Path::new("/${PROJECT_ROOT}").join(rv).to_string_lossy());
+ url
};
req.version_or_url = match req.version_or_url {
Some(_) => bail!("requirement already has a version marker"),
- None => Some(pep508_rs::VersionOrUrl::Url(file_url)),
+ None => Some(VersionOrUrl::Url(file_url)),
};
}
for feature in self.features.iter().flat_map(|x| x.split(',')) {
@@ -197,7 +159,7 @@ pub struct Args {
#[command(flatten)]
req_extras: ReqExtras,
/// Add this as dev dependency.
- #[arg(long)]
+ #[arg(short, long)]
dev: bool,
/// Add this as an excluded dependency that will not be installed even if it's a sub dependency.
#[arg(long, conflicts_with = "dev", conflicts_with = "optional")]
@@ -205,9 +167,6 @@ pub struct Args {
/// Add this to an optional dependency group.
#[arg(long, conflicts_with = "dev", conflicts_with = "excluded")]
optional: Option,
- /// Include pre-releases when finding a package version.
- #[arg(long)]
- pre: bool,
/// Overrides the pin operator
#[arg(long)]
pin: Option,
@@ -223,12 +182,24 @@ pub struct Args {
/// Turns off all output.
#[arg(short, long, conflicts_with = "verbose")]
quiet: bool,
+
+ /// Include pre-releases when finding a package version and automatically syncing the workspace.
+ #[arg(long)]
+ pre: bool,
+ /// Set to `true` to lock with sources in the lockfile when automatically syncing the workspace.
+ #[arg(long)]
+ with_sources: bool,
+ /// Set to `true` to lock with hashes in the lockfile when automatically syncing the workspace.
+ #[arg(long)]
+ generate_hashes: bool,
+ /// Attempt to use `keyring` for authentication for index URLs.
+ #[arg(long, value_enum, default_value_t)]
+ keyring_provider: KeyringProvider,
}
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()?;
@@ -259,30 +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,
- )?;
- } else {
- 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 {
@@ -302,144 +259,19 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
}
if (cfg.autosync() && !cmd.no_sync) || cmd.sync {
- autosync(&pyproject_toml, output)?;
+ autosync(
+ &pyproject_toml,
+ output,
+ cmd.pre,
+ cmd.with_sources,
+ cmd.generate_hashes,
+ cmd.keyring_provider,
+ )?;
}
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,
@@ -447,50 +279,35 @@ fn resolve_requirements_with_uv(
pre: bool,
output: CommandOutput,
default_operator: &Operator,
+ keyring_provider: KeyringProvider,
) -> Result<(), Error> {
- let mut cmd = Uv::ensure_exists(output)?.cmd();
- cmd.arg("pip")
- .arg("compile")
- .arg("--python-version")
- .arg(py_ver.format_simple())
- .arg("--no-deps")
- .arg("--no-header")
- .arg("-")
- .env("VIRTUAL_ENV", pyproject_toml.venv_path().as_os_str());
- if pre {
- cmd.arg("--prerelease=allow");
- }
- if output == CommandOutput::Quiet {
- cmd.arg("-q");
- }
- // this primarily exists for testing
- if let Ok(dt) = env::var("__RYE_UV_EXCLUDE_NEWER") {
- cmd.arg("--exclude-newer").arg(dt);
- }
+ let venv_path = pyproject_toml.venv_path();
+ let py_bin = get_venv_python_bin(&venv_path);
let sources = ExpandedSources::from_sources(&pyproject_toml.sources()?)?;
- sources.add_as_pip_args(&mut cmd);
- let mut child = cmd
- .stdin(Stdio::piped())
- .stdout(Stdio::piped())
- .stderr(Stdio::piped())
- .spawn()?;
- let child_stdin = child.stdin.as_mut().unwrap();
- for requirement in &*requirements {
- writeln!(child_stdin, "{}", requirement)?;
- }
- let rv = child.wait_with_output()?;
- if !rv.status.success() {
- let log = String::from_utf8_lossy(&rv.stderr);
- bail!("failed to resolve packages:\n{}", log);
- }
+ let uv = UvBuilder::new()
+ .with_output(output.quieter())
+ .with_sources(sources)
+ .ensure_exists()?
+ .venv(&venv_path, &py_bin, py_ver, None)?;
+
+ for req in requirements {
+ let mut new_req = uv.resolve(
+ py_ver,
+ req,
+ pre,
+ env::var("__RYE_UV_EXCLUDE_NEWER").ok(),
+ keyring_provider,
+ )?;
+
+ // if a version or URL is already provided we just use the normalized package name but
+ // retain all old information.
+ if req.version_or_url.is_some() {
+ req.name = new_req.name;
+ continue;
+ }
- for (line, req) in String::from_utf8_lossy(&rv.stdout)
- .lines()
- .zip(requirements)
- {
- *req = line.parse()?;
- if let Some(ref mut version_or_url) = req.version_or_url {
+ if let Some(ref mut version_or_url) = new_req.version_or_url {
if let VersionOrUrl::VersionSpecifier(ref mut specs) = version_or_url {
*version_or_url = VersionOrUrl::VersionSpecifier(VersionSpecifiers::from_iter({
let mut new_specs = Vec::new();
@@ -511,6 +328,10 @@ fn resolve_requirements_with_uv(
}));
}
}
+ if let Some(old_extras) = &req.extras {
+ new_req.extras = Some(old_extras.clone());
+ }
+ *req = new_req;
}
Ok(())
diff --git a/rye/src/cli/build.rs b/rye/src/cli/build.rs
index d1db714fd7..3950ca7c7b 100644
--- a/rye/src/cli/build.rs
+++ b/rye/src/cli/build.rs
@@ -2,18 +2,21 @@ use std::fs;
use std::path::PathBuf;
use std::process::{Command, Stdio};
-use anyhow::{bail, Error};
+use anyhow::{anyhow, bail, Context, Error};
use clap::Parser;
use console::style;
-use crate::bootstrap::ensure_self_venv;
+use crate::bootstrap::{fetch, FetchOptions};
+
+use crate::platform::get_toolchain_python_bin;
use crate::pyproject::{locate_projects, PyProject};
-use crate::utils::{get_venv_python_bin, CommandOutput, IoPathContext};
+use crate::utils::{get_venv_python_bin, prepend_path_to_path_env, CommandOutput, IoPathContext};
+use crate::uv::UvBuilder;
/// Builds a package for distribution.
#[derive(Parser, Debug)]
pub struct Args {
- /// Build an sdist
+ /// Build a sdist
#[arg(long)]
sdist: bool,
/// Build a wheel
@@ -44,8 +47,8 @@ pub struct Args {
pub fn execute(cmd: Args) -> Result<(), Error> {
let output = CommandOutput::from_quiet_and_verbose(cmd.quiet, cmd.verbose);
- let venv = ensure_self_venv(output)?;
let project = PyProject::load_or_discover(cmd.pyproject.as_deref())?;
+ let py_ver = project.venv_python_version()?;
let out = match cmd.out {
Some(path) => path,
@@ -63,17 +66,42 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
let projects = locate_projects(project, cmd.all, &cmd.package[..])?;
+ let all_virtual = projects.iter().all(|p| p.is_virtual());
+ if all_virtual {
+ warn!("skipping build, all projects are virtual");
+ return Ok(());
+ }
+
+ // Make sure we have a compatible Python version.
+ let py_ver = fetch(&py_ver.into(), FetchOptions::with_output(output))
+ .context("failed fetching toolchain ahead of sync")?;
+ echo!(if output, "Python version: {}", style(&py_ver).cyan());
+ let py_bin = get_toolchain_python_bin(&py_ver)?;
+
+ // Create a virtual environment in which to perform the builds.
+ let uv = UvBuilder::new()
+ .with_output(CommandOutput::Quiet)
+ .ensure_exists()?;
+ let venv_dir = tempfile::tempdir().context("failed to create temporary directory")?;
+ let uv_venv = uv
+ .venv(venv_dir.path(), &py_bin, &py_ver, None)
+ .context("failed to create build environment")?;
+ uv_venv.write_marker()?;
+ uv_venv.bootstrap()?;
+
for project in projects {
// skip over virtual packages on build
if project.is_virtual() {
continue;
}
- if output != CommandOutput::Quiet {
- echo!("building {}", style(project.normalized_name()?).cyan());
- }
+ echo!(
+ if output,
+ "building {}",
+ style(project.normalized_name()?).cyan()
+ );
- let mut build_cmd = Command::new(get_venv_python_bin(&venv));
+ let mut build_cmd = Command::new(get_venv_python_bin(venv_dir.path()));
build_cmd
.arg("-mbuild")
.env("NO_COLOR", "1")
@@ -81,6 +109,18 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
.arg(&out)
.arg(&*project.root_path());
+ // 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");
}
@@ -88,6 +128,10 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
build_cmd.arg("--sdist");
}
+ if output == CommandOutput::Verbose {
+ build_cmd.arg("--verbose");
+ }
+
if output == CommandOutput::Quiet {
build_cmd.stdout(Stdio::null());
build_cmd.stderr(Stdio::null());
@@ -98,6 +142,5 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
bail!("failed to build dist");
}
}
-
Ok(())
}
diff --git a/rye/src/cli/fetch.rs b/rye/src/cli/fetch.rs
index 376ff774ae..dfbd2284c4 100644
--- a/rye/src/cli/fetch.rs
+++ b/rye/src/cli/fetch.rs
@@ -1,14 +1,16 @@
+use std::path::PathBuf;
+
use anyhow::{Context, Error};
use clap::Parser;
-use crate::bootstrap::fetch;
+use crate::bootstrap::{fetch, FetchOptions};
use crate::config::Config;
use crate::platform::get_python_version_request_from_pyenv_pin;
use crate::pyproject::PyProject;
use crate::sources::py::PythonVersionRequest;
use crate::utils::CommandOutput;
-/// Fetches a Python interpreter for the local machine. This is an alias of `rye toolchain fetch`.
+/// Fetches a Python interpreter for the local machine.
#[derive(Parser, Debug)]
pub struct Args {
/// The version of Python to fetch.
@@ -18,6 +20,15 @@ pub struct Args {
/// Fetch the Python toolchain even if it is already installed.
#[arg(short, long)]
force: bool,
+ /// Fetches the Python toolchain into an explicit location rather.
+ #[arg(long)]
+ target_path: Option,
+ /// Fetches with build info.
+ #[arg(long)]
+ build_info: bool,
+ /// Fetches without build info.
+ #[arg(long, conflicts_with = "build_info")]
+ no_build_info: bool,
/// Enables verbose diagnostics.
#[arg(short, long)]
verbose: bool,
@@ -43,6 +54,21 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
}
};
- fetch(&version, output, cmd.force).context("error while fetching Python installation")?;
+ fetch(
+ &version,
+ FetchOptions {
+ output,
+ force: cmd.force,
+ target_path: cmd.target_path,
+ build_info: if cmd.build_info {
+ Some(true)
+ } else if cmd.no_build_info {
+ Some(false)
+ } else {
+ None
+ },
+ },
+ )
+ .context("error while fetching Python installation")?;
Ok(())
}
diff --git a/rye/src/cli/init.rs b/rye/src/cli/init.rs
index 1bd9d847d2..e88608357a 100644
--- a/rye/src/cli/init.rs
+++ b/rye/src/cli/init.rs
@@ -152,7 +152,8 @@ if __name__ == "setuptools":
pub fn execute(cmd: Args) -> Result<(), Error> {
let cfg = Config::current();
- let env = Environment::new();
+ let mut env = Environment::new();
+ env.set_keep_trailing_newline(true);
let dir = env::current_dir()?.join(cmd.path);
let toml = dir.join("pyproject.toml");
let readme = dir.join("README.md");
@@ -252,7 +253,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
}
if metadata.author.is_none() {
is_metadata_author_none = true;
- metadata.author = author.clone();
+ metadata.author.clone_from(&author);
}
if metadata.requires_python.is_none() {
metadata.requires_python = Some(requires_python);
@@ -335,28 +336,28 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
.status()
.map(|status| status.success())
.unwrap_or(false)
+ && is_metadata_author_none
{
- let gitignore = dir.join(".gitignore");
-
- // create a .gitignore if one is missing
- if !gitignore.is_file() {
- let rv = env.render_named_str(
- "gitignore.txt",
- GITIGNORE_TEMPLATE,
- context! {
- is_rust => matches!(build_system, BuildSystem::Maturin)
- },
- )?;
- fs::write(&gitignore, rv).path_context(&gitignore, "failed to write .gitignore")?;
- }
- if is_metadata_author_none {
- let new_author = get_default_author_with_fallback(&dir);
- if author != new_author {
- metadata.author = new_author;
- }
+ let new_author = get_default_author_with_fallback(&dir);
+ if author != new_author {
+ metadata.author = new_author;
}
}
+ let gitignore = dir.join(".gitignore");
+
+ // create a .gitignore if one is missing
+ if !gitignore.is_file() {
+ let rv = env.render_named_str(
+ "gitignore.txt",
+ GITIGNORE_TEMPLATE,
+ context! {
+ is_rust => matches!(build_system, BuildSystem::Maturin)
+ },
+ )?;
+ fs::write(&gitignore, rv).path_context(&gitignore, "failed to write .gitignore")?;
+ }
+
let rv = env.render_named_str(
"pyproject.json",
TOML_TEMPLATE,
@@ -444,15 +445,14 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
}
}
- if output != CommandOutput::Quiet {
- echo!(
- "{} Initialized {}project in {}",
- style("success:").green(),
- if is_virtual { "virtual " } else { "" },
- dir.display()
- );
- echo!(" Run `rye sync` to get started");
- }
+ echo!(
+ if output,
+ "{} Initialized {}project in {}",
+ style("success:").green(),
+ if is_virtual { "virtual " } else { "" },
+ dir.display()
+ );
+ echo!(if output, " Run `rye sync` to get started");
Ok(())
}
diff --git a/rye/src/cli/install.rs b/rye/src/cli/install.rs
index 64aeb49d8a..aae1632584 100644
--- a/rye/src/cli/install.rs
+++ b/rye/src/cli/install.rs
@@ -7,10 +7,11 @@ use pep508_rs::Requirement;
use crate::cli::add::ReqExtras;
use crate::config::Config;
use crate::installer::{install, resolve_local_requirement};
+use crate::lock::KeyringProvider;
use crate::sources::py::PythonVersionRequest;
use crate::utils::CommandOutput;
-/// Installs a package as global tool. This is an alias of `rye tools install`.
+/// Installs a package as global tool.
#[derive(Parser, Debug)]
pub struct Args {
/// The name of the package to install.
@@ -29,6 +30,9 @@ pub struct Args {
/// Force install the package even if it's already there.
#[arg(short, long)]
force: bool,
+ /// Attempt to use `keyring` for authentication for index URLs.
+ #[arg(long, value_enum, default_value_t)]
+ keyring_provider: KeyringProvider,
/// Enables verbose diagnostics.
#[arg(short, long)]
verbose: bool,
@@ -74,6 +78,7 @@ pub fn execute(mut cmd: Args) -> Result<(), Error> {
&cmd.include_dep,
&extra_requirements,
output,
+ cmd.keyring_provider,
)?;
Ok(())
}
diff --git a/rye/src/cli/list.rs b/rye/src/cli/list.rs
index c091d37365..92812e25a3 100644
--- a/rye/src/cli/list.rs
+++ b/rye/src/cli/list.rs
@@ -1,15 +1,11 @@
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::Uv;
+use crate::uv::{UvBuilder, Venv};
/// Prints the currently installed packages.
#[derive(Parser, Debug)]
@@ -23,30 +19,12 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
let project = PyProject::load_or_discover(cmd.pyproject.as_deref())?;
let python = get_venv_python_bin(&project.venv_path());
if !python.is_file() {
+ warn!("Project is not synced, no virtualenv found. Run `rye sync`.");
return Ok(());
}
- let self_venv = ensure_self_venv(CommandOutput::Normal)?;
-
- let status = if Config::current().use_uv() {
- Uv::ensure_exists(CommandOutput::Normal)?
- .cmd()
- .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");
- }
-
+ let uv = UvBuilder::new()
+ .with_output(CommandOutput::Normal)
+ .ensure_exists()?;
+ uv.read_only_venv(&project.venv_path())?.freeze()?;
Ok(())
}
diff --git a/rye/src/cli/lock.rs b/rye/src/cli/lock.rs
index e12e0b6ccb..31b3b16292 100644
--- a/rye/src/cli/lock.rs
+++ b/rye/src/cli/lock.rs
@@ -3,7 +3,7 @@ use std::path::PathBuf;
use anyhow::Error;
use clap::Parser;
-use crate::lock::LockOptions;
+use crate::lock::{KeyringProvider, LockOptions};
use crate::sync::{sync, SyncMode, SyncOptions};
use crate::utils::CommandOutput;
@@ -34,10 +34,19 @@ pub struct Args {
/// Set to true to lock with sources in the lockfile.
#[arg(long)]
with_sources: bool,
+ /// Attempt to use `keyring` for authentication for index URLs.
+ #[arg(long, value_enum, default_value_t)]
+ keyring_provider: KeyringProvider,
+ /// Set to true to lock with hashes in the lockfile.
+ #[arg(long)]
+ generate_hashes: bool,
+ /// Use universal lock files.
+ #[arg(long)]
+ universal: bool,
/// Reset prior lock options.
#[arg(long)]
reset: bool,
- /// Use this pyproject.toml file
+ /// Use this pyproject.toml file.
#[arg(long, value_name = "PYPROJECT_TOML")]
pyproject: Option,
}
@@ -55,8 +64,11 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
all_features: cmd.all_features,
with_sources: cmd.with_sources,
reset: cmd.reset,
+ generate_hashes: cmd.generate_hashes,
+ universal: cmd.universal,
},
pyproject: cmd.pyproject,
+ keyring_provider: cmd.keyring_provider,
..SyncOptions::default()
})?;
Ok(())
diff --git a/rye/src/cli/mod.rs b/rye/src/cli/mod.rs
index c34a163de3..ee5ece42b4 100644
--- a/rye/src/cli/mod.rs
+++ b/rye/src/cli/mod.rs
@@ -1,4 +1,5 @@
use std::env;
+use std::path::PathBuf;
use anyhow::{bail, Error};
use clap::Parser;
@@ -22,6 +23,7 @@ mod rye;
mod shim;
mod show;
mod sync;
+mod test;
mod toolchain;
mod tools;
mod uninstall;
@@ -29,9 +31,11 @@ mod version;
use git_testament::git_testament;
-use crate::bootstrap::SELF_PYTHON_TARGET_VERSION;
+use crate::bootstrap::{get_self_venv_status, SELF_PYTHON_TARGET_VERSION};
use crate::config::Config;
use crate::platform::symlinks_supported;
+use crate::pyproject::read_venv_marker;
+use crate::utils::IoPathContext;
git_testament!(TESTAMENT);
@@ -41,6 +45,9 @@ git_testament!(TESTAMENT);
struct Args {
#[command(subcommand)]
command: Option,
+ /// Load one or more .env files.
+ #[arg(long)]
+ env_file: Vec,
/// Print the version
#[arg(long)]
version: bool,
@@ -66,6 +73,7 @@ enum Command {
Run(run::Args),
Show(show::Args),
Sync(sync::Args),
+ Test(test::Args),
Toolchain(toolchain::Args),
Tools(tools::Args),
#[command(name = "self")]
@@ -99,6 +107,13 @@ pub fn execute() -> Result<(), Error> {
}
let args = Args::try_parse()?;
+
+ // handle --env-file. As this happens here this cannot influence `RYE_HOME` or
+ // the behavior of the shims.
+ for env_file in &args.env_file {
+ dotenvy::from_path(env_file).path_context(env_file, "unable to load env file")?;
+ }
+
let cmd = if args.version {
return print_version();
} else if let Some(cmd) = args.command {
@@ -107,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),
@@ -124,6 +146,7 @@ pub fn execute() -> Result<(), Error> {
Command::Run(cmd) => run::execute(cmd),
Command::Show(cmd) => show::execute(cmd),
Command::Sync(cmd) => sync::execute(cmd),
+ Command::Test(cmd) => test::execute(cmd),
Command::Toolchain(cmd) => toolchain::execute(cmd),
Command::Tools(cmd) => tools::execute(cmd),
Command::Rye(cmd) => rye::execute(cmd),
@@ -151,8 +174,20 @@ fn print_version() -> Result<(), Error> {
std::env::consts::OS,
std::env::consts::ARCH
);
- echo!("self-python: {}", SELF_PYTHON_TARGET_VERSION);
+
+ let self_venv_python = match get_self_venv_status() {
+ Ok(venv_dir) | Err((venv_dir, _)) => read_venv_marker(&venv_dir).map(|mark| mark.python),
+ };
+
+ if let Some(python) = self_venv_python {
+ echo!("self-python: {}", python);
+ } else {
+ echo!(
+ "self-python: not bootstrapped (target: {})",
+ SELF_PYTHON_TARGET_VERSION
+ );
+ }
echo!("symlink support: {}", symlinks_supported());
- echo!("uv enabled: {}", Config::current().use_uv());
+ echo!("uv enabled: {}", true);
Ok(())
}
diff --git a/rye/src/cli/publish.rs b/rye/src/cli/publish.rs
index 45bc4756e2..4cc31b01f1 100644
--- a/rye/src/cli/publish.rs
+++ b/rye/src/cli/publish.rs
@@ -42,6 +42,9 @@ pub struct Args {
/// Path to alternate CA bundle.
#[arg(long)]
cert: Option,
+ /// Skip files that have already been published (only applies to repositories supporting this feature)
+ #[arg(long)]
+ skip_existing: bool,
/// Skip prompts.
#[arg(short, long)]
yes: bool,
@@ -56,16 +59,17 @@ pub struct Args {
pub fn execute(cmd: Args) -> Result<(), Error> {
let output = CommandOutput::from_quiet_and_verbose(cmd.quiet, cmd.verbose);
let venv = ensure_self_venv(output)?;
- let project = PyProject::discover()?;
-
- if project.is_virtual() {
- bail!("virtual packages cannot be published");
- }
// Get the files to publish.
let files = match cmd.dist {
Some(paths) => paths,
- None => vec![project.workspace_path().join("dist").join("*")],
+ None => {
+ let project = PyProject::discover()?;
+ if project.is_virtual() {
+ bail!("virtual packages cannot be published");
+ }
+ vec![project.workspace_path().join("dist").join("*")]
+ }
};
// a. Get token from arguments and offer encryption, then store in credentials file.
@@ -112,7 +116,6 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
let maybe_encrypted = maybe_encrypt(&secret, cmd.yes)?;
let maybe_encoded = maybe_encode(&secret, &maybe_encrypted);
credentials[repository]["token"] = Item::Value(maybe_encoded.expose_secret().into());
- write_credentials(&credentials)?;
secret
} else if let Some(token) = credentials
@@ -167,6 +170,9 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
if let Some(cert) = cmd.cert {
publish_cmd.arg("--cert").arg(cert);
}
+ if cmd.skip_existing {
+ publish_cmd.arg("--skip-existing");
+ }
if output == CommandOutput::Quiet {
publish_cmd.stdout(Stdio::null());
diff --git a/rye/src/cli/remove.rs b/rye/src/cli/remove.rs
index 73adaa2632..3ded3a76bb 100644
--- a/rye/src/cli/remove.rs
+++ b/rye/src/cli/remove.rs
@@ -5,6 +5,7 @@ use clap::Parser;
use pep508_rs::Requirement;
use crate::config::Config;
+use crate::lock::KeyringProvider;
use crate::pyproject::{DependencyKind, PyProject};
use crate::sync::autosync;
use crate::utils::{format_requirement, CommandOutput};
@@ -16,7 +17,7 @@ pub struct Args {
#[arg(required = true)]
requirements: Vec,
/// Remove this from dev dependencies.
- #[arg(long)]
+ #[arg(short, long)]
dev: bool,
/// Remove this from an optional dependency group.
#[arg(long, conflicts_with = "dev")]
@@ -33,6 +34,19 @@ pub struct Args {
/// Turns off all output.
#[arg(short, long, conflicts_with = "verbose")]
quiet: bool,
+
+ /// Include pre-releases when automatically syncing the workspace.
+ #[arg(long)]
+ pre: bool,
+ /// Set to `true` to lock with sources in the lockfile when automatically syncing the workspace.
+ #[arg(long)]
+ with_sources: bool,
+ /// Set to `true` to lock with hashes in the lockfile when automatically syncing the workspace.
+ #[arg(long)]
+ generate_hashes: bool,
+ /// Attempt to use `keyring` for authentication for index URLs.
+ #[arg(long, value_enum, default_value_t)]
+ keyring_provider: KeyringProvider,
}
pub fn execute(cmd: Args) -> Result<(), Error> {
@@ -65,7 +79,14 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
}
if (Config::current().autosync() && !cmd.no_sync) || cmd.sync {
- autosync(&pyproject_toml, output)?;
+ autosync(
+ &pyproject_toml,
+ output,
+ cmd.pre,
+ cmd.with_sources,
+ cmd.generate_hashes,
+ cmd.keyring_provider,
+ )?;
}
Ok(())
diff --git a/rye/src/cli/run.rs b/rye/src/cli/run.rs
index 0daf1980b6..d69433c2f4 100644
--- a/rye/src/cli/run.rs
+++ b/rye/src/cli/run.rs
@@ -1,3 +1,4 @@
+use std::collections::HashMap;
use std::env::{self, join_paths, split_paths};
use std::ffi::OsString;
use std::path::PathBuf;
@@ -10,7 +11,7 @@ use console::style;
use crate::pyproject::{PyProject, Script};
use crate::sync::{sync, SyncOptions};
use crate::tui::redirect_to_stderr;
-use crate::utils::{exec_spawn, get_venv_python_bin, success_status};
+use crate::utils::{exec_spawn, get_venv_python_bin, success_status, IoPathContext};
/// Runs a command installed into this package.
#[derive(Parser, Debug)]
@@ -34,7 +35,7 @@ enum Cmd {
}
pub fn execute(cmd: Args) -> Result<(), Error> {
- let _guard = redirect_to_stderr(true);
+ let guard = redirect_to_stderr(true);
let pyproject = PyProject::load_or_discover(cmd.pyproject.as_deref())?;
// make sure we have the minimal virtualenv.
@@ -42,6 +43,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
.context("failed to sync ahead of run")?;
if cmd.list || cmd.cmd.is_none() {
+ drop(guard);
return list_scripts(&pyproject);
}
let args = match cmd.cmd {
@@ -62,9 +64,9 @@ fn invoke_script(
let mut env_overrides = None;
match pyproject.get_script_cmd(&args[0].to_string_lossy()) {
- Some(Script::Call(entry, env_vars)) => {
+ Some(Script::Call(entry, env_vars, env_file)) => {
let py = OsString::from(get_venv_python_bin(&pyproject.venv_path()));
- env_overrides = Some(env_vars);
+ env_overrides = Some(load_env_vars(pyproject, env_file, env_vars)?);
args = if let Some((module, func)) = entry.split_once(':') {
if module.is_empty() || func.is_empty() {
bail!("Python callable must be in the form : or ")
@@ -86,11 +88,11 @@ fn invoke_script(
.chain(args.into_iter().skip(1))
.collect();
}
- Some(Script::Cmd(script_args, env_vars)) => {
+ Some(Script::Cmd(script_args, env_vars, env_file)) => {
if script_args.is_empty() {
bail!("script has no arguments");
}
- env_overrides = Some(env_vars);
+ env_overrides = Some(load_env_vars(pyproject, env_file, env_vars)?);
let script_target = venv_bin.join(&script_args[0]);
if script_target.is_file() {
args = Some(script_target.as_os_str().to_owned())
@@ -157,6 +159,23 @@ fn invoke_script(
}
}
+fn load_env_vars(
+ pyproject: &PyProject,
+ env_file: Option,
+ mut env_vars: HashMap,
+) -> Result, Error> {
+ if let Some(ref env_file) = env_file {
+ let env_file = pyproject.root_path().join(env_file);
+ for item in
+ dotenvy::from_path_iter(&env_file).path_context(&env_file, "could not load env-file")?
+ {
+ let (k, v) = item.path_context(&env_file, "invalid value in env-file")?;
+ env_vars.insert(k, v);
+ }
+ }
+ Ok(env_vars)
+}
+
fn list_scripts(pyproject: &PyProject) -> Result<(), Error> {
let mut scripts: Vec<_> = pyproject
.list_scripts()
diff --git a/rye/src/cli/rye.rs b/rye/src/cli/rye.rs
index 85cd9b8aba..87299b89a1 100644
--- a/rye/src/cli/rye.rs
+++ b/rye/src/cli/rye.rs
@@ -7,8 +7,10 @@ use std::sync::Arc;
use std::{env, fs};
use anyhow::{anyhow, bail, Context, Error};
-use clap::{CommandFactory, Parser};
-use clap_complete::Shell;
+use clap::builder::PossibleValue;
+use clap::{CommandFactory, Parser, ValueEnum};
+use clap_complete::{Generator, Shell};
+use clap_complete_nushell::Nushell;
use console::style;
use minijinja::render;
use self_replace::self_delete_outside_path;
@@ -52,12 +54,80 @@ pub struct Args {
command: SubCommand,
}
+#[derive(Clone, Debug)]
+enum ShellCompletion {
+ /// Bourne Again SHell (bash)
+ Bash,
+ /// Elvish shell
+ Elvish,
+ /// Friendly Interactive SHell (fish)
+ Fish,
+ /// PowerShell
+ PowerShell,
+ /// Z SHell (zsh)
+ Zsh,
+ /// Nushell
+ Nushell,
+}
+
+impl ValueEnum for ShellCompletion {
+ /// Returns the variants for the shell completion.
+ fn value_variants<'a>() -> &'a [Self] {
+ &[
+ ShellCompletion::Bash,
+ ShellCompletion::Elvish,
+ ShellCompletion::Fish,
+ ShellCompletion::PowerShell,
+ ShellCompletion::Zsh,
+ ShellCompletion::Nushell,
+ ]
+ }
+
+ /// Returns the possible value for the shell completion.
+ fn to_possible_value<'a>(&self) -> Option {
+ Some(match self {
+ ShellCompletion::Bash => PossibleValue::new("bash"),
+ ShellCompletion::Elvish => PossibleValue::new("elvish"),
+ ShellCompletion::Fish => PossibleValue::new("fish"),
+ ShellCompletion::PowerShell => PossibleValue::new("powershell"),
+ ShellCompletion::Zsh => PossibleValue::new("zsh"),
+ ShellCompletion::Nushell => PossibleValue::new("nushell"),
+ })
+ }
+}
+
+impl Generator for ShellCompletion {
+ /// Generate the file name for the completion script.
+ fn file_name(&self, name: &str) -> String {
+ match self {
+ ShellCompletion::Nushell => Nushell.file_name(name),
+ ShellCompletion::Bash => Shell::Bash.file_name(name),
+ ShellCompletion::Elvish => Shell::Elvish.file_name(name),
+ ShellCompletion::Fish => Shell::Fish.file_name(name),
+ ShellCompletion::PowerShell => Shell::PowerShell.file_name(name),
+ ShellCompletion::Zsh => Shell::Zsh.file_name(name),
+ }
+ }
+
+ /// Generate the completion script for the shell.
+ fn generate(&self, cmd: &clap::Command, buf: &mut dyn std::io::Write) {
+ match self {
+ ShellCompletion::Nushell => Nushell.generate(cmd, buf),
+ ShellCompletion::Bash => Shell::Bash.generate(cmd, buf),
+ ShellCompletion::Elvish => Shell::Elvish.generate(cmd, buf),
+ ShellCompletion::Fish => Shell::Fish.generate(cmd, buf),
+ ShellCompletion::PowerShell => Shell::PowerShell.generate(cmd, buf),
+ ShellCompletion::Zsh => Shell::Zsh.generate(cmd, buf),
+ }
+ }
+}
+
/// Generates a completion script for a shell.
#[derive(Parser, Debug)]
pub struct CompletionCommand {
/// The shell to generate a completion script for (defaults to 'bash').
#[arg(short, long)]
- shell: Option,
+ shell: Option,
}
/// Performs an update of rye.
@@ -75,6 +145,9 @@ pub struct UpdateCommand {
/// Update to a specific git rev.
#[arg(long, conflicts_with = "tag")]
rev: Option,
+ /// Update to a specific git branch.
+ #[arg(long, conflicts_with = "tag", conflicts_with = "rev")]
+ branch: Option,
/// Force reinstallation
#[arg(long)]
force: bool,
@@ -177,7 +250,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
fn completion(args: CompletionCommand) -> Result<(), Error> {
clap_complete::generate(
- args.shell.unwrap_or(Shell::Bash),
+ args.shell.unwrap_or(ShellCompletion::Bash),
&mut super::Args::command(),
"rye",
&mut std::io::stdout(),
@@ -192,7 +265,7 @@ fn update(args: UpdateCommand) -> Result<(), Error> {
let current_exe = env::current_exe()?;
// git based installation with cargo
- if args.rev.is_some() || args.tag.is_some() {
+ if args.rev.is_some() || args.tag.is_some() || args.branch.is_some() {
let mut cmd = Command::new("cargo");
let tmp = tempdir()?;
cmd.arg("install")
@@ -214,6 +287,9 @@ fn update(args: UpdateCommand) -> Result<(), Error> {
} else if let Some(ref tag) = args.tag {
cmd.arg("--tag");
cmd.arg(tag);
+ } else if let Some(ref branch) = args.branch {
+ cmd.arg("--branch");
+ cmd.arg(branch);
}
if args.force {
cmd.arg("--force");
@@ -273,6 +349,10 @@ fn update(args: UpdateCommand) -> Result<(), Error> {
Please stop running Python interpreters and retry the update.",
)?;
+ echo!("Validate updated installation");
+ validate_updated_exe(¤t_exe)
+ .context("unable to perform validation of updated installation")?;
+
echo!("Updated!");
echo!();
Command::new(current_exe).arg("--version").status()?;
@@ -280,6 +360,43 @@ fn update(args: UpdateCommand) -> Result<(), Error> {
Ok(())
}
+fn validate_updated_exe(rye: &Path) -> Result<(), Error> {
+ let folder = tempfile::tempdir()?;
+
+ // first create a dummy project via the new rye version
+ if !Command::new(rye)
+ .arg("init")
+ .arg("--name=test-project")
+ .arg("-q")
+ .arg(".")
+ .current_dir(folder.path())
+ .status()?
+ .success()
+ {
+ bail!("failed to initialize test project");
+ }
+
+ // then try to run the python shim in the context of that project.
+ // this as a by product should update outdated internals and perform
+ // a python only sync in all versions of rye known currently.
+ if !Command::new(
+ get_app_dir()
+ .join("shims")
+ .join("python")
+ .with_extension(EXE_EXTENSION),
+ )
+ .arg("-c")
+ .arg("")
+ .current_dir(folder.path())
+ .status()?
+ .success()
+ {
+ bail!("failed to run python shim in test project");
+ }
+
+ Ok(())
+}
+
fn update_exe_and_shims(new_exe: &Path) -> Result<(), Error> {
let app_dir = get_app_dir().canonicalize()?;
let current_exe = env::current_exe()?.canonicalize()?;
@@ -337,7 +454,7 @@ fn uninstall(args: UninstallCommand) -> Result<(), Error> {
let shim_dir = app_dir.join("shims");
if let Ok(dir) = shim_dir.read_dir() {
for entry in dir.flatten() {
- fs::remove_file(&entry.path()).ok();
+ fs::remove_file(entry.path()).ok();
}
}
@@ -427,7 +544,7 @@ fn perform_install(
// Also pick up the target version from the RYE_TOOLCHAIN_VERSION
// environment variable.
- let toolchain_version_request = match toolchain_version {
+ let mut toolchain_version_request = match toolchain_version {
Some(version) => Some(version),
None => match env::var("RYE_TOOLCHAIN_VERSION") {
Ok(val) => Some(val.parse()?),
@@ -443,7 +560,7 @@ fn perform_install(
echo!("automatically started the installer for you. For more information");
echo!(
"read {}",
- style("https://rye-up.com/guide/installation/").yellow()
+ style("https://rye.astral.sh/guide/installation/").yellow()
);
}
@@ -481,7 +598,7 @@ fn perform_install(
echo!("enable symlinks. You need to enable this before continuing the setup.");
echo!(
"Learn more at {}",
- style("https://rye-up.com/guide/faq/#windows-developer-mode").yellow()
+ style("https://rye.astral.sh/guide/faq/#windows-developer-mode").yellow()
);
}
@@ -495,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()
- && !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
@@ -534,12 +634,17 @@ fn perform_install(
// can fill in the default.
if !matches!(mode, InstallMode::NoPrompts) {
if toolchain_path.is_none() {
- prompt_for_default_toolchain(
+ // Prompt the user for the default toolchain version.
+ let user_version_request = prompt_for_default_toolchain(
toolchain_version_request
.clone()
.unwrap_or(SELF_PYTHON_TARGET_VERSION),
config_doc,
)?;
+
+ // If the user has selected a toolchain version, we should use that as the default,
+ // unless the `RYE_TOOLCHAIN_VERSION` environment variable is set.
+ toolchain_version_request = toolchain_version_request.or(Some(user_version_request));
} else {
prompt_for_toolchain_later = true;
}
@@ -687,7 +792,7 @@ fn add_rye_to_path(mode: &InstallMode, shims: &Path, ask: bool) -> Result<(), Er
);
echo!();
}
- echo!("For more information read https://rye-up.com/guide/installation/");
+ echo!("For more information read https://rye.astral.sh/guide/installation/");
}
}
// On Windows, we add the rye directory to the user's PATH unconditionally.
@@ -701,8 +806,8 @@ fn add_rye_to_path(mode: &InstallMode, shims: &Path, ask: bool) -> Result<(), Er
fn prompt_for_default_toolchain(
default_toolchain: PythonVersionRequest,
- config_doc: &mut toml_edit::Document,
-) -> Result<(), Error> {
+ config_doc: &mut toml_edit::DocumentMut,
+) -> Result {
let choice = dialoguer::Input::with_theme(tui_theme())
.with_prompt("Which version of Python should be used as default toolchain?")
.default(default_toolchain.clone())
@@ -718,7 +823,7 @@ fn prompt_for_default_toolchain(
})
.interact_text()?;
toml::ensure_table(config_doc, "default")["toolchain"] = toml_edit::value(choice.to_string());
- Ok(())
+ Ok(choice)
}
pub fn auto_self_install() -> Result {
diff --git a/rye/src/cli/shim.rs b/rye/src/cli/shim.rs
index b0d7e94a39..c4decafd2b 100644
--- a/rye/src/cli/shim.rs
+++ b/rye/src/cli/shim.rs
@@ -31,7 +31,7 @@ fn detect_shim(args: &[OsString]) -> Option {
// rye is itself placed in the shims folder, so it must not
// detect itself.
- if shim_name == "rye" || shim_name == "rye.exe" {
+ if shim_name.eq_ignore_ascii_case("rye") || shim_name.eq_ignore_ascii_case("rye.exe") {
return None;
}
@@ -132,7 +132,11 @@ fn is_pointless_windows_store_applink(path: &std::path::Path) -> bool {
return false;
}
- let mut path_encoded = path.as_os_str().encode_wide().collect::>();
+ let mut path_encoded = path
+ .as_os_str()
+ .encode_wide()
+ .chain(Some(0))
+ .collect::>();
let reparse_handle = unsafe {
CreateFileW(
path_encoded.as_mut_ptr(),
@@ -183,7 +187,7 @@ fn get_shim_target(
) -> Result