From ae1964935f2095e22311503298441996c371e577 Mon Sep 17 00:00:00 2001
From: Zanie Blue
Date: Fri, 25 Jul 2025 15:40:09 -0500
Subject: [PATCH 01/26] Remove extra newline (#14907)
Fixes https://github.com/astral-sh/uv/pull/14905#discussion_r2231915714
---
crates/uv-settings/src/lib.rs | 1 -
1 file changed, 1 deletion(-)
diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs
index ed226b549..0e3f13f71 100644
--- a/crates/uv-settings/src/lib.rs
+++ b/crates/uv-settings/src/lib.rs
@@ -271,7 +271,6 @@ fn validate_uv_toml(path: &Path, options: &Options) -> Result<(), Error> {
"environments",
));
}
-
if required_environments.is_some() {
return Err(Error::PyprojectOnlyField(
path.to_path_buf(),
From 0a51489ec4ccc863bc84bc8bddd244859defbf96 Mon Sep 17 00:00:00 2001
From: Charlie Marsh
Date: Sat, 26 Jul 2025 00:04:28 -0400
Subject: [PATCH 02/26] Remove resolved TODO in `allowed_indexes` (#14912)
## Summary
This got solved in #14858.
---
crates/uv-distribution-types/src/index_url.rs | 3 ---
1 file changed, 3 deletions(-)
diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs
index a75f1ea1b..e995e059d 100644
--- a/crates/uv-distribution-types/src/index_url.rs
+++ b/crates/uv-distribution-types/src/index_url.rs
@@ -408,9 +408,6 @@ impl<'a> IndexLocations {
} else {
let mut indexes = vec![];
- // TODO(charlie): By only yielding the first default URL, we'll drop credentials if,
- // e.g., an authenticated default URL is provided in a configuration file, but an
- // unauthenticated default URL is present in the receipt.
let mut seen = FxHashSet::default();
let mut default = false;
for index in {
From 8cb36d6f404c4ed93df1a57391627b9b770c56e8 Mon Sep 17 00:00:00 2001
From: konsti
Date: Mon, 28 Jul 2025 14:33:54 +0200
Subject: [PATCH 03/26] Move all retry tests to `network.rs` (#14935)
Retry behavior isn't tied to a specific installation method, but
underlies all of them.
---
crates/uv/tests/it/network.rs | 60 ++++++++++++++++++++++++++++++
crates/uv/tests/it/pip_install.rs | 62 +------------------------------
2 files changed, 61 insertions(+), 61 deletions(-)
diff --git a/crates/uv/tests/it/network.rs b/crates/uv/tests/it/network.rs
index a9376e07e..c27bf5ef2 100644
--- a/crates/uv/tests/it/network.rs
+++ b/crates/uv/tests/it/network.rs
@@ -300,3 +300,63 @@ async fn python_install_io_error() {
Caused by: connection closed before message completed
");
}
+
+#[tokio::test]
+async fn install_http_retries() {
+ let context = TestContext::new("3.12");
+
+ let server = MockServer::start().await;
+
+ // Create a server that always fails, so we can see the number of retries used
+ Mock::given(method("GET"))
+ .respond_with(ResponseTemplate::new(503))
+ .mount(&server)
+ .await;
+
+ uv_snapshot!(context.filters(), context.pip_install()
+ .arg("anyio")
+ .arg("--index")
+ .arg(server.uri())
+ .env(EnvVars::UV_HTTP_RETRIES, "foo"), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: Failed to parse `UV_HTTP_RETRIES`
+ Caused by: invalid digit found in string
+ "
+ );
+
+ uv_snapshot!(context.filters(), context.pip_install()
+ .arg("anyio")
+ .arg("--index")
+ .arg(server.uri())
+ .env(EnvVars::UV_HTTP_RETRIES, "999999999999"), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: Failed to parse `UV_HTTP_RETRIES`
+ Caused by: number too large to fit in target type
+ "
+ );
+
+ uv_snapshot!(context.filters(), context.pip_install()
+ .arg("anyio")
+ .arg("--index")
+ .arg(server.uri())
+ .env(EnvVars::UV_HTTP_RETRIES, "5")
+ .env(EnvVars::UV_TEST_NO_HTTP_RETRY_DELAY, "true"), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: Request failed after 5 retries
+ Caused by: Failed to fetch: `http://[LOCALHOST]/anyio/`
+ Caused by: HTTP status server error (503 Service Unavailable) for url (http://[LOCALHOST]/anyio/)
+ "
+ );
+}
diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs
index 7a48b61fd..9ee284f3d 100644
--- a/crates/uv/tests/it/pip_install.rs
+++ b/crates/uv/tests/it/pip_install.rs
@@ -522,66 +522,6 @@ fn install_package() {
context.assert_command("import flask").success();
}
-#[tokio::test]
-async fn install_http_retries() {
- let context = TestContext::new("3.12");
-
- let server = MockServer::start().await;
-
- // Create a server that always fails, so we can see the number of retries used
- Mock::given(method("GET"))
- .respond_with(ResponseTemplate::new(503))
- .mount(&server)
- .await;
-
- uv_snapshot!(context.filters(), context.pip_install()
- .arg("anyio")
- .arg("--index")
- .arg(server.uri())
- .env(EnvVars::UV_HTTP_RETRIES, "foo"), @r"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- error: Failed to parse `UV_HTTP_RETRIES`
- Caused by: invalid digit found in string
- "
- );
-
- uv_snapshot!(context.filters(), context.pip_install()
- .arg("anyio")
- .arg("--index")
- .arg(server.uri())
- .env(EnvVars::UV_HTTP_RETRIES, "999999999999"), @r"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- error: Failed to parse `UV_HTTP_RETRIES`
- Caused by: number too large to fit in target type
- "
- );
-
- uv_snapshot!(context.filters(), context.pip_install()
- .arg("anyio")
- .arg("--index")
- .arg(server.uri())
- .env(EnvVars::UV_HTTP_RETRIES, "5")
- .env(EnvVars::UV_TEST_NO_HTTP_RETRY_DELAY, "true"), @r"
- success: false
- exit_code: 2
- ----- stdout -----
-
- ----- stderr -----
- error: Request failed after 5 retries
- Caused by: Failed to fetch: `http://[LOCALHOST]/anyio/`
- Caused by: HTTP status server error (503 Service Unavailable) for url (http://[LOCALHOST]/anyio/)
- "
- );
-}
-
/// Install a package from a `requirements.txt` into a virtual environment.
#[test]
fn install_requirements_txt() -> Result<()> {
@@ -11842,7 +11782,7 @@ fn strip_shebang_arguments() -> Result<()> {
[tool.setuptools]
packages = ["shebang_test"]
-
+
[tool.setuptools.data-files]
"scripts" = ["scripts/custom_script", "scripts/custom_gui_script"]
"#})?;
From ecbe32a4b5cf6e5c120675cad29dbbe727b9516a Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 28 Jul 2025 12:44:37 +0000
Subject: [PATCH 04/26] Update astral-sh/setup-uv action to v6.4.3 (#14924)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| [astral-sh/setup-uv](https://redirect.github.com/astral-sh/setup-uv) |
action | patch | `v6.4.1` -> `v6.4.3` |
---
> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.
---
### Release Notes
astral-sh/setup-uv (astral-sh/setup-uv)
###
[`v6.4.3`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v6.4.3):
🌈 fix relative paths starting with dots
[Compare
Source](https://redirect.github.com/astral-sh/setup-uv/compare/v6.4.2...v6.4.3)
#### 🐛 Bug fixes
- fix relative paths starting with dots
[@eifinger](https://redirect.github.com/eifinger)
([#500](https://redirect.github.com/astral-sh/setup-uv/issues/500))
###
[`v6.4.2`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v6.4.2):
🌈 Interpret relative inputs as under working-directory
[Compare
Source](https://redirect.github.com/astral-sh/setup-uv/compare/v6.4.1...v6.4.2)
#### Changes
This release will interpret relative paths in inputs as relative
to the value of `working-directory` (default is `${{ github.workspace
}}`) .
This means the following configuration
```yaml
- uses: astral-sh/setup-uv@v6
with:
working-directory: /my/path
cache-dependency-glob: uv.lock
```
will look for the `cache-dependency-glob` under `/my/path/uv.lock`
#### 🐛 Bug fixes
- interpret relative inputs as under working-directory
[@eifinger](https://redirect.github.com/eifinger)
([#498](https://redirect.github.com/astral-sh/setup-uv/issues/498))
#### 🧰 Maintenance
- chore: update known versions for 0.8.1/0.8.2
@[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#497](https://redirect.github.com/astral-sh/setup-uv/issues/497))
- chore: update known versions for 0.8.0
@[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#491](https://redirect.github.com/astral-sh/setup-uv/issues/491))
---
### Configuration
📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).
🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/uv).
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
.github/workflows/ci.yml | 10 +++++-----
.github/workflows/publish-pypi.yml | 4 ++--
.github/workflows/sync-python-releases.yml | 2 +-
3 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a93e00038..e2f5bf1b3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -82,7 +82,7 @@ jobs:
run: rustup component add rustfmt
- name: "Install uv"
- uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1
+ uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: "rustfmt"
run: cargo fmt --all --check
@@ -213,7 +213,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- - uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1
+ - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: "Install required Python versions"
run: uv python install
@@ -249,7 +249,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- - uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1
+ - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: "Install required Python versions"
run: uv python install
@@ -286,7 +286,7 @@ jobs:
run: |
Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.UV_WORKSPACE }}" -Recurse
- - uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1
+ - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: "Install required Python versions"
run: uv python install
@@ -439,7 +439,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- - uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1
+ - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index 7897aa245..d9f85ac14 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: "Install uv"
- uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1
+ uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: wheels_uv-*
@@ -41,7 +41,7 @@ jobs:
id-token: write
steps:
- name: "Install uv"
- uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1
+ uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: wheels_uv_build-*
diff --git a/.github/workflows/sync-python-releases.yml b/.github/workflows/sync-python-releases.yml
index bbc9e7b07..1bea5a353 100644
--- a/.github/workflows/sync-python-releases.yml
+++ b/.github/workflows/sync-python-releases.yml
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- - uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1
+ - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
version: "latest"
enable-cache: true
From a1a17718a912b6c97d9e49d1502de6a758755634 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 28 Jul 2025 07:50:10 -0500
Subject: [PATCH 05/26] Update taiki-e/install-action action to v2.57.1
(#14929)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
|
[taiki-e/install-action](https://redirect.github.com/taiki-e/install-action)
| action | minor | `v2.56.19` -> `v2.57.1` |
---
> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.
---
### Release Notes
taiki-e/install-action (taiki-e/install-action)
###
[`v2.57.1`](https://redirect.github.com/taiki-e/install-action/blob/HEAD/CHANGELOG.md#100---2021-12-30)
[Compare
Source](https://redirect.github.com/taiki-e/install-action/compare/v2.57.0...v2.57.1)
Initial release
[Unreleased]:
https://redirect.github.com/taiki-e/install-action/compare/v2.57.1...HEAD
[2.57.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.57.0...v2.57.1
[2.57.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.24...v2.57.0
[2.56.24]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.23...v2.56.24
[2.56.23]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.22...v2.56.23
[2.56.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.21...v2.56.22
[2.56.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.20...v2.56.21
[2.56.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.19...v2.56.20
[2.56.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.18...v2.56.19
[2.56.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.17...v2.56.18
[2.56.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.16...v2.56.17
[2.56.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.15...v2.56.16
[2.56.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.14...v2.56.15
[2.56.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.13...v2.56.14
[2.56.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.12...v2.56.13
[2.56.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.11...v2.56.12
[2.56.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.10...v2.56.11
[2.56.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.9...v2.56.10
[2.56.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.8...v2.56.9
[2.56.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.7...v2.56.8
[2.56.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.6...v2.56.7
[2.56.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.5...v2.56.6
[2.56.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.4...v2.56.5
[2.56.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.3...v2.56.4
[2.56.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.2...v2.56.3
[2.56.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.1...v2.56.2
[2.56.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.0...v2.56.1
[2.56.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.55.4...v2.56.0
[2.55.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.55.3...v2.55.4
[2.55.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.55.2...v2.55.3
[2.55.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.55.1...v2.55.2
[2.55.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.55.0...v2.55.1
[2.55.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.54.3...v2.55.0
[2.54.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.54.2...v2.54.3
[2.54.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.54.1...v2.54.2
[2.54.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.54.0...v2.54.1
[2.54.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.53.2...v2.54.0
[2.53.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.53.1...v2.53.2
[2.53.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.53.0...v2.53.1
[2.53.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.52.8...v2.53.0
[2.52.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.52.7...v2.52.8
[2.52.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.52.6...v2.52.7
[2.52.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.52.5...v2.52.6
[2.52.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.52.4...v2.52.5
[2.52.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.52.3...v2.52.4
[2.52.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.52.2...v2.52.3
[2.52.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.52.1...v2.52.2
[2.52.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.52.0...v2.52.1
[2.52.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.51.3...v2.52.0
[2.51.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.51.2...v2.51.3
[2.51.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.51.1...v2.51.2
[2.51.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.51.0...v2.51.1
[2.51.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.10...v2.51.0
[2.50.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.9...v2.50.10
[2.50.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.8...v2.50.9
[2.50.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.7...v2.50.8
[2.50.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.6...v2.50.7
[2.50.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.5...v2.50.6
[2.50.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.4...v2.50.5
[2.50.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.3...v2.50.4
[2.50.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.2...v2.50.3
[2.50.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.1...v2.50.2
[2.50.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.0...v2.50.1
[2.50.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.50...v2.50.0
[2.49.50]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.49...v2.49.50
[2.49.49]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.48...v2.49.49
[2.49.48]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.47...v2.49.48
[2.49.47]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.46...v2.49.47
[2.49.46]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.45...v2.49.46
[2.49.45]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.44...v2.49.45
[2.49.44]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.43...v2.49.44
[2.49.43]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.42...v2.49.43
[2.49.42]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.41...v2.49.42
[2.49.41]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.40...v2.49.41
[2.49.40]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.39...v2.49.40
[2.49.39]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.38...v2.49.39
[2.49.38]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.37...v2.49.38
[2.49.37]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.36...v2.49.37
[2.49.36]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.35...v2.49.36
[2.49.35]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.34...v2.49.35
[2.49.34]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.33...v2.49.34
[2.49.33]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.32...v2.49.33
[2.49.32]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.31...v2.49.32
[2.49.31]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.30...v2.49.31
[2.49.30]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.29...v2.49.30
[2.49.29]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.28...v2.49.29
[2.49.28]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.27...v2.49.28
[2.49.27]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.26...v2.49.27
[2.49.26]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.25...v2.49.26
[2.49.25]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.24...v2.49.25
[2.49.24]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.23...v2.49.24
[2.49.23]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.22...v2.49.23
[2.49.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.21...v2.49.22
[2.49.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.20...v2.49.21
[2.49.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.19...v2.49.20
[2.49.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.18...v2.49.19
[2.49.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.17...v2.49.18
[2.49.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.16...v2.49.17
[2.49.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.15...v2.49.16
[2.49.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.14...v2.49.15
[2.49.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.13...v2.49.14
[2.49.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.12...v2.49.13
[2.49.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.11...v2.49.12
[2.49.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.10...v2.49.11
[2.49.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.9...v2.49.10
[2.49.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.8...v2.49.9
[2.49.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.7...v2.49.8
[2.49.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.6...v2.49.7
[2.49.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.5...v2.49.6
[2.49.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.4...v2.49.5
[2.49.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.3...v2.49.4
[2.49.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.2...v2.49.3
[2.49.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.1...v2.49.2
[2.49.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.0...v2.49.1
[2.49.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.22...v2.49.0
[2.48.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.21...v2.48.22
[2.48.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.20...v2.48.21
[2.48.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.19...v2.48.20
[2.48.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.18...v2.48.19
[2.48.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.17...v2.48.18
[2.48.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.16...v2.48.17
[2.48.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.15...v2.48.16
[2.48.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.14...v2.48.15
[2.48.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.13...v2.48.14
[2.48.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.12...v2.48.13
[2.48.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.11...v2.48.12
[2.48.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.10...v2.48.11
[2.48.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.9...v2.48.10
[2.48.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.8...v2.48.9
[2.48.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.7...v2.48.8
[2.48.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.6...v2.48.7
[2.48.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.5...v2.48.6
[2.48.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.4...v2.48.5
[2.48.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.3...v2.48.4
[2.48.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.2...v2.48.3
[2.48.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.1...v2.48.2
[2.48.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.0...v2.48.1
[2.48.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.32...v2.48.0
[2.47.32]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.31...v2.47.32
[2.47.31]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.30...v2.47.31
[2.47.30]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.29...v2.47.30
[2.47.29]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.28...v2.47.29
[2.47.28]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.27...v2.47.28
[2.47.27]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.26...v2.47.27
[2.47.26]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.25...v2.47.26
[2.47.25]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.24...v2.47.25
[2.47.24]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.23...v2.47.24
[2.47.23]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.22...v2.47.23
[2.47.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.21...v2.47.22
[2.47.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.20...v2.47.21
[2.47.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.19...v2.47.20
[2.47.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.18...v2.47.19
[2.47.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.17...v2.47.18
[2.47.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.16...v2.47.17
[2.47.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.15...v2.47.16
[2.47.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.14...v2.47.15
[2.47.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.13...v2.47.14
[2.47.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.12...v2.47.13
[2.47.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.11...v2.47.12
[2.47.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.10...v2.47.11
[2.47.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.9...v2.47.10
[2.47.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.8...v2.47.9
[2.47.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.7...v2.47.8
[2.47.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.6...v2.47.7
[2.47.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.5...v2.47.6
[2.47.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.4...v2.47.5
[2.47.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.3...v2.47.4
[2.47.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.2...v2.47.3
[2.47.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.1...v2.47.2
[2.47.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.0...v2.47.1
[2.47.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.20...v2.47.0
[2.46.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.19...v2.46.20
[2.46.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.18...v2.46.19
[2.46.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.17...v2.46.18
[2.46.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.16...v2.46.17
[2.46.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.15...v2.46.16
[2.46.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.14...v2.46.15
[2.46.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.13...v2.46.14
[2.46.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.12...v2.46.13
[2.46.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.11...v2.46.12
[2.46.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.10...v2.46.11
[2.46.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.9...v2.46.10
[2.46.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.8...v2.46.9
[2.46.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.7...v2.46.8
[2.46.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.6...v2.46.7
[2.46.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.5...v2.46.6
[2.46.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.4...v2.46.5
[2.46.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.3...v2.46.4
[2.46.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.2...v2.46.3
[2.46.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.1...v2.46.2
[2.46.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.0...v2.46.1
[2.46.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.15...v2.46.0
[2.45.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.14...v2.45.15
[2.45.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.13...v2.45.14
[2.45.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.12...v2.45.13
[2.45.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.11...v2.45.12
[2.45.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.10...v2.45.11
[2.45.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.9...v2.45.10
[2.45.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.8...v2.45.9
[2.45.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.7...v2.45.8
[2.45.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.6...v2.45.7
[2.45.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.5...v2.45.6
[2.45.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.4...v2.45.5
[2.45.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.3...v2.45.4
[2.45.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.2...v2.45.3
[2.45.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.1...v2.45.2
[2.45.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.0...v2.45.1
[2.45.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.72...v2.45.0
[2.44.72]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.71...v2.44.72
[2.44.71]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.70...v2.44.71
[2.44.70]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.69...v2.44.70
[2.44.69]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.68...v2.44.69
[2.44.68]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.67...v2.44.68
[2.44.67]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.66...v2.44.67
[2.44.66]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.65...v2.44.66
[2.44.65]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.64...v2.44.65
[2.44.64]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.63...v2.44.64
[2.44.63]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.62...v2.44.63
[2.44.62]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.61...v2.44.62
[2.44.61]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.60...v2.44.61
[2.44.60]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.59...v2.44.60
[2.44.59]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.58...v2.44.59
[2.44.58]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.57...v2.44.58
[2.44.57]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.56...v2.44.57
[2.44.56]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.55...v2.44.56
[2.44.55]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.54...v2.44.55
[2.44.54]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.53...v2.44.54
[2.44.53]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.52...v2.44.53
[2.44.52]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.51...v2.44.52
[2.44.51]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.50...v2.44.51
[2.44.50]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.49...v2.44.50
[2.44.49]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.48...v2.44.49
[2.44.48]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.47...v2.44.48
[2.44.47]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.46...v2.44.47
[2.44.46]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.45...v2.44.46
[2.44.45]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.44...v2.44.45
[2.44.44]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.43...v2.44.44
[2.44.43]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.42...v2.44.43
[2.44.42]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.41...v2.44.42
[2.44.41]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.40...v2.44.41
[2.44.40]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.39...v2.44.40
[2.44.39]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.38...v2.44.39
[2.44.38]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.37...v2.44.38
[2.44.37]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.36...v2.44.37
[2.44.36]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.35...v2.44.36
[2.44.35]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.34...v2.44.35
[2.44.34]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.33...v2.44.34
[2.44.33]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.32...v2.44.33
[2.44.32]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.31...v2.44.32
[2.44.31]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.30...v2.44.31
[2.44.30]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.29...v2.44.30
[2.44.29]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.28...v2.44.29
[2.44.28]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.27...v2.44.28
[2.44.27]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.26...v2.44.27
[2.44.26]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.25...v2.44.26
[2.44.25]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.24...v2.44.25
[2.44.24]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.23...v2.44.24
[2.44.23]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.22...v2.44.23
[2.44.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.21...v2.44.22
[2.44.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.20...v2.44.21
[2.44.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.19...v2.44.20
[2.44.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.18...v2.44.19
[2.44.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.17...v2.44.18
[2.44.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.16...v2.44.17
[2.44.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.15...v2.44.16
[2.44.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.14...v2.44.15
[2.44.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.13...v2.44.14
[2.44.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.12...v2.44.13
[2.44.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.11...v2.44.12
[2.44.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.10...v2.44.11
[2.44.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.9...v2.44.10
[2.44.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.8...v2.44.9
[2.44.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.7...v2.44.8
[2.44.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.6...v2.44.7
[2.44.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.5...v2.44.6
[2.44.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.4...v2.44.5
[2.44.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.3...v2.44.4
[2.44.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.2...v2.44.3
[2.44.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.1...v2.44.2
[2.44.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.0...v2.44.1
[2.44.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.43.7...v2.44.0
[2.43.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.43.6...v2.43.7
[2.43.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.43.5...v2.43.6
[2.43.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.43.4...v2.43.5
[2.43.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.43.3...v2.43.4
[2.43.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.43.2...v2.43.3
[2.43.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.43.1...v2.43.2
[2.43.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.43.0...v2.43.1
[2.43.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.42...v2.43.0
[2.42.42]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.41...v2.42.42
[2.42.41]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.40...v2.42.41
[2.42.40]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.39...v2.42.40
[2.42.39]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.38...v2.42.39
[2.42.38]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.37...v2.42.38
[2.42.37]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.36...v2.42.37
[2.42.36]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.35...v2.42.36
[2.42.35]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.34...v2.42.35
[2.42.34]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.33...v2.42.34
[2.42.33]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.32...v2.42.33
[2.42.32]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.31...v2.42.32
[2.42.31]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.30...v2.42.31
[2.42.30]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.29...v2.42.30
[2.42.29]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.28...v2.42.29
[2.42.28]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.27...v2.42.28
[2.42.27]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.26...v2.42.27
[2.42.26]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.25...v2.42.26
[2.42.25]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.24...v2.42.25
[2.42.24]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.23...v2.42.24
[2.42.23]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.22...v2.42.23
[2.42.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.21...v2.42.22
[2.42.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.20...v2.42.21
[2.42.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.19...v2.42.20
[2.42.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.18...v2.42.19
[2.42.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.17...v2.42.18
[2.42.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.16...v2.42.17
[2.42.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.15...v2.42.16
[2.42.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.14...v2.42.15
[2.42.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.13...v2.42.14
[2.42.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.12...v2.42.13
[2.42.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.11...v2.42.12
[2.42.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.10...v2.42.11
[2.42.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.9...v2.42.10
[2.42.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.8...v2.42.9
[2.42.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.7...v2.42.8
[2.42.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.6...v2.42.7
[2.42.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.5...v2.42.6
[2.42.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.4...v2.42.5
[2.42.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.3...v2.42.4
[2.42.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.2...v2.42.3
[2.42.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.1...v2.42.2
[2.42.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.0...v2.42.1
[2.42.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.18...v2.42.0
[2.41.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.17...v2.41.18
[2.41.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.16...v2.41.17
[2.41.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.15...v2.41.16
[2.41.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.14...v2.41.15
[2.41.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.13...v2.41.14
[2.41.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.12...v2.41.13
[2.41.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.11...v2.41.12
[2.41.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.10...v2.41.11
[2.41.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.9...v2.41.10
[2.41.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.8...v2.41.9
[2.41.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.7...v2.41.8
[2.41.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.6...v2.41.7
[2.41.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.5...v2.41.6
[2.41.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.4...v2.41.5
[2.41.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.3...v2.41.4
[2.41.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.2...v2.41.3
[2.41.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.1...v2.41.2
[2.41.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.0...v2.41.1
[2.41.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.40.2...v2.41.0
[2.40.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.40.1...v2.40.2
[2.40.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.40.0...v2.40.1
[2.40.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.39.2...v2.40.0
[2.39.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.39.1...v2.39.2
[2.39.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.39.0...v2.39.1
[2.39.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.38.7...v2.39.0
[2.38.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.38.6...v2.38.7
[2.38.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.38.5...v2.38.6
[2.38.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.38.4...v2.38.5
[2.38.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.38.3...v2.38.4
[2.38.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.38.2...v2.38.3
[2.38.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.38.1...v2.38.2
[2.38.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.38.0...v2.38.1
[2.38.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.37.0...v2.38.0
[2.37.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.36.0...v2.37.0
[2.36.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.35.0...v2.36.0
[2.35.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.34.3...v2.35.0
[2.34.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.34.2...v2.34.3
[2.34.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.34.1...v2.34.2
[2.34.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.34.0...v2.34.1
[2.34.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.36...v2.34.0
[2.33.36]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.35...v2.33.36
[2.33.35]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.34...v2.33.35
[2.33.34]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.33...v2.33.34
[2.33.33]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.32...v2.33.33
[2.33.32]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.31...v2.33.32
[2.33.31]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.30...v2.33.31
[2.33.30]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.29...v2.33.30
[2.33.29]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.28...v2.33.29
[2.33.28]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.27...v2.33.28
[2.33.27]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.26...v2.33.27
[2.33.26]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.25...v2.33.26
[2.33.25]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.24...v2.33.25
[2.33.24]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.23...v2.33.24
[2.33.23]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.22...v2.33.23
[2.33.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.21...v2.33.22
[2.33.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.20...v2.33.21
[2.33.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.19...v2.33.20
[2.33.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.18...v2.33.19
[2.33.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.17...v2.33.18
[2.33.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.16...v2.33.17
[2.33.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.15...v2.33.16
[2.33.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.14...v2.33.15
[2.33.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.13...v2.33.14
[2.33.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.12...v2.33.13
[2.33.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.11...v2.33.12
[2.33.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.10...v2.33.11
[2.33.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.9...v2.33.10
[2.33.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.8...v2.33.9
[2.33.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.7...v2.33.8
[2.33.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.6...v2.33.7
[2.33.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.5...v2.33.6
[2.33.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.4...v2.33.5
[2.33.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.3...v2.33.4
[2.33.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.2...v2.33.3
[2.33.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.1...v2.33.2
[2.33.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.0...v2.33.1
[2.33.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.20...v2.33.0
[2.32.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.19...v2.32.20
[2.32.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.18...v2.32.19
[2.32.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.17...v2.32.18
[2.32.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.16...v2.32.17
[2.32.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.15...v2.32.16
[2.32.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.14...v2.32.15
[2.32.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.13...v2.32.14
[2.32.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.12...v2.32.13
[2.32.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.11...v2.32.12
[2.32.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.10...v2.32.11
[2.32.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.9...v2.32.10
[2.32.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.8...v2.32.9
[2.32.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.7...v2.32.8
[2.32.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.6...v2.32.7
[2.32.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.5...v2.32.6
[2.32.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.4...v2.32.5
[2.32.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.3...v2.32.4
[2.32.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.2...v2.32.3
[2.32.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.1...v2.32.2
[2.32.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.0...v2.32.1
[2.32.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.31.3...v2.32.0
[2.31.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.31.2...v2.31.3
[2.31.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.31.1...v2.31.2
[2.31.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.31.0...v2.31.1
[2.31.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.30.0...v2.31.0
[2.30.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.29.8...v2.30.0
[2.29.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.29.7...v2.29.8
[2.29.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.29.6...v2.29.7
[2.29.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.29.5...v2.29.6
[2.29.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.29.4...v2.29.5
[2.29.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.29.3...v2.29.4
[2.29.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.29.2...v2.29.3
[2.29.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.29.1...v2.29.2
[2.29.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.29.0...v2.29.1
[2.29.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.16...v2.29.0
[2.28.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.15...v2.28.16
[2.28.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.14...v2.28.15
[2.28.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.13...v2.28.14
[2.28.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.12...v2.28.13
[2.28.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.11...v2.28.12
[2.28.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.10...v2.28.11
[2.28.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.9...v2.28.10
[2.28.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.8...v2.28.9
[2.28.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.7...v2.28.8
[2.28.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.6...v2.28.7
[2.28.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.5...v2.28.6
[2.28.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.4...v2.28.5
[2.28.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.3...v2.28.4
[2.28.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.2...v2.28.3
[2.28.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.1...v2.28.2
[2.28.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.0...v2.28.1
[2.28.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.15...v2.28.0
[2.27.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.14...v2.27.15
[2.27.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.13...v2.27.14
[2.27.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.12...v2.27.13
[2.27.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.11...v2.27.12
[2.27.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.10...v2.27.11
[2.27.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.9...v2.27.10
[2.27.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.8...v2.27.9
[2.27.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.7...v2.27.8
[2.27.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.6...v2.27.7
[2.27.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.5...v2.27.6
[2.27.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.4...v2.27.5
[2.27.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.3...v2.27.4
[2.27.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.2...v2.27.3
[2.27.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.1...v2.27.2
[2.27.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.0...v2.27.1
[2.27.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.20...v2.27.0
[2.26.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.19...v2.26.20
[2.26.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.18...v2.26.19
[2.26.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.17...v2.26.18
[2.26.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.16...v2.26.17
[2.26.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.15...v2.26.16
[2.26.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.14...v2.26.15
[2.26.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.13...v2.26.14
[2.26.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.12...v2.26.13
[2.26.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.11...v2.26.12
[2.26.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.10...v2.26.11
[2.26.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.9...v2.26.10
[2.26.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.8...v2.26.9
[2.26.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.7...v2.26.8
[2.26.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.6...v2.26.7
[2.26.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.5...v2.26.6
[2.26.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.4...v2.26.5
[2.26.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.3...v2.26.4
[2.26.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.2...v2.26.3
[2.26.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.1...v2.26.2
[2.26.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.0...v2.26.1
[2.26.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.11...v2.26.0
[2.25.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.10...v2.25.11
[2.25.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.9...v2.25.10
[2.25.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.8...v2.25.9
[2.25.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.7...v2.25.8
[2.25.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.6...v2.25.7
[2.25.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.5...v2.25.6
[2.25.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.4...v2.25.5
[2.25.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.3...v2.25.4
[2.25.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.2...v2.25.3
[2.25.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.1...v2.25.2
[2.25.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.0...v2.25.1
[2.25.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.24.4...v2.25.0
[2.24.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.24.3...v2.24.4
[2.24.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.24.2...v2.24.3
[2.24.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.24.1...v2.24.2
[2.24.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.24.0...v2.24.1
[2.24.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.9...v2.24.0
[2.23.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.8...v2.23.9
[2.23.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.7...v2.23.8
[2.23.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.6...v2.23.7
[2.23.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.5...v2.23.6
[2.23.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.4...v2.23.5
[2.23.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.3...v2.23.4
[2.23.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.2...v2.23.3
[2.23.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.1...v2.23.2
[2.23.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.0...v2.23.1
[2.23.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.10...v2.23.0
[2.22.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.9...v2.22.10
[2.22.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.8...v2.22.9
[2.22.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.7...v2.22.8
[2.22.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.6...v2.22.7
[2.22.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.5...v2.22.6
[2.22.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.4...v2.22.5
[2.22.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.3...v2.22.4
[2.22.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.2...v2.22.3
[2.22.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.1...v2.22.2
[2.22.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.0...v2.22.1
[2.22.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.27...v2.22.0
[2.21.27]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.26...v2.21.27
[2.21.26]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.25...v2.21.26
[2.21.25]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.24...v2.21.25
[2.21.24]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.23...v2.21.24
[2.21.23]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.22...v2.21.23
[2.21.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.21...v2.21.22
[2.21.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.20...v2.21.21
[2.21.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.19...v2.21.20
[2.21.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.18...v2.21.19
[2.21.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.17...v2.21.18
[2.21.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.16...v2.21.17
[2.21.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.15...v2.21.16
[2.21.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.14...v2.21.15
[2.21.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.13...v2.21.14
[2.21.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.12...v2.21.13
[2.21.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.11...v2.21.12
[2.21.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.10...v2.21.11
[2.21.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.9...v2.21.10
[2.21.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.8...v2.21.9
[2.21.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.7...v2.21.8
[2.21.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.6...v2.21.7
[2.21.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.5...v2.21.6
[2.21.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.4...v2.21.5
[2.21.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.3...v2.21.4
[2.21.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.2...v2.21.3
[2.21.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.1...v2.21.2
[2.21.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.0...v2.21.1
[2.21.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.17...v2.21.0
[2.20.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.16...v2.20.17
[2.20.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.15...v2.20.16
[2.20.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.14...v2.20.15
[2.20.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.13...v2.20.14
[2.20.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.12...v2.20.13
[2.20.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.11...v2.20.12
[2.20.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.10...v2.20.11
[2.20.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.9...v2.20.10
[2.20.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.8...v2.20.9
[2.20.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.7...v2.20.8
[2.20.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.6...v2.20.7
[2.20.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.5...v2.20.6
[2.20.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.4...v2.20.5
[2.20.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.3...v2.20.4
[2.20.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.2...v2.20.3
[2.20.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.1...v2.20.2
[2.20.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.0...v2.20.1
[2.20.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.19.4...v2.20.0
[2.19.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.19.3...v2.19.4
[2.19.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.19.2...v2.19.3
[2.19.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.19.1...v2.19.2
[2.19.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.19.0...v2.19.1
[2.19.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.17...v2.19.0
[2.18.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.16...v2.18.17
[2.18.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.15...v2.18.16
[2.18.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.14...v2.18.15
[2.18.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.13...v2.18.14
[2.18.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.12...v2.18.13
[2.18.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.11...v2.18.12
[2.18.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.10...v2.18.11
[2.18.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.9...v2.18.10
[2.18.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.8...v2.18.9
[2.18.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.7...v2.18.8
[2.18.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.6...v2.18.7
[2.18.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.5...v2.18.6
[2.18.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.4...v2.18.5
[2.18.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.3...v2.18.4
[2.18.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.2...v2.18.3
[2.18.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.1...v2.18.2
[2.18.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.0...v2.18.1
[2.18.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.17.8...v2.18.0
[2.17.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.17.7...v2.17.8
[2.17.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.17.6...v2.17.7
[2.17.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.17.5...v2.17.6
[2.17.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.17.4...v2.17.5
[2.17.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.17.3...v2.17.4
[2.17.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.17.2...v2.17.3
[2.17.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.17.1...v2.17.2
[2.17.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.17.0...v2.17.1
[2.17.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.16.5...v2.17.0
[2.16.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.16.4...v2.16.5
[2.16.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.16.3...v2.16.4
[2.16.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.16.2...v2.16.3
[2.16.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.16.1...v2.16.2
[2.16.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.16.0...v2.16.1
[2.16.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.15.6...v2.16.0
[2.15.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.15.5...v2.15.6
[2.15.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.15.4...v2.15.5
[2.15.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.15.3...v2.15.4
[2.15.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.15.2...v2.15.3
[2.15.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.15.1...v2.15.2
[2.15.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.15.0...v2.15.1
[2.15.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.14.3...v2.15.0
[2.14.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.14.2...v2.14.3
[2.14.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.14.1...v2.14.2
[2.14.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.14.0...v2.14.1
[2.14.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.13.6...v2.14.0
[2.13.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.13.5...v2.13.6
[2.13.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.13.4...v2.13.5
[2.13.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.13.3...v2.13.4
[2.13.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.13.2...v2.13.3
[2.13.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.13.1...v2.13.2
[2.13.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.13.0...v2.13.1
[2.13.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.12.23...v2.13.0
[2.12.23]:
https://redirect.github.com/taiki-e/install-action/compare/v2.12.22...v2.12.23
[2.12.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.12.21...v2.12.22
[2.12.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.12.20...v2.12.21
[2.12.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.12.19...v2.12.20
[2.12.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.12.18...v2.12.19
[2.12.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.12.17...v2.12.18
[2.12.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.12.16...v2.12.17
[2.12.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.12.15...v2.12.16
[2.12.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.12.14...v2.12.15
[2.12.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.12.13...v2.12.14
[2.12.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.12.12...v2.12.13
[2.12.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.12.11...v2.12.12
[2.12.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.12.10...v2.12.11
[2.12.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.12.9...v2.12.10
[2.12.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.12.8...v2.12.9
[2.12.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.12.7...v2.12.8
[2.
---
### Configuration
📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).
🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/uv).
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
.github/workflows/ci.yml | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e2f5bf1b3..d0aecc8d9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -188,7 +188,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: "Install cargo shear"
- uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
+ uses: taiki-e/install-action@a416ddeedbd372e614cc1386e8b642692f66865e # v2.57.1
with:
tool: cargo-shear
- run: cargo shear
@@ -218,7 +218,7 @@ jobs:
run: uv python install
- name: "Install cargo nextest"
- uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
+ uses: taiki-e/install-action@a416ddeedbd372e614cc1386e8b642692f66865e # v2.57.1
with:
tool: cargo-nextest
@@ -254,7 +254,7 @@ jobs:
run: uv python install
- name: "Install cargo nextest"
- uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
+ uses: taiki-e/install-action@a416ddeedbd372e614cc1386e8b642692f66865e # v2.57.1
with:
tool: cargo-nextest
@@ -299,7 +299,7 @@ jobs:
run: rustup show
- name: "Install cargo nextest"
- uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
+ uses: taiki-e/install-action@a416ddeedbd372e614cc1386e8b642692f66865e # v2.57.1
with:
tool: cargo-nextest
@@ -352,7 +352,7 @@ jobs:
rustup component add rust-src --target ${{ matrix.target-arch }}-pc-windows-msvc
- name: "Install cargo-bloat"
- uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
+ uses: taiki-e/install-action@a416ddeedbd372e614cc1386e8b642692f66865e # v2.57.1
with:
tool: cargo-bloat
@@ -2523,7 +2523,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
- uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
+ uses: taiki-e/install-action@a416ddeedbd372e614cc1386e8b642692f66865e # v2.57.1
with:
tool: cargo-codspeed
@@ -2560,7 +2560,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
- uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
+ uses: taiki-e/install-action@a416ddeedbd372e614cc1386e8b642692f66865e # v2.57.1
with:
tool: cargo-codspeed
From a5fdc5319d5e96157b52eaf23a64063c34113376 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 28 Jul 2025 07:50:32 -0500
Subject: [PATCH 06/26] Update CodSpeedHQ/action action to v3.8.0 (#14926)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| [CodSpeedHQ/action](https://redirect.github.com/CodSpeedHQ/action) |
action | minor | `v3.7.0` -> `v3.8.0` |
---
> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.
---
### Release Notes
CodSpeedHQ/action (CodSpeedHQ/action)
###
[`v3.8.0`](https://redirect.github.com/CodSpeedHQ/action/releases/tag/v3.8.0)
[Compare
Source](https://redirect.github.com/CodSpeedHQ/action/compare/v3.7.0...v3.8.0)
##### What's Changed
##### 🐛 Bug Fixes
- Adjust offset for symbols of module loaded at preferred base by
[@not-matthias](https://redirect.github.com/not-matthias) in
[#97](https://redirect.github.com/CodSpeedHQ/runner/pull/97)
- Run with --scope to allow perf to trace the benchmark process by
[@not-matthias](https://redirect.github.com/not-matthias)
- Run with bash to support complex scripts by
[@not-matthias](https://redirect.github.com/not-matthias)
- Execute pre- and post-bench scripts for non-perf walltime runner by
[@not-matthias](https://redirect.github.com/not-matthias) in
[#96](https://redirect.github.com/CodSpeedHQ/runner/pull/96)
##### 🏗️ Refactor
- Process memory mappings in separate function by
[@not-matthias](https://redirect.github.com/not-matthias)
##### ⚙️ Internals
- Add debug logs for perf.map collection by
[@not-matthias](https://redirect.github.com/not-matthias)
- Add complex cmd and env tests by
[@not-matthias](https://redirect.github.com/not-matthias)
**Full Changelog**:
https://github.com/CodSpeedHQ/action/compare/v3.7.0...v3.8.0
**Full Runner Changelog**:
https://github.com/CodSpeedHQ/runner/blob/main/CHANGELOG.md
---
### Configuration
📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).
🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/uv).
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
.github/workflows/ci.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d0aecc8d9..23efa58d2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2539,7 +2539,7 @@ jobs:
run: cargo codspeed build --profile profiling --features codspeed -p uv-bench
- name: "Run benchmarks"
- uses: CodSpeedHQ/action@c28fe9fbe7d57a3da1b7834ae3761c1d8217612d # v3.7.0
+ uses: CodSpeedHQ/action@0b6e7a3d96c9d2a6057e7bcea6b45aaf2f7ce60b # v3.8.0
with:
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
@@ -2576,7 +2576,7 @@ jobs:
run: cargo codspeed build --profile profiling --features codspeed -p uv-bench
- name: "Run benchmarks"
- uses: CodSpeedHQ/action@c28fe9fbe7d57a3da1b7834ae3761c1d8217612d # v3.7.0
+ uses: CodSpeedHQ/action@0b6e7a3d96c9d2a6057e7bcea6b45aaf2f7ce60b # v3.8.0
with:
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
From d71c65abd451ce3ebc18dd2a4f271f64358ff52a Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 28 Jul 2025 08:09:38 -0500
Subject: [PATCH 07/26] Update Rust crate tokio to v1.47.0 (#14928)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| [tokio](https://tokio.rs)
([source](https://redirect.github.com/tokio-rs/tokio)) |
workspace.dependencies | minor | `1.46.1` -> `1.47.0` |
---
> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.
---
### Release Notes
tokio-rs/tokio (tokio)
###
[`v1.47.0`](https://redirect.github.com/tokio-rs/tokio/releases/tag/tokio-1.47.0):
Tokio v1.47.0
[Compare
Source](https://redirect.github.com/tokio-rs/tokio/compare/tokio-1.46.1...tokio-1.47.0)
##### 1.47.0 (July 25th, 2025)
This release adds `poll_proceed` and `cooperative` to the `coop` module
for
cooperative scheduling, adds `SetOnce` to the `sync` module which
provides
similar functionality to \[`std::sync::OnceLock`], and adds a new method
`sync::Notify::notified_owned()` which returns an `OwnedNotified`
without
a lifetime parameter.
##### Added
- coop: add `cooperative` and `poll_proceed` ([#7405])
- sync: add `SetOnce` ([#7418])
- sync: add `sync::Notify::notified_owned()` ([#7465])
##### Changed
- deps: upgrade windows-sys 0.52 → 0.59
(\[[#7117](https://redirect.github.com/tokio-rs/tokio/issues/7117)])
- deps: update to socket2 v0.6
(\[[#7443](https://redirect.github.com/tokio-rs/tokio/issues/7443)])
- sync: improve `AtomicWaker::wake` performance ([#7450])
##### Documented
- metrics: fix listed feature requirements for some metrics
([#7449])
- runtime: improve safety comments of `Readiness<'_>` ([#7415])
[#7405]: https://redirect.github.com/tokio-rs/tokio/pull/7405
[#7415]: https://redirect.github.com/tokio-rs/tokio/pull/7415
[#7418]: https://redirect.github.com/tokio-rs/tokio/pull/7418
[#7449]: https://redirect.github.com/tokio-rs/tokio/pull/7449
[#7450]: https://redirect.github.com/tokio-rs/tokio/pull/7450
[#7465]: https://redirect.github.com/tokio-rs/tokio/pull/7465
---
### Configuration
📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).
🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/uv).
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
Cargo.lock | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index a345b9ab5..ebf81521a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -749,7 +749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
- "windows-sys 0.59.0",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -4153,9 +4153,9 @@ source = "git+https://github.com/astral-sh/tl.git?rev=6e25b2ee2513d75385101a8ff9
[[package]]
name = "tokio"
-version = "1.46.1"
+version = "1.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17"
+checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35"
dependencies = [
"backtrace",
"bytes",
@@ -4166,9 +4166,9 @@ dependencies = [
"pin-project-lite",
"signal-hook-registry",
"slab",
- "socket2 0.5.10",
+ "socket2 0.6.0",
"tokio-macros",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -6332,7 +6332,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.48.0",
]
[[package]]
From 8cd8c95071e26c60cad1c396cedfdebf9df4a6f8 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 28 Jul 2025 08:10:06 -0500
Subject: [PATCH 08/26] Update Rust crate criterion to 0.7.0 (#14927)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| [criterion](https://bheisler.github.io/criterion.rs/book/index.html)
([source](https://redirect.github.com/bheisler/criterion.rs)) |
dependencies | minor | `0.6.0` -> `0.7.0` |
---
> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.
---
### Release Notes
bheisler/criterion.rs (criterion)
###
[`v0.7.0`](https://redirect.github.com/bheisler/criterion.rs/blob/HEAD/CHANGELOG.md#070---2025-07-25)
[Compare
Source](https://redirect.github.com/bheisler/criterion.rs/compare/0.6.0...0.7.0)
- Bump version of criterion-plot to align dependencies.
---
### Configuration
📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).
🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/uv).
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
Cargo.lock | 18 ++++++++++++++----
crates/uv-bench/Cargo.toml | 2 +-
2 files changed, 15 insertions(+), 5 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index ebf81521a..ff72f1418 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -716,7 +716,7 @@ dependencies = [
"ciborium",
"clap",
"codspeed",
- "criterion-plot",
+ "criterion-plot 0.5.0",
"is-terminal",
"itertools 0.10.5",
"num-traits",
@@ -843,15 +843,15 @@ dependencies = [
[[package]]
name = "criterion"
-version = "0.6.0"
+version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679"
+checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
- "criterion-plot",
+ "criterion-plot 0.6.0",
"itertools 0.13.0",
"num-traits",
"oorandom",
@@ -873,6 +873,16 @@ dependencies = [
"itertools 0.10.5",
]
+[[package]]
+name = "criterion-plot"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338"
+dependencies = [
+ "cast",
+ "itertools 0.13.0",
+]
+
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
diff --git a/crates/uv-bench/Cargo.toml b/crates/uv-bench/Cargo.toml
index 8c08d4dd2..e2664bf9c 100644
--- a/crates/uv-bench/Cargo.toml
+++ b/crates/uv-bench/Cargo.toml
@@ -43,7 +43,7 @@ uv-workspace = { workspace = true }
anyhow = { workspace = true }
codspeed-criterion-compat = { version = "3.0.2", default-features = false, optional = true }
-criterion = { version = "0.6.0", default-features = false, features = [
+criterion = { version = "0.7.0", default-features = false, features = [
"async_tokio",
] }
jiff = { workspace = true }
From c97d12bcf37526b225d802d9929f632fe519adba Mon Sep 17 00:00:00 2001
From: Zanie Blue
Date: Mon, 28 Jul 2025 08:26:23 -0500
Subject: [PATCH 09/26] Unhide `uv` from `--build-backend` options (#14939)
Closes https://github.com/astral-sh/uv/issues/14921
---
crates/uv-configuration/src/project_build_backend.rs | 6 +-----
docs/reference/cli.md | 1 +
2 files changed, 2 insertions(+), 5 deletions(-)
diff --git a/crates/uv-configuration/src/project_build_backend.rs b/crates/uv-configuration/src/project_build_backend.rs
index c95da6bf8..8c5db4b55 100644
--- a/crates/uv-configuration/src/project_build_backend.rs
+++ b/crates/uv-configuration/src/project_build_backend.rs
@@ -4,11 +4,7 @@
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum ProjectBuildBackend {
- #[cfg_attr(
- feature = "clap",
- value(alias = "uv-build", alias = "uv_build", hide = true)
- )]
- #[cfg_attr(feature = "schemars", schemars(skip))]
+ #[cfg_attr(feature = "clap", value(alias = "uv-build", alias = "uv_build"))]
/// Use uv as the project build backend.
Uv,
#[serde(alias = "hatchling")]
diff --git a/docs/reference/cli.md b/docs/reference/cli.md
index 43f7a2aa7..f761a49f0 100644
--- a/docs/reference/cli.md
+++ b/docs/reference/cli.md
@@ -304,6 +304,7 @@ uv init [OPTIONS] [PATH]
Implicitly sets --package.
May also be set with the UV_INIT_BUILD_BACKEND environment variable.
Possible values:
+uv: Use uv as the project build backend
hatch: Use hatchling as the project build backend
flit: Use flit-core as the project build backend
pdm: Use pdm-backend as the project build backend
From 55df84592223e6d08d96927cdfbfa150f0223e96 Mon Sep 17 00:00:00 2001
From: shikinamiasuka <67509746+yumeminami@users.noreply.github.com>
Date: Mon, 28 Jul 2025 21:56:08 +0800
Subject: [PATCH 10/26] Fix incorrect file permissions in wheel packages
(#14930)
Fixes #14920
## Summary
Problem: When building wheel packages, metadata files (such as RECORD,
METADATA, WHEEL, and
license files) were being created with incorrect Unix permissions
(--w--wx---), lacking
read permissions and having unexpected executable permissions.
Solution: The fix ensures that all metadata files in wheel packages are
created with proper
644 (rw-r--r--) permissions by:
- Adding explicit unix_permissions(0o644) setting in the write_bytes
method for metadata
files
- Updating permission constants to use octal notation for clarity
- Improving code comments to document the permission settings
Impact: This change ensures wheel packages created by uv have standard
file permissions
consistent with other Python build tools like setuptools, improving
compatibility and
following Python packaging best practices.
---
crates/uv-build-backend/src/lib.rs | 2 +-
crates/uv-build-backend/src/wheel.rs | 9 ++++++---
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs
index 5800d04d2..319925b6a 100644
--- a/crates/uv-build-backend/src/lib.rs
+++ b/crates/uv-build-backend/src/lib.rs
@@ -622,7 +622,7 @@ mod tests {
// Check that the wheel is reproducible across platforms.
assert_snapshot!(
format!("{:x}", sha2::Sha256::digest(fs_err::read(&wheel_path).unwrap())),
- @"ac3f68ac448023bca26de689d80401bff57f764396ae802bf4666234740ffbe3"
+ @"342bf60c8406144f459358cde92408686c1631fe22389d042ce80379e589d6ec"
);
assert_snapshot!(build.wheel_contents.join("\n"), @r"
built_by_uv-0.1.0.data/data/
diff --git a/crates/uv-build-backend/src/wheel.rs b/crates/uv-build-backend/src/wheel.rs
index 6eeb899d0..4800bb435 100644
--- a/crates/uv-build-backend/src/wheel.rs
+++ b/crates/uv-build-backend/src/wheel.rs
@@ -621,8 +621,8 @@ impl ZipDirectoryWriter {
path: &str,
executable_bit: bool,
) -> Result, Error> {
- // 644 is the default of the zip crate.
- let permissions = if executable_bit { 775 } else { 664 };
+ // Set file permissions: 644 (rw-r--r--) for regular files, 755 (rwxr-xr-x) for executables
+ let permissions = if executable_bit { 0o755 } else { 0o644 };
let options = zip::write::SimpleFileOptions::default()
.unix_permissions(permissions)
.compression_method(self.compression);
@@ -634,7 +634,10 @@ impl ZipDirectoryWriter {
impl DirectoryWriter for ZipDirectoryWriter {
fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error> {
trace!("Adding {}", path);
- let options = zip::write::SimpleFileOptions::default().compression_method(self.compression);
+ // Set appropriate permissions for metadata files (644 = rw-r--r--)
+ let options = zip::write::SimpleFileOptions::default()
+ .unix_permissions(0o644)
+ .compression_method(self.compression);
self.writer.start_file(path, options)?;
self.writer.write_all(bytes)?;
From 90885fd0d92a230102f9ea329ae7a231a33eae44 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 28 Jul 2025 08:57:33 -0500
Subject: [PATCH 11/26] Update pre-commit hook astral-sh/ruff-pre-commit to
v0.12.5 (#14925)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
|
[astral-sh/ruff-pre-commit](https://redirect.github.com/astral-sh/ruff-pre-commit)
| repository | patch | `v0.12.4` -> `v0.12.5` |
---
> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.
Note: The `pre-commit` manager in Renovate is not supported by the
`pre-commit` maintainers or community. Please do not report any problems
there, instead [create a Discussion in the Renovate
repository](https://redirect.github.com/renovatebot/renovate/discussions/new)
if you have any questions.
---
### Release Notes
astral-sh/ruff-pre-commit (astral-sh/ruff-pre-commit)
###
[`v0.12.5`](https://redirect.github.com/astral-sh/ruff-pre-commit/releases/tag/v0.12.5)
[Compare
Source](https://redirect.github.com/astral-sh/ruff-pre-commit/compare/v0.12.4...v0.12.5)
See: https://github.com/astral-sh/ruff/releases/tag/0.12.5
---
### Configuration
📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).
🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/uv).
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3a8e4a39a..d4925056e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -42,7 +42,7 @@ repos:
types_or: [yaml, json5]
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.12.4
+ rev: v0.12.5
hooks:
- id: ruff-format
- id: ruff
From ac135278c3af16a4bd26ec3c707ea1c85a6d154d Mon Sep 17 00:00:00 2001
From: konsti
Date: Mon, 28 Jul 2025 18:23:39 +0200
Subject: [PATCH 12/26] Better warning chain styling (#14934)
Improve the styling of warning chains for Python installation errors.
Apply the same logic to other internal warning and error formatting
locations.
**Before**
**After**
---
crates/uv-warnings/src/lib.rs | 41 ++++++++++++++
crates/uv/src/commands/publish.rs | 23 +++-----
crates/uv/src/commands/python/install.rs | 72 +++++++++++++-----------
crates/uv/src/commands/tool/upgrade.rs | 20 +++----
crates/uv/tests/it/publish.rs | 7 ++-
5 files changed, 99 insertions(+), 64 deletions(-)
diff --git a/crates/uv-warnings/src/lib.rs b/crates/uv-warnings/src/lib.rs
index 2b664be8d..a076682ef 100644
--- a/crates/uv-warnings/src/lib.rs
+++ b/crates/uv-warnings/src/lib.rs
@@ -1,3 +1,5 @@
+use std::error::Error;
+use std::iter;
use std::sync::atomic::AtomicBool;
use std::sync::{LazyLock, Mutex};
@@ -6,6 +8,7 @@ use std::sync::{LazyLock, Mutex};
pub use anstream;
#[doc(hidden)]
pub use owo_colors;
+use owo_colors::{DynColor, OwoColorize};
use rustc_hash::FxHashSet;
/// Whether user-facing warnings are enabled.
@@ -56,3 +59,41 @@ macro_rules! warn_user_once {
}
}};
}
+
+/// Format an error or warning chain.
+///
+/// # Example
+///
+/// ```text
+/// error: Failed to install app
+/// Caused By: Failed to install dependency
+/// Caused By: Error writing failed `/home/ferris/deps/foo`: Permission denied
+/// ```
+///
+/// ```text
+/// warning: Failed to create registry entry for Python 3.12
+/// Caused By: Security policy forbids chaining registry entries
+/// ```
+pub fn write_error_chain(
+ err: &dyn Error,
+ mut stream: impl std::fmt::Write,
+ level: impl AsRef,
+ color: impl DynColor + Copy,
+) -> std::fmt::Result {
+ writeln!(
+ &mut stream,
+ "{}{} {}",
+ level.as_ref().color(color).bold(),
+ ":".bold(),
+ err.to_string().trim()
+ )?;
+ for source in iter::successors(err.source(), |&err| err.source()) {
+ writeln!(
+ &mut stream,
+ " {}: {}",
+ "Caused by".color(color).bold(),
+ source.to_string().trim()
+ )?;
+ }
+ Ok(())
+}
diff --git a/crates/uv/src/commands/publish.rs b/crates/uv/src/commands/publish.rs
index e7f5e00a2..083b6503a 100644
--- a/crates/uv/src/commands/publish.rs
+++ b/crates/uv/src/commands/publish.rs
@@ -1,11 +1,10 @@
use std::fmt::Write;
-use std::iter;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result, bail};
use console::Term;
-use owo_colors::OwoColorize;
+use owo_colors::{AnsiColors, OwoColorize};
use tokio::sync::Semaphore;
use tracing::{debug, info};
use uv_auth::Credentials;
@@ -17,7 +16,7 @@ use uv_publish::{
CheckUrlClient, TrustedPublishResult, check_trusted_publishing, files_for_publishing, upload,
};
use uv_redacted::DisplaySafeUrl;
-use uv_warnings::warn_user_once;
+use uv_warnings::{warn_user_once, write_error_chain};
use crate::commands::reporters::PublishReporter;
use crate::commands::{ExitStatus, human_readable_bytes};
@@ -274,19 +273,15 @@ async fn gather_credentials(
fetching the trusted publishing token. If you don't want to use trusted \
publishing, you can ignore this error, but you need to provide credentials."
)?;
- writeln!(
+
+ write_error_chain(
+ anyhow::Error::from(err)
+ .context("Trusted publishing failed")
+ .as_ref(),
printer.stderr(),
- "{}: {err}",
- "Trusted publishing error".red().bold()
+ "error",
+ AnsiColors::Red,
)?;
- for source in iter::successors(std::error::Error::source(&err), |&err| err.source()) {
- writeln!(
- printer.stderr(),
- " {}: {}",
- "Caused by".red().bold(),
- source.to_string().trim()
- )?;
- }
}
}
diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs
index f2082ce8a..02e4c27e5 100644
--- a/crates/uv/src/commands/python/install.rs
+++ b/crates/uv/src/commands/python/install.rs
@@ -10,7 +10,7 @@ use futures::StreamExt;
use futures::stream::FuturesUnordered;
use indexmap::IndexSet;
use itertools::{Either, Itertools};
-use owo_colors::OwoColorize;
+use owo_colors::{AnsiColors, OwoColorize};
use rustc_hash::{FxHashMap, FxHashSet};
use tracing::{debug, trace};
@@ -30,7 +30,7 @@ use uv_python::{
};
use uv_shell::Shell;
use uv_trampoline_builder::{Launcher, LauncherKind};
-use uv_warnings::warn_user;
+use uv_warnings::{warn_user, write_error_chain};
use crate::commands::python::{ChangeEvent, ChangeEventKind};
use crate::commands::reporters::PythonDownloadReporter;
@@ -139,7 +139,7 @@ impl Changelog {
enum InstallErrorKind {
DownloadUnpack,
Bin,
- #[cfg(windows)]
+ #[cfg_attr(not(windows), allow(dead_code))]
Registry,
}
@@ -667,7 +667,6 @@ pub(crate) async fn install(
// to warn
let fatal = !errors.iter().all(|(kind, _, _)| match kind {
InstallErrorKind::Bin => bin.is_none(),
- #[cfg(windows)]
InstallErrorKind::Registry => registry.is_none(),
InstallErrorKind::DownloadUnpack => false,
});
@@ -676,40 +675,45 @@ pub(crate) async fn install(
.into_iter()
.sorted_unstable_by(|(_, key_a, _), (_, key_b, _)| key_a.cmp(key_b))
{
- let (level, verb) = match kind {
- InstallErrorKind::DownloadUnpack => ("error".red().bold().to_string(), "install"),
+ match kind {
+ InstallErrorKind::DownloadUnpack => {
+ write_error_chain(
+ err.context(format!("Failed to install {key}")).as_ref(),
+ printer.stderr(),
+ "error",
+ AnsiColors::Red,
+ )?;
+ }
InstallErrorKind::Bin => {
- let level = match bin {
- None => "warning".yellow().bold().to_string(),
+ let (level, color) = match bin {
+ None => ("warning", AnsiColors::Yellow),
Some(false) => continue,
- Some(true) => "error".red().bold().to_string(),
+ Some(true) => ("error", AnsiColors::Red),
};
- (level, "install executable for")
- }
- #[cfg(windows)]
- InstallErrorKind::Registry => {
- let level = match registry {
- None => "warning".yellow().bold().to_string(),
- Some(false) => continue,
- Some(true) => "error".red().bold().to_string(),
- };
- (level, "install registry entry for")
- }
- };
- writeln!(
- printer.stderr(),
- "{level}{} Failed to {verb} {}",
- ":".bold(),
- key.green()
- )?;
- for err in err.chain() {
- writeln!(
- printer.stderr(),
- " {}: {}",
- "Caused by".red().bold(),
- err.to_string().trim()
- )?;
+ write_error_chain(
+ err.context(format!("Failed to install executable for {key}"))
+ .as_ref(),
+ printer.stderr(),
+ level,
+ color,
+ )?;
+ }
+ InstallErrorKind::Registry => {
+ let (level, color) = match registry {
+ None => ("warning", AnsiColors::Yellow),
+ Some(false) => continue,
+ Some(true) => ("error", AnsiColors::Red),
+ };
+
+ write_error_chain(
+ err.context(format!("Failed to create registry entry for {key}"))
+ .as_ref(),
+ printer.stderr(),
+ level,
+ color,
+ )?;
+ }
}
}
diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs
index 13e1f2ae3..af42a9eef 100644
--- a/crates/uv/src/commands/tool/upgrade.rs
+++ b/crates/uv/src/commands/tool/upgrade.rs
@@ -1,6 +1,6 @@
use anyhow::Result;
use itertools::Itertools;
-use owo_colors::OwoColorize;
+use owo_colors::{AnsiColors, OwoColorize};
use std::collections::BTreeMap;
use std::fmt::Write;
use tracing::debug;
@@ -18,6 +18,7 @@ use uv_python::{
use uv_requirements::RequirementsSpecification;
use uv_settings::{Combine, PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
use uv_tool::InstalledTools;
+use uv_warnings::write_error_chain;
use uv_workspace::WorkspaceCache;
use crate::commands::pip::loggers::{
@@ -155,20 +156,13 @@ pub(crate) async fn upgrade(
.into_iter()
.sorted_unstable_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b))
{
- writeln!(
+ write_error_chain(
+ err.context(format!("Failed to upgrade {}", name.green()))
+ .as_ref(),
printer.stderr(),
- "{}: Failed to upgrade {}",
- "error".red().bold(),
- name.green()
+ "error",
+ AnsiColors::Red,
)?;
- for err in err.chain() {
- writeln!(
- printer.stderr(),
- " {}: {}",
- "Caused by".red().bold(),
- err.to_string().trim()
- )?;
- }
}
return Ok(ExitStatus::Failure);
}
diff --git a/crates/uv/tests/it/publish.rs b/crates/uv/tests/it/publish.rs
index 0fdf435e8..ba86cf7e9 100644
--- a/crates/uv/tests/it/publish.rs
+++ b/crates/uv/tests/it/publish.rs
@@ -126,7 +126,7 @@ fn no_credentials() {
// Emulate CI
.env(EnvVars::GITHUB_ACTIONS, "true")
// Just to make sure
- .env_remove(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN), @r###"
+ .env_remove(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN), @r"
success: false
exit_code: 2
----- stdout -----
@@ -134,12 +134,13 @@ fn no_credentials() {
----- stderr -----
Publishing 1 file to https://test.pypi.org/legacy/
Note: Neither credentials nor keyring are configured, and there was an error fetching the trusted publishing token. If you don't want to use trusted publishing, you can ignore this error, but you need to provide credentials.
- Trusted publishing error: Environment variable ACTIONS_ID_TOKEN_REQUEST_TOKEN not set, is the `id-token: write` permission missing?
+ error: Trusted publishing failed
+ Caused by: Environment variable ACTIONS_ID_TOKEN_REQUEST_TOKEN not set, is the `id-token: write` permission missing?
Uploading ok-1.0.0-py3-none-any.whl ([SIZE])
error: Failed to publish `../../scripts/links/ok-1.0.0-py3-none-any.whl` to https://test.pypi.org/legacy/
Caused by: Failed to send POST request
Caused by: Missing credentials for https://test.pypi.org/legacy/
- "###
+ "
);
}
From 568677146449a865a3923e4c13bba05484642b26 Mon Sep 17 00:00:00 2001
From: Zanie Blue
Date: Mon, 28 Jul 2025 12:33:57 -0500
Subject: [PATCH 13/26] Cache Python downloads by default in `python install`
tests (#14326)
Adds a cache bucket for Python installs and uses it by default during
tests, extending the opt-in cache added in
https://github.com/astral-sh/uv/pull/12175
Updates the `python_install` tests to use a shared cache for Python
installs. This reduces the `python_install` test runtime on my machine
from 23s -> 17s. The difference should be much larger on machines with
slower internet and less cores for test workers :) This should also
improve stability in CI by reducing reliance on the network during test
runs, see #14327
---
crates/uv-cache/src/lib.rs | 10 +-
crates/uv/tests/it/common/mod.rs | 18 ++-
crates/uv/tests/it/python_install.rs | 182 +++++++++++++++++++++++----
3 files changed, 185 insertions(+), 25 deletions(-)
diff --git a/crates/uv-cache/src/lib.rs b/crates/uv-cache/src/lib.rs
index af28bb26c..d16c6427c 100644
--- a/crates/uv-cache/src/lib.rs
+++ b/crates/uv-cache/src/lib.rs
@@ -985,6 +985,8 @@ pub enum CacheBucket {
Builds,
/// Reusable virtual environments used to invoke Python tools.
Environments,
+ /// Cached Python downloads
+ Python,
}
impl CacheBucket {
@@ -1007,6 +1009,7 @@ impl CacheBucket {
Self::Archive => "archive-v0",
Self::Builds => "builds-v0",
Self::Environments => "environments-v2",
+ Self::Python => "python-v0",
}
}
@@ -1108,7 +1111,12 @@ impl CacheBucket {
let root = cache.bucket(self);
summary += rm_rf(root)?;
}
- Self::Git | Self::Interpreter | Self::Archive | Self::Builds | Self::Environments => {
+ Self::Git
+ | Self::Interpreter
+ | Self::Archive
+ | Self::Builds
+ | Self::Environments
+ | Self::Python => {
// Nothing to do.
}
}
diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs
index d7fbdaa6f..c894b70a0 100644
--- a/crates/uv/tests/it/common/mod.rs
+++ b/crates/uv/tests/it/common/mod.rs
@@ -20,7 +20,7 @@ use predicates::prelude::predicate;
use regex::Regex;
use tokio::io::AsyncWriteExt;
-use uv_cache::Cache;
+use uv_cache::{Cache, CacheBucket};
use uv_configuration::Preview;
use uv_fs::Simplified;
use uv_python::managed::ManagedPythonInstallations;
@@ -423,6 +423,22 @@ impl TestContext {
self
}
+ /// Use a shared global cache for Python downloads.
+ #[must_use]
+ pub fn with_python_download_cache(mut self) -> Self {
+ self.extra_env.push((
+ EnvVars::UV_PYTHON_CACHE_DIR.into(),
+ // Respect `UV_PYTHON_CACHE_DIR` if set, or use the default cache directory
+ env::var_os(EnvVars::UV_PYTHON_CACHE_DIR).unwrap_or_else(|| {
+ uv_cache::Cache::from_settings(false, None)
+ .unwrap()
+ .bucket(CacheBucket::Python)
+ .into()
+ }),
+ ));
+ self
+ }
+
/// Add extra directories and configuration for managed Python installations.
#[must_use]
pub fn with_managed_python_dirs(mut self) -> Self {
diff --git a/crates/uv/tests/it/python_install.rs b/crates/uv/tests/it/python_install.rs
index c5f98af0d..868aaa157 100644
--- a/crates/uv/tests/it/python_install.rs
+++ b/crates/uv/tests/it/python_install.rs
@@ -20,7 +20,8 @@ fn python_install() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
- .with_managed_python_dirs();
+ .with_managed_python_dirs()
+ .with_python_download_cache();
// Install the latest version
uv_snapshot!(context.filters(), context.python_install(), @r"
@@ -142,7 +143,8 @@ fn python_reinstall() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
- .with_managed_python_dirs();
+ .with_managed_python_dirs()
+ .with_python_download_cache();
// Install a couple versions
uv_snapshot!(context.filters(), context.python_install().arg("3.12").arg("3.13"), @r"
@@ -196,7 +198,8 @@ fn python_reinstall_patch() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
- .with_managed_python_dirs();
+ .with_managed_python_dirs()
+ .with_python_download_cache();
// Install a couple patch versions
uv_snapshot!(context.filters(), context.python_install().arg("3.12.6").arg("3.12.7"), @r"
@@ -230,7 +233,8 @@ fn python_install_automatic() {
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_filtered_python_sources()
- .with_managed_python_dirs();
+ .with_managed_python_dirs()
+ .with_python_download_cache();
// With downloads disabled, the automatic install should fail
uv_snapshot!(context.filters(), context.run()
@@ -341,7 +345,8 @@ fn regression_cpython() {
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_filtered_python_sources()
- .with_managed_python_dirs();
+ .with_managed_python_dirs()
+ .with_python_download_cache();
let init = context.temp_dir.child("mre.py");
init.write_str(indoc! { r#"
@@ -575,7 +580,8 @@ fn python_install_preview() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
- .with_managed_python_dirs();
+ .with_managed_python_dirs()
+ .with_python_download_cache();
// Install the latest version
uv_snapshot!(context.filters(), context.python_install().arg("--preview"), @r"
@@ -848,7 +854,8 @@ fn python_install_preview_no_bin() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
- .with_managed_python_dirs();
+ .with_managed_python_dirs()
+ .with_python_download_cache();
// Install the latest version
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("--no-bin"), @r"
@@ -894,7 +901,8 @@ fn python_install_preview_upgrade() {
let context = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
- .with_managed_python_dirs();
+ .with_managed_python_dirs()
+ .with_python_download_cache();
let bin_python = context
.bin_dir
@@ -1052,7 +1060,8 @@ fn python_install_freethreaded() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
- .with_managed_python_dirs();
+ .with_managed_python_dirs()
+ .with_python_download_cache();
// Install the latest version
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13t"), @r"
@@ -1185,7 +1194,8 @@ fn python_install_invalid_request() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
- .with_managed_python_dirs();
+ .with_managed_python_dirs()
+ .with_python_download_cache();
// Request something that is not a Python version
uv_snapshot!(context.filters(), context.python_install().arg("foobar"), @r###"
@@ -1223,7 +1233,8 @@ fn python_install_default() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
- .with_managed_python_dirs();
+ .with_managed_python_dirs()
+ .with_python_download_cache();
let bin_python_minor_13 = context
.bin_dir
@@ -1461,7 +1472,8 @@ fn python_install_default_preview() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
- .with_managed_python_dirs();
+ .with_managed_python_dirs()
+ .with_python_download_cache();
let bin_python_minor_13 = context
.bin_dir
@@ -1844,7 +1856,9 @@ fn read_link(path: &Path) -> String {
#[test]
fn python_install_unknown() {
- let context: TestContext = TestContext::new_with_versions(&[]).with_managed_python_dirs();
+ let context: TestContext = TestContext::new_with_versions(&[])
+ .with_managed_python_dirs()
+ .with_python_download_cache();
// An unknown request
uv_snapshot!(context.filters(), context.python_install().arg("foobar"), @r###"
@@ -1878,7 +1892,8 @@ fn python_install_broken_link() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
- .with_managed_python_dirs();
+ .with_managed_python_dirs()
+ .with_python_download_cache();
let bin_python = context.bin_dir.child("python3.13");
@@ -1912,7 +1927,8 @@ fn python_install_default_from_env() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
- .with_managed_python_dirs();
+ .with_managed_python_dirs()
+ .with_python_download_cache();
// Install the version specified by the `UV_PYTHON` environment variable by default
uv_snapshot!(context.filters(), context.python_install().env(EnvVars::UV_PYTHON, "3.12"), @r"
@@ -2002,7 +2018,8 @@ fn python_install_patch_dylib() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
- .with_managed_python_dirs();
+ .with_managed_python_dirs()
+ .with_python_download_cache();
// Install the latest version
context
@@ -2045,7 +2062,8 @@ fn python_install_314() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_managed_python_dirs()
- .with_filtered_exe_suffix();
+ .with_filtered_exe_suffix()
+ .with_python_download_cache();
// Install 3.14
// For now, this provides test coverage of pre-release handling
@@ -2132,13 +2150,14 @@ fn python_install_314() {
");
}
-/// Test caching Python archives with `UV_PYTHON_CACHE_DIR`.
+/// A duplicate of [`python_install`] with an isolated `UV_PYTHON_CACHE_DIR`.
+///
+/// See also, [`python_install_no_cache`].
#[test]
fn python_install_cached() {
- // It does not make sense to run this test when the developer selected faster test runs
- // by setting the env var.
- if env::var_os("UV_PYTHON_CACHE_DIR").is_some() {
- debug!("Skipping test because UV_PYTHON_CACHE_DIR is set");
+ // Skip this test if the developer has set `UV_PYTHON_CACHE_DIR` locally since it's slow
+ if env::var_os("UV_PYTHON_CACHE_DIR").is_some() && env::var_os("CI").is_none() {
+ debug!("Skipping test because `UV_PYTHON_CACHE_DIR` is set");
return;
}
@@ -2227,12 +2246,122 @@ fn python_install_cached() {
");
}
+/// Duplicate of [`python_install`] with the cache directory disabled.
+#[test]
+fn python_install_no_cache() {
+ // Skip this test if the developer has set `UV_PYTHON_CACHE_DIR` locally since it's slow
+ if env::var_os("UV_PYTHON_CACHE_DIR").is_some() && env::var_os("CI").is_none() {
+ debug!("Skipping test because `UV_PYTHON_CACHE_DIR` is set");
+ return;
+ }
+
+ let context: TestContext = TestContext::new_with_versions(&[])
+ .with_filtered_python_keys()
+ .with_filtered_exe_suffix()
+ .with_managed_python_dirs();
+
+ // Install the latest version
+ uv_snapshot!(context.filters(), context.python_install(), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Installed Python 3.13.5 in [TIME]
+ + cpython-3.13.5-[PLATFORM] (python3.13)
+ ");
+
+ let bin_python = context
+ .bin_dir
+ .child(format!("python3.13{}", std::env::consts::EXE_SUFFIX));
+
+ // The executable should not present in the bin directory
+ bin_python.assert(predicate::path::exists());
+
+ // Should be a no-op when already installed
+ uv_snapshot!(context.filters(), context.python_install(), @r###"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Python is already installed. Use `uv python install ` to install another version.
+ "###);
+
+ // Similarly, when a requested version is already installed
+ uv_snapshot!(context.filters(), context.python_install().arg("3.13"), @r###"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ "###);
+
+ // You can opt-in to a reinstall
+ uv_snapshot!(context.filters(), context.python_install().arg("3.13").arg("--reinstall"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Installed Python 3.13.5 in [TIME]
+ ~ cpython-3.13.5-[PLATFORM] (python3.13)
+ ");
+
+ // Uninstallation requires an argument
+ uv_snapshot!(context.filters(), context.python_uninstall(), @r###"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: the following required arguments were not provided:
+ ...
+
+ Usage: uv python uninstall --install-dir ...
+
+ For more information, try '--help'.
+ "###);
+
+ uv_snapshot!(context.filters(), context.python_uninstall().arg("3.13"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Searching for Python versions matching: Python 3.13
+ Uninstalled Python 3.13.5 in [TIME]
+ - cpython-3.13.5-[PLATFORM] (python3.13)
+ ");
+
+ // 3.12 isn't cached, so it can't be installed
+ let mut filters = context.filters();
+ filters.push((
+ "cpython-3.12.*.tar.gz",
+ "cpython-3.12.[PATCH]-[DATE]-[PLATFORM].tar.gz",
+ ));
+ uv_snapshot!(filters, context
+ .python_install()
+ .arg("3.12")
+ .arg("--offline"), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+
+ ----- stderr -----
+ error: Failed to install cpython-3.12.11-[PLATFORM]
+ Caused by: Failed to download https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.[PATCH]-[DATE]-[PLATFORM].tar.gz
+ Caused by: Network connectivity is disabled, but the requested data wasn't found in the cache for: `https://github.com/astral-sh/python-build-standalone/releases/download/20250723/cpython-3.12.[PATCH]-[DATE]-[PLATFORM].tar.gz`
+ ");
+}
+
#[cfg(target_os = "macos")]
#[test]
fn python_install_emulated_macos() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_exe_suffix()
- .with_managed_python_dirs();
+ .with_managed_python_dirs()
+ .with_python_download_cache();
// Before installation, `uv python list` should not show the x86_64 download
uv_snapshot!(context.filters(), context.python_list().arg("3.13"), @r"
@@ -2304,6 +2433,7 @@ fn install_transparent_patch_upgrade_uv_venv() {
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs()
+ .with_python_download_cache()
.with_filtered_python_install_bin();
// Install a lower patch version.
@@ -2397,6 +2527,7 @@ fn install_multiple_patches() {
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs()
+ .with_python_download_cache()
.with_filtered_python_install_bin();
// Install 3.12 patches in ascending order list
@@ -2487,6 +2618,7 @@ fn uninstall_highest_patch() {
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs()
+ .with_python_download_cache()
.with_filtered_python_install_bin();
// Install patches in ascending order list
@@ -2560,6 +2692,7 @@ fn install_no_transparent_upgrade_with_venv_patch_specification() {
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs()
+ .with_python_download_cache()
.with_filtered_python_install_bin();
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12.9"), @r"
@@ -2629,6 +2762,7 @@ fn install_transparent_patch_upgrade_venv_module() {
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs()
+ .with_python_download_cache()
.with_filtered_python_install_bin();
let bin_dir = context.temp_dir.child("bin");
@@ -2706,6 +2840,7 @@ fn install_lower_patch_automatically() {
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs()
+ .with_python_download_cache()
.with_filtered_python_install_bin();
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12.11"), @r"
@@ -2775,6 +2910,7 @@ fn uninstall_last_patch() {
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs()
+ .with_python_download_cache()
.with_filtered_virtualenv_bin();
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.17"), @r"
From 00efde06b61756f0f305fcf67b12db71a29063d3 Mon Sep 17 00:00:00 2001
From: Zanie Blue
Date: Mon, 28 Jul 2025 14:12:04 -0500
Subject: [PATCH 14/26] Split platform detection code into a dedicated
`uv-platform` crate (#14918)
In service of some subsequent work...
---
Cargo.lock | 21 +-
Cargo.toml | 1 +
crates/uv-platform/Cargo.toml | 34 ++
crates/uv-platform/src/arch.rs | 249 ++++++++++
.../{uv-python => uv-platform}/src/cpuinfo.rs | 4 +-
crates/uv-platform/src/lib.rs | 26 ++
crates/{uv-python => uv-platform}/src/libc.rs | 88 +++-
crates/uv-platform/src/os.rs | 88 ++++
crates/uv-python/Cargo.toml | 5 +-
crates/uv-python/src/discovery.rs | 20 +-
crates/uv-python/src/downloads.rs | 5 +-
crates/uv-python/src/installation.rs | 2 +-
crates/uv-python/src/interpreter.rs | 2 +-
crates/uv-python/src/lib.rs | 3 -
crates/uv-python/src/managed.rs | 9 +-
crates/uv-python/src/platform.rs | 427 ------------------
crates/uv-python/src/windows_registry.rs | 2 +-
crates/uv/Cargo.toml | 1 +
crates/uv/src/commands/python/install.rs | 2 +-
crates/uv/tests/it/python_find.rs | 2 +-
crates/uv/tests/it/python_list.rs | 2 +-
crates/uv/tests/it/python_pin.rs | 6 +-
22 files changed, 530 insertions(+), 469 deletions(-)
create mode 100644 crates/uv-platform/Cargo.toml
create mode 100644 crates/uv-platform/src/arch.rs
rename crates/{uv-python => uv-platform}/src/cpuinfo.rs (94%)
create mode 100644 crates/uv-platform/src/lib.rs
rename crates/{uv-python => uv-platform}/src/libc.rs (77%)
create mode 100644 crates/uv-platform/src/os.rs
delete mode 100644 crates/uv-python/src/platform.rs
diff --git a/Cargo.lock b/Cargo.lock
index ff72f1418..5b2fd66b8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4732,6 +4732,7 @@ dependencies = [
"uv-pep440",
"uv-pep508",
"uv-performance-memory-allocator",
+ "uv-platform",
"uv-platform-tags",
"uv-publish",
"uv-pypi-types",
@@ -5553,6 +5554,23 @@ dependencies = [
"tikv-jemallocator",
]
+[[package]]
+name = "uv-platform"
+version = "0.0.1"
+dependencies = [
+ "fs-err",
+ "goblin",
+ "indoc",
+ "procfs",
+ "regex",
+ "target-lexicon",
+ "thiserror 2.0.12",
+ "tracing",
+ "uv-fs",
+ "uv-platform-tags",
+ "uv-static",
+]
+
[[package]]
name = "uv-platform-tags"
version = "0.0.1"
@@ -5646,14 +5664,12 @@ dependencies = [
"dunce",
"fs-err",
"futures",
- "goblin",
"indexmap",
"indoc",
"insta",
"itertools 0.14.0",
"once_cell",
"owo-colors",
- "procfs",
"ref-cast",
"regex",
"reqwest",
@@ -5687,6 +5703,7 @@ dependencies = [
"uv-install-wheel",
"uv-pep440",
"uv-pep508",
+ "uv-platform",
"uv-platform-tags",
"uv-pypi-types",
"uv-redacted",
diff --git a/Cargo.toml b/Cargo.toml
index f11b91556..6e9742bc8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -49,6 +49,7 @@ uv-once-map = { path = "crates/uv-once-map" }
uv-options-metadata = { path = "crates/uv-options-metadata" }
uv-pep440 = { path = "crates/uv-pep440", features = ["tracing", "rkyv", "version-ranges"] }
uv-pep508 = { path = "crates/uv-pep508", features = ["non-pep508-extensions"] }
+uv-platform = { path = "crates/uv-platform" }
uv-platform-tags = { path = "crates/uv-platform-tags" }
uv-publish = { path = "crates/uv-publish" }
uv-pypi-types = { path = "crates/uv-pypi-types" }
diff --git a/crates/uv-platform/Cargo.toml b/crates/uv-platform/Cargo.toml
new file mode 100644
index 000000000..0bb891ed9
--- /dev/null
+++ b/crates/uv-platform/Cargo.toml
@@ -0,0 +1,34 @@
+[package]
+name = "uv-platform"
+version = "0.0.1"
+edition = { workspace = true }
+rust-version = { workspace = true }
+homepage = { workspace = true }
+documentation = { workspace = true }
+repository = { workspace = true }
+authors = { workspace = true }
+license = { workspace = true }
+
+[lib]
+doctest = false
+
+[lints]
+workspace = true
+
+[dependencies]
+uv-static = { workspace = true }
+uv-fs = { workspace = true }
+uv-platform-tags = { workspace = true }
+
+fs-err = { workspace = true }
+goblin = { workspace = true }
+regex = { workspace = true }
+target-lexicon = { workspace = true }
+thiserror = { workspace = true }
+tracing = { workspace = true }
+
+[target.'cfg(target_os = "linux")'.dependencies]
+procfs = { workspace = true }
+
+[dev-dependencies]
+indoc = { workspace = true }
diff --git a/crates/uv-platform/src/arch.rs b/crates/uv-platform/src/arch.rs
new file mode 100644
index 000000000..f64312489
--- /dev/null
+++ b/crates/uv-platform/src/arch.rs
@@ -0,0 +1,249 @@
+use crate::Error;
+use std::fmt::Display;
+use std::str::FromStr;
+use std::{cmp, fmt};
+
+/// Architecture variants, e.g., with support for different instruction sets
+#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, Ord, PartialOrd)]
+pub enum ArchVariant {
+ /// Targets 64-bit Intel/AMD CPUs newer than Nehalem (2008).
+ /// Includes SSE3, SSE4 and other post-2003 CPU instructions.
+ V2,
+ /// Targets 64-bit Intel/AMD CPUs newer than Haswell (2013) and Excavator (2015).
+ /// Includes AVX, AVX2, MOVBE and other newer CPU instructions.
+ V3,
+ /// Targets 64-bit Intel/AMD CPUs with AVX-512 instructions (post-2017 Intel CPUs).
+ /// Many post-2017 Intel CPUs do not support AVX-512.
+ V4,
+}
+
+#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
+pub struct Arch {
+ pub(crate) family: target_lexicon::Architecture,
+ pub(crate) variant: Option,
+}
+
+impl Ord for Arch {
+ fn cmp(&self, other: &Self) -> cmp::Ordering {
+ if self.family == other.family {
+ return self.variant.cmp(&other.variant);
+ }
+
+ // For the time being, manually make aarch64 windows disfavored
+ // on its own host platform, because most packages don't have wheels for
+ // aarch64 windows, making emulation more useful than native execution!
+ //
+ // The reason we do this in "sorting" and not "supports" is so that we don't
+ // *refuse* to use an aarch64 windows pythons if they happen to be installed
+ // and nothing else is available.
+ //
+ // Similarly if someone manually requests an aarch64 windows install, we
+ // should respect that request (this is the way users should "override"
+ // this behaviour).
+ let preferred = if cfg!(all(windows, target_arch = "aarch64")) {
+ Arch {
+ family: target_lexicon::Architecture::X86_64,
+ variant: None,
+ }
+ } else {
+ // Prefer native architectures
+ Arch::from_env()
+ };
+
+ match (
+ self.family == preferred.family,
+ other.family == preferred.family,
+ ) {
+ (true, true) => unreachable!(),
+ (true, false) => cmp::Ordering::Less,
+ (false, true) => cmp::Ordering::Greater,
+ (false, false) => {
+ // Both non-preferred, fallback to lexicographic order
+ self.family.to_string().cmp(&other.family.to_string())
+ }
+ }
+ }
+}
+
+impl PartialOrd for Arch {
+ fn partial_cmp(&self, other: &Self) -> Option {
+ Some(self.cmp(other))
+ }
+}
+
+impl Arch {
+ pub fn new(family: target_lexicon::Architecture, variant: Option) -> Self {
+ Self { family, variant }
+ }
+
+ pub fn from_env() -> Self {
+ Self {
+ family: target_lexicon::HOST.architecture,
+ variant: None,
+ }
+ }
+
+ /// Does the current architecture support running the other?
+ ///
+ /// When the architecture is equal, this is always true. Otherwise, this is true if the
+ /// architecture is transparently emulated or is a microarchitecture with worse performance
+ /// characteristics.
+ pub fn supports(self, other: Self) -> bool {
+ if self == other {
+ return true;
+ }
+
+ // TODO: Implement `variant` support checks
+
+ // Windows ARM64 runs emulated x86_64 binaries transparently
+ // Similarly, macOS aarch64 runs emulated x86_64 binaries transparently if you have Rosetta
+ // installed. We don't try to be clever and check if that's the case here, we just assume
+ // that if x86_64 distributions are available, they're usable.
+ if (cfg!(windows) || cfg!(target_os = "macos"))
+ && matches!(self.family, target_lexicon::Architecture::Aarch64(_))
+ {
+ return other.family == target_lexicon::Architecture::X86_64;
+ }
+
+ false
+ }
+
+ pub fn family(&self) -> target_lexicon::Architecture {
+ self.family
+ }
+
+ pub fn is_arm(&self) -> bool {
+ matches!(self.family, target_lexicon::Architecture::Arm(_))
+ }
+}
+
+impl Display for Arch {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self.family {
+ target_lexicon::Architecture::X86_32(target_lexicon::X86_32Architecture::I686) => {
+ write!(f, "x86")?;
+ }
+ inner => write!(f, "{inner}")?,
+ }
+ if let Some(variant) = self.variant {
+ write!(f, "_{variant}")?;
+ }
+ Ok(())
+ }
+}
+
+impl FromStr for Arch {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result {
+ fn parse_family(s: &str) -> Result {
+ let inner = match s {
+ // Allow users to specify "x86" as a shorthand for the "i686" variant, they should not need
+ // to specify the exact architecture and this variant is what we have downloads for.
+ "x86" => {
+ target_lexicon::Architecture::X86_32(target_lexicon::X86_32Architecture::I686)
+ }
+ _ => target_lexicon::Architecture::from_str(s)
+ .map_err(|()| Error::UnknownArch(s.to_string()))?,
+ };
+ if matches!(inner, target_lexicon::Architecture::Unknown) {
+ return Err(Error::UnknownArch(s.to_string()));
+ }
+ Ok(inner)
+ }
+
+ // First check for a variant
+ if let Some((Ok(family), Ok(variant))) = s
+ .rsplit_once('_')
+ .map(|(family, variant)| (parse_family(family), ArchVariant::from_str(variant)))
+ {
+ // We only support variants for `x86_64` right now
+ if !matches!(family, target_lexicon::Architecture::X86_64) {
+ return Err(Error::UnsupportedVariant(
+ variant.to_string(),
+ family.to_string(),
+ ));
+ }
+ return Ok(Self {
+ family,
+ variant: Some(variant),
+ });
+ }
+
+ let family = parse_family(s)?;
+
+ Ok(Self {
+ family,
+ variant: None,
+ })
+ }
+}
+
+impl FromStr for ArchVariant {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result {
+ match s {
+ "v2" => Ok(Self::V2),
+ "v3" => Ok(Self::V3),
+ "v4" => Ok(Self::V4),
+ _ => Err(()),
+ }
+ }
+}
+
+impl Display for ArchVariant {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::V2 => write!(f, "v2"),
+ Self::V3 => write!(f, "v3"),
+ Self::V4 => write!(f, "v4"),
+ }
+ }
+}
+
+impl From<&uv_platform_tags::Arch> for Arch {
+ fn from(value: &uv_platform_tags::Arch) -> Self {
+ match value {
+ uv_platform_tags::Arch::Aarch64 => Arch::new(
+ target_lexicon::Architecture::Aarch64(target_lexicon::Aarch64Architecture::Aarch64),
+ None,
+ ),
+ uv_platform_tags::Arch::Armv5TEL => Arch::new(
+ target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv5te),
+ None,
+ ),
+ uv_platform_tags::Arch::Armv6L => Arch::new(
+ target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv6),
+ None,
+ ),
+ uv_platform_tags::Arch::Armv7L => Arch::new(
+ target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv7),
+ None,
+ ),
+ uv_platform_tags::Arch::S390X => Arch::new(target_lexicon::Architecture::S390x, None),
+ uv_platform_tags::Arch::Powerpc => {
+ Arch::new(target_lexicon::Architecture::Powerpc, None)
+ }
+ uv_platform_tags::Arch::Powerpc64 => {
+ Arch::new(target_lexicon::Architecture::Powerpc64, None)
+ }
+ uv_platform_tags::Arch::Powerpc64Le => {
+ Arch::new(target_lexicon::Architecture::Powerpc64le, None)
+ }
+ uv_platform_tags::Arch::X86 => Arch::new(
+ target_lexicon::Architecture::X86_32(target_lexicon::X86_32Architecture::I686),
+ None,
+ ),
+ uv_platform_tags::Arch::X86_64 => Arch::new(target_lexicon::Architecture::X86_64, None),
+ uv_platform_tags::Arch::LoongArch64 => {
+ Arch::new(target_lexicon::Architecture::LoongArch64, None)
+ }
+ uv_platform_tags::Arch::Riscv64 => Arch::new(
+ target_lexicon::Architecture::Riscv64(target_lexicon::Riscv64Architecture::Riscv64),
+ None,
+ ),
+ uv_platform_tags::Arch::Wasm32 => Arch::new(target_lexicon::Architecture::Wasm32, None),
+ }
+ }
+}
diff --git a/crates/uv-python/src/cpuinfo.rs b/crates/uv-platform/src/cpuinfo.rs
similarity index 94%
rename from crates/uv-python/src/cpuinfo.rs
rename to crates/uv-platform/src/cpuinfo.rs
index f0827886b..89a4f89e9 100644
--- a/crates/uv-python/src/cpuinfo.rs
+++ b/crates/uv-platform/src/cpuinfo.rs
@@ -1,6 +1,6 @@
//! Fetches CPU information.
-use anyhow::Error;
+use std::io::Error;
#[cfg(target_os = "linux")]
use procfs::{CpuInfo, Current};
@@ -14,7 +14,7 @@ use procfs::{CpuInfo, Current};
/// More information on this can be found in the [Debian ARM Hard Float Port documentation](https://wiki.debian.org/ArmHardFloatPort#VFP).
#[cfg(target_os = "linux")]
pub(crate) fn detect_hardware_floating_point_support() -> Result {
- let cpu_info = CpuInfo::current()?;
+ let cpu_info = CpuInfo::current().map_err(Error::other)?;
if let Some(features) = cpu_info.fields.get("Features") {
if features.contains("vfp") {
return Ok(true); // "vfp" found: hard-float (gnueabihf) detected
diff --git a/crates/uv-platform/src/lib.rs b/crates/uv-platform/src/lib.rs
new file mode 100644
index 000000000..7eb23875a
--- /dev/null
+++ b/crates/uv-platform/src/lib.rs
@@ -0,0 +1,26 @@
+//! Platform detection for operating system, architecture, and libc.
+
+use thiserror::Error;
+
+pub use crate::arch::{Arch, ArchVariant};
+pub use crate::libc::{Libc, LibcDetectionError, LibcVersion};
+pub use crate::os::Os;
+
+mod arch;
+mod cpuinfo;
+mod libc;
+mod os;
+
+#[derive(Error, Debug)]
+pub enum Error {
+ #[error("Unknown operating system: {0}")]
+ UnknownOs(String),
+ #[error("Unknown architecture: {0}")]
+ UnknownArch(String),
+ #[error("Unknown libc environment: {0}")]
+ UnknownLibc(String),
+ #[error("Unsupported variant `{0}` for architecture `{1}`")]
+ UnsupportedVariant(String, String),
+ #[error(transparent)]
+ LibcDetectionError(#[from] crate::libc::LibcDetectionError),
+}
diff --git a/crates/uv-python/src/libc.rs b/crates/uv-platform/src/libc.rs
similarity index 77%
rename from crates/uv-python/src/libc.rs
rename to crates/uv-platform/src/libc.rs
index 40950ae08..184f0487c 100644
--- a/crates/uv-python/src/libc.rs
+++ b/crates/uv-platform/src/libc.rs
@@ -3,18 +3,22 @@
//! Taken from `glibc_version` (),
//! which used the Apache 2.0 license (but not the MIT license)
+use crate::cpuinfo::detect_hardware_floating_point_support;
use fs_err as fs;
use goblin::elf::Elf;
use regex::Regex;
+use std::fmt::Display;
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
+use std::str::FromStr;
use std::sync::LazyLock;
-use thiserror::Error;
+use std::{env, fmt};
use tracing::trace;
use uv_fs::Simplified;
+use uv_static::EnvVars;
-#[derive(Debug, Error)]
+#[derive(Debug, thiserror::Error)]
pub enum LibcDetectionError {
#[error(
"Could not detect either glibc version nor musl libc version, at least one of which is required"
@@ -45,11 +49,89 @@ pub enum LibcDetectionError {
/// We support glibc (manylinux) and musl (musllinux) on linux.
#[derive(Debug, PartialEq, Eq)]
-pub(crate) enum LibcVersion {
+pub enum LibcVersion {
Manylinux { major: u32, minor: u32 },
Musllinux { major: u32, minor: u32 },
}
+#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
+pub enum Libc {
+ Some(target_lexicon::Environment),
+ None,
+}
+
+impl Libc {
+ pub fn from_env() -> Result {
+ match env::consts::OS {
+ "linux" => {
+ if let Ok(libc) = env::var(EnvVars::UV_LIBC) {
+ if !libc.is_empty() {
+ return Self::from_str(&libc);
+ }
+ }
+
+ Ok(Self::Some(match detect_linux_libc()? {
+ LibcVersion::Manylinux { .. } => match env::consts::ARCH {
+ // Checks if the CPU supports hardware floating-point operations.
+ // Depending on the result, it selects either the `gnueabihf` (hard-float) or `gnueabi` (soft-float) environment.
+ // download-metadata.json only includes armv7.
+ "arm" | "armv5te" | "armv7" => {
+ match detect_hardware_floating_point_support() {
+ Ok(true) => target_lexicon::Environment::Gnueabihf,
+ Ok(false) => target_lexicon::Environment::Gnueabi,
+ Err(_) => target_lexicon::Environment::Gnu,
+ }
+ }
+ _ => target_lexicon::Environment::Gnu,
+ },
+ LibcVersion::Musllinux { .. } => target_lexicon::Environment::Musl,
+ }))
+ }
+ "windows" | "macos" => Ok(Self::None),
+ // Use `None` on platforms without explicit support.
+ _ => Ok(Self::None),
+ }
+ }
+
+ pub fn is_musl(&self) -> bool {
+ matches!(self, Self::Some(target_lexicon::Environment::Musl))
+ }
+}
+
+impl FromStr for Libc {
+ type Err = crate::Error;
+
+ fn from_str(s: &str) -> Result {
+ match s {
+ "gnu" => Ok(Self::Some(target_lexicon::Environment::Gnu)),
+ "gnueabi" => Ok(Self::Some(target_lexicon::Environment::Gnueabi)),
+ "gnueabihf" => Ok(Self::Some(target_lexicon::Environment::Gnueabihf)),
+ "musl" => Ok(Self::Some(target_lexicon::Environment::Musl)),
+ "none" => Ok(Self::None),
+ _ => Err(crate::Error::UnknownLibc(s.to_string())),
+ }
+ }
+}
+
+impl Display for Libc {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Some(env) => write!(f, "{env}"),
+ Self::None => write!(f, "none"),
+ }
+ }
+}
+
+impl From<&uv_platform_tags::Os> for Libc {
+ fn from(value: &uv_platform_tags::Os) -> Self {
+ match value {
+ uv_platform_tags::Os::Manylinux { .. } => Libc::Some(target_lexicon::Environment::Gnu),
+ uv_platform_tags::Os::Musllinux { .. } => Libc::Some(target_lexicon::Environment::Musl),
+ _ => Libc::None,
+ }
+ }
+}
+
/// Determine whether we're running glibc or musl and in which version, given we are on linux.
///
/// Normally, we determine this from the python interpreter, which is more accurate, but when
diff --git a/crates/uv-platform/src/os.rs b/crates/uv-platform/src/os.rs
new file mode 100644
index 000000000..01f896f3f
--- /dev/null
+++ b/crates/uv-platform/src/os.rs
@@ -0,0 +1,88 @@
+use crate::Error;
+use std::fmt;
+use std::fmt::Display;
+use std::ops::Deref;
+use std::str::FromStr;
+
+#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
+pub struct Os(pub(crate) target_lexicon::OperatingSystem);
+
+impl Os {
+ pub fn new(os: target_lexicon::OperatingSystem) -> Self {
+ Self(os)
+ }
+
+ pub fn from_env() -> Self {
+ Self(target_lexicon::HOST.operating_system)
+ }
+
+ pub fn is_windows(&self) -> bool {
+ matches!(self.0, target_lexicon::OperatingSystem::Windows)
+ }
+}
+
+impl Display for Os {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match &**self {
+ target_lexicon::OperatingSystem::Darwin(_) => write!(f, "macos"),
+ inner => write!(f, "{inner}"),
+ }
+ }
+}
+
+impl FromStr for Os {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result {
+ let inner = match s {
+ "macos" => target_lexicon::OperatingSystem::Darwin(None),
+ _ => target_lexicon::OperatingSystem::from_str(s)
+ .map_err(|()| Error::UnknownOs(s.to_string()))?,
+ };
+ if matches!(inner, target_lexicon::OperatingSystem::Unknown) {
+ return Err(Error::UnknownOs(s.to_string()));
+ }
+ Ok(Self(inner))
+ }
+}
+
+impl Deref for Os {
+ type Target = target_lexicon::OperatingSystem;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl From<&uv_platform_tags::Os> for Os {
+ fn from(value: &uv_platform_tags::Os) -> Self {
+ match value {
+ uv_platform_tags::Os::Dragonfly { .. } => {
+ Os::new(target_lexicon::OperatingSystem::Dragonfly)
+ }
+ uv_platform_tags::Os::FreeBsd { .. } => {
+ Os::new(target_lexicon::OperatingSystem::Freebsd)
+ }
+ uv_platform_tags::Os::Haiku { .. } => Os::new(target_lexicon::OperatingSystem::Haiku),
+ uv_platform_tags::Os::Illumos { .. } => {
+ Os::new(target_lexicon::OperatingSystem::Illumos)
+ }
+ uv_platform_tags::Os::Macos { .. } => {
+ Os::new(target_lexicon::OperatingSystem::Darwin(None))
+ }
+ uv_platform_tags::Os::Manylinux { .. }
+ | uv_platform_tags::Os::Musllinux { .. }
+ | uv_platform_tags::Os::Android { .. } => {
+ Os::new(target_lexicon::OperatingSystem::Linux)
+ }
+ uv_platform_tags::Os::NetBsd { .. } => Os::new(target_lexicon::OperatingSystem::Netbsd),
+ uv_platform_tags::Os::OpenBsd { .. } => {
+ Os::new(target_lexicon::OperatingSystem::Openbsd)
+ }
+ uv_platform_tags::Os::Windows => Os::new(target_lexicon::OperatingSystem::Windows),
+ uv_platform_tags::Os::Pyodide { .. } => {
+ Os::new(target_lexicon::OperatingSystem::Emscripten)
+ }
+ }
+ }
+}
diff --git a/crates/uv-python/Cargo.toml b/crates/uv-python/Cargo.toml
index 53c70ba5f..1c6f09b15 100644
--- a/crates/uv-python/Cargo.toml
+++ b/crates/uv-python/Cargo.toml
@@ -28,6 +28,7 @@ uv-fs = { workspace = true }
uv-install-wheel = { workspace = true }
uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true }
+uv-platform = { workspace = true }
uv-platform-tags = { workspace = true }
uv-pypi-types = { workspace = true }
uv-redacted = { workspace = true }
@@ -42,7 +43,6 @@ configparser = { workspace = true }
dunce = { workspace = true }
fs-err = { workspace = true, features = ["tokio"] }
futures = { workspace = true }
-goblin = { workspace = true, default-features = false }
indexmap = { workspace = true }
itertools = { workspace = true }
owo-colors = { workspace = true }
@@ -68,9 +68,6 @@ url = { workspace = true }
which = { workspace = true }
once_cell = { workspace = true }
-[target.'cfg(target_os = "linux")'.dependencies]
-procfs = { workspace = true }
-
[target.'cfg(target_os = "windows")'.dependencies]
windows-registry = { workspace = true }
windows-result = { workspace = true }
diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs
index 466ea4b0f..496191818 100644
--- a/crates/uv-python/src/discovery.rs
+++ b/crates/uv-python/src/discovery.rs
@@ -3066,8 +3066,8 @@ mod tests {
discovery::{PythonRequest, VersionRequest},
downloads::{ArchRequest, PythonDownloadRequest},
implementation::ImplementationName,
- platform::{Arch, Libc, Os},
};
+ use uv_platform::{Arch, Libc, Os};
use super::{Error, PythonVariant};
@@ -3154,11 +3154,11 @@ mod tests {
PythonVariant::Default
)),
implementation: Some(ImplementationName::CPython),
- arch: Some(ArchRequest::Explicit(Arch {
- family: Architecture::Aarch64(Aarch64Architecture::Aarch64),
- variant: None
- })),
- os: Some(Os(target_lexicon::OperatingSystem::Darwin(None))),
+ arch: Some(ArchRequest::Explicit(Arch::new(
+ Architecture::Aarch64(Aarch64Architecture::Aarch64),
+ None
+ ))),
+ os: Some(Os::new(target_lexicon::OperatingSystem::Darwin(None))),
libc: Some(Libc::None),
prereleases: None
})
@@ -3189,10 +3189,10 @@ mod tests {
PythonVariant::Default
)),
implementation: None,
- arch: Some(ArchRequest::Explicit(Arch {
- family: Architecture::Aarch64(Aarch64Architecture::Aarch64),
- variant: None
- })),
+ arch: Some(ArchRequest::Explicit(Arch::new(
+ Architecture::Aarch64(Aarch64Architecture::Aarch64),
+ None
+ ))),
os: None,
libc: None,
prereleases: None
diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs
index 05bf17cd1..9e1e03c91 100644
--- a/crates/uv-python/src/downloads.rs
+++ b/crates/uv-python/src/downloads.rs
@@ -25,6 +25,7 @@ use uv_client::{BaseClient, WrappedReqwestError, is_extended_transient_error};
use uv_distribution_filename::{ExtensionError, SourceDistExtension};
use uv_extract::hash::Hasher;
use uv_fs::{Simplified, rename_with_retry};
+use uv_platform::{self as platform, Arch, Libc, Os};
use uv_pypi_types::{HashAlgorithm, HashDigest};
use uv_redacted::DisplaySafeUrl;
use uv_static::EnvVars;
@@ -34,9 +35,7 @@ use crate::implementation::{
Error as ImplementationError, ImplementationName, LenientImplementationName,
};
use crate::installation::PythonInstallationKey;
-use crate::libc::LibcDetectionError;
use crate::managed::ManagedPythonInstallation;
-use crate::platform::{self, Arch, Libc, Os};
use crate::{Interpreter, PythonRequest, PythonVersion, VersionRequest};
#[derive(Error, Debug)]
@@ -98,7 +97,7 @@ pub enum Error {
#[error("A mirror was provided via `{0}`, but the URL does not match the expected format: {0}")]
Mirror(&'static str, &'static str),
#[error("Failed to determine the libc used on the current platform")]
- LibcDetection(#[from] LibcDetectionError),
+ LibcDetection(#[from] platform::LibcDetectionError),
#[error("Remote Python downloads JSON is not yet supported, please use a local path")]
RemoteJSONNotSupported,
#[error("The JSON of the python downloads is invalid: {0}")]
diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs
index 3f5b506a6..8cdc33106 100644
--- a/crates/uv-python/src/installation.rs
+++ b/crates/uv-python/src/installation.rs
@@ -10,6 +10,7 @@ use uv_cache::Cache;
use uv_client::BaseClientBuilder;
use uv_configuration::Preview;
use uv_pep440::{Prerelease, Version};
+use uv_platform::{Arch, Libc, Os};
use crate::discovery::{
EnvironmentPreference, PythonRequest, find_best_python_installation, find_python_installation,
@@ -17,7 +18,6 @@ use crate::discovery::{
use crate::downloads::{DownloadResult, ManagedPythonDownload, PythonDownloadRequest, Reporter};
use crate::implementation::LenientImplementationName;
use crate::managed::{ManagedPythonInstallation, ManagedPythonInstallations};
-use crate::platform::{Arch, Libc, Os};
use crate::{
Error, ImplementationName, Interpreter, PythonDownloads, PythonPreference, PythonSource,
PythonVariant, PythonVersion, downloads,
diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs
index dd9dd1cb4..3a7cce3f0 100644
--- a/crates/uv-python/src/interpreter.rs
+++ b/crates/uv-python/src/interpreter.rs
@@ -21,13 +21,13 @@ use uv_fs::{LockedFile, PythonExt, Simplified, write_atomic_sync};
use uv_install_wheel::Layout;
use uv_pep440::Version;
use uv_pep508::{MarkerEnvironment, StringVersion};
+use uv_platform::{Arch, Libc, Os};
use uv_platform_tags::Platform;
use uv_platform_tags::{Tags, TagsError};
use uv_pypi_types::{ResolverMarkerEnvironment, Scheme};
use crate::implementation::LenientImplementationName;
use crate::managed::ManagedPythonInstallations;
-use crate::platform::{Arch, Libc, Os};
use crate::pointer_size::PointerSize;
use crate::{
Prefix, PythonInstallationKey, PythonVariant, PythonVersion, Target, VersionRequest,
diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs
index 8b8e9c129..f08198d97 100644
--- a/crates/uv-python/src/lib.rs
+++ b/crates/uv-python/src/lib.rs
@@ -29,19 +29,16 @@ pub use crate::version_files::{
};
pub use crate::virtualenv::{Error as VirtualEnvError, PyVenvConfiguration, VirtualEnvironment};
-mod cpuinfo;
mod discovery;
pub mod downloads;
mod environment;
mod implementation;
mod installation;
mod interpreter;
-mod libc;
pub mod macos_dylib;
pub mod managed;
#[cfg(windows)]
mod microsoft_store;
-pub mod platform;
mod pointer_size;
mod prefix;
mod python_version;
diff --git a/crates/uv-python/src/managed.rs b/crates/uv-python/src/managed.rs
index d9b96e5ed..69d12a0a3 100644
--- a/crates/uv-python/src/managed.rs
+++ b/crates/uv-python/src/managed.rs
@@ -17,6 +17,8 @@ use uv_configuration::{Preview, PreviewFeatures};
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
use uv_fs::{LockedFile, Simplified, replace_symlink, symlink_or_copy_file};
+use uv_platform::Error as PlatformError;
+use uv_platform::{Arch, Libc, LibcDetectionError, Os};
use uv_state::{StateBucket, StateStore};
use uv_static::EnvVars;
use uv_trampoline_builder::{Launcher, windows_python_launcher};
@@ -26,9 +28,6 @@ use crate::implementation::{
Error as ImplementationError, ImplementationName, LenientImplementationName,
};
use crate::installation::{self, PythonInstallationKey};
-use crate::libc::LibcDetectionError;
-use crate::platform::Error as PlatformError;
-use crate::platform::{Arch, Libc, Os};
use crate::python_version::PythonVersion;
use crate::{
PythonInstallationMinorVersionKey, PythonRequest, PythonVariant, macos_dylib, sysconfig,
@@ -271,7 +270,7 @@ impl ManagedPythonInstallations {
&& (arch.supports(installation.key.arch)
// TODO(zanieb): Allow inequal variants, as `Arch::supports` does not
// implement this yet. See https://github.com/astral-sh/uv/pull/9788
- || arch.family == installation.key.arch.family)
+ || arch.family() == installation.key.arch.family())
&& installation.key.libc == libc
});
@@ -545,7 +544,7 @@ impl ManagedPythonInstallation {
/// standard `EXTERNALLY-MANAGED` file.
pub fn ensure_externally_managed(&self) -> Result<(), Error> {
// Construct the path to the `stdlib` directory.
- let stdlib = if matches!(self.key.os, Os(target_lexicon::OperatingSystem::Windows)) {
+ let stdlib = if self.key.os.is_windows() {
self.python_dir().join("Lib")
} else {
let lib_suffix = self.key.variant.suffix();
diff --git a/crates/uv-python/src/platform.rs b/crates/uv-python/src/platform.rs
deleted file mode 100644
index 606e05e28..000000000
--- a/crates/uv-python/src/platform.rs
+++ /dev/null
@@ -1,427 +0,0 @@
-use crate::cpuinfo::detect_hardware_floating_point_support;
-use crate::libc::{LibcDetectionError, LibcVersion, detect_linux_libc};
-use std::fmt::Display;
-use std::ops::Deref;
-use std::{fmt, str::FromStr};
-use thiserror::Error;
-
-use uv_static::EnvVars;
-
-#[derive(Error, Debug)]
-pub enum Error {
- #[error("Unknown operating system: {0}")]
- UnknownOs(String),
- #[error("Unknown architecture: {0}")]
- UnknownArch(String),
- #[error("Unknown libc environment: {0}")]
- UnknownLibc(String),
- #[error("Unsupported variant `{0}` for architecture `{1}`")]
- UnsupportedVariant(String, String),
- #[error(transparent)]
- LibcDetectionError(#[from] LibcDetectionError),
-}
-
-/// Architecture variants, e.g., with support for different instruction sets
-#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, Ord, PartialOrd)]
-pub enum ArchVariant {
- /// Targets 64-bit Intel/AMD CPUs newer than Nehalem (2008).
- /// Includes SSE3, SSE4 and other post-2003 CPU instructions.
- V2,
- /// Targets 64-bit Intel/AMD CPUs newer than Haswell (2013) and Excavator (2015).
- /// Includes AVX, AVX2, MOVBE and other newer CPU instructions.
- V3,
- /// Targets 64-bit Intel/AMD CPUs with AVX-512 instructions (post-2017 Intel CPUs).
- /// Many post-2017 Intel CPUs do not support AVX-512.
- V4,
-}
-
-#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
-pub struct Arch {
- pub(crate) family: target_lexicon::Architecture,
- pub(crate) variant: Option,
-}
-
-impl Ord for Arch {
- fn cmp(&self, other: &Self) -> std::cmp::Ordering {
- if self.family == other.family {
- return self.variant.cmp(&other.variant);
- }
-
- // For the time being, manually make aarch64 windows disfavored
- // on its own host platform, because most packages don't have wheels for
- // aarch64 windows, making emulation more useful than native execution!
- //
- // The reason we do this in "sorting" and not "supports" is so that we don't
- // *refuse* to use an aarch64 windows pythons if they happen to be installed
- // and nothing else is available.
- //
- // Similarly if someone manually requests an aarch64 windows install, we
- // should respect that request (this is the way users should "override"
- // this behaviour).
- let preferred = if cfg!(all(windows, target_arch = "aarch64")) {
- Arch {
- family: target_lexicon::Architecture::X86_64,
- variant: None,
- }
- } else {
- // Prefer native architectures
- Arch::from_env()
- };
-
- match (
- self.family == preferred.family,
- other.family == preferred.family,
- ) {
- (true, true) => unreachable!(),
- (true, false) => std::cmp::Ordering::Less,
- (false, true) => std::cmp::Ordering::Greater,
- (false, false) => {
- // Both non-preferred, fallback to lexicographic order
- self.family.to_string().cmp(&other.family.to_string())
- }
- }
- }
-}
-
-impl PartialOrd for Arch {
- fn partial_cmp(&self, other: &Self) -> Option {
- Some(self.cmp(other))
- }
-}
-
-#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
-pub struct Os(pub(crate) target_lexicon::OperatingSystem);
-
-#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
-pub enum Libc {
- Some(target_lexicon::Environment),
- None,
-}
-
-impl Libc {
- pub(crate) fn from_env() -> Result {
- match std::env::consts::OS {
- "linux" => {
- if let Ok(libc) = std::env::var(EnvVars::UV_LIBC) {
- if !libc.is_empty() {
- return Self::from_str(&libc);
- }
- }
-
- Ok(Self::Some(match detect_linux_libc()? {
- LibcVersion::Manylinux { .. } => match std::env::consts::ARCH {
- // Checks if the CPU supports hardware floating-point operations.
- // Depending on the result, it selects either the `gnueabihf` (hard-float) or `gnueabi` (soft-float) environment.
- // download-metadata.json only includes armv7.
- "arm" | "armv5te" | "armv7" => {
- match detect_hardware_floating_point_support() {
- Ok(true) => target_lexicon::Environment::Gnueabihf,
- Ok(false) => target_lexicon::Environment::Gnueabi,
- Err(_) => target_lexicon::Environment::Gnu,
- }
- }
- _ => target_lexicon::Environment::Gnu,
- },
- LibcVersion::Musllinux { .. } => target_lexicon::Environment::Musl,
- }))
- }
- "windows" | "macos" => Ok(Self::None),
- // Use `None` on platforms without explicit support.
- _ => Ok(Self::None),
- }
- }
-
- pub fn is_musl(&self) -> bool {
- matches!(self, Self::Some(target_lexicon::Environment::Musl))
- }
-}
-
-impl FromStr for Libc {
- type Err = Error;
-
- fn from_str(s: &str) -> Result {
- match s {
- "gnu" => Ok(Self::Some(target_lexicon::Environment::Gnu)),
- "gnueabi" => Ok(Self::Some(target_lexicon::Environment::Gnueabi)),
- "gnueabihf" => Ok(Self::Some(target_lexicon::Environment::Gnueabihf)),
- "musl" => Ok(Self::Some(target_lexicon::Environment::Musl)),
- "none" => Ok(Self::None),
- _ => Err(Error::UnknownLibc(s.to_string())),
- }
- }
-}
-
-impl Os {
- pub fn from_env() -> Self {
- Self(target_lexicon::HOST.operating_system)
- }
-}
-
-impl Arch {
- pub fn from_env() -> Self {
- Self {
- family: target_lexicon::HOST.architecture,
- variant: None,
- }
- }
-
- /// Does the current architecture support running the other?
- ///
- /// When the architecture is equal, this is always true. Otherwise, this is true if the
- /// architecture is transparently emulated or is a microarchitecture with worse performance
- /// characteristics.
- pub(crate) fn supports(self, other: Self) -> bool {
- if self == other {
- return true;
- }
-
- // TODO: Implement `variant` support checks
-
- // Windows ARM64 runs emulated x86_64 binaries transparently
- // Similarly, macOS aarch64 runs emulated x86_64 binaries transparently if you have Rosetta
- // installed. We don't try to be clever and check if that's the case here, we just assume
- // that if x86_64 distributions are available, they're usable.
- if (cfg!(windows) || cfg!(target_os = "macos"))
- && matches!(self.family, target_lexicon::Architecture::Aarch64(_))
- {
- return other.family == target_lexicon::Architecture::X86_64;
- }
-
- false
- }
-
- pub fn family(&self) -> target_lexicon::Architecture {
- self.family
- }
-
- pub fn is_arm(&self) -> bool {
- matches!(self.family, target_lexicon::Architecture::Arm(_))
- }
-}
-
-impl Display for Libc {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::Some(env) => write!(f, "{env}"),
- Self::None => write!(f, "none"),
- }
- }
-}
-
-impl Display for Os {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match &**self {
- target_lexicon::OperatingSystem::Darwin(_) => write!(f, "macos"),
- inner => write!(f, "{inner}"),
- }
- }
-}
-
-impl Display for Arch {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self.family {
- target_lexicon::Architecture::X86_32(target_lexicon::X86_32Architecture::I686) => {
- write!(f, "x86")?;
- }
- inner => write!(f, "{inner}")?,
- }
- if let Some(variant) = self.variant {
- write!(f, "_{variant}")?;
- }
- Ok(())
- }
-}
-
-impl FromStr for Os {
- type Err = Error;
-
- fn from_str(s: &str) -> Result {
- let inner = match s {
- "macos" => target_lexicon::OperatingSystem::Darwin(None),
- _ => target_lexicon::OperatingSystem::from_str(s)
- .map_err(|()| Error::UnknownOs(s.to_string()))?,
- };
- if matches!(inner, target_lexicon::OperatingSystem::Unknown) {
- return Err(Error::UnknownOs(s.to_string()));
- }
- Ok(Self(inner))
- }
-}
-
-impl FromStr for Arch {
- type Err = Error;
-
- fn from_str(s: &str) -> Result {
- fn parse_family(s: &str) -> Result {
- let inner = match s {
- // Allow users to specify "x86" as a shorthand for the "i686" variant, they should not need
- // to specify the exact architecture and this variant is what we have downloads for.
- "x86" => {
- target_lexicon::Architecture::X86_32(target_lexicon::X86_32Architecture::I686)
- }
- _ => target_lexicon::Architecture::from_str(s)
- .map_err(|()| Error::UnknownArch(s.to_string()))?,
- };
- if matches!(inner, target_lexicon::Architecture::Unknown) {
- return Err(Error::UnknownArch(s.to_string()));
- }
- Ok(inner)
- }
-
- // First check for a variant
- if let Some((Ok(family), Ok(variant))) = s
- .rsplit_once('_')
- .map(|(family, variant)| (parse_family(family), ArchVariant::from_str(variant)))
- {
- // We only support variants for `x86_64` right now
- if !matches!(family, target_lexicon::Architecture::X86_64) {
- return Err(Error::UnsupportedVariant(
- variant.to_string(),
- family.to_string(),
- ));
- }
- return Ok(Self {
- family,
- variant: Some(variant),
- });
- }
-
- let family = parse_family(s)?;
-
- Ok(Self {
- family,
- variant: None,
- })
- }
-}
-
-impl FromStr for ArchVariant {
- type Err = ();
-
- fn from_str(s: &str) -> Result {
- match s {
- "v2" => Ok(Self::V2),
- "v3" => Ok(Self::V3),
- "v4" => Ok(Self::V4),
- _ => Err(()),
- }
- }
-}
-
-impl Display for ArchVariant {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::V2 => write!(f, "v2"),
- Self::V3 => write!(f, "v3"),
- Self::V4 => write!(f, "v4"),
- }
- }
-}
-
-impl Deref for Os {
- type Target = target_lexicon::OperatingSystem;
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-
-impl From<&uv_platform_tags::Arch> for Arch {
- fn from(value: &uv_platform_tags::Arch) -> Self {
- match value {
- uv_platform_tags::Arch::Aarch64 => Self {
- family: target_lexicon::Architecture::Aarch64(
- target_lexicon::Aarch64Architecture::Aarch64,
- ),
- variant: None,
- },
- uv_platform_tags::Arch::Armv5TEL => Self {
- family: target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv5te),
- variant: None,
- },
- uv_platform_tags::Arch::Armv6L => Self {
- family: target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv6),
- variant: None,
- },
- uv_platform_tags::Arch::Armv7L => Self {
- family: target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv7),
- variant: None,
- },
- uv_platform_tags::Arch::S390X => Self {
- family: target_lexicon::Architecture::S390x,
- variant: None,
- },
- uv_platform_tags::Arch::Powerpc => Self {
- family: target_lexicon::Architecture::Powerpc,
- variant: None,
- },
- uv_platform_tags::Arch::Powerpc64 => Self {
- family: target_lexicon::Architecture::Powerpc64,
- variant: None,
- },
- uv_platform_tags::Arch::Powerpc64Le => Self {
- family: target_lexicon::Architecture::Powerpc64le,
- variant: None,
- },
- uv_platform_tags::Arch::X86 => Self {
- family: target_lexicon::Architecture::X86_32(
- target_lexicon::X86_32Architecture::I686,
- ),
- variant: None,
- },
- uv_platform_tags::Arch::X86_64 => Self {
- family: target_lexicon::Architecture::X86_64,
- variant: None,
- },
- uv_platform_tags::Arch::LoongArch64 => Self {
- family: target_lexicon::Architecture::LoongArch64,
- variant: None,
- },
- uv_platform_tags::Arch::Riscv64 => Self {
- family: target_lexicon::Architecture::Riscv64(
- target_lexicon::Riscv64Architecture::Riscv64,
- ),
- variant: None,
- },
- uv_platform_tags::Arch::Wasm32 => Self {
- family: target_lexicon::Architecture::Wasm32,
- variant: None,
- },
- }
- }
-}
-
-impl From<&uv_platform_tags::Os> for Libc {
- fn from(value: &uv_platform_tags::Os) -> Self {
- match value {
- uv_platform_tags::Os::Manylinux { .. } => Self::Some(target_lexicon::Environment::Gnu),
- uv_platform_tags::Os::Musllinux { .. } => Self::Some(target_lexicon::Environment::Musl),
- _ => Self::None,
- }
- }
-}
-
-impl From<&uv_platform_tags::Os> for Os {
- fn from(value: &uv_platform_tags::Os) -> Self {
- match value {
- uv_platform_tags::Os::Dragonfly { .. } => {
- Self(target_lexicon::OperatingSystem::Dragonfly)
- }
- uv_platform_tags::Os::FreeBsd { .. } => Self(target_lexicon::OperatingSystem::Freebsd),
- uv_platform_tags::Os::Haiku { .. } => Self(target_lexicon::OperatingSystem::Haiku),
- uv_platform_tags::Os::Illumos { .. } => Self(target_lexicon::OperatingSystem::Illumos),
- uv_platform_tags::Os::Macos { .. } => {
- Self(target_lexicon::OperatingSystem::Darwin(None))
- }
- uv_platform_tags::Os::Manylinux { .. }
- | uv_platform_tags::Os::Musllinux { .. }
- | uv_platform_tags::Os::Android { .. } => Self(target_lexicon::OperatingSystem::Linux),
- uv_platform_tags::Os::NetBsd { .. } => Self(target_lexicon::OperatingSystem::Netbsd),
- uv_platform_tags::Os::OpenBsd { .. } => Self(target_lexicon::OperatingSystem::Openbsd),
- uv_platform_tags::Os::Windows => Self(target_lexicon::OperatingSystem::Windows),
- uv_platform_tags::Os::Pyodide { .. } => {
- Self(target_lexicon::OperatingSystem::Emscripten)
- }
- }
- }
-}
diff --git a/crates/uv-python/src/windows_registry.rs b/crates/uv-python/src/windows_registry.rs
index 0020f95e9..cd6393aec 100644
--- a/crates/uv-python/src/windows_registry.rs
+++ b/crates/uv-python/src/windows_registry.rs
@@ -1,7 +1,6 @@
//! PEP 514 interactions with the Windows registry.
use crate::managed::ManagedPythonInstallation;
-use crate::platform::Arch;
use crate::{COMPANY_DISPLAY_NAME, COMPANY_KEY, PythonInstallationKey, PythonVersion};
use anyhow::anyhow;
use std::cmp::Ordering;
@@ -11,6 +10,7 @@ use std::str::FromStr;
use target_lexicon::PointerWidth;
use thiserror::Error;
use tracing::debug;
+use uv_platform::Arch;
use uv_warnings::{warn_user, warn_user_once};
use windows_registry::{CURRENT_USER, HSTRING, Key, LOCAL_MACHINE, Value};
use windows_result::HRESULT;
diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml
index 5acbb7f20..f37e8c2f0 100644
--- a/crates/uv/Cargo.toml
+++ b/crates/uv/Cargo.toml
@@ -38,6 +38,7 @@ uv-normalize = { workspace = true }
uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true }
uv-performance-memory-allocator = { path = "../uv-performance-memory-allocator", optional = true }
+uv-platform = { workspace = true }
uv-platform-tags = { workspace = true }
uv-publish = { workspace = true }
uv-pypi-types = { workspace = true }
diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs
index 02e4c27e5..e3b1ef797 100644
--- a/crates/uv/src/commands/python/install.rs
+++ b/crates/uv/src/commands/python/install.rs
@@ -16,6 +16,7 @@ use tracing::{debug, trace};
use uv_configuration::{Preview, PreviewFeatures};
use uv_fs::Simplified;
+use uv_platform::{Arch, Libc};
use uv_python::downloads::{
self, ArchRequest, DownloadResult, ManagedPythonDownload, PythonDownloadRequest,
};
@@ -23,7 +24,6 @@ use uv_python::managed::{
ManagedPythonInstallation, ManagedPythonInstallations, PythonMinorVersionLink,
create_link_to_executable, python_executable_dir,
};
-use uv_python::platform::{Arch, Libc};
use uv_python::{
PythonDownloads, PythonInstallationKey, PythonInstallationMinorVersionKey, PythonRequest,
PythonVersionFile, VersionFileDiscoveryOptions, VersionFilePreference, VersionRequest,
diff --git a/crates/uv/tests/it/python_find.rs b/crates/uv/tests/it/python_find.rs
index d2ae51e38..5086fe2df 100644
--- a/crates/uv/tests/it/python_find.rs
+++ b/crates/uv/tests/it/python_find.rs
@@ -2,7 +2,7 @@ use assert_fs::prelude::{FileTouch, PathChild};
use assert_fs::{fixture::FileWriteStr, prelude::PathCreateDir};
use indoc::indoc;
-use uv_python::platform::{Arch, Os};
+use uv_platform::{Arch, Os};
use uv_static::EnvVars;
use crate::common::{TestContext, uv_snapshot, venv_bin_path};
diff --git a/crates/uv/tests/it/python_list.rs b/crates/uv/tests/it/python_list.rs
index 11472baec..5c23a3e93 100644
--- a/crates/uv/tests/it/python_list.rs
+++ b/crates/uv/tests/it/python_list.rs
@@ -1,4 +1,4 @@
-use uv_python::platform::{Arch, Os};
+use uv_platform::{Arch, Os};
use uv_static::EnvVars;
use crate::common::{TestContext, uv_snapshot};
diff --git a/crates/uv/tests/it/python_pin.rs b/crates/uv/tests/it/python_pin.rs
index 97093831c..0f01a0011 100644
--- a/crates/uv/tests/it/python_pin.rs
+++ b/crates/uv/tests/it/python_pin.rs
@@ -5,10 +5,8 @@ use anyhow::Result;
use assert_cmd::assert::OutputAssertExt;
use assert_fs::fixture::{FileWriteStr, PathChild, PathCreateDir};
use insta::assert_snapshot;
-use uv_python::{
- PYTHON_VERSION_FILENAME, PYTHON_VERSIONS_FILENAME,
- platform::{Arch, Os},
-};
+use uv_platform::{Arch, Os};
+use uv_python::{PYTHON_VERSION_FILENAME, PYTHON_VERSIONS_FILENAME};
#[test]
fn python_pin() {
From 11fe8f70f9148f96168015777402a764f83ee923 Mon Sep 17 00:00:00 2001
From: Zanie Blue
Date: Tue, 29 Jul 2025 17:00:25 -0500
Subject: [PATCH 15/26] Add `exclude-newer-package` (#14489)
Adds `exclude-newer-package = { package = timestamp, ... } ` and
`--exclude-newer-package package=timestamp`. These take precedence over
`exclude-newer` for a given package.
This does need to be serialized to the lockfile, so the revision is
bumped to 3. I tested a previous version and we can read a lockfile with
this information just fine.
Closes https://github.com/astral-sh/uv/issues/14394
---
crates/uv-bench/benches/uv.rs | 10 +-
crates/uv-cli/src/lib.rs | 62 +-
crates/uv-cli/src/options.rs | 16 +-
crates/uv-dispatch/src/lib.rs | 6 +-
crates/uv-resolver/src/exclude_newer.rs | 181 +++++-
crates/uv-resolver/src/lib.rs | 4 +-
crates/uv-resolver/src/lock/mod.rs | 61 +-
...r__lock__tests__hash_optional_missing.snap | 1 +
...r__lock__tests__hash_optional_present.snap | 1 +
...r__lock__tests__hash_required_present.snap | 1 +
...missing_dependency_source_unambiguous.snap | 1 +
...dependency_source_version_unambiguous.snap | 1 +
...s__missing_dependency_version_dynamic.snap | 1 +
...issing_dependency_version_unambiguous.snap | 1 +
...lock__tests__source_direct_has_subdir.snap | 1 +
..._lock__tests__source_direct_no_subdir.snap | 1 +
...solver__lock__tests__source_directory.snap | 1 +
...esolver__lock__tests__source_editable.snap | 1 +
crates/uv-resolver/src/options.rs | 6 +-
crates/uv-resolver/src/resolver/mod.rs | 10 +-
crates/uv-resolver/src/resolver/provider.rs | 6 +-
crates/uv-resolver/src/version_map.rs | 8 +-
crates/uv-settings/src/combine.rs | 41 +-
crates/uv-settings/src/lib.rs | 4 +
crates/uv-settings/src/settings.rs | 67 +-
crates/uv/src/commands/build_frontend.rs | 4 +-
crates/uv/src/commands/pip/compile.rs | 6 +-
crates/uv/src/commands/pip/install.rs | 4 +-
crates/uv/src/commands/pip/latest.rs | 9 +-
crates/uv/src/commands/pip/list.rs | 2 +-
crates/uv/src/commands/pip/sync.rs | 4 +-
crates/uv/src/commands/pip/tree.rs | 2 +-
crates/uv/src/commands/project/add.rs | 4 +-
crates/uv/src/commands/project/export.rs | 1 +
crates/uv/src/commands/project/lock.rs | 30 +-
crates/uv/src/commands/project/mod.rs | 10 +-
crates/uv/src/commands/project/remove.rs | 1 +
crates/uv/src/commands/project/sync.rs | 1 +
crates/uv/src/commands/venv.rs | 2 +-
crates/uv/src/settings.rs | 43 +-
crates/uv/tests/it/branching_urls.rs | 10 +-
crates/uv/tests/it/edit.rs | 100 +--
crates/uv/tests/it/export.rs | 8 +-
crates/uv/tests/it/lock.rs | 576 +++++++++++-------
crates/uv/tests/it/lock_conflict.rs | 64 +-
crates/uv/tests/it/lock_scenarios.rs | 68 +--
crates/uv/tests/it/pip_compile.rs | 192 +++++-
crates/uv/tests/it/run.rs | 8 +-
crates/uv/tests/it/show_settings.rs | 297 +++++++--
.../it__ecosystem__black-lock-file.snap | 2 +-
...system__github-wikidata-bot-lock-file.snap | 2 +-
...system__home-assistant-core-lock-file.snap | 2 +-
.../it__ecosystem__packse-lock-file.snap | 2 +-
.../it__ecosystem__saleor-lock-file.snap | 2 +-
...it__ecosystem__transformers-lock-file.snap | 2 +-
.../it__ecosystem__warehouse-lock-file.snap | 2 +-
.../it__workflow__jax_instability-2.snap | 2 +-
...se_add_remove_existing_package_noop-2.snap | 2 +-
...flow__packse_add_remove_one_package-2.snap | 2 +-
...te_transitive_to_direct_then_remove-2.snap | 2 +-
crates/uv/tests/it/sync.rs | 191 +++++-
crates/uv/tests/it/tree.rs | 4 +-
crates/uv/tests/it/version.rs | 10 +-
crates/uv/tests/it/workspace.rs | 2 +-
docs/reference/cli.md | 80 ++-
docs/reference/settings.md | 54 ++
uv.schema.json | 34 +-
67 files changed, 1784 insertions(+), 552 deletions(-)
diff --git a/crates/uv-bench/benches/uv.rs b/crates/uv-bench/benches/uv.rs
index eedc0f92f..8adfd5a0e 100644
--- a/crates/uv-bench/benches/uv.rs
+++ b/crates/uv-bench/benches/uv.rs
@@ -99,8 +99,8 @@ mod resolver {
use uv_pypi_types::{Conflicts, ResolverMarkerEnvironment};
use uv_python::Interpreter;
use uv_resolver::{
- FlatIndex, InMemoryIndex, Manifest, OptionsBuilder, PythonRequirement, Resolver,
- ResolverEnvironment, ResolverOutput,
+ ExcludeNewer, FlatIndex, InMemoryIndex, Manifest, OptionsBuilder, PythonRequirement,
+ Resolver, ResolverEnvironment, ResolverOutput,
};
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_workspace::WorkspaceCache;
@@ -145,7 +145,7 @@ mod resolver {
let concurrency = Concurrency::default();
let config_settings = ConfigSettings::default();
let config_settings_package = PackageConfigSettings::default();
- let exclude_newer = Some(
+ let exclude_newer = ExcludeNewer::global(
jiff::civil::date(2024, 9, 1)
.to_zoned(jiff::tz::TimeZone::UTC)
.unwrap()
@@ -159,7 +159,9 @@ mod resolver {
let index = InMemoryIndex::default();
let index_locations = IndexLocations::default();
let installed_packages = EmptyInstalledPackages;
- let options = OptionsBuilder::new().exclude_newer(exclude_newer).build();
+ let options = OptionsBuilder::new()
+ .exclude_newer(exclude_newer.clone())
+ .build();
let sources = SourceStrategy::default();
let dependency_metadata = DependencyMetadata::default();
let conflicts = Conflicts::empty();
diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs
index 7cd743bc6..e5bd3b0e2 100644
--- a/crates/uv-cli/src/lib.rs
+++ b/crates/uv-cli/src/lib.rs
@@ -20,7 +20,10 @@ use uv_pep508::{MarkerTree, Requirement};
use uv_pypi_types::VerbatimParsedUrl;
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
use uv_redacted::DisplaySafeUrl;
-use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode};
+use uv_resolver::{
+ AnnotationStyle, ExcludeNewerPackageEntry, ExcludeNewerTimestamp, ForkStrategy, PrereleaseMode,
+ ResolutionMode,
+};
use uv_static::EnvVars;
use uv_torch::TorchMode;
use uv_workspace::pyproject_mut::AddBoundsKind;
@@ -2749,7 +2752,16 @@ pub struct VenvArgs {
/// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same
/// format (e.g., `2006-12-02`) in your system's configured time zone.
#[arg(long, env = EnvVars::UV_EXCLUDE_NEWER)]
- pub exclude_newer: Option,
+ pub exclude_newer: Option,
+
+ /// Limit candidate packages for a specific package to those that were uploaded prior to the given date.
+ ///
+ /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 timestamp
+ /// (e.g., `2006-12-02T02:07:43Z`) or local date (e.g., `2006-12-02`) in your system's configured time zone.
+ ///
+ /// Can be provided multiple times for different packages.
+ #[arg(long)]
+ pub exclude_newer_package: Option>,
/// The method to use when installing packages from the global cache.
///
@@ -4777,7 +4789,16 @@ pub struct ToolUpgradeArgs {
/// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same
/// format (e.g., `2006-12-02`) in your system's configured time zone.
#[arg(long, env = EnvVars::UV_EXCLUDE_NEWER, help_heading = "Resolver options")]
- pub exclude_newer: Option,
+ pub exclude_newer: Option,
+
+ /// Limit candidate packages for specific packages to those that were uploaded prior to the given date.
+ ///
+ /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 timestamp
+ /// (e.g., `2006-12-02T02:07:43Z`) or local date (e.g., `2006-12-02`) in your system's configured time zone.
+ ///
+ /// Can be provided multiple times for different packages.
+ #[arg(long, help_heading = "Resolver options")]
+ pub exclude_newer_package: Option>,
/// The method to use when installing packages from the global cache.
///
@@ -5572,7 +5593,16 @@ pub struct InstallerArgs {
/// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same
/// format (e.g., `2006-12-02`) in your system's configured time zone.
#[arg(long, env = EnvVars::UV_EXCLUDE_NEWER, help_heading = "Resolver options")]
- pub exclude_newer: Option,
+ pub exclude_newer: Option,
+
+ /// Limit candidate packages for specific packages to those that were uploaded prior to the given date.
+ ///
+ /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 timestamp
+ /// (e.g., `2006-12-02T02:07:43Z`) or local date (e.g., `2006-12-02`) in your system's configured time zone.
+ ///
+ /// Can be provided multiple times for different packages.
+ #[arg(long, help_heading = "Resolver options")]
+ pub exclude_newer_package: Option>,
/// The method to use when installing packages from the global cache.
///
@@ -5773,7 +5803,16 @@ pub struct ResolverArgs {
/// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same
/// format (e.g., `2006-12-02`) in your system's configured time zone.
#[arg(long, env = EnvVars::UV_EXCLUDE_NEWER, help_heading = "Resolver options")]
- pub exclude_newer: Option,
+ pub exclude_newer: Option,
+
+ /// Limit candidate packages for a specific package to those that were uploaded prior to the given date.
+ ///
+ /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 timestamp
+ /// (e.g., `2006-12-02T02:07:43Z`) or local date (e.g., `2006-12-02`) in your system's configured time zone.
+ ///
+ /// Can be provided multiple times for different packages.
+ #[arg(long, help_heading = "Resolver options")]
+ pub exclude_newer_package: Option>,
/// The method to use when installing packages from the global cache.
///
@@ -5970,7 +6009,16 @@ pub struct ResolverInstallerArgs {
/// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same
/// format (e.g., `2006-12-02`) in your system's configured time zone.
#[arg(long, env = EnvVars::UV_EXCLUDE_NEWER, help_heading = "Resolver options")]
- pub exclude_newer: Option,
+ pub exclude_newer: Option,
+
+ /// Limit candidate packages for specific packages to those that were uploaded prior to the given date.
+ ///
+ /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 timestamp
+ /// (e.g., `2006-12-02T02:07:43Z`) or local date (e.g., `2006-12-02`) in your system's configured time zone.
+ ///
+ /// Can be provided multiple times for different packages.
+ #[arg(long, help_heading = "Resolver options")]
+ pub exclude_newer_package: Option>,
/// The method to use when installing packages from the global cache.
///
@@ -6059,7 +6107,7 @@ pub struct FetchArgs {
/// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same
/// format (e.g., `2006-12-02`) in your system's configured time zone.
#[arg(long, env = EnvVars::UV_EXCLUDE_NEWER, help_heading = "Resolver options")]
- pub exclude_newer: Option,
+ pub exclude_newer: Option,
}
#[derive(Args)]
diff --git a/crates/uv-cli/src/options.rs b/crates/uv-cli/src/options.rs
index d2e651a19..c5f94de1b 100644
--- a/crates/uv-cli/src/options.rs
+++ b/crates/uv-cli/src/options.rs
@@ -2,7 +2,7 @@ use anstream::eprintln;
use uv_cache::Refresh;
use uv_configuration::{ConfigSettings, PackageConfigSettings};
-use uv_resolver::PrereleaseMode;
+use uv_resolver::{ExcludeNewer, ExcludeNewerPackage, PrereleaseMode};
use uv_settings::{Combine, PipOptions, ResolverInstallerOptions, ResolverOptions};
use uv_warnings::owo_colors::OwoColorize;
@@ -69,6 +69,7 @@ impl From for PipOptions {
exclude_newer,
link_mode,
no_sources,
+ exclude_newer_package,
} = args;
Self {
@@ -93,6 +94,7 @@ impl From for PipOptions {
no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
no_build_isolation_package: Some(no_build_isolation_package),
exclude_newer,
+ exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
link_mode,
no_sources: if no_sources { Some(true) } else { None },
..PipOptions::from(index_args)
@@ -118,6 +120,7 @@ impl From for PipOptions {
compile_bytecode,
no_compile_bytecode,
no_sources,
+ exclude_newer_package,
} = args;
Self {
@@ -134,6 +137,7 @@ impl From for PipOptions {
}),
no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
exclude_newer,
+ exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
link_mode,
compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
no_sources: if no_sources { Some(true) } else { None },
@@ -168,6 +172,7 @@ impl From for PipOptions {
compile_bytecode,
no_compile_bytecode,
no_sources,
+ exclude_newer_package,
} = args;
Self {
@@ -194,6 +199,7 @@ impl From for PipOptions {
no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
no_build_isolation_package: Some(no_build_isolation_package),
exclude_newer,
+ exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
link_mode,
compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
no_sources: if no_sources { Some(true) } else { None },
@@ -285,6 +291,7 @@ pub fn resolver_options(
exclude_newer,
link_mode,
no_sources,
+ exclude_newer_package,
} = resolver_args;
let BuildOptionsArgs {
@@ -347,7 +354,10 @@ pub fn resolver_options(
}),
no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
no_build_isolation_package: Some(no_build_isolation_package),
- exclude_newer,
+ exclude_newer: ExcludeNewer::from_args(
+ exclude_newer,
+ exclude_newer_package.unwrap_or_default(),
+ ),
link_mode,
no_build: flag(no_build, build, "build"),
no_build_package: Some(no_build_package),
@@ -382,6 +392,7 @@ pub fn resolver_installer_options(
no_build_isolation_package,
build_isolation,
exclude_newer,
+ exclude_newer_package,
link_mode,
compile_bytecode,
no_compile_bytecode,
@@ -465,6 +476,7 @@ pub fn resolver_installer_options(
Some(no_build_isolation_package)
},
exclude_newer,
+ exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
link_mode,
compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
no_build: flag(no_build, build, "build"),
diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs
index aa48fecf7..5d0adb47b 100644
--- a/crates/uv-dispatch/src/lib.rs
+++ b/crates/uv-dispatch/src/lib.rs
@@ -93,7 +93,7 @@ pub struct BuildDispatch<'a> {
config_settings: &'a ConfigSettings,
config_settings_package: &'a PackageConfigSettings,
hasher: &'a HashStrategy,
- exclude_newer: Option,
+ exclude_newer: ExcludeNewer,
source_build_context: SourceBuildContext,
build_extra_env_vars: FxHashMap,
sources: SourceStrategy,
@@ -119,7 +119,7 @@ impl<'a> BuildDispatch<'a> {
link_mode: uv_install_wheel::LinkMode,
build_options: &'a BuildOptions,
hasher: &'a HashStrategy,
- exclude_newer: Option,
+ exclude_newer: ExcludeNewer,
sources: SourceStrategy,
workspace_cache: WorkspaceCache,
concurrency: Concurrency,
@@ -231,7 +231,7 @@ impl BuildContext for BuildDispatch<'_> {
let resolver = Resolver::new(
Manifest::simple(requirements.to_vec()).with_constraints(self.constraints.clone()),
OptionsBuilder::new()
- .exclude_newer(self.exclude_newer)
+ .exclude_newer(self.exclude_newer.clone())
.index_strategy(self.index_strategy)
.build_options(self.build_options.clone())
.flexibility(Flexibility::Fixed)
diff --git a/crates/uv-resolver/src/exclude_newer.rs b/crates/uv-resolver/src/exclude_newer.rs
index 65fa55cfe..7f4166f98 100644
--- a/crates/uv-resolver/src/exclude_newer.rs
+++ b/crates/uv-resolver/src/exclude_newer.rs
@@ -1,30 +1,35 @@
#[cfg(feature = "schemars")]
use std::borrow::Cow;
-use std::str::FromStr;
+use std::{
+ ops::{Deref, DerefMut},
+ str::FromStr,
+};
use jiff::{Timestamp, ToSpan, tz::TimeZone};
+use rustc_hash::FxHashMap;
+use uv_normalize::PackageName;
/// A timestamp that excludes files newer than it.
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
-pub struct ExcludeNewer(Timestamp);
+pub struct ExcludeNewerTimestamp(Timestamp);
-impl ExcludeNewer {
+impl ExcludeNewerTimestamp {
/// Returns the timestamp in milliseconds.
pub fn timestamp_millis(&self) -> i64 {
self.0.as_millisecond()
}
}
-impl From for ExcludeNewer {
+impl From for ExcludeNewerTimestamp {
fn from(timestamp: Timestamp) -> Self {
Self(timestamp)
}
}
-impl FromStr for ExcludeNewer {
+impl FromStr for ExcludeNewerTimestamp {
type Err = String;
- /// Parse an [`ExcludeNewer`] from a string.
+ /// Parse an [`ExcludeNewerTimestamp`] from a string.
///
/// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same
/// format (e.g., `2006-12-02`).
@@ -61,16 +66,174 @@ impl FromStr for ExcludeNewer {
}
}
-impl std::fmt::Display for ExcludeNewer {
+impl std::fmt::Display for ExcludeNewerTimestamp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
+/// A package-specific exclude-newer entry.
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+pub struct ExcludeNewerPackageEntry {
+ pub package: PackageName,
+ pub timestamp: ExcludeNewerTimestamp,
+}
+
+impl FromStr for ExcludeNewerPackageEntry {
+ type Err = String;
+
+ /// Parses a [`ExcludeNewerPackageEntry`] from a string in the format `PACKAGE=DATE`.
+ fn from_str(s: &str) -> Result {
+ let Some((package, date)) = s.split_once('=') else {
+ return Err(format!(
+ "Invalid `exclude-newer-package` value `{s}`: expected format `PACKAGE=DATE`"
+ ));
+ };
+
+ let package = PackageName::from_str(package).map_err(|err| {
+ format!("Invalid `exclude-newer-package` package name `{package}`: {err}")
+ })?;
+ let timestamp = ExcludeNewerTimestamp::from_str(date)
+ .map_err(|err| format!("Invalid `exclude-newer-package` timestamp `{date}`: {err}"))?;
+
+ Ok(Self { package, timestamp })
+ }
+}
+
+impl From<(PackageName, ExcludeNewerTimestamp)> for ExcludeNewerPackageEntry {
+ fn from((package, timestamp): (PackageName, ExcludeNewerTimestamp)) -> Self {
+ Self { package, timestamp }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
+#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+pub struct ExcludeNewerPackage(FxHashMap);
+
+impl Deref for ExcludeNewerPackage {
+ type Target = FxHashMap;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for ExcludeNewerPackage {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl FromIterator for ExcludeNewerPackage {
+ fn from_iter>(iter: T) -> Self {
+ Self(
+ iter.into_iter()
+ .map(|entry| (entry.package, entry.timestamp))
+ .collect(),
+ )
+ }
+}
+
+impl IntoIterator for ExcludeNewerPackage {
+ type Item = (PackageName, ExcludeNewerTimestamp);
+ type IntoIter = std::collections::hash_map::IntoIter;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.0.into_iter()
+ }
+}
+
+impl<'a> IntoIterator for &'a ExcludeNewerPackage {
+ type Item = (&'a PackageName, &'a ExcludeNewerTimestamp);
+ type IntoIter = std::collections::hash_map::Iter<'a, PackageName, ExcludeNewerTimestamp>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.0.iter()
+ }
+}
+
+impl ExcludeNewerPackage {
+ /// Convert to the inner `HashMap`.
+ pub fn into_inner(self) -> FxHashMap {
+ self.0
+ }
+}
+
+/// A setting that excludes files newer than a timestamp, at a global level or per-package.
+#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
+#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+pub struct ExcludeNewer {
+ /// Global timestamp that applies to all packages if no package-specific timestamp is set.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub global: Option,
+ /// Per-package timestamps that override the global timestamp.
+ #[serde(default, skip_serializing_if = "FxHashMap::is_empty")]
+ pub package: ExcludeNewerPackage,
+}
+
+impl ExcludeNewer {
+ /// Create a new exclude newer configuration with just a global timestamp.
+ pub fn global(global: ExcludeNewerTimestamp) -> Self {
+ Self {
+ global: Some(global),
+ package: ExcludeNewerPackage::default(),
+ }
+ }
+
+ /// Create a new exclude newer configuration.
+ pub fn new(global: Option, package: ExcludeNewerPackage) -> Self {
+ Self { global, package }
+ }
+
+ /// Create from CLI arguments.
+ pub fn from_args(
+ global: Option,
+ package: Vec,
+ ) -> Self {
+ let package: ExcludeNewerPackage = package.into_iter().collect();
+
+ Self { global, package }
+ }
+
+ /// Returns the timestamp for a specific package, falling back to the global timestamp if set.
+ pub fn exclude_newer_package(
+ &self,
+ package_name: &PackageName,
+ ) -> Option {
+ self.package.get(package_name).copied().or(self.global)
+ }
+
+ /// Returns true if this has any configuration (global or per-package).
+ pub fn is_empty(&self) -> bool {
+ self.global.is_none() && self.package.is_empty()
+ }
+}
+
+impl std::fmt::Display for ExcludeNewer {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ if let Some(global) = self.global {
+ write!(f, "global: {global}")?;
+ if !self.package.is_empty() {
+ write!(f, ", ")?;
+ }
+ }
+ let mut first = true;
+ for (name, timestamp) in &self.package {
+ if !first {
+ write!(f, ", ")?;
+ }
+ write!(f, "{name}: {timestamp}")?;
+ first = false;
+ }
+ Ok(())
+ }
+}
+
#[cfg(feature = "schemars")]
-impl schemars::JsonSchema for ExcludeNewer {
+impl schemars::JsonSchema for ExcludeNewerTimestamp {
fn schema_name() -> Cow<'static, str> {
- Cow::Borrowed("ExcludeNewer")
+ Cow::Borrowed("ExcludeNewerTimestamp")
}
fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs
index e91df3a7e..00cb9732e 100644
--- a/crates/uv-resolver/src/lib.rs
+++ b/crates/uv-resolver/src/lib.rs
@@ -1,6 +1,8 @@
pub use dependency_mode::DependencyMode;
pub use error::{ErrorTree, NoSolutionError, NoSolutionHeader, ResolveError, SentinelRange};
-pub use exclude_newer::ExcludeNewer;
+pub use exclude_newer::{
+ ExcludeNewer, ExcludeNewerPackage, ExcludeNewerPackageEntry, ExcludeNewerTimestamp,
+};
pub use exclusions::Exclusions;
pub use flat_index::{FlatDistributions, FlatIndex};
pub use fork_strategy::ForkStrategy;
diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs
index 2e3ee56d4..44aa915e6 100644
--- a/crates/uv-resolver/src/lock/mod.rs
+++ b/crates/uv-resolver/src/lock/mod.rs
@@ -60,7 +60,8 @@ pub use crate::lock::tree::TreeDisplay;
use crate::resolution::{AnnotatedDist, ResolutionGraphNode};
use crate::universal_marker::{ConflictMarker, UniversalMarker};
use crate::{
- ExcludeNewer, InMemoryIndex, MetadataResponse, PrereleaseMode, ResolutionMode, ResolverOutput,
+ ExcludeNewer, ExcludeNewerTimestamp, InMemoryIndex, MetadataResponse, PrereleaseMode,
+ ResolutionMode, ResolverOutput,
};
mod export;
@@ -72,7 +73,7 @@ mod tree;
pub const VERSION: u32 = 1;
/// The current revision of the lockfile format.
-const REVISION: u32 = 2;
+const REVISION: u32 = 3;
static LINUX_MARKERS: LazyLock = LazyLock::new(|| {
let pep508 = MarkerTree::from_str("os_name == 'posix' and sys_platform == 'linux'").unwrap();
@@ -278,11 +279,23 @@ impl Lock {
}
let packages = packages.into_values().collect();
+ let (exclude_newer, exclude_newer_package) = {
+ let exclude_newer = &resolution.options.exclude_newer;
+ let global_exclude_newer = exclude_newer.global;
+ let package_exclude_newer = if exclude_newer.package.is_empty() {
+ None
+ } else {
+ Some(exclude_newer.package.clone().into_inner())
+ };
+ (global_exclude_newer, package_exclude_newer)
+ };
+
let options = ResolverOptions {
resolution_mode: resolution.options.resolution_mode,
prerelease_mode: resolution.options.prerelease_mode,
fork_strategy: resolution.options.fork_strategy,
- exclude_newer: resolution.options.exclude_newer,
+ exclude_newer,
+ exclude_newer_package,
};
let lock = Self::new(
VERSION,
@@ -643,8 +656,8 @@ impl Lock {
}
/// Returns the exclude newer setting used to generate this lock.
- pub fn exclude_newer(&self) -> Option {
- self.options.exclude_newer
+ pub fn exclude_newer(&self) -> ExcludeNewer {
+ self.options.exclude_newer()
}
/// Returns the conflicting groups that were used to generate this lock.
@@ -890,8 +903,21 @@ impl Lock {
value(self.options.fork_strategy.to_string()),
);
}
- if let Some(exclude_newer) = self.options.exclude_newer {
- options_table.insert("exclude-newer", value(exclude_newer.to_string()));
+ let exclude_newer = &self.options.exclude_newer();
+ if !exclude_newer.is_empty() {
+ // Always serialize global exclude-newer as a string
+ if let Some(global) = exclude_newer.global {
+ options_table.insert("exclude-newer", value(global.to_string()));
+ }
+
+ // Serialize package-specific exclusions as a separate field
+ if !exclude_newer.package.is_empty() {
+ let mut package_table = toml_edit::Table::new();
+ for (name, timestamp) in &exclude_newer.package {
+ package_table.insert(name.as_ref(), value(timestamp.to_string()));
+ }
+ options_table.insert("exclude-newer-package", Item::Table(package_table));
+ }
}
if !options_table.is_empty() {
@@ -1870,8 +1896,25 @@ struct ResolverOptions {
/// The [`ForkStrategy`] used to generate this lock.
#[serde(default)]
fork_strategy: ForkStrategy,
- /// The [`ExcludeNewer`] used to generate this lock.
- exclude_newer: Option,
+ /// The global [`ExcludeNewer`] timestamp.
+ exclude_newer: Option,
+ /// Package-specific [`ExcludeNewer`] timestamps.
+ exclude_newer_package: Option>,
+}
+
+impl ResolverOptions {
+ /// Get the combined exclude-newer configuration.
+ fn exclude_newer(&self) -> ExcludeNewer {
+ ExcludeNewer::from_args(
+ self.exclude_newer,
+ self.exclude_newer_package
+ .clone()
+ .unwrap_or_default()
+ .into_iter()
+ .map(Into::into)
+ .collect(),
+ )
+ }
}
#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap
index dc113609c..1e0f3ef0f 100644
--- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap
+++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap
@@ -37,6 +37,7 @@ Ok(
prerelease_mode: IfNecessaryOrExplicit,
fork_strategy: RequiresPython,
exclude_newer: None,
+ exclude_newer_package: None,
},
packages: [
Package {
diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap
index 203aebdb5..123c3521b 100644
--- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap
+++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap
@@ -37,6 +37,7 @@ Ok(
prerelease_mode: IfNecessaryOrExplicit,
fork_strategy: RequiresPython,
exclude_newer: None,
+ exclude_newer_package: None,
},
packages: [
Package {
diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap
index 14ddaa43f..3c7d13be1 100644
--- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap
+++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap
@@ -37,6 +37,7 @@ Ok(
prerelease_mode: IfNecessaryOrExplicit,
fork_strategy: RequiresPython,
exclude_newer: None,
+ exclude_newer_package: None,
},
packages: [
Package {
diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap
index 811ac8260..c6fe9c4af 100644
--- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap
+++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap
@@ -37,6 +37,7 @@ Ok(
prerelease_mode: IfNecessaryOrExplicit,
fork_strategy: RequiresPython,
exclude_newer: None,
+ exclude_newer_package: None,
},
packages: [
Package {
diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap
index 811ac8260..c6fe9c4af 100644
--- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap
+++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap
@@ -37,6 +37,7 @@ Ok(
prerelease_mode: IfNecessaryOrExplicit,
fork_strategy: RequiresPython,
exclude_newer: None,
+ exclude_newer_package: None,
},
packages: [
Package {
diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_dynamic.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_dynamic.snap
index 0393168f6..b023d8238 100644
--- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_dynamic.snap
+++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_dynamic.snap
@@ -37,6 +37,7 @@ Ok(
prerelease_mode: IfNecessaryOrExplicit,
fork_strategy: RequiresPython,
exclude_newer: None,
+ exclude_newer_package: None,
},
packages: [
Package {
diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap
index 811ac8260..c6fe9c4af 100644
--- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap
+++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap
@@ -37,6 +37,7 @@ Ok(
prerelease_mode: IfNecessaryOrExplicit,
fork_strategy: RequiresPython,
exclude_newer: None,
+ exclude_newer_package: None,
},
packages: [
Package {
diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap
index df411251c..59786dddf 100644
--- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap
+++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap
@@ -37,6 +37,7 @@ Ok(
prerelease_mode: IfNecessaryOrExplicit,
fork_strategy: RequiresPython,
exclude_newer: None,
+ exclude_newer_package: None,
},
packages: [
Package {
diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap
index a0519d53a..337b3fea5 100644
--- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap
+++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap
@@ -37,6 +37,7 @@ Ok(
prerelease_mode: IfNecessaryOrExplicit,
fork_strategy: RequiresPython,
exclude_newer: None,
+ exclude_newer_package: None,
},
packages: [
Package {
diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap
index 4ac13fff9..2db372e3e 100644
--- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap
+++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap
@@ -37,6 +37,7 @@ Ok(
prerelease_mode: IfNecessaryOrExplicit,
fork_strategy: RequiresPython,
exclude_newer: None,
+ exclude_newer_package: None,
},
packages: [
Package {
diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap
index 0244bfc40..4c7642f83 100644
--- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap
+++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap
@@ -37,6 +37,7 @@ Ok(
prerelease_mode: IfNecessaryOrExplicit,
fork_strategy: RequiresPython,
exclude_newer: None,
+ exclude_newer_package: None,
},
packages: [
Package {
diff --git a/crates/uv-resolver/src/options.rs b/crates/uv-resolver/src/options.rs
index 176b32910..f7baa4ef2 100644
--- a/crates/uv-resolver/src/options.rs
+++ b/crates/uv-resolver/src/options.rs
@@ -12,7 +12,7 @@ pub struct Options {
pub prerelease_mode: PrereleaseMode,
pub dependency_mode: DependencyMode,
pub fork_strategy: ForkStrategy,
- pub exclude_newer: Option,
+ pub exclude_newer: ExcludeNewer,
pub index_strategy: IndexStrategy,
pub required_environments: SupportedEnvironments,
pub flexibility: Flexibility,
@@ -27,7 +27,7 @@ pub struct OptionsBuilder {
prerelease_mode: PrereleaseMode,
dependency_mode: DependencyMode,
fork_strategy: ForkStrategy,
- exclude_newer: Option,
+ exclude_newer: ExcludeNewer,
index_strategy: IndexStrategy,
required_environments: SupportedEnvironments,
flexibility: Flexibility,
@@ -71,7 +71,7 @@ impl OptionsBuilder {
/// Sets the exclusion date.
#[must_use]
- pub fn exclude_newer(mut self, exclude_newer: Option) -> Self {
+ pub fn exclude_newer(mut self, exclude_newer: ExcludeNewer) -> Self {
self.exclude_newer = exclude_newer;
self
}
diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs
index 14eff5a9d..769a748db 100644
--- a/crates/uv-resolver/src/resolver/mod.rs
+++ b/crates/uv-resolver/src/resolver/mod.rs
@@ -182,7 +182,7 @@ impl<'a, Context: BuildContext, InstalledPackages: InstalledPackagesProvider>
python_requirement.target(),
AllowedYanks::from_manifest(&manifest, &env, options.dependency_mode),
hasher,
- options.exclude_newer,
+ options.exclude_newer.clone(),
build_context.build_options(),
build_context.capabilities(),
);
@@ -366,7 +366,7 @@ impl ResolverState ResolverState,
+ exclude_newer: Option<&ExcludeNewer>,
visited: &FxHashSet,
) -> ResolveError {
err = NoSolutionError::collapse_local_version_segments(NoSolutionError::collapse_proxies(
@@ -2596,7 +2596,9 @@ impl ResolverState {
requires_python: RequiresPython,
allowed_yanks: AllowedYanks,
hasher: HashStrategy,
- exclude_newer: Option,
+ exclude_newer: ExcludeNewer,
build_options: &'a BuildOptions,
capabilities: &'a IndexCapabilities,
}
@@ -130,7 +130,7 @@ impl<'a, Context: BuildContext> DefaultResolverProvider<'a, Context> {
requires_python: &'a RequiresPython,
allowed_yanks: AllowedYanks,
hasher: &'a HashStrategy,
- exclude_newer: Option,
+ exclude_newer: ExcludeNewer,
build_options: &'a BuildOptions,
capabilities: &'a IndexCapabilities,
) -> Self {
@@ -184,7 +184,7 @@ impl ResolverProvider for DefaultResolverProvider<'_, Con
&self.requires_python,
&self.allowed_yanks,
&self.hasher,
- self.exclude_newer.as_ref(),
+ Some(&self.exclude_newer),
flat_index
.and_then(|flat_index| flat_index.get(package_name))
.cloned(),
diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs
index 63132ad0d..4b0e07ad8 100644
--- a/crates/uv-resolver/src/version_map.rs
+++ b/crates/uv-resolver/src/version_map.rs
@@ -22,7 +22,7 @@ use uv_types::HashStrategy;
use uv_warnings::warn_user_once;
use crate::flat_index::FlatDistributions;
-use crate::{ExcludeNewer, yanks::AllowedYanks};
+use crate::{ExcludeNewer, ExcludeNewerTimestamp, yanks::AllowedYanks};
/// A map from versions to distributions.
#[derive(Debug)]
@@ -112,7 +112,7 @@ impl VersionMap {
allowed_yanks: allowed_yanks.clone(),
hasher: hasher.clone(),
requires_python: requires_python.clone(),
- exclude_newer: exclude_newer.copied(),
+ exclude_newer: exclude_newer.and_then(|en| en.exclude_newer_package(package_name)),
}),
}
}
@@ -365,7 +365,7 @@ struct VersionMapLazy {
/// in the current environment.
tags: Option,
/// Whether files newer than this timestamp should be excluded or not.
- exclude_newer: Option,
+ exclude_newer: Option,
/// Which yanked versions are allowed
allowed_yanks: AllowedYanks,
/// The hashes of allowed distributions.
@@ -420,7 +420,7 @@ impl VersionMapLazy {
for (filename, file) in files.all() {
// Support resolving as if it were an earlier timestamp, at least as long files have
// upload time information.
- let (excluded, upload_time) = if let Some(exclude_newer) = self.exclude_newer {
+ let (excluded, upload_time) = if let Some(exclude_newer) = &self.exclude_newer {
match file.upload_time_utc_ms.as_ref() {
Some(&upload_time) if upload_time >= exclude_newer.timestamp_millis() => {
(true, Some(upload_time))
diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs
index 738b00ffe..a2d78e7b1 100644
--- a/crates/uv-settings/src/combine.rs
+++ b/crates/uv-settings/src/combine.rs
@@ -12,7 +12,10 @@ use uv_install_wheel::LinkMode;
use uv_pypi_types::{SchemaConflicts, SupportedEnvironments};
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
use uv_redacted::DisplaySafeUrl;
-use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode};
+use uv_resolver::{
+ AnnotationStyle, ExcludeNewer, ExcludeNewerPackage, ExcludeNewerTimestamp, ForkStrategy,
+ PrereleaseMode, ResolutionMode,
+};
use uv_torch::TorchMode;
use uv_workspace::pyproject_mut::AddBoundsKind;
@@ -78,6 +81,7 @@ macro_rules! impl_combine_or {
impl_combine_or!(AddBoundsKind);
impl_combine_or!(AnnotationStyle);
impl_combine_or!(ExcludeNewer);
+impl_combine_or!(ExcludeNewerTimestamp);
impl_combine_or!(ExportFormat);
impl_combine_or!(ForkStrategy);
impl_combine_or!(Index);
@@ -120,6 +124,22 @@ impl Combine for Option> {
}
}
+impl Combine for Option {
+ /// Combine two [`ExcludeNewerPackage`] instances by merging them, with the values in `self` taking precedence.
+ fn combine(self, other: Option) -> Option {
+ match (self, other) {
+ (Some(mut a), Some(b)) => {
+ // Extend with values from b, but a takes precedence (we don't overwrite existing keys)
+ for (key, value) in b {
+ a.entry(key).or_insert(value);
+ }
+ Some(a)
+ }
+ (a, b) => a.or(b),
+ }
+ }
+}
+
impl Combine for Option {
/// Combine two maps by merging the map in `self` with the map in `other`, if they're both
/// `Some`.
@@ -153,3 +173,22 @@ impl Combine for Option {
self
}
}
+
+impl Combine for ExcludeNewer {
+ fn combine(mut self, other: Self) -> Self {
+ self.global = self.global.combine(other.global);
+
+ if !other.package.is_empty() {
+ if self.package.is_empty() {
+ self.package = other.package;
+ } else {
+ // Merge package-specific timestamps, with self taking precedence
+ for (pkg, timestamp) in &other.package {
+ self.package.entry(pkg.clone()).or_insert(*timestamp);
+ }
+ }
+ }
+
+ self
+ }
+}
diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs
index 0e3f13f71..f8385d006 100644
--- a/crates/uv-settings/src/lib.rs
+++ b/crates/uv-settings/src/lib.rs
@@ -318,6 +318,7 @@ fn warn_uv_toml_masked_fields(options: &Options) {
no_build_isolation,
no_build_isolation_package,
exclude_newer,
+ exclude_newer_package,
link_mode,
compile_bytecode,
no_sources,
@@ -447,6 +448,9 @@ fn warn_uv_toml_masked_fields(options: &Options) {
if exclude_newer.is_some() {
masked_fields.push("exclude-newer");
}
+ if exclude_newer_package.is_some() {
+ masked_fields.push("exclude-newer-package");
+ }
if link_mode.is_some() {
masked_fields.push("link-mode");
}
diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs
index 9eb765a1e..6062d5d0e 100644
--- a/crates/uv-settings/src/settings.rs
+++ b/crates/uv-settings/src/settings.rs
@@ -12,12 +12,16 @@ use uv_distribution_types::{
};
use uv_install_wheel::LinkMode;
use uv_macros::{CombineOptions, OptionsMetadata};
+
use uv_normalize::{ExtraName, PackageName, PipGroupName};
use uv_pep508::Requirement;
use uv_pypi_types::{SupportedEnvironments, VerbatimParsedUrl};
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
use uv_redacted::DisplaySafeUrl;
-use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode};
+use uv_resolver::{
+ AnnotationStyle, ExcludeNewer, ExcludeNewerPackage, ExcludeNewerTimestamp, ForkStrategy,
+ PrereleaseMode, ResolutionMode,
+};
use uv_static::EnvVars;
use uv_torch::TorchMode;
use uv_workspace::pyproject_mut::AddBoundsKind;
@@ -333,7 +337,7 @@ pub struct InstallerOptions {
pub index_strategy: Option,
pub keyring_provider: Option,
pub config_settings: Option,
- pub exclude_newer: Option,
+ pub exclude_newer: Option,
pub link_mode: Option,
pub compile_bytecode: Option,
pub reinstall: Option,
@@ -362,7 +366,7 @@ pub struct ResolverOptions {
pub dependency_metadata: Option>,
pub config_settings: Option,
pub config_settings_package: Option,
- pub exclude_newer: Option,
+ pub exclude_newer: ExcludeNewer,
pub link_mode: Option,
pub upgrade: Option,
pub upgrade_package: Option>>,
@@ -636,7 +640,18 @@ pub struct ResolverInstallerOptions {
exclude-newer = "2006-12-02T02:07:43Z"
"#
)]
- pub exclude_newer: Option,
+ pub exclude_newer: Option,
+ /// Limit candidate packages for specific packages to those that were uploaded prior to the given date.
+ ///
+ /// Accepts package-date pairs in a dictionary format.
+ #[option(
+ default = "None",
+ value_type = "dict",
+ example = r#"
+ exclude-newer-package = { tqdm = "2022-04-04T00:00:00Z" }
+ "#
+ )]
+ pub exclude_newer_package: Option,
/// The method to use when installing packages from the global cache.
///
/// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and
@@ -1409,7 +1424,18 @@ pub struct PipOptions {
exclude-newer = "2006-12-02T02:07:43Z"
"#
)]
- pub exclude_newer: Option,
+ pub exclude_newer: Option,
+ /// Limit candidate packages for specific packages to those that were uploaded prior to the given date.
+ ///
+ /// Accepts package-date pairs in a dictionary format.
+ #[option(
+ default = "None",
+ value_type = "dict",
+ example = r#"
+ exclude-newer-package = { tqdm = "2022-04-04T00:00:00Z" }
+ "#
+ )]
+ pub exclude_newer_package: Option,
/// Specify a package to omit from the output resolution. Its dependencies will still be
/// included in the resolution. Equivalent to pip-compile's `--unsafe-package` option.
#[option(
@@ -1675,7 +1701,15 @@ impl From for ResolverOptions {
dependency_metadata: value.dependency_metadata,
config_settings: value.config_settings,
config_settings_package: value.config_settings_package,
- exclude_newer: value.exclude_newer,
+ exclude_newer: ExcludeNewer::from_args(
+ value.exclude_newer,
+ value
+ .exclude_newer_package
+ .unwrap_or_default()
+ .into_iter()
+ .map(Into::into)
+ .collect(),
+ ),
link_mode: value.link_mode,
upgrade: value.upgrade,
upgrade_package: value.upgrade_package,
@@ -1701,7 +1735,16 @@ impl From for InstallerOptions {
index_strategy: value.index_strategy,
keyring_provider: value.keyring_provider,
config_settings: value.config_settings,
- exclude_newer: value.exclude_newer,
+ exclude_newer: ExcludeNewer::from_args(
+ value.exclude_newer,
+ value
+ .exclude_newer_package
+ .unwrap_or_default()
+ .into_iter()
+ .map(Into::into)
+ .collect(),
+ )
+ .global,
link_mode: value.link_mode,
compile_bytecode: value.compile_bytecode,
reinstall: value.reinstall,
@@ -1741,7 +1784,8 @@ pub struct ToolOptions {
pub config_settings_package: Option,
pub no_build_isolation: Option,
pub no_build_isolation_package: Option>,
- pub exclude_newer: Option,
+ pub exclude_newer: Option,
+ pub exclude_newer_package: Option,
pub link_mode: Option,
pub compile_bytecode: Option,
pub no_sources: Option,
@@ -1770,6 +1814,7 @@ impl From for ToolOptions {
no_build_isolation: value.no_build_isolation,
no_build_isolation_package: value.no_build_isolation_package,
exclude_newer: value.exclude_newer,
+ exclude_newer_package: value.exclude_newer_package,
link_mode: value.link_mode,
compile_bytecode: value.compile_bytecode,
no_sources: value.no_sources,
@@ -1800,6 +1845,7 @@ impl From for ResolverInstallerOptions {
no_build_isolation: value.no_build_isolation,
no_build_isolation_package: value.no_build_isolation_package,
exclude_newer: value.exclude_newer,
+ exclude_newer_package: value.exclude_newer_package,
link_mode: value.link_mode,
compile_bytecode: value.compile_bytecode,
no_sources: value.no_sources,
@@ -1852,7 +1898,8 @@ pub struct OptionsWire {
config_settings_package: Option,
no_build_isolation: Option,
no_build_isolation_package: Option>,
- exclude_newer: Option,
+ exclude_newer: Option,
+ exclude_newer_package: Option,
link_mode: Option,
compile_bytecode: Option,
no_sources: Option,
@@ -1943,6 +1990,7 @@ impl From for Options {
no_build_isolation,
no_build_isolation_package,
exclude_newer,
+ exclude_newer_package,
link_mode,
compile_bytecode,
no_sources,
@@ -2010,6 +2058,7 @@ impl From for Options {
no_build_isolation,
no_build_isolation_package,
exclude_newer,
+ exclude_newer_package,
link_mode,
compile_bytecode,
no_sources,
diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs
index 24cda1cf3..78e27e975 100644
--- a/crates/uv/src/commands/build_frontend.rs
+++ b/crates/uv/src/commands/build_frontend.rs
@@ -348,7 +348,7 @@ async fn build_impl(
no_build_isolation_package,
*index_strategy,
*keyring_provider,
- *exclude_newer,
+ exclude_newer.clone(),
*sources,
concurrency,
build_options,
@@ -426,7 +426,7 @@ async fn build_package(
no_build_isolation_package: &[PackageName],
index_strategy: IndexStrategy,
keyring_provider: KeyringProviderType,
- exclude_newer: Option,
+ exclude_newer: ExcludeNewer,
sources: SourceStrategy,
concurrency: Concurrency,
build_options: &BuildOptions,
diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs
index 8c512a2e9..dba91e106 100644
--- a/crates/uv/src/commands/pip/compile.rs
+++ b/crates/uv/src/commands/pip/compile.rs
@@ -99,7 +99,7 @@ pub(crate) async fn pip_compile(
mut python_version: Option,
python_platform: Option,
universal: bool,
- exclude_newer: Option,
+ exclude_newer: ExcludeNewer,
sources: SourceStrategy,
annotation_style: AnnotationStyle,
link_mode: LinkMode,
@@ -485,7 +485,7 @@ pub(crate) async fn pip_compile(
link_mode,
&build_options,
&build_hashes,
- exclude_newer,
+ exclude_newer.clone(),
sources,
WorkspaceCache::default(),
concurrency,
@@ -497,7 +497,7 @@ pub(crate) async fn pip_compile(
.prerelease_mode(prerelease_mode)
.fork_strategy(fork_strategy)
.dependency_mode(dependency_mode)
- .exclude_newer(exclude_newer)
+ .exclude_newer(exclude_newer.clone())
.index_strategy(index_strategy)
.torch_backend(torch_backend)
.build_options(build_options.clone())
diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs
index d3917db11..39092d03f 100644
--- a/crates/uv/src/commands/pip/install.rs
+++ b/crates/uv/src/commands/pip/install.rs
@@ -83,7 +83,7 @@ pub(crate) async fn pip_install(
python_version: Option,
python_platform: Option,
strict: bool,
- exclude_newer: Option,
+ exclude_newer: ExcludeNewer,
sources: SourceStrategy,
python: Option,
system: bool,
@@ -429,7 +429,7 @@ pub(crate) async fn pip_install(
link_mode,
&build_options,
&build_hasher,
- exclude_newer,
+ exclude_newer.clone(),
sources,
WorkspaceCache::default(),
concurrency,
diff --git a/crates/uv/src/commands/pip/latest.rs b/crates/uv/src/commands/pip/latest.rs
index 25da8466c..87e26f3f5 100644
--- a/crates/uv/src/commands/pip/latest.rs
+++ b/crates/uv/src/commands/pip/latest.rs
@@ -13,12 +13,12 @@ use uv_warnings::warn_user_once;
///
/// The returned distribution is guaranteed to be compatible with the provided tags and Python
/// requirement.
-#[derive(Debug, Copy, Clone)]
+#[derive(Debug, Clone)]
pub(crate) struct LatestClient<'env> {
pub(crate) client: &'env RegistryClient,
pub(crate) capabilities: &'env IndexCapabilities,
pub(crate) prerelease: PrereleaseMode,
- pub(crate) exclude_newer: Option,
+ pub(crate) exclude_newer: ExcludeNewer,
pub(crate) tags: Option<&'env Tags>,
pub(crate) requires_python: &'env RequiresPython,
}
@@ -70,7 +70,7 @@ impl LatestClient<'_> {
for (filename, file) in files.all() {
// Skip distributions uploaded after the cutoff.
- if let Some(exclude_newer) = self.exclude_newer {
+ if let Some(exclude_newer) = self.exclude_newer.exclude_newer_package(package) {
match file.upload_time_utc_ms.as_ref() {
Some(&upload_time)
if upload_time >= exclude_newer.timestamp_millis() =>
@@ -79,8 +79,9 @@ impl LatestClient<'_> {
}
None => {
warn_user_once!(
- "{} is missing an upload date, but user provided: {exclude_newer}",
+ "{} is missing an upload date, but user provided: {}",
file.filename,
+ self.exclude_newer
);
}
_ => {}
diff --git a/crates/uv/src/commands/pip/list.rs b/crates/uv/src/commands/pip/list.rs
index 9205268ba..fb7f69011 100644
--- a/crates/uv/src/commands/pip/list.rs
+++ b/crates/uv/src/commands/pip/list.rs
@@ -49,7 +49,7 @@ pub(crate) async fn pip_list(
network_settings: &NetworkSettings,
concurrency: Concurrency,
strict: bool,
- exclude_newer: Option,
+ exclude_newer: ExcludeNewer,
python: Option<&str>,
system: bool,
cache: &Cache,
diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs
index 2053f535b..9e8943d64 100644
--- a/crates/uv/src/commands/pip/sync.rs
+++ b/crates/uv/src/commands/pip/sync.rs
@@ -71,7 +71,7 @@ pub(crate) async fn pip_sync(
python_version: Option,
python_platform: Option,
strict: bool,
- exclude_newer: Option,
+ exclude_newer: ExcludeNewer,
python: Option,
system: bool,
break_system_packages: bool,
@@ -364,7 +364,7 @@ pub(crate) async fn pip_sync(
link_mode,
&build_options,
&build_hasher,
- exclude_newer,
+ exclude_newer.clone(),
sources,
WorkspaceCache::default(),
concurrency,
diff --git a/crates/uv/src/commands/pip/tree.rs b/crates/uv/src/commands/pip/tree.rs
index 8b0aa0c3a..2d6c8a4f7 100644
--- a/crates/uv/src/commands/pip/tree.rs
+++ b/crates/uv/src/commands/pip/tree.rs
@@ -47,7 +47,7 @@ pub(crate) async fn pip_tree(
network_settings: NetworkSettings,
concurrency: Concurrency,
strict: bool,
- exclude_newer: Option,
+ exclude_newer: ExcludeNewer,
python: Option<&str>,
system: bool,
cache: &Cache,
diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs
index 2b7938da6..45727b53e 100644
--- a/crates/uv/src/commands/project/add.rs
+++ b/crates/uv/src/commands/project/add.rs
@@ -444,7 +444,7 @@ pub(crate) async fn add(
settings.resolver.link_mode,
&settings.resolver.build_options,
&build_hasher,
- settings.resolver.exclude_newer,
+ settings.resolver.exclude_newer.clone(),
sources,
// No workspace caching since `uv add` changes the workspace definition.
WorkspaceCache::default(),
@@ -1282,6 +1282,7 @@ impl PythonTarget {
/// Represents the destination where dependencies are added, either to a project or a script.
#[derive(Debug, Clone)]
+#[allow(clippy::large_enum_variant)]
pub(super) enum AddTarget {
/// A PEP 723 script, with inline metadata.
Script(Pep723Script, Box),
@@ -1382,6 +1383,7 @@ impl AddTarget {
}
#[derive(Debug, Clone)]
+#[allow(clippy::large_enum_variant)]
enum AddTargetSnapshot {
Script(Pep723Script, Option>),
Project(VirtualProject, Option>),
diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs
index 6b06e493a..df839be08 100644
--- a/crates/uv/src/commands/project/export.rs
+++ b/crates/uv/src/commands/project/export.rs
@@ -32,6 +32,7 @@ use crate::printer::Printer;
use crate::settings::{NetworkSettings, ResolverSettings};
#[derive(Debug, Clone)]
+#[allow(clippy::large_enum_variant)]
enum ExportTarget {
/// A PEP 723 script, with inline metadata.
Script(Pep723Script),
diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs
index 8eb0d4869..fd9536b64 100644
--- a/crates/uv/src/commands/project/lock.rs
+++ b/crates/uv/src/commands/project/lock.rs
@@ -641,7 +641,7 @@ async fn do_lock(
.resolution_mode(*resolution)
.prerelease_mode(*prerelease)
.fork_strategy(*fork_strategy)
- .exclude_newer(*exclude_newer)
+ .exclude_newer(exclude_newer.clone())
.index_strategy(*index_strategy)
.build_options(build_options.clone())
.required_environments(required_environments.cloned().unwrap_or_default())
@@ -680,7 +680,7 @@ async fn do_lock(
*link_mode,
build_options,
&build_hasher,
- *exclude_newer,
+ exclude_newer.clone(),
*sources,
workspace_cache.clone(),
concurrency,
@@ -958,31 +958,37 @@ impl ValidatedLock {
);
return Ok(Self::Unusable(lock));
}
- match (lock.exclude_newer(), options.exclude_newer) {
- (None, None) => (),
- (Some(existing), Some(provided)) if existing == provided => (),
- (Some(existing), Some(provided)) => {
+ let lock_exclude_newer = lock.exclude_newer();
+ let options_exclude_newer = &options.exclude_newer;
+
+ match (
+ lock_exclude_newer.is_empty(),
+ options_exclude_newer.is_empty(),
+ ) {
+ (true, true) => (),
+ (false, false) if lock_exclude_newer == *options_exclude_newer => (),
+ (false, false) => {
let _ = writeln!(
printer.stderr(),
"Ignoring existing lockfile due to change in timestamp cutoff: `{}` vs. `{}`",
- existing.cyan(),
- provided.cyan()
+ lock_exclude_newer.cyan(),
+ options_exclude_newer.cyan()
);
return Ok(Self::Unusable(lock));
}
- (Some(existing), None) => {
+ (false, true) => {
let _ = writeln!(
printer.stderr(),
"Ignoring existing lockfile due to removal of timestamp cutoff: `{}`",
- existing.cyan(),
+ lock_exclude_newer.cyan(),
);
return Ok(Self::Unusable(lock));
}
- (None, Some(provided)) => {
+ (true, false) => {
let _ = writeln!(
printer.stderr(),
"Ignoring existing lockfile due to addition of timestamp cutoff: `{}`",
- provided.cyan()
+ options_exclude_newer.cyan()
);
return Ok(Self::Unusable(lock));
}
diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs
index a5953cd76..052d41bea 100644
--- a/crates/uv/src/commands/project/mod.rs
+++ b/crates/uv/src/commands/project/mod.rs
@@ -1756,7 +1756,7 @@ pub(crate) async fn resolve_names(
*link_mode,
build_options,
&build_hasher,
- *exclude_newer,
+ exclude_newer.clone(),
*sources,
workspace_cache.clone(),
concurrency,
@@ -1901,7 +1901,7 @@ pub(crate) async fn resolve_environment(
.resolution_mode(*resolution)
.prerelease_mode(*prerelease)
.fork_strategy(*fork_strategy)
- .exclude_newer(*exclude_newer)
+ .exclude_newer(exclude_newer.clone())
.index_strategy(*index_strategy)
.build_options(build_options.clone())
.build();
@@ -1964,7 +1964,7 @@ pub(crate) async fn resolve_environment(
*link_mode,
build_options,
&build_hasher,
- *exclude_newer,
+ exclude_newer.clone(),
*sources,
workspace_cache,
concurrency,
@@ -2283,7 +2283,7 @@ pub(crate) async fn update_environment(
.resolution_mode(*resolution)
.prerelease_mode(*prerelease)
.fork_strategy(*fork_strategy)
- .exclude_newer(*exclude_newer)
+ .exclude_newer(exclude_newer.clone())
.index_strategy(*index_strategy)
.build_options(build_options.clone())
.build();
@@ -2326,7 +2326,7 @@ pub(crate) async fn update_environment(
*link_mode,
build_options,
&build_hasher,
- *exclude_newer,
+ exclude_newer.clone(),
*sources,
workspace_cache,
concurrency,
diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs
index 1af4c818f..50c498833 100644
--- a/crates/uv/src/commands/project/remove.rs
+++ b/crates/uv/src/commands/project/remove.rs
@@ -386,6 +386,7 @@ pub(crate) async fn remove(
/// Represents the destination where dependencies are added, either to a project or a script.
#[derive(Debug)]
+#[allow(clippy::large_enum_variant)]
enum RemoveTarget {
/// A PEP 723 script, with inline metadata.
Project(VirtualProject),
diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs
index 7d1b0f3ce..197fbc343 100644
--- a/crates/uv/src/commands/project/sync.rs
+++ b/crates/uv/src/commands/project/sync.rs
@@ -494,6 +494,7 @@ fn identify_installation_target<'a>(
}
#[derive(Debug, Clone)]
+#[allow(clippy::large_enum_variant)]
enum SyncTarget {
/// Sync a project environment.
Project(VirtualProject),
diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs
index 1f2ce3dfb..f391ef499 100644
--- a/crates/uv/src/commands/venv.rs
+++ b/crates/uv/src/commands/venv.rs
@@ -75,7 +75,7 @@ pub(crate) async fn venv(
system_site_packages: bool,
seed: bool,
on_existing: OnExisting,
- exclude_newer: Option,
+ exclude_newer: ExcludeNewer,
concurrency: Concurrency,
no_config: bool,
no_project: bool,
diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs
index b563b0b8e..bc09e8257 100644
--- a/crates/uv/src/settings.rs
+++ b/crates/uv/src/settings.rs
@@ -35,7 +35,8 @@ use uv_pypi_types::SupportedEnvironments;
use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target};
use uv_redacted::DisplaySafeUrl;
use uv_resolver::{
- AnnotationStyle, DependencyMode, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode,
+ AnnotationStyle, DependencyMode, ExcludeNewer, ExcludeNewerPackage, ForkStrategy,
+ PrereleaseMode, ResolutionMode,
};
use uv_settings::{
Combine, EnvironmentOptions, FilesystemOptions, Options, PipOptions, PublishOptions,
@@ -723,6 +724,7 @@ impl ToolUpgradeSettings {
compile_bytecode,
no_compile_bytecode,
no_sources,
+ exclude_newer_package,
build,
} = args;
@@ -754,6 +756,7 @@ impl ToolUpgradeSettings {
no_build_isolation_package,
build_isolation,
exclude_newer,
+ exclude_newer_package,
link_mode,
compile_bytecode,
no_compile_bytecode,
@@ -2666,6 +2669,7 @@ impl VenvSettings {
link_mode,
refresh,
compat_args: _,
+ exclude_newer_package,
} = args;
Self {
@@ -2685,6 +2689,8 @@ impl VenvSettings {
index_strategy,
keyring_provider,
exclude_newer,
+ exclude_newer_package: exclude_newer_package
+ .map(ExcludeNewerPackage::from_iter),
link_mode,
..PipOptions::from(index_args)
},
@@ -2708,7 +2714,7 @@ pub(crate) struct InstallerSettingsRef<'a> {
pub(crate) config_settings_package: &'a PackageConfigSettings,
pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: &'a [PackageName],
- pub(crate) exclude_newer: Option,
+ pub(crate) exclude_newer: ExcludeNewer,
pub(crate) link_mode: LinkMode,
pub(crate) compile_bytecode: bool,
pub(crate) reinstall: &'a Reinstall,
@@ -2726,7 +2732,7 @@ pub(crate) struct ResolverSettings {
pub(crate) config_setting: ConfigSettings,
pub(crate) config_settings_package: PackageConfigSettings,
pub(crate) dependency_metadata: DependencyMetadata,
- pub(crate) exclude_newer: Option,
+ pub(crate) exclude_newer: ExcludeNewer,
pub(crate) fork_strategy: ForkStrategy,
pub(crate) index_locations: IndexLocations,
pub(crate) index_strategy: IndexStrategy,
@@ -2867,7 +2873,15 @@ impl From for ResolverInstallerSettings {
dependency_metadata: DependencyMetadata::from_entries(
value.dependency_metadata.into_iter().flatten(),
),
- exclude_newer: value.exclude_newer,
+ exclude_newer: ExcludeNewer::from_args(
+ value.exclude_newer,
+ value
+ .exclude_newer_package
+ .unwrap_or_default()
+ .into_iter()
+ .map(Into::into)
+ .collect(),
+ ),
fork_strategy: value.fork_strategy.unwrap_or_default(),
index_locations,
index_strategy: value.index_strategy.unwrap_or_default(),
@@ -2937,7 +2951,7 @@ pub(crate) struct PipSettings {
pub(crate) python_version: Option,
pub(crate) python_platform: Option,
pub(crate) universal: bool,
- pub(crate) exclude_newer: Option,
+ pub(crate) exclude_newer: ExcludeNewer,
pub(crate) no_emit_package: Vec,
pub(crate) emit_index_url: bool,
pub(crate) emit_find_links: bool,
@@ -3024,6 +3038,7 @@ impl PipSettings {
upgrade_package,
reinstall,
reinstall_package,
+ exclude_newer_package,
} = pip.unwrap_or_default();
let ResolverInstallerOptions {
@@ -3054,6 +3069,7 @@ impl PipSettings {
no_build_package: top_level_no_build_package,
no_binary: top_level_no_binary,
no_binary_package: top_level_no_binary_package,
+ exclude_newer_package: top_level_exclude_newer_package,
} = top_level;
// Merge the top-level options (`tool.uv`) with the pip-specific options (`tool.uv.pip`),
@@ -3077,7 +3093,15 @@ impl PipSettings {
let no_build_isolation = no_build_isolation.combine(top_level_no_build_isolation);
let no_build_isolation_package =
no_build_isolation_package.combine(top_level_no_build_isolation_package);
- let exclude_newer = exclude_newer.combine(top_level_exclude_newer);
+ let exclude_newer = args
+ .exclude_newer
+ .combine(exclude_newer)
+ .combine(top_level_exclude_newer);
+ let exclude_newer_package = args
+ .exclude_newer_package
+ .combine(exclude_newer_package)
+ .combine(top_level_exclude_newer_package)
+ .unwrap_or_default();
let link_mode = link_mode.combine(top_level_link_mode);
let compile_bytecode = compile_bytecode.combine(top_level_compile_bytecode);
let no_sources = no_sources.combine(top_level_no_sources);
@@ -3184,7 +3208,10 @@ impl PipSettings {
python_version: args.python_version.combine(python_version),
python_platform: args.python_platform.combine(python_platform),
universal: args.universal.combine(universal).unwrap_or_default(),
- exclude_newer: args.exclude_newer.combine(exclude_newer),
+ exclude_newer: ExcludeNewer::from_args(
+ exclude_newer,
+ exclude_newer_package.into_iter().map(Into::into).collect(),
+ ),
no_emit_package: args
.no_emit_package
.combine(no_emit_package)
@@ -3276,7 +3303,7 @@ impl<'a> From<&'a ResolverInstallerSettings> for InstallerSettingsRef<'a> {
config_settings_package: &settings.resolver.config_settings_package,
no_build_isolation: settings.resolver.no_build_isolation,
no_build_isolation_package: &settings.resolver.no_build_isolation_package,
- exclude_newer: settings.resolver.exclude_newer,
+ exclude_newer: settings.resolver.exclude_newer.clone(),
link_mode: settings.resolver.link_mode,
compile_bytecode: settings.compile_bytecode,
reinstall: &settings.reinstall,
diff --git a/crates/uv/tests/it/branching_urls.rs b/crates/uv/tests/it/branching_urls.rs
index aa6edd090..29493ef14 100644
--- a/crates/uv/tests/it/branching_urls.rs
+++ b/crates/uv/tests/it/branching_urls.rs
@@ -212,7 +212,7 @@ fn root_package_splits_transitive_too() -> Result<()> {
assert_snapshot!(context.read("uv.lock"), @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11, <3.13"
resolution-markers = [
"python_full_version >= '3.12'",
@@ -409,7 +409,7 @@ fn root_package_splits_other_dependencies_too() -> Result<()> {
assert_snapshot!(context.read("uv.lock"), @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11, <3.13"
resolution-markers = [
"python_full_version >= '3.12'",
@@ -572,7 +572,7 @@ fn branching_between_registry_and_direct_url() -> Result<()> {
// We have source dist and wheel for the registry, but only the wheel for the direct URL.
assert_snapshot!(context.read("uv.lock"), @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11, <3.13"
resolution-markers = [
"python_full_version >= '3.12'",
@@ -659,7 +659,7 @@ fn branching_urls_of_different_sources_disjoint() -> Result<()> {
// We have source dist and wheel for the registry, but only the wheel for the direct URL.
assert_snapshot!(context.read("uv.lock"), @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11, <3.13"
resolution-markers = [
"python_full_version >= '3.12'",
@@ -789,7 +789,7 @@ fn dont_pre_visit_url_packages() -> Result<()> {
assert_snapshot!(context.read("uv.lock"), @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11, <3.13"
[options]
diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs
index 7fa46f0c3..92d0d7b6a 100644
--- a/crates/uv/tests/it/edit.rs
+++ b/crates/uv/tests/it/edit.rs
@@ -76,7 +76,7 @@ fn add_registry() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -230,7 +230,7 @@ fn add_git() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -361,7 +361,7 @@ fn add_git_private_source() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -456,7 +456,7 @@ fn add_git_private_raw() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -739,7 +739,7 @@ fn add_git_raw() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -1033,7 +1033,7 @@ fn add_unnamed() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -1129,7 +1129,7 @@ fn add_remove_dev() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -1246,7 +1246,7 @@ fn add_remove_dev() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -1336,7 +1336,7 @@ fn add_remove_optional() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -1453,7 +1453,7 @@ fn add_remove_optional() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -1689,7 +1689,7 @@ fn add_remove_workspace() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -1773,7 +1773,7 @@ fn add_remove_workspace() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -2315,7 +2315,7 @@ fn add_workspace_editable() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -2447,7 +2447,7 @@ fn add_workspace_path() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -2575,7 +2575,7 @@ fn add_path_implicit_workspace() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -2697,7 +2697,7 @@ fn add_path_no_workspace() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -2811,7 +2811,7 @@ fn add_path_adjacent_directory() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -2986,7 +2986,7 @@ fn update() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -3754,7 +3754,7 @@ fn add_inexact() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -3882,7 +3882,7 @@ fn remove_registry() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -4518,7 +4518,7 @@ fn add_lower_bound_optional() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -4631,7 +4631,7 @@ fn add_lower_bound_local() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[[package]]
@@ -4733,7 +4733,7 @@ fn add_non_project() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -5131,7 +5131,7 @@ fn add_requirements_file_constraints() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -6069,7 +6069,7 @@ fn add_script_settings() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11"
[options]
@@ -6612,7 +6612,7 @@ fn add_remove_script_lock() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11"
[options]
@@ -6795,7 +6795,7 @@ fn add_remove_script_lock() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11"
[options]
@@ -7000,7 +7000,7 @@ fn add_remove_script_lock() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11"
[options]
@@ -8629,7 +8629,7 @@ fn add_warn_index_url() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -8731,7 +8731,7 @@ fn add_no_warn_index_url() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -8824,7 +8824,7 @@ fn add_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -8910,7 +8910,7 @@ fn add_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9022,7 +9022,7 @@ fn add_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9142,7 +9142,7 @@ fn add_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9271,7 +9271,7 @@ fn add_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9407,7 +9407,7 @@ fn add_default_index_url() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9480,7 +9480,7 @@ fn add_default_index_url() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9581,7 +9581,7 @@ fn add_index_credentials() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9677,7 +9677,7 @@ fn existing_index_credentials() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9770,7 +9770,7 @@ fn add_index_with_trailing_slash() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9866,7 +9866,7 @@ fn add_index_without_trailing_slash() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -10142,7 +10142,7 @@ fn add_group_comment() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11"
[options]
@@ -10276,7 +10276,7 @@ fn add_index_comments() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -10596,7 +10596,7 @@ fn add_direct_url_subdirectory() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -10726,7 +10726,7 @@ fn add_direct_url_subdirectory_raw() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -11374,7 +11374,7 @@ fn multiple_index_cli() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -11485,7 +11485,7 @@ fn repeated_index_cli_environment_variable() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -11591,7 +11591,7 @@ fn repeated_index_cli_environment_variable_newline() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -11701,7 +11701,7 @@ fn repeated_index_cli() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -11811,7 +11811,7 @@ fn repeated_index_cli_reversed() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs
index b48536f2e..dfa251ab7 100644
--- a/crates/uv/tests/it/export.rs
+++ b/crates/uv/tests/it/export.rs
@@ -583,7 +583,7 @@ fn requirements_txt_dependency_conflicting_markers() -> Result<()> {
insta::assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"sys_platform == 'darwin'",
@@ -1505,7 +1505,7 @@ fn requirements_txt_non_project_fork() -> Result<()> {
insta::assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"sys_platform == 'win32'",
@@ -2477,7 +2477,7 @@ fn requirements_txt_script() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11"
resolution-markers = [
"sys_platform == 'win32'",
@@ -2596,7 +2596,7 @@ fn requirements_txt_script() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11"
resolution-markers = [
"sys_platform == 'win32'",
diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs
index 9f38a168a..5bd4b31b5 100644
--- a/crates/uv/tests/it/lock.rs
+++ b/crates/uv/tests/it/lock.rs
@@ -45,7 +45,7 @@ fn lock_wheel_registry() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -177,7 +177,7 @@ fn lock_sdist_registry() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -275,7 +275,7 @@ fn lock_sdist_git() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -365,7 +365,7 @@ fn lock_sdist_git() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -422,7 +422,7 @@ fn lock_sdist_git() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -479,7 +479,7 @@ fn lock_sdist_git() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -541,7 +541,7 @@ fn lock_sdist_git_subdirectory() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -635,7 +635,7 @@ fn lock_sdist_git_pep508() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -699,7 +699,7 @@ fn lock_sdist_git_pep508() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -753,7 +753,7 @@ fn lock_sdist_git_pep508() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -807,7 +807,7 @@ fn lock_sdist_git_pep508() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -872,7 +872,7 @@ fn lock_sdist_git_short_rev() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -976,7 +976,7 @@ fn lock_wheel_url() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -1130,7 +1130,7 @@ fn lock_sdist_url() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -1274,7 +1274,7 @@ fn lock_sdist_url_subdirectory() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -1408,7 +1408,7 @@ fn lock_sdist_url_subdirectory_pep508() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -1545,7 +1545,7 @@ fn lock_project_extra() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -2056,7 +2056,7 @@ fn lock_dependency_extra() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -2255,7 +2255,7 @@ fn lock_conditional_dependency_extra() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.7"
resolution-markers = [
"python_full_version >= '3.10'",
@@ -2553,7 +2553,7 @@ fn lock_dependency_non_existent_extra() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -2734,7 +2734,7 @@ fn lock_upgrade_log() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -2816,7 +2816,7 @@ fn lock_upgrade_log() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -2904,7 +2904,7 @@ fn lock_upgrade_log_multi_version() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"sys_platform != 'win32'",
@@ -2990,7 +2990,7 @@ fn lock_upgrade_log_multi_version() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -3064,7 +3064,7 @@ fn lock_preference() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -3124,7 +3124,7 @@ fn lock_preference() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -3171,7 +3171,7 @@ fn lock_preference() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -3240,7 +3240,7 @@ fn lock_git_plus_prefix() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -3326,7 +3326,7 @@ fn lock_partial_git() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.12'",
@@ -3618,7 +3618,7 @@ fn lock_git_sha() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -3719,7 +3719,7 @@ fn lock_requires_python() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.7"
resolution-markers = [
"python_full_version >= '3.8'",
@@ -4010,7 +4010,7 @@ fn lock_requires_python() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.7.9"
resolution-markers = [
"python_full_version >= '3.8'",
@@ -4230,7 +4230,7 @@ fn lock_requires_python() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -4369,7 +4369,7 @@ fn lock_requires_python_upper() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = "==3.11.*"
[options]
@@ -4494,7 +4494,7 @@ fn lock_requires_python_exact() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = "==3.13"
[options]
@@ -4636,7 +4636,7 @@ fn lock_requires_python_fork() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.9"
[options]
@@ -4731,7 +4731,7 @@ fn lock_requires_python_wheels() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = "==3.12.*"
[options]
@@ -4816,7 +4816,7 @@ fn lock_requires_python_wheels() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = "==3.11.*"
[options]
@@ -4910,7 +4910,7 @@ fn lock_requires_python_star() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = "==3.11.*"
[options]
@@ -5032,7 +5032,7 @@ fn lock_requires_python_not_equal() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">3.10, !=3.10.9, !=3.10.10, !=3.11.*, <3.13"
[options]
@@ -5111,7 +5111,7 @@ fn lock_requires_python_pre() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11"
[options]
@@ -5233,7 +5233,7 @@ fn lock_requires_python_unbounded() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = "<=3.12"
resolution-markers = [
"python_full_version >= '3.7'",
@@ -5374,7 +5374,7 @@ fn lock_requires_python_maximum_version() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"python_full_version >= '3.9'",
@@ -5533,7 +5533,7 @@ fn lock_requires_python_fewest_versions() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
[options]
@@ -5650,7 +5650,7 @@ fn lock_python_version_marker_complement() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"python_full_version >= '3.11'",
@@ -5761,7 +5761,7 @@ fn lock_dev() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -5875,7 +5875,7 @@ fn lock_conditional_unconditional() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -5953,7 +5953,7 @@ fn lock_multiple_markers() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -6069,7 +6069,7 @@ fn lock_relative_and_absolute_paths() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11, <3.13"
[options]
@@ -6149,7 +6149,7 @@ fn lock_cycles() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -6352,7 +6352,7 @@ fn lock_new_extras() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -6477,7 +6477,7 @@ fn lock_new_extras() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -6725,7 +6725,7 @@ fn lock_resolution_mode() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -6806,7 +6806,7 @@ fn lock_resolution_mode() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -6968,7 +6968,7 @@ fn lock_same_version_multiple_urls() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"sys_platform == 'darwin'",
@@ -7190,7 +7190,7 @@ fn lock_exclusion() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -7496,7 +7496,7 @@ fn lock_peer_member() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -7619,7 +7619,7 @@ fn lock_index_workspace_member() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -7768,7 +7768,7 @@ fn lock_dev_transitive() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -7901,7 +7901,7 @@ fn lock_redact_https() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -8084,7 +8084,7 @@ fn lock_redact_git_pep508() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -8172,7 +8172,7 @@ fn lock_redact_git_sources() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -8258,7 +8258,7 @@ fn lock_redact_git_pep508_non_project() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -8342,7 +8342,7 @@ fn lock_redact_index_sources() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -8430,7 +8430,7 @@ fn lock_redact_url_sources() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -8538,7 +8538,7 @@ fn lock_env_credentials() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -8699,7 +8699,7 @@ fn lock_relative_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -8811,7 +8811,7 @@ fn lock_no_sources() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -8900,7 +8900,7 @@ fn lock_no_sources() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9070,7 +9070,7 @@ fn lock_migrate() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9169,7 +9169,7 @@ fn lock_upgrade_package() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9265,7 +9265,7 @@ fn lock_upgrade_package() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9349,7 +9349,7 @@ fn lock_upgrade_package() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9539,7 +9539,7 @@ fn lock_find_links_local_wheel() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9660,7 +9660,7 @@ fn lock_find_links_ignore_explicit_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9777,7 +9777,7 @@ fn lock_find_links_relative_url() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9890,7 +9890,7 @@ fn lock_find_links_local_sdist() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9981,7 +9981,7 @@ fn lock_find_links_http_wheel() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -10072,7 +10072,7 @@ fn lock_find_links_http_sdist() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -10190,7 +10190,7 @@ fn lock_find_links_explicit_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -10292,7 +10292,7 @@ fn lock_find_links_higher_priority_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -10387,7 +10387,7 @@ fn lock_find_links_lower_priority_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -10508,7 +10508,7 @@ fn lock_local_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[[package]]
@@ -10595,7 +10595,7 @@ fn lock_sources_url() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -10731,7 +10731,7 @@ fn lock_sources_archive() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -10882,7 +10882,7 @@ fn lock_sources_source_tree() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -11019,7 +11019,7 @@ fn lock_editable() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -11202,7 +11202,7 @@ fn lock_mixed_extras() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -11399,7 +11399,7 @@ fn lock_transitive_extra() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -11550,7 +11550,7 @@ fn lock_mismatched_sources() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -11593,7 +11593,7 @@ fn lock_mismatched_sources() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -11661,7 +11661,7 @@ fn lock_mismatched_versions() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -11949,7 +11949,7 @@ fn lock_change_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -11996,7 +11996,7 @@ fn lock_change_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -12090,7 +12090,7 @@ fn lock_remove_member() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -12214,7 +12214,7 @@ fn lock_remove_member() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -12320,7 +12320,7 @@ fn lock_remove_member() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -12378,7 +12378,7 @@ fn lock_add_member_with_build_system() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -12489,7 +12489,7 @@ fn lock_add_member_with_build_system() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -12590,7 +12590,7 @@ fn lock_add_member_without_build_system() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -12697,7 +12697,7 @@ fn lock_add_member_without_build_system() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -12816,7 +12816,7 @@ fn lock_add_member_without_build_system() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -12926,7 +12926,7 @@ fn lock_redundant_add_member() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -13031,7 +13031,7 @@ fn lock_redundant_add_member() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -13123,7 +13123,7 @@ fn lock_new_constraints() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -13230,7 +13230,7 @@ fn lock_new_constraints() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -13335,7 +13335,7 @@ fn lock_remove_member_non_project() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -13444,7 +13444,7 @@ fn lock_remove_member_non_project() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -13491,7 +13491,7 @@ fn lock_rename_project() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -13572,7 +13572,7 @@ fn lock_rename_project() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -13689,7 +13689,7 @@ fn lock_missing_metadata() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -13841,7 +13841,7 @@ fn lock_dev_dependencies_alias() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -13924,7 +13924,7 @@ fn lock_reorder() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -14074,7 +14074,7 @@ fn lock_narrowed_python_version_upper() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.7, <4"
resolution-markers = [
"python_full_version >= '3.10'",
@@ -14185,7 +14185,7 @@ fn lock_narrowed_python_version() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.7"
resolution-markers = [
"python_full_version >= '3.11'",
@@ -14285,7 +14285,7 @@ fn lock_exclude_unnecessary_python_forks() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"sys_platform == 'darwin'",
@@ -14393,7 +14393,7 @@ fn lock_constrained_environment() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"sys_platform != 'win32'",
@@ -14571,7 +14571,7 @@ fn lock_constrained_environment() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -14717,7 +14717,7 @@ fn lock_constrained_environment_legacy() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"sys_platform != 'win32'",
@@ -14906,7 +14906,7 @@ fn lock_non_project_fork() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.11'",
@@ -15099,7 +15099,7 @@ fn lock_non_project_conditional() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -15208,7 +15208,7 @@ fn lock_non_project_group() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.10"
[options]
@@ -15349,7 +15349,7 @@ fn lock_non_project_sources() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -15432,7 +15432,7 @@ fn lock_dropped_dev_extra() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -15546,7 +15546,7 @@ fn lock_empty_dev_dependencies() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -15650,7 +15650,7 @@ fn lock_empty_dependency_group() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -15751,7 +15751,7 @@ fn lock_add_empty_dependency_group() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -15833,7 +15833,7 @@ fn lock_add_empty_dependency_group() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -15915,7 +15915,7 @@ fn lock_add_empty_dependency_group() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -15993,7 +15993,7 @@ fn lock_trailing_slash_index_url() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -16169,7 +16169,7 @@ fn lock_explicit_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -16277,7 +16277,7 @@ fn lock_explicit_default_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -16363,7 +16363,7 @@ fn lock_explicit_default_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -16440,7 +16440,7 @@ fn lock_named_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -16509,7 +16509,7 @@ fn lock_default_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -16572,7 +16572,7 @@ fn lock_default_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -16652,7 +16652,7 @@ fn lock_named_index_cli() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -16802,7 +16802,7 @@ fn lock_repeat_named_index_member() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -16891,7 +16891,7 @@ fn lock_unique_named_index() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -16965,7 +16965,7 @@ fn lock_repeat_named_index_cli() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -17032,7 +17032,7 @@ fn lock_repeat_named_index_cli() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -17131,7 +17131,7 @@ fn lock_named_index_overlap() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"sys_platform == 'linux'",
@@ -17213,7 +17213,7 @@ fn lock_explicit_virtual_project() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -17431,7 +17431,7 @@ fn lock_implicit_virtual_project() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -17659,7 +17659,7 @@ fn lock_implicit_package_path() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -17843,7 +17843,7 @@ fn lock_split_python_environment() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.7"
resolution-markers = [
"python_full_version < '3.8'",
@@ -17954,7 +17954,7 @@ fn lock_python_upper_bound() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"python_full_version >= '3.9' and python_full_version < '3.13'",
@@ -18326,7 +18326,7 @@ fn lock_simplified_environments() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = "==3.11.*"
resolution-markers = [
"sys_platform == 'darwin'",
@@ -18436,7 +18436,7 @@ fn lock_dependency_metadata() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -18678,7 +18678,7 @@ fn lock_dependency_metadata_git() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -18791,7 +18791,7 @@ fn lock_strip_fragment() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -19191,7 +19191,7 @@ fn lock_change_requires_python() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"python_full_version >= '3.13'",
@@ -19301,7 +19301,7 @@ fn lock_change_requires_python() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.13'",
@@ -19446,7 +19446,7 @@ fn lock_keyring_credentials() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -19634,7 +19634,7 @@ fn lock_keyring_credentials_always_authenticate_fetches_username() -> Result<()>
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -19761,7 +19761,7 @@ fn lock_multiple_sources() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"sys_platform != 'win32'",
@@ -19949,7 +19949,7 @@ fn lock_multiple_sources_index_disjoint_markers() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"sys_platform == 'win32'",
@@ -20080,7 +20080,7 @@ fn lock_multiple_sources_index_mixed() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"sys_platform == 'win32'",
@@ -20214,7 +20214,7 @@ fn lock_multiple_sources_index_non_total() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"sys_platform == 'win32'",
@@ -20315,7 +20315,7 @@ fn lock_multiple_sources_index_explicit() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"sys_platform == 'win32'",
@@ -20461,7 +20461,7 @@ fn lock_multiple_sources_non_total() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"sys_platform == 'darwin'",
@@ -20562,7 +20562,7 @@ fn lock_multiple_sources_respect_marker() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -20644,7 +20644,7 @@ fn lock_multiple_sources_extra() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -21074,7 +21074,7 @@ fn lock_group_include() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -21261,7 +21261,7 @@ fn lock_group_requires_python() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"python_full_version >= '3.13'",
@@ -21383,7 +21383,7 @@ fn lock_group_includes_requires_python() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"python_full_version >= '3.13.1'",
@@ -21598,7 +21598,7 @@ fn lock_group_includes_requires_python_contradiction() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"python_full_version >= '3.13'",
@@ -22063,7 +22063,7 @@ fn lock_group_workspace() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -22254,7 +22254,7 @@ fn lock_transitive_git() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -22417,7 +22417,7 @@ fn lock_dynamic_version() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -22456,7 +22456,7 @@ fn lock_dynamic_version() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -22527,7 +22527,7 @@ fn lock_dynamic_version_dependencies() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -22566,7 +22566,7 @@ fn lock_dynamic_version_dependencies() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -22723,7 +22723,7 @@ fn lock_dynamic_version_workspace_member() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -22793,7 +22793,7 @@ fn lock_dynamic_version_workspace_member() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -22911,7 +22911,7 @@ fn lock_dynamic_version_path_dependency() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -22975,7 +22975,7 @@ fn lock_dynamic_version_path_dependency() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -23075,7 +23075,7 @@ fn lock_dynamic_version_self_extra_hatchling() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -23235,7 +23235,7 @@ fn lock_dynamic_version_self_extra_setuptools() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -23387,7 +23387,7 @@ fn lock_dynamic_built_cache() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -23434,7 +23434,7 @@ fn lock_dynamic_built_cache() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -23509,7 +23509,7 @@ fn lock_shared_build_dependency() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"python_full_version >= '3.9'",
@@ -23788,7 +23788,7 @@ fn lock_dynamic_to_static() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -23845,7 +23845,7 @@ fn lock_dynamic_to_static() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -23899,7 +23899,7 @@ fn lock_static_to_dynamic() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -23976,7 +23976,7 @@ fn lock_static_to_dynamic() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -24024,7 +24024,7 @@ fn lock_bump_static_version() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -24078,7 +24078,7 @@ fn lock_bump_static_version() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -24412,7 +24412,7 @@ fn lock_relative_project() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -24514,7 +24514,7 @@ fn lock_recursive_extra() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -24653,7 +24653,7 @@ fn lock_no_build_static_metadata() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -24778,7 +24778,7 @@ fn lock_self_compatible() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -24878,7 +24878,7 @@ fn lock_self_exact() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -25011,7 +25011,7 @@ fn lock_self_extra_to_extra_compatible() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -25182,7 +25182,7 @@ fn lock_self_extra_compatible() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -25316,7 +25316,7 @@ fn lock_self_marker_compatible() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -25451,7 +25451,7 @@ fn lock_split_on_windows() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"sys_platform != 'win32'",
@@ -25575,7 +25575,7 @@ fn lock_arm() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"platform_machine == 'arm64'",
@@ -25650,7 +25650,7 @@ fn lock_x86_64() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"platform_machine == 'x86_64'",
@@ -25726,7 +25726,7 @@ fn lock_x86() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"platform_machine == 'i686'",
@@ -25798,7 +25798,7 @@ fn lock_script() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11"
[options]
@@ -25935,7 +25935,7 @@ fn lock_script_path() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11"
[options]
@@ -26051,7 +26051,7 @@ fn lock_script_initialize() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -26159,7 +26159,7 @@ fn lock_pytorch_cpu() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12.[X]"
resolution-markers = [
"(platform_machine != 'aarch64' and extra != 'extra-7-project-cpu' and extra == 'extra-7-project-cu124') or (sys_platform != 'linux' and extra != 'extra-7-project-cpu' and extra == 'extra-7-project-cu124')",
@@ -26812,7 +26812,7 @@ fn lock_pytorch_index_preferences() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.10.0"
resolution-markers = [
"sys_platform != 'darwin' and extra != 'extra-7-project-cpu' and extra == 'extra-7-project-cu118'",
@@ -27279,7 +27279,7 @@ fn lock_intel_mac() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11"
resolution-markers = [
"(python_full_version >= '3.12' and platform_machine != 'x86_64') or (python_full_version >= '3.12' and sys_platform != 'darwin')",
@@ -27670,7 +27670,7 @@ fn lock_pytorch_local_preference() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12.[X]"
resolution-markers = [
"sys_platform == 'darwin'",
@@ -28005,7 +28005,7 @@ fn windows_arm() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = "==3.12.*"
resolution-markers = [
"platform_machine == 'x86_64' and sys_platform == 'linux'",
@@ -28082,7 +28082,7 @@ fn windows_amd64_required() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = "==3.12.*"
required-markers = [
"platform_machine == 'x86' and sys_platform == 'win32'",
@@ -28151,7 +28151,7 @@ fn lock_empty_extra() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -28258,7 +28258,7 @@ fn lock_empty_extra() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -28421,7 +28421,7 @@ fn lock_omit_wheels_exclude_newer() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -28606,7 +28606,7 @@ fn lock_requires_python_empty_lock_file() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = "==3.13.0"
resolution-markers = [
"sys_platform == 'darwin'",
@@ -28682,7 +28682,7 @@ fn lock_requires_python_empty_lock_file() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = "==3.13.2"
resolution-markers = [
"sys_platform == 'darwin'",
@@ -28871,7 +28871,7 @@ fn lock_trailing_slash_index_url_in_pyproject_not_index_argument() -> Result<()>
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -29240,7 +29240,7 @@ fn lock_trailing_slash_find_links() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -29321,7 +29321,7 @@ fn lock_trailing_slash_find_links() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -29412,6 +29412,148 @@ fn test_tilde_equals_python_version() -> Result<()> {
Ok(())
}
+/// Test that exclude-newer-package is properly serialized in the lockfile.
+#[test]
+fn lock_exclude_newer_package() -> Result<()> {
+ let context = TestContext::new("3.12");
+
+ let pyproject_toml = context.temp_dir.child("pyproject.toml");
+ pyproject_toml.write_str(
+ r#"
+ [project]
+ name = "project"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = ["requests", "tqdm"]
+ "#,
+ )?;
+
+ // Lock with both global exclude-newer and package-specific overrides
+ uv_snapshot!(context.filters(), context
+ .lock()
+ .env_remove(EnvVars::UV_EXCLUDE_NEWER)
+ .arg("--exclude-newer")
+ .arg("2022-04-04T12:00:00Z")
+ .arg("--exclude-newer-package")
+ .arg("tqdm=2022-09-04T00:00:00Z"), @r###"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Resolved 8 packages in [TIME]
+ "###);
+
+ let lock = context.read("uv.lock");
+
+ insta::with_settings!({
+ filters => context.filters(),
+ }, {
+ assert_snapshot!(
+ lock, @r#"
+ version = 1
+ revision = 3
+ requires-python = ">=3.12"
+
+ [options]
+ exclude-newer = "2022-04-04T12:00:00Z"
+
+ [options.exclude-newer-package]
+ tqdm = "2022-09-04T00:00:00Z"
+
+ [[package]]
+ name = "certifi"
+ version = "2021.10.8"
+ source = { registry = "https://pypi.org/simple" }
+ sdist = { url = "https://files.pythonhosted.org/packages/6c/ae/d26450834f0acc9e3d1f74508da6df1551ceab6c2ce0766a593362d6d57f/certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", size = 151214, upload-time = "2021-10-08T19:32:15.277Z" }
+ wheels = [
+ { url = "https://files.pythonhosted.org/packages/37/45/946c02767aabb873146011e665728b680884cd8fe70dde973c640e45b775/certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569", size = 149195, upload-time = "2021-10-08T19:32:10.712Z" },
+ ]
+
+ [[package]]
+ name = "charset-normalizer"
+ version = "2.0.12"
+ source = { registry = "https://pypi.org/simple" }
+ sdist = { url = "https://files.pythonhosted.org/packages/56/31/7bcaf657fafb3c6db8c787a865434290b726653c912085fbd371e9b92e1c/charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", size = 79105, upload-time = "2022-02-12T14:33:13.788Z" }
+ wheels = [
+ { url = "https://files.pythonhosted.org/packages/06/b3/24afc8868eba069a7f03650ac750a778862dc34941a4bebeb58706715726/charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df", size = 39623, upload-time = "2022-02-12T14:33:12.294Z" },
+ ]
+
+ [[package]]
+ name = "colorama"
+ version = "0.4.4"
+ source = { registry = "https://pypi.org/simple" }
+ sdist = { url = "https://files.pythonhosted.org/packages/1f/bb/5d3246097ab77fa083a61bd8d3d527b7ae063c7d8e8671b1cf8c4ec10cbe/colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", size = 27813, upload-time = "2020-10-15T18:36:33.372Z" }
+ wheels = [
+ { url = "https://files.pythonhosted.org/packages/44/98/5b86278fbbf250d239ae0ecb724f8572af1c91f4a11edf4d36a206189440/colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2", size = 16028, upload-time = "2020-10-13T02:42:26.463Z" },
+ ]
+
+ [[package]]
+ name = "idna"
+ version = "3.3"
+ source = { registry = "https://pypi.org/simple" }
+ sdist = { url = "https://files.pythonhosted.org/packages/62/08/e3fc7c8161090f742f504f40b1bccbfc544d4a4e09eb774bf40aafce5436/idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d", size = 286689, upload-time = "2021-10-12T23:33:41.312Z" }
+ wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/a2/d918dcd22354d8958fe113e1a3630137e0fc8b44859ade3063982eacd2a4/idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", size = 61160, upload-time = "2021-10-12T23:33:38.02Z" },
+ ]
+
+ [[package]]
+ name = "project"
+ version = "0.1.0"
+ source = { virtual = "." }
+ dependencies = [
+ { name = "requests" },
+ { name = "tqdm" },
+ ]
+
+ [package.metadata]
+ requires-dist = [
+ { name = "requests" },
+ { name = "tqdm" },
+ ]
+
+ [[package]]
+ name = "requests"
+ version = "2.27.1"
+ source = { registry = "https://pypi.org/simple" }
+ dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+ ]
+ sdist = { url = "https://files.pythonhosted.org/packages/60/f3/26ff3767f099b73e0efa138a9998da67890793bfa475d8278f84a30fec77/requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", size = 106758, upload-time = "2022-01-05T15:40:51.698Z" }
+ wheels = [
+ { url = "https://files.pythonhosted.org/packages/2d/61/08076519c80041bc0ffa1a8af0cbd3bf3e2b62af10435d269a9d0f40564d/requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d", size = 63133, upload-time = "2022-01-05T15:40:49.334Z" },
+ ]
+
+ [[package]]
+ name = "tqdm"
+ version = "4.64.1"
+ source = { registry = "https://pypi.org/simple" }
+ dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ ]
+ sdist = { url = "https://files.pythonhosted.org/packages/c1/c2/d8a40e5363fb01806870e444fc1d066282743292ff32a9da54af51ce36a2/tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4", size = 169599, upload-time = "2022-09-03T11:10:30.943Z" }
+ wheels = [
+ { url = "https://files.pythonhosted.org/packages/47/bb/849011636c4da2e44f1253cd927cfb20ada4374d8b3a4e425416e84900cc/tqdm-4.64.1-py2.py3-none-any.whl", hash = "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1", size = 78468, upload-time = "2022-09-03T11:10:27.148Z" },
+ ]
+
+ [[package]]
+ name = "urllib3"
+ version = "1.26.9"
+ source = { registry = "https://pypi.org/simple" }
+ sdist = { url = "https://files.pythonhosted.org/packages/1b/a5/4eab74853625505725cefdf168f48661b2cd04e7843ab836f3f63abf81da/urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e", size = 295258, upload-time = "2022-03-16T13:28:19.197Z" }
+ wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/03/062e6444ce4baf1eac17a6a0ebfe36bb1ad05e1df0e20b110de59c278498/urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", size = 138990, upload-time = "2022-03-16T13:28:16.026Z" },
+ ]
+ "#
+ );
+ });
+
+ Ok(())
+}
+
/// Test that lockfile validation includes explicit indexes from path dependencies.
///
#[test]
diff --git a/crates/uv/tests/it/lock_conflict.rs b/crates/uv/tests/it/lock_conflict.rs
index 2025dbd8b..868dfb9aa 100644
--- a/crates/uv/tests/it/lock_conflict.rs
+++ b/crates/uv/tests/it/lock_conflict.rs
@@ -91,7 +91,7 @@ fn extra_basic() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", extra = "extra1" },
@@ -285,7 +285,7 @@ fn extra_basic_three_extras() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", extra = "extra1" },
@@ -760,7 +760,7 @@ fn extra_multiple_independent() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", extra = "extra1" },
@@ -910,7 +910,7 @@ fn extra_config_change_ignore_lockfile() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", extra = "extra1" },
@@ -1787,7 +1787,7 @@ fn extra_depends_on_conflicting_extra_transitive() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "example", extra = "bar" },
@@ -1972,7 +1972,7 @@ fn group_basic() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", group = "group1" },
@@ -2127,7 +2127,7 @@ fn group_default() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", group = "group1" },
@@ -2339,7 +2339,7 @@ fn mixed() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", extra = "extra1" },
@@ -2509,7 +2509,7 @@ fn multiple_sources_index_disjoint_extras() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", extra = "cu118" },
@@ -2659,7 +2659,7 @@ fn multiple_sources_index_disjoint_groups() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", group = "cu118" },
@@ -2808,7 +2808,7 @@ fn multiple_sources_index_disjoint_extras_with_extra() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", extra = "cu118" },
@@ -2977,7 +2977,7 @@ fn multiple_sources_index_disjoint_extras_with_marker() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"extra != 'extra-7-project-cu118' and extra == 'extra-7-project-cu124'",
@@ -3303,7 +3303,7 @@ fn shared_optional_dependency_extra1() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", extra = "bar" },
@@ -3443,7 +3443,7 @@ fn shared_optional_dependency_group1() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", group = "bar" },
@@ -3584,7 +3584,7 @@ fn shared_optional_dependency_mixed1() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", extra = "foo" },
@@ -3729,7 +3729,7 @@ fn shared_optional_dependency_extra2() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = "==3.11.*"
conflicts = [[
{ package = "project", extra = "bar" },
@@ -3870,7 +3870,7 @@ fn shared_optional_dependency_group2() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = "==3.11.*"
conflicts = [[
{ package = "project", group = "bar" },
@@ -4016,7 +4016,7 @@ fn shared_optional_dependency_mixed2() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = "==3.11.*"
conflicts = [[
{ package = "project", extra = "foo" },
@@ -4160,7 +4160,7 @@ fn shared_dependency_extra() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", extra = "bar" },
@@ -4335,7 +4335,7 @@ fn shared_dependency_group() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", group = "bar" },
@@ -4511,7 +4511,7 @@ fn shared_dependency_mixed() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", extra = "foo" },
@@ -4729,7 +4729,7 @@ conflicts = [
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = "==3.11.*"
conflicts = [[
{ package = "project", extra = "x1" },
@@ -4915,7 +4915,7 @@ fn jinja_no_conflict_markers1() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", extra = "cu118" },
@@ -5077,7 +5077,7 @@ fn jinja_no_conflict_markers2() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"extra != 'extra-7-project-cu118' and extra == 'extra-7-project-cu124'",
@@ -5238,7 +5238,7 @@ fn collision_extra() -> Result<()> {
lock,
@r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "pkg", extra = "bar" },
@@ -5467,7 +5467,7 @@ fn extra_inferences() -> Result<()> {
lock,
@r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "pkg", extra = "x1" },
@@ -7503,7 +7503,7 @@ fn deduplicate_resolution_markers() -> Result<()> {
lock,
@r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"sys_platform != 'linux' and extra != 'extra-3-pkg-x1' and extra == 'extra-3-pkg-x2'",
@@ -7657,7 +7657,7 @@ fn incorrect_extra_simplification_leads_to_multiple_torch_packages() -> Result<(
lock,
@r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.12' and sys_platform == 'win32' and extra != 'extra-4-test-chgnet' and extra == 'extra-4-test-m3gnet'",
@@ -10432,7 +10432,7 @@ fn duplicate_torch_and_sympy_because_of_wrong_inferences() -> Result<()> {
lock,
@r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.12' and sys_platform == 'win32' and extra != 'extra-4-test-alignn' and extra == 'extra-4-test-all' and extra == 'extra-4-test-chgnet' and extra != 'extra-4-test-m3gnet'",
@@ -13654,7 +13654,7 @@ fn overlapping_resolution_markers() -> Result<()> {
lock,
@r#"
version = 1
- revision = 2
+ revision = 3
requires-python = "==3.10.*"
resolution-markers = [
"sys_platform == 'linux' and extra != 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118'",
@@ -14337,7 +14337,7 @@ fn avoids_exponential_lock_file_growth() -> Result<()> {
lock,
@r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"extra != 'extra-27-resolution-markers-for-days-cpu' and extra == 'extra-27-resolution-markers-for-days-cu124'",
@@ -14752,7 +14752,7 @@ fn avoids_exponential_lock_file_growth() -> Result<()> {
lock,
@r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
resolution-markers = [
"extra != 'extra-27-resolution-markers-for-days-cpu' and extra == 'extra-27-resolution-markers-for-days-cu124'",
diff --git a/crates/uv/tests/it/lock_scenarios.rs b/crates/uv/tests/it/lock_scenarios.rs
index 3be986ad1..f1a72361f 100644
--- a/crates/uv/tests/it/lock_scenarios.rs
+++ b/crates/uv/tests/it/lock_scenarios.rs
@@ -137,7 +137,7 @@ fn wrong_backtracking_basic() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
[[package]]
@@ -319,7 +319,7 @@ fn wrong_backtracking_indirect() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
[[package]]
@@ -466,7 +466,7 @@ fn fork_allows_non_conflicting_non_overlapping_dependencies() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"sys_platform == 'darwin'",
@@ -586,7 +586,7 @@ fn fork_allows_non_conflicting_repeated_dependencies() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
[[package]]
@@ -688,7 +688,7 @@ fn fork_basic() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"sys_platform == 'darwin'",
@@ -991,7 +991,7 @@ fn fork_filter_sibling_dependencies() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"sys_platform == 'linux'",
@@ -1174,7 +1174,7 @@ fn fork_upgrade() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
[[package]]
@@ -1299,7 +1299,7 @@ fn fork_incomplete_markers() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"python_full_version >= '3.11'",
@@ -1456,7 +1456,7 @@ fn fork_marker_accrue() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
[[package]]
@@ -1667,7 +1667,7 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"implementation_name == 'pypy' and sys_platform == 'darwin'",
@@ -1853,7 +1853,7 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"implementation_name == 'pypy' and sys_platform == 'darwin'",
@@ -2028,7 +2028,7 @@ fn fork_marker_inherit_combined() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"implementation_name == 'pypy' and sys_platform == 'darwin'",
@@ -2194,7 +2194,7 @@ fn fork_marker_inherit_isolated() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"sys_platform == 'darwin'",
@@ -2348,7 +2348,7 @@ fn fork_marker_inherit_transitive() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"sys_platform == 'darwin'",
@@ -2508,7 +2508,7 @@ fn fork_marker_inherit() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"sys_platform == 'darwin'",
@@ -2651,7 +2651,7 @@ fn fork_marker_limited_inherit() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"sys_platform == 'darwin'",
@@ -2811,7 +2811,7 @@ fn fork_marker_selection() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"sys_platform == 'darwin'",
@@ -2974,7 +2974,7 @@ fn fork_marker_track() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"sys_platform == 'darwin'",
@@ -3131,7 +3131,7 @@ fn fork_non_fork_marker_transitive() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
[[package]]
@@ -3442,7 +3442,7 @@ fn fork_overlapping_markers_basic() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"python_full_version >= '3.11'",
@@ -3626,7 +3626,7 @@ fn preferences_dependent_forking_bistable() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"sys_platform == 'linux'",
@@ -4038,7 +4038,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"sys_platform == 'linux'",
@@ -4332,7 +4332,7 @@ fn preferences_dependent_forking() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"sys_platform == 'linux'",
@@ -4512,7 +4512,7 @@ fn fork_remaining_universe_partitioning() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"os_name == 'darwin' and sys_platform == 'illumos'",
@@ -4665,7 +4665,7 @@ fn fork_requires_python_full_prerelease() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.10"
[[package]]
@@ -4750,7 +4750,7 @@ fn fork_requires_python_full() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.10"
[[package]]
@@ -4839,7 +4839,7 @@ fn fork_requires_python_patch_overlap() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.10.1"
[[package]]
@@ -4933,7 +4933,7 @@ fn fork_requires_python() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.10"
[[package]]
@@ -5014,7 +5014,7 @@ fn requires_python_wheels() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.10"
[[package]]
@@ -5113,7 +5113,7 @@ fn unreachable_package() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
[[package]]
@@ -5218,7 +5218,7 @@ fn unreachable_wheels() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
[[package]]
@@ -5352,7 +5352,7 @@ fn marker_variants_have_different_extras() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
resolution-markers = [
"platform_python_implementation != 'PyPy'",
@@ -5494,7 +5494,7 @@ fn virtual_package_extra_priorities() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[[package]]
@@ -5618,7 +5618,7 @@ fn specific_architecture() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.8"
[[package]]
diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs
index 34af0ed08..a453b3b57 100644
--- a/crates/uv/tests/it/pip_compile.rs
+++ b/crates/uv/tests/it/pip_compile.rs
@@ -3362,7 +3362,7 @@ fn compile_exclude_newer() -> Result<()> {
.arg("--exclude-newer")
// 4.64.0: 2022-04-04T01:48:46.194635Z1
// 4.64.1: 2022-09-03T11:10:27.148080Z
- .arg("2022-04-04T12:00:00Z"), @r###"
+ .arg("2022-04-04T12:00:00Z"), @r"
success: true
exit_code: 0
----- stdout -----
@@ -3373,7 +3373,7 @@ fn compile_exclude_newer() -> Result<()> {
----- stderr -----
Resolved 1 package in [TIME]
- "###
+ "
);
// Use a date as input instead.
@@ -3383,7 +3383,7 @@ fn compile_exclude_newer() -> Result<()> {
.env_remove(EnvVars::UV_EXCLUDE_NEWER)
.arg("requirements.in")
.arg("--exclude-newer")
- .arg("2022-04-04"), @r###"
+ .arg("2022-04-04"), @r"
success: true
exit_code: 0
----- stdout -----
@@ -3394,7 +3394,7 @@ fn compile_exclude_newer() -> Result<()> {
----- stderr -----
Resolved 1 package in [TIME]
- "###
+ "
);
// Check the error message for invalid datetime
@@ -3438,6 +3438,190 @@ fn compile_exclude_newer() -> Result<()> {
Ok(())
}
+/// Test per-package exclude-newer functionality
+#[test]
+fn compile_exclude_newer_package() -> Result<()> {
+ let context = TestContext::new("3.12");
+ let requirements_in = context.temp_dir.child("requirements.in");
+ requirements_in.write_str("tqdm\nrequests")?;
+
+ // First, establish baseline with global exclude-newer
+ // tqdm 4.64.0 was released on 2022-04-04, 4.64.1 on 2022-09-03
+ // requests 2.27.1 was released on 2022-01-05, 2.28.0 on 2022-05-29
+ uv_snapshot!(context
+ .pip_compile()
+ .env_remove(EnvVars::UV_EXCLUDE_NEWER)
+ .arg("requirements.in")
+ .arg("--exclude-newer")
+ .arg("2022-04-04T12:00:00Z"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ # This file was autogenerated by uv via the following command:
+ # uv pip compile --cache-dir [CACHE_DIR] requirements.in --exclude-newer 2022-04-04T12:00:00Z
+ certifi==2021.10.8
+ # via requests
+ charset-normalizer==2.0.12
+ # via requests
+ idna==3.3
+ # via requests
+ requests==2.27.1
+ # via -r requirements.in
+ tqdm==4.64.0
+ # via -r requirements.in
+ urllib3==1.26.9
+ # via requests
+
+ ----- stderr -----
+ Resolved 6 packages in [TIME]
+ "
+ );
+
+ // Test override: allow tqdm to use newer versions while keeping requests pinned
+ uv_snapshot!(context
+ .pip_compile()
+ .env_remove(EnvVars::UV_EXCLUDE_NEWER)
+ .arg("requirements.in")
+ .arg("--exclude-newer")
+ .arg("2022-04-04T12:00:00Z")
+ .arg("--exclude-newer-package")
+ .arg("tqdm=2022-09-04T00:00:00Z"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ # This file was autogenerated by uv via the following command:
+ # uv pip compile --cache-dir [CACHE_DIR] requirements.in --exclude-newer 2022-04-04T12:00:00Z --exclude-newer-package tqdm=2022-09-04T00:00:00Z
+ certifi==2021.10.8
+ # via requests
+ charset-normalizer==2.0.12
+ # via requests
+ idna==3.3
+ # via requests
+ requests==2.27.1
+ # via -r requirements.in
+ tqdm==4.64.1
+ # via -r requirements.in
+ urllib3==1.26.9
+ # via requests
+
+ ----- stderr -----
+ Resolved 6 packages in [TIME]
+ "
+ );
+
+ // Test multiple package overrides
+ uv_snapshot!(context
+ .pip_compile()
+ .env_remove(EnvVars::UV_EXCLUDE_NEWER)
+ .arg("requirements.in")
+ .arg("--exclude-newer")
+ .arg("2022-01-01T00:00:00Z")
+ .arg("--exclude-newer-package")
+ .arg("tqdm=2022-09-04T00:00:00Z")
+ .arg("--exclude-newer-package")
+ .arg("requests=2022-06-01T00:00:00Z"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ # This file was autogenerated by uv via the following command:
+ # uv pip compile --cache-dir [CACHE_DIR] requirements.in --exclude-newer 2022-01-01T00:00:00Z --exclude-newer-package tqdm=2022-09-04T00:00:00Z --exclude-newer-package requests=2022-06-01T00:00:00Z
+ certifi==2021.10.8
+ # via requests
+ charset-normalizer==2.0.9
+ # via requests
+ idna==3.3
+ # via requests
+ requests==2.27.1
+ # via -r requirements.in
+ tqdm==4.64.1
+ # via -r requirements.in
+ urllib3==1.26.7
+ # via requests
+
+ ----- stderr -----
+ Resolved 6 packages in [TIME]
+ "
+ );
+
+ // Test exclude-newer-package without global exclude-newer
+ uv_snapshot!(context
+ .pip_compile()
+ .env_remove(EnvVars::UV_EXCLUDE_NEWER)
+ .arg("requirements.in")
+ .arg("--exclude-newer-package")
+ .arg("tqdm=2022-04-04T12:00:00Z"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ # This file was autogenerated by uv via the following command:
+ # uv pip compile --cache-dir [CACHE_DIR] requirements.in --exclude-newer-package tqdm=2022-04-04T12:00:00Z
+ certifi==2025.7.14
+ # via requests
+ charset-normalizer==3.4.2
+ # via requests
+ idna==3.10
+ # via requests
+ requests==2.32.4
+ # via -r requirements.in
+ tqdm==4.64.0
+ # via -r requirements.in
+ urllib3==2.5.0
+ # via requests
+
+ ----- stderr -----
+ Resolved 6 packages in [TIME]
+ "
+ );
+
+ Ok(())
+}
+
+/// Test error handling for malformed --exclude-newer-package
+#[test]
+fn compile_exclude_newer_package_errors() -> Result<()> {
+ let context = TestContext::new("3.12");
+ let requirements_in = context.temp_dir.child("requirements.in");
+ requirements_in.write_str("tqdm")?;
+
+ // Test invalid format (missing =)
+ uv_snapshot!(context
+ .pip_compile()
+ .env_remove(EnvVars::UV_EXCLUDE_NEWER)
+ .arg("requirements.in")
+ .arg("--exclude-newer-package")
+ .arg("tqdm"), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: invalid value 'tqdm' for '--exclude-newer-package ': Invalid `exclude-newer-package` value `tqdm`: expected format `PACKAGE=DATE`
+
+ For more information, try '--help'.
+ "
+ );
+
+ // Test invalid date format
+ uv_snapshot!(context
+ .pip_compile()
+ .env_remove(EnvVars::UV_EXCLUDE_NEWER)
+ .arg("requirements.in")
+ .arg("--exclude-newer-package")
+ .arg("tqdm=invalid-date"), @r#"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: invalid value 'tqdm=invalid-date' for '--exclude-newer-package ': Invalid `exclude-newer-package` timestamp `invalid-date`: `invalid-date` could not be parsed as a valid date: failed to parse year in date "invalid-date": failed to parse "inva" as year (a four digit integer): invalid digit, expected 0-9 but got i
+
+ For more information, try '--help'.
+ "#
+ );
+
+ Ok(())
+}
+
/// Resolve a local path dependency on a specific wheel.
#[test]
fn compile_wheel_path_dependency() -> Result<()> {
diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs
index 300a87f06..acccbc43b 100644
--- a/crates/uv/tests/it/run.rs
+++ b/crates/uv/tests/it/run.rs
@@ -888,7 +888,7 @@ fn run_pep723_script_lock() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11"
[options]
@@ -997,7 +997,7 @@ fn run_pep723_script_lock() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11"
[options]
@@ -2121,7 +2121,7 @@ fn run_locked() -> Result<()> {
assert_snapshot!(
existing, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -5513,7 +5513,7 @@ fn run_pep723_script_with_constraints_lock() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11"
[options]
diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs
index 65555ba56..9de38cb31 100644
--- a/crates/uv/tests/it/show_settings.rs
+++ b/crates/uv/tests/it/show_settings.rs
@@ -213,7 +213,12 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -402,7 +407,12 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -592,7 +602,12 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -814,7 +829,12 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -971,7 +991,12 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -1174,7 +1199,12 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
X8664UnknownLinuxGnu,
),
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -1421,7 +1451,12 @@ fn resolve_index_url() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -1680,7 +1715,12 @@ fn resolve_index_url() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -1894,7 +1934,12 @@ fn resolve_find_links() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -2073,7 +2118,12 @@ fn resolve_top_level() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -2312,7 +2362,12 @@ fn resolve_top_level() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -2534,7 +2589,12 @@ fn resolve_top_level() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -2712,7 +2772,12 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -2874,7 +2939,12 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -3036,7 +3106,12 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -3200,7 +3275,12 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -3328,6 +3408,7 @@ fn resolve_tool() -> anyhow::Result<()> {
no_build_isolation: None,
no_build_isolation_package: None,
exclude_newer: None,
+ exclude_newer_package: None,
link_mode: Some(
Clone,
),
@@ -3357,7 +3438,12 @@ fn resolve_tool() -> anyhow::Result<()> {
dependency_metadata: DependencyMetadata(
{},
),
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
fork_strategy: RequiresPython,
index_locations: IndexLocations {
indexes: [],
@@ -3556,7 +3642,12 @@ fn resolve_poetry_toml() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -3786,7 +3877,12 @@ fn resolve_both() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -4020,7 +4116,12 @@ fn resolve_both_special_fields() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -4333,7 +4434,12 @@ fn resolve_config_file() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -4384,7 +4490,7 @@ fn resolve_config_file() -> anyhow::Result<()> {
|
1 | [project]
| ^^^^^^^
- unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend`
+ unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend`
"
);
@@ -4588,7 +4694,12 @@ fn resolve_skip_empty() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -4753,7 +4864,12 @@ fn resolve_skip_empty() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -4937,7 +5053,12 @@ fn allow_insecure_host() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -5182,7 +5303,12 @@ fn index_priority() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -5406,7 +5532,12 @@ fn index_priority() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -5636,7 +5767,12 @@ fn index_priority() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -5861,7 +5997,12 @@ fn index_priority() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -6093,7 +6234,12 @@ fn index_priority() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -6318,7 +6464,12 @@ fn index_priority() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -6487,7 +6638,12 @@ fn verify_hashes() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -6642,7 +6798,12 @@ fn verify_hashes() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -6795,7 +6956,12 @@ fn verify_hashes() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -6950,7 +7116,12 @@ fn verify_hashes() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -7103,7 +7274,12 @@ fn verify_hashes() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -7257,7 +7433,12 @@ fn verify_hashes() -> anyhow::Result<()> {
python_version: None,
python_platform: None,
universal: false,
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
@@ -7374,7 +7555,12 @@ fn preview_features() {
dependency_metadata: DependencyMetadata(
{},
),
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
fork_strategy: RequiresPython,
index_locations: IndexLocations {
indexes: [],
@@ -7476,7 +7662,12 @@ fn preview_features() {
dependency_metadata: DependencyMetadata(
{},
),
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
fork_strategy: RequiresPython,
index_locations: IndexLocations {
indexes: [],
@@ -7578,7 +7769,12 @@ fn preview_features() {
dependency_metadata: DependencyMetadata(
{},
),
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
fork_strategy: RequiresPython,
index_locations: IndexLocations {
indexes: [],
@@ -7680,7 +7876,12 @@ fn preview_features() {
dependency_metadata: DependencyMetadata(
{},
),
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
fork_strategy: RequiresPython,
index_locations: IndexLocations {
indexes: [],
@@ -7782,7 +7983,12 @@ fn preview_features() {
dependency_metadata: DependencyMetadata(
{},
),
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
fork_strategy: RequiresPython,
index_locations: IndexLocations {
indexes: [],
@@ -7886,7 +8092,12 @@ fn preview_features() {
dependency_metadata: DependencyMetadata(
{},
),
- exclude_newer: None,
+ exclude_newer: ExcludeNewer {
+ global: None,
+ package: ExcludeNewerPackage(
+ {},
+ ),
+ },
fork_strategy: RequiresPython,
index_locations: IndexLocations {
indexes: [],
diff --git a/crates/uv/tests/it/snapshots/it__ecosystem__black-lock-file.snap b/crates/uv/tests/it/snapshots/it__ecosystem__black-lock-file.snap
index 68c1d6bb4..082fef710 100644
--- a/crates/uv/tests/it/snapshots/it__ecosystem__black-lock-file.snap
+++ b/crates/uv/tests/it/snapshots/it__ecosystem__black-lock-file.snap
@@ -3,7 +3,7 @@ source: crates/uv/tests/it/ecosystem.rs
expression: lock
---
version = 1
-revision = 2
+revision = 3
requires-python = ">=3.8"
resolution-markers = [
"python_full_version >= '3.10' and implementation_name == 'pypy' and sys_platform == 'win32'",
diff --git a/crates/uv/tests/it/snapshots/it__ecosystem__github-wikidata-bot-lock-file.snap b/crates/uv/tests/it/snapshots/it__ecosystem__github-wikidata-bot-lock-file.snap
index 506f4ca6d..210dbb210 100644
--- a/crates/uv/tests/it/snapshots/it__ecosystem__github-wikidata-bot-lock-file.snap
+++ b/crates/uv/tests/it/snapshots/it__ecosystem__github-wikidata-bot-lock-file.snap
@@ -3,7 +3,7 @@ source: crates/uv/tests/it/ecosystem.rs
expression: lock
---
version = 1
-revision = 2
+revision = 3
requires-python = ">=3.12"
resolution-markers = [
"python_full_version >= '3.13'",
diff --git a/crates/uv/tests/it/snapshots/it__ecosystem__home-assistant-core-lock-file.snap b/crates/uv/tests/it/snapshots/it__ecosystem__home-assistant-core-lock-file.snap
index f17f8b0c7..e7babf608 100644
--- a/crates/uv/tests/it/snapshots/it__ecosystem__home-assistant-core-lock-file.snap
+++ b/crates/uv/tests/it/snapshots/it__ecosystem__home-assistant-core-lock-file.snap
@@ -3,7 +3,7 @@ source: crates/uv/tests/it/ecosystem.rs
expression: lock
---
version = 1
-revision = 2
+revision = 3
requires-python = ">=3.12.[X]"
[options]
diff --git a/crates/uv/tests/it/snapshots/it__ecosystem__packse-lock-file.snap b/crates/uv/tests/it/snapshots/it__ecosystem__packse-lock-file.snap
index 29810d89f..e15913e5c 100644
--- a/crates/uv/tests/it/snapshots/it__ecosystem__packse-lock-file.snap
+++ b/crates/uv/tests/it/snapshots/it__ecosystem__packse-lock-file.snap
@@ -3,7 +3,7 @@ source: crates/uv/tests/it/ecosystem.rs
expression: lock
---
version = 1
-revision = 2
+revision = 3
requires-python = ">=3.12"
[options]
diff --git a/crates/uv/tests/it/snapshots/it__ecosystem__saleor-lock-file.snap b/crates/uv/tests/it/snapshots/it__ecosystem__saleor-lock-file.snap
index 7ec8e0b5b..d757d7668 100644
--- a/crates/uv/tests/it/snapshots/it__ecosystem__saleor-lock-file.snap
+++ b/crates/uv/tests/it/snapshots/it__ecosystem__saleor-lock-file.snap
@@ -3,7 +3,7 @@ source: crates/uv/tests/it/ecosystem.rs
expression: lock
---
version = 1
-revision = 2
+revision = 3
requires-python = ">=3.12"
[options]
diff --git a/crates/uv/tests/it/snapshots/it__ecosystem__transformers-lock-file.snap b/crates/uv/tests/it/snapshots/it__ecosystem__transformers-lock-file.snap
index 616fddd6d..1d1a8ee2a 100644
--- a/crates/uv/tests/it/snapshots/it__ecosystem__transformers-lock-file.snap
+++ b/crates/uv/tests/it/snapshots/it__ecosystem__transformers-lock-file.snap
@@ -3,7 +3,7 @@ source: crates/uv/tests/it/ecosystem.rs
expression: lock
---
version = 1
-revision = 2
+revision = 3
requires-python = ">=3.9.0"
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'darwin'",
diff --git a/crates/uv/tests/it/snapshots/it__ecosystem__warehouse-lock-file.snap b/crates/uv/tests/it/snapshots/it__ecosystem__warehouse-lock-file.snap
index bf786a591..624e755ac 100644
--- a/crates/uv/tests/it/snapshots/it__ecosystem__warehouse-lock-file.snap
+++ b/crates/uv/tests/it/snapshots/it__ecosystem__warehouse-lock-file.snap
@@ -3,7 +3,7 @@ source: crates/uv/tests/it/ecosystem.rs
expression: lock
---
version = 1
-revision = 2
+revision = 3
requires-python = "==3.11.*"
[options]
diff --git a/crates/uv/tests/it/snapshots/it__workflow__jax_instability-2.snap b/crates/uv/tests/it/snapshots/it__workflow__jax_instability-2.snap
index ca4a08d1a..47b39f721 100644
--- a/crates/uv/tests/it/snapshots/it__workflow__jax_instability-2.snap
+++ b/crates/uv/tests/it/snapshots/it__workflow__jax_instability-2.snap
@@ -3,7 +3,7 @@ source: crates/uv/tests/it/workflow.rs
expression: lock
---
version = 1
-revision = 2
+revision = 3
requires-python = ">=3.9.0"
resolution-markers = [
"python_full_version >= '3.12'",
diff --git a/crates/uv/tests/it/snapshots/it__workflow__packse_add_remove_existing_package_noop-2.snap b/crates/uv/tests/it/snapshots/it__workflow__packse_add_remove_existing_package_noop-2.snap
index df7f8e63a..a08aa7d24 100644
--- a/crates/uv/tests/it/snapshots/it__workflow__packse_add_remove_existing_package_noop-2.snap
+++ b/crates/uv/tests/it/snapshots/it__workflow__packse_add_remove_existing_package_noop-2.snap
@@ -3,7 +3,7 @@ source: crates/uv/tests/it/workflow.rs
expression: lock
---
version = 1
-revision = 2
+revision = 3
requires-python = ">=3.12"
[options]
diff --git a/crates/uv/tests/it/snapshots/it__workflow__packse_add_remove_one_package-2.snap b/crates/uv/tests/it/snapshots/it__workflow__packse_add_remove_one_package-2.snap
index df7f8e63a..a08aa7d24 100644
--- a/crates/uv/tests/it/snapshots/it__workflow__packse_add_remove_one_package-2.snap
+++ b/crates/uv/tests/it/snapshots/it__workflow__packse_add_remove_one_package-2.snap
@@ -3,7 +3,7 @@ source: crates/uv/tests/it/workflow.rs
expression: lock
---
version = 1
-revision = 2
+revision = 3
requires-python = ">=3.12"
[options]
diff --git a/crates/uv/tests/it/snapshots/it__workflow__packse_promote_transitive_to_direct_then_remove-2.snap b/crates/uv/tests/it/snapshots/it__workflow__packse_promote_transitive_to_direct_then_remove-2.snap
index df7f8e63a..a08aa7d24 100644
--- a/crates/uv/tests/it/snapshots/it__workflow__packse_promote_transitive_to_direct_then_remove-2.snap
+++ b/crates/uv/tests/it/snapshots/it__workflow__packse_promote_transitive_to_direct_then_remove-2.snap
@@ -3,7 +3,7 @@ source: crates/uv/tests/it/workflow.rs
expression: lock
---
version = 1
-revision = 2
+revision = 3
requires-python = ">=3.12"
[options]
diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs
index 49951b42b..48e03c5bc 100644
--- a/crates/uv/tests/it/sync.rs
+++ b/crates/uv/tests/it/sync.rs
@@ -1691,7 +1691,7 @@ fn sync_relative_wheel() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -3246,7 +3246,7 @@ fn sync_group_legacy_non_project_member() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -3357,7 +3357,7 @@ fn sync_group_self() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -4367,7 +4367,7 @@ fn convert_to_virtual() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -4427,7 +4427,7 @@ fn convert_to_virtual() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -4496,7 +4496,7 @@ fn convert_to_package() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -4561,7 +4561,7 @@ fn convert_to_package() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -6456,7 +6456,7 @@ fn sync_dynamic_extra() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -7879,7 +7879,7 @@ fn sync_stale_egg_info() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.13"
[options]
@@ -7986,7 +7986,7 @@ fn sync_git_repeated_member_static_metadata() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.13"
[options]
@@ -8080,7 +8080,7 @@ fn sync_git_repeated_member_dynamic_metadata() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.13"
[options]
@@ -8198,7 +8198,7 @@ fn sync_git_repeated_member_backwards_path() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.13"
[options]
@@ -8380,7 +8380,7 @@ fn sync_git_path_dependency() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.13"
[options]
@@ -8488,7 +8488,7 @@ fn sync_build_tag() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -9156,7 +9156,7 @@ fn sync_locked_script() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11"
[options]
@@ -9261,7 +9261,7 @@ fn sync_locked_script() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11"
[options]
@@ -10027,7 +10027,7 @@ fn locked_version_coherence() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -10131,7 +10131,7 @@ fn sync_build_constraints() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -10635,6 +10635,157 @@ fn sync_url_with_query_parameters() -> Result<()> {
Ok(())
}
+/// Test uv sync with --exclude-newer-package
+#[test]
+fn sync_exclude_newer_package() -> Result<()> {
+ let context = TestContext::new("3.12").with_filtered_counts();
+
+ let pyproject_toml = context.temp_dir.child("pyproject.toml");
+ pyproject_toml.write_str(
+ r#"
+[project]
+name = "project"
+version = "0.1.0"
+requires-python = ">=3.12"
+dependencies = [
+ "tqdm",
+ "requests",
+]
+"#,
+ )?;
+
+ // First sync with only the global exclude-newer to show the baseline
+ uv_snapshot!(context.filters(), context
+ .sync()
+ .env_remove(EnvVars::UV_EXCLUDE_NEWER)
+ .arg("--exclude-newer")
+ .arg("2022-04-04T12:00:00Z"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Resolved [N] packages in [TIME]
+ Prepared [N] packages in [TIME]
+ Installed [N] packages in [TIME]
+ + certifi==2021.10.8
+ + charset-normalizer==2.0.12
+ + idna==3.3
+ + requests==2.27.1
+ + tqdm==4.64.0
+ + urllib3==1.26.9
+ "
+ );
+
+ // Now sync with --exclude-newer-package to allow tqdm to use a newer version
+ uv_snapshot!(context.filters(), context
+ .sync()
+ .env_remove(EnvVars::UV_EXCLUDE_NEWER)
+ .arg("--exclude-newer")
+ .arg("2022-04-04T12:00:00Z")
+ .arg("--exclude-newer-package")
+ .arg("tqdm=2022-09-04T00:00:00Z"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Ignoring existing lockfile due to change in timestamp cutoff: `global: 2022-04-04T12:00:00Z` vs. `global: 2022-04-04T12:00:00Z, tqdm: 2022-09-04T00:00:00Z`
+ Resolved [N] packages in [TIME]
+ Prepared [N] packages in [TIME]
+ Uninstalled [N] packages in [TIME]
+ Installed [N] packages in [TIME]
+ - tqdm==4.64.0
+ + tqdm==4.64.1
+ "
+ );
+
+ Ok(())
+}
+
+/// Test exclude-newer-package in pyproject.toml configuration
+#[test]
+fn sync_exclude_newer_package_config() -> Result<()> {
+ let context = TestContext::new("3.12").with_filtered_counts();
+
+ let pyproject_toml = context.temp_dir.child("pyproject.toml");
+ pyproject_toml.write_str(
+ r#"
+[project]
+name = "project"
+version = "0.1.0"
+requires-python = ">=3.12"
+dependencies = [
+ "tqdm",
+ "requests",
+]
+
+[tool.uv]
+exclude-newer = "2022-04-04T12:00:00Z"
+"#,
+ )?;
+
+ // First sync with only the global exclude-newer from the config
+ uv_snapshot!(context.filters(), context
+ .sync()
+ .env_remove(EnvVars::UV_EXCLUDE_NEWER), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Resolved [N] packages in [TIME]
+ Prepared [N] packages in [TIME]
+ Installed [N] packages in [TIME]
+ + certifi==2021.10.8
+ + charset-normalizer==2.0.12
+ + idna==3.3
+ + requests==2.27.1
+ + tqdm==4.64.0
+ + urllib3==1.26.9
+ "
+ );
+
+ // Now add the package-specific exclude-newer to the config
+ pyproject_toml.write_str(
+ r#"
+[project]
+name = "project"
+version = "0.1.0"
+requires-python = ">=3.12"
+dependencies = [
+ "tqdm",
+ "requests",
+]
+
+[tool.uv]
+exclude-newer = "2022-04-04T12:00:00Z"
+exclude-newer-package = { tqdm = "2022-09-04T00:00:00Z" }
+"#,
+ )?;
+
+ // Sync again with the package-specific override
+ uv_snapshot!(context.filters(), context
+ .sync()
+ .env_remove(EnvVars::UV_EXCLUDE_NEWER), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Ignoring existing lockfile due to change in timestamp cutoff: `global: 2022-04-04T12:00:00Z` vs. `global: 2022-04-04T12:00:00Z, tqdm: 2022-09-04T00:00:00Z`
+ Resolved [N] packages in [TIME]
+ Prepared [N] packages in [TIME]
+ Uninstalled [N] packages in [TIME]
+ Installed [N] packages in [TIME]
+ - tqdm==4.64.0
+ + tqdm==4.64.1
+ "
+ );
+
+ Ok(())
+}
+
#[test]
#[cfg(unix)]
fn read_only() -> Result<()> {
@@ -10803,7 +10954,7 @@ fn conflicting_editable() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", group = "bar" },
@@ -10969,7 +11120,7 @@ fn undeclared_editable() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
conflicts = [[
{ package = "project", group = "bar" },
diff --git a/crates/uv/tests/it/tree.rs b/crates/uv/tests/it/tree.rs
index 66d7cf2bc..5ef2be396 100644
--- a/crates/uv/tests/it/tree.rs
+++ b/crates/uv/tests/it/tree.rs
@@ -1250,7 +1250,7 @@ fn script() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11"
[options]
@@ -1436,7 +1436,7 @@ fn script() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.11"
[options]
diff --git a/crates/uv/tests/it/version.rs b/crates/uv/tests/it/version.rs
index e2f9f1201..0ac660d97 100644
--- a/crates/uv/tests/it/version.rs
+++ b/crates/uv/tests/it/version.rs
@@ -1962,7 +1962,7 @@ fn version_set_workspace() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -2022,7 +2022,7 @@ fn version_set_workspace() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -2149,7 +2149,7 @@ fn version_set_workspace() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -2295,7 +2295,7 @@ fn version_set_evil_constraints() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
@@ -2381,7 +2381,7 @@ fn version_set_evil_constraints() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
diff --git a/crates/uv/tests/it/workspace.rs b/crates/uv/tests/it/workspace.rs
index 631e4f6c3..1dfe51f74 100644
--- a/crates/uv/tests/it/workspace.rs
+++ b/crates/uv/tests/it/workspace.rs
@@ -1226,7 +1226,7 @@ fn workspace_inherit_sources() -> Result<()> {
assert_snapshot!(
lock, @r#"
version = 1
- revision = 2
+ revision = 3
requires-python = ">=3.12"
[options]
diff --git a/docs/reference/cli.md b/docs/reference/cli.md
index f761a49f0..5d4ffea68 100644
--- a/docs/reference/cli.md
+++ b/docs/reference/cli.md
@@ -97,7 +97,10 @@ uv run [OPTIONS] [COMMAND]
When enabled, uv will remove any extraneous packages from the environment. By default, uv run will make the minimum necessary changes to satisfy the requirements.
--exclude-newer exclude-newerLimit candidate packages to those that were uploaded prior to the given date.
Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.
-May also be set with the UV_EXCLUDE_NEWER environment variable.
Include optional dependencies from the specified extra name.
+May also be set with the UV_EXCLUDE_NEWER environment variable.
--exclude-newer-package exclude-newer-packageLimit candidate packages for specific packages to those that were uploaded prior to the given date.
+Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.
+Can be provided multiple times for different packages.
+Include optional dependencies from the specified extra name.
May be provided more than once.
Optional dependencies are defined via project.optional-dependencies in a pyproject.toml.
This option is only available when running in a project.
@@ -459,7 +462,10 @@ uv add [OPTIONS] >
--editableAdd the requirements as editable
--exclude-newer exclude-newerLimit candidate packages to those that were uploaded prior to the given date.
Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.
-May also be set with the UV_EXCLUDE_NEWER environment variable.
Extras to enable for the dependency.
+May also be set with the UV_EXCLUDE_NEWER environment variable.
--exclude-newer-package exclude-newer-packageLimit candidate packages for specific packages to those that were uploaded prior to the given date.
+Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.
+Can be provided multiple times for different packages.
+Extras to enable for the dependency.
May be provided more than once.
To add this dependency to an optional extra instead, see --optional.
(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
@@ -653,7 +659,10 @@ uv remove [OPTIONS] ...
See --project to only change the project root directory.
--exclude-newer exclude-newerLimit candidate packages to those that were uploaded prior to the given date.
Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.
-May also be set with the UV_EXCLUDE_NEWER environment variable.
(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
+May also be set with the UV_EXCLUDE_NEWER environment variable.
--exclude-newer-package exclude-newer-packageLimit candidate packages for specific packages to those that were uploaded prior to the given date.
+Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.
+Can be provided multiple times for different packages.
+(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.
May also be set with the UV_EXTRA_INDEX_URL environment variable.
--find-links, -f find-linksLocations to search for candidate distributions, in addition to those found in the registry indexes.
@@ -832,7 +841,10 @@ uv version [OPTIONS] [VALUE]
Instead, the version will be displayed.
--exclude-newer exclude-newerLimit candidate packages to those that were uploaded prior to the given date.
Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.
-May also be set with the UV_EXCLUDE_NEWER environment variable.
(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
+May also be set with the UV_EXCLUDE_NEWER environment variable.
--exclude-newer-package exclude-newer-packageLimit candidate packages for specific packages to those that were uploaded prior to the given date.
+Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.
+Can be provided multiple times for different packages.
+(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.
May also be set with the UV_EXTRA_INDEX_URL environment variable.
--find-links, -f find-linksLocations to search for candidate distributions, in addition to those found in the registry indexes.
@@ -1017,7 +1029,10 @@ uv sync [OPTIONS]
In dry-run mode, uv will resolve the project's dependencies and report on the resulting changes to both the lockfile and the project environment, but will not modify either.
--exclude-newer exclude-newerLimit candidate packages to those that were uploaded prior to the given date.
Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.
-May also be set with the UV_EXCLUDE_NEWER environment variable.
Include optional dependencies from the specified extra name.
+May also be set with the UV_EXCLUDE_NEWER environment variable.
--exclude-newer-package exclude-newer-packageLimit candidate packages for specific packages to those that were uploaded prior to the given date.
+Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.
+Can be provided multiple times for different packages.
+Include optional dependencies from the specified extra name.
May be provided more than once.
When multiple extras or groups are specified that appear in tool.uv.conflicts, uv will report an error.
Note that all optional dependencies are always included in the resolution; this option only affects the selection of packages to install.
@@ -1265,7 +1280,10 @@ uv lock [OPTIONS]
In dry-run mode, uv will resolve the project's dependencies and report on the resulting changes, but will not write the lockfile to disk.
--exclude-newer exclude-newerLimit candidate packages to those that were uploaded prior to the given date.
Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.
-May also be set with the UV_EXCLUDE_NEWER environment variable.
(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
+May also be set with the UV_EXCLUDE_NEWER environment variable.
--exclude-newer-package exclude-newer-packageLimit candidate packages for a specific package to those that were uploaded prior to the given date.
+Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.
+Can be provided multiple times for different packages.
+(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.
May also be set with the UV_EXTRA_INDEX_URL environment variable.
--find-links, -f find-linksLocations to search for candidate distributions, in addition to those found in the registry indexes.
@@ -1427,7 +1445,10 @@ uv export [OPTIONS]
See --project to only change the project root directory.
--exclude-newer exclude-newerLimit candidate packages to those that were uploaded prior to the given date.
Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.
-May also be set with the UV_EXCLUDE_NEWER environment variable.
Include optional dependencies from the specified extra name.
+May also be set with the UV_EXCLUDE_NEWER environment variable.
--exclude-newer-package exclude-newer-packageLimit candidate packages for a specific package to those that were uploaded prior to the given date.
+Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.
+Can be provided multiple times for different packages.
+Include optional dependencies from the specified extra name.
May be provided more than once.
(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
@@ -1623,7 +1644,10 @@ uv tree [OPTIONS]
See --project to only change the project root directory.
--exclude-newer exclude-newerLimit candidate packages to those that were uploaded prior to the given date.
Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.
-May also be set with the UV_EXCLUDE_NEWER environment variable.
(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
+May also be set with the UV_EXCLUDE_NEWER environment variable.
--exclude-newer-package exclude-newer-packageLimit candidate packages for a specific package to those that were uploaded prior to the given date.
+Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.
+Can be provided multiple times for different packages.
+(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.
May also be set with the UV_EXTRA_INDEX_URL environment variable.
--find-links, -f find-linksLocations to search for candidate distributions, in addition to those found in the registry indexes.
@@ -1886,7 +1910,10 @@ uv tool run [OPTIONS] [COMMAND]
Can be provided multiple times, with subsequent files overriding values defined in previous files.
May also be set with the UV_ENV_FILE environment variable.
--exclude-newer exclude-newerLimit candidate packages to those that were uploaded prior to the given date.
Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.
-May also be set with the UV_EXCLUDE_NEWER environment variable.
(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
+May also be set with the UV_EXCLUDE_NEWER environment variable.
--exclude-newer-package exclude-newer-packageLimit candidate packages for specific packages to those that were uploaded prior to the given date.
+Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.
+Can be provided multiple times for different packages.
+(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.
May also be set with the UV_EXTRA_INDEX_URL environment variable.
--find-links, -f find-linksLocations to search for candidate distributions, in addition to those found in the registry indexes.
@@ -2058,7 +2085,10 @@ uv tool install [OPTIONS]
--editable, -eInstall the target package in editable mode, such that changes in the package's source directory are reflected without reinstallation
--exclude-newer exclude-newerLimit candidate packages to those that were uploaded prior to the given date.
Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.
-May also be set with the UV_EXCLUDE_NEWER environment variable.
(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
+May also be set with the UV_EXCLUDE_NEWER environment variable.
--exclude-newer-package exclude-newer-packageLimit candidate packages for specific packages to those that were uploaded prior to the given date.
+Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.
+Can be provided multiple times for different packages.
+(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.
May also be set with the UV_EXTRA_INDEX_URL environment variable.
--find-links, -f find-linksLocations to search for candidate distributions, in addition to those found in the registry indexes.
@@ -2222,7 +2252,10 @@ uv tool upgrade [OPTIONS] ...
See --project to only change the project root directory.
--exclude-newer exclude-newerLimit candidate packages to those that were uploaded prior to the given date.
Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.
-May also be set with the UV_EXCLUDE_NEWER environment variable.
(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
+May also be set with the UV_EXCLUDE_NEWER environment variable.
--exclude-newer-package exclude-newer-packageLimit candidate packages for specific packages to those that were uploaded prior to the given date.
+Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.
+Can be provided multiple times for different packages.
+(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.
May also be set with the UV_EXTRA_INDEX_URL environment variable.
--find-links, -f find-linksLocations to search for candidate distributions, in addition to those found in the registry indexes.
@@ -3377,7 +3410,10 @@ uv pip compile [OPTIONS] >
--emit-index-urlInclude --index-url and --extra-index-url entries in the generated output file
--exclude-newer exclude-newerLimit candidate packages to those that were uploaded prior to the given date.
Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.
-May also be set with the UV_EXCLUDE_NEWER environment variable.
Include optional dependencies from the specified extra name; may be provided more than once.
+May also be set with the UV_EXCLUDE_NEWER environment variable.
--exclude-newer-package exclude-newer-packageLimit candidate packages for a specific package to those that were uploaded prior to the given date.
+Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.
+Can be provided multiple times for different packages.
+Include optional dependencies from the specified extra name; may be provided more than once.
Only applies to pyproject.toml, setup.py, and setup.cfg sources.
(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
@@ -3680,7 +3716,10 @@ uv pip sync [OPTIONS] ...
--dry-runPerform a dry run, i.e., don't actually install anything but resolve the dependencies and print the resulting plan
--exclude-newer exclude-newerLimit candidate packages to those that were uploaded prior to the given date.
Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.
-May also be set with the UV_EXCLUDE_NEWER environment variable.
Include optional dependencies from the specified extra name; may be provided more than once.
+May also be set with the UV_EXCLUDE_NEWER environment variable.
--exclude-newer-package exclude-newer-packageLimit candidate packages for specific packages to those that were uploaded prior to the given date.
+Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.
+Can be provided multiple times for different packages.
+Include optional dependencies from the specified extra name; may be provided more than once.
Only applies to pylock.toml, pyproject.toml, setup.py, and setup.cfg sources.
(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
@@ -3939,7 +3978,10 @@ uv pip install [OPTIONS] |--editable By default, installing will make the minimum necessary changes to satisfy the requirements. When enabled, uv will update the environment to exactly match the requirements, removing packages that are not included in the requirements.
--exclude-newer exclude-newerLimit candidate packages to those that were uploaded prior to the given date.
Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.
-May also be set with the UV_EXCLUDE_NEWER environment variable.
Include optional dependencies from the specified extra name; may be provided more than once.
+May also be set with the UV_EXCLUDE_NEWER environment variable.
--exclude-newer-package exclude-newer-packageLimit candidate packages for specific packages to those that were uploaded prior to the given date.
+Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.
+Can be provided multiple times for different packages.
+Include optional dependencies from the specified extra name; may be provided more than once.
Only applies to pylock.toml, pyproject.toml, setup.py, and setup.cfg sources.
(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
@@ -4735,7 +4777,10 @@ uv venv [OPTIONS] [PATH]
See --project to only change the project root directory.
--exclude-newer exclude-newerLimit candidate packages to those that were uploaded prior to the given date.
Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.
-May also be set with the UV_EXCLUDE_NEWER environment variable.
(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
+May also be set with the UV_EXCLUDE_NEWER environment variable.
--exclude-newer-package exclude-newer-packageLimit candidate packages for a specific package to those that were uploaded prior to the given date.
+Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.
+Can be provided multiple times for different packages.
+(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.
May also be set with the UV_EXTRA_INDEX_URL environment variable.
--find-links, -f find-linksLocations to search for candidate distributions, in addition to those found in the registry indexes.
@@ -4878,7 +4923,10 @@ uv build [OPTIONS] [SRC]
See --project to only change the project root directory.
--exclude-newer exclude-newerLimit candidate packages to those that were uploaded prior to the given date.
Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.
-May also be set with the UV_EXCLUDE_NEWER environment variable.
(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
+May also be set with the UV_EXCLUDE_NEWER environment variable.
--exclude-newer-package exclude-newer-packageLimit candidate packages for a specific package to those that were uploaded prior to the given date.
+Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.
+Can be provided multiple times for different packages.
+(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.
May also be set with the UV_EXTRA_INDEX_URL environment variable.
--find-links, -f find-linksLocations to search for candidate distributions, in addition to those found in the registry indexes.
diff --git a/docs/reference/settings.md b/docs/reference/settings.md
index 6469a584b..4a1d74b42 100644
--- a/docs/reference/settings.md
+++ b/docs/reference/settings.md
@@ -1101,6 +1101,32 @@ behave consistently across timezones.
---
+### [`exclude-newer-package`](#exclude-newer-package) {: #exclude-newer-package }
+
+Limit candidate packages for specific packages to those that were uploaded prior to the given date.
+
+Accepts package-date pairs in a dictionary format.
+
+**Default value**: `None`
+
+**Type**: `dict`
+
+**Example usage**:
+
+=== "pyproject.toml"
+
+ ```toml
+ [tool.uv]
+ exclude-newer-package = { tqdm = "2022-04-04T00:00:00Z" }
+ ```
+=== "uv.toml"
+
+ ```toml
+ exclude-newer-package = { tqdm = "2022-04-04T00:00:00Z" }
+ ```
+
+---
+
### [`extra-index-url`](#extra-index-url) {: #extra-index-url }
Extra URLs of package indexes to use, in addition to `--index-url`.
@@ -2534,6 +2560,34 @@ behave consistently across timezones.
---
+#### [`exclude-newer-package`](#pip_exclude-newer-package) {: #pip_exclude-newer-package }
+
+
+Limit candidate packages for specific packages to those that were uploaded prior to the given date.
+
+Accepts package-date pairs in a dictionary format.
+
+**Default value**: `None`
+
+**Type**: `dict`
+
+**Example usage**:
+
+=== "pyproject.toml"
+
+ ```toml
+ [tool.uv.pip]
+ exclude-newer-package = { tqdm = "2022-04-04T00:00:00Z" }
+ ```
+=== "uv.toml"
+
+ ```toml
+ [pip]
+ exclude-newer-package = { tqdm = "2022-04-04T00:00:00Z" }
+ ```
+
+---
+
#### [`extra`](#pip_extra) {: #pip_extra }
diff --git a/uv.schema.json b/uv.schema.json
index 7ca04d4f8..a2f3f0113 100644
--- a/uv.schema.json
+++ b/uv.schema.json
@@ -207,7 +207,18 @@
"description": "Limit candidate packages to those that were uploaded prior to a given point in time.\n\nAccepts a superset of [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) (e.g.,\n`2006-12-02T02:07:43Z`). A full timestamp is required to ensure that the resolver will\nbehave consistently across timezones.",
"anyOf": [
{
- "$ref": "#/definitions/ExcludeNewer"
+ "$ref": "#/definitions/ExcludeNewerTimestamp"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "exclude-newer-package": {
+ "description": "Limit candidate packages for specific packages to those that were uploaded prior to the given date.\n\nAccepts package-date pairs in a dictionary format.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/ExcludeNewerPackage"
},
{
"type": "null"
@@ -851,7 +862,13 @@
"type": "string",
"format": "uri"
},
- "ExcludeNewer": {
+ "ExcludeNewerPackage": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/definitions/ExcludeNewerTimestamp"
+ }
+ },
+ "ExcludeNewerTimestamp": {
"description": "Exclude distributions uploaded after the given timestamp.\n\nAccepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same format (e.g., `2006-12-02`).",
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2}))?$"
@@ -1270,7 +1287,18 @@
"description": "Limit candidate packages to those that were uploaded prior to a given point in time.\n\nAccepts a superset of [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) (e.g.,\n`2006-12-02T02:07:43Z`). A full timestamp is required to ensure that the resolver will\nbehave consistently across timezones.",
"anyOf": [
{
- "$ref": "#/definitions/ExcludeNewer"
+ "$ref": "#/definitions/ExcludeNewerTimestamp"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "exclude-newer-package": {
+ "description": "Limit candidate packages for specific packages to those that were uploaded prior to the given date.\n\nAccepts package-date pairs in a dictionary format.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/ExcludeNewerPackage"
},
{
"type": "null"
From e7c8b47b7aa885dab1927e6f207d82342327c53f Mon Sep 17 00:00:00 2001
From: Zanie Blue
Date: Tue, 29 Jul 2025 20:13:30 -0500
Subject: [PATCH 16/26] Clarify messaging when a new resolution needs to be
performed (#14938)
We do not just "ignore" the existing lockfile here. We retain the
existing messaging for cases where we do actually throw out the
lockfile, like `--upgrade`.
---
crates/uv/src/commands/project/lock.rs | 62 +++++++++++++-------------
crates/uv/tests/it/lock.rs | 12 ++---
crates/uv/tests/it/sync.rs | 8 ++--
3 files changed, 41 insertions(+), 41 deletions(-)
diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs
index fd9536b64..8edcaff71 100644
--- a/crates/uv/src/commands/project/lock.rs
+++ b/crates/uv/src/commands/project/lock.rs
@@ -943,7 +943,7 @@ impl ValidatedLock {
if lock.prerelease_mode() != options.prerelease_mode {
let _ = writeln!(
printer.stderr(),
- "Ignoring existing lockfile due to change in pre-release mode: `{}` vs. `{}`",
+ "Resolving despite existing lockfile due to change in pre-release mode: `{}` vs. `{}`",
lock.prerelease_mode().cyan(),
options.prerelease_mode.cyan()
);
@@ -1015,7 +1015,7 @@ impl ValidatedLock {
// to re-use the existing fork markers.
if let Err((fork_markers_union, environments_union)) = lock.check_marker_coverage() {
warn_user!(
- "Ignoring existing lockfile due to fork markers not covering the supported environments: `{}` vs `{}`",
+ "Resolving despite existing lockfile due to fork markers not covering the supported environments: `{}` vs `{}`",
fork_markers_union
.try_to_string()
.unwrap_or("true".to_string()),
@@ -1032,7 +1032,7 @@ impl ValidatedLock {
lock.requires_python_coverage(requires_python)
{
warn_user!(
- "Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `{}` vs `{}`",
+ "Resolving despite existing lockfile due to fork markers being disjoint with `requires-python`: `{}` vs `{}`",
fork_markers_union
.try_to_string()
.unwrap_or("true".to_string()),
@@ -1046,7 +1046,7 @@ impl ValidatedLock {
if let Upgrade::Packages(_) = upgrade {
// If the user specified `--upgrade-package`, then at best we can prefer some of
// the existing versions.
- debug!("Ignoring existing lockfile due to `--upgrade-package`");
+ debug!("Resolving despite existing lockfile due to `--upgrade-package`");
return Ok(Self::Preferable(lock));
}
@@ -1054,7 +1054,7 @@ impl ValidatedLock {
// the set of `resolution-markers` may no longer cover the entire supported Python range.
if lock.requires_python().range() != requires_python.range() {
debug!(
- "Ignoring existing lockfile due to change in Python requirement: `{}` vs. `{}`",
+ "Resolving despite existing lockfile due to change in Python requirement: `{}` vs. `{}`",
lock.requires_python(),
requires_python,
);
@@ -1076,7 +1076,7 @@ impl ValidatedLock {
.collect::>();
if expected != actual {
debug!(
- "Ignoring existing lockfile due to change in supported environments: `{:?}` vs. `{:?}`",
+ "Resolving despite existing lockfile due to change in supported environments: `{:?}` vs. `{:?}`",
expected, actual
);
return Ok(Self::Versions(lock));
@@ -1093,7 +1093,7 @@ impl ValidatedLock {
.collect::>();
if expected != actual {
debug!(
- "Ignoring existing lockfile due to change in supported environments: `{:?}` vs. `{:?}`",
+ "Resolving despite existing lockfile due to change in supported environments: `{:?}` vs. `{:?}`",
expected, actual
);
return Ok(Self::Versions(lock));
@@ -1102,7 +1102,7 @@ impl ValidatedLock {
// If the conflicting group config has changed, we have to perform a clean resolution.
if conflicts != lock.conflicts() {
debug!(
- "Ignoring existing lockfile due to change in conflicting groups: `{:?}` vs. `{:?}`",
+ "Resolving despite existing lockfile due to change in conflicting groups: `{:?}` vs. `{:?}`",
conflicts,
lock.conflicts(),
);
@@ -1149,7 +1149,7 @@ impl ValidatedLock {
}
SatisfiesResult::MismatchedMembers(expected, actual) => {
debug!(
- "Ignoring existing lockfile due to mismatched members:\n Requested: {:?}\n Existing: {:?}",
+ "Resolving despite existing lockfile due to mismatched members:\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
Ok(Self::Preferable(lock))
@@ -1157,11 +1157,11 @@ impl ValidatedLock {
SatisfiesResult::MismatchedVirtual(name, expected) => {
if expected {
debug!(
- "Ignoring existing lockfile due to mismatched source: `{name}` (expected: `virtual`)"
+ "Resolving despite existing lockfile due to mismatched source: `{name}` (expected: `virtual`)"
);
} else {
debug!(
- "Ignoring existing lockfile due to mismatched source: `{name}` (expected: `editable`)"
+ "Resolving despite existing lockfile due to mismatched source: `{name}` (expected: `editable`)"
);
}
Ok(Self::Preferable(lock))
@@ -1169,11 +1169,11 @@ impl ValidatedLock {
SatisfiesResult::MismatchedDynamic(name, expected) => {
if expected {
debug!(
- "Ignoring existing lockfile due to static version: `{name}` (expected a dynamic version)"
+ "Resolving despite existing lockfile due to static version: `{name}` (expected a dynamic version)"
);
} else {
debug!(
- "Ignoring existing lockfile due to dynamic version: `{name}` (expected a static version)"
+ "Resolving despite existing lockfile due to dynamic version: `{name}` (expected a static version)"
);
}
Ok(Self::Preferable(lock))
@@ -1181,70 +1181,70 @@ impl ValidatedLock {
SatisfiesResult::MismatchedVersion(name, expected, actual) => {
if let Some(actual) = actual {
debug!(
- "Ignoring existing lockfile due to mismatched version: `{name}` (expected: `{expected}`, found: `{actual}`)"
+ "Resolving despite existing lockfile due to mismatched version: `{name}` (expected: `{expected}`, found: `{actual}`)"
);
} else {
debug!(
- "Ignoring existing lockfile due to mismatched version: `{name}` (expected: `{expected}`)"
+ "Resolving despite existing lockfile due to mismatched version: `{name}` (expected: `{expected}`)"
);
}
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedRequirements(expected, actual) => {
debug!(
- "Ignoring existing lockfile due to mismatched requirements:\n Requested: {:?}\n Existing: {:?}",
+ "Resolving despite existing lockfile due to mismatched requirements:\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedConstraints(expected, actual) => {
debug!(
- "Ignoring existing lockfile due to mismatched constraints:\n Requested: {:?}\n Existing: {:?}",
+ "Resolving despite existing lockfile due to mismatched constraints:\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedOverrides(expected, actual) => {
debug!(
- "Ignoring existing lockfile due to mismatched overrides:\n Requested: {:?}\n Existing: {:?}",
+ "Resolving despite existing lockfile due to mismatched overrides:\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedBuildConstraints(expected, actual) => {
debug!(
- "Ignoring existing lockfile due to mismatched build constraints:\n Requested: {:?}\n Existing: {:?}",
+ "Resolving despite existing lockfile due to mismatched build constraints:\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedDependencyGroups(expected, actual) => {
debug!(
- "Ignoring existing lockfile due to mismatched dependency groups:\n Requested: {:?}\n Existing: {:?}",
+ "Resolving despite existing lockfile due to mismatched dependency groups:\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedStaticMetadata(expected, actual) => {
debug!(
- "Ignoring existing lockfile due to mismatched static metadata:\n Requested: {:?}\n Existing: {:?}",
+ "Resolving despite existing lockfile due to mismatched static metadata:\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MissingRoot(name) => {
- debug!("Ignoring existing lockfile due to missing root package: `{name}`");
+ debug!("Resolving despite existing lockfile due to missing root package: `{name}`");
Ok(Self::Preferable(lock))
}
SatisfiesResult::MissingRemoteIndex(name, version, index) => {
debug!(
- "Ignoring existing lockfile due to missing remote index: `{name}` `{version}` from `{index}`"
+ "Resolving despite existing lockfile due to missing remote index: `{name}` `{version}` from `{index}`"
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MissingLocalIndex(name, version, index) => {
debug!(
- "Ignoring existing lockfile due to missing local index: `{name}` `{version}` from `{}`",
+ "Resolving despite existing lockfile due to missing local index: `{name}` `{version}` from `{}`",
index.display()
);
Ok(Self::Preferable(lock))
@@ -1252,12 +1252,12 @@ impl ValidatedLock {
SatisfiesResult::MismatchedPackageRequirements(name, version, expected, actual) => {
if let Some(version) = version {
debug!(
- "Ignoring existing lockfile due to mismatched requirements for: `{name}=={version}`\n Requested: {:?}\n Existing: {:?}",
+ "Resolving despite existing lockfile due to mismatched requirements for: `{name}=={version}`\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
} else {
debug!(
- "Ignoring existing lockfile due to mismatched requirements for: `{name}`\n Requested: {:?}\n Existing: {:?}",
+ "Resolving despite existing lockfile due to mismatched requirements for: `{name}`\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
}
@@ -1266,12 +1266,12 @@ impl ValidatedLock {
SatisfiesResult::MismatchedPackageDependencyGroups(name, version, expected, actual) => {
if let Some(version) = version {
debug!(
- "Ignoring existing lockfile due to mismatched dependency groups for: `{name}=={version}`\n Requested: {:?}\n Existing: {:?}",
+ "Resolving despite existing lockfile due to mismatched dependency groups for: `{name}=={version}`\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
} else {
debug!(
- "Ignoring existing lockfile due to mismatched dependency groups for: `{name}`\n Requested: {:?}\n Existing: {:?}",
+ "Resolving despite existing lockfile due to mismatched dependency groups for: `{name}`\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
}
@@ -1280,19 +1280,19 @@ impl ValidatedLock {
SatisfiesResult::MismatchedPackageProvidesExtra(name, version, expected, actual) => {
if let Some(version) = version {
debug!(
- "Ignoring existing lockfile due to mismatched extras for: `{name}=={version}`\n Requested: {:?}\n Existing: {:?}",
+ "Resolving despite existing lockfile due to mismatched extras for: `{name}=={version}`\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
} else {
debug!(
- "Ignoring existing lockfile due to mismatched extras for: `{name}`\n Requested: {:?}\n Existing: {:?}",
+ "Resolving despite existing lockfile due to mismatched extras for: `{name}`\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
}
Ok(Self::Preferable(lock))
}
SatisfiesResult::MissingVersion(name) => {
- debug!("Ignoring existing lockfile due to missing version: `{name}`");
+ debug!("Resolving despite existing lockfile due to missing version: `{name}`");
Ok(Self::Preferable(lock))
}
}
diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs
index 5bd4b31b5..acc36eacf 100644
--- a/crates/uv/tests/it/lock.rs
+++ b/crates/uv/tests/it/lock.rs
@@ -4804,7 +4804,7 @@ fn lock_requires_python_wheels() -> Result<()> {
----- stderr -----
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
- warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version == '3.12.*'` vs `python_full_version == '3.11.*'`
+ warning: Resolving despite existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version == '3.12.*'` vs `python_full_version == '3.11.*'`
Resolved 2 packages in [TIME]
");
@@ -16338,7 +16338,7 @@ fn lock_explicit_default_index() -> Result<()> {
DEBUG Using request timeout of [TIME]
DEBUG Found static `pyproject.toml` for: project @ file://[TEMP_DIR]/
DEBUG No workspace root found, using project root
- DEBUG Ignoring existing lockfile due to mismatched requirements for: `project==0.1.0`
+ DEBUG Resolving despite existing lockfile due to mismatched requirements for: `project==0.1.0`
Requested: {Requirement { name: PackageName("anyio"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers([]), index: None, conflict: None }, origin: None }}
Existing: {Requirement { name: PackageName("iniconfig"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers([VersionSpecifier { operator: Equal, version: "2.0.0" }]), index: Some(IndexMetadata { url: Url(VerbatimUrl { url: DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("test.pypi.org")), port: None, path: "/simple", query: None, fragment: None }, given: None }), format: Simple }), conflict: None }, origin: None }}
DEBUG Solving with installed Python version: 3.12.[X]
@@ -28365,16 +28365,16 @@ fn lock_invalid_fork_markers() -> Result<()> {
"#,
)?;
- uv_snapshot!(context.filters(), context.lock(), @r###"
+ uv_snapshot!(context.filters(), context.lock(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
- warning: Ignoring existing lockfile due to fork markers not covering the supported environments: `(python_full_version >= '3.8' and python_full_version < '3.10') or (python_full_version >= '3.8' and platform_python_implementation == 'CPython')` vs `python_full_version >= '3.8'`
+ warning: Resolving despite existing lockfile due to fork markers not covering the supported environments: `(python_full_version >= '3.8' and python_full_version < '3.10') or (python_full_version >= '3.8' and platform_python_implementation == 'CPython')` vs `python_full_version >= '3.8'`
Resolved 2 packages in [TIME]
Updated idna v3.10 -> v3.6
- "###);
+ ");
// Check that the lockfile got updated and we don't show the warning anymore.
uv_snapshot!(context.filters(), context.lock(), @r###"
@@ -28671,7 +28671,7 @@ fn lock_requires_python_empty_lock_file() -> Result<()> {
----- stderr -----
Using CPython 3.13.2 interpreter at: [PYTHON-3.13.2]
- warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version == '3.13.0'` vs `python_full_version == '3.13.2'`
+ warning: Resolving despite existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version == '3.13.0'` vs `python_full_version == '3.13.2'`
Resolved 3 packages in [TIME]
");
diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs
index 48e03c5bc..a9dc60dcc 100644
--- a/crates/uv/tests/it/sync.rs
+++ b/crates/uv/tests/it/sync.rs
@@ -8830,7 +8830,7 @@ fn sync_dry_run() -> Result<()> {
----- stderr -----
Using CPython 3.9.[X] interpreter at: [PYTHON-3.9]
Would replace project environment at: .venv
- warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.12'` vs `python_full_version == '3.9.*'`
+ warning: Resolving despite existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.12'` vs `python_full_version == '3.9.*'`
Resolved 2 packages in [TIME]
Would update lockfile at: uv.lock
Would install 1 package
@@ -8847,7 +8847,7 @@ fn sync_dry_run() -> Result<()> {
Using CPython 3.9.[X] interpreter at: [PYTHON-3.9]
Removed virtual environment at: .venv
Creating virtual environment at: .venv
- warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.12'` vs `python_full_version == '3.9.*'`
+ warning: Resolving despite existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.12'` vs `python_full_version == '3.9.*'`
Resolved 2 packages in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
@@ -9338,7 +9338,7 @@ fn sync_locked_script() -> Result<()> {
----- stderr -----
Updating script environment at: [CACHE_DIR]/environments-v2/script-[HASH]
- warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.11'` vs `python_full_version >= '3.8' and python_full_version < '3.11'`
+ warning: Resolving despite existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.11'` vs `python_full_version >= '3.8' and python_full_version < '3.11'`
Resolved 6 packages in [TIME]
The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
");
@@ -9350,7 +9350,7 @@ fn sync_locked_script() -> Result<()> {
----- stderr -----
Using script environment at: [CACHE_DIR]/environments-v2/script-[HASH]
- warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.11'` vs `python_full_version >= '3.8' and python_full_version < '3.11'`
+ warning: Resolving despite existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.11'` vs `python_full_version >= '3.8' and python_full_version < '3.11'`
Resolved 6 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 6 packages in [TIME]
From b31d786fe9559869799bb402061b56340b35abb5 Mon Sep 17 00:00:00 2001
From: Charlie Marsh
Date: Tue, 29 Jul 2025 21:24:59 -0400
Subject: [PATCH 17/26] Add `UV_` prefix to installer environment variables
(#14964)
## Summary
Available as of https://github.com/astral-sh/cargo-dist/pull/46.
---
.github/workflows/release.yml | 2 +-
crates/uv-static/src/env_vars.rs | 10 ++++++++--
dist-workspace.toml | 2 +-
docs/getting-started/installation.md | 2 +-
docs/reference/environment.md | 17 ++++++++++++-----
docs/reference/installer.md | 8 ++++----
6 files changed, 27 insertions(+), 14 deletions(-)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 2688c3fc8..3ea118d88 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -69,7 +69,7 @@ jobs:
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
- run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.7-prerelease.1/cargo-dist-installer.sh | sh"
+ run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.7-prerelease.2/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs
index b8adf225f..5032d42b6 100644
--- a/crates/uv-static/src/env_vars.rs
+++ b/crates/uv-static/src/env_vars.rs
@@ -731,9 +731,15 @@ impl EnvVars {
/// the installer from modifying shell profiles or environment variables.
pub const UV_UNMANAGED_INSTALL: &'static str = "UV_UNMANAGED_INSTALL";
+ /// The URL from which to download uv using the standalone installer. By default, installs from
+ /// uv's GitHub Releases. `INSTALLER_DOWNLOAD_URL` is also supported as an alias, for backwards
+ /// compatibility.
+ pub const UV_DOWNLOAD_URL: &'static str = "UV_DOWNLOAD_URL";
+
/// Avoid modifying the `PATH` environment variable when installing uv using the standalone
- /// installer and `self update` feature.
- pub const INSTALLER_NO_MODIFY_PATH: &'static str = "INSTALLER_NO_MODIFY_PATH";
+ /// installer and `self update` feature. `INSTALLER_NO_MODIFY_PATH` is also supported as an
+ /// alias, for backwards compatibility.
+ pub const UV_NO_MODIFY_PATH: &'static str = "UV_NO_MODIFY_PATH";
/// Skip writing `uv` installer metadata files (e.g., `INSTALLER`, `REQUESTED`, and `direct_url.json`) to site-packages `.dist-info` directories.
pub const UV_NO_INSTALLER_METADATA: &'static str = "UV_NO_INSTALLER_METADATA";
diff --git a/dist-workspace.toml b/dist-workspace.toml
index 3e16bd4cf..15b703d98 100644
--- a/dist-workspace.toml
+++ b/dist-workspace.toml
@@ -4,7 +4,7 @@ members = ["cargo:."]
# Config for 'dist'
[dist]
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
-cargo-dist-version = "0.28.7-prerelease.1"
+cargo-dist-version = "0.28.7-prerelease.2"
# make a package being included in our releases opt-in instead of opt-out
dist = false
# CI backends to support
diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md
index 4b815b9bf..2688c418b 100644
--- a/docs/getting-started/installation.md
+++ b/docs/getting-started/installation.md
@@ -151,7 +151,7 @@ $ uv self update
!!! tip
Updating uv will re-run the installer and can modify your shell profiles. To disable this
- behavior, set `INSTALLER_NO_MODIFY_PATH=1`.
+ behavior, set `UV_NO_MODIFY_PATH=1`.
When another installation method is used, self-updates are disabled. Use the package manager's
upgrade method instead. For example, with `pip`:
diff --git a/docs/reference/environment.md b/docs/reference/environment.md
index 1958b8780..a7c1b530d 100644
--- a/docs/reference/environment.md
+++ b/docs/reference/environment.md
@@ -68,6 +68,12 @@ script, to include the name of the wrapper script in the output file.
Equivalent to the `--default-index` command-line argument. If set, uv will use
this URL as the default index when searching for packages.
+### `UV_DOWNLOAD_URL`
+
+The URL from which to download uv using the standalone installer. By default, installs from
+uv's GitHub Releases. `INSTALLER_DOWNLOAD_URL` is also supported as an alias, for backwards
+compatibility.
+
### `UV_ENV_FILE`
`.env` files from which to load environment variables when executing `uv run` commands.
@@ -269,6 +275,12 @@ Skip writing `uv` installer metadata files (e.g., `INSTALLER`, `REQUESTED`, and
Disable use of uv-managed Python versions.
+### `UV_NO_MODIFY_PATH`
+
+Avoid modifying the `PATH` environment variable when installing uv using the standalone
+installer and `self update` feature. `INSTALLER_NO_MODIFY_PATH` is also supported as an
+alias, for backwards compatibility.
+
### `UV_NO_PROGRESS`
Equivalent to the `--no-progress` command-line argument. Disables all progress output. For
@@ -562,11 +574,6 @@ Proxy for HTTP requests.
Timeout (in seconds) for HTTP requests. Equivalent to `UV_HTTP_TIMEOUT`.
-### `INSTALLER_NO_MODIFY_PATH`
-
-Avoid modifying the `PATH` environment variable when installing uv using the standalone
-installer and `self update` feature.
-
### `JPY_SESSION_NAME`
Used to detect when running inside a Jupyter notebook.
diff --git a/docs/reference/installer.md b/docs/reference/installer.md
index b6bb34df9..609a8f7ec 100644
--- a/docs/reference/installer.md
+++ b/docs/reference/installer.md
@@ -23,14 +23,14 @@ To change the installation path, use `UV_INSTALL_DIR`:
## Disabling shell modifications
The installer may also update your shell profiles to ensure the uv binary is on your `PATH`. To
-disable this behavior, use `INSTALLER_NO_MODIFY_PATH`. For example:
+disable this behavior, use `UV_NO_MODIFY_PATH`. For example:
```console
-$ curl -LsSf https://astral.sh/uv/install.sh | env INSTALLER_NO_MODIFY_PATH=1 sh
+$ curl -LsSf https://astral.sh/uv/install.sh | env UV_NO_MODIFY_PATH=1 sh
```
-If installed with `INSTALLER_NO_MODIFY_PATH`, subsequent operations, like `uv self update`, will not
-modify your shell profiles.
+If installed with `UV_NO_MODIFY_PATH`, subsequent operations, like `uv self update`, will not modify
+your shell profiles.
## Unmanaged installations
From b2eff990df2b7f6b29dd60b13c7647076dfbb621 Mon Sep 17 00:00:00 2001
From: Boseong Choi <31615733+cbscsm@users.noreply.github.com>
Date: Wed, 30 Jul 2025 19:25:48 +0900
Subject: [PATCH 18/26] Fix typo in uv-pep440/README.md (#14965)
## Summary
I noticed what appears to be a small typo in the documentation. In the
section describing dev versions, it says `sbpth table releases`. I
believe this was meant to be `both stable releases`, to match the
structure of the previous sentence about post versions.
---
crates/uv-pep440/Readme.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/crates/uv-pep440/Readme.md b/crates/uv-pep440/Readme.md
index cc9281738..8a6e142ca 100644
--- a/crates/uv-pep440/Readme.md
+++ b/crates/uv-pep440/Readme.md
@@ -27,7 +27,7 @@ PEP 440 has a lot of unintuitive features, including:
- An epoch that you can prefix the version with, e.g., `1!1.2.3`. Lower epoch always means lower
version (`1.0 <=2!0.1`)
- Post versions, which can be attached to both stable releases and pre-releases
-- Dev versions, which can be attached to sbpth table releases and pre-releases. When attached to a
+- Dev versions, which can be attached to both stable releases and pre-releases. When attached to a
pre-release the dev version is ordered just below the normal pre-release, however when attached to
a stable version, the dev version is sorted before a pre-releases
- Pre-release handling is a mess: "Pre-releases of any kind, including developmental releases, are
From 17f0c91896d43b66851229d26385a6989d9e71b2 Mon Sep 17 00:00:00 2001
From: konsti
Date: Wed, 30 Jul 2025 14:04:07 +0200
Subject: [PATCH 19/26] Show uv_build in projects documentation (#14968)
Fix https://github.com/astral-sh/uv/issues/14957
---
docs/concepts/projects/init.md | 12 ++++++------
docs/concepts/projects/workspaces.md | 12 ++++++------
pyproject.toml | 2 ++
3 files changed, 14 insertions(+), 12 deletions(-)
diff --git a/docs/concepts/projects/init.md b/docs/concepts/projects/init.md
index 3a8dd244e..1a012393e 100644
--- a/docs/concepts/projects/init.md
+++ b/docs/concepts/projects/init.md
@@ -111,8 +111,8 @@ dependencies = []
example-pkg = "example_pkg:main"
[build-system]
-requires = ["hatchling"]
-build-backend = "hatchling.build"
+requires = ["uv_build>=0.8.3,<0.9.0"]
+build-backend = "uv_build"
```
!!! tip
@@ -134,8 +134,8 @@ dependencies = []
example-pkg = "example_pkg:main"
[build-system]
-requires = ["hatchling"]
-build-backend = "hatchling.build"
+requires = ["uv_build>=0.8.3,<0.9.0"]
+build-backend = "uv_build"
```
The command can be executed with `uv run`:
@@ -195,8 +195,8 @@ requires-python = ">=3.11"
dependencies = []
[build-system]
-requires = ["hatchling"]
-build-backend = "hatchling.build"
+requires = ["uv_build>=0.8.3,<0.9.0"]
+build-backend = "uv_build"
```
!!! tip
diff --git a/docs/concepts/projects/workspaces.md b/docs/concepts/projects/workspaces.md
index 4b2d670b4..641b4d21f 100644
--- a/docs/concepts/projects/workspaces.md
+++ b/docs/concepts/projects/workspaces.md
@@ -75,8 +75,8 @@ bird-feeder = { workspace = true }
members = ["packages/*"]
[build-system]
-requires = ["hatchling"]
-build-backend = "hatchling.build"
+requires = ["uv_build>=0.8.3,<0.9.0"]
+build-backend = "uv_build"
```
In this example, the `albatross` project depends on the `bird-feeder` project, which is a member of
@@ -106,8 +106,8 @@ tqdm = { git = "https://github.com/tqdm/tqdm" }
members = ["packages/*"]
[build-system]
-requires = ["hatchling"]
-build-backend = "hatchling.build"
+requires = ["uv_build>=0.8.3,<0.9.0"]
+build-backend = "uv_build"
```
Every workspace member would, by default, install `tqdm` from GitHub, unless a specific member
@@ -188,8 +188,8 @@ dependencies = ["bird-feeder", "tqdm>=4,<5"]
bird-feeder = { path = "packages/bird-feeder" }
[build-system]
-requires = ["hatchling"]
-build-backend = "hatchling.build"
+requires = ["uv_build>=0.8.3,<0.9.0"]
+build-backend = "uv_build"
```
This approach conveys many of the same benefits, but allows for more fine-grained control over
diff --git a/pyproject.toml b/pyproject.toml
index 547c4dd28..a88ff8554 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -87,6 +87,8 @@ version_files = [
"docs/guides/integration/github.md",
"docs/guides/integration/aws-lambda.md",
"docs/concepts/build-backend.md",
+ "docs/concepts/projects/init.md",
+ "docs/concepts/projects/workspaces.md",
]
[tool.mypy]
From 6856a27711910d240a488d53d7967b73340b1524 Mon Sep 17 00:00:00 2001
From: Zanie Blue
Date: Wed, 30 Jul 2025 09:53:07 -0500
Subject: [PATCH 20/26] Add `extra-build-dependencies` (#14735)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Replaces https://github.com/astral-sh/uv/pull/14092
Adds `tool.uv.extra-build-dependencies = {package = [dependency, ...]}`
which extends `build-system.requires` during package builds.
These are lowered via workspace sources, are applied to transitive
dependencies, and are included in the wheel cache shard hash.
There are some features we need to follow-up on, but are out of scope
here:
- Preferring locked versions for build dependencies
- Settings for requiring locked versions for build depencies
There are some quality of life follow-ups we should also do:
- Warn on `extra-build-dependencies` that do not apply to any packages
- Add test cases and improve error messaging when the
`extra-build-dependencies` resolve fails
-------
There ~are~ were a few open decisions to be made here
1. Should we resolve these dependencies alongside the
`build-system.requires` dependencies? Or should we resolve separately?
(I think the latter is more powerful? because you can override things?
but it opens the door to breaking your build)
2. Should we install these dependencies into the same environment? Or
should we layer it on top as we do elsewhere? (I think it's fine to
install into the same environment)
3. Should we respect sources defined in the parent project? (I think
yes, but then we need to lower the dependencies earlier — I don't think
that's a big deal, but it's not implemented)
4. Should we respect sources defined in the child project? (I think no,
this gets really complicated and seems weird to allow)
5. Should we apply this to transitive dependencies? (I think so)
---------
Co-authored-by: Aria Desires
Co-authored-by: konstin
---
Cargo.lock | 4 +
crates/uv-bench/benches/uv.rs | 2 +
crates/uv-build-frontend/src/lib.rs | 48 +-
crates/uv-cli/src/options.rs | 2 +
crates/uv-configuration/src/preview.rs | 7 +
crates/uv-dispatch/src/lib.rs | 9 +
crates/uv-distribution/src/lib.rs | 4 +-
.../src/metadata/build_requires.rs | 93 ++-
crates/uv-distribution/src/metadata/mod.rs | 2 +-
crates/uv-distribution/src/source/mod.rs | 70 ++-
crates/uv-pep440/Cargo.toml | 1 +
crates/uv-pep440/src/version.rs | 107 +++-
crates/uv-pep440/src/version_specifier.rs | 5 +
crates/uv-pep508/Cargo.toml | 1 +
crates/uv-pep508/src/lib.rs | 47 ++
crates/uv-scripts/Cargo.toml | 2 +
crates/uv-scripts/src/lib.rs | 49 ++
crates/uv-settings/src/combine.rs | 45 +-
crates/uv-settings/src/lib.rs | 4 +
crates/uv-settings/src/settings.rs | 38 +-
crates/uv-types/src/traits.rs | 3 +
crates/uv-workspace/src/pyproject.rs | 183 ++++--
crates/uv-workspace/src/workspace.rs | 6 +
crates/uv/src/commands/build_frontend.rs | 7 +
crates/uv/src/commands/pip/compile.rs | 20 +-
crates/uv/src/commands/pip/install.rs | 16 +-
crates/uv/src/commands/pip/sync.rs | 16 +-
crates/uv/src/commands/project/add.rs | 26 +-
crates/uv/src/commands/project/export.rs | 4 +-
crates/uv/src/commands/project/lock.rs | 31 +-
crates/uv/src/commands/project/mod.rs | 100 +--
crates/uv/src/commands/project/remove.rs | 4 +-
crates/uv/src/commands/project/run.rs | 7 +-
crates/uv/src/commands/project/sync.rs | 73 ++-
crates/uv/src/commands/project/tree.rs | 5 +-
crates/uv/src/commands/tool/install.rs | 3 +-
crates/uv/src/commands/tool/upgrade.rs | 3 +-
crates/uv/src/commands/venv.rs | 5 +-
crates/uv/src/lib.rs | 39 +-
crates/uv/src/settings.rs | 16 +-
crates/uv/tests/it/pip_install.rs | 5 +-
crates/uv/tests/it/show_settings.rs | 133 +++-
crates/uv/tests/it/sync.rs | 576 ++++++++++++++++++
docs/reference/settings.md | 84 +++
.../packages/anyio_local/anyio/__init__.py | 2 +
uv.schema.json | 31 +
46 files changed, 1744 insertions(+), 194 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 5b2fd66b8..396b77bea 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5516,6 +5516,7 @@ dependencies = [
"tracing",
"unicode-width 0.2.1",
"unscanny",
+ "uv-cache-key",
"version-ranges",
]
@@ -5539,6 +5540,7 @@ dependencies = [
"tracing-test",
"unicode-width 0.2.1",
"url",
+ "uv-cache-key",
"uv-fs",
"uv-normalize",
"uv-pep440",
@@ -5864,6 +5866,8 @@ dependencies = [
"thiserror 2.0.12",
"toml",
"url",
+ "uv-configuration",
+ "uv-distribution-types",
"uv-pep440",
"uv-pep508",
"uv-pypi-types",
diff --git a/crates/uv-bench/benches/uv.rs b/crates/uv-bench/benches/uv.rs
index 8adfd5a0e..df19354f6 100644
--- a/crates/uv-bench/benches/uv.rs
+++ b/crates/uv-bench/benches/uv.rs
@@ -141,6 +141,7 @@ mod resolver {
universal: bool,
) -> Result {
let build_isolation = BuildIsolation::default();
+ let extra_build_requires = uv_distribution::ExtraBuildRequires::default();
let build_options = BuildOptions::default();
let concurrency = Concurrency::default();
let config_settings = ConfigSettings::default();
@@ -189,6 +190,7 @@ mod resolver {
&config_settings,
&config_settings_package,
build_isolation,
+ &extra_build_requires,
LinkMode::default(),
&build_options,
&hashes,
diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs
index b815719ba..58cf441ab 100644
--- a/crates/uv-build-frontend/src/lib.rs
+++ b/crates/uv-build-frontend/src/lib.rs
@@ -4,6 +4,7 @@
mod error;
+use std::borrow::Cow;
use std::ffi::OsString;
use std::fmt::Formatter;
use std::fmt::Write;
@@ -42,6 +43,7 @@ use uv_static::EnvVars;
use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, SourceBuildTrait};
use uv_warnings::warn_user_once;
use uv_workspace::WorkspaceCache;
+use uv_workspace::pyproject::ExtraBuildDependencies;
pub use crate::error::{Error, MissingHeaderCause};
@@ -281,6 +283,7 @@ impl SourceBuild {
workspace_cache: &WorkspaceCache,
config_settings: ConfigSettings,
build_isolation: BuildIsolation<'_>,
+ extra_build_dependencies: &ExtraBuildDependencies,
build_stack: &BuildStack,
build_kind: BuildKind,
mut environment_variables: FxHashMap,
@@ -297,7 +300,6 @@ impl SourceBuild {
};
let default_backend: Pep517Backend = DEFAULT_BACKEND.clone();
-
// Check if we have a PEP 517 build backend.
let (pep517_backend, project) = Self::extract_pep517_backend(
&source_tree,
@@ -322,6 +324,14 @@ impl SourceBuild {
.or(fallback_package_version)
.cloned();
+ let extra_build_dependencies: Vec = package_name
+ .as_ref()
+ .and_then(|name| extra_build_dependencies.get(name).cloned())
+ .unwrap_or_default()
+ .into_iter()
+ .map(Requirement::from)
+ .collect();
+
// Create a virtual environment, or install into the shared environment if requested.
let venv = if let Some(venv) = build_isolation.shared_environment(package_name.as_ref()) {
venv.clone()
@@ -344,11 +354,18 @@ impl SourceBuild {
if build_isolation.is_isolated(package_name.as_ref()) {
debug!("Resolving build requirements");
+ let dependency_sources = if extra_build_dependencies.is_empty() {
+ "`build-system.requires`"
+ } else {
+ "`build-system.requires` and `extra-build-dependencies`"
+ };
+
let resolved_requirements = Self::get_resolved_requirements(
build_context,
source_build_context,
&default_backend,
&pep517_backend,
+ extra_build_dependencies,
build_stack,
)
.await?;
@@ -356,7 +373,7 @@ impl SourceBuild {
build_context
.install(&resolved_requirements, &venv, build_stack)
.await
- .map_err(|err| Error::RequirementsInstall("`build-system.requires`", err.into()))?;
+ .map_err(|err| Error::RequirementsInstall(dependency_sources, err.into()))?;
} else {
debug!("Proceeding without build isolation");
}
@@ -471,10 +488,13 @@ impl SourceBuild {
source_build_context: SourceBuildContext,
default_backend: &Pep517Backend,
pep517_backend: &Pep517Backend,
+ extra_build_dependencies: Vec,
build_stack: &BuildStack,
) -> Result {
Ok(
- if pep517_backend.requirements == default_backend.requirements {
+ if pep517_backend.requirements == default_backend.requirements
+ && extra_build_dependencies.is_empty()
+ {
let mut resolution = source_build_context.default_resolution.lock().await;
if let Some(resolved_requirements) = &*resolution {
resolved_requirements.clone()
@@ -489,12 +509,25 @@ impl SourceBuild {
resolved_requirements
}
} else {
+ let (requirements, dependency_sources) = if extra_build_dependencies.is_empty() {
+ (
+ Cow::Borrowed(&pep517_backend.requirements),
+ "`build-system.requires`",
+ )
+ } else {
+ // If there are extra build dependencies, we need to resolve them together with
+ // the backend requirements.
+ let mut requirements = pep517_backend.requirements.clone();
+ requirements.extend(extra_build_dependencies);
+ (
+ Cow::Owned(requirements),
+ "`build-system.requires` and `extra-build-dependencies`",
+ )
+ };
build_context
- .resolve(&pep517_backend.requirements, build_stack)
+ .resolve(&requirements, build_stack)
.await
- .map_err(|err| {
- Error::RequirementsResolve("`build-system.requires`", err.into())
- })?
+ .map_err(|err| Error::RequirementsResolve(dependency_sources, err.into()))?
},
)
}
@@ -604,6 +637,7 @@ impl SourceBuild {
);
}
}
+
default_backend.clone()
};
Ok((backend, pyproject_toml.project))
diff --git a/crates/uv-cli/src/options.rs b/crates/uv-cli/src/options.rs
index c5f94de1b..54c2d80ca 100644
--- a/crates/uv-cli/src/options.rs
+++ b/crates/uv-cli/src/options.rs
@@ -354,6 +354,7 @@ pub fn resolver_options(
}),
no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
no_build_isolation_package: Some(no_build_isolation_package),
+ extra_build_dependencies: None,
exclude_newer: ExcludeNewer::from_args(
exclude_newer,
exclude_newer_package.unwrap_or_default(),
@@ -475,6 +476,7 @@ pub fn resolver_installer_options(
} else {
Some(no_build_isolation_package)
},
+ extra_build_dependencies: None,
exclude_newer,
exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
link_mode,
diff --git a/crates/uv-configuration/src/preview.rs b/crates/uv-configuration/src/preview.rs
index c8d67be5b..fab7dd34e 100644
--- a/crates/uv-configuration/src/preview.rs
+++ b/crates/uv-configuration/src/preview.rs
@@ -14,6 +14,7 @@ bitflags::bitflags! {
const JSON_OUTPUT = 1 << 2;
const PYLOCK = 1 << 3;
const ADD_BOUNDS = 1 << 4;
+ const EXTRA_BUILD_DEPENDENCIES = 1 << 5;
}
}
@@ -28,6 +29,7 @@ impl PreviewFeatures {
Self::JSON_OUTPUT => "json-output",
Self::PYLOCK => "pylock",
Self::ADD_BOUNDS => "add-bounds",
+ Self::EXTRA_BUILD_DEPENDENCIES => "extra-build-dependencies",
_ => panic!("`flag_as_str` can only be used for exactly one feature flag"),
}
}
@@ -70,6 +72,7 @@ impl FromStr for PreviewFeatures {
"json-output" => Self::JSON_OUTPUT,
"pylock" => Self::PYLOCK,
"add-bounds" => Self::ADD_BOUNDS,
+ "extra-build-dependencies" => Self::EXTRA_BUILD_DEPENDENCIES,
_ => {
warn_user_once!("Unknown preview feature: `{part}`");
continue;
@@ -232,6 +235,10 @@ mod tests {
assert_eq!(PreviewFeatures::JSON_OUTPUT.flag_as_str(), "json-output");
assert_eq!(PreviewFeatures::PYLOCK.flag_as_str(), "pylock");
assert_eq!(PreviewFeatures::ADD_BOUNDS.flag_as_str(), "add-bounds");
+ assert_eq!(
+ PreviewFeatures::EXTRA_BUILD_DEPENDENCIES.flag_as_str(),
+ "extra-build-dependencies"
+ );
}
#[test]
diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs
index 5d0adb47b..ddc2d5ed5 100644
--- a/crates/uv-dispatch/src/lib.rs
+++ b/crates/uv-dispatch/src/lib.rs
@@ -22,6 +22,7 @@ use uv_configuration::{
};
use uv_configuration::{BuildOutput, Concurrency};
use uv_distribution::DistributionDatabase;
+use uv_distribution::ExtraBuildRequires;
use uv_distribution_filename::DistFilename;
use uv_distribution_types::{
CachedDist, DependencyMetadata, Identifier, IndexCapabilities, IndexLocations,
@@ -88,6 +89,7 @@ pub struct BuildDispatch<'a> {
shared_state: SharedState,
dependency_metadata: &'a DependencyMetadata,
build_isolation: BuildIsolation<'a>,
+ extra_build_requires: &'a ExtraBuildRequires,
link_mode: uv_install_wheel::LinkMode,
build_options: &'a BuildOptions,
config_settings: &'a ConfigSettings,
@@ -116,6 +118,7 @@ impl<'a> BuildDispatch<'a> {
config_settings: &'a ConfigSettings,
config_settings_package: &'a PackageConfigSettings,
build_isolation: BuildIsolation<'a>,
+ extra_build_requires: &'a ExtraBuildRequires,
link_mode: uv_install_wheel::LinkMode,
build_options: &'a BuildOptions,
hasher: &'a HashStrategy,
@@ -138,6 +141,7 @@ impl<'a> BuildDispatch<'a> {
config_settings,
config_settings_package,
build_isolation,
+ extra_build_requires,
link_mode,
build_options,
hasher,
@@ -219,6 +223,10 @@ impl BuildContext for BuildDispatch<'_> {
&self.workspace_cache
}
+ fn extra_build_dependencies(&self) -> &uv_workspace::pyproject::ExtraBuildDependencies {
+ &self.extra_build_requires.extra_build_dependencies
+ }
+
async fn resolve<'data>(
&'data self,
requirements: &'data [Requirement],
@@ -452,6 +460,7 @@ impl BuildContext for BuildDispatch<'_> {
self.workspace_cache(),
config_settings,
self.build_isolation,
+ &self.extra_build_requires.extra_build_dependencies,
&build_stack,
build_kind,
self.build_extra_env_vars.clone(),
diff --git a/crates/uv-distribution/src/lib.rs b/crates/uv-distribution/src/lib.rs
index 07958f715..6371d58af 100644
--- a/crates/uv-distribution/src/lib.rs
+++ b/crates/uv-distribution/src/lib.rs
@@ -3,8 +3,8 @@ pub use download::LocalWheel;
pub use error::Error;
pub use index::{BuiltWheelIndex, RegistryWheelIndex};
pub use metadata::{
- ArchiveMetadata, BuildRequires, FlatRequiresDist, LoweredRequirement, LoweringError, Metadata,
- MetadataError, RequiresDist, SourcedDependencyGroups,
+ ArchiveMetadata, BuildRequires, ExtraBuildRequires, FlatRequiresDist, LoweredRequirement,
+ LoweringError, Metadata, MetadataError, RequiresDist, SourcedDependencyGroups,
};
pub use reporter::Reporter;
pub use source::prune;
diff --git a/crates/uv-distribution/src/metadata/build_requires.rs b/crates/uv-distribution/src/metadata/build_requires.rs
index 99b528017..9aa14bd9e 100644
--- a/crates/uv-distribution/src/metadata/build_requires.rs
+++ b/crates/uv-distribution/src/metadata/build_requires.rs
@@ -4,7 +4,8 @@ use std::path::Path;
use uv_configuration::SourceStrategy;
use uv_distribution_types::{IndexLocations, Requirement};
use uv_normalize::PackageName;
-use uv_workspace::pyproject::ToolUvSources;
+use uv_pypi_types::VerbatimParsedUrl;
+use uv_workspace::pyproject::{ExtraBuildDependencies, ToolUvSources};
use uv_workspace::{
DiscoveryOptions, MemberDiscovery, ProjectWorkspace, Workspace, WorkspaceCache,
};
@@ -203,3 +204,93 @@ impl BuildRequires {
})
}
}
+
+/// Lowered extra build dependencies with source resolution applied.
+#[derive(Debug, Clone, Default)]
+pub struct ExtraBuildRequires {
+ pub extra_build_dependencies: ExtraBuildDependencies,
+}
+
+impl ExtraBuildRequires {
+ /// Lower extra build dependencies from a workspace, applying source resolution.
+ pub fn from_workspace(
+ extra_build_dependencies: ExtraBuildDependencies,
+ workspace: &Workspace,
+ index_locations: &IndexLocations,
+ source_strategy: SourceStrategy,
+ ) -> Result {
+ match source_strategy {
+ SourceStrategy::Enabled => {
+ // Collect project sources and indexes
+ let project_indexes = workspace
+ .pyproject_toml()
+ .tool
+ .as_ref()
+ .and_then(|tool| tool.uv.as_ref())
+ .and_then(|uv| uv.index.as_deref())
+ .unwrap_or(&[]);
+
+ let empty_sources = BTreeMap::default();
+ let project_sources = workspace
+ .pyproject_toml()
+ .tool
+ .as_ref()
+ .and_then(|tool| tool.uv.as_ref())
+ .and_then(|uv| uv.sources.as_ref())
+ .map(ToolUvSources::inner)
+ .unwrap_or(&empty_sources);
+
+ // Lower each package's extra build dependencies
+ let mut result = ExtraBuildDependencies::default();
+ for (package_name, requirements) in extra_build_dependencies {
+ let lowered: Vec> = requirements
+ .into_iter()
+ .flat_map(|requirement| {
+ let requirement_name = requirement.name.clone();
+ let extra = requirement.marker.top_level_extra_name();
+ let group = None;
+ LoweredRequirement::from_requirement(
+ requirement,
+ None,
+ workspace.install_path(),
+ project_sources,
+ project_indexes,
+ extra.as_deref(),
+ group,
+ index_locations,
+ workspace,
+ None,
+ )
+ .map(
+ move |requirement| match requirement {
+ Ok(requirement) => Ok(requirement.into_inner().into()),
+ Err(err) => Err(MetadataError::LoweringError(
+ requirement_name.clone(),
+ Box::new(err),
+ )),
+ },
+ )
+ })
+ .collect::, _>>()?;
+ result.insert(package_name, lowered);
+ }
+ Ok(Self {
+ extra_build_dependencies: result,
+ })
+ }
+ SourceStrategy::Disabled => {
+ // Without source resolution, just return the dependencies as-is
+ Ok(Self {
+ extra_build_dependencies,
+ })
+ }
+ }
+ }
+
+ /// Create from pre-lowered dependencies (for non-workspace contexts).
+ pub fn from_lowered(extra_build_dependencies: ExtraBuildDependencies) -> Self {
+ Self {
+ extra_build_dependencies,
+ }
+ }
+}
diff --git a/crates/uv-distribution/src/metadata/mod.rs b/crates/uv-distribution/src/metadata/mod.rs
index a56a1c354..3375f9fe2 100644
--- a/crates/uv-distribution/src/metadata/mod.rs
+++ b/crates/uv-distribution/src/metadata/mod.rs
@@ -11,7 +11,7 @@ use uv_pypi_types::{HashDigests, ResolutionMetadata};
use uv_workspace::dependency_groups::DependencyGroupError;
use uv_workspace::{WorkspaceCache, WorkspaceError};
-pub use crate::metadata::build_requires::BuildRequires;
+pub use crate::metadata::build_requires::{BuildRequires, ExtraBuildRequires};
pub use crate::metadata::dependency_groups::SourcedDependencyGroups;
pub use crate::metadata::lowering::LoweredRequirement;
pub use crate::metadata::lowering::LoweringError;
diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs
index 66b6122e0..f269c1b87 100644
--- a/crates/uv-distribution/src/source/mod.rs
+++ b/crates/uv-distribution/src/source/mod.rs
@@ -404,6 +404,20 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
}
}
+ /// Determine the extra build dependencies for the given package name.
+ fn extra_build_dependencies_for(
+ &self,
+ name: Option<&PackageName>,
+ ) -> &[uv_pep508::Requirement] {
+ name.and_then(|name| {
+ self.build_context
+ .extra_build_dependencies()
+ .get(name)
+ .map(|v| v.as_slice())
+ })
+ .unwrap_or(&[])
+ }
+
/// Build a source distribution from a remote URL.
async fn url<'data>(
&self,
@@ -438,12 +452,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
let cache_shard = cache_shard.shard(revision.id());
let source_dist_entry = cache_shard.entry(SOURCE);
- // If there are build settings, we need to scope to a cache shard.
+ // If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
- let cache_shard = if config_settings.is_empty() {
+ let extra_build_deps = self.extra_build_dependencies_for(source.name());
+ let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
- cache_shard.shard(cache_digest(&&config_settings))
+ cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// If the cache contains a compatible wheel, return it.
@@ -614,12 +629,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
}
}
- // If there are build settings, we need to scope to a cache shard.
+ // If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
- let cache_shard = if config_settings.is_empty() {
+ let extra_build_deps = self.extra_build_dependencies_for(source.name());
+ let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
- cache_shard.shard(cache_digest(&config_settings))
+ cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// Otherwise, we either need to build the metadata.
@@ -827,12 +843,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
let cache_shard = cache_shard.shard(revision.id());
let source_entry = cache_shard.entry(SOURCE);
- // If there are build settings, we need to scope to a cache shard.
+ // If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
- let cache_shard = if config_settings.is_empty() {
+ let extra_build_deps = self.extra_build_dependencies_for(source.name());
+ let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
- cache_shard.shard(cache_digest(&config_settings))
+ cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// If the cache contains a compatible wheel, return it.
@@ -989,12 +1006,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
});
}
- // If there are build settings, we need to scope to a cache shard.
+ // If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
- let cache_shard = if config_settings.is_empty() {
+ let extra_build_deps = self.extra_build_dependencies_for(source.name());
+ let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
- cache_shard.shard(cache_digest(&config_settings))
+ cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// Otherwise, we need to build a wheel.
@@ -1131,12 +1149,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
// freshness, since entries have to be fresher than the revision itself.
let cache_shard = cache_shard.shard(revision.id());
- // If there are build settings, we need to scope to a cache shard.
+ // If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
- let cache_shard = if config_settings.is_empty() {
+ let extra_build_deps = self.extra_build_dependencies_for(source.name());
+ let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
- cache_shard.shard(cache_digest(&config_settings))
+ cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// If the cache contains a compatible wheel, return it.
@@ -1319,12 +1338,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
));
}
- // If there are build settings, we need to scope to a cache shard.
+ // If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
- let cache_shard = if config_settings.is_empty() {
+ let extra_build_deps = self.extra_build_dependencies_for(source.name());
+ let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
- cache_shard.shard(cache_digest(&config_settings))
+ cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// Otherwise, we need to build a wheel.
@@ -1524,12 +1544,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
// Acquire the advisory lock.
let _lock = cache_shard.lock().await.map_err(Error::CacheWrite)?;
- // If there are build settings, we need to scope to a cache shard.
+ // If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
- let cache_shard = if config_settings.is_empty() {
+ let extra_build_deps = self.extra_build_dependencies_for(source.name());
+ let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
- cache_shard.shard(cache_digest(&config_settings))
+ cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// If the cache contains a compatible wheel, return it.
@@ -1827,12 +1848,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
));
}
- // If there are build settings, we need to scope to a cache shard.
+ // If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
- let cache_shard = if config_settings.is_empty() {
+ let extra_build_deps = self.extra_build_dependencies_for(source.name());
+ let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
- cache_shard.shard(cache_digest(&config_settings))
+ cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// Otherwise, we need to build a wheel.
diff --git a/crates/uv-pep440/Cargo.toml b/crates/uv-pep440/Cargo.toml
index 128db08ef..278077a2b 100644
--- a/crates/uv-pep440/Cargo.toml
+++ b/crates/uv-pep440/Cargo.toml
@@ -20,6 +20,7 @@ serde = { workspace = true, features = ["derive"] }
tracing = { workspace = true, optional = true }
unicode-width = { workspace = true }
unscanny = { workspace = true }
+uv-cache-key = { workspace = true }
# Adds conversions from [`VersionSpecifiers`] to [`version_ranges::Ranges`]
version-ranges = { workspace = true, optional = true }
diff --git a/crates/uv-pep440/src/version.rs b/crates/uv-pep440/src/version.rs
index 223701692..31b5a8e35 100644
--- a/crates/uv-pep440/src/version.rs
+++ b/crates/uv-pep440/src/version.rs
@@ -10,6 +10,7 @@ use std::{
str::FromStr,
sync::Arc,
};
+use uv_cache_key::{CacheKey, CacheKeyHasher};
/// One of `~=` `==` `!=` `<=` `>=` `<` `>` `===`
#[derive(Eq, Ord, PartialEq, PartialOrd, Debug, Hash, Clone, Copy)]
@@ -114,6 +115,24 @@ impl Operator {
pub fn is_star(self) -> bool {
matches!(self, Self::EqualStar | Self::NotEqualStar)
}
+
+ /// Returns the string representation of this operator.
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Equal => "==",
+ // Beware, this doesn't print the star
+ Self::EqualStar => "==",
+ #[allow(deprecated)]
+ Self::ExactEqual => "===",
+ Self::NotEqual => "!=",
+ Self::NotEqualStar => "!=",
+ Self::TildeEqual => "~=",
+ Self::LessThan => "<",
+ Self::LessThanEqual => "<=",
+ Self::GreaterThan => ">",
+ Self::GreaterThanEqual => ">=",
+ }
+ }
}
impl FromStr for Operator {
@@ -150,21 +169,7 @@ impl FromStr for Operator {
impl std::fmt::Display for Operator {
/// Note the `EqualStar` is also `==`.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let operator = match self {
- Self::Equal => "==",
- // Beware, this doesn't print the star
- Self::EqualStar => "==",
- #[allow(deprecated)]
- Self::ExactEqual => "===",
- Self::NotEqual => "!=",
- Self::NotEqualStar => "!=",
- Self::TildeEqual => "~=",
- Self::LessThan => "<",
- Self::LessThanEqual => "<=",
- Self::GreaterThan => ">",
- Self::GreaterThanEqual => ">=",
- };
-
+ let operator = self.as_str();
write!(f, "{operator}")
}
}
@@ -930,6 +935,46 @@ impl Hash for Version {
}
}
+impl CacheKey for Version {
+ fn cache_key(&self, state: &mut CacheKeyHasher) {
+ self.epoch().cache_key(state);
+
+ let release = self.release();
+ release.len().cache_key(state);
+ for segment in release.iter() {
+ segment.cache_key(state);
+ }
+
+ if let Some(pre) = self.pre() {
+ 1u8.cache_key(state);
+ match pre.kind {
+ PrereleaseKind::Alpha => 0u8.cache_key(state),
+ PrereleaseKind::Beta => 1u8.cache_key(state),
+ PrereleaseKind::Rc => 2u8.cache_key(state),
+ }
+ pre.number.cache_key(state);
+ } else {
+ 0u8.cache_key(state);
+ }
+
+ if let Some(post) = self.post() {
+ 1u8.cache_key(state);
+ post.cache_key(state);
+ } else {
+ 0u8.cache_key(state);
+ }
+
+ if let Some(dev) = self.dev() {
+ 1u8.cache_key(state);
+ dev.cache_key(state);
+ } else {
+ 0u8.cache_key(state);
+ }
+
+ self.local().cache_key(state);
+ }
+}
+
impl PartialOrd for Version {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option {
@@ -1711,6 +1756,23 @@ impl std::fmt::Display for LocalVersionSlice<'_> {
}
}
+impl CacheKey for LocalVersionSlice<'_> {
+ fn cache_key(&self, state: &mut CacheKeyHasher) {
+ match self {
+ LocalVersionSlice::Segments(segments) => {
+ 0u8.cache_key(state);
+ segments.len().cache_key(state);
+ for segment in *segments {
+ segment.cache_key(state);
+ }
+ }
+ LocalVersionSlice::Max => {
+ 1u8.cache_key(state);
+ }
+ }
+ }
+}
+
impl PartialOrd for LocalVersionSlice<'_> {
fn partial_cmp(&self, other: &Self) -> Option {
Some(self.cmp(other))
@@ -1777,6 +1839,21 @@ impl std::fmt::Display for LocalSegment {
}
}
+impl CacheKey for LocalSegment {
+ fn cache_key(&self, state: &mut CacheKeyHasher) {
+ match self {
+ Self::String(string) => {
+ 0u8.cache_key(state);
+ string.cache_key(state);
+ }
+ Self::Number(number) => {
+ 1u8.cache_key(state);
+ number.cache_key(state);
+ }
+ }
+ }
+}
+
impl PartialOrd for LocalSegment {
fn partial_cmp(&self, other: &Self) -> Option {
Some(self.cmp(other))
diff --git a/crates/uv-pep440/src/version_specifier.rs b/crates/uv-pep440/src/version_specifier.rs
index e111c5118..13e2687cf 100644
--- a/crates/uv-pep440/src/version_specifier.rs
+++ b/crates/uv-pep440/src/version_specifier.rs
@@ -48,6 +48,11 @@ impl VersionSpecifiers {
Self(Box::new([]))
}
+ /// The number of specifiers.
+ pub fn len(&self) -> usize {
+ self.0.len()
+ }
+
/// Whether all specifiers match the given version.
pub fn contains(&self, version: &Version) -> bool {
self.iter().all(|specifier| specifier.contains(version))
diff --git a/crates/uv-pep508/Cargo.toml b/crates/uv-pep508/Cargo.toml
index cb23cfc04..c4830043b 100644
--- a/crates/uv-pep508/Cargo.toml
+++ b/crates/uv-pep508/Cargo.toml
@@ -19,6 +19,7 @@ doctest = false
workspace = true
[dependencies]
+uv-cache-key = { workspace = true }
uv-fs = { workspace = true }
uv-normalize = { workspace = true }
uv-pep440 = { workspace = true }
diff --git a/crates/uv-pep508/src/lib.rs b/crates/uv-pep508/src/lib.rs
index 10e4142e7..dd516f570 100644
--- a/crates/uv-pep508/src/lib.rs
+++ b/crates/uv-pep508/src/lib.rs
@@ -26,6 +26,7 @@ use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use thiserror::Error;
use url::Url;
+use uv_cache_key::{CacheKey, CacheKeyHasher};
use cursor::Cursor;
pub use marker::{
@@ -251,6 +252,52 @@ impl Serialize for Requirement {
}
}
+impl CacheKey for Requirement
+where
+ T: Display,
+{
+ fn cache_key(&self, state: &mut CacheKeyHasher) {
+ self.name.as_str().cache_key(state);
+
+ self.extras.len().cache_key(state);
+ for extra in &self.extras {
+ extra.as_str().cache_key(state);
+ }
+
+ // TODO(zanieb): We inline cache key handling for the child types here, but we could
+ // move the implementations to the children. The intent here was to limit the scope of
+ // types exposing the `CacheKey` trait for now.
+ if let Some(version_or_url) = &self.version_or_url {
+ 1u8.cache_key(state);
+ match version_or_url {
+ VersionOrUrl::VersionSpecifier(spec) => {
+ 0u8.cache_key(state);
+ spec.len().cache_key(state);
+ for specifier in spec.iter() {
+ specifier.operator().as_str().cache_key(state);
+ specifier.version().cache_key(state);
+ }
+ }
+ VersionOrUrl::Url(url) => {
+ 1u8.cache_key(state);
+ url.to_string().cache_key(state);
+ }
+ }
+ } else {
+ 0u8.cache_key(state);
+ }
+
+ if let Some(marker) = self.marker.contents() {
+ 1u8.cache_key(state);
+ marker.to_string().cache_key(state);
+ } else {
+ 0u8.cache_key(state);
+ }
+
+ // `origin` is intentionally omitted
+ }
+}
+
impl Requirement {
/// Returns whether the markers apply for the given environment
pub fn evaluate_markers(&self, env: &MarkerEnvironment, extras: &[ExtraName]) -> bool {
diff --git a/crates/uv-scripts/Cargo.toml b/crates/uv-scripts/Cargo.toml
index 124eb1fea..4cff3f5bc 100644
--- a/crates/uv-scripts/Cargo.toml
+++ b/crates/uv-scripts/Cargo.toml
@@ -11,6 +11,8 @@ doctest = false
workspace = true
[dependencies]
+uv-configuration = { workspace = true }
+uv-distribution-types = { workspace = true }
uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true }
uv-pypi-types = { workspace = true }
diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs
index b80cdc219..474b1f91b 100644
--- a/crates/uv-scripts/src/lib.rs
+++ b/crates/uv-scripts/src/lib.rs
@@ -9,6 +9,7 @@ use serde::Deserialize;
use thiserror::Error;
use url::Url;
+use uv_configuration::SourceStrategy;
use uv_pep440::VersionSpecifiers;
use uv_pep508::PackageName;
use uv_pypi_types::VerbatimParsedUrl;
@@ -96,6 +97,46 @@ impl Pep723ItemRef<'_> {
Self::Remote(..) => None,
}
}
+
+ /// Determine the working directory for the script.
+ pub fn directory(&self) -> Result {
+ match self {
+ Self::Script(script) => Ok(std::path::absolute(&script.path)?
+ .parent()
+ .expect("script path has no parent")
+ .to_owned()),
+ Self::Stdin(..) | Self::Remote(..) => std::env::current_dir(),
+ }
+ }
+
+ /// Collect any `tool.uv.index` from the script.
+ pub fn indexes(&self, source_strategy: SourceStrategy) -> &[uv_distribution_types::Index] {
+ match source_strategy {
+ SourceStrategy::Enabled => self
+ .metadata()
+ .tool
+ .as_ref()
+ .and_then(|tool| tool.uv.as_ref())
+ .and_then(|uv| uv.top_level.index.as_deref())
+ .unwrap_or(&[]),
+ SourceStrategy::Disabled => &[],
+ }
+ }
+
+ /// Collect any `tool.uv.sources` from the script.
+ pub fn sources(&self, source_strategy: SourceStrategy) -> &BTreeMap {
+ static EMPTY: BTreeMap = BTreeMap::new();
+ match source_strategy {
+ SourceStrategy::Enabled => self
+ .metadata()
+ .tool
+ .as_ref()
+ .and_then(|tool| tool.uv.as_ref())
+ .and_then(|uv| uv.sources.as_ref())
+ .unwrap_or(&EMPTY),
+ SourceStrategy::Disabled => &EMPTY,
+ }
+ }
}
impl<'item> From<&'item Pep723Item> for Pep723ItemRef<'item> {
@@ -108,6 +149,12 @@ impl<'item> From<&'item Pep723Item> for Pep723ItemRef<'item> {
}
}
+impl<'item> From<&'item Pep723Script> for Pep723ItemRef<'item> {
+ fn from(script: &'item Pep723Script) -> Self {
+ Self::Script(script)
+ }
+}
+
/// A PEP 723 script, including its [`Pep723Metadata`].
#[derive(Debug, Clone)]
pub struct Pep723Script {
@@ -381,6 +428,8 @@ pub struct ToolUv {
pub override_dependencies: Option>>,
pub constraint_dependencies: Option>>,
pub build_constraint_dependencies: Option>>,
+ pub extra_build_dependencies:
+ Option>>>,
pub sources: Option>,
}
diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs
index a2d78e7b1..084846948 100644
--- a/crates/uv-settings/src/combine.rs
+++ b/crates/uv-settings/src/combine.rs
@@ -1,5 +1,5 @@
-use std::num::NonZeroUsize;
use std::path::PathBuf;
+use std::{collections::BTreeMap, num::NonZeroUsize};
use url::Url;
@@ -17,6 +17,7 @@ use uv_resolver::{
PrereleaseMode, ResolutionMode,
};
use uv_torch::TorchMode;
+use uv_workspace::pyproject::ExtraBuildDependencies;
use uv_workspace::pyproject_mut::AddBoundsKind;
use crate::{FilesystemOptions, Options, PipOptions};
@@ -124,6 +125,21 @@ impl Combine for Option> {
}
}
+impl Combine for Option>> {
+ /// Combine two maps of vecs by combining their vecs
+ fn combine(self, other: Option>>) -> Option>> {
+ match (self, other) {
+ (Some(mut a), Some(b)) => {
+ for (key, value) in b {
+ a.entry(key).or_default().extend(value);
+ }
+ Some(a)
+ }
+ (a, b) => a.or(b),
+ }
+ }
+}
+
impl Combine for Option {
/// Combine two [`ExcludeNewerPackage`] instances by merging them, with the values in `self` taking precedence.
fn combine(self, other: Option) -> Option {
@@ -192,3 +208,30 @@ impl Combine for ExcludeNewer {
self
}
}
+
+impl Combine for ExtraBuildDependencies {
+ fn combine(mut self, other: Self) -> Self {
+ for (key, value) in other {
+ match self.entry(key) {
+ std::collections::btree_map::Entry::Occupied(mut entry) => {
+ // Combine the vecs, with self taking precedence
+ let existing = entry.get_mut();
+ existing.extend(value);
+ }
+ std::collections::btree_map::Entry::Vacant(entry) => {
+ entry.insert(value);
+ }
+ }
+ }
+ self
+ }
+}
+
+impl Combine for Option {
+ fn combine(self, other: Option) -> Option {
+ match (self, other) {
+ (Some(a), Some(b)) => Some(a.combine(b)),
+ (a, b) => a.or(b),
+ }
+ }
+}
diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs
index f8385d006..4ca8a5af8 100644
--- a/crates/uv-settings/src/lib.rs
+++ b/crates/uv-settings/src/lib.rs
@@ -317,6 +317,7 @@ fn warn_uv_toml_masked_fields(options: &Options) {
config_settings_package,
no_build_isolation,
no_build_isolation_package,
+ extra_build_dependencies,
exclude_newer,
exclude_newer_package,
link_mode,
@@ -445,6 +446,9 @@ fn warn_uv_toml_masked_fields(options: &Options) {
if no_build_isolation_package.is_some() {
masked_fields.push("no-build-isolation-package");
}
+ if extra_build_dependencies.is_some() {
+ masked_fields.push("extra-build-dependencies");
+ }
if exclude_newer.is_some() {
masked_fields.push("exclude-newer");
}
diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs
index 6062d5d0e..8e50d6999 100644
--- a/crates/uv-settings/src/settings.rs
+++ b/crates/uv-settings/src/settings.rs
@@ -24,7 +24,7 @@ use uv_resolver::{
};
use uv_static::EnvVars;
use uv_torch::TorchMode;
-use uv_workspace::pyproject_mut::AddBoundsKind;
+use uv_workspace::{pyproject::ExtraBuildDependencies, pyproject_mut::AddBoundsKind};
/// A `pyproject.toml` with an (optional) `[tool.uv]` section.
#[allow(dead_code)]
@@ -376,6 +376,7 @@ pub struct ResolverOptions {
pub no_binary_package: Option>,
pub no_build_isolation: Option,
pub no_build_isolation_package: Option>,
+ pub extra_build_dependencies: Option,
pub no_sources: Option,
}
@@ -628,6 +629,20 @@ pub struct ResolverInstallerOptions {
"#
)]
pub no_build_isolation_package: Option>,
+ /// Additional build dependencies for packages.
+ ///
+ /// This allows extending the PEP 517 build environment for the project's dependencies with
+ /// additional packages. This is useful for packages that assume the presence of packages like
+ /// `pip`, and do not declare them as build dependencies.
+ #[option(
+ default = "[]",
+ value_type = "dict",
+ example = r#"
+ [extra-build-dependencies]
+ pytest = ["setuptools"]
+ "#
+ )]
+ pub extra_build_dependencies: Option,
/// Limit candidate packages to those that were uploaded prior to a given point in time.
///
/// Accepts a superset of [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) (e.g.,
@@ -1135,6 +1150,20 @@ pub struct PipOptions {
"#
)]
pub no_build_isolation_package: Option>,
+ /// Additional build dependencies for packages.
+ ///
+ /// This allows extending the PEP 517 build environment for the project's dependencies with
+ /// additional packages. This is useful for packages that assume the presence of packages like
+ /// `pip`, and do not declare them as build dependencies.
+ #[option(
+ default = "[]",
+ value_type = "dict",
+ example = r#"
+ [extra-build-dependencies]
+ pytest = ["setuptools"]
+ "#
+ )]
+ pub extra_build_dependencies: Option,
/// Validate the Python environment, to detect packages with missing dependencies and other
/// issues.
#[option(
@@ -1719,6 +1748,7 @@ impl From for ResolverOptions {
no_binary_package: value.no_binary_package,
no_build_isolation: value.no_build_isolation,
no_build_isolation_package: value.no_build_isolation_package,
+ extra_build_dependencies: value.extra_build_dependencies,
no_sources: value.no_sources,
}
}
@@ -1784,6 +1814,7 @@ pub struct ToolOptions {
pub config_settings_package: Option,
pub no_build_isolation: Option,
pub no_build_isolation_package: Option>,
+ pub extra_build_dependencies: Option,
pub exclude_newer: Option,
pub exclude_newer_package: Option,
pub link_mode: Option,
@@ -1813,6 +1844,7 @@ impl From for ToolOptions {
config_settings_package: value.config_settings_package,
no_build_isolation: value.no_build_isolation,
no_build_isolation_package: value.no_build_isolation_package,
+ extra_build_dependencies: value.extra_build_dependencies,
exclude_newer: value.exclude_newer,
exclude_newer_package: value.exclude_newer_package,
link_mode: value.link_mode,
@@ -1844,6 +1876,7 @@ impl From for ResolverInstallerOptions {
config_settings_package: value.config_settings_package,
no_build_isolation: value.no_build_isolation,
no_build_isolation_package: value.no_build_isolation_package,
+ extra_build_dependencies: value.extra_build_dependencies,
exclude_newer: value.exclude_newer,
exclude_newer_package: value.exclude_newer_package,
link_mode: value.link_mode,
@@ -1898,6 +1931,7 @@ pub struct OptionsWire {
config_settings_package: Option,
no_build_isolation: Option,
no_build_isolation_package: Option>,
+ extra_build_dependencies: Option,
exclude_newer: Option,
exclude_newer_package: Option,
link_mode: Option,
@@ -2017,6 +2051,7 @@ impl From for Options {
sources,
default_groups,
dependency_groups,
+ extra_build_dependencies,
dev_dependencies,
managed,
package,
@@ -2057,6 +2092,7 @@ impl From for Options {
config_settings_package,
no_build_isolation,
no_build_isolation_package,
+ extra_build_dependencies,
exclude_newer,
exclude_newer_package,
link_mode,
diff --git a/crates/uv-types/src/traits.rs b/crates/uv-types/src/traits.rs
index e3f4ee012..875742da9 100644
--- a/crates/uv-types/src/traits.rs
+++ b/crates/uv-types/src/traits.rs
@@ -101,6 +101,9 @@ pub trait BuildContext {
/// Workspace discovery caching.
fn workspace_cache(&self) -> &WorkspaceCache;
+ /// Get the extra build dependencies.
+ fn extra_build_dependencies(&self) -> &uv_workspace::pyproject::ExtraBuildDependencies;
+
/// Resolve the given requirements into a ready-to-install set of package versions.
fn resolve<'a>(
&'a self,
diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs
index b02dadc5d..5b933a130 100644
--- a/crates/uv-workspace/src/pyproject.rs
+++ b/crates/uv-workspace/src/pyproject.rs
@@ -50,6 +50,55 @@ pub enum PyprojectTomlError {
MissingVersion,
}
+/// Helper function to deserialize a map while ensuring all keys are unique.
+fn deserialize_unique_map<'de, D, K, V, F>(
+ deserializer: D,
+ error_msg: F,
+) -> Result, D::Error>
+where
+ D: Deserializer<'de>,
+ K: Deserialize<'de> + Ord + std::fmt::Display,
+ V: Deserialize<'de>,
+ F: FnOnce(&K) -> String,
+{
+ struct Visitor(F, std::marker::PhantomData<(K, V)>);
+
+ impl<'de, K, V, F> serde::de::Visitor<'de> for Visitor
+ where
+ K: Deserialize<'de> + Ord + std::fmt::Display,
+ V: Deserialize<'de>,
+ F: FnOnce(&K) -> String,
+ {
+ type Value = BTreeMap;
+
+ fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+ formatter.write_str("a map with unique keys")
+ }
+
+ fn visit_map(self, mut access: M) -> Result
+ where
+ M: serde::de::MapAccess<'de>,
+ {
+ use std::collections::btree_map::Entry;
+
+ let mut map = BTreeMap::new();
+ while let Some((key, value)) = access.next_entry::()? {
+ match map.entry(key) {
+ Entry::Occupied(entry) => {
+ return Err(serde::de::Error::custom((self.0)(entry.key())));
+ }
+ Entry::Vacant(entry) => {
+ entry.insert(value);
+ }
+ }
+ }
+ Ok(map)
+ }
+ }
+
+ deserializer.deserialize_map(Visitor(error_msg, std::marker::PhantomData))
+}
+
/// A `pyproject.toml` as specified in PEP 517.
#[derive(Deserialize, Debug, Clone)]
#[cfg_attr(test, derive(Serialize))]
@@ -378,6 +427,21 @@ pub struct ToolUv {
)]
pub dependency_groups: Option,
+ /// Additional build dependencies for packages.
+ ///
+ /// This allows extending the PEP 517 build environment for the project's dependencies with
+ /// additional packages. This is useful for packages that assume the presence of packages, like,
+ /// `pip`, and do not declare them as build dependencies.
+ #[option(
+ default = "[]",
+ value_type = "dict",
+ example = r#"
+ [tool.uv.extra-build-dependencies]
+ pytest = ["pip"]
+ "#
+ )]
+ pub extra_build_dependencies: Option,
+
/// The project's development dependencies.
///
/// Development dependencies will be installed by default in `uv run` and `uv sync`, but will
@@ -643,38 +707,10 @@ impl<'de> serde::de::Deserialize<'de> for ToolUvSources {
where
D: Deserializer<'de>,
{
- struct SourcesVisitor;
-
- impl<'de> serde::de::Visitor<'de> for SourcesVisitor {
- type Value = ToolUvSources;
-
- fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
- formatter.write_str("a map with unique keys")
- }
-
- fn visit_map(self, mut access: M) -> Result
- where
- M: serde::de::MapAccess<'de>,
- {
- let mut sources = BTreeMap::new();
- while let Some((key, value)) = access.next_entry::()? {
- match sources.entry(key) {
- std::collections::btree_map::Entry::Occupied(entry) => {
- return Err(serde::de::Error::custom(format!(
- "duplicate sources for package `{}`",
- entry.key()
- )));
- }
- std::collections::btree_map::Entry::Vacant(entry) => {
- entry.insert(value);
- }
- }
- }
- Ok(ToolUvSources(sources))
- }
- }
-
- deserializer.deserialize_map(SourcesVisitor)
+ deserialize_unique_map(deserializer, |key: &PackageName| {
+ format!("duplicate sources for package `{key}`")
+ })
+ .map(ToolUvSources)
}
}
@@ -702,40 +738,10 @@ impl<'de> serde::de::Deserialize<'de> for ToolUvDependencyGroups {
where
D: Deserializer<'de>,
{
- struct SourcesVisitor;
-
- impl<'de> serde::de::Visitor<'de> for SourcesVisitor {
- type Value = ToolUvDependencyGroups;
-
- fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
- formatter.write_str("a map with unique keys")
- }
-
- fn visit_map(self, mut access: M) -> Result
- where
- M: serde::de::MapAccess<'de>,
- {
- let mut groups = BTreeMap::new();
- while let Some((key, value)) =
- access.next_entry::()?
- {
- match groups.entry(key) {
- std::collections::btree_map::Entry::Occupied(entry) => {
- return Err(serde::de::Error::custom(format!(
- "duplicate settings for dependency group `{}`",
- entry.key()
- )));
- }
- std::collections::btree_map::Entry::Vacant(entry) => {
- entry.insert(value);
- }
- }
- }
- Ok(ToolUvDependencyGroups(groups))
- }
- }
-
- deserializer.deserialize_map(SourcesVisitor)
+ deserialize_unique_map(deserializer, |key: &GroupName| {
+ format!("duplicate settings for dependency group `{key}`")
+ })
+ .map(ToolUvDependencyGroups)
}
}
@@ -749,6 +755,51 @@ pub struct DependencyGroupSettings {
pub requires_python: Option,
}
+#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize)]
+#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+pub struct ExtraBuildDependencies(
+ BTreeMap>>,
+);
+
+impl std::ops::Deref for ExtraBuildDependencies {
+ type Target = BTreeMap>>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl std::ops::DerefMut for ExtraBuildDependencies {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl IntoIterator for ExtraBuildDependencies {
+ type Item = (PackageName, Vec>);
+ type IntoIter = std::collections::btree_map::IntoIter<
+ PackageName,
+ Vec>,
+ >;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.0.into_iter()
+ }
+}
+
+/// Ensure that all keys in the TOML table are unique.
+impl<'de> serde::de::Deserialize<'de> for ExtraBuildDependencies {
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: Deserializer<'de>,
+ {
+ deserialize_unique_map(deserializer, |key: &PackageName| {
+ format!("duplicate extra-build-dependencies for `{key}`")
+ })
+ .map(ExtraBuildDependencies)
+ }
+}
+
#[derive(Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(Serialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs
index 09f2b692a..53342865e 100644
--- a/crates/uv-workspace/src/workspace.rs
+++ b/crates/uv-workspace/src/workspace.rs
@@ -1970,6 +1970,7 @@ mod tests {
"package": null,
"default-groups": null,
"dependency-groups": null,
+ "extra-build-dependencies": null,
"dev-dependencies": null,
"override-dependencies": null,
"constraint-dependencies": null,
@@ -2070,6 +2071,7 @@ mod tests {
"package": null,
"default-groups": null,
"dependency-groups": null,
+ "extra-build-dependencies": null,
"dev-dependencies": null,
"override-dependencies": null,
"constraint-dependencies": null,
@@ -2283,6 +2285,7 @@ mod tests {
"package": null,
"default-groups": null,
"dependency-groups": null,
+ "extra-build-dependencies": null,
"dev-dependencies": null,
"override-dependencies": null,
"constraint-dependencies": null,
@@ -2392,6 +2395,7 @@ mod tests {
"package": null,
"default-groups": null,
"dependency-groups": null,
+ "extra-build-dependencies": null,
"dev-dependencies": null,
"override-dependencies": null,
"constraint-dependencies": null,
@@ -2514,6 +2518,7 @@ mod tests {
"package": null,
"default-groups": null,
"dependency-groups": null,
+ "extra-build-dependencies": null,
"dev-dependencies": null,
"override-dependencies": null,
"constraint-dependencies": null,
@@ -2610,6 +2615,7 @@ mod tests {
"package": null,
"default-groups": null,
"dependency-groups": null,
+ "extra-build-dependencies": null,
"dev-dependencies": null,
"override-dependencies": null,
"constraint-dependencies": null,
diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs
index 78e27e975..7ec57e1d6 100644
--- a/crates/uv/src/commands/build_frontend.rs
+++ b/crates/uv/src/commands/build_frontend.rs
@@ -38,6 +38,7 @@ use uv_requirements::RequirementsSource;
use uv_resolver::{ExcludeNewer, FlatIndex};
use uv_settings::PythonInstallMirrors;
use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy};
+use uv_workspace::pyproject::ExtraBuildDependencies;
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache, WorkspaceError};
use crate::commands::ExitStatus;
@@ -200,6 +201,7 @@ async fn build_impl(
config_settings_package,
no_build_isolation,
no_build_isolation_package,
+ extra_build_dependencies,
exclude_newer,
link_mode,
upgrade: _,
@@ -346,6 +348,7 @@ async fn build_impl(
build_constraints,
*no_build_isolation,
no_build_isolation_package,
+ extra_build_dependencies,
*index_strategy,
*keyring_provider,
exclude_newer.clone(),
@@ -424,6 +427,7 @@ async fn build_package(
build_constraints: &[RequirementsSource],
no_build_isolation: bool,
no_build_isolation_package: &[PackageName],
+ extra_build_dependencies: &ExtraBuildDependencies,
index_strategy: IndexStrategy,
keyring_provider: KeyringProviderType,
exclude_newer: ExcludeNewer,
@@ -560,6 +564,8 @@ async fn build_package(
let workspace_cache = WorkspaceCache::default();
// Create a build dispatch.
+ let extra_build_requires =
+ uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let build_dispatch = BuildDispatch::new(
&client,
cache,
@@ -573,6 +579,7 @@ async fn build_package(
config_setting,
config_settings_package,
build_isolation,
+ &extra_build_requires,
link_mode,
build_options,
&hasher,
diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs
index dba91e106..26ae9f11c 100644
--- a/crates/uv/src/commands/pip/compile.rs
+++ b/crates/uv/src/commands/pip/compile.rs
@@ -14,8 +14,8 @@ use uv_cache::Cache;
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
BuildOptions, Concurrency, ConfigSettings, Constraints, ExportFormat, ExtrasSpecification,
- IndexStrategy, NoBinary, NoBuild, PackageConfigSettings, Preview, Reinstall, SourceStrategy,
- Upgrade,
+ IndexStrategy, NoBinary, NoBuild, PackageConfigSettings, Preview, PreviewFeatures, Reinstall,
+ SourceStrategy, Upgrade,
};
use uv_configuration::{KeyringProviderType, TargetTriple};
use uv_dispatch::{BuildDispatch, SharedState};
@@ -44,8 +44,9 @@ use uv_resolver::{
};
use uv_torch::{TorchMode, TorchStrategy};
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
-use uv_warnings::warn_user;
+use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::WorkspaceCache;
+use uv_workspace::pyproject::ExtraBuildDependencies;
use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::pip::{operations, resolution_environment};
@@ -95,6 +96,7 @@ pub(crate) async fn pip_compile(
config_settings_package: PackageConfigSettings,
no_build_isolation: bool,
no_build_isolation_package: Vec,
+ extra_build_dependencies: &ExtraBuildDependencies,
build_options: BuildOptions,
mut python_version: Option,
python_platform: Option,
@@ -112,6 +114,15 @@ pub(crate) async fn pip_compile(
printer: Printer,
preview: Preview,
) -> Result {
+ if !preview.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES)
+ && !extra_build_dependencies.is_empty()
+ {
+ warn_user_once!(
+ "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
+ PreviewFeatures::EXTRA_BUILD_DEPENDENCIES
+ );
+ }
+
// If the user provides a `pyproject.toml` or other TOML file as the output file, raise an
// error.
if output_file
@@ -469,6 +480,8 @@ pub(crate) async fn pip_compile(
.map(|constraint| constraint.requirement.clone()),
);
+ let extra_build_requires =
+ uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let build_dispatch = BuildDispatch::new(
&client,
&cache,
@@ -482,6 +495,7 @@ pub(crate) async fn pip_compile(
&config_settings,
&config_settings_package,
build_isolation,
+ &extra_build_requires,
link_mode,
&build_options,
&build_hashes,
diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs
index 39092d03f..8e0276bc9 100644
--- a/crates/uv/src/commands/pip/install.rs
+++ b/crates/uv/src/commands/pip/install.rs
@@ -36,8 +36,9 @@ use uv_resolver::{
};
use uv_torch::{TorchMode, TorchStrategy};
use uv_types::{BuildIsolation, HashStrategy};
-use uv_warnings::warn_user;
+use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::WorkspaceCache;
+use uv_workspace::pyproject::ExtraBuildDependencies;
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger};
use crate::commands::pip::operations::Modifications;
@@ -78,6 +79,7 @@ pub(crate) async fn pip_install(
config_settings_package: &PackageConfigSettings,
no_build_isolation: bool,
no_build_isolation_package: Vec,
+ extra_build_dependencies: &ExtraBuildDependencies,
build_options: BuildOptions,
modifications: Modifications,
python_version: Option,
@@ -99,6 +101,15 @@ pub(crate) async fn pip_install(
) -> anyhow::Result {
let start = std::time::Instant::now();
+ if !preview.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES)
+ && !extra_build_dependencies.is_empty()
+ {
+ warn_user_once!(
+ "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
+ PreviewFeatures::EXTRA_BUILD_DEPENDENCIES
+ );
+ }
+
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
@@ -413,6 +424,8 @@ pub(crate) async fn pip_install(
let state = SharedState::default();
// Create a build dispatch.
+ let extra_build_requires =
+ uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let build_dispatch = BuildDispatch::new(
&client,
&cache,
@@ -426,6 +439,7 @@ pub(crate) async fn pip_install(
config_settings,
config_settings_package,
build_isolation,
+ &extra_build_requires,
link_mode,
&build_options,
&build_hasher,
diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs
index 9e8943d64..90bffc2aa 100644
--- a/crates/uv/src/commands/pip/sync.rs
+++ b/crates/uv/src/commands/pip/sync.rs
@@ -32,8 +32,9 @@ use uv_resolver::{
};
use uv_torch::{TorchMode, TorchStrategy};
use uv_types::{BuildIsolation, HashStrategy};
-use uv_warnings::warn_user;
+use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::WorkspaceCache;
+use uv_workspace::pyproject::ExtraBuildDependencies;
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
use crate::commands::pip::operations::Modifications;
@@ -67,6 +68,7 @@ pub(crate) async fn pip_sync(
config_settings_package: &PackageConfigSettings,
no_build_isolation: bool,
no_build_isolation_package: Vec,
+ extra_build_dependencies: &ExtraBuildDependencies,
build_options: BuildOptions,
python_version: Option,
python_platform: Option,
@@ -85,6 +87,15 @@ pub(crate) async fn pip_sync(
printer: Printer,
preview: Preview,
) -> Result {
+ if !preview.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES)
+ && !extra_build_dependencies.is_empty()
+ {
+ warn_user_once!(
+ "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
+ PreviewFeatures::EXTRA_BUILD_DEPENDENCIES
+ );
+ }
+
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
@@ -348,6 +359,8 @@ pub(crate) async fn pip_sync(
let state = SharedState::default();
// Create a build dispatch.
+ let extra_build_requires =
+ uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let build_dispatch = BuildDispatch::new(
&client,
&cache,
@@ -361,6 +374,7 @@ pub(crate) async fn pip_sync(
config_settings,
config_settings_package,
build_isolation,
+ &extra_build_requires,
link_mode,
&build_options,
&build_hasher,
diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs
index 45727b53e..dbbc5a77b 100644
--- a/crates/uv/src/commands/project/add.rs
+++ b/crates/uv/src/commands/project/add.rs
@@ -37,7 +37,7 @@ use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreferenc
use uv_redacted::DisplaySafeUrl;
use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification};
use uv_resolver::FlatIndex;
-use uv_scripts::{Pep723ItemRef, Pep723Metadata, Pep723Script};
+use uv_scripts::{Pep723Metadata, Pep723Script};
use uv_settings::PythonInstallMirrors;
use uv_types::{BuildIsolation, HashStrategy};
use uv_warnings::warn_user_once;
@@ -104,6 +104,15 @@ pub(crate) async fn add(
);
}
+ if !preview.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES)
+ && !settings.resolver.extra_build_dependencies.is_empty()
+ {
+ warn_user_once!(
+ "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
+ PreviewFeatures::EXTRA_BUILD_DEPENDENCIES
+ );
+ }
+
for source in &requirements {
match source {
RequirementsSource::PyprojectToml(_) => {
@@ -212,7 +221,7 @@ pub(crate) async fn add(
// Discover the interpreter.
let interpreter = ScriptInterpreter::discover(
- Pep723ItemRef::Script(&script),
+ (&script).into(),
python.as_deref().map(PythonRequest::parse),
&network_settings,
python_preference,
@@ -428,6 +437,18 @@ pub(crate) async fn add(
};
// Create a build dispatch.
+ let extra_build_requires = if let AddTarget::Project(project, _) = &target {
+ uv_distribution::ExtraBuildRequires::from_workspace(
+ settings.resolver.extra_build_dependencies.clone(),
+ project.workspace(),
+ &settings.resolver.index_locations,
+ settings.resolver.sources,
+ )?
+ } else {
+ uv_distribution::ExtraBuildRequires::from_lowered(
+ settings.resolver.extra_build_dependencies.clone(),
+ )
+ };
let build_dispatch = BuildDispatch::new(
&client,
cache,
@@ -441,6 +462,7 @@ pub(crate) async fn add(
&settings.resolver.config_setting,
&settings.resolver.config_settings_package,
build_isolation,
+ &extra_build_requires,
settings.resolver.link_mode,
&settings.resolver.build_options,
&build_hasher,
diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs
index df839be08..2f1b733d6 100644
--- a/crates/uv/src/commands/project/export.rs
+++ b/crates/uv/src/commands/project/export.rs
@@ -15,7 +15,7 @@ use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
use uv_requirements::is_pylock_toml;
use uv_resolver::{PylockToml, RequirementsTxtExport};
-use uv_scripts::{Pep723ItemRef, Pep723Script};
+use uv_scripts::Pep723Script;
use uv_settings::PythonInstallMirrors;
use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache};
@@ -132,7 +132,7 @@ pub(crate) async fn export(
} else {
Some(match &target {
ExportTarget::Script(script) => ScriptInterpreter::discover(
- Pep723ItemRef::Script(script),
+ script.into(),
python.as_deref().map(PythonRequest::parse),
&network_settings,
python_preference,
diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs
index 8edcaff71..ad45d126b 100644
--- a/crates/uv/src/commands/project/lock.rs
+++ b/crates/uv/src/commands/project/lock.rs
@@ -13,7 +13,7 @@ use uv_cache::Cache;
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
Concurrency, Constraints, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification, Preview,
- Reinstall, Upgrade,
+ PreviewFeatures, Reinstall, Upgrade,
};
use uv_dispatch::BuildDispatch;
use uv_distribution::DistributionDatabase;
@@ -32,7 +32,7 @@ use uv_resolver::{
FlatIndex, InMemoryIndex, Lock, Options, OptionsBuilder, PythonRequirement,
ResolverEnvironment, ResolverManifest, SatisfiesResult, UniversalMarker,
};
-use uv_scripts::{Pep723ItemRef, Pep723Script};
+use uv_scripts::Pep723Script;
use uv_settings::PythonInstallMirrors;
use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_warnings::{warn_user, warn_user_once};
@@ -42,7 +42,7 @@ use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, Summary
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState,
- init_script_python_requirement,
+ init_script_python_requirement, script_extra_build_requires,
};
use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter};
use crate::commands::{ExitStatus, ScriptPath, diagnostics, pip};
@@ -162,7 +162,7 @@ pub(crate) async fn lock(
.await?
.into_interpreter(),
LockTarget::Script(script) => ScriptInterpreter::discover(
- Pep723ItemRef::Script(script),
+ script.into(),
python.as_deref().map(PythonRequest::parse),
&network_settings,
python_preference,
@@ -435,6 +435,7 @@ async fn do_lock(
config_settings_package,
no_build_isolation,
no_build_isolation_package,
+ extra_build_dependencies,
exclude_newer,
link_mode,
upgrade,
@@ -442,6 +443,15 @@ async fn do_lock(
sources,
} = settings;
+ if !preview.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES)
+ && !extra_build_dependencies.is_empty()
+ {
+ warn_user_once!(
+ "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
+ PreviewFeatures::EXTRA_BUILD_DEPENDENCIES
+ );
+ }
+
// Collect the requirements, etc.
let members = target.members();
let packages = target.packages();
@@ -664,6 +674,18 @@ async fn do_lock(
};
// Create a build dispatch.
+ let extra_build_requires = match &target {
+ LockTarget::Workspace(workspace) => uv_distribution::ExtraBuildRequires::from_workspace(
+ extra_build_dependencies.clone(),
+ workspace,
+ index_locations,
+ *sources,
+ )?,
+ LockTarget::Script(script) => {
+ // Try to get extra build dependencies from the script metadata
+ script_extra_build_requires((*script).into(), settings)?
+ }
+ };
let build_dispatch = BuildDispatch::new(
&client,
cache,
@@ -677,6 +699,7 @@ async fn do_lock(
config_setting,
config_settings_package,
build_isolation,
+ &extra_build_requires,
*link_mode,
build_options,
&build_hasher,
diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs
index 052d41bea..6b4be560b 100644
--- a/crates/uv/src/commands/project/mod.rs
+++ b/crates/uv/src/commands/project/mod.rs
@@ -13,7 +13,7 @@ use uv_cache_key::cache_digest;
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
Concurrency, Constraints, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification, Preview,
- PreviewFeatures, Reinstall, SourceStrategy, Upgrade,
+ PreviewFeatures, Reinstall, Upgrade,
};
use uv_dispatch::{BuildDispatch, SharedState};
use uv_distribution::{DistributionDatabase, LoweredRequirement};
@@ -46,6 +46,7 @@ use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_virtualenv::remove_virtualenv;
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::dependency_groups::DependencyGroupError;
+use uv_workspace::pyproject::ExtraBuildDependencies;
use uv_workspace::pyproject::PyProjectToml;
use uv_workspace::{RequiresPythonSources, Workspace, WorkspaceCache};
@@ -1692,6 +1693,7 @@ pub(crate) async fn resolve_names(
link_mode,
no_build_isolation,
no_build_isolation_package,
+ extra_build_dependencies,
prerelease: _,
resolution: _,
sources,
@@ -1740,6 +1742,8 @@ pub(crate) async fn resolve_names(
let build_hasher = HashStrategy::default();
// Create a build dispatch.
+ let extra_build_requires =
+ uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let build_dispatch = BuildDispatch::new(
&client,
cache,
@@ -1753,6 +1757,7 @@ pub(crate) async fn resolve_names(
config_setting,
config_settings_package,
build_isolation,
+ &extra_build_requires,
*link_mode,
build_options,
&build_hasher,
@@ -1845,6 +1850,7 @@ pub(crate) async fn resolve_environment(
config_settings_package,
no_build_isolation,
no_build_isolation_package,
+ extra_build_dependencies,
exclude_newer,
link_mode,
upgrade: _,
@@ -1948,6 +1954,8 @@ pub(crate) async fn resolve_environment(
let workspace_cache = WorkspaceCache::default();
// Create a build dispatch.
+ let extra_build_requires =
+ uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let resolve_dispatch = BuildDispatch::new(
&client,
cache,
@@ -1961,6 +1969,7 @@ pub(crate) async fn resolve_environment(
config_setting,
config_settings_package,
build_isolation,
+ &extra_build_requires,
*link_mode,
build_options,
&build_hasher,
@@ -2028,6 +2037,7 @@ pub(crate) async fn sync_environment(
config_settings_package,
no_build_isolation,
no_build_isolation_package,
+ extra_build_dependencies,
exclude_newer,
link_mode,
compile_bytecode,
@@ -2086,6 +2096,8 @@ pub(crate) async fn sync_environment(
};
// Create a build dispatch.
+ let extra_build_requires =
+ uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let build_dispatch = BuildDispatch::new(
&client,
cache,
@@ -2099,6 +2111,7 @@ pub(crate) async fn sync_environment(
config_setting,
config_settings_package,
build_isolation,
+ &extra_build_requires,
link_mode,
build_options,
&build_hasher,
@@ -2164,6 +2177,7 @@ pub(crate) async fn update_environment(
spec: RequirementsSpecification,
modifications: Modifications,
build_constraints: Constraints,
+ extra_build_requires: uv_distribution::ExtraBuildRequires,
settings: &ResolverInstallerSettings,
network_settings: &NetworkSettings,
state: &SharedState,
@@ -2194,6 +2208,7 @@ pub(crate) async fn update_environment(
link_mode,
no_build_isolation,
no_build_isolation_package,
+ extra_build_dependencies: _,
prerelease,
resolution,
sources,
@@ -2323,6 +2338,7 @@ pub(crate) async fn update_environment(
config_setting,
config_settings_package,
build_isolation,
+ &extra_build_requires,
*link_mode,
build_options,
&build_hasher,
@@ -2537,40 +2553,9 @@ pub(crate) fn script_specification(
return Ok(None);
};
- // Determine the working directory for the script.
- let script_dir = match &script {
- Pep723ItemRef::Script(script) => std::path::absolute(&script.path)?
- .parent()
- .expect("script path has no parent")
- .to_owned(),
- Pep723ItemRef::Stdin(..) | Pep723ItemRef::Remote(..) => std::env::current_dir()?,
- };
-
- // Collect any `tool.uv.index` from the script.
- let empty = Vec::default();
- let script_indexes = match settings.sources {
- SourceStrategy::Enabled => script
- .metadata()
- .tool
- .as_ref()
- .and_then(|tool| tool.uv.as_ref())
- .and_then(|uv| uv.top_level.index.as_deref())
- .unwrap_or(&empty),
- SourceStrategy::Disabled => &empty,
- };
-
- // Collect any `tool.uv.sources` from the script.
- let empty = BTreeMap::default();
- let script_sources = match settings.sources {
- SourceStrategy::Enabled => script
- .metadata()
- .tool
- .as_ref()
- .and_then(|tool| tool.uv.as_ref())
- .and_then(|uv| uv.sources.as_ref())
- .unwrap_or(&empty),
- SourceStrategy::Disabled => &empty,
- };
+ let script_dir = script.directory()?;
+ let script_indexes = script.indexes(settings.sources);
+ let script_sources = script.sources(settings.sources);
let requirements = dependencies
.iter()
@@ -2634,6 +2619,51 @@ pub(crate) fn script_specification(
)))
}
+/// Determine the extra build requires for a script.
+#[allow(clippy::result_large_err)]
+pub(crate) fn script_extra_build_requires(
+ script: Pep723ItemRef<'_>,
+ settings: &ResolverSettings,
+) -> Result {
+ let script_dir = script.directory()?;
+ let script_indexes = script.indexes(settings.sources);
+ let script_sources = script.sources(settings.sources);
+
+ // Collect any `tool.uv.extra-build-dependencies` from the script.
+ let empty = BTreeMap::default();
+ let script_extra_build_dependencies = script
+ .metadata()
+ .tool
+ .as_ref()
+ .and_then(|tool| tool.uv.as_ref())
+ .and_then(|uv| uv.extra_build_dependencies.as_ref())
+ .unwrap_or(&empty);
+
+ // Lower the extra build dependencies
+ let mut extra_build_dependencies = ExtraBuildDependencies::default();
+ for (name, requirements) in script_extra_build_dependencies {
+ let lowered_requirements: Vec<_> = requirements
+ .iter()
+ .cloned()
+ .flat_map(|requirement| {
+ LoweredRequirement::from_non_workspace_requirement(
+ requirement,
+ script_dir.as_ref(),
+ script_sources,
+ script_indexes,
+ &settings.index_locations,
+ )
+ .map_ok(|req| req.into_inner().into())
+ })
+ .collect::, _>>()?;
+ extra_build_dependencies.insert(name.clone(), lowered_requirements);
+ }
+
+ Ok(uv_distribution::ExtraBuildRequires::from_lowered(
+ extra_build_dependencies,
+ ))
+}
+
/// Warn if the user provides (e.g.) an `--index-url` in a requirements file.
fn warn_on_requirements_txt_setting(spec: &RequirementsSpecification, settings: &ResolverSettings) {
let RequirementsSpecification {
diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs
index 50c498833..649e6c887 100644
--- a/crates/uv/src/commands/project/remove.rs
+++ b/crates/uv/src/commands/project/remove.rs
@@ -16,7 +16,7 @@ use uv_fs::Simplified;
use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras, DefaultGroups};
use uv_pep508::PackageName;
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
-use uv_scripts::{Pep723ItemRef, Pep723Metadata, Pep723Script};
+use uv_scripts::{Pep723Metadata, Pep723Script};
use uv_settings::PythonInstallMirrors;
use uv_warnings::warn_user_once;
use uv_workspace::pyproject::DependencyType;
@@ -261,7 +261,7 @@ pub(crate) async fn remove(
}
RemoveTarget::Script(script) => {
let interpreter = ScriptInterpreter::discover(
- Pep723ItemRef::Script(&script),
+ (&script).into(),
python.as_deref().map(PythonRequest::parse),
&network_settings,
python_preference,
diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs
index 20e11db18..65fb62ef7 100644
--- a/crates/uv/src/commands/project/run.rs
+++ b/crates/uv/src/commands/project/run.rs
@@ -53,8 +53,8 @@ use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
EnvironmentSpecification, PreferenceLocation, ProjectEnvironment, ProjectError,
ScriptEnvironment, ScriptInterpreter, UniversalState, WorkspacePython,
- default_dependency_groups, script_specification, update_environment,
- validate_project_requires_python,
+ default_dependency_groups, script_extra_build_requires, script_specification,
+ update_environment, validate_project_requires_python,
};
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::{ExitStatus, diagnostics, project};
@@ -359,6 +359,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
// Install the script requirements, if necessary. Otherwise, use an isolated environment.
if let Some(spec) = script_specification((&script).into(), &settings.resolver)? {
+ let script_extra_build_requires =
+ script_extra_build_requires((&script).into(), &settings.resolver)?;
let environment = ScriptEnvironment::get_or_init(
(&script).into(),
python.as_deref().map(PythonRequest::parse),
@@ -407,6 +409,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
spec,
modifications,
build_constraints.unwrap_or_default(),
+ script_extra_build_requires,
&settings,
&network_settings,
&sync_state,
diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs
index 197fbc343..dbf483c03 100644
--- a/crates/uv/src/commands/project/sync.rs
+++ b/crates/uv/src/commands/project/sync.rs
@@ -14,7 +14,7 @@ use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
Concurrency, Constraints, DependencyGroups, DependencyGroupsWithDefaults, DryRun, EditableMode,
ExtrasSpecification, ExtrasSpecificationWithDefaults, HashCheckingMode, InstallOptions,
- Preview, PreviewFeatures, TargetTriple,
+ Preview, PreviewFeatures, TargetTriple, Upgrade,
};
use uv_dispatch::BuildDispatch;
use uv_distribution_types::{
@@ -26,11 +26,11 @@ use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
use uv_pep508::{MarkerTree, VersionOrUrl};
use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl};
use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
-use uv_resolver::{FlatIndex, Installable, Lock};
-use uv_scripts::{Pep723ItemRef, Pep723Script};
+use uv_resolver::{FlatIndex, ForkStrategy, Installable, Lock, PrereleaseMode, ResolutionMode};
+use uv_scripts::Pep723Script;
use uv_settings::PythonInstallMirrors;
use uv_types::{BuildIsolation, HashStrategy};
-use uv_warnings::warn_user;
+use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::pyproject::Source;
use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache};
@@ -43,11 +43,14 @@ use crate::commands::project::lock::{LockMode, LockOperation, LockResult};
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
PlatformState, ProjectEnvironment, ProjectError, ScriptEnvironment, UniversalState,
- default_dependency_groups, detect_conflicts, script_specification, update_environment,
+ default_dependency_groups, detect_conflicts, script_extra_build_requires, script_specification,
+ update_environment,
};
use crate::commands::{ExitStatus, diagnostics};
use crate::printer::Printer;
-use crate::settings::{InstallerSettingsRef, NetworkSettings, ResolverInstallerSettings};
+use crate::settings::{
+ InstallerSettingsRef, NetworkSettings, ResolverInstallerSettings, ResolverSettings,
+};
/// Sync the project environment.
#[allow(clippy::fn_params_excessive_bools)]
@@ -164,7 +167,7 @@ pub(crate) async fn sync(
),
SyncTarget::Script(script) => SyncEnvironment::Script(
ScriptEnvironment::get_or_init(
- Pep723ItemRef::Script(script),
+ script.into(),
python.as_deref().map(PythonRequest::parse),
&network_settings,
python_preference,
@@ -222,8 +225,9 @@ pub(crate) async fn sync(
}
// Parse the requirements from the script.
- let spec = script_specification(Pep723ItemRef::Script(script), &settings.resolver)?
- .unwrap_or_default();
+ let spec = script_specification(script.into(), &settings.resolver)?.unwrap_or_default();
+ let script_extra_build_requires =
+ script_extra_build_requires(script.into(), &settings.resolver)?;
// Parse the build constraints from the script.
let build_constraints = script
@@ -248,6 +252,7 @@ pub(crate) async fn sync(
spec,
modifications,
build_constraints.unwrap_or_default(),
+ script_extra_build_requires,
&settings,
&network_settings,
&PlatformState::default(),
@@ -579,6 +584,7 @@ pub(super) async fn do_sync(
config_settings_package,
no_build_isolation,
no_build_isolation_package,
+ extra_build_dependencies,
exclude_newer,
link_mode,
compile_bytecode,
@@ -587,6 +593,52 @@ pub(super) async fn do_sync(
sources,
} = settings;
+ if !preview.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES)
+ && !extra_build_dependencies.is_empty()
+ {
+ warn_user_once!(
+ "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
+ PreviewFeatures::EXTRA_BUILD_DEPENDENCIES
+ );
+ }
+
+ // Lower the extra build dependencies with source resolution
+ let extra_build_requires = match &target {
+ InstallTarget::Workspace { workspace, .. }
+ | InstallTarget::Project { workspace, .. }
+ | InstallTarget::NonProjectWorkspace { workspace, .. } => {
+ uv_distribution::ExtraBuildRequires::from_workspace(
+ extra_build_dependencies.clone(),
+ workspace,
+ index_locations,
+ sources,
+ )?
+ }
+ InstallTarget::Script { script, .. } => {
+ // Try to get extra build dependencies from the script metadata
+ let resolver_settings = ResolverSettings {
+ build_options: build_options.clone(),
+ config_setting: config_setting.clone(),
+ config_settings_package: config_settings_package.clone(),
+ dependency_metadata: dependency_metadata.clone(),
+ exclude_newer: exclude_newer.clone(),
+ fork_strategy: ForkStrategy::default(),
+ index_locations: index_locations.clone(),
+ index_strategy,
+ keyring_provider,
+ link_mode,
+ no_build_isolation,
+ no_build_isolation_package: no_build_isolation_package.to_vec(),
+ extra_build_dependencies: extra_build_dependencies.clone(),
+ prerelease: PrereleaseMode::default(),
+ resolution: ResolutionMode::default(),
+ sources,
+ upgrade: Upgrade::default(),
+ };
+ script_extra_build_requires((*script).into(), &resolver_settings)?
+ }
+ };
+
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
@@ -715,10 +767,11 @@ pub(super) async fn do_sync(
config_setting,
config_settings_package,
build_isolation,
+ &extra_build_requires,
link_mode,
build_options,
&build_hasher,
- exclude_newer,
+ exclude_newer.clone(),
sources,
workspace_cache.clone(),
concurrency,
diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs
index 1d594bd53..1f8f46a3d 100644
--- a/crates/uv/src/commands/project/tree.rs
+++ b/crates/uv/src/commands/project/tree.rs
@@ -13,7 +13,7 @@ use uv_normalize::DefaultGroups;
use uv_pep508::PackageName;
use uv_python::{PythonDownloads, PythonPreference, PythonRequest, PythonVersion};
use uv_resolver::{PackageMap, TreeDisplay};
-use uv_scripts::{Pep723ItemRef, Pep723Script};
+use uv_scripts::Pep723Script;
use uv_settings::PythonInstallMirrors;
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache};
@@ -86,7 +86,7 @@ pub(crate) async fn tree(
} else {
Some(match target {
LockTarget::Script(script) => ScriptInterpreter::discover(
- Pep723ItemRef::Script(script),
+ script.into(),
python.as_deref().map(PythonRequest::parse),
network_settings,
python_preference,
@@ -203,6 +203,7 @@ pub(crate) async fn tree(
config_settings_package: _,
no_build_isolation: _,
no_build_isolation_package: _,
+ extra_build_dependencies: _,
exclude_newer: _,
link_mode: _,
upgrade: _,
diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs
index 192597e93..6528f61d2 100644
--- a/crates/uv/src/commands/tool/install.rs
+++ b/crates/uv/src/commands/tool/install.rs
@@ -23,7 +23,7 @@ use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_settings::{PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
use uv_tool::InstalledTools;
use uv_warnings::warn_user;
-use uv_workspace::WorkspaceCache;
+use uv_workspace::{WorkspaceCache, pyproject::ExtraBuildDependencies};
use crate::commands::ExitStatus;
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
@@ -439,6 +439,7 @@ pub(crate) async fn install(
spec,
Modifications::Exact,
Constraints::from_requirements(build_constraints.iter().cloned()),
+ uv_distribution::ExtraBuildRequires::from_lowered(ExtraBuildDependencies::default()),
&settings,
&network_settings,
&state,
diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs
index af42a9eef..f7bce3197 100644
--- a/crates/uv/src/commands/tool/upgrade.rs
+++ b/crates/uv/src/commands/tool/upgrade.rs
@@ -19,7 +19,7 @@ use uv_requirements::RequirementsSpecification;
use uv_settings::{Combine, PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
use uv_tool::InstalledTools;
use uv_warnings::write_error_chain;
-use uv_workspace::WorkspaceCache;
+use uv_workspace::{WorkspaceCache, pyproject::ExtraBuildDependencies};
use crate::commands::pip::loggers::{
DefaultInstallLogger, SummaryResolveLogger, UpgradeInstallLogger,
@@ -337,6 +337,7 @@ async fn upgrade_tool(
spec,
Modifications::Exact,
build_constraints,
+ uv_distribution::ExtraBuildRequires::from_lowered(ExtraBuildDependencies::default()),
&settings,
network_settings,
&state,
diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs
index f391ef499..9bfa9d24d 100644
--- a/crates/uv/src/commands/venv.rs
+++ b/crates/uv/src/commands/venv.rs
@@ -29,6 +29,7 @@ use uv_shell::{Shell, shlex_posix, shlex_windows};
use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy};
use uv_virtualenv::OnExisting;
use uv_warnings::warn_user;
+use uv_workspace::pyproject::ExtraBuildDependencies;
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceError};
use crate::commands::ExitStatus;
@@ -266,7 +267,8 @@ pub(crate) async fn venv(
// Do not allow builds
let build_options = BuildOptions::new(NoBinary::None, NoBuild::All);
-
+ let extra_build_requires =
+ uv_distribution::ExtraBuildRequires::from_lowered(ExtraBuildDependencies::default());
// Prep the build context.
let build_dispatch = BuildDispatch::new(
&client,
@@ -281,6 +283,7 @@ pub(crate) async fn venv(
&config_settings,
&config_settings_package,
BuildIsolation::Isolated,
+ &extra_build_requires,
link_mode,
&build_options,
&build_hasher,
diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs
index 4a937b0db..0d5163e4d 100644
--- a/crates/uv/src/lib.rs
+++ b/crates/uv/src/lib.rs
@@ -28,7 +28,7 @@ use uv_cli::{
ProjectCommand, PythonCommand, PythonNamespace, SelfCommand, SelfNamespace, ToolCommand,
ToolNamespace, TopLevelArgs, compat::CompatArgs,
};
-use uv_configuration::min_stack_size;
+use uv_configuration::{PreviewFeatures, min_stack_size};
use uv_fs::{CWD, Simplified};
#[cfg(feature = "self-update")]
use uv_pep440::release_specifiers_to_ranges;
@@ -37,7 +37,7 @@ use uv_pypi_types::{ParsedDirectoryUrl, ParsedUrl};
use uv_python::PythonRequest;
use uv_requirements::{GroupsSpecification, RequirementsSource};
use uv_requirements_txt::RequirementsTxtRequirement;
-use uv_scripts::{Pep723Error, Pep723Item, Pep723ItemRef, Pep723Metadata, Pep723Script};
+use uv_scripts::{Pep723Error, Pep723Item, Pep723Metadata, Pep723Script};
use uv_settings::{Combine, EnvironmentOptions, FilesystemOptions, Options};
use uv_static::EnvVars;
use uv_warnings::{warn_user, warn_user_once};
@@ -443,6 +443,16 @@ async fn run(mut cli: Cli) -> Result {
// Resolve the settings from the command-line arguments and workspace configuration.
let args = PipCompileSettings::resolve(args, filesystem);
show_settings!(args);
+ if !args.settings.extra_build_dependencies.is_empty()
+ && !globals
+ .preview
+ .is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES)
+ {
+ warn_user_once!(
+ "The `extra-build-dependencies` setting is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
+ PreviewFeatures::EXTRA_BUILD_DEPENDENCIES
+ );
+ }
// Initialize the cache.
let cache = cache.init()?.with_refresh(
@@ -516,6 +526,7 @@ async fn run(mut cli: Cli) -> Result {
args.settings.config_settings_package,
args.settings.no_build_isolation,
args.settings.no_build_isolation_package,
+ &args.settings.extra_build_dependencies,
args.settings.build_options,
args.settings.python_version,
args.settings.python_platform,
@@ -543,6 +554,16 @@ async fn run(mut cli: Cli) -> Result {
// Resolve the settings from the command-line arguments and workspace configuration.
let args = PipSyncSettings::resolve(args, filesystem);
show_settings!(args);
+ if !args.settings.extra_build_dependencies.is_empty()
+ && !globals
+ .preview
+ .is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES)
+ {
+ warn_user_once!(
+ "The `extra-build-dependencies` setting is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
+ PreviewFeatures::EXTRA_BUILD_DEPENDENCIES
+ );
+ }
// Initialize the cache.
let cache = cache.init()?.with_refresh(
@@ -593,6 +614,7 @@ async fn run(mut cli: Cli) -> Result {
&args.settings.config_settings_package,
args.settings.no_build_isolation,
args.settings.no_build_isolation_package,
+ &args.settings.extra_build_dependencies,
args.settings.build_options,
args.settings.python_version,
args.settings.python_platform,
@@ -621,6 +643,16 @@ async fn run(mut cli: Cli) -> Result {
// Resolve the settings from the command-line arguments and workspace configuration.
let mut args = PipInstallSettings::resolve(args, filesystem);
show_settings!(args);
+ if !args.settings.extra_build_dependencies.is_empty()
+ && !globals
+ .preview
+ .is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES)
+ {
+ warn_user_once!(
+ "The `extra-build-dependencies` setting is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
+ PreviewFeatures::EXTRA_BUILD_DEPENDENCIES
+ );
+ }
let mut requirements = Vec::with_capacity(
args.package.len() + args.editables.len() + args.requirements.len(),
@@ -735,6 +767,7 @@ async fn run(mut cli: Cli) -> Result {
&args.settings.config_settings_package,
args.settings.no_build_isolation,
args.settings.no_build_isolation_package,
+ &args.settings.extra_build_dependencies,
args.settings.build_options,
args.modifications,
args.settings.python_version,
@@ -1467,7 +1500,7 @@ async fn run(mut cli: Cli) -> Result {
if let Some(Pep723Item::Script(script)) = script {
commands::python_find_script(
- Pep723ItemRef::Script(&script),
+ (&script).into(),
args.show_version,
&globals.network_settings,
globals.python_preference,
diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs
index bc09e8257..7746f0667 100644
--- a/crates/uv/src/settings.rs
+++ b/crates/uv/src/settings.rs
@@ -45,7 +45,7 @@ use uv_settings::{
use uv_static::EnvVars;
use uv_torch::TorchMode;
use uv_warnings::warn_user_once;
-use uv_workspace::pyproject::DependencyType;
+use uv_workspace::pyproject::{DependencyType, ExtraBuildDependencies};
use uv_workspace::pyproject_mut::AddBoundsKind;
use crate::commands::ToolRunCommand;
@@ -2714,6 +2714,7 @@ pub(crate) struct InstallerSettingsRef<'a> {
pub(crate) config_settings_package: &'a PackageConfigSettings,
pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: &'a [PackageName],
+ pub(crate) extra_build_dependencies: &'a ExtraBuildDependencies,
pub(crate) exclude_newer: ExcludeNewer,
pub(crate) link_mode: LinkMode,
pub(crate) compile_bytecode: bool,
@@ -2740,6 +2741,7 @@ pub(crate) struct ResolverSettings {
pub(crate) link_mode: LinkMode,
pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: Vec,
+ pub(crate) extra_build_dependencies: ExtraBuildDependencies,
pub(crate) prerelease: PrereleaseMode,
pub(crate) resolution: ResolutionMode,
pub(crate) sources: SourceStrategy,
@@ -2792,6 +2794,7 @@ impl From for ResolverSettings {
config_settings_package: value.config_settings_package.unwrap_or_default(),
no_build_isolation: value.no_build_isolation.unwrap_or_default(),
no_build_isolation_package: value.no_build_isolation_package.unwrap_or_default(),
+ extra_build_dependencies: value.extra_build_dependencies.unwrap_or_default(),
exclude_newer: value.exclude_newer,
link_mode: value.link_mode.unwrap_or_default(),
sources: SourceStrategy::from_args(value.no_sources.unwrap_or_default()),
@@ -2889,6 +2892,7 @@ impl From for ResolverInstallerSettings {
link_mode: value.link_mode.unwrap_or_default(),
no_build_isolation: value.no_build_isolation.unwrap_or_default(),
no_build_isolation_package: value.no_build_isolation_package.unwrap_or_default(),
+ extra_build_dependencies: value.extra_build_dependencies.unwrap_or_default(),
prerelease: value.prerelease.unwrap_or_default(),
resolution: value.resolution.unwrap_or_default(),
sources: SourceStrategy::from_args(value.no_sources.unwrap_or_default()),
@@ -2931,6 +2935,7 @@ pub(crate) struct PipSettings {
pub(crate) torch_backend: Option,
pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: Vec,
+ pub(crate) extra_build_dependencies: ExtraBuildDependencies,
pub(crate) build_options: BuildOptions,
pub(crate) allow_empty_requirements: bool,
pub(crate) strict: bool,
@@ -2998,6 +3003,7 @@ impl PipSettings {
only_binary,
no_build_isolation,
no_build_isolation_package,
+ extra_build_dependencies,
strict,
extra,
all_extras,
@@ -3057,6 +3063,7 @@ impl PipSettings {
config_settings_package: top_level_config_settings_package,
no_build_isolation: top_level_no_build_isolation,
no_build_isolation_package: top_level_no_build_isolation_package,
+ extra_build_dependencies: top_level_extra_build_dependencies,
exclude_newer: top_level_exclude_newer,
link_mode: top_level_link_mode,
compile_bytecode: top_level_compile_bytecode,
@@ -3093,6 +3100,8 @@ impl PipSettings {
let no_build_isolation = no_build_isolation.combine(top_level_no_build_isolation);
let no_build_isolation_package =
no_build_isolation_package.combine(top_level_no_build_isolation_package);
+ let extra_build_dependencies =
+ extra_build_dependencies.combine(top_level_extra_build_dependencies);
let exclude_newer = args
.exclude_newer
.combine(exclude_newer)
@@ -3196,6 +3205,10 @@ impl PipSettings {
.no_build_isolation_package
.combine(no_build_isolation_package)
.unwrap_or_default(),
+ extra_build_dependencies: args
+ .extra_build_dependencies
+ .combine(extra_build_dependencies)
+ .unwrap_or_default(),
config_setting: args
.config_settings
.combine(config_settings)
@@ -3303,6 +3316,7 @@ impl<'a> From<&'a ResolverInstallerSettings> for InstallerSettingsRef<'a> {
config_settings_package: &settings.resolver.config_settings_package,
no_build_isolation: settings.resolver.no_build_isolation,
no_build_isolation_package: &settings.resolver.no_build_isolation_package,
+ extra_build_dependencies: &settings.resolver.extra_build_dependencies,
exclude_newer: settings.resolver.exclude_newer.clone(),
link_mode: settings.resolver.link_mode,
compile_bytecode: settings.compile_bytecode,
diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs
index 9ee284f3d..fc8dbf344 100644
--- a/crates/uv/tests/it/pip_install.rs
+++ b/crates/uv/tests/it/pip_install.rs
@@ -3920,16 +3920,17 @@ fn config_settings_registry() {
.arg("iniconfig")
.arg("--no-binary")
.arg("iniconfig")
- .arg("-C=global-option=build_ext"), @r###"
+ .arg("-C=global-option=build_ext"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
+ Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
- "###
+ "
);
// Uninstall the package.
diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs
index 9de38cb31..6775f2e3d 100644
--- a/crates/uv/tests/it/show_settings.rs
+++ b/crates/uv/tests/it/show_settings.rs
@@ -184,6 +184,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -378,6 +381,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -573,6 +579,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -800,6 +809,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -962,6 +974,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -1168,6 +1183,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -1422,6 +1440,9 @@ fn resolve_index_url() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -1686,6 +1707,9 @@ fn resolve_index_url() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -1905,6 +1929,9 @@ fn resolve_find_links() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -2089,6 +2116,9 @@ fn resolve_top_level() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -2333,6 +2363,9 @@ fn resolve_top_level() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -2560,6 +2593,9 @@ fn resolve_top_level() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -2743,6 +2779,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -2910,6 +2949,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -3077,6 +3119,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -3246,6 +3291,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -3407,6 +3455,7 @@ fn resolve_tool() -> anyhow::Result<()> {
config_settings_package: None,
no_build_isolation: None,
no_build_isolation_package: None,
+ extra_build_dependencies: None,
exclude_newer: None,
exclude_newer_package: None,
link_mode: Some(
@@ -3455,6 +3504,9 @@ fn resolve_tool() -> anyhow::Result<()> {
link_mode: Clone,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
prerelease: IfNecessaryOrExplicit,
resolution: LowestDirect,
sources: Enabled,
@@ -3613,6 +3665,9 @@ fn resolve_poetry_toml() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -3848,6 +3903,9 @@ fn resolve_both() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -4087,6 +4145,9 @@ fn resolve_both_special_fields() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -4405,6 +4466,9 @@ fn resolve_config_file() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -4490,7 +4554,7 @@ fn resolve_config_file() -> anyhow::Result<()> {
|
1 | [project]
| ^^^^^^^
- unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend`
+ unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `extra-build-dependencies`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend`
"
);
@@ -4665,6 +4729,9 @@ fn resolve_skip_empty() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -4835,6 +4902,9 @@ fn resolve_skip_empty() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -5024,6 +5094,9 @@ fn allow_insecure_host() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -5274,6 +5347,9 @@ fn index_priority() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -5503,6 +5579,9 @@ fn index_priority() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -5738,6 +5817,9 @@ fn index_priority() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -5968,6 +6050,9 @@ fn index_priority() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -6205,6 +6290,9 @@ fn index_priority() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -6435,6 +6523,9 @@ fn index_priority() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -6609,6 +6700,9 @@ fn verify_hashes() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -6769,6 +6863,9 @@ fn verify_hashes() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -6927,6 +7024,9 @@ fn verify_hashes() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -7087,6 +7187,9 @@ fn verify_hashes() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -7245,6 +7348,9 @@ fn verify_hashes() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -7404,6 +7510,9 @@ fn verify_hashes() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@@ -7501,7 +7610,7 @@ fn preview_features() {
show_settings: true,
preview: Preview {
flags: PreviewFeatures(
- PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS,
+ PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | EXTRA_BUILD_DEPENDENCIES,
),
},
python_preference: Managed,
@@ -7572,6 +7681,9 @@ fn preview_features() {
link_mode: Clone,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
prerelease: IfNecessaryOrExplicit,
resolution: Highest,
sources: Enabled,
@@ -7679,6 +7791,9 @@ fn preview_features() {
link_mode: Clone,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
prerelease: IfNecessaryOrExplicit,
resolution: Highest,
sources: Enabled,
@@ -7715,7 +7830,7 @@ fn preview_features() {
show_settings: true,
preview: Preview {
flags: PreviewFeatures(
- PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS,
+ PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | EXTRA_BUILD_DEPENDENCIES,
),
},
python_preference: Managed,
@@ -7786,6 +7901,9 @@ fn preview_features() {
link_mode: Clone,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
prerelease: IfNecessaryOrExplicit,
resolution: Highest,
sources: Enabled,
@@ -7893,6 +8011,9 @@ fn preview_features() {
link_mode: Clone,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
prerelease: IfNecessaryOrExplicit,
resolution: Highest,
sources: Enabled,
@@ -8000,6 +8121,9 @@ fn preview_features() {
link_mode: Clone,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
prerelease: IfNecessaryOrExplicit,
resolution: Highest,
sources: Enabled,
@@ -8109,6 +8233,9 @@ fn preview_features() {
link_mode: Clone,
no_build_isolation: false,
no_build_isolation_package: [],
+ extra_build_dependencies: ExtraBuildDependencies(
+ {},
+ ),
prerelease: IfNecessaryOrExplicit,
resolution: Highest,
sources: Enabled,
diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs
index a9dc60dcc..820899d06 100644
--- a/crates/uv/tests/it/sync.rs
+++ b/crates/uv/tests/it/sync.rs
@@ -1567,6 +1567,401 @@ fn sync_build_isolation_extra() -> Result<()> {
Ok(())
}
+#[test]
+fn sync_extra_build_dependencies() -> Result<()> {
+ let context = TestContext::new("3.12").with_filtered_counts();
+
+ // Write a test package that arbitrarily requires `anyio` at build time
+ let child = context.temp_dir.child("child");
+ child.create_dir_all()?;
+ let child_pyproject_toml = child.child("pyproject.toml");
+ child_pyproject_toml.write_str(indoc! {r#"
+ [project]
+ name = "child"
+ version = "0.1.0"
+ requires-python = ">=3.9"
+
+ [build-system]
+ requires = ["hatchling"]
+ backend-path = ["."]
+ build-backend = "build_backend"
+ "#})?;
+ let build_backend = child.child("build_backend.py");
+ build_backend.write_str(indoc! {r#"
+ import sys
+
+ from hatchling.build import *
+
+ try:
+ import anyio
+ except ModuleNotFoundError:
+ print("Missing `anyio` module", file=sys.stderr)
+ sys.exit(1)
+ "#})?;
+ child.child("src/child/__init__.py").touch()?;
+
+ let parent = &context.temp_dir;
+ let pyproject_toml = parent.child("pyproject.toml");
+ pyproject_toml.write_str(indoc! {r#"
+ [project]
+ name = "parent"
+ version = "0.1.0"
+ requires-python = ">=3.9"
+ dependencies = ["child"]
+
+ [tool.uv.sources]
+ child = { path = "child" }
+ "#})?;
+
+ context.venv().arg("--clear").assert().success();
+ // Running `uv sync` should fail due to missing build-dependencies
+ uv_snapshot!(context.filters(), context.sync(), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+
+ ----- stderr -----
+ Resolved [N] packages in [TIME]
+ × Failed to build `child @ file://[TEMP_DIR]/child`
+ ├─▶ The build backend returned an error
+ ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1)
+
+ [stderr]
+ Missing `anyio` module
+
+ hint: This usually indicates a problem with the package or the build environment.
+ help: `child` was included because `parent` (v0.1.0) depends on `child`
+ ");
+
+ // Adding `extra-build-dependencies` should solve the issue
+ pyproject_toml.write_str(indoc! {r#"
+ [project]
+ name = "parent"
+ version = "0.1.0"
+ requires-python = ">=3.9"
+ dependencies = ["child"]
+
+ [tool.uv.sources]
+ child = { path = "child" }
+
+ [tool.uv.extra-build-dependencies]
+ child = ["anyio"]
+ "#})?;
+
+ context.venv().arg("--clear").assert().success();
+ uv_snapshot!(context.filters(), context.sync(), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning.
+ Resolved [N] packages in [TIME]
+ Prepared [N] packages in [TIME]
+ Installed [N] packages in [TIME]
+ + child==0.1.0 (from file://[TEMP_DIR]/child)
+ ");
+
+ // Adding `extra-build-dependencies` with the wrong name should fail the build
+ // (the cache is invalidated when extra build dependencies change)
+ pyproject_toml.write_str(indoc! {r#"
+ [project]
+ name = "parent"
+ version = "0.1.0"
+ requires-python = ">=3.9"
+ dependencies = ["child"]
+
+ [tool.uv.sources]
+ child = { path = "child" }
+
+ [tool.uv.extra-build-dependencies]
+ wrong_name = ["anyio"]
+ "#})?;
+
+ context.venv().arg("--clear").assert().success();
+ uv_snapshot!(context.filters(), context.sync(), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+
+ ----- stderr -----
+ warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning.
+ Resolved [N] packages in [TIME]
+ × Failed to build `child @ file://[TEMP_DIR]/child`
+ ├─▶ The build backend returned an error
+ ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1)
+
+ [stderr]
+ Missing `anyio` module
+
+ hint: This usually indicates a problem with the package or the build environment.
+ help: `child` was included because `parent` (v0.1.0) depends on `child`
+ ");
+
+ // Write a test package that arbitrarily bans `anyio` at build time
+ let bad_child = context.temp_dir.child("bad_child");
+ bad_child.create_dir_all()?;
+ let bad_child_pyproject_toml = bad_child.child("pyproject.toml");
+ bad_child_pyproject_toml.write_str(indoc! {r#"
+ [project]
+ name = "bad_child"
+ version = "0.1.0"
+ requires-python = ">=3.9"
+
+ [build-system]
+ requires = ["hatchling"]
+ backend-path = ["."]
+ build-backend = "build_backend"
+ "#})?;
+ let build_backend = bad_child.child("build_backend.py");
+ build_backend.write_str(indoc! {r#"
+ import sys
+
+ from hatchling.build import *
+
+ try:
+ import anyio
+ except ModuleNotFoundError:
+ pass
+ else:
+ print("Found `anyio` module", file=sys.stderr)
+ sys.exit(1)
+ "#})?;
+ bad_child.child("src/bad_child/__init__.py").touch()?;
+
+ // Depend on `bad_child` too
+ pyproject_toml.write_str(indoc! {r#"
+ [project]
+ name = "parent"
+ version = "0.1.0"
+ requires-python = ">=3.9"
+ dependencies = ["child", "bad_child"]
+
+ [tool.uv.sources]
+ child = { path = "child" }
+ bad_child = { path = "bad_child" }
+
+ [tool.uv.extra-build-dependencies]
+ child = ["anyio"]
+ bad_child = ["anyio"]
+ "#})?;
+
+ // Confirm that `bad_child` fails if anyio is provided
+ context.venv().arg("--clear").assert().success();
+ uv_snapshot!(context.filters(), context.sync(), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+
+ ----- stderr -----
+ warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning.
+ Resolved [N] packages in [TIME]
+ × Failed to build `bad-child @ file://[TEMP_DIR]/bad_child`
+ ├─▶ The build backend returned an error
+ ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1)
+
+ [stderr]
+ Found `anyio` module
+
+ hint: This usually indicates a problem with the package or the build environment.
+ help: `bad-child` was included because `parent` (v0.1.0) depends on `bad-child`
+ ");
+
+ // But `anyio` is not provided to `bad_child` if scoped to `child`
+ pyproject_toml.write_str(indoc! {r#"
+ [project]
+ name = "parent"
+ version = "0.1.0"
+ requires-python = ">=3.9"
+ dependencies = ["child", "bad_child"]
+
+ [tool.uv.sources]
+ child = { path = "child" }
+ bad_child = { path = "bad_child" }
+
+ [tool.uv.extra-build-dependencies]
+ child = ["anyio"]
+ "#})?;
+
+ context.venv().arg("--clear").assert().success();
+ uv_snapshot!(context.filters(), context.sync(), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning.
+ Resolved [N] packages in [TIME]
+ Prepared [N] packages in [TIME]
+ Installed [N] packages in [TIME]
+ + bad-child==0.1.0 (from file://[TEMP_DIR]/bad_child)
+ + child==0.1.0 (from file://[TEMP_DIR]/child)
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn sync_extra_build_dependencies_sources() -> Result<()> {
+ let context = TestContext::new("3.12").with_filtered_counts();
+
+ let anyio_local = context.workspace_root.join("scripts/packages/anyio_local");
+
+ // Write a test package that arbitrarily requires `anyio` at a specific _path_ at build time
+ let child = context.temp_dir.child("child");
+ child.create_dir_all()?;
+ let child_pyproject_toml = child.child("pyproject.toml");
+ child_pyproject_toml.write_str(indoc! {r#"
+ [project]
+ name = "child"
+ version = "0.1.0"
+ requires-python = ">=3.9"
+
+ [build-system]
+ requires = ["hatchling"]
+ backend-path = ["."]
+ build-backend = "build_backend"
+ "#})?;
+ let build_backend = child.child("build_backend.py");
+ build_backend.write_str(&formatdoc! {r#"
+ import sys
+
+ from hatchling.build import *
+
+ try:
+ import anyio
+ except ModuleNotFoundError:
+ print("Missing `anyio` module", file=sys.stderr)
+ sys.exit(1)
+
+ # Check that we got the local version of anyio by checking for the marker
+ if not hasattr(anyio, 'LOCAL_ANYIO_MARKER'):
+ print("Found system anyio instead of local anyio", file=sys.stderr)
+ sys.exit(1)
+ "#})?;
+ child.child("src/child/__init__.py").touch()?;
+
+ let pyproject_toml = context.temp_dir.child("pyproject.toml");
+ pyproject_toml.write_str(&formatdoc! {r#"
+ [project]
+ name = "project"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = ["child"]
+
+ [tool.uv.sources]
+ anyio = {{ path = "{anyio_local}" }}
+ child = {{ path = "child" }}
+
+ [tool.uv.extra-build-dependencies]
+ child = ["anyio"]
+ "#,
+ anyio_local = anyio_local.portable_display(),
+ })?;
+
+ // Running `uv sync` should succeed, as `anyio` is provided as a source
+ uv_snapshot!(context.filters(), context.sync(), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning.
+ Resolved [N] packages in [TIME]
+ Prepared [N] packages in [TIME]
+ Installed [N] packages in [TIME]
+ + child==0.1.0 (from file://[TEMP_DIR]/child)
+ ");
+
+ // TODO(zanieb): We want to test with `--no-sources` too but unfortunately that's not easy
+ // because it'll disable the `child` path source too!
+
+ Ok(())
+}
+
+#[test]
+fn sync_extra_build_dependencies_sources_from_child() -> Result<()> {
+ let context = TestContext::new("3.12").with_filtered_counts();
+
+ let anyio_local = context.workspace_root.join("scripts/packages/anyio_local");
+
+ // Write a test package that arbitrarily requires `anyio` at a specific _path_ at build time
+ let child = context.temp_dir.child("child");
+ child.create_dir_all()?;
+ let child_pyproject_toml = child.child("pyproject.toml");
+ child_pyproject_toml.write_str(&formatdoc! {r#"
+ [project]
+ name = "child"
+ version = "0.1.0"
+ requires-python = ">=3.9"
+
+ [build-system]
+ requires = ["hatchling"]
+ backend-path = ["."]
+ build-backend = "build_backend"
+
+ [tool.uv.sources]
+ anyio = {{ path = "{}" }}
+ "#, anyio_local.portable_display()
+ })?;
+ let build_backend = child.child("build_backend.py");
+ build_backend.write_str(&formatdoc! {r#"
+ import sys
+
+ from hatchling.build import *
+
+ try:
+ import anyio
+ except ModuleNotFoundError:
+ print("Missing `anyio` module", file=sys.stderr)
+ sys.exit(1)
+
+ # Check that we got the local version of anyio by checking for the marker
+ if not hasattr(anyio, 'LOCAL_ANYIO_MARKER'):
+ print("Found system anyio instead of local anyio", file=sys.stderr)
+ sys.exit(1)
+ "#})?;
+ child.child("src/child/__init__.py").touch()?;
+
+ let pyproject_toml = context.temp_dir.child("pyproject.toml");
+ pyproject_toml.write_str(indoc! {r#"
+ [project]
+ name = "project"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = ["child"]
+
+ [tool.uv.sources]
+ child = { path = "child" }
+
+ [tool.uv.extra-build-dependencies]
+ child = ["anyio"]
+ "#,
+ })?;
+
+ // Running `uv sync` should fail due to the unapplied source
+ uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+
+ ----- stderr -----
+ warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning.
+ Resolved [N] packages in [TIME]
+ × Failed to build `child @ file://[TEMP_DIR]/child`
+ ├─▶ The build backend returned an error
+ ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1)
+
+ [stderr]
+ Found system anyio instead of local anyio
+
+ hint: This usually indicates a problem with the package or the build environment.
+ help: `child` was included because `project` (v0.1.0) depends on `child`
+ ");
+
+ Ok(())
+}
+
/// Avoid using incompatible versions for build dependencies that are also part of the resolved
/// environment. This is a very subtle issue, but: when locking, we don't enforce platform
/// compatibility. So, if we reuse the resolver state to install, and the install itself has to
@@ -4198,6 +4593,187 @@ fn no_install_project_no_build() -> Result<()> {
Ok(())
}
+#[test]
+fn sync_extra_build_dependencies_script() -> Result<()> {
+ let context = TestContext::new("3.12").with_filtered_counts();
+
+ // Write a test package that arbitrarily requires `anyio` at build time
+ let child = context.temp_dir.child("child");
+ child.create_dir_all()?;
+ let child_pyproject_toml = child.child("pyproject.toml");
+ child_pyproject_toml.write_str(indoc! {r#"
+ [project]
+ name = "child"
+ version = "0.1.0"
+ requires-python = ">=3.9"
+ [build-system]
+ requires = ["hatchling"]
+ backend-path = ["."]
+ build-backend = "build_backend"
+ "#})?;
+ let build_backend = child.child("build_backend.py");
+ build_backend.write_str(indoc! {r#"
+ import sys
+ from hatchling.build import *
+ try:
+ import anyio
+ except ModuleNotFoundError:
+ print("Missing `anyio` module", file=sys.stderr)
+ sys.exit(1)
+ "#})?;
+ child.child("src/child/__init__.py").touch()?;
+
+ // Create a script that depends on the child package
+ let script = context.temp_dir.child("script.py");
+ script.write_str(indoc! {r#"
+ # /// script
+ # requires-python = ">=3.12"
+ # dependencies = ["child"]
+ #
+ # [tool.uv.sources]
+ # child = { path = "child" }
+ # ///
+ "#})?;
+
+ let filters = context
+ .filters()
+ .into_iter()
+ .chain(vec![(
+ r"environments-v2/script-[a-z0-9]+",
+ "environments-v2/script-[HASH]",
+ )])
+ .collect::>();
+
+ // Running `uv sync` should fail due to missing build-dependencies
+ uv_snapshot!(filters, context.sync().arg("--script").arg("script.py"), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+
+ ----- stderr -----
+ Creating script environment at: [CACHE_DIR]/environments-v2/script-[HASH]
+ Resolved [N] packages in [TIME]
+ × Failed to build `child @ file://[TEMP_DIR]/child`
+ ├─▶ The build backend returned an error
+ ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1)
+
+ [stderr]
+ Missing `anyio` module
+
+ hint: This usually indicates a problem with the package or the build environment.
+ ");
+
+ // Add extra build dependencies to the script
+ script.write_str(indoc! {r#"
+ # /// script
+ # requires-python = ">=3.12"
+ # dependencies = ["child"]
+ #
+ # [tool.uv.sources]
+ # child = { path = "child" }
+ #
+ # [tool.uv.extra-build-dependencies]
+ # child = ["anyio"]
+ # ///
+ "#})?;
+
+ // Running `uv sync` should now succeed due to extra build-dependencies
+ context.venv().arg("--clear").assert().success();
+ uv_snapshot!(filters, context.sync().arg("--script").arg("script.py"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Using script environment at: [CACHE_DIR]/environments-v2/script-[HASH]
+ Resolved [N] packages in [TIME]
+ Prepared [N] packages in [TIME]
+ Installed [N] packages in [TIME]
+ + child==0.1.0 (from file://[TEMP_DIR]/child)
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn sync_extra_build_dependencies_script_sources() -> Result<()> {
+ let context = TestContext::new("3.12").with_filtered_counts();
+ let anyio_local = context.workspace_root.join("scripts/packages/anyio_local");
+
+ // Write a test package that arbitrarily requires `anyio` at a specific _path_ at build time
+ let child = context.temp_dir.child("child");
+ child.create_dir_all()?;
+ let child_pyproject_toml = child.child("pyproject.toml");
+ child_pyproject_toml.write_str(indoc! {r#"
+ [project]
+ name = "child"
+ version = "0.1.0"
+ requires-python = ">=3.9"
+ [build-system]
+ requires = ["hatchling"]
+ backend-path = ["."]
+ build-backend = "build_backend"
+ "#})?;
+ let build_backend = child.child("build_backend.py");
+ build_backend.write_str(&formatdoc! {r#"
+ import sys
+ from hatchling.build import *
+ try:
+ import anyio
+ except ModuleNotFoundError:
+ print("Missing `anyio` module", file=sys.stderr)
+ sys.exit(1)
+
+ # Check that we got the local version of anyio by checking for the marker
+ if not hasattr(anyio, 'LOCAL_ANYIO_MARKER'):
+ print("Found system anyio instead of local anyio", file=sys.stderr)
+ sys.exit(1)
+ "#})?;
+ child.child("src/child/__init__.py").touch()?;
+
+ // Create a script that depends on the child package
+ let script = context.temp_dir.child("script.py");
+ script.write_str(&formatdoc! {r#"
+ # /// script
+ # requires-python = ">=3.12"
+ # dependencies = ["child"]
+ #
+ # [tool.uv.sources]
+ # anyio = {{ path = "{}" }}
+ # child = {{ path = "child" }}
+ #
+ # [tool.uv.extra-build-dependencies]
+ # child = ["anyio"]
+ # ///
+ "#, anyio_local.portable_display()
+ })?;
+
+ let filters = context
+ .filters()
+ .into_iter()
+ .chain(vec![(
+ r"environments-v2/script-[a-z0-9]+",
+ "environments-v2/script-[HASH]",
+ )])
+ .collect::>();
+
+ // Running `uv sync` should succeed with the sources applied
+ uv_snapshot!(filters, context.sync().arg("--script").arg("script.py"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Creating script environment at: [CACHE_DIR]/environments-v2/script-[HASH]
+ Resolved [N] packages in [TIME]
+ Prepared [N] packages in [TIME]
+ Installed [N] packages in [TIME]
+ + child==0.1.0 (from file://[TEMP_DIR]/child)
+ ");
+
+ Ok(())
+}
+
#[test]
fn virtual_no_build() -> Result<()> {
let context = TestContext::new("3.12");
diff --git a/docs/reference/settings.md b/docs/reference/settings.md
index 4a1d74b42..8062f227d 100644
--- a/docs/reference/settings.md
+++ b/docs/reference/settings.md
@@ -202,6 +202,28 @@ environments = ["sys_platform == 'darwin'"]
---
+### [`extra-build-dependencies`](#extra-build-dependencies) {: #extra-build-dependencies }
+
+Additional build dependencies for packages.
+
+This allows extending the PEP 517 build environment for the project's dependencies with
+additional packages. This is useful for packages that assume the presence of packages, like,
+`pip`, and do not declare them as build dependencies.
+
+**Default value**: `[]`
+
+**Type**: `dict`
+
+**Example usage**:
+
+```toml title="pyproject.toml"
+
+[tool.uv.extra-build-dependencies]
+pytest = ["pip"]
+```
+
+---
+
### [`index`](#index) {: #index }
The indexes to use when resolving dependencies.
@@ -1127,6 +1149,36 @@ Accepts package-date pairs in a dictionary format.
---
+### [`extra-build-dependencies`](#extra-build-dependencies) {: #extra-build-dependencies }
+
+Additional build dependencies for packages.
+
+This allows extending the PEP 517 build environment for the project's dependencies with
+additional packages. This is useful for packages that assume the presence of packages like
+`pip`, and do not declare them as build dependencies.
+
+**Default value**: `[]`
+
+**Type**: `dict`
+
+**Example usage**:
+
+=== "pyproject.toml"
+
+ ```toml
+ [tool.uv]
+ [extra-build-dependencies]
+ pytest = ["setuptools"]
+ ```
+=== "uv.toml"
+
+ ```toml
+ [extra-build-dependencies]
+ pytest = ["setuptools"]
+ ```
+
+---
+
### [`extra-index-url`](#extra-index-url) {: #extra-index-url }
Extra URLs of package indexes to use, in addition to `--index-url`.
@@ -2616,6 +2668,38 @@ Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources.
---
+#### [`extra-build-dependencies`](#pip_extra-build-dependencies) {: #pip_extra-build-dependencies }
+
+
+Additional build dependencies for packages.
+
+This allows extending the PEP 517 build environment for the project's dependencies with
+additional packages. This is useful for packages that assume the presence of packages like
+`pip`, and do not declare them as build dependencies.
+
+**Default value**: `[]`
+
+**Type**: `dict`
+
+**Example usage**:
+
+=== "pyproject.toml"
+
+ ```toml
+ [tool.uv.pip]
+ [extra-build-dependencies]
+ pytest = ["setuptools"]
+ ```
+=== "uv.toml"
+
+ ```toml
+ [pip]
+ [extra-build-dependencies]
+ pytest = ["setuptools"]
+ ```
+
+---
+
#### [`extra-index-url`](#pip_extra-index-url) {: #pip_extra-index-url }
diff --git a/scripts/packages/anyio_local/anyio/__init__.py b/scripts/packages/anyio_local/anyio/__init__.py
index e69de29bb..a3e374663 100644
--- a/scripts/packages/anyio_local/anyio/__init__.py
+++ b/scripts/packages/anyio_local/anyio/__init__.py
@@ -0,0 +1,2 @@
+# This is a local dummy anyio package
+LOCAL_ANYIO_MARKER = True
\ No newline at end of file
diff --git a/uv.schema.json b/uv.schema.json
index a2f3f0113..17ae72b1d 100644
--- a/uv.schema.json
+++ b/uv.schema.json
@@ -225,6 +225,17 @@
}
]
},
+ "extra-build-dependencies": {
+ "description": "Additional build dependencies for packages.\n\nThis allows extending the PEP 517 build environment for the project's dependencies with\nadditional packages. This is useful for packages that assume the presence of packages, like,\n`pip`, and do not declare them as build dependencies.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/ExtraBuildDependencies"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"extra-index-url": {
"description": "Extra URLs of package indexes to use, in addition to `--index-url`.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/)\n(the simple repository API), or a local directory laid out in the same format.\n\nAll indexes provided via this flag take priority over the index specified by\n[`index_url`](#index-url) or [`index`](#index) with `default = true`. When multiple indexes\nare provided, earlier values take priority.\n\nTo control uv's resolution strategy when multiple indexes are present, see\n[`index_strategy`](#index-strategy).\n\n(Deprecated: use `index` instead.)",
"type": [
@@ -873,6 +884,15 @@
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2}))?$"
},
+ "ExtraBuildDependencies": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Requirement"
+ }
+ }
+ },
"ExtraName": {
"description": "The normalized name of an extra dependency.\n\nConverts the name to lowercase and collapses runs of `-`, `_`, and `.` down to a single `-`.\nFor example, `---`, `.`, and `__` are all converted to a single `-`.\n\nSee:\n- \n- ",
"type": "string"
@@ -1315,6 +1335,17 @@
"$ref": "#/definitions/ExtraName"
}
},
+ "extra-build-dependencies": {
+ "description": "Additional build dependencies for packages.\n\nThis allows extending the PEP 517 build environment for the project's dependencies with\nadditional packages. This is useful for packages that assume the presence of packages like\n`pip`, and do not declare them as build dependencies.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/ExtraBuildDependencies"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"extra-index-url": {
"description": "Extra URLs of package indexes to use, in addition to `--index-url`.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/)\n(the simple repository API), or a local directory laid out in the same format.\n\nAll indexes provided via this flag take priority over the index specified by\n[`index_url`](#index-url). When multiple indexes are provided, earlier values take priority.\n\nTo control uv's resolution strategy when multiple indexes are present, see\n[`index_strategy`](#index-strategy).",
"type": [
From 9b8ff44a04139cd789033d2670cb842227104075 Mon Sep 17 00:00:00 2001
From: Charlie Marsh
Date: Wed, 30 Jul 2025 11:12:22 -0400
Subject: [PATCH 21/26] Perform wheel lockfile filtering based on platform and
OS intersection (#14976)
## Summary
Ensures that if the user filters to macOS ARM, we don't include macOS
x86_64 wheels.
Closes https://github.com/astral-sh/uv/issues/14901.
---
crates/uv-platform-tags/src/platform_tag.rs | 5 +
crates/uv-resolver/src/lock/mod.rs | 119 +++++++++++++++++++-
crates/uv/tests/it/lock.rs | 103 +++++++++++++++++
3 files changed, 224 insertions(+), 3 deletions(-)
diff --git a/crates/uv-platform-tags/src/platform_tag.rs b/crates/uv-platform-tags/src/platform_tag.rs
index 1162a83d7..4ee3ec672 100644
--- a/crates/uv-platform-tags/src/platform_tag.rs
+++ b/crates/uv-platform-tags/src/platform_tag.rs
@@ -105,6 +105,11 @@ impl PlatformTag {
}
impl PlatformTag {
+ /// Returns `true` if the platform is "any" (i.e., not specific to a platform).
+ pub fn is_any(&self) -> bool {
+ matches!(self, Self::Any)
+ }
+
/// Returns `true` if the platform is manylinux-only.
pub fn is_manylinux(&self) -> bool {
matches!(
diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs
index 44aa915e6..cb87fa2cc 100644
--- a/crates/uv-resolver/src/lock/mod.rs
+++ b/crates/uv-resolver/src/lock/mod.rs
@@ -106,6 +106,51 @@ static X86_MARKERS: LazyLock = LazyLock::new(|| {
.unwrap();
UniversalMarker::new(pep508, ConflictMarker::TRUE)
});
+static LINUX_ARM_MARKERS: LazyLock = LazyLock::new(|| {
+ let mut marker = *LINUX_MARKERS;
+ marker.and(*ARM_MARKERS);
+ marker
+});
+static LINUX_X86_64_MARKERS: LazyLock = LazyLock::new(|| {
+ let mut marker = *LINUX_MARKERS;
+ marker.and(*X86_64_MARKERS);
+ marker
+});
+static LINUX_X86_MARKERS: LazyLock = LazyLock::new(|| {
+ let mut marker = *LINUX_MARKERS;
+ marker.and(*X86_MARKERS);
+ marker
+});
+static WINDOWS_ARM_MARKERS: LazyLock = LazyLock::new(|| {
+ let mut marker = *WINDOWS_MARKERS;
+ marker.and(*ARM_MARKERS);
+ marker
+});
+static WINDOWS_X86_64_MARKERS: LazyLock = LazyLock::new(|| {
+ let mut marker = *WINDOWS_MARKERS;
+ marker.and(*X86_64_MARKERS);
+ marker
+});
+static WINDOWS_X86_MARKERS: LazyLock = LazyLock::new(|| {
+ let mut marker = *WINDOWS_MARKERS;
+ marker.and(*X86_MARKERS);
+ marker
+});
+static MAC_ARM_MARKERS: LazyLock = LazyLock::new(|| {
+ let mut marker = *MAC_MARKERS;
+ marker.and(*ARM_MARKERS);
+ marker
+});
+static MAC_X86_64_MARKERS: LazyLock = LazyLock::new(|| {
+ let mut marker = *MAC_MARKERS;
+ marker.and(*X86_64_MARKERS);
+ marker
+});
+static MAC_X86_MARKERS: LazyLock = LazyLock::new(|| {
+ let mut marker = *MAC_MARKERS;
+ marker.and(*X86_MARKERS);
+ marker
+});
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(try_from = "LockWire")]
@@ -336,14 +381,61 @@ impl Lock {
// a single disjointness check with the intersection is sufficient, so we have one
// constant per platform.
let platform_tags = wheel.filename.platform_tags();
+
+ if platform_tags.iter().all(PlatformTag::is_any) {
+ return true;
+ }
+
if platform_tags.iter().all(PlatformTag::is_linux) {
- if graph.graph[node_index].marker().is_disjoint(*LINUX_MARKERS) {
+ if platform_tags.iter().all(PlatformTag::is_arm) {
+ if graph.graph[node_index]
+ .marker()
+ .is_disjoint(*LINUX_ARM_MARKERS)
+ {
+ return false;
+ }
+ } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
+ if graph.graph[node_index]
+ .marker()
+ .is_disjoint(*LINUX_X86_64_MARKERS)
+ {
+ return false;
+ }
+ } else if platform_tags.iter().all(PlatformTag::is_x86) {
+ if graph.graph[node_index]
+ .marker()
+ .is_disjoint(*LINUX_X86_MARKERS)
+ {
+ return false;
+ }
+ } else if graph.graph[node_index].marker().is_disjoint(*LINUX_MARKERS) {
return false;
}
}
if platform_tags.iter().all(PlatformTag::is_windows) {
- if graph.graph[node_index]
+ if platform_tags.iter().all(PlatformTag::is_arm) {
+ if graph.graph[node_index]
+ .marker()
+ .is_disjoint(*WINDOWS_ARM_MARKERS)
+ {
+ return false;
+ }
+ } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
+ if graph.graph[node_index]
+ .marker()
+ .is_disjoint(*WINDOWS_X86_64_MARKERS)
+ {
+ return false;
+ }
+ } else if platform_tags.iter().all(PlatformTag::is_x86) {
+ if graph.graph[node_index]
+ .marker()
+ .is_disjoint(*WINDOWS_X86_MARKERS)
+ {
+ return false;
+ }
+ } else if graph.graph[node_index]
.marker()
.is_disjoint(*WINDOWS_MARKERS)
{
@@ -352,7 +444,28 @@ impl Lock {
}
if platform_tags.iter().all(PlatformTag::is_macos) {
- if graph.graph[node_index].marker().is_disjoint(*MAC_MARKERS) {
+ if platform_tags.iter().all(PlatformTag::is_arm) {
+ if graph.graph[node_index]
+ .marker()
+ .is_disjoint(*MAC_ARM_MARKERS)
+ {
+ return false;
+ }
+ } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
+ if graph.graph[node_index]
+ .marker()
+ .is_disjoint(*MAC_X86_64_MARKERS)
+ {
+ return false;
+ }
+ } else if platform_tags.iter().all(PlatformTag::is_x86) {
+ if graph.graph[node_index]
+ .marker()
+ .is_disjoint(*MAC_X86_MARKERS)
+ {
+ return false;
+ }
+ } else if graph.graph[node_index].marker().is_disjoint(*MAC_MARKERS) {
return false;
}
}
diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs
index acc36eacf..71cdffd52 100644
--- a/crates/uv/tests/it/lock.rs
+++ b/crates/uv/tests/it/lock.rs
@@ -30040,3 +30040,106 @@ fn lock_circular_path_dependency_explicit_index() -> Result<()> {
Ok(())
}
+
+#[test]
+fn lock_required_intersection() -> Result<()> {
+ let context = TestContext::new("3.12");
+
+ let pyproject_toml = context.temp_dir.child("pyproject.toml");
+ pyproject_toml.write_str(
+ r#"
+ [project]
+ name = "project"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = [
+ "numpy",
+ ]
+
+ [tool.uv]
+ environments = [
+ "(sys_platform=='linux' and platform_machine=='x86_64')",
+ "(platform_machine=='arm64' and sys_platform=='darwin')"
+ ]
+ "#,
+ )?;
+
+ uv_snapshot!(context.filters(), context.lock(), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Resolved 2 packages in [TIME]
+ ");
+
+ let lock = context.read("uv.lock");
+
+ insta::with_settings!({
+ filters => context.filters(),
+ }, {
+ assert_snapshot!(
+ lock, @r#"
+ version = 1
+ revision = 3
+ requires-python = ">=3.12"
+ resolution-markers = [
+ "platform_machine == 'x86_64' and sys_platform == 'linux'",
+ "platform_machine == 'arm64' and sys_platform == 'darwin'",
+ ]
+ supported-markers = [
+ "platform_machine == 'x86_64' and sys_platform == 'linux'",
+ "platform_machine == 'arm64' and sys_platform == 'darwin'",
+ ]
+
+ [options]
+ exclude-newer = "2024-03-25T00:00:00Z"
+
+ [[package]]
+ name = "numpy"
+ version = "1.26.4"
+ source = { registry = "https://pypi.org/simple" }
+ sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" }
+ wheels = [
+ { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" },
+ { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" },
+ ]
+
+ [[package]]
+ name = "project"
+ version = "0.1.0"
+ source = { virtual = "." }
+ dependencies = [
+ { name = "numpy", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+ ]
+
+ [package.metadata]
+ requires-dist = [{ name = "numpy" }]
+ "#
+ );
+ });
+
+ // Re-run with `--locked`.
+ uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Resolved 2 packages in [TIME]
+ ");
+
+ // Re-run with `--offline`. We shouldn't need a network connection to validate an
+ // already-correct lockfile with immutable metadata.
+ uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Resolved 2 packages in [TIME]
+ "###);
+
+ Ok(())
+}
From a76e538aa5951dd1057cca8e57487b82cdbb75c2 Mon Sep 17 00:00:00 2001
From: Charlie Marsh
Date: Wed, 30 Jul 2025 11:26:44 -0400
Subject: [PATCH 22/26] Extend wheel filtering to Android tags (#14977)
## Summary
Just while I'm here for https://github.com/astral-sh/uv/pull/14976.
---
crates/uv-platform-tags/src/platform_tag.rs | 5 ++
crates/uv-resolver/src/lock/mod.rs | 49 +++++++++++
crates/uv/tests/it/lock.rs | 97 +++++++++++++++++++++
3 files changed, 151 insertions(+)
diff --git a/crates/uv-platform-tags/src/platform_tag.rs b/crates/uv-platform-tags/src/platform_tag.rs
index 4ee3ec672..93a614bdf 100644
--- a/crates/uv-platform-tags/src/platform_tag.rs
+++ b/crates/uv-platform-tags/src/platform_tag.rs
@@ -139,6 +139,11 @@ impl PlatformTag {
matches!(self, Self::Macos { .. })
}
+ /// Returns `true` if the platform is Android-only.
+ pub fn is_android(&self) -> bool {
+ matches!(self, Self::Android { .. })
+ }
+
/// Returns `true` if the platform is Windows-only.
pub fn is_windows(&self) -> bool {
matches!(
diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs
index cb87fa2cc..b68336fd7 100644
--- a/crates/uv-resolver/src/lock/mod.rs
+++ b/crates/uv-resolver/src/lock/mod.rs
@@ -87,6 +87,10 @@ static MAC_MARKERS: LazyLock = LazyLock::new(|| {
let pep508 = MarkerTree::from_str("os_name == 'posix' and sys_platform == 'darwin'").unwrap();
UniversalMarker::new(pep508, ConflictMarker::TRUE)
});
+static ANDROID_MARKERS: LazyLock = LazyLock::new(|| {
+ let pep508 = MarkerTree::from_str("sys_platform == 'android'").unwrap();
+ UniversalMarker::new(pep508, ConflictMarker::TRUE)
+});
static ARM_MARKERS: LazyLock = LazyLock::new(|| {
let pep508 =
MarkerTree::from_str("platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'ARM64'")
@@ -151,6 +155,21 @@ static MAC_X86_MARKERS: LazyLock = LazyLock::new(|| {
marker.and(*X86_MARKERS);
marker
});
+static ANDROID_ARM_MARKERS: LazyLock = LazyLock::new(|| {
+ let mut marker = *ANDROID_MARKERS;
+ marker.and(*ARM_MARKERS);
+ marker
+});
+static ANDROID_X86_64_MARKERS: LazyLock = LazyLock::new(|| {
+ let mut marker = *ANDROID_MARKERS;
+ marker.and(*X86_64_MARKERS);
+ marker
+});
+static ANDROID_X86_MARKERS: LazyLock = LazyLock::new(|| {
+ let mut marker = *ANDROID_MARKERS;
+ marker.and(*X86_MARKERS);
+ marker
+});
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(try_from = "LockWire")]
@@ -470,6 +489,36 @@ impl Lock {
}
}
+ if platform_tags.iter().all(PlatformTag::is_android) {
+ if platform_tags.iter().all(PlatformTag::is_arm) {
+ if graph.graph[node_index]
+ .marker()
+ .is_disjoint(*ANDROID_ARM_MARKERS)
+ {
+ return false;
+ }
+ } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
+ if graph.graph[node_index]
+ .marker()
+ .is_disjoint(*ANDROID_X86_64_MARKERS)
+ {
+ return false;
+ }
+ } else if platform_tags.iter().all(PlatformTag::is_x86) {
+ if graph.graph[node_index]
+ .marker()
+ .is_disjoint(*ANDROID_X86_MARKERS)
+ {
+ return false;
+ }
+ } else if graph.graph[node_index]
+ .marker()
+ .is_disjoint(*ANDROID_MARKERS)
+ {
+ return false;
+ }
+ }
+
if platform_tags.iter().all(PlatformTag::is_arm) {
if graph.graph[node_index].marker().is_disjoint(*ARM_MARKERS) {
return false;
diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs
index 71cdffd52..5e9542fa6 100644
--- a/crates/uv/tests/it/lock.rs
+++ b/crates/uv/tests/it/lock.rs
@@ -30041,6 +30041,103 @@ fn lock_circular_path_dependency_explicit_index() -> Result<()> {
Ok(())
}
+#[test]
+fn lock_android() -> Result<()> {
+ let context = TestContext::new("3.12").with_exclude_newer("2025-06-01T00:00:00Z");
+
+ let pyproject_toml = context.temp_dir.child("pyproject.toml");
+ pyproject_toml.write_str(
+ r#"
+ [project]
+ name = "project"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = [
+ "deltachat-rpc-server",
+ ]
+
+ [tool.uv]
+ environments = ["sys_platform == 'android'"]
+ "#,
+ )?;
+
+ uv_snapshot!(context.filters(), context.lock(), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Resolved 2 packages in [TIME]
+ ");
+
+ let lock = context.read("uv.lock");
+
+ insta::with_settings!({
+ filters => context.filters(),
+ }, {
+ assert_snapshot!(
+ lock, @r#"
+ version = 1
+ revision = 3
+ requires-python = ">=3.12"
+ resolution-markers = [
+ "sys_platform == 'android'",
+ ]
+ supported-markers = [
+ "sys_platform == 'android'",
+ ]
+
+ [options]
+ exclude-newer = "2025-06-01T00:00:00Z"
+
+ [[package]]
+ name = "deltachat-rpc-server"
+ version = "1.159.5"
+ source = { registry = "https://pypi.org/simple" }
+ sdist = { url = "https://files.pythonhosted.org/packages/c4/59/fd0dee6b1c950ba5c93e02ed6692990bffd6e843710f0c1b547de661534b/deltachat_rpc_server-1.159.5.tar.gz", hash = "sha256:7e015f9f8a8400133648971049032851c560729c6e9807f865a3f026b74b13a0", size = 1471, upload-time = "2025-05-14T17:35:33.428Z" }
+ wheels = [
+ { url = "https://files.pythonhosted.org/packages/c0/47/97e67319025afedb8cc5fc4e8e3779ef407836dbe2c111eeb403a7a83e8c/deltachat_rpc_server-1.159.5-py3-none-android_21_arm64_v8a.whl", hash = "sha256:3fb08568e12984cb2fc85409d6bc5bfa5b965b834c5d45fecd2f63ad3893396c", size = 10495646, upload-time = "2025-05-14T17:35:07.189Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/93/d470afef50ddd4b101d78401f8aaf5adbaa7c27a984a151017bc3c449171/deltachat_rpc_server-1.159.5-py3-none-android_21_armeabi_v7a.whl", hash = "sha256:560178de3f61dc9ef1c69b7d9bd238b45b07b98331d85d074d8652babac9de49", size = 8821626, upload-time = "2025-05-14T17:35:09.645Z" },
+ ]
+
+ [[package]]
+ name = "project"
+ version = "0.1.0"
+ source = { virtual = "." }
+ dependencies = [
+ { name = "deltachat-rpc-server", marker = "sys_platform == 'android'" },
+ ]
+
+ [package.metadata]
+ requires-dist = [{ name = "deltachat-rpc-server" }]
+ "#
+ );
+ });
+
+ // Re-run with `--locked`.
+ uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Resolved 2 packages in [TIME]
+ ");
+
+ // Re-run with `--offline`. We shouldn't need a network connection to validate an
+ // already-correct lockfile with immutable metadata.
+ uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Resolved 2 packages in [TIME]
+ "###);
+
+ Ok(())
+}
+
#[test]
fn lock_required_intersection() -> Result<()> {
let context = TestContext::new("3.12");
From c9d3d60a189ef087c8ce82a4a85ff9e181e254c7 Mon Sep 17 00:00:00 2001
From: Zanie Blue
Date: Wed, 30 Jul 2025 10:44:06 -0500
Subject: [PATCH 23/26] Implement `CacheKey` for all `Pep508Url` variants
(#14978)
Closes #14973
---
Cargo.lock | 1 +
crates/uv-pep508/src/lib.rs | 9 +++------
crates/uv-pep508/src/verbatim_url.rs | 7 +++++++
crates/uv-pypi-types/Cargo.toml | 1 +
crates/uv-pypi-types/src/parsed_url.rs | 7 +++++++
5 files changed, 19 insertions(+), 6 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 396b77bea..37cc267e1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5646,6 +5646,7 @@ dependencies = [
"toml_edit",
"tracing",
"url",
+ "uv-cache-key",
"uv-distribution-filename",
"uv-git-types",
"uv-normalize",
diff --git a/crates/uv-pep508/src/lib.rs b/crates/uv-pep508/src/lib.rs
index dd516f570..9167d0964 100644
--- a/crates/uv-pep508/src/lib.rs
+++ b/crates/uv-pep508/src/lib.rs
@@ -252,10 +252,7 @@ impl Serialize for Requirement {
}
}
-impl CacheKey for Requirement
-where
- T: Display,
-{
+impl CacheKey for Requirement {
fn cache_key(&self, state: &mut CacheKeyHasher) {
self.name.as_str().cache_key(state);
@@ -280,7 +277,7 @@ where
}
VersionOrUrl::Url(url) => {
1u8.cache_key(state);
- url.to_string().cache_key(state);
+ url.cache_key(state);
}
}
} else {
@@ -330,7 +327,7 @@ impl Requirement {
}
/// Type to parse URLs from `name @ ` into. Defaults to [`Url`].
-pub trait Pep508Url: Display + Debug + Sized {
+pub trait Pep508Url: Display + Debug + Sized + CacheKey {
/// String to URL parsing error
type Err: Error + Debug;
diff --git a/crates/uv-pep508/src/verbatim_url.rs b/crates/uv-pep508/src/verbatim_url.rs
index 9f0e9a5ee..480d4fb67 100644
--- a/crates/uv-pep508/src/verbatim_url.rs
+++ b/crates/uv-pep508/src/verbatim_url.rs
@@ -10,6 +10,7 @@ use arcstr::ArcStr;
use regex::Regex;
use thiserror::Error;
use url::{ParseError, Url};
+use uv_cache_key::{CacheKey, CacheKeyHasher};
#[cfg_attr(not(feature = "non-pep508-extensions"), allow(unused_imports))]
use uv_fs::{normalize_absolute_path, normalize_url_path};
@@ -37,6 +38,12 @@ impl Hash for VerbatimUrl {
}
}
+impl CacheKey for VerbatimUrl {
+ fn cache_key(&self, state: &mut CacheKeyHasher) {
+ self.url.as_str().cache_key(state);
+ }
+}
+
impl PartialEq for VerbatimUrl {
fn eq(&self, other: &Self) -> bool {
self.url == other.url
diff --git a/crates/uv-pypi-types/Cargo.toml b/crates/uv-pypi-types/Cargo.toml
index 31a532d6e..e5ceef631 100644
--- a/crates/uv-pypi-types/Cargo.toml
+++ b/crates/uv-pypi-types/Cargo.toml
@@ -16,6 +16,7 @@ doctest = false
workspace = true
[dependencies]
+uv-cache-key = { workspace = true }
uv-distribution-filename = { workspace = true }
uv-git-types = { workspace = true }
uv-normalize = { workspace = true }
diff --git a/crates/uv-pypi-types/src/parsed_url.rs b/crates/uv-pypi-types/src/parsed_url.rs
index 57afbcdf9..3b3b21f17 100644
--- a/crates/uv-pypi-types/src/parsed_url.rs
+++ b/crates/uv-pypi-types/src/parsed_url.rs
@@ -3,6 +3,7 @@ use std::path::{Path, PathBuf};
use thiserror::Error;
use url::{ParseError, Url};
+use uv_cache_key::{CacheKey, CacheKeyHasher};
use uv_distribution_filename::{DistExtension, ExtensionError};
use uv_git_types::{GitUrl, GitUrlParseError};
@@ -45,6 +46,12 @@ pub struct VerbatimParsedUrl {
pub verbatim: VerbatimUrl,
}
+impl CacheKey for VerbatimParsedUrl {
+ fn cache_key(&self, state: &mut CacheKeyHasher) {
+ self.verbatim.cache_key(state);
+ }
+}
+
impl VerbatimParsedUrl {
/// Returns `true` if the URL is editable.
pub fn is_editable(&self) -> bool {
From 630394476eb30abb025bd4b1d1e42ea37771614d Mon Sep 17 00:00:00 2001
From: Zanie Blue
Date: Wed, 30 Jul 2025 11:00:16 -0500
Subject: [PATCH 24/26] Copy entrypoints that have a shebang that differs in
`python` vs `python3` (#14970)
In https://github.com/astral-sh/uv/issues/14919 it was reported that
uv's behavior differed after the first invocation. I noticed we weren't
copying entrypoints after the first invocation. It turns out the
shebangs were written with `.../python` but on a subsequent invocation
the `sys.executable` was `.../python3` so we didn't detect these as
matching.
This is a pretty naive fix, but it seems much easier than ensuring the
entry point path exactly matches the subsequent `sys.executable` we
find.
I guess we should fix this in reverse too? but I think we might always
prefer `python3` when loading interpreters from environments.
See #14790 for more background.
---
crates/uv/src/commands/project/run.rs | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs
index 65fb62ef7..be9ac3783 100644
--- a/crates/uv/src/commands/project/run.rs
+++ b/crates/uv/src/commands/project/run.rs
@@ -1806,8 +1806,15 @@ fn copy_entrypoint(
' '''
"#,
)
- // Or an absolute path shebang
+ // Or, an absolute path shebang
.or_else(|| contents.strip_prefix(&format!("#!{}\n", previous_executable.display())))
+ // If the previous executable ends with `python3`, check for a shebang with `python` too
+ .or_else(|| {
+ previous_executable
+ .to_str()
+ .and_then(|path| path.strip_suffix("3"))
+ .and_then(|path| contents.strip_prefix(&format!("#!{path}\n")))
+ })
else {
// If it's not a Python shebang, we'll skip it
trace!(
From e176e17144fb6e4ec010f56a7c8fa098b66ba80b Mon Sep 17 00:00:00 2001
From: Zanie Blue
Date: Wed, 30 Jul 2025 11:24:20 -0500
Subject: [PATCH 25/26] Bump version to 0.8.4 (#14980)
---
CHANGELOG.md | 32 +++++++++++++++++++++++++++
Cargo.lock | 6 ++---
crates/uv-build/Cargo.toml | 2 +-
crates/uv-build/pyproject.toml | 2 +-
crates/uv-version/Cargo.toml | 2 +-
crates/uv/Cargo.toml | 2 +-
docs/concepts/build-backend.md | 2 +-
docs/concepts/projects/init.md | 6 ++---
docs/concepts/projects/workspaces.md | 6 ++---
docs/getting-started/installation.md | 4 ++--
docs/guides/integration/aws-lambda.md | 4 ++--
docs/guides/integration/docker.md | 10 ++++-----
docs/guides/integration/github.md | 2 +-
docs/guides/integration/pre-commit.md | 10 ++++-----
pyproject.toml | 2 +-
15 files changed, 62 insertions(+), 30 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab84b82c1..870f70bdd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,38 @@
+## 0.8.4
+
+### Enhancements
+
+- Improve styling of warning cause chains ([#14934](https://github.com/astral-sh/uv/pull/14934))
+- Extend wheel filtering to Android tags ([#14977](https://github.com/astral-sh/uv/pull/14977))
+- Perform wheel lockfile filtering based on platform and OS intersection ([#14976](https://github.com/astral-sh/uv/pull/14976))
+- Clarify messaging when a new resolution needs to be performed ([#14938](https://github.com/astral-sh/uv/pull/14938))
+
+### Preview features
+
+- Add support for extending package's build dependencies with `extra-build-dependencies` ([#14735](https://github.com/astral-sh/uv/pull/14735))
+- Split preview mode into separate feature flags ([#14823](https://github.com/astral-sh/uv/pull/14823))
+
+### Configuration
+
+- Add support for package specific `exclude-newer` dates via `exclude-newer-package` ([#14489](https://github.com/astral-sh/uv/pull/14489))
+
+### Bug fixes
+
+- Avoid invalidating lockfile when path or workspace dependencies define explicit indexes ([#14876](https://github.com/astral-sh/uv/pull/14876))
+- Copy entrypoints that have a shebang that differs in `python` vs `python3` ([#14970](https://github.com/astral-sh/uv/pull/14970))
+- Fix incorrect file permissions in wheel packages ([#14930](https://github.com/astral-sh/uv/pull/14930))
+- Update validation for `environments` and `required-environments` in `uv.toml` ([#14905](https://github.com/astral-sh/uv/pull/14905))
+
+### Documentation
+
+- Show `uv_build` in projects documentation ([#14968](https://github.com/astral-sh/uv/pull/14968))
+- Add `UV_` prefix to installer environment variables ([#14964](https://github.com/astral-sh/uv/pull/14964))
+- Un-hide `uv` from `--build-backend` options ([#14939](https://github.com/astral-sh/uv/pull/14939))
+- Update documentation for preview flags ([#14902](https://github.com/astral-sh/uv/pull/14902))
+
## 0.8.3
### Python
diff --git a/Cargo.lock b/Cargo.lock
index 37cc267e1..cf19b6c4b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4655,7 +4655,7 @@ dependencies = [
[[package]]
name = "uv"
-version = "0.8.3"
+version = "0.8.4"
dependencies = [
"anstream",
"anyhow",
@@ -4822,7 +4822,7 @@ dependencies = [
[[package]]
name = "uv-build"
-version = "0.8.3"
+version = "0.8.4"
dependencies = [
"anyhow",
"uv-build-backend",
@@ -6039,7 +6039,7 @@ dependencies = [
[[package]]
name = "uv-version"
-version = "0.8.3"
+version = "0.8.4"
[[package]]
name = "uv-virtualenv"
diff --git a/crates/uv-build/Cargo.toml b/crates/uv-build/Cargo.toml
index 3e80beb1f..fc6145501 100644
--- a/crates/uv-build/Cargo.toml
+++ b/crates/uv-build/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "uv-build"
-version = "0.8.3"
+version = "0.8.4"
edition.workspace = true
rust-version.workspace = true
homepage.workspace = true
diff --git a/crates/uv-build/pyproject.toml b/crates/uv-build/pyproject.toml
index 970950ebc..a46c8a5ea 100644
--- a/crates/uv-build/pyproject.toml
+++ b/crates/uv-build/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "uv-build"
-version = "0.8.3"
+version = "0.8.4"
description = "The uv build backend"
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
requires-python = ">=3.8"
diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml
index 9fa7125e1..64179e3c7 100644
--- a/crates/uv-version/Cargo.toml
+++ b/crates/uv-version/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "uv-version"
-version = "0.8.3"
+version = "0.8.4"
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }
diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml
index f37e8c2f0..e188ecdf6 100644
--- a/crates/uv/Cargo.toml
+++ b/crates/uv/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "uv"
-version = "0.8.3"
+version = "0.8.4"
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }
diff --git a/docs/concepts/build-backend.md b/docs/concepts/build-backend.md
index e7288492f..b4c685146 100644
--- a/docs/concepts/build-backend.md
+++ b/docs/concepts/build-backend.md
@@ -31,7 +31,7 @@ To use uv as a build backend in an existing project, add `uv_build` to the
```toml title="pyproject.toml"
[build-system]
-requires = ["uv_build>=0.8.3,<0.9.0"]
+requires = ["uv_build>=0.8.4,<0.9.0"]
build-backend = "uv_build"
```
diff --git a/docs/concepts/projects/init.md b/docs/concepts/projects/init.md
index 1a012393e..7085b5a35 100644
--- a/docs/concepts/projects/init.md
+++ b/docs/concepts/projects/init.md
@@ -111,7 +111,7 @@ dependencies = []
example-pkg = "example_pkg:main"
[build-system]
-requires = ["uv_build>=0.8.3,<0.9.0"]
+requires = ["uv_build>=0.8.4,<0.9.0"]
build-backend = "uv_build"
```
@@ -134,7 +134,7 @@ dependencies = []
example-pkg = "example_pkg:main"
[build-system]
-requires = ["uv_build>=0.8.3,<0.9.0"]
+requires = ["uv_build>=0.8.4,<0.9.0"]
build-backend = "uv_build"
```
@@ -195,7 +195,7 @@ requires-python = ">=3.11"
dependencies = []
[build-system]
-requires = ["uv_build>=0.8.3,<0.9.0"]
+requires = ["uv_build>=0.8.4,<0.9.0"]
build-backend = "uv_build"
```
diff --git a/docs/concepts/projects/workspaces.md b/docs/concepts/projects/workspaces.md
index 641b4d21f..d31751b04 100644
--- a/docs/concepts/projects/workspaces.md
+++ b/docs/concepts/projects/workspaces.md
@@ -75,7 +75,7 @@ bird-feeder = { workspace = true }
members = ["packages/*"]
[build-system]
-requires = ["uv_build>=0.8.3,<0.9.0"]
+requires = ["uv_build>=0.8.4,<0.9.0"]
build-backend = "uv_build"
```
@@ -106,7 +106,7 @@ tqdm = { git = "https://github.com/tqdm/tqdm" }
members = ["packages/*"]
[build-system]
-requires = ["uv_build>=0.8.3,<0.9.0"]
+requires = ["uv_build>=0.8.4,<0.9.0"]
build-backend = "uv_build"
```
@@ -188,7 +188,7 @@ dependencies = ["bird-feeder", "tqdm>=4,<5"]
bird-feeder = { path = "packages/bird-feeder" }
[build-system]
-requires = ["uv_build>=0.8.3,<0.9.0"]
+requires = ["uv_build>=0.8.4,<0.9.0"]
build-backend = "uv_build"
```
diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md
index 2688c418b..7a3cab208 100644
--- a/docs/getting-started/installation.md
+++ b/docs/getting-started/installation.md
@@ -25,7 +25,7 @@ uv provides a standalone installer to download and install uv:
Request a specific version by including it in the URL:
```console
- $ curl -LsSf https://astral.sh/uv/0.8.3/install.sh | sh
+ $ curl -LsSf https://astral.sh/uv/0.8.4/install.sh | sh
```
=== "Windows"
@@ -41,7 +41,7 @@ uv provides a standalone installer to download and install uv:
Request a specific version by including it in the URL:
```pwsh-session
- PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.8.3/install.ps1 | iex"
+ PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.8.4/install.ps1 | iex"
```
!!! tip
diff --git a/docs/guides/integration/aws-lambda.md b/docs/guides/integration/aws-lambda.md
index 9077cb96e..a82e402a1 100644
--- a/docs/guides/integration/aws-lambda.md
+++ b/docs/guides/integration/aws-lambda.md
@@ -92,7 +92,7 @@ the second stage, we'll copy this directory over to the final image, omitting th
other unnecessary files.
```dockerfile title="Dockerfile"
-FROM ghcr.io/astral-sh/uv:0.8.3 AS uv
+FROM ghcr.io/astral-sh/uv:0.8.4 AS uv
# First, bundle the dependencies into the task root.
FROM public.ecr.aws/lambda/python:3.13 AS builder
@@ -334,7 +334,7 @@ And confirm that opening http://127.0.0.1:8000/ in a web browser displays, "Hell
Finally, we'll update the Dockerfile to include the local library in the deployment package:
```dockerfile title="Dockerfile"
-FROM ghcr.io/astral-sh/uv:0.8.3 AS uv
+FROM ghcr.io/astral-sh/uv:0.8.4 AS uv
# First, bundle the dependencies into the task root.
FROM public.ecr.aws/lambda/python:3.13 AS builder
diff --git a/docs/guides/integration/docker.md b/docs/guides/integration/docker.md
index 4ebd34ca1..cf317576b 100644
--- a/docs/guides/integration/docker.md
+++ b/docs/guides/integration/docker.md
@@ -31,7 +31,7 @@ $ docker run --rm -it ghcr.io/astral-sh/uv:debian uv --help
The following distroless images are available:
- `ghcr.io/astral-sh/uv:latest`
-- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.8.3`
+- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.8.4`
- `ghcr.io/astral-sh/uv:{major}.{minor}`, e.g., `ghcr.io/astral-sh/uv:0.8` (the latest patch
version)
@@ -75,7 +75,7 @@ And the following derived images are available:
As with the distroless image, each derived image is published with uv version tags as
`ghcr.io/astral-sh/uv:{major}.{minor}.{patch}-{base}` and
-`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.8.3-alpine`.
+`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.8.4-alpine`.
In addition, starting with `0.8` each derived image also sets `UV_TOOL_BIN_DIR` to `/usr/local/bin`
to allow `uv tool install` to work as expected with the default user.
@@ -116,7 +116,7 @@ Note this requires `curl` to be available.
In either case, it is best practice to pin to a specific uv version, e.g., with:
```dockerfile
-COPY --from=ghcr.io/astral-sh/uv:0.8.3 /uv /uvx /bin/
+COPY --from=ghcr.io/astral-sh/uv:0.8.4 /uv /uvx /bin/
```
!!! tip
@@ -134,7 +134,7 @@ COPY --from=ghcr.io/astral-sh/uv:0.8.3 /uv /uvx /bin/
Or, with the installer:
```dockerfile
-ADD https://astral.sh/uv/0.8.3/install.sh /uv-installer.sh
+ADD https://astral.sh/uv/0.8.4/install.sh /uv-installer.sh
```
### Installing a project
@@ -560,5 +560,5 @@ Verified OK
!!! tip
These examples use `latest`, but best practice is to verify the attestation for a specific
- version tag, e.g., `ghcr.io/astral-sh/uv:0.8.3`, or (even better) the specific image digest,
+ version tag, e.g., `ghcr.io/astral-sh/uv:0.8.4`, or (even better) the specific image digest,
such as `ghcr.io/astral-sh/uv:0.5.27@sha256:5adf09a5a526f380237408032a9308000d14d5947eafa687ad6c6a2476787b4f`.
diff --git a/docs/guides/integration/github.md b/docs/guides/integration/github.md
index 582ac344d..1852a9b18 100644
--- a/docs/guides/integration/github.md
+++ b/docs/guides/integration/github.md
@@ -47,7 +47,7 @@ jobs:
uses: astral-sh/setup-uv@v6
with:
# Install a specific version of uv.
- version: "0.8.3"
+ version: "0.8.4"
```
## Setting up Python
diff --git a/docs/guides/integration/pre-commit.md b/docs/guides/integration/pre-commit.md
index 7bf393871..8ecec60a3 100644
--- a/docs/guides/integration/pre-commit.md
+++ b/docs/guides/integration/pre-commit.md
@@ -19,7 +19,7 @@ To make sure your `uv.lock` file is up to date even if your `pyproject.toml` fil
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
- rev: 0.8.3
+ rev: 0.8.4
hooks:
- id: uv-lock
```
@@ -30,7 +30,7 @@ To keep a `requirements.txt` file in sync with your `uv.lock` file:
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
- rev: 0.8.3
+ rev: 0.8.4
hooks:
- id: uv-export
```
@@ -41,7 +41,7 @@ To compile requirements files:
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
- rev: 0.8.3
+ rev: 0.8.4
hooks:
# Compile requirements
- id: pip-compile
@@ -54,7 +54,7 @@ To compile alternative requirements files, modify `args` and `files`:
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
- rev: 0.8.3
+ rev: 0.8.4
hooks:
# Compile requirements
- id: pip-compile
@@ -68,7 +68,7 @@ To run the hook over multiple files at the same time, add additional entries:
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
- rev: 0.8.3
+ rev: 0.8.4
hooks:
# Compile requirements
- id: pip-compile
diff --git a/pyproject.toml b/pyproject.toml
index a88ff8554..a6268870e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "uv"
-version = "0.8.3"
+version = "0.8.4"
description = "An extremely fast Python package and project manager, written in Rust."
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
requires-python = ">=3.8"
From 3df972f18a0fca92c278a0ec1492e9c29314118d Mon Sep 17 00:00:00 2001
From: Aaron Ang <67321817+aaron-ang@users.noreply.github.com>
Date: Wed, 30 Jul 2025 12:50:24 -0700
Subject: [PATCH 26/26] Support installing additional executables in `uv tool
install` (#14014)
Close #6314
## Summary
Continuing from #7592. Created a new PR to rebase the old branch with
`main`, cleaned up test errors, and improved readability.
## Test Plan
Same test cases as in #7592.
---------
Co-authored-by: Zanie Blue
---
crates/uv-cli/src/lib.rs | 4 +
crates/uv-tool/src/tool.rs | 19 +-
crates/uv/src/commands/tool/common.rs | 261 +++++++++++++----------
crates/uv/src/commands/tool/install.rs | 56 ++---
crates/uv/src/commands/tool/upgrade.rs | 10 +-
crates/uv/src/lib.rs | 37 +++-
crates/uv/src/settings.rs | 6 +
crates/uv/tests/it/show_settings.rs | 1 +
crates/uv/tests/it/tool_install.rs | 274 ++++++++++++++++++-------
crates/uv/tests/it/tool_list.rs | 16 +-
crates/uv/tests/it/tool_upgrade.rs | 68 ++++++
docs/concepts/tools.md | 34 +++
docs/guides/tools.md | 8 +
docs/reference/cli.md | 1 +
14 files changed, 567 insertions(+), 228 deletions(-)
diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs
index e5bd3b0e2..f80abc06d 100644
--- a/crates/uv-cli/src/lib.rs
+++ b/crates/uv-cli/src/lib.rs
@@ -4490,6 +4490,10 @@ pub struct ToolInstallArgs {
#[arg(long)]
pub with_editable: Vec,
+ /// Install executables from the following packages.
+ #[arg(long)]
+ pub with_executables_from: Vec,
+
/// Constrain versions using the given requirements files.
///
/// Constraints files are `requirements.txt`-like files that only control the _version_ of a
diff --git a/crates/uv-tool/src/tool.rs b/crates/uv-tool/src/tool.rs
index cce3a2f58..0fbf59e22 100644
--- a/crates/uv-tool/src/tool.rs
+++ b/crates/uv-tool/src/tool.rs
@@ -103,6 +103,7 @@ impl TryFrom for Tool {
pub struct ToolEntrypoint {
pub name: String,
pub install_path: PathBuf,
+ pub from: Option,
}
impl Display for ToolEntrypoint {
@@ -166,10 +167,10 @@ impl Tool {
overrides: Vec,
build_constraints: Vec,
python: Option,
- entrypoints: impl Iterator- ,
+ entrypoints: impl IntoIterator
- ,
options: ToolOptions,
) -> Self {
- let mut entrypoints: Vec<_> = entrypoints.collect();
+ let mut entrypoints: Vec<_> = entrypoints.into_iter().collect();
entrypoints.sort();
Self {
requirements,
@@ -345,8 +346,15 @@ impl Tool {
impl ToolEntrypoint {
/// Create a new [`ToolEntrypoint`].
- pub fn new(name: String, install_path: PathBuf) -> Self {
- Self { name, install_path }
+ pub fn new(name: &str, install_path: PathBuf, from: String) -> Self {
+ let name = name
+ .trim_end_matches(std::env::consts::EXE_SUFFIX)
+ .to_string();
+ Self {
+ name,
+ install_path,
+ from: Some(from),
+ }
}
/// Returns the TOML table for this entrypoint.
@@ -358,6 +366,9 @@ impl ToolEntrypoint {
// Use cross-platform slashes so the toml string type does not change
value(PortablePath::from(&self.install_path).to_string()),
);
+ if let Some(from) = &self.from {
+ table.insert("from", value(from));
+ }
table
}
}
diff --git a/crates/uv/src/commands/tool/common.rs b/crates/uv/src/commands/tool/common.rs
index 5647afa32..8a45a3153 100644
--- a/crates/uv/src/commands/tool/common.rs
+++ b/crates/uv/src/commands/tool/common.rs
@@ -1,9 +1,12 @@
use anyhow::{Context, bail};
use itertools::Itertools;
use owo_colors::OwoColorize;
-use std::collections::Bound;
-use std::fmt::Write;
-use std::{collections::BTreeSet, ffi::OsString};
+use std::{
+ collections::{BTreeSet, Bound},
+ ffi::OsString,
+ fmt::Write,
+ path::Path,
+};
use tracing::{debug, warn};
use uv_cache::Cache;
use uv_client::BaseClientBuilder;
@@ -22,12 +25,12 @@ use uv_python::{
};
use uv_settings::{PythonInstallMirrors, ToolOptions};
use uv_shell::Shell;
-use uv_tool::{InstalledTools, Tool, ToolEntrypoint, entrypoint_paths, tool_executable_dir};
-use uv_warnings::warn_user;
+use uv_tool::{InstalledTools, Tool, ToolEntrypoint, entrypoint_paths};
+use uv_warnings::warn_user_once;
+use crate::commands::pip;
use crate::commands::project::ProjectError;
use crate::commands::reporters::PythonDownloadReporter;
-use crate::commands::{ExitStatus, pip};
use crate::printer::Printer;
/// Return all packages which contain an executable with the given name.
@@ -169,8 +172,9 @@ pub(crate) async fn refine_interpreter(
pub(crate) fn finalize_tool_install(
environment: &PythonEnvironment,
name: &PackageName,
+ entrypoints: &[PackageName],
installed_tools: &InstalledTools,
- options: ToolOptions,
+ options: &ToolOptions,
force: bool,
python: Option,
requirements: Vec,
@@ -178,120 +182,152 @@ pub(crate) fn finalize_tool_install(
overrides: Vec,
build_constraints: Vec,
printer: Printer,
-) -> anyhow::Result {
- let site_packages = SitePackages::from_environment(environment)?;
- let installed = site_packages.get_packages(name);
- let Some(installed_dist) = installed.first().copied() else {
- bail!("Expected at least one requirement")
- };
-
- // Find a suitable path to install into
- let executable_directory = tool_executable_dir()?;
+) -> anyhow::Result<()> {
+ let executable_directory = uv_tool::tool_executable_dir()?;
fs_err::create_dir_all(&executable_directory)
.context("Failed to create executable directory")?;
-
debug!(
"Installing tool executables into: {}",
executable_directory.user_display()
);
- let entry_points = entrypoint_paths(
- &site_packages,
- installed_dist.name(),
- installed_dist.version(),
- )?;
-
- // Determine the entry points targets. Use a sorted collection for deterministic output.
- let target_entry_points = entry_points
+ let mut installed_entrypoints = Vec::new();
+ let site_packages = SitePackages::from_environment(environment)?;
+ let ordered_packages = entrypoints
+ // Install dependencies first
+ .iter()
+ .filter(|pkg| *pkg != name)
+ .collect::>()
+ // Then install the root package last
.into_iter()
- .map(|(name, source_path)| {
- let target_path = executable_directory.join(
- source_path
- .file_name()
- .map(std::borrow::ToOwned::to_owned)
- .unwrap_or_else(|| OsString::from(name.clone())),
- );
- (name, source_path, target_path)
- })
- .collect::>();
+ .chain(std::iter::once(name));
- if target_entry_points.is_empty() {
- writeln!(
- printer.stdout(),
- "No executables are provided by package `{from}`; removing tool",
- from = name.cyan()
- )?;
+ for package in ordered_packages {
+ if package == name {
+ debug!("Installing entrypoints for tool `{package}`");
+ } else {
+ debug!("Installing entrypoints for `{package}` as part of tool `{name}`");
+ }
- hint_executable_from_dependency(name, &site_packages, printer)?;
+ let installed = site_packages.get_packages(package);
+ let dist = installed
+ .first()
+ .context("Expected at least one requirement")?;
+ let dist_entrypoints = entrypoint_paths(&site_packages, dist.name(), dist.version())?;
- // Clean up the environment we just created.
- installed_tools.remove_environment(name)?;
+ // Determine the entry points targets. Use a sorted collection for deterministic output.
+ let target_entrypoints = dist_entrypoints
+ .into_iter()
+ .map(|(name, source_path)| {
+ let target_path = executable_directory.join(
+ source_path
+ .file_name()
+ .map(std::borrow::ToOwned::to_owned)
+ .unwrap_or_else(|| OsString::from(name.clone())),
+ );
+ (name, source_path, target_path)
+ })
+ .collect::>();
- return Ok(ExitStatus::Failure);
- }
+ if target_entrypoints.is_empty() {
+ // If package is not the root package, suggest to install it as a dependency.
+ if package != name {
+ writeln!(
+ printer.stdout(),
+ "No executables are provided by package `{}`\n{}{} Use `--with {}` to include `{}` as a dependency without installing its executables.",
+ package.cyan(),
+ "hint".bold().cyan(),
+ ":".bold(),
+ package.cyan(),
+ package.cyan(),
+ )?;
+ continue;
+ }
- // Error if we're overwriting an existing entrypoint, unless the user passed `--force`.
- if !force {
- let mut existing_entry_points = target_entry_points
- .iter()
- .filter(|(_, _, target_path)| target_path.exists())
- .peekable();
- if existing_entry_points.peek().is_some() {
- // Clean up the environment we just created
+ // For the root package, this is a fatal error
+ writeln!(
+ printer.stdout(),
+ "No executables are provided by package `{}`; removing tool",
+ package.cyan()
+ )?;
+
+ hint_executable_from_dependency(package, &site_packages, printer)?;
+
+ // Clean up the environment we just created.
installed_tools.remove_environment(name)?;
- let existing_entry_points = existing_entry_points
- // SAFETY: We know the target has a filename because we just constructed it above
- .map(|(_, _, target)| target.file_name().unwrap().to_string_lossy())
- .collect::>();
- let (s, exists) = if existing_entry_points.len() == 1 {
- ("", "exists")
- } else {
- ("s", "exist")
- };
- bail!(
- "Executable{s} already {exists}: {} (use `--force` to overwrite)",
- existing_entry_points
- .iter()
- .map(|name| name.bold())
- .join(", ")
- )
+ return Err(anyhow::anyhow!(
+ "Failed to install entrypoints for `{}`",
+ package.cyan()
+ ));
}
- }
- #[cfg(windows)]
- let itself = std::env::current_exe().ok();
+ // Error if we're overwriting an existing entrypoint, unless the user passed `--force`.
+ if !force {
+ let mut existing_entrypoints = target_entrypoints
+ .iter()
+ .filter(|(_, _, target_path)| target_path.exists())
+ .peekable();
+ if existing_entrypoints.peek().is_some() {
+ // Clean up the environment we just created
+ installed_tools.remove_environment(name)?;
- for (name, source_path, target_path) in &target_entry_points {
- debug!("Installing executable: `{name}`");
-
- #[cfg(unix)]
- replace_symlink(source_path, target_path).context("Failed to install executable")?;
+ let existing_entrypoints = existing_entrypoints
+ // SAFETY: We know the target has a filename because we just constructed it above
+ .map(|(_, _, target)| target.file_name().unwrap().to_string_lossy())
+ .collect::>();
+ let (s, exists) = if existing_entrypoints.len() == 1 {
+ ("", "exists")
+ } else {
+ ("s", "exist")
+ };
+ bail!(
+ "Executable{s} already {exists}: {} (use `--force` to overwrite)",
+ existing_entrypoints
+ .iter()
+ .map(|name| name.bold())
+ .join(", ")
+ )
+ }
+ }
#[cfg(windows)]
- if itself.as_ref().is_some_and(|itself| {
- std::path::absolute(target_path).is_ok_and(|target| *itself == target)
- }) {
- self_replace::self_replace(source_path).context("Failed to install entrypoint")?;
- } else {
- fs_err::copy(source_path, target_path).context("Failed to install entrypoint")?;
- }
- }
+ let itself = std::env::current_exe().ok();
- let s = if target_entry_points.len() == 1 {
- ""
- } else {
- "s"
- };
- writeln!(
- printer.stderr(),
- "Installed {} executable{s}: {}",
- target_entry_points.len(),
- target_entry_points
- .iter()
- .map(|(name, _, _)| name.bold())
- .join(", ")
- )?;
+ let mut names = BTreeSet::new();
+ for (name, src, target) in target_entrypoints {
+ debug!("Installing executable: `{name}`");
+
+ #[cfg(unix)]
+ replace_symlink(src, &target).context("Failed to install executable")?;
+
+ #[cfg(windows)]
+ if itself.as_ref().is_some_and(|itself| {
+ std::path::absolute(&target).is_ok_and(|target| *itself == target)
+ }) {
+ self_replace::self_replace(src).context("Failed to install entrypoint")?;
+ } else {
+ fs_err::copy(src, &target).context("Failed to install entrypoint")?;
+ }
+
+ let tool_entry = ToolEntrypoint::new(&name, target, package.to_string());
+ names.insert(tool_entry.name.clone());
+ installed_entrypoints.push(tool_entry);
+ }
+
+ let s = if names.len() == 1 { "" } else { "s" };
+ let from_pkg = if name == package {
+ String::new()
+ } else {
+ format!(" from `{package}`")
+ };
+ writeln!(
+ printer.stderr(),
+ "Installed {} executable{s}{from_pkg}: {}",
+ names.len(),
+ names.iter().map(|name| name.bold()).join(", ")
+ )?;
+ }
debug!("Adding receipt for tool `{name}`");
let tool = Tool::new(
@@ -300,45 +336,48 @@ pub(crate) fn finalize_tool_install(
overrides,
build_constraints,
python,
- target_entry_points
- .into_iter()
- .map(|(name, _, target_path)| ToolEntrypoint::new(name, target_path)),
- options,
+ installed_entrypoints,
+ options.clone(),
);
installed_tools.add_tool_receipt(name, tool)?;
+ warn_out_of_path(&executable_directory);
+
+ Ok(())
+}
+
+fn warn_out_of_path(executable_directory: &Path) {
// If the executable directory isn't on the user's PATH, warn.
- if !Shell::contains_path(&executable_directory) {
+ if !Shell::contains_path(executable_directory) {
if let Some(shell) = Shell::from_env() {
- if let Some(command) = shell.prepend_path(&executable_directory) {
+ if let Some(command) = shell.prepend_path(executable_directory) {
if shell.supports_update() {
- warn_user!(
+ warn_user_once!(
"`{}` is not on your PATH. To use installed tools, run `{}` or `{}`.",
executable_directory.simplified_display().cyan(),
command.green(),
"uv tool update-shell".green()
);
} else {
- warn_user!(
+ warn_user_once!(
"`{}` is not on your PATH. To use installed tools, run `{}`.",
executable_directory.simplified_display().cyan(),
command.green()
);
}
} else {
- warn_user!(
+ warn_user_once!(
"`{}` is not on your PATH. To use installed tools, add the directory to your PATH.",
executable_directory.simplified_display().cyan(),
);
}
} else {
- warn_user!(
+ warn_user_once!(
"`{}` is not on your PATH. To use installed tools, add the directory to your PATH.",
executable_directory.simplified_display().cyan(),
);
}
}
- Ok(ExitStatus::Success)
}
/// Displays a hint if an executable matching the package name can be found in a dependency of the package.
diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs
index 6528f61d2..4917934c4 100644
--- a/crates/uv/src/commands/tool/install.rs
+++ b/crates/uv/src/commands/tool/install.rs
@@ -50,6 +50,7 @@ pub(crate) async fn install(
constraints: &[RequirementsSource],
overrides: &[RequirementsSource],
build_constraints: &[RequirementsSource],
+ entrypoints: &[PackageName],
python: Option,
install_mirrors: PythonInstallMirrors,
force: bool,
@@ -113,7 +114,7 @@ pub(crate) async fn install(
};
// Resolve the `--from` requirement.
- let from = match &request {
+ let requirement = match &request {
// Ex) `ruff`
ToolRequest::Package {
executable,
@@ -219,14 +220,16 @@ pub(crate) async fn install(
}
// Ex) `python`
ToolRequest::Python { .. } => {
- return Err(anyhow::anyhow!(
+ bail!(
"Cannot install Python with `{}`. Did you mean to use `{}`?",
"uv tool install".cyan(),
"uv python install".cyan(),
- ));
+ );
}
};
+ let package_name = &requirement.name;
+
// If the user passed, e.g., `ruff@latest`, we need to mark it as upgradable.
let settings = if request.is_latest() {
ResolverInstallerSettings {
@@ -234,7 +237,7 @@ pub(crate) async fn install(
upgrade: settings
.resolver
.upgrade
- .combine(Upgrade::package(from.name.clone())),
+ .combine(Upgrade::package(package_name.clone())),
..settings.resolver
},
..settings
@@ -248,7 +251,7 @@ pub(crate) async fn install(
ResolverInstallerSettings {
reinstall: settings
.reinstall
- .combine(Reinstall::package(from.name.clone())),
+ .combine(Reinstall::package(package_name.clone())),
..settings
}
} else {
@@ -268,7 +271,7 @@ pub(crate) async fn install(
// Resolve the `--from` and `--with` requirements.
let requirements = {
let mut requirements = Vec::with_capacity(1 + with.len());
- requirements.push(from.clone());
+ requirements.push(requirement.clone());
requirements.extend(
resolve_names(
spec.requirements.clone(),
@@ -332,16 +335,16 @@ pub(crate) async fn install(
// (If we find existing entrypoints later on, and the tool _doesn't_ exist, we'll avoid removing
// the external tool's entrypoints (without `--force`).)
let (existing_tool_receipt, invalid_tool_receipt) =
- match installed_tools.get_tool_receipt(&from.name) {
+ match installed_tools.get_tool_receipt(package_name) {
Ok(None) => (None, false),
Ok(Some(receipt)) => (Some(receipt), false),
Err(_) => {
// If the tool is not installed properly, remove the environment and continue.
- match installed_tools.remove_environment(&from.name) {
+ match installed_tools.remove_environment(package_name) {
Ok(()) => {
warn_user!(
- "Removed existing `{from}` with invalid receipt",
- from = from.name.cyan()
+ "Removed existing `{}` with invalid receipt",
+ package_name.cyan()
);
}
Err(uv_tool::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {}
@@ -355,20 +358,20 @@ pub(crate) async fn install(
let existing_environment =
installed_tools
- .get_environment(&from.name, &cache)?
+ .get_environment(package_name, &cache)?
.filter(|environment| {
if environment.uses(&interpreter) {
trace!(
"Existing interpreter matches the requested interpreter for `{}`: {}",
- from.name,
+ package_name,
environment.interpreter().sys_executable().display()
);
true
} else {
let _ = writeln!(
printer.stderr(),
- "Ignoring existing environment for `{from}`: the requested Python interpreter does not match the environment interpreter",
- from = from.name.cyan(),
+ "Ignoring existing environment for `{}`: the requested Python interpreter does not match the environment interpreter",
+ package_name.cyan(),
);
false
}
@@ -393,15 +396,17 @@ pub(crate) async fn install(
{
if *tool_receipt.options() != options {
// ...but the options differ, we need to update the receipt.
- installed_tools
- .add_tool_receipt(&from.name, tool_receipt.clone().with_options(options))?;
+ installed_tools.add_tool_receipt(
+ package_name,
+ tool_receipt.clone().with_options(options),
+ )?;
}
// We're done, though we might need to update the receipt.
writeln!(
printer.stderr(),
- "`{from}` is already installed",
- from = from.cyan()
+ "`{}` is already installed",
+ requirement.cyan()
)?;
return Ok(ExitStatus::Success);
@@ -560,7 +565,7 @@ pub(crate) async fn install(
},
};
- let environment = installed_tools.create_environment(&from.name, interpreter, preview)?;
+ let environment = installed_tools.create_environment(package_name, interpreter, preview)?;
// At this point, we removed any existing environment, so we should remove any of its
// executables.
@@ -587,8 +592,8 @@ pub(crate) async fn install(
.await
.inspect_err(|_| {
// If we failed to sync, remove the newly created environment.
- debug!("Failed to sync environment; removing `{}`", from.name);
- let _ = installed_tools.remove_environment(&from.name);
+ debug!("Failed to sync environment; removing `{}`", package_name);
+ let _ = installed_tools.remove_environment(package_name);
}) {
Ok(environment) => environment,
Err(ProjectError::Operation(err)) => {
@@ -602,9 +607,10 @@ pub(crate) async fn install(
finalize_tool_install(
&environment,
- &from.name,
+ package_name,
+ entrypoints,
&installed_tools,
- options,
+ &options,
force || invalid_tool_receipt,
python_request,
requirements,
@@ -612,5 +618,7 @@ pub(crate) async fn install(
overrides,
build_constraints,
printer,
- )
+ )?;
+
+ Ok(ExitStatus::Success)
}
diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs
index f7bce3197..cac86c149 100644
--- a/crates/uv/src/commands/tool/upgrade.rs
+++ b/crates/uv/src/commands/tool/upgrade.rs
@@ -3,6 +3,7 @@ use itertools::Itertools;
use owo_colors::{AnsiColors, OwoColorize};
use std::collections::BTreeMap;
use std::fmt::Write;
+use std::str::FromStr;
use tracing::debug;
use uv_cache::Cache;
@@ -372,12 +373,19 @@ async fn upgrade_tool(
// existing executables.
remove_entrypoints(&existing_tool_receipt);
+ let entrypoints: Vec<_> = existing_tool_receipt
+ .entrypoints()
+ .iter()
+ .filter_map(|entry| PackageName::from_str(entry.from.as_ref()?).ok())
+ .collect();
+
// If we modified the target tool, reinstall the entrypoints.
finalize_tool_install(
&environment,
name,
+ &entrypoints,
installed_tools,
- ToolOptions::from(options),
+ &ToolOptions::from(options),
true,
existing_tool_receipt.python().to_owned(),
existing_tool_receipt.requirements().to_vec(),
diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs
index 0d5163e4d..db8b25084 100644
--- a/crates/uv/src/lib.rs
+++ b/crates/uv/src/lib.rs
@@ -1258,21 +1258,35 @@ async fn run(mut cli: Cli) -> Result {
.combine(Refresh::from(args.settings.resolver.upgrade.clone())),
);
+ let mut entrypoints = Vec::with_capacity(args.with_executables_from.len());
let mut requirements = Vec::with_capacity(
- args.with.len() + args.with_editable.len() + args.with_requirements.len(),
+ args.with.len()
+ + args.with_editable.len()
+ + args.with_requirements.len()
+ + args.with_executables_from.len(),
);
- for package in args.with {
- requirements.push(RequirementsSource::from_with_package_argument(&package)?);
+ for pkg in args.with {
+ requirements.push(RequirementsSource::from_with_package_argument(&pkg)?);
}
- for package in args.with_editable {
- requirements.push(RequirementsSource::from_editable(&package)?);
+ for pkg in args.with_editable {
+ requirements.push(RequirementsSource::from_editable(&pkg)?);
+ }
+ for path in args.with_requirements {
+ requirements.push(RequirementsSource::from_requirements_file(path)?);
+ }
+ for pkg in &args.with_executables_from {
+ let source = RequirementsSource::from_with_package_argument(pkg)?;
+ let RequirementsSource::Package(RequirementsTxtRequirement::Named(requirement)) =
+ &source
+ else {
+ bail!(
+ "Expected a named package for `--with-executables-from`, but got: {}",
+ source.to_string().cyan()
+ )
+ };
+ entrypoints.push(requirement.name.clone());
+ requirements.push(source);
}
- requirements.extend(
- args.with_requirements
- .into_iter()
- .map(RequirementsSource::from_requirements_file)
- .collect::, _>>()?,
- );
let constraints = args
.constraints
@@ -1298,6 +1312,7 @@ async fn run(mut cli: Cli) -> Result {
&constraints,
&overrides,
&build_constraints,
+ &entrypoints,
args.python,
args.install_mirrors,
args.force,
diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs
index 7746f0667..2e2623357 100644
--- a/crates/uv/src/settings.rs
+++ b/crates/uv/src/settings.rs
@@ -598,6 +598,7 @@ pub(crate) struct ToolInstallSettings {
pub(crate) from: Option,
pub(crate) with: Vec,
pub(crate) with_requirements: Vec,
+ pub(crate) with_executables_from: Vec,
pub(crate) with_editable: Vec,
pub(crate) constraints: Vec,
pub(crate) overrides: Vec,
@@ -622,6 +623,7 @@ impl ToolInstallSettings {
with,
with_editable,
with_requirements,
+ with_executables_from,
constraints,
overrides,
build_constraints,
@@ -662,6 +664,10 @@ impl ToolInstallSettings {
.into_iter()
.filter_map(Maybe::into_option)
.collect(),
+ with_executables_from: with_executables_from
+ .into_iter()
+ .flat_map(CommaSeparatedRequirements::into_iter)
+ .collect(),
constraints: constraints
.into_iter()
.filter_map(Maybe::into_option)
diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs
index 6775f2e3d..ff9c4383f 100644
--- a/crates/uv/tests/it/show_settings.rs
+++ b/crates/uv/tests/it/show_settings.rs
@@ -3424,6 +3424,7 @@ fn resolve_tool() -> anyhow::Result<()> {
from: None,
with: [],
with_requirements: [],
+ with_executables_from: [],
with_editable: [],
constraints: [],
overrides: [],
diff --git a/crates/uv/tests/it/tool_install.rs b/crates/uv/tests/it/tool_install.rs
index 0af2510fb..cd080f404 100644
--- a/crates/uv/tests/it/tool_install.rs
+++ b/crates/uv/tests/it/tool_install.rs
@@ -82,8 +82,8 @@ fn tool_install() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -168,7 +168,7 @@ fn tool_install() {
[tool]
requirements = [{ name = "flask" }]
entrypoints = [
- { name = "flask", install-path = "[TEMP_DIR]/bin/flask" },
+ { name = "flask", install-path = "[TEMP_DIR]/bin/flask", from = "flask" },
]
[tool.options]
@@ -382,8 +382,8 @@ fn tool_install_with_compatible_build_constraints() -> Result<()> {
]
build-constraint-dependencies = [{ name = "setuptools", specifier = ">=40" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -450,7 +450,7 @@ fn tool_install_suggest_other_packages_with_executable() {
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r"
success: false
- exit_code: 1
+ exit_code: 2
----- stdout -----
No executables are provided by package `fastapi`; removing tool
hint: An executable with the name `fastapi` is available via dependency `fastapi-cli`.
@@ -494,6 +494,7 @@ fn tool_install_suggest_other_packages_with_executable() {
+ uvicorn==0.29.0
+ watchfiles==0.21.0
+ websockets==12.0
+ error: Failed to install entrypoints for `fastapi`
");
}
@@ -565,8 +566,8 @@ fn tool_install_version() {
[tool]
requirements = [{ name = "black", specifier = "==24.2.0" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -649,7 +650,7 @@ fn tool_install_editable() {
[tool]
requirements = [{ name = "black", editable = "[WORKSPACE]/scripts/packages/black_editable" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
]
[tool.options]
@@ -690,7 +691,7 @@ fn tool_install_editable() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
]
[tool.options]
@@ -733,8 +734,8 @@ fn tool_install_editable() {
[tool]
requirements = [{ name = "black", specifier = "==24.2.0" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -781,8 +782,8 @@ fn tool_install_remove_on_empty() -> Result<()> {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -823,7 +824,7 @@ fn tool_install_remove_on_empty() -> Result<()> {
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @r"
success: false
- exit_code: 1
+ exit_code: 2
----- stdout -----
No executables are provided by package `black`; removing tool
@@ -839,6 +840,7 @@ fn tool_install_remove_on_empty() -> Result<()> {
- packaging==24.0
- pathspec==0.12.1
- platformdirs==4.2.0
+ error: Failed to install entrypoints for `black`
");
// Re-request `black`. It should reinstall, without requiring `--force`.
@@ -871,8 +873,8 @@ fn tool_install_remove_on_empty() -> Result<()> {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -949,7 +951,7 @@ fn tool_install_editable_from() {
[tool]
requirements = [{ name = "black", editable = "[WORKSPACE]/scripts/packages/black_editable" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
]
[tool.options]
@@ -1101,8 +1103,8 @@ fn tool_install_already_installed() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -1137,8 +1139,8 @@ fn tool_install_already_installed() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -1428,8 +1430,8 @@ fn tool_install_force() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -1466,8 +1468,8 @@ fn tool_install_force() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -1651,7 +1653,7 @@ fn tool_install_no_entrypoints() {
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @r"
success: false
- exit_code: 1
+ exit_code: 2
----- stdout -----
No executables are provided by package `iniconfig`; removing tool
@@ -1660,6 +1662,7 @@ fn tool_install_no_entrypoints() {
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
+ error: Failed to install entrypoints for `iniconfig`
");
// Ensure the tool environment is not created.
@@ -1794,8 +1797,8 @@ fn tool_install_unnamed_package() {
[tool]
requirements = [{ name = "black", url = "https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -1909,8 +1912,8 @@ fn tool_install_unnamed_from() {
[tool]
requirements = [{ name = "black", url = "https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -2003,8 +2006,8 @@ fn tool_install_unnamed_with() {
{ name = "iniconfig", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" },
]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -2072,8 +2075,8 @@ fn tool_install_requirements_txt() {
{ name = "iniconfig" },
]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -2117,8 +2120,8 @@ fn tool_install_requirements_txt() {
{ name = "idna" },
]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -2181,8 +2184,8 @@ fn tool_install_requirements_txt_arguments() {
{ name = "idna" },
]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -2295,8 +2298,8 @@ fn tool_install_upgrade() {
[tool]
requirements = [{ name = "black", specifier = "==24.1.1" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -2329,8 +2332,8 @@ fn tool_install_upgrade() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -2369,8 +2372,8 @@ fn tool_install_upgrade() {
{ name = "iniconfig", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" },
]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -2409,8 +2412,8 @@ fn tool_install_upgrade() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -2878,7 +2881,7 @@ fn tool_install_malformed_dist_info() {
[tool]
requirements = [{ name = "executable-application" }]
entrypoints = [
- { name = "app", install-path = "[TEMP_DIR]/bin/app" },
+ { name = "app", install-path = "[TEMP_DIR]/bin/app", from = "executable-application" },
]
[tool.options]
@@ -2958,7 +2961,7 @@ fn tool_install_settings() {
[tool]
requirements = [{ name = "flask", specifier = ">=3" }]
entrypoints = [
- { name = "flask", install-path = "[TEMP_DIR]/bin/flask" },
+ { name = "flask", install-path = "[TEMP_DIR]/bin/flask", from = "flask" },
]
[tool.options]
@@ -2991,7 +2994,7 @@ fn tool_install_settings() {
[tool]
requirements = [{ name = "flask", specifier = ">=3" }]
entrypoints = [
- { name = "flask", install-path = "[TEMP_DIR]/bin/flask" },
+ { name = "flask", install-path = "[TEMP_DIR]/bin/flask", from = "flask" },
]
[tool.options]
@@ -3031,7 +3034,7 @@ fn tool_install_settings() {
[tool]
requirements = [{ name = "flask", specifier = ">=3" }]
entrypoints = [
- { name = "flask", install-path = "[TEMP_DIR]/bin/flask" },
+ { name = "flask", install-path = "[TEMP_DIR]/bin/flask", from = "flask" },
]
[tool.options]
@@ -3080,8 +3083,8 @@ fn tool_install_at_version() {
[tool]
requirements = [{ name = "black", specifier = "==24.1.0" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -3146,8 +3149,8 @@ fn tool_install_at_latest() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -3192,7 +3195,7 @@ fn tool_install_from_at_latest() {
[tool]
requirements = [{ name = "executable-application" }]
entrypoints = [
- { name = "app", install-path = "[TEMP_DIR]/bin/app" },
+ { name = "app", install-path = "[TEMP_DIR]/bin/app", from = "executable-application" },
]
[tool.options]
@@ -3237,7 +3240,7 @@ fn tool_install_from_at_version() {
[tool]
requirements = [{ name = "executable-application", specifier = "==0.2.0" }]
entrypoints = [
- { name = "app", install-path = "[TEMP_DIR]/bin/app" },
+ { name = "app", install-path = "[TEMP_DIR]/bin/app", from = "executable-application" },
]
[tool.options]
@@ -3286,8 +3289,8 @@ fn tool_install_at_latest_upgrade() {
[tool]
requirements = [{ name = "black", specifier = "==24.1.1" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -3320,8 +3323,8 @@ fn tool_install_at_latest_upgrade() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -3357,8 +3360,8 @@ fn tool_install_at_latest_upgrade() {
[tool]
requirements = [{ name = "black" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -3419,8 +3422,8 @@ fn tool_install_constraints() -> Result<()> {
{ name = "anyio", specifier = ">=3" },
]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -3526,8 +3529,8 @@ fn tool_install_overrides() -> Result<()> {
{ name = "anyio", specifier = ">=3" },
]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -3700,7 +3703,7 @@ fn tool_install_credentials() {
[tool]
requirements = [{ name = "executable-application" }]
entrypoints = [
- { name = "app", install-path = "[TEMP_DIR]/bin/app" },
+ { name = "app", install-path = "[TEMP_DIR]/bin/app", from = "executable-application" },
]
[tool.options]
@@ -3789,7 +3792,7 @@ fn tool_install_default_credentials() -> Result<()> {
[tool]
requirements = [{ name = "executable-application" }]
entrypoints = [
- { name = "app", install-path = "[TEMP_DIR]/bin/app" },
+ { name = "app", install-path = "[TEMP_DIR]/bin/app", from = "executable-application" },
]
[tool.options]
@@ -3832,3 +3835,136 @@ fn tool_install_default_credentials() -> Result<()> {
Ok(())
}
+
+/// Test installing a tool with `--with-executables-from`.
+#[test]
+fn tool_install_with_executables_from() {
+ let context = TestContext::new("3.12")
+ .with_filtered_counts()
+ .with_filtered_exe_suffix();
+ let tool_dir = context.temp_dir.child("tools");
+ let bin_dir = context.temp_dir.child("bin");
+
+ uv_snapshot!(context.filters(), context.tool_install()
+ .arg("--with-executables-from")
+ .arg("ansible-core,black")
+ .arg("ansible==9.3.0")
+ .env("UV_TOOL_DIR", tool_dir.as_os_str())
+ .env("XDG_BIN_HOME", bin_dir.as_os_str())
+ .env("PATH", bin_dir.as_os_str()), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Resolved [N] packages in [TIME]
+ Prepared [N] packages in [TIME]
+ Installed [N] packages in [TIME]
+ + ansible==9.3.0
+ + ansible-core==2.16.4
+ + black==24.3.0
+ + cffi==1.16.0
+ + click==8.1.7
+ + cryptography==42.0.5
+ + jinja2==3.1.3
+ + markupsafe==2.1.5
+ + mypy-extensions==1.0.0
+ + packaging==24.0
+ + pathspec==0.12.1
+ + platformdirs==4.2.0
+ + pycparser==2.21
+ + pyyaml==6.0.1
+ + resolvelib==1.0.1
+ Installed 11 executables from `ansible-core`: ansible, ansible-config, ansible-connection, ansible-console, ansible-doc, ansible-galaxy, ansible-inventory, ansible-playbook, ansible-pull, ansible-test, ansible-vault
+ Installed 2 executables from `black`: black, blackd
+ Installed 1 executable: ansible-community
+ ");
+
+ insta::with_settings!({
+ filters => context.filters(),
+ }, {
+ assert_snapshot!(fs_err::read_to_string(tool_dir.join("ansible").join("uv-receipt.toml")).unwrap(), @r###"
+ [tool]
+ requirements = [
+ { name = "ansible", specifier = "==9.3.0" },
+ { name = "ansible-core" },
+ { name = "black" },
+ ]
+ entrypoints = [
+ { name = "ansible", install-path = "[TEMP_DIR]/bin/ansible", from = "ansible-core" },
+ { name = "ansible-community", install-path = "[TEMP_DIR]/bin/ansible-community", from = "ansible" },
+ { name = "ansible-config", install-path = "[TEMP_DIR]/bin/ansible-config", from = "ansible-core" },
+ { name = "ansible-connection", install-path = "[TEMP_DIR]/bin/ansible-connection", from = "ansible-core" },
+ { name = "ansible-console", install-path = "[TEMP_DIR]/bin/ansible-console", from = "ansible-core" },
+ { name = "ansible-doc", install-path = "[TEMP_DIR]/bin/ansible-doc", from = "ansible-core" },
+ { name = "ansible-galaxy", install-path = "[TEMP_DIR]/bin/ansible-galaxy", from = "ansible-core" },
+ { name = "ansible-inventory", install-path = "[TEMP_DIR]/bin/ansible-inventory", from = "ansible-core" },
+ { name = "ansible-playbook", install-path = "[TEMP_DIR]/bin/ansible-playbook", from = "ansible-core" },
+ { name = "ansible-pull", install-path = "[TEMP_DIR]/bin/ansible-pull", from = "ansible-core" },
+ { name = "ansible-test", install-path = "[TEMP_DIR]/bin/ansible-test", from = "ansible-core" },
+ { name = "ansible-vault", install-path = "[TEMP_DIR]/bin/ansible-vault", from = "ansible-core" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
+ ]
+
+ [tool.options]
+ exclude-newer = "2024-03-25T00:00:00Z"
+ "###);
+ });
+
+ uv_snapshot!(context.filters(), context.tool_uninstall()
+ .arg("ansible")
+ .env("UV_TOOL_DIR", tool_dir.as_os_str())
+ .env("XDG_BIN_HOME", bin_dir.as_os_str())
+ .env("PATH", bin_dir.as_os_str()), @r###"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Uninstalled 14 executables: ansible, ansible-community, ansible-config, ansible-connection, ansible-console, ansible-doc, ansible-galaxy, ansible-inventory, ansible-playbook, ansible-pull, ansible-test, ansible-vault, black, blackd
+ "###);
+}
+
+/// Test installing a tool with `--with-executables-from`, but the package has no entrypoints.
+#[test]
+fn tool_install_with_executables_from_no_entrypoints() {
+ let context = TestContext::new("3.12")
+ .with_filtered_counts()
+ .with_filtered_exe_suffix();
+ let tool_dir = context.temp_dir.child("tools");
+ let bin_dir = context.temp_dir.child("bin");
+
+ // Try to install flask with executables from requests (which has no executables)
+ uv_snapshot!(context.filters(), context.tool_install()
+ .arg("--with-executables-from")
+ .arg("requests")
+ .arg("flask")
+ .env("UV_TOOL_DIR", tool_dir.as_os_str())
+ .env("XDG_BIN_HOME", bin_dir.as_os_str())
+ .env("PATH", bin_dir.as_os_str()), @r###"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ No executables are provided by package `requests`
+ hint: Use `--with requests` to include `requests` as a dependency without installing its executables.
+
+ ----- stderr -----
+ Resolved [N] packages in [TIME]
+ Prepared [N] packages in [TIME]
+ Installed [N] packages in [TIME]
+ + blinker==1.7.0
+ + certifi==2024.2.2
+ + charset-normalizer==3.3.2
+ + click==8.1.7
+ + flask==3.0.2
+ + idna==3.6
+ + itsdangerous==2.1.2
+ + jinja2==3.1.3
+ + markupsafe==2.1.5
+ + requests==2.31.0
+ + urllib3==2.2.1
+ + werkzeug==3.0.1
+ Installed 1 executable: flask
+ "###);
+}
diff --git a/crates/uv/tests/it/tool_list.rs b/crates/uv/tests/it/tool_list.rs
index 9268118ca..cb767d457 100644
--- a/crates/uv/tests/it/tool_list.rs
+++ b/crates/uv/tests/it/tool_list.rs
@@ -89,8 +89,8 @@ fn tool_list_paths_windows() {
exit_code: 0
----- stdout -----
black v24.2.0 ([TEMP_DIR]\tools\black)
- - black.exe ([TEMP_DIR]\bin\black.exe)
- - blackd.exe ([TEMP_DIR]\bin\blackd.exe)
+ - black ([TEMP_DIR]\bin\black.exe)
+ - blackd ([TEMP_DIR]\bin\blackd.exe)
----- stderr -----
"###);
@@ -218,8 +218,8 @@ fn tool_list_deprecated() -> Result<()> {
[tool]
requirements = [{ name = "black", specifier = "==24.2.0" }]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
@@ -234,8 +234,8 @@ fn tool_list_deprecated() -> Result<()> {
[tool]
requirements = ["black==24.2.0"]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
"#,
)?;
@@ -261,8 +261,8 @@ fn tool_list_deprecated() -> Result<()> {
[tool]
requirements = ["black<>24.2.0"]
entrypoints = [
- { name = "black", install-path = "[TEMP_DIR]/bin/black" },
- { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
+ { name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
+ { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
"#,
)?;
diff --git a/crates/uv/tests/it/tool_upgrade.rs b/crates/uv/tests/it/tool_upgrade.rs
index 70309f04d..74ae4befd 100644
--- a/crates/uv/tests/it/tool_upgrade.rs
+++ b/crates/uv/tests/it/tool_upgrade.rs
@@ -835,3 +835,71 @@ fn tool_upgrade_python_with_all() {
assert_snapshot!(lines[lines.len() - 3], @"version_info = 3.12.[X]");
});
}
+
+/// Upgrade a tool together with any additional entrypoints from other
+/// packages.
+#[test]
+fn test_tool_upgrade_additional_entrypoints() {
+ let context = TestContext::new_with_versions(&["3.11", "3.12"])
+ .with_filtered_counts()
+ .with_filtered_exe_suffix();
+ let tool_dir = context.temp_dir.child("tools");
+ let bin_dir = context.temp_dir.child("bin");
+
+ // Install `babel` entrypoint, and all additional ones from `black` too.
+ uv_snapshot!(context.filters(), context.tool_install()
+ .arg("--python")
+ .arg("3.11")
+ .arg("--with-executables-from")
+ .arg("black")
+ .arg("babel==2.14.0")
+ .env("UV_TOOL_DIR", tool_dir.as_os_str())
+ .env("XDG_BIN_HOME", bin_dir.as_os_str())
+ .env("PATH", bin_dir.as_os_str()), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Resolved [N] packages in [TIME]
+ Prepared [N] packages in [TIME]
+ Installed [N] packages in [TIME]
+ + babel==2.14.0
+ + black==24.3.0
+ + click==8.1.7
+ + mypy-extensions==1.0.0
+ + packaging==24.0
+ + pathspec==0.12.1
+ + platformdirs==4.2.0
+ Installed 2 executables from `black`: black, blackd
+ Installed 1 executable: pybabel
+ ");
+
+ // Upgrade python, and make sure that all the entrypoints above get
+ // re-installed.
+ uv_snapshot!(context.filters(), context.tool_upgrade()
+ .arg("--python")
+ .arg("3.12")
+ .arg("babel")
+ .env("UV_TOOL_DIR", tool_dir.as_os_str())
+ .env("XDG_BIN_HOME", bin_dir.as_os_str())
+ .env("PATH", bin_dir.as_os_str()), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Prepared [N] packages in [TIME]
+ Installed [N] packages in [TIME]
+ + babel==2.14.0
+ + black==24.3.0
+ + click==8.1.7
+ + mypy-extensions==1.0.0
+ + packaging==24.0
+ + pathspec==0.12.1
+ + platformdirs==4.2.0
+ Installed 2 executables from `black`: black, blackd
+ Installed 1 executable: pybabel
+ Upgraded tool environment for `babel` to Python 3.12
+ ");
+}
diff --git a/docs/concepts/tools.md b/docs/concepts/tools.md
index 7c5eb9564..b721dfa8e 100644
--- a/docs/concepts/tools.md
+++ b/docs/concepts/tools.md
@@ -209,6 +209,40 @@ $ uvx -w
If the requested version conflicts with the requirements of the tool package, package resolution
will fail and the command will error.
+## Installing executables from additional packages
+
+When installing a tool, you may want to include executables from additional packages in the same
+tool environment. This is useful when you have related tools that work together or when you want to
+install multiple executables that share dependencies.
+
+The `--with-executables-from` option allows you to specify additional packages whose executables
+should be installed alongside the main tool:
+
+```console
+$ uv tool install --with-executables-from ,
+```
+
+For example, to install Ansible along with executables from `ansible-core` and `ansible-lint`:
+
+```console
+$ uv tool install --with-executables-from ansible-core,ansible-lint ansible
+```
+
+This will install all executables from the `ansible`, `ansible-core`, and `ansible-lint` packages
+into the same tool environment, making them all available on the `PATH`.
+
+The `--with-executables-from` option can be combined with other installation options:
+
+```console
+$ uv tool install --with-executables-from ansible-core --with mkdocs-material ansible
+```
+
+Note that `--with-executables-from` differs from `--with` in that:
+
+- `--with` includes additional packages as dependencies but does not install their executables
+- `--with-executables-from` includes both the packages as dependencies and installs their
+ executables
+
## Python versions
Each tool environment is linked to a specific Python version. This uses the same Python version
diff --git a/docs/guides/tools.md b/docs/guides/tools.md
index b281b89b2..e86bcf627 100644
--- a/docs/guides/tools.md
+++ b/docs/guides/tools.md
@@ -213,6 +213,14 @@ As with `uvx`, installations can include additional packages:
$ uv tool install mkdocs --with mkdocs-material
```
+Multiple related executables can be installed together in the same tool environment, using the
+`--with-executables-from` flag. For example, the following will install the executables from
+`ansible`, plus those ones provided by `ansible-core` and `ansible-lint`:
+
+```console
+$ uv tool install --with-executables-from ansible-core,ansible-lint ansible
+```
+
## Upgrading tools
To upgrade a tool, use `uv tool upgrade`:
diff --git a/docs/reference/cli.md b/docs/reference/cli.md
index 5d4ffea68..01b5184c8 100644
--- a/docs/reference/cli.md
+++ b/docs/reference/cli.md
@@ -2198,6 +2198,7 @@ uv tool install [OPTIONS]
You can configure fine-grained logging using the RUST_LOG environment variable. (https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives)
--with, -w withInclude the following additional requirements
--with-editable with-editableInclude the given packages in editable mode
+--with-executables-from with-executables-fromInstall executables from the following packages
--with-requirements with-requirementsInclude all requirements listed in the given requirements.txt files