mirror of https://github.com/astral-sh/uv
Compare commits
188 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
0a83bf7dd5 | |
|
|
e603761862 | |
|
|
4f6f56b070 | |
|
|
66f7093ad2 | |
|
|
60df92f9aa | |
|
|
0cee76417f | |
|
|
af348c2a88 | |
|
|
b58f543e5e | |
|
|
13e7ad62cb | |
|
|
94c97b6434 | |
|
|
af95677b9b | |
|
|
a5d50a20d2 | |
|
|
a768a9d111 | |
|
|
d20948bec2 | |
|
|
a2d64aa224 | |
|
|
c43315f4eb | |
|
|
e77ee15204 | |
|
|
ed37f3b432 | |
|
|
3e80b10272 | |
|
|
7ad441a0bd | |
|
|
5a55bbe883 | |
|
|
6ad80c5150 | |
|
|
38ae414682 | |
|
|
6de869cc88 | |
|
|
59d73fdddf | |
|
|
4c1571fb76 | |
|
|
ebdffaf728 | |
|
|
3bb7f67c71 | |
|
|
caac4814df | |
|
|
a550743bed | |
|
|
94f1f02d85 | |
|
|
36806f8e66 | |
|
|
2b5d65e61d | |
|
|
81c99dd438 | |
|
|
b931c6687c | |
|
|
eca36eed08 | |
|
|
69910b4aab | |
|
|
8d2c2e8cdf | |
|
|
b6686fbce3 | |
|
|
4af2d2b922 | |
|
|
2502577c9d | |
|
|
d0a6f5d13f | |
|
|
7b6b02a7d1 | |
|
|
0dd71f4382 | |
|
|
38ce3b2919 | |
|
|
a70ee58ae1 | |
|
|
9774f8f1d4 | |
|
|
77df5887e4 | |
|
|
4e1469b151 | |
|
|
5a6f2ea319 | |
|
|
28a8194a67 | |
|
|
a63e5b62e3 | |
|
|
ed19672f1f | |
|
|
9635258867 | |
|
|
c269619b1b | |
|
|
eaa1882c51 | |
|
|
8390b311f8 | |
|
|
b73281d222 | |
|
|
9f58280eb8 | |
|
|
f6ad3dcd57 | |
|
|
fb5de2228c | |
|
|
0c5391a7c7 | |
|
|
ee6e3be815 | |
|
|
d3cd94ecaf | |
|
|
62bf92132b | |
|
|
2748dce860 | |
|
|
2abe56a357 | |
|
|
2f553bfc51 | |
|
|
539b7368cd | |
|
|
99660a8574 | |
|
|
20ab80ad8f | |
|
|
1d8252599a | |
|
|
05fa19c440 | |
|
|
e00cc8c35f | |
|
|
932d7b8fce | |
|
|
49b70e7225 | |
|
|
b1078fe595 | |
|
|
f01366bae8 | |
|
|
ed63be5dab | |
|
|
d2db06983a | |
|
|
7954b34989 | |
|
|
5eafae3327 | |
|
|
89c411f0ae | |
|
|
18a36528ea | |
|
|
eb65f9ff74 | |
|
|
e7af5838bb | |
|
|
87adf14fdf | |
|
|
9fc07c8773 | |
|
|
d2162e27e6 | |
|
|
99c40f74c5 | |
|
|
e38cab64ce | |
|
|
e4d193a5f8 | |
|
|
fee7f9d093 | |
|
|
5947fb0c83 | |
|
|
54f9932362 | |
|
|
c8996d24a1 | |
|
|
2cdbf9e547 | |
|
|
3347e196bb | |
|
|
23b8fc9d18 | |
|
|
082be90177 | |
|
|
fbf925ee63 | |
|
|
efa47adefb | |
|
|
05814f9cd5 | |
|
|
6b00d6522c | |
|
|
5773b12fa9 | |
|
|
825ab78790 | |
|
|
d0931e0ca9 | |
|
|
6d8866a4f3 | |
|
|
8a73958c4a | |
|
|
2c3b907dc0 | |
|
|
0b70eba917 | |
|
|
0ae54dbd8a | |
|
|
c29304aaca | |
|
|
5f3d46c241 | |
|
|
5498e4d6f6 | |
|
|
e2bda1173e | |
|
|
0db41803cd | |
|
|
c67a0fdd7b | |
|
|
f02b459d04 | |
|
|
eaa4651df0 | |
|
|
76d769d7a0 | |
|
|
fa6afd5a71 | |
|
|
7ca92dcf66 | |
|
|
735b87004c | |
|
|
ca62066194 | |
|
|
4d747f6e86 | |
|
|
4bb219f8b9 | |
|
|
bfdee80f6c | |
|
|
17c1061676 | |
|
|
d735e27750 | |
|
|
0fb1233363 | |
|
|
7b3199f07c | |
|
|
4b92f4fde4 | |
|
|
666059bd88 | |
|
|
1a6238c835 | |
|
|
d6eb285f02 | |
|
|
1fa6612c08 | |
|
|
5936e4324a | |
|
|
7b8240dca9 | |
|
|
ba46a448d4 | |
|
|
1de0cbea94 | |
|
|
e550f960e8 | |
|
|
f7f159234f | |
|
|
a8bf05d83b | |
|
|
563438f13d | |
|
|
9b251c5667 | |
|
|
985abdc555 | |
|
|
f3cdfac93e | |
|
|
b086eabe5f | |
|
|
d3a9455998 | |
|
|
6e48fb130d | |
|
|
8d8aabb884 | |
|
|
f2e92b4bfb | |
|
|
c5c44168e0 | |
|
|
4be1e0a83c | |
|
|
dfe89047bb | |
|
|
e799a088a5 | |
|
|
5eda329e5a | |
|
|
aebd7578bb | |
|
|
79bfa2b4cd | |
|
|
5b4446f086 | |
|
|
75bd2ea0c5 | |
|
|
fd7e6d0a05 | |
|
|
7d8634bf35 | |
|
|
4a867dc60b | |
|
|
fc0cf90795 | |
|
|
3ac43e8d15 | |
|
|
0e88114882 | |
|
|
f78ddf05c4 | |
|
|
512c0ca5ed | |
|
|
cda7fc3fda | |
|
|
44f5a14f40 | |
|
|
07e03ee776 | |
|
|
6f525f9462 | |
|
|
2d75aca8e3 | |
|
|
163729ecc3 | |
|
|
bf99f0a195 | |
|
|
b9826778b9 | |
|
|
7f4d8c67a8 | |
|
|
4e4235648a | |
|
|
56b0db3359 | |
|
|
181262bdbd | |
|
|
1a14d595fd | |
|
|
f5ce5b47c8 | |
|
|
e28dc62358 | |
|
|
c167146f8c | |
|
|
88811553e4 | |
|
|
aec42540a1 |
|
|
@ -0,0 +1,81 @@
|
|||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
"""Post-edit hook to auto-format files after Claude edits."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def format_rust(file_path: str, cwd: str) -> None:
|
||||
"""Format Rust files with cargo fmt."""
|
||||
try:
|
||||
subprocess.run(
|
||||
["cargo", "fmt", "--", file_path],
|
||||
cwd=cwd,
|
||||
capture_output=True,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def format_python(file_path: str, cwd: str) -> None:
|
||||
"""Format Python files with ruff."""
|
||||
try:
|
||||
subprocess.run(
|
||||
["uvx", "ruff", "format", file_path],
|
||||
cwd=cwd,
|
||||
capture_output=True,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def format_prettier(file_path: str, cwd: str, prose_wrap: bool = False) -> None:
|
||||
"""Format files with prettier."""
|
||||
args = ["npx", "prettier", "--write"]
|
||||
if prose_wrap:
|
||||
args.extend(["--prose-wrap", "always"])
|
||||
args.append(file_path)
|
||||
try:
|
||||
subprocess.run(args, cwd=cwd, capture_output=True)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def main() -> None:
|
||||
import os
|
||||
|
||||
input_data = json.load(sys.stdin)
|
||||
|
||||
tool_name = input_data.get("tool_name")
|
||||
tool_input = input_data.get("tool_input", {})
|
||||
file_path = tool_input.get("file_path")
|
||||
|
||||
# Only process Write, Edit, and MultiEdit tools
|
||||
if tool_name not in ("Write", "Edit", "MultiEdit"):
|
||||
return
|
||||
|
||||
if not file_path:
|
||||
return
|
||||
|
||||
cwd = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
||||
path = Path(file_path)
|
||||
ext = path.suffix
|
||||
|
||||
if ext == ".rs":
|
||||
format_rust(file_path, cwd)
|
||||
elif ext in (".py", ".pyi"):
|
||||
format_python(file_path, cwd)
|
||||
elif ext in (".json5", ".yaml", ".yml"):
|
||||
format_prettier(file_path, cwd)
|
||||
elif ext == ".md":
|
||||
format_prettier(file_path, cwd, prose_wrap=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "uv run .claude/hooks/post-edit-format.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -3,15 +3,14 @@
|
|||
dependencyDashboard: true,
|
||||
suppressNotifications: ["prEditedNotification"],
|
||||
extends: [
|
||||
"config:recommended",
|
||||
"github>astral-sh/renovate-config",
|
||||
// For tool versions defined in GitHub Actions:
|
||||
"customManagers:githubActionsVersions",
|
||||
],
|
||||
labels: ["internal"],
|
||||
schedule: ["before 4am on Monday"],
|
||||
schedule: ["* 0-3 * * 1"],
|
||||
semanticCommits: "disabled",
|
||||
separateMajorMinor: false,
|
||||
prHourlyLimit: 10,
|
||||
enabledManagers: ["github-actions", "pre-commit", "cargo", "custom.regex"],
|
||||
cargo: {
|
||||
// See https://docs.renovatebot.com/configuration-options/#rangestrategy
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ jobs:
|
|||
|
||||
macos-x86_64:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
|
||||
runs-on: macos-14
|
||||
runs-on: depot-macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
|
|
@ -157,7 +157,7 @@ jobs:
|
|||
|
||||
macos-aarch64:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
|
||||
runs-on: macos-14
|
||||
runs-on: depot-macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
|
|
@ -417,7 +417,7 @@ jobs:
|
|||
|
||||
linux-arm:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-22.04-8
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
|
|
@ -956,7 +956,7 @@ jobs:
|
|||
|
||||
musllinux-cross:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-22.04-8
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
|
|
|
|||
|
|
@ -184,13 +184,13 @@ jobs:
|
|||
- buildpack-deps:trixie,trixie,debian
|
||||
- debian:bookworm-slim,bookworm-slim
|
||||
- buildpack-deps:bookworm,bookworm
|
||||
- python:3.14-alpine,python3.14-alpine
|
||||
- python:3.13-alpine,python3.13-alpine
|
||||
- python:3.12-alpine,python3.12-alpine
|
||||
- python:3.11-alpine,python3.11-alpine
|
||||
- python:3.10-alpine,python3.10-alpine
|
||||
- python:3.9-alpine,python3.9-alpine
|
||||
- python:3.8-alpine,python3.8-alpine
|
||||
- python:3.14-alpine3.23,python3.14-alpine3.23,python3.14-alpine
|
||||
- python:3.13-alpine3.23,python3.13-alpine3.23,python3.13-alpine
|
||||
- python:3.12-alpine3.23,python3.12-alpine3.23,python3.12-alpine
|
||||
- python:3.11-alpine3.23,python3.11-alpine3.23,python3.11-alpine
|
||||
- python:3.10-alpine3.23,python3.10-alpine3.23,python3.10-alpine
|
||||
- python:3.9-alpine3.22,python3.9-alpine3.22,python3.9-alpine
|
||||
- python:3.8-alpine3.20,python3.8-alpine3.20,python3.8-alpine
|
||||
- python:3.14-trixie,python3.14-trixie
|
||||
- python:3.13-trixie,python3.13-trixie
|
||||
- python:3.12-trixie,python3.12-trixie
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ jobs:
|
|||
outputs:
|
||||
# Flag that is raised when any code is changed
|
||||
code: ${{ steps.changed.outputs.code_any_changed }}
|
||||
# Flag that is raised when uv.schema.json is changed (e.g., in a release PR)
|
||||
schema: ${{ steps.changed.outputs.schema_changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
|
|
@ -40,10 +42,16 @@ jobs:
|
|||
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha || 'origin/main' }}...HEAD)
|
||||
|
||||
CODE_CHANGED=false
|
||||
SCHEMA_CHANGED=false
|
||||
|
||||
while IFS= read -r file; do
|
||||
# Generated markdown and JSON files are checked during test runs.
|
||||
if [[ "${file}" =~ ^docs/ && ! "${file}" =~ ^docs/reference/(cli|settings).md && ! "${file}" =~ ^docs/reference/environment.md ]]; then
|
||||
# Check if the schema file changed (e.g., in a release PR)
|
||||
if [[ "${file}" == "uv.schema.json" ]]; then
|
||||
echo "Detected schema change: ${file}"
|
||||
SCHEMA_CHANGED=true
|
||||
fi
|
||||
|
||||
if [[ "${file}" =~ ^docs/ ]]; then
|
||||
echo "Skipping ${file} (matches docs/ pattern)"
|
||||
continue
|
||||
fi
|
||||
|
|
@ -70,6 +78,7 @@ jobs:
|
|||
|
||||
done <<< "${CHANGED_FILES}"
|
||||
echo "code_any_changed=${CODE_CHANGED}" >> "${GITHUB_OUTPUT}"
|
||||
echo "schema_changed=${SCHEMA_CHANGED}" >> "${GITHUB_OUTPUT}"
|
||||
lint:
|
||||
timeout-minutes: 10
|
||||
name: "lint"
|
||||
|
|
@ -89,7 +98,7 @@ jobs:
|
|||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
with:
|
||||
version: "0.9.8"
|
||||
version: "0.9.13"
|
||||
|
||||
- name: "rustfmt"
|
||||
run: cargo fmt --all --check
|
||||
|
|
@ -135,7 +144,7 @@ jobs:
|
|||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Check uv_build dependencies"
|
||||
|
|
@ -167,7 +176,7 @@ jobs:
|
|||
run: |
|
||||
Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
workspaces: ${{ env.UV_WORKSPACE }}
|
||||
|
||||
|
|
@ -178,6 +187,22 @@ jobs:
|
|||
working-directory: ${{ env.UV_WORKSPACE }}
|
||||
run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
|
||||
|
||||
cargo-publish-dry-run:
|
||||
timeout-minutes: 20
|
||||
needs: determine_changes
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
runs-on: depot-ubuntu-22.04-8
|
||||
name: "cargo publish dry-run"
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "cargo publish dry-run"
|
||||
run: cargo publish --workspace --dry-run
|
||||
|
||||
cargo-dev-generate-all:
|
||||
timeout-minutes: 10
|
||||
needs: determine_changes
|
||||
|
|
@ -188,11 +213,16 @@ jobs:
|
|||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Generate all"
|
||||
run: cargo dev generate-all --mode check
|
||||
run: cargo dev generate-all --mode dry-run
|
||||
- name: "Check sysconfig mappings"
|
||||
run: cargo dev generate-sysconfig-metadata --mode check
|
||||
- name: "Check JSON schema"
|
||||
if: ${{ needs.determine_changes.outputs.schema == 'true' }}
|
||||
run: cargo dev generate-json-schema --mode check
|
||||
|
||||
cargo-shear:
|
||||
timeout-minutes: 10
|
||||
|
|
@ -203,7 +233,7 @@ jobs:
|
|||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install cargo shear"
|
||||
uses: taiki-e/install-action@a416ddeedbd372e614cc1386e8b642692f66865e # v2.57.1
|
||||
uses: taiki-e/install-action@d850aa816998e5cf15f67a78c7b933f2a5033f8a # v2.63.3
|
||||
with:
|
||||
tool: cargo-shear
|
||||
- run: cargo shear
|
||||
|
|
@ -225,14 +255,14 @@ jobs:
|
|||
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
with:
|
||||
version: "0.9.8"
|
||||
version: "0.9.13"
|
||||
|
||||
- name: "Install required Python versions"
|
||||
run: uv python install
|
||||
|
|
@ -259,12 +289,13 @@ jobs:
|
|||
UV_HTTP_RETRIES: 5
|
||||
run: |
|
||||
cargo nextest run \
|
||||
--cargo-profile fast-build \
|
||||
--features python-patch,native-auth,secret-service \
|
||||
--workspace \
|
||||
--status-level skip --failure-output immediate-final --no-fail-fast -j 20 --final-status-level slow
|
||||
|
||||
cargo-test-macos:
|
||||
timeout-minutes: 15
|
||||
timeout-minutes: 20
|
||||
needs: determine_changes
|
||||
# Only run macOS tests on main without opt-in
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'test:macos') || github.ref == 'refs/heads/main' }}
|
||||
|
|
@ -277,14 +308,14 @@ jobs:
|
|||
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
with:
|
||||
version: "0.9.8"
|
||||
version: "0.9.13"
|
||||
|
||||
- name: "Install required Python versions"
|
||||
run: uv python install
|
||||
|
|
@ -300,8 +331,9 @@ jobs:
|
|||
UV_HTTP_RETRIES: 5
|
||||
run: |
|
||||
cargo nextest run \
|
||||
--cargo-profile fast-build \
|
||||
--no-default-features \
|
||||
--features python,python-managed,pypi,git,performance,crates-io,native-auth,apple-native \
|
||||
--features python,python-managed,pypi,git,git-lfs,performance,crates-io,native-auth,apple-native \
|
||||
--workspace \
|
||||
--status-level skip --failure-output immediate-final --no-fail-fast -j 12 --final-status-level slow
|
||||
|
||||
|
|
@ -326,12 +358,12 @@ jobs:
|
|||
|
||||
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
with:
|
||||
version: "0.9.8"
|
||||
version: "0.9.13"
|
||||
|
||||
- name: "Install required Python versions"
|
||||
run: uv python install
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
workspaces: ${{ env.UV_WORKSPACE }}
|
||||
|
||||
|
|
@ -355,6 +387,7 @@ jobs:
|
|||
shell: bash
|
||||
run: |
|
||||
cargo nextest run \
|
||||
--cargo-profile fast-build \
|
||||
--no-default-features \
|
||||
--features python,pypi,python-managed,native-auth,windows-native \
|
||||
--workspace \
|
||||
|
|
@ -384,7 +417,7 @@ jobs:
|
|||
run: |
|
||||
Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
workspaces: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline
|
||||
|
||||
|
|
@ -444,7 +477,7 @@ jobs:
|
|||
- name: Copy Git Repo to Dev Drive
|
||||
run: |
|
||||
Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
workspaces: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline
|
||||
- name: "Install Rust toolchain"
|
||||
|
|
@ -462,8 +495,8 @@ jobs:
|
|||
working-directory: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline
|
||||
run: |
|
||||
cargo build --target ${{ matrix.target-arch }}-pc-windows-msvc
|
||||
cp target/${{ matrix.target-arch }}-pc-windows-msvc/debug/uv-trampoline-console.exe trampolines/uv-trampoline-${{ matrix.target-arch }}-console.exe
|
||||
cp target/${{ matrix.target-arch }}-pc-windows-msvc/debug/uv-trampoline-gui.exe trampolines/uv-trampoline-${{ matrix.target-arch }}-gui.exe
|
||||
cp target/${{ matrix.target-arch }}-pc-windows-msvc/debug/uv-trampoline-console.exe ../uv-trampoline-builder/trampolines/uv-trampoline-${{ matrix.target-arch }}-console.exe
|
||||
cp target/${{ matrix.target-arch }}-pc-windows-msvc/debug/uv-trampoline-gui.exe ../uv-trampoline-builder/trampolines/uv-trampoline-${{ matrix.target-arch }}-gui.exe
|
||||
- name: "Test new binaries"
|
||||
working-directory: ${{ env.UV_WORKSPACE }}
|
||||
run: |
|
||||
|
|
@ -491,9 +524,17 @@ jobs:
|
|||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
with:
|
||||
version: "0.9.8"
|
||||
version: "0.9.13"
|
||||
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Generate reference documentation"
|
||||
run: |
|
||||
cargo dev generate-options-reference
|
||||
cargo dev generate-cli-reference
|
||||
cargo dev generate-env-vars-reference
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
|
||||
|
|
@ -520,18 +561,18 @@ jobs:
|
|||
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
|
||||
- name: "Build"
|
||||
run: cargo build
|
||||
run: cargo build --profile no-debug
|
||||
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: uv-linux-libc-${{ github.sha }}
|
||||
path: |
|
||||
./target/debug/uv
|
||||
./target/debug/uvx
|
||||
./target/no-debug/uv
|
||||
./target/no-debug/uvx
|
||||
retention-days: 1
|
||||
|
||||
build-binary-linux-aarch64:
|
||||
|
|
@ -547,18 +588,18 @@ jobs:
|
|||
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
|
||||
- name: "Build"
|
||||
run: cargo build
|
||||
run: cargo build --profile no-debug
|
||||
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: uv-linux-aarch64-${{ github.sha }}
|
||||
path: |
|
||||
./target/debug/uv
|
||||
./target/debug/uvx
|
||||
./target/no-debug/uv
|
||||
./target/no-debug/uvx
|
||||
retention-days: 1
|
||||
|
||||
build-binary-linux-musl:
|
||||
|
|
@ -579,18 +620,18 @@ jobs:
|
|||
sudo apt-get install musl-tools
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
|
||||
- name: "Build"
|
||||
run: cargo build --target x86_64-unknown-linux-musl --bin uv --bin uvx
|
||||
run: cargo build --profile no-debug --target x86_64-unknown-linux-musl --bin uv --bin uvx
|
||||
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: uv-linux-musl-${{ github.sha }}
|
||||
path: |
|
||||
./target/x86_64-unknown-linux-musl/debug/uv
|
||||
./target/x86_64-unknown-linux-musl/debug/uvx
|
||||
./target/x86_64-unknown-linux-musl/no-debug/uv
|
||||
./target/x86_64-unknown-linux-musl/no-debug/uvx
|
||||
retention-days: 1
|
||||
|
||||
build-binary-macos-aarch64:
|
||||
|
|
@ -606,17 +647,17 @@ jobs:
|
|||
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
- name: "Build"
|
||||
run: cargo build --bin uv --bin uvx
|
||||
run: cargo build --profile no-debug --bin uv --bin uvx
|
||||
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: uv-macos-aarch64-${{ github.sha }}
|
||||
path: |
|
||||
./target/debug/uv
|
||||
./target/debug/uvx
|
||||
./target/no-debug/uv
|
||||
./target/no-debug/uvx
|
||||
retention-days: 1
|
||||
|
||||
build-binary-macos-x86_64:
|
||||
|
|
@ -632,17 +673,17 @@ jobs:
|
|||
|
||||
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
- name: "Build"
|
||||
run: cargo build --bin uv --bin uvx
|
||||
run: cargo build --profile no-debug --bin uv --bin uvx
|
||||
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: uv-macos-x86_64-${{ github.sha }}
|
||||
path: |
|
||||
./target/debug/uv
|
||||
./target/debug/uvx
|
||||
./target/no-debug/uv
|
||||
./target/no-debug/uvx
|
||||
retention-days: 1
|
||||
|
||||
build-binary-windows-x86_64:
|
||||
|
|
@ -664,21 +705,21 @@ jobs:
|
|||
run: |
|
||||
Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
workspaces: ${{ env.UV_WORKSPACE }}
|
||||
|
||||
- name: "Build"
|
||||
working-directory: ${{ env.UV_WORKSPACE }}
|
||||
run: cargo build --bin uv --bin uvx
|
||||
run: cargo build --profile no-debug --bin uv --bin uvx
|
||||
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: uv-windows-x86_64-${{ github.sha }}
|
||||
path: |
|
||||
${{ env.UV_WORKSPACE }}/target/debug/uv.exe
|
||||
${{ env.UV_WORKSPACE }}/target/debug/uvx.exe
|
||||
${{ env.UV_WORKSPACE }}/target/no-debug/uv.exe
|
||||
${{ env.UV_WORKSPACE }}/target/no-debug/uvx.exe
|
||||
retention-days: 1
|
||||
|
||||
build-binary-windows-aarch64:
|
||||
|
|
@ -701,7 +742,7 @@ jobs:
|
|||
run: |
|
||||
Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
workspaces: ${{ env.UV_WORKSPACE }}
|
||||
|
||||
|
|
@ -710,15 +751,15 @@ jobs:
|
|||
|
||||
- name: "Build"
|
||||
working-directory: ${{ env.UV_WORKSPACE }}
|
||||
run: cargo build --target aarch64-pc-windows-msvc
|
||||
run: cargo build --profile no-debug --target aarch64-pc-windows-msvc
|
||||
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: uv-windows-aarch64-${{ github.sha }}
|
||||
path: |
|
||||
${{ env.UV_WORKSPACE }}/target/aarch64-pc-windows-msvc/debug/uv.exe
|
||||
${{ env.UV_WORKSPACE }}/target/aarch64-pc-windows-msvc/debug/uvx.exe
|
||||
${{ env.UV_WORKSPACE }}/target/aarch64-pc-windows-msvc/no-debug/uv.exe
|
||||
${{ env.UV_WORKSPACE }}/target/aarch64-pc-windows-msvc/no-debug/uvx.exe
|
||||
retention-days: 1
|
||||
|
||||
build-binary-msrv:
|
||||
|
|
@ -742,11 +783,11 @@ jobs:
|
|||
MSRV: ${{ steps.msrv.outputs.value }}
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- run: cargo +${MSRV} build
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
- run: cargo +${MSRV} build --profile no-debug
|
||||
env:
|
||||
MSRV: ${{ steps.msrv.outputs.value }}
|
||||
- run: ./target/debug/uv --version
|
||||
- run: ./target/no-debug/uv --version
|
||||
|
||||
build-binary-freebsd:
|
||||
needs: determine_changes
|
||||
|
|
@ -759,7 +800,7 @@ jobs:
|
|||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
- name: "Cross build"
|
||||
run: |
|
||||
# Install cross from `freebsd-firecracker`
|
||||
|
|
@ -767,7 +808,7 @@ jobs:
|
|||
chmod +x cross
|
||||
mv cross /usr/local/bin/cross
|
||||
|
||||
cross build --target x86_64-unknown-freebsd
|
||||
cross build --target x86_64-unknown-freebsd --profile no-debug
|
||||
|
||||
- name: Test in Firecracker VM
|
||||
uses: acj/freebsd-firecracker-action@a5a3fc1709c5b5368141a5699f10259aca3cd965 # v0.6.0
|
||||
|
|
@ -781,8 +822,8 @@ jobs:
|
|||
cat <<EOF > $include_path
|
||||
target
|
||||
target/x86_64-unknown-freebsd
|
||||
target/x86_64-unknown-freebsd/debug
|
||||
target/x86_64-unknown-freebsd/debug/uv
|
||||
target/x86_64-unknown-freebsd/no-debug
|
||||
target/x86_64-unknown-freebsd/no-debug/uv
|
||||
EOF
|
||||
|
||||
rsync -r -e "ssh" \
|
||||
|
|
@ -792,7 +833,7 @@ jobs:
|
|||
--exclude "*" \
|
||||
. firecracker:
|
||||
run-in-vm: |
|
||||
mv target/x86_64-unknown-freebsd/debug/uv uv
|
||||
mv target/x86_64-unknown-freebsd/no-debug/uv uv
|
||||
chmod +x uv
|
||||
./uv --version
|
||||
|
||||
|
|
@ -1832,7 +1873,7 @@ jobs:
|
|||
run: chmod +x ./uv
|
||||
|
||||
- name: "Configure AWS credentials"
|
||||
uses: aws-actions/configure-aws-credentials@a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 # v5.0.0
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
|
|
@ -1991,6 +2032,7 @@ jobs:
|
|||
UV_TEST_PUBLISH_GITLAB_PAT: ${{ secrets.UV_TEST_PUBLISH_GITLAB_PAT }}
|
||||
UV_TEST_PUBLISH_CODEBERG_TOKEN: ${{ secrets.UV_TEST_PUBLISH_CODEBERG_TOKEN }}
|
||||
UV_TEST_PUBLISH_CLOUDSMITH_TOKEN: ${{ secrets.UV_TEST_PUBLISH_CLOUDSMITH_TOKEN }}
|
||||
UV_TEST_PUBLISH_PYX_TOKEN: ${{ secrets.UV_TEST_PUBLISH_PYX_TOKEN }}
|
||||
UV_TEST_PUBLISH_PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
integration-uv-build-backend:
|
||||
|
|
@ -2025,22 +2067,22 @@ jobs:
|
|||
|
||||
# Test the main path (`build_wheel`) through pip
|
||||
./uv venv -v --seed
|
||||
./uv run --no-project python -m pip install -v scripts/packages/built-by-uv --find-links crates/uv-build/dist --no-index --no-deps
|
||||
./uv run --no-project python -m pip install -v test/packages/built-by-uv --find-links crates/uv-build/dist --no-index --no-deps
|
||||
./uv run --no-project python -c "from built_by_uv import greet; print(greet())"
|
||||
|
||||
# Test both `build_wheel` and `build_sdist` through uv
|
||||
./uv venv -c -v
|
||||
./uv build -v --force-pep517 scripts/packages/built-by-uv --find-links crates/uv-build/dist --offline
|
||||
./uv pip install -v scripts/packages/built-by-uv/dist/*.tar.gz --find-links crates/uv-build/dist --offline --no-deps
|
||||
./uv build -v --force-pep517 test/packages/built-by-uv --find-links crates/uv-build/dist --offline
|
||||
./uv pip install -v test/packages/built-by-uv/dist/*.tar.gz --find-links crates/uv-build/dist --offline --no-deps
|
||||
./uv run --no-project python -c "from built_by_uv import greet; print(greet())"
|
||||
|
||||
# Test both `build_wheel` and `build_sdist` through the official `build`
|
||||
rm -rf scripts/packages/built-by-uv/dist/
|
||||
rm -rf test/packages/built-by-uv/dist/
|
||||
./uv venv -c -v
|
||||
./uv pip install build
|
||||
# Add the uv binary to PATH for `build` to find
|
||||
PATH="$(pwd):$PATH" UV_OFFLINE=1 UV_FIND_LINKS=crates/uv-build/dist ./uv run --no-project python -m build -v --installer uv scripts/packages/built-by-uv
|
||||
./uv pip install -v scripts/packages/built-by-uv/dist/*.tar.gz --find-links crates/uv-build/dist --offline --no-deps
|
||||
PATH="$(pwd):$PATH" UV_OFFLINE=1 UV_FIND_LINKS=crates/uv-build/dist ./uv run --no-project python -m build -v --installer uv test/packages/built-by-uv
|
||||
./uv pip install -v test/packages/built-by-uv/dist/*.tar.gz --find-links crates/uv-build/dist --offline --no-deps
|
||||
./uv run --no-project python -c "from built_by_uv import greet; print(greet())"
|
||||
|
||||
cache-test-ubuntu:
|
||||
|
|
@ -2882,14 +2924,14 @@ jobs:
|
|||
runs-on: codspeed-macro
|
||||
needs: determine_changes
|
||||
if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: "Checkout Branch"
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
|
@ -2904,8 +2946,8 @@ jobs:
|
|||
sudo apt-get update
|
||||
sudo apt-get install -y libsasl2-dev libldap2-dev libkrb5-dev
|
||||
cargo run --bin uv -- venv --cache-dir .cache
|
||||
cargo run --bin uv -- pip compile scripts/requirements/jupyter.in --universal --exclude-newer 2024-08-08 --cache-dir .cache
|
||||
cargo run --bin uv -- pip compile scripts/requirements/airflow.in --universal --exclude-newer 2024-08-08 --cache-dir .cache
|
||||
cargo run --bin uv -- pip compile test/requirements/jupyter.in --universal --exclude-newer 2024-08-08 --cache-dir .cache
|
||||
cargo run --bin uv -- pip compile test/requirements/airflow.in --universal --exclude-newer 2024-08-08 --cache-dir .cache
|
||||
|
||||
- name: "Build benchmarks"
|
||||
run: cargo codspeed build --profile profiling -p uv-bench
|
||||
|
|
@ -2929,7 +2971,7 @@ jobs:
|
|||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
|
@ -2944,8 +2986,8 @@ jobs:
|
|||
sudo apt-get update
|
||||
sudo apt-get install -y libsasl2-dev libldap2-dev libkrb5-dev
|
||||
cargo run --bin uv -- venv --cache-dir .cache
|
||||
cargo run --bin uv -- pip compile scripts/requirements/jupyter.in --universal --exclude-newer 2024-08-08 --cache-dir .cache
|
||||
cargo run --bin uv -- pip compile scripts/requirements/airflow.in --universal --exclude-newer 2024-08-08 --cache-dir .cache
|
||||
cargo run --bin uv -- pip compile test/requirements/jupyter.in --universal --exclude-newer 2024-08-08 --cache-dir .cache
|
||||
cargo run --bin uv -- pip compile test/requirements/airflow.in --universal --exclude-newer 2024-08-08 --cache-dir .cache
|
||||
|
||||
- name: "Build benchmarks"
|
||||
run: cargo codspeed build --profile profiling -p uv-bench
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
# Publish a release to crates.io.
|
||||
#
|
||||
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a publish job
|
||||
# within `cargo-dist`.
|
||||
name: "Publish to crates.io"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
plan:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
crates-publish-uv:
|
||||
name: Upload uv to crates.io
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
# TODO(zanieb): Switch to trusted publishing once published
|
||||
# - uses: rust-lang/crates-io-auth-action@v1
|
||||
# id: auth
|
||||
- name: Publish workspace crates
|
||||
# Note `--no-verify` is safe because we do a publish dry-run elsewhere in CI
|
||||
run: cargo publish --workspace --no-verify
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_TOKEN }}
|
||||
|
|
@ -36,6 +36,14 @@ jobs:
|
|||
with:
|
||||
python-version: 3.12
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
|
||||
- name: "Generate reference documentation"
|
||||
run: |
|
||||
cargo dev generate-options-reference
|
||||
cargo dev generate-cli-reference
|
||||
cargo dev generate-env-vars-reference
|
||||
|
||||
- name: "Set docs display name"
|
||||
run: |
|
||||
version="${VERSION}"
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ jobs:
|
|||
environment:
|
||||
name: release
|
||||
permissions:
|
||||
# For PyPI's trusted publishing.
|
||||
id-token: write
|
||||
id-token: write # For PyPI's trusted publishing
|
||||
steps:
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
|
|
@ -37,8 +36,7 @@ jobs:
|
|||
environment:
|
||||
name: release
|
||||
permissions:
|
||||
# For PyPI's trusted publishing.
|
||||
id-token: write
|
||||
id-token: write # For PyPI's trusted publishing
|
||||
steps:
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
|
|
|
|||
|
|
@ -222,16 +222,31 @@ jobs:
|
|||
"id-token": "write"
|
||||
"packages": "write"
|
||||
|
||||
custom-publish-crates:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
- custom-publish-pypi # DIRTY: see #16989
|
||||
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
|
||||
uses: ./.github/workflows/publish-crates.yml
|
||||
with:
|
||||
plan: ${{ needs.plan.outputs.val }}
|
||||
secrets: inherit
|
||||
# publish jobs get escalated permissions
|
||||
permissions:
|
||||
"contents": "read"
|
||||
|
||||
# Create a GitHub Release while uploading all files to it
|
||||
announce:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
- custom-publish-pypi
|
||||
- custom-publish-crates
|
||||
# use "always() && ..." to allow us to wait for all publish jobs while
|
||||
# still allowing individual publish jobs to skip themselves (for prereleases).
|
||||
# "host" however must run to completion, no skipping allowed!
|
||||
if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') }}
|
||||
if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') && (needs.custom-publish-crates.result == 'skipped' || needs.custom-publish-crates.result == 'success') }}
|
||||
runs-on: "depot-ubuntu-latest-4"
|
||||
permissions:
|
||||
"attestations": "write"
|
||||
|
|
|
|||
|
|
@ -49,3 +49,4 @@ jobs:
|
|||
title: "Sync latest Python releases"
|
||||
body: "Automated update for Python releases."
|
||||
base: "main"
|
||||
draft: true
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@ profile.json.gz
|
|||
# MkDocs
|
||||
/site
|
||||
|
||||
# Generated reference docs (use `cargo dev generate-all` to regenerate)
|
||||
/docs/reference/cli.md
|
||||
/docs/reference/environment.md
|
||||
/docs/reference/settings.md
|
||||
|
||||
# macOS
|
||||
**/.DS_Store
|
||||
|
||||
|
|
|
|||
|
|
@ -4,5 +4,5 @@ PREVIEW-CHANGELOG.md
|
|||
docs/reference/cli.md
|
||||
docs/reference/settings.md
|
||||
docs/reference/environment.md
|
||||
ecosystem/home-assistant-core/LICENSE.md
|
||||
test/ecosystem/home-assistant-core/LICENSE.md
|
||||
docs/guides/integration/gitlab.md
|
||||
|
|
|
|||
260
CHANGELOG.md
260
CHANGELOG.md
|
|
@ -3,6 +3,253 @@
|
|||
<!-- prettier-ignore-start -->
|
||||
|
||||
|
||||
## 0.9.18
|
||||
|
||||
Released on 2025-12-16.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Add value hints to command line arguments to improve shell completion accuracy ([#17080](https://github.com/astral-sh/uv/pull/17080))
|
||||
- Improve error handling in `uv publish` ([#17096](https://github.com/astral-sh/uv/pull/17096))
|
||||
- Improve rendering of multiline error messages ([#17132](https://github.com/astral-sh/uv/pull/17132))
|
||||
- Support redirects in `uv publish` ([#17130](https://github.com/astral-sh/uv/pull/17130))
|
||||
- Include Docker images with the alpine version, e.g., `python3.x-alpine3.23` ([#17100](https://github.com/astral-sh/uv/pull/17100))
|
||||
|
||||
### Configuration
|
||||
|
||||
- Accept `--torch-backend` in `[tool.uv]` ([#17116](https://github.com/astral-sh/uv/pull/17116))
|
||||
|
||||
### Performance
|
||||
|
||||
- Speed up `uv cache size` ([#17015](https://github.com/astral-sh/uv/pull/17015))
|
||||
- Initialize S3 signer once ([#17092](https://github.com/astral-sh/uv/pull/17092))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Avoid panics due to reads on failed requests ([#17098](https://github.com/astral-sh/uv/pull/17098))
|
||||
- Enforce latest-version in `@latest` requests ([#17114](https://github.com/astral-sh/uv/pull/17114))
|
||||
- Explicitly set `EntryType` for file entries in tar ([#17043](https://github.com/astral-sh/uv/pull/17043))
|
||||
- Ignore `pyproject.toml` index username in lockfile comparison ([#16995](https://github.com/astral-sh/uv/pull/16995))
|
||||
- Relax error when using `uv add` with `UV_GIT_LFS` set ([#17127](https://github.com/astral-sh/uv/pull/17127))
|
||||
- Support file locks on ExFAT on macOS ([#17115](https://github.com/astral-sh/uv/pull/17115))
|
||||
- Change schema for `exclude-newer` into optional string ([#17121](https://github.com/astral-sh/uv/pull/17121))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Drop arm musl caveat from Docker documentation ([#17111](https://github.com/astral-sh/uv/pull/17111))
|
||||
- Fix version reference in resolver example ([#17085](https://github.com/astral-sh/uv/pull/17085))
|
||||
- Better documentation for `exclude-newer*` ([#17079](https://github.com/astral-sh/uv/pull/17079))
|
||||
|
||||
## 0.9.17
|
||||
|
||||
Released on 2025-12-09.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Add `torch-tensorrt` and `torchao` to the PyTorch list ([#17053](https://github.com/astral-sh/uv/pull/17053))
|
||||
- Add hint for misplaced `--verbose` in `uv tool run` ([#17020](https://github.com/astral-sh/uv/pull/17020))
|
||||
- Add support for relative durations in `exclude-newer` (a.k.a., dependency cooldowns) ([#16814](https://github.com/astral-sh/uv/pull/16814))
|
||||
- Add support for relocatable nushell activation script ([#17036](https://github.com/astral-sh/uv/pull/17036))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Respect dropped (but explicit) indexes in dependency groups ([#17012](https://github.com/astral-sh/uv/pull/17012))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Improve `source-exclude` reference docs ([#16832](https://github.com/astral-sh/uv/pull/16832))
|
||||
- Recommend `UV_NO_DEV` in Docker installs ([#17030](https://github.com/astral-sh/uv/pull/17030))
|
||||
- Update `UV_VERSION` in docs for GitLab CI/CD ([#17040](https://github.com/astral-sh/uv/pull/17040))
|
||||
|
||||
## 0.9.16
|
||||
|
||||
Released on 2025-12-06.
|
||||
|
||||
### Python
|
||||
|
||||
- Add CPython 3.14.2
|
||||
- Add CPython 3.13.11
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Add a 5m default timeout to acquiring file locks to fail faster on deadlock ([#16342](https://github.com/astral-sh/uv/pull/16342))
|
||||
- Add a stub `debug` subcommand to `uv pip` announcing its intentional absence ([#16966](https://github.com/astral-sh/uv/pull/16966))
|
||||
- Add bounds in `uv add --script` ([#16954](https://github.com/astral-sh/uv/pull/16954))
|
||||
- Add brew specific message for `uv self update` ([#16838](https://github.com/astral-sh/uv/pull/16838))
|
||||
- Error when built wheel is for the wrong platform ([#16074](https://github.com/astral-sh/uv/pull/16074))
|
||||
- Filter wheels from PEP 751 files based on `--no-binary` et al in `uv pip compile` ([#16956](https://github.com/astral-sh/uv/pull/16956))
|
||||
- Support `--target` and `--prefix` in `uv pip list`, `uv pip freeze`, and `uv pip show` ([#16955](https://github.com/astral-sh/uv/pull/16955))
|
||||
- Tweak language for build backend validation errors ([#16720](https://github.com/astral-sh/uv/pull/16720))
|
||||
- Use explicit credentials cache instead of global static ([#16768](https://github.com/astral-sh/uv/pull/16768))
|
||||
- Enable SIMD in HTML parsing ([#17010](https://github.com/astral-sh/uv/pull/17010))
|
||||
|
||||
### Preview features
|
||||
|
||||
- Fix missing preview warning in `uv workspace metadata` ([#16988](https://github.com/astral-sh/uv/pull/16988))
|
||||
- Add a `uv auth helper --protocol bazel` command ([#16886](https://github.com/astral-sh/uv/pull/16886))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fix Pyston wheel compatibility tags ([#16972](https://github.com/astral-sh/uv/pull/16972))
|
||||
- Allow redundant entries in `tool.uv.build-backend.module-name` but emit warnings ([#16928](https://github.com/astral-sh/uv/pull/16928))
|
||||
- Fix infinite loop in non-attribute re-treats during HTML parsing ([#17010](https://github.com/astral-sh/uv/pull/17010))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Clarify `--project` flag help text to indicate project discovery ([#16965](https://github.com/astral-sh/uv/pull/16965))
|
||||
- Regenerate the crates.io READMEs on release ([#16992](https://github.com/astral-sh/uv/pull/16992))
|
||||
- Update Docker integration guide to prefer `COPY` over `ADD` for simple cases ([#16883](https://github.com/astral-sh/uv/pull/16883))
|
||||
- Update PyTorch documentation to include information about supporting CUDA 13.0.x ([#16957](https://github.com/astral-sh/uv/pull/16957))
|
||||
- Update the versioning policy ([#16710](https://github.com/astral-sh/uv/pull/16710))
|
||||
- Upgrade PyTorch documentation to latest versions ([#16970](https://github.com/astral-sh/uv/pull/16970))
|
||||
|
||||
## 0.9.15
|
||||
|
||||
Released on 2025-12-02.
|
||||
|
||||
### Python
|
||||
|
||||
- Add CPython 3.14.1
|
||||
- Add CPython 3.13.10
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Add ROCm 6.4 to `--torch-backend=auto` ([#16919](https://github.com/astral-sh/uv/pull/16919))
|
||||
- Add a Windows manifest to uv binaries ([#16894](https://github.com/astral-sh/uv/pull/16894))
|
||||
- Add LFS toggle to Git sources ([#16143](https://github.com/astral-sh/uv/pull/16143))
|
||||
- Cache source reads during resolution ([#16888](https://github.com/astral-sh/uv/pull/16888))
|
||||
- Allow reading requirements from scripts without an extension ([#16923](https://github.com/astral-sh/uv/pull/16923))
|
||||
- Allow reading requirements from scripts with HTTP(S) paths ([#16891](https://github.com/astral-sh/uv/pull/16891))
|
||||
|
||||
### Configuration
|
||||
|
||||
- Add `UV_HIDE_BUILD_OUTPUT` to omit build logs ([#16885](https://github.com/astral-sh/uv/pull/16885))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fix `uv-trampoline-builder` builds from crates.io by moving bundled executables ([#16922](https://github.com/astral-sh/uv/pull/16922))
|
||||
- Respect `NO_COLOR` and always show the command as a header when paging `uv help` output ([#16908](https://github.com/astral-sh/uv/pull/16908))
|
||||
- Use `0o666` permissions for flock files instead of `0o777` ([#16845](https://github.com/astral-sh/uv/pull/16845))
|
||||
- Revert "Bump `astral-tl` to v0.7.10 (#16887)" to narrow down a regression causing hangs in metadata retrieval ([#16938](https://github.com/astral-sh/uv/pull/16938))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Link to the uv version in crates.io member READMEs ([#16939](https://github.com/astral-sh/uv/pull/16939))
|
||||
|
||||
## 0.9.14
|
||||
|
||||
Released on 2025-12-01.
|
||||
|
||||
### Performance
|
||||
|
||||
- Bump `astral-tl` to v0.7.10 to enable SIMD for HTML parsing ([#16887](https://github.com/astral-sh/uv/pull/16887))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Allow earlier post releases with exclusive ordering ([#16881](https://github.com/astral-sh/uv/pull/16881))
|
||||
- Prefer updating existing `.zshenv` over creating a new one in `tool update-shell` ([#16866](https://github.com/astral-sh/uv/pull/16866))
|
||||
- Respect `-e` flags in `uv add` ([#16882](https://github.com/astral-sh/uv/pull/16882))
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Attach subcommand to User-Agent string ([#16837](https://github.com/astral-sh/uv/pull/16837))
|
||||
- Prefer `UV_WORKING_DIR` over `UV_WORKING_DIRECTORY` for consistency ([#16884](https://github.com/astral-sh/uv/pull/16884))
|
||||
|
||||
## 0.9.13
|
||||
|
||||
Released on 2025-11-26.
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Revert "Allow `--with-requirements` to load extensionless inline-metadata scripts" to fix reading of requirements files from streams ([#16861](https://github.com/astral-sh/uv/pull/16861))
|
||||
- Validate URL wheel tags against `Requires-Python` and required environments ([#16824](https://github.com/astral-sh/uv/pull/16824))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Drop unpublished crates from the uv crates.io README ([#16847](https://github.com/astral-sh/uv/pull/16847))
|
||||
- Fix the links to uv in crates.io member READMEs ([#16848](https://github.com/astral-sh/uv/pull/16848))
|
||||
|
||||
## 0.9.12
|
||||
|
||||
Released on 2025-11-24.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Allow `--with-requirements` to load extensionless inline-metadata scripts ([#16744](https://github.com/astral-sh/uv/pull/16744))
|
||||
- Collect and upload PEP 740 attestations during `uv publish` ([#16731](https://github.com/astral-sh/uv/pull/16731))
|
||||
- Prevent `uv export` from overwriting `pyproject.toml` ([#16745](https://github.com/astral-sh/uv/pull/16745))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add a crates.io README for uv ([#16809](https://github.com/astral-sh/uv/pull/16809))
|
||||
- Add documentation for intermediate Docker layers in a workspace ([#16787](https://github.com/astral-sh/uv/pull/16787))
|
||||
- Enumerate workspace members in the uv crate README ([#16811](https://github.com/astral-sh/uv/pull/16811))
|
||||
- Fix documentation links for crates ([#16801](https://github.com/astral-sh/uv/pull/16801))
|
||||
- Generate a crates.io README for uv workspace members ([#16812](https://github.com/astral-sh/uv/pull/16812))
|
||||
- Move the "Export" guide to the projects concept section ([#16835](https://github.com/astral-sh/uv/pull/16835))
|
||||
- Update the cargo install recommendation to use crates ([#16800](https://github.com/astral-sh/uv/pull/16800))
|
||||
- Use the word "internal" in crate descriptions ([#16810](https://github.com/astral-sh/uv/pull/16810))
|
||||
|
||||
## 0.9.11
|
||||
|
||||
Released on 2025-11-20.
|
||||
|
||||
### Python
|
||||
|
||||
- Add CPython 3.15.0a2
|
||||
|
||||
See the [`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20251120) for details.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Add SBOM support to `uv export` ([#16523](https://github.com/astral-sh/uv/pull/16523))
|
||||
- Publish to `crates.io` ([#16770](https://github.com/astral-sh/uv/pull/16770))
|
||||
|
||||
### Preview features
|
||||
|
||||
- Add `uv workspace list --paths` ([#16776](https://github.com/astral-sh/uv/pull/16776))
|
||||
- Fix the preview warning on `uv workspace dir` ([#16775](https://github.com/astral-sh/uv/pull/16775))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fix `uv init` author serialization via `toml_edit` inline tables ([#16778](https://github.com/astral-sh/uv/pull/16778))
|
||||
- Fix status messages without TTY ([#16785](https://github.com/astral-sh/uv/pull/16785))
|
||||
- Preserve end-of-line comment whitespace when editing `pyproject.toml` ([#16734](https://github.com/astral-sh/uv/pull/16734))
|
||||
- Disable `always-authenticate` when running under Dependabot ([#16773](https://github.com/astral-sh/uv/pull/16773))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Document the new behavior for free-threaded python versions ([#16781](https://github.com/astral-sh/uv/pull/16781))
|
||||
- Improve note about build system in publish guide ([#16788](https://github.com/astral-sh/uv/pull/16788))
|
||||
- Move do not upload publish note out of the guide into concepts ([#16789](https://github.com/astral-sh/uv/pull/16789))
|
||||
|
||||
## 0.9.10
|
||||
|
||||
Released on 2025-11-17.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Add support for `SSL_CERT_DIR` ([#16473](https://github.com/astral-sh/uv/pull/16473))
|
||||
- Enforce UTF‑8-encoded license files during `uv build` ([#16699](https://github.com/astral-sh/uv/pull/16699))
|
||||
- Error when a `project.license-files` glob matches nothing ([#16697](https://github.com/astral-sh/uv/pull/16697))
|
||||
- `pip install --target` (and `sync`) install Python if necessary ([#16694](https://github.com/astral-sh/uv/pull/16694))
|
||||
- Account for `python_downloads_json_url` in pre-release Python version warnings ([#16737](https://github.com/astral-sh/uv/pull/16737))
|
||||
- Support HTTP/HTTPS URLs in `uv python --python-downloads-json-url` ([#16542](https://github.com/astral-sh/uv/pull/16542))
|
||||
|
||||
### Preview features
|
||||
|
||||
- Add support for `--upgrade` in `uv python install` ([#16676](https://github.com/astral-sh/uv/pull/16676))
|
||||
- Fix handling of `python install --default` for pre-release Python versions ([#16706](https://github.com/astral-sh/uv/pull/16706))
|
||||
- Add `uv workspace list` to list workspace members ([#16691](https://github.com/astral-sh/uv/pull/16691))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Don't check file URLs for ambiguously parsed credentials ([#16759](https://github.com/astral-sh/uv/pull/16759))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add a "storage" reference document ([#15954](https://github.com/astral-sh/uv/pull/15954))
|
||||
|
||||
## 0.9.9
|
||||
|
||||
Released on 2025-11-12.
|
||||
|
|
@ -38,7 +285,6 @@ Released on 2025-11-12.
|
|||
|
||||
- Fix `CMD` path in FastAPI Dockerfile ([#16701](https://github.com/astral-sh/uv/pull/16701))
|
||||
|
||||
|
||||
## 0.9.8
|
||||
|
||||
Released on 2025-11-07.
|
||||
|
|
@ -257,25 +503,25 @@ There are no breaking changes to [`uv_build`](https://docs.astral.sh/uv/concepts
|
|||
### Breaking changes
|
||||
|
||||
- **Python 3.14 is now the default stable version**
|
||||
|
||||
|
||||
The default Python version has changed from 3.13 to 3.14. This applies to Python version installation when no Python version is requested, e.g., `uv python install`. By default, uv will use the system Python version if present, so this may not cause changes to general use of uv. For example, if Python 3.13 is installed already, then `uv venv` will use that version. If no Python versions are installed on a machine and automatic downloads are enabled, uv will now use 3.14 instead of 3.13, e.g., for `uv venv` or `uvx python`. This change will not affect users who are using a `.python-version` file to pin to a specific Python version.
|
||||
- **Allow use of free-threaded variants in Python 3.14+ without explicit opt-in** ([#16142](https://github.com/astral-sh/uv/pull/16142))
|
||||
|
||||
|
||||
Previously, free-threaded variants of Python were considered experimental and required explicit opt-in (i.e., with `3.14t`) for usage. Now uv will allow use of free-threaded Python 3.14+ interpreters without explicit selection. The GIL-enabled build of Python will still be preferred, e.g., when performing an installation with `uv python install 3.14`. However, e.g., if a free-threaded interpreter comes before a GIL-enabled build on the `PATH`, it will be used. This change does not apply to free-threaded Python 3.13 interpreters, which will continue to require opt-in.
|
||||
- **Use Python 3.14 stable Docker images** ([#16150](https://github.com/astral-sh/uv/pull/16150))
|
||||
|
||||
|
||||
Previously, the Python 3.14 images had an `-rc` suffix, e.g., `python:3.14-rc-alpine` or
|
||||
`python:3.14-rc-trixie`. Now, the `-rc` suffix has been removed to match the stable
|
||||
[upstream images](https://hub.docker.com/_/python). The `-rc` images tags will no longer be
|
||||
updated. This change should not break existing workflows.
|
||||
- **Upgrade Alpine Docker image to Alpine 3.22**
|
||||
|
||||
|
||||
Previously, the `uv:alpine` Docker image was based on Alpine 3.21. Now, this image is based on Alpine 3.22. The previous image can be recovered with `uv:alpine3.21` and will continue to be updated until a future release.
|
||||
- **Upgrade Debian Docker images to Debian 13 "Trixie"**
|
||||
|
||||
|
||||
Previously, the `uv:debian` and `uv:debian-slim` Docker images were based on Debian 12 "Bookworm". Now, these images are based on Debian 13 "Trixie". The previous images can be recovered with `uv:bookworm` and `uv:bookworm-slim` and will continue to be updated until a future release.
|
||||
- **Fix incorrect output path when a trailing `/` is used in `uv build`** ([#15133](https://github.com/astral-sh/uv/pull/15133))
|
||||
|
||||
|
||||
When using `uv build` in a workspace, the artifacts are intended to be written to a `dist` directory in the workspace root. A bug caused workspace root determination to fail when the input path included a trailing `/` causing the `dist` directory to be placed in the child directory. This bug has been fixed in this release. For example, `uv build child/` is used, the output path will now be in `<workspace root>/dist/` rather than `<workspace root>/child/dist/`.
|
||||
|
||||
### Python
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
- [Our Pledge](#our-pledge)
|
||||
- [Our Standards](#our-standards)
|
||||
- [Enforcement Responsibilities](#enforcement-responsibilities)
|
||||
- [Scope](#scope)
|
||||
- [Enforcement](#enforcement)
|
||||
- [Enforcement Guidelines](#enforcement-guidelines)
|
||||
- [1. Correction](#1-correction)
|
||||
- [2. Warning](#2-warning)
|
||||
- [3. Temporary Ban](#3-temporary-ban)
|
||||
- [4. Permanent Ban](#4-permanent-ban)
|
||||
- [Attribution](#attribution)
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our community a
|
||||
harassment-free experience for everyone, regardless of age, body size, visible or invisible
|
||||
disability, ethnicity, sex characteristics, gender identity and expression, level of experience,
|
||||
education, socio-economic status, nationality, personal appearance, race, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and
|
||||
healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the
|
||||
experience
|
||||
- Focusing on what is best not just for us as individuals, but for the overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email address, without their
|
||||
explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior
|
||||
and will take appropriate and fair corrective action in response to any behavior that they deem
|
||||
inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject comments, commits,
|
||||
code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and
|
||||
will communicate reasons for moderation decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when an individual is
|
||||
officially representing the community in public spaces. Examples of representing our community
|
||||
include using an official e-mail address, posting via an official social media account, or acting as
|
||||
an appointed representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community
|
||||
leaders responsible for enforcement at <hey@astral.sh>. All complaints will be reviewed and
|
||||
investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the reporter of any
|
||||
incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining the consequences for
|
||||
any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or
|
||||
unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing clarity around the
|
||||
nature of the violation and an explanation of why the behavior was inappropriate. A public apology
|
||||
may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No interaction with the people
|
||||
involved, including unsolicited interaction with those enforcing the Code of Conduct, for a
|
||||
specified period of time. This includes avoiding interactions in community spaces as well as
|
||||
external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including sustained inappropriate
|
||||
behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public communication with the
|
||||
community for a specified period of time. No public or private interaction with the people involved,
|
||||
including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this
|
||||
period. Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community standards, including
|
||||
sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement
|
||||
of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available
|
||||
[here](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html).
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
For answers to common questions about this code of conduct, see the
|
||||
[FAQ](https://www.contributor-covenant.org/faq). Translations are available
|
||||
[here](https://www.contributor-covenant.org/translations).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
|
@ -86,6 +86,13 @@ cargo test --package <package> --test <test> -- <test_name> -- --exact
|
|||
cargo insta review
|
||||
```
|
||||
|
||||
### Git and Git LFS
|
||||
|
||||
A subset of uv tests require both [Git](https://git-scm.com) and [Git LFS](https://git-lfs.com/) to
|
||||
execute properly.
|
||||
|
||||
These tests can be disabled by turning off either `git` or `git-lfs` uv features.
|
||||
|
||||
### Local testing
|
||||
|
||||
You can invoke your development version of uv with `cargo run -- <args>`. For example:
|
||||
|
|
@ -95,6 +102,15 @@ cargo run -- venv
|
|||
cargo run -- pip install requests
|
||||
```
|
||||
|
||||
## Crate structure
|
||||
|
||||
Rust does not allow circular dependencies between crates. To visualize the crate hierarchy, install
|
||||
[cargo-depgraph](https://github.com/jplatte/cargo-depgraph) and graphviz, then run:
|
||||
|
||||
```shell
|
||||
cargo depgraph --dedup-transitive-deps --workspace-only | dot -Tpng > graph.png
|
||||
```
|
||||
|
||||
## Running inside a Docker container
|
||||
|
||||
Source distributions can run arbitrary code on build and can make unwanted modifications to your
|
||||
|
|
@ -120,7 +136,7 @@ Please refer to Ruff's
|
|||
it applies to uv, too.
|
||||
|
||||
We provide diverse sets of requirements for testing and benchmarking the resolver in
|
||||
`scripts/requirements` and for the installer in `scripts/requirements/compiled`.
|
||||
`test/requirements` and for the installer in `test/requirements/compiled`.
|
||||
|
||||
You can use `scripts/benchmark` to benchmark predefined workloads between uv versions and with other
|
||||
tools, e.g., from the `scripts/benchmark` directory:
|
||||
|
|
@ -131,7 +147,7 @@ uv run resolver \
|
|||
--poetry \
|
||||
--benchmark \
|
||||
resolve-cold \
|
||||
../scripts/requirements/trio.in
|
||||
../test/requirements/trio.in
|
||||
```
|
||||
|
||||
### Analyzing concurrency
|
||||
|
|
@ -141,7 +157,7 @@ visualize parallel requests and find any spots where uv is CPU-bound. Example us
|
|||
`uv-dev` respectively:
|
||||
|
||||
```shell
|
||||
RUST_LOG=uv=info TRACING_DURATIONS_FILE=target/traces/jupyter.ndjson cargo run --features tracing-durations-export --profile profiling -- pip compile scripts/requirements/jupyter.in
|
||||
RUST_LOG=uv=info TRACING_DURATIONS_FILE=target/traces/jupyter.ndjson cargo run --features tracing-durations-export --profile profiling -- pip compile test/requirements/jupyter.in
|
||||
```
|
||||
|
||||
```shell
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
164
Cargo.toml
164
Cargo.toml
|
|
@ -4,9 +4,6 @@ exclude = [
|
|||
"scripts",
|
||||
# Needs nightly
|
||||
"crates/uv-trampoline",
|
||||
# Only used to pull in features, allocators, etc. — we specifically don't want them
|
||||
# to be part of a workspace-wide cargo check, cargo clippy, etc.
|
||||
"crates/uv-performance-memory-allocator",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
|
@ -14,71 +11,71 @@ resolver = "2"
|
|||
edition = "2024"
|
||||
rust-version = "1.89"
|
||||
homepage = "https://pypi.org/project/uv/"
|
||||
documentation = "https://pypi.org/project/uv/"
|
||||
repository = "https://github.com/astral-sh/uv"
|
||||
authors = ["uv"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
uv-auth = { path = "crates/uv-auth" }
|
||||
uv-bin-install = { path = "crates/uv-bin-install" }
|
||||
uv-build-backend = { path = "crates/uv-build-backend" }
|
||||
uv-build-frontend = { path = "crates/uv-build-frontend" }
|
||||
uv-cache = { path = "crates/uv-cache" }
|
||||
uv-cache-info = { path = "crates/uv-cache-info" }
|
||||
uv-cache-key = { path = "crates/uv-cache-key" }
|
||||
uv-cli = { path = "crates/uv-cli" }
|
||||
uv-client = { path = "crates/uv-client" }
|
||||
uv-configuration = { path = "crates/uv-configuration" }
|
||||
uv-console = { path = "crates/uv-console" }
|
||||
uv-dirs = { path = "crates/uv-dirs" }
|
||||
uv-dispatch = { path = "crates/uv-dispatch" }
|
||||
uv-distribution = { path = "crates/uv-distribution" }
|
||||
uv-distribution-filename = { path = "crates/uv-distribution-filename" }
|
||||
uv-distribution-types = { path = "crates/uv-distribution-types" }
|
||||
uv-extract = { path = "crates/uv-extract" }
|
||||
uv-flags = { path = "crates/uv-flags" }
|
||||
uv-fs = { path = "crates/uv-fs", features = ["serde", "tokio"] }
|
||||
uv-git = { path = "crates/uv-git" }
|
||||
uv-git-types = { path = "crates/uv-git-types" }
|
||||
uv-globfilter = { path = "crates/uv-globfilter" }
|
||||
uv-install-wheel = { path = "crates/uv-install-wheel", default-features = false }
|
||||
uv-installer = { path = "crates/uv-installer" }
|
||||
uv-keyring = { path = "crates/uv-keyring" }
|
||||
uv-logging = { path = "crates/uv-logging" }
|
||||
uv-macros = { path = "crates/uv-macros" }
|
||||
uv-metadata = { path = "crates/uv-metadata" }
|
||||
uv-normalize = { path = "crates/uv-normalize" }
|
||||
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-preview = { path = "crates/uv-preview" }
|
||||
uv-publish = { path = "crates/uv-publish" }
|
||||
uv-pypi-types = { path = "crates/uv-pypi-types" }
|
||||
uv-python = { path = "crates/uv-python" }
|
||||
uv-redacted = { path = "crates/uv-redacted" }
|
||||
uv-requirements = { path = "crates/uv-requirements" }
|
||||
uv-requirements-txt = { path = "crates/uv-requirements-txt" }
|
||||
uv-resolver = { path = "crates/uv-resolver" }
|
||||
uv-scripts = { path = "crates/uv-scripts" }
|
||||
uv-settings = { path = "crates/uv-settings" }
|
||||
uv-shell = { path = "crates/uv-shell" }
|
||||
uv-small-str = { path = "crates/uv-small-str" }
|
||||
uv-state = { path = "crates/uv-state" }
|
||||
uv-static = { path = "crates/uv-static" }
|
||||
uv-tool = { path = "crates/uv-tool" }
|
||||
uv-torch = { path = "crates/uv-torch" }
|
||||
uv-trampoline-builder = { path = "crates/uv-trampoline-builder" }
|
||||
uv-types = { path = "crates/uv-types" }
|
||||
uv-version = { path = "crates/uv-version" }
|
||||
uv-virtualenv = { path = "crates/uv-virtualenv" }
|
||||
uv-warnings = { path = "crates/uv-warnings" }
|
||||
uv-workspace = { path = "crates/uv-workspace" }
|
||||
uv-auth = { version = "0.0.8", path = "crates/uv-auth" }
|
||||
uv-bin-install = { version = "0.0.8", path = "crates/uv-bin-install" }
|
||||
uv-build-backend = { version = "0.0.8", path = "crates/uv-build-backend" }
|
||||
uv-build-frontend = { version = "0.0.8", path = "crates/uv-build-frontend" }
|
||||
uv-cache = { version = "0.0.8", path = "crates/uv-cache" }
|
||||
uv-cache-info = { version = "0.0.8", path = "crates/uv-cache-info" }
|
||||
uv-cache-key = { version = "0.0.8", path = "crates/uv-cache-key" }
|
||||
uv-cli = { version = "0.0.8", path = "crates/uv-cli" }
|
||||
uv-client = { version = "0.0.8", path = "crates/uv-client" }
|
||||
uv-configuration = { version = "0.0.8", path = "crates/uv-configuration" }
|
||||
uv-console = { version = "0.0.8", path = "crates/uv-console" }
|
||||
uv-dirs = { version = "0.0.8", path = "crates/uv-dirs" }
|
||||
uv-dispatch = { version = "0.0.8", path = "crates/uv-dispatch" }
|
||||
uv-distribution = { version = "0.0.8", path = "crates/uv-distribution" }
|
||||
uv-distribution-filename = { version = "0.0.8", path = "crates/uv-distribution-filename" }
|
||||
uv-distribution-types = { version = "0.0.8", path = "crates/uv-distribution-types" }
|
||||
uv-extract = { version = "0.0.8", path = "crates/uv-extract" }
|
||||
uv-flags = { version = "0.0.8", path = "crates/uv-flags" }
|
||||
uv-fs = { version = "0.0.8", path = "crates/uv-fs", features = ["serde", "tokio"] }
|
||||
uv-git = { version = "0.0.8", path = "crates/uv-git" }
|
||||
uv-git-types = { version = "0.0.8", path = "crates/uv-git-types" }
|
||||
uv-globfilter = { version = "0.0.8", path = "crates/uv-globfilter" }
|
||||
uv-install-wheel = { version = "0.0.8", path = "crates/uv-install-wheel", default-features = false }
|
||||
uv-installer = { version = "0.0.8", path = "crates/uv-installer" }
|
||||
uv-keyring = { version = "0.0.8", path = "crates/uv-keyring" }
|
||||
uv-logging = { version = "0.0.8", path = "crates/uv-logging" }
|
||||
uv-macros = { version = "0.0.8", path = "crates/uv-macros" }
|
||||
uv-metadata = { version = "0.0.8", path = "crates/uv-metadata" }
|
||||
uv-normalize = { version = "0.0.8", path = "crates/uv-normalize" }
|
||||
uv-once-map = { version = "0.0.8", path = "crates/uv-once-map" }
|
||||
uv-options-metadata = { version = "0.0.8", path = "crates/uv-options-metadata" }
|
||||
uv-performance-memory-allocator = { version = "0.0.8", path = "crates/uv-performance-memory-allocator" }
|
||||
uv-pep440 = { version = "0.0.8", path = "crates/uv-pep440", features = ["tracing", "rkyv", "version-ranges"] }
|
||||
uv-pep508 = { version = "0.0.8", path = "crates/uv-pep508", features = ["non-pep508-extensions"] }
|
||||
uv-platform = { version = "0.0.8", path = "crates/uv-platform" }
|
||||
uv-platform-tags = { version = "0.0.8", path = "crates/uv-platform-tags" }
|
||||
uv-preview = { version = "0.0.8", path = "crates/uv-preview" }
|
||||
uv-publish = { version = "0.0.8", path = "crates/uv-publish" }
|
||||
uv-pypi-types = { version = "0.0.8", path = "crates/uv-pypi-types" }
|
||||
uv-python = { version = "0.0.8", path = "crates/uv-python" }
|
||||
uv-redacted = { version = "0.0.8", path = "crates/uv-redacted" }
|
||||
uv-requirements = { version = "0.0.8", path = "crates/uv-requirements" }
|
||||
uv-requirements-txt = { version = "0.0.8", path = "crates/uv-requirements-txt" }
|
||||
uv-resolver = { version = "0.0.8", path = "crates/uv-resolver" }
|
||||
uv-scripts = { version = "0.0.8", path = "crates/uv-scripts" }
|
||||
uv-settings = { version = "0.0.8", path = "crates/uv-settings" }
|
||||
uv-shell = { version = "0.0.8", path = "crates/uv-shell" }
|
||||
uv-small-str = { version = "0.0.8", path = "crates/uv-small-str" }
|
||||
uv-state = { version = "0.0.8", path = "crates/uv-state" }
|
||||
uv-static = { version = "0.0.8", path = "crates/uv-static" }
|
||||
uv-tool = { version = "0.0.8", path = "crates/uv-tool" }
|
||||
uv-torch = { version = "0.0.8", path = "crates/uv-torch" }
|
||||
uv-trampoline-builder = { version = "0.0.8", path = "crates/uv-trampoline-builder" }
|
||||
uv-types = { version = "0.0.8", path = "crates/uv-types" }
|
||||
uv-version = { version = "0.9.18", path = "crates/uv-version" }
|
||||
uv-virtualenv = { version = "0.0.8", path = "crates/uv-virtualenv" }
|
||||
uv-warnings = { version = "0.0.8", path = "crates/uv-warnings" }
|
||||
uv-workspace = { version = "0.0.8", path = "crates/uv-workspace" }
|
||||
|
||||
ambient-id = { version = "0.0.5" }
|
||||
ambient-id = { version = "0.0.7", default-features = false, features = ["astral-reqwest-middleware"] }
|
||||
anstream = { version = "0.6.15" }
|
||||
anyhow = { version = "1.0.89" }
|
||||
arcstr = { version = "1.2.0" }
|
||||
|
|
@ -87,8 +84,8 @@ astral-tokio-tar = { version = "0.5.6" }
|
|||
async-channel = { version = "2.3.1" }
|
||||
async-compression = { version = "0.4.12", features = ["bzip2", "gzip", "xz", "zstd"] }
|
||||
async-trait = { version = "0.1.82" }
|
||||
async_http_range_reader = { version = "0.9.1" }
|
||||
async_zip = { git = "https://github.com/astral-sh/rs-async-zip", rev = "f6a41d32866003c868d03ed791a89c794f61b703", features = ["bzip2", "deflate", "lzma", "tokio", "xz", "zstd"] }
|
||||
async_http_range_reader = { version = "0.9.1", package = "astral_async_http_range_reader" }
|
||||
async_zip = { version = "0.0.17", package = "astral_async_zip", features = ["bzip2", "deflate", "lzma", "tokio", "xz", "zstd"] }
|
||||
axoupdater = { version = "0.9.0", default-features = false }
|
||||
backon = { version = "1.3.0" }
|
||||
base64 = { version = "0.22.1" }
|
||||
|
|
@ -103,13 +100,17 @@ configparser = { version = "3.1.0" }
|
|||
console = { version = "0.16.0", default-features = false, features = ["std"] }
|
||||
csv = { version = "1.3.0" }
|
||||
ctrlc = { version = "3.4.5" }
|
||||
cyclonedx-bom = { version = "0.8.0" }
|
||||
dashmap = { version = "6.1.0" }
|
||||
data-encoding = { version = "2.6.0" }
|
||||
diskus = { version = "0.9.0", default-features = false }
|
||||
dotenvy = { version = "0.15.7" }
|
||||
dunce = { version = "1.0.5" }
|
||||
either = { version = "1.13.0" }
|
||||
encoding_rs_io = { version = "0.1.7" }
|
||||
embed-manifest = { version = "1.5.0" }
|
||||
etcetera = { version = "0.11.0" }
|
||||
fastrand = { version = "2.3.0" }
|
||||
flate2 = { version = "1.0.33", default-features = false, features = ["zlib-rs"] }
|
||||
fs-err = { version = "3.0.0", features = ["tokio"] }
|
||||
futures = { version = "0.3.30" }
|
||||
|
|
@ -134,7 +135,6 @@ memchr = { version = "2.7.4" }
|
|||
miette = { version = "7.2.0", features = ["fancy-no-backtrace"] }
|
||||
nanoid = { version = "0.4.0" }
|
||||
nix = { version = "0.30.0", features = ["signal"] }
|
||||
once_cell = { version = "1.20.2" }
|
||||
open = { version = "5.3.2" }
|
||||
owo-colors = { version = "4.1.0" }
|
||||
path-slash = { version = "0.2.1" }
|
||||
|
|
@ -143,7 +143,7 @@ percent-encoding = { version = "2.3.1" }
|
|||
petgraph = { version = "0.8.0" }
|
||||
proc-macro2 = { version = "1.0.86" }
|
||||
procfs = { version = "0.17.0", default-features = false, features = ["flate2"] }
|
||||
pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "d8efd77673c9a90792da9da31b6c0da7ea8a324b" }
|
||||
pubgrub = { version = "0.3.3" , package = "astral-pubgrub" }
|
||||
quote = { version = "1.0.37" }
|
||||
rayon = { version = "1.10.0" }
|
||||
ref-cast = { version = "1.0.24" }
|
||||
|
|
@ -152,8 +152,8 @@ regex = { version = "1.10.6" }
|
|||
regex-automata = { version = "0.4.8", default-features = false, features = ["dfa-build", "dfa-search", "perf", "std", "syntax"] }
|
||||
reqsign = { version = "0.18.0", features = ["aws", "default-context"], default-features = false }
|
||||
reqwest = { version = "0.12.22", default-features = false, features = ["json", "gzip", "deflate", "zstd", "stream", "system-proxy", "rustls-tls", "rustls-tls-native-roots", "socks", "multipart", "http2", "blocking"] }
|
||||
reqwest-middleware = { git = "https://github.com/astral-sh/reqwest-middleware", rev = "7650ed76215a962a96d94a79be71c27bffde7ab2", features = ["multipart"] }
|
||||
reqwest-retry = { git = "https://github.com/astral-sh/reqwest-middleware", rev = "7650ed76215a962a96d94a79be71c27bffde7ab2" }
|
||||
reqwest-middleware = { version = "0.4.2", package = "astral-reqwest-middleware", features = ["multipart"] }
|
||||
reqwest-retry = { version = "0.7.0", package = "astral-reqwest-retry" }
|
||||
rkyv = { version = "0.8.8", features = ["bytecheck"] }
|
||||
rmp-serde = { version = "1.3.0" }
|
||||
rust-netrc = { version = "0.1.2" }
|
||||
|
|
@ -170,7 +170,7 @@ serde-untagged = { version = "0.1.6" }
|
|||
serde_json = { version = "1.0.128" }
|
||||
sha2 = { version = "0.10.8" }
|
||||
smallvec = { version = "1.13.2" }
|
||||
spdx = { version = "0.12.0" }
|
||||
spdx = { version = "0.13.0" }
|
||||
syn = { version = "2.0.77" }
|
||||
sys-info = { version = "0.9.1" }
|
||||
tar = { version = "0.4.43" }
|
||||
|
|
@ -178,8 +178,8 @@ target-lexicon = { version = "0.13.0" }
|
|||
tempfile = { version = "3.14.0" }
|
||||
textwrap = { version = "0.16.1" }
|
||||
thiserror = { version = "2.0.0" }
|
||||
astral-tl = { version = "0.7.9" }
|
||||
tokio = { version = "1.40.0", features = ["fs", "io-util", "macros", "process", "rt", "signal", "sync"] }
|
||||
astral-tl = { version = "0.7.11" }
|
||||
tokio = { version = "1.40.0", features = ["fs", "io-util", "macros", "process", "rt", "signal", "sync", "time"] }
|
||||
tokio-stream = { version = "0.1.16" }
|
||||
tokio-util = { version = "0.7.12", features = ["compat", "io"] }
|
||||
toml = { version = "0.9.2", features = ["fast_hash"] }
|
||||
|
|
@ -193,7 +193,7 @@ unicode-width = { version = "0.2.0" }
|
|||
unscanny = { version = "0.1.0" }
|
||||
url = { version = "2.5.2", features = ["serde"] }
|
||||
uuid = { version = "1.16.0" }
|
||||
version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "d8efd77673c9a90792da9da31b6c0da7ea8a324b" }
|
||||
version-ranges = { version = "0.1.3", package = "astral-version-ranges" }
|
||||
walkdir = { version = "2.5.0" }
|
||||
which = { version = "8.0.0", features = ["regex"] }
|
||||
windows = { version = "0.59.0", features = ["std", "Win32_Globalization", "Win32_System_LibraryLoader", "Win32_System_Console", "Win32_System_Kernel", "Win32_System_Diagnostics_Debug", "Win32_Storage_FileSystem", "Win32_Security", "Win32_System_Registry", "Win32_System_IO", "Win32_System_Ioctl"] }
|
||||
|
|
@ -212,20 +212,19 @@ byteorder = { version = "1.5.0" }
|
|||
filetime = { version = "0.2.25" }
|
||||
http-body-util = { version = "0.1.2" }
|
||||
hyper = { version = "1.4.1", features = ["server", "http1"] }
|
||||
hyper-util = { version = "0.1.8", features = ["tokio"] }
|
||||
hyper-util = { version = "0.1.8", features = ["tokio", "server", "http1"] }
|
||||
ignore = { version = "0.4.23" }
|
||||
insta = { version = "1.40.0", features = ["json", "filters", "redactions"] }
|
||||
predicates = { version = "3.1.2" }
|
||||
rcgen = { version = "0.14.5", features = ["crypto", "pem", "ring"], default-features = false }
|
||||
rustls = { version = "0.23.29", default-features = false }
|
||||
similar = { version = "2.6.0" }
|
||||
temp-env = { version = "0.3.6" }
|
||||
test-case = { version = "3.3.1" }
|
||||
test-log = { version = "0.2.16", features = ["trace"], default-features = false }
|
||||
tokio-rustls = { version = "0.26.2", default-features = false }
|
||||
whoami = { version = "1.6.0" }
|
||||
|
||||
[workspace.metadata.cargo-shear]
|
||||
ignored = ["flate2", "xz2", "h2"]
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "warn"
|
||||
unreachable_pub = "warn"
|
||||
|
|
@ -311,12 +310,21 @@ strip = false
|
|||
debug = "full"
|
||||
lto = false
|
||||
|
||||
# Profile for fast test execution: Skip debug info generation, and
|
||||
# apply basic optimization, which speed up build and running tests.
|
||||
[profile.fast-build]
|
||||
inherits = "dev"
|
||||
opt-level = 1
|
||||
debug = 0
|
||||
strip = "debuginfo"
|
||||
|
||||
# Profile for faster builds: Skip debug info generation, for faster
|
||||
# builds of smaller binaries.
|
||||
[profile.no-debug]
|
||||
inherits = "dev"
|
||||
debug = 0
|
||||
strip = "debuginfo"
|
||||
|
||||
# Profile to build a minimally sized binary for uv-build
|
||||
[profile.minimal-size]
|
||||
inherits = "release"
|
||||
|
|
@ -328,7 +336,3 @@ codegen-units = 1
|
|||
# The profile that 'cargo dist' will build with.
|
||||
[profile.dist]
|
||||
inherits = "release"
|
||||
|
||||
[patch.crates-io]
|
||||
reqwest-middleware = { git = "https://github.com/astral-sh/reqwest-middleware", rev = "7650ed76215a962a96d94a79be71c27bffde7ab2" }
|
||||
reqwest-retry = { git = "https://github.com/astral-sh/reqwest-middleware", rev = "7650ed76215a962a96d94a79be71c27bffde7ab2" }
|
||||
|
|
|
|||
33
README.md
33
README.md
|
|
@ -42,7 +42,7 @@ An extremely fast Python package and project manager, written in Rust.
|
|||
- 🖥️ Supports macOS, Linux, and Windows.
|
||||
|
||||
uv is backed by [Astral](https://astral.sh), the creators of
|
||||
[Ruff](https://github.com/astral-sh/ruff).
|
||||
[Ruff](https://github.com/astral-sh/ruff) and [ty](https://github.com/astral-sh/ty).
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -192,14 +192,12 @@ uv installs Python and allows quickly switching between versions.
|
|||
Install multiple Python versions:
|
||||
|
||||
```console
|
||||
$ uv python install 3.10 3.11 3.12
|
||||
Searching for Python versions matching: Python 3.10
|
||||
Searching for Python versions matching: Python 3.11
|
||||
Searching for Python versions matching: Python 3.12
|
||||
Installed 3 versions in 3.42s
|
||||
+ cpython-3.10.14-macos-aarch64-none
|
||||
+ cpython-3.11.9-macos-aarch64-none
|
||||
+ cpython-3.12.4-macos-aarch64-none
|
||||
$ uv python install 3.12 3.13 3.14
|
||||
Installed 3 versions in 972ms
|
||||
+ cpython-3.12.12-macos-aarch64-none (python3.12)
|
||||
+ cpython-3.13.9-macos-aarch64-none (python3.13)
|
||||
+ cpython-3.14.0-macos-aarch64-none (python3.14)
|
||||
|
||||
```
|
||||
|
||||
Download Python versions as needed:
|
||||
|
|
@ -270,14 +268,6 @@ Installed 43 packages in 208ms
|
|||
|
||||
See the [pip interface documentation](https://docs.astral.sh/uv/pip/index/) to get started.
|
||||
|
||||
## Platform support
|
||||
|
||||
See uv's [platform support](https://docs.astral.sh/uv/reference/platforms/) document.
|
||||
|
||||
## Versioning policy
|
||||
|
||||
See uv's [versioning policy](https://docs.astral.sh/uv/reference/versioning/) document.
|
||||
|
||||
## Contributing
|
||||
|
||||
We are passionate about supporting contributors of all levels of experience and would love to see
|
||||
|
|
@ -294,6 +284,15 @@ It's pronounced as "you - vee" ([`/juː viː/`](https://en.wikipedia.org/wiki/He
|
|||
|
||||
Just "uv", please. See the [style guide](./STYLE.md#styling-uv) for details.
|
||||
|
||||
#### What platforms does uv support?
|
||||
|
||||
See uv's [platform support](https://docs.astral.sh/uv/reference/platforms/) document.
|
||||
|
||||
#### Is uv ready for production?
|
||||
|
||||
Yes, uv is stable and widely used in production. See uv's
|
||||
[versioning policy](https://docs.astral.sh/uv/reference/versioning/) document for details.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
uv's dependency resolver uses [PubGrub](https://github.com/pubgrub-rs/pubgrub) under the hood. We're
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
[files]
|
||||
extend-exclude = [
|
||||
"**/snapshots/",
|
||||
"ecosystem/**",
|
||||
"scripts/**/*.in",
|
||||
"test/ecosystem/**",
|
||||
"test/requirements/**/*.in",
|
||||
"crates/uv-build-frontend/src/pipreqs/mapping",
|
||||
]
|
||||
ignore-hidden = false
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
[package]
|
||||
name = "uv-auth"
|
||||
version = "0.0.1"
|
||||
version = "0.0.8"
|
||||
description = "This is an internal component crate of uv"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- This file is generated. DO NOT EDIT -->
|
||||
|
||||
# uv-auth
|
||||
|
||||
This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here
|
||||
is unstable and will have frequent breaking changes.
|
||||
|
||||
This version (0.0.8) is a component of [uv 0.9.18](https://crates.io/crates/uv/0.9.18). The source
|
||||
can be found [here](https://github.com/astral-sh/uv/blob/0.9.18/crates/uv-auth).
|
||||
|
||||
See uv's
|
||||
[crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning)
|
||||
for details on versioning.
|
||||
|
|
@ -11,8 +11,8 @@ use url::Url;
|
|||
use uv_once_map::OnceMap;
|
||||
use uv_redacted::DisplaySafeUrl;
|
||||
|
||||
use crate::Realm;
|
||||
use crate::credentials::{Authentication, Username};
|
||||
use crate::{Credentials, Realm};
|
||||
|
||||
type FxOnceMap<K, V> = OnceMap<K, V, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
|
|
@ -33,6 +33,7 @@ impl Display for FetchUrl {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)] // All internal types are redacted.
|
||||
pub struct CredentialsCache {
|
||||
/// A cache per realm and username
|
||||
realms: RwLock<FxHashMap<(Realm, Username), Arc<Authentication>>>,
|
||||
|
|
@ -58,6 +59,27 @@ impl CredentialsCache {
|
|||
}
|
||||
}
|
||||
|
||||
/// Populate the global authentication store with credentials on a URL, if there are any.
|
||||
///
|
||||
/// Returns `true` if the store was updated.
|
||||
pub fn store_credentials_from_url(&self, url: &DisplaySafeUrl) -> bool {
|
||||
if let Some(credentials) = Credentials::from_url(url) {
|
||||
trace!("Caching credentials for {url}");
|
||||
self.insert(url, Arc::new(Authentication::from(credentials)));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Populate the global authentication store with credentials on a URL, if there are any.
|
||||
///
|
||||
/// Returns `true` if the store was updated.
|
||||
pub fn store_credentials(&self, url: &DisplaySafeUrl, credentials: Credentials) {
|
||||
trace!("Caching credentials for {url}");
|
||||
self.insert(url, Arc::new(Authentication::from(credentials)));
|
||||
}
|
||||
|
||||
/// Return the credentials that should be used for a realm and username, if any.
|
||||
pub(crate) fn get_realm(
|
||||
&self,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use tracing::trace;
|
||||
|
||||
use uv_redacted::DisplaySafeUrl;
|
||||
|
||||
use crate::credentials::Authentication;
|
||||
pub use access_token::AccessToken;
|
||||
use cache::CredentialsCache;
|
||||
pub use cache::CredentialsCache;
|
||||
pub use credentials::{Credentials, Username};
|
||||
pub use index::{AuthPolicy, Index, Indexes};
|
||||
pub use keyring::KeyringProvider;
|
||||
|
|
@ -29,32 +22,3 @@ mod pyx;
|
|||
mod realm;
|
||||
mod service;
|
||||
mod store;
|
||||
|
||||
// TODO(zanieb): Consider passing a cache explicitly throughout
|
||||
|
||||
/// Global authentication cache for a uv invocation
|
||||
///
|
||||
/// This is used to share credentials across uv clients.
|
||||
pub(crate) static CREDENTIALS_CACHE: LazyLock<CredentialsCache> =
|
||||
LazyLock::new(CredentialsCache::default);
|
||||
|
||||
/// Populate the global authentication store with credentials on a URL, if there are any.
|
||||
///
|
||||
/// Returns `true` if the store was updated.
|
||||
pub fn store_credentials_from_url(url: &DisplaySafeUrl) -> bool {
|
||||
if let Some(credentials) = Credentials::from_url(url) {
|
||||
trace!("Caching credentials for {url}");
|
||||
CREDENTIALS_CACHE.insert(url, Arc::new(Authentication::from(credentials)));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Populate the global authentication store with credentials on a URL, if there are any.
|
||||
///
|
||||
/// Returns `true` if the store was updated.
|
||||
pub fn store_credentials(url: &DisplaySafeUrl, credentials: Credentials) {
|
||||
trace!("Caching credentials for {url}");
|
||||
CREDENTIALS_CACHE.insert(url, Arc::new(Authentication::from(credentials)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,19 +10,24 @@ use tracing::{debug, trace, warn};
|
|||
|
||||
use uv_preview::{Preview, PreviewFeatures};
|
||||
use uv_redacted::DisplaySafeUrl;
|
||||
use uv_static::EnvVars;
|
||||
use uv_warnings::owo_colors::OwoColorize;
|
||||
|
||||
use crate::credentials::Authentication;
|
||||
use crate::providers::{HuggingFaceProvider, S3EndpointProvider};
|
||||
use crate::pyx::{DEFAULT_TOLERANCE_SECS, PyxTokenStore};
|
||||
use crate::{
|
||||
AccessToken, CREDENTIALS_CACHE, CredentialsCache, KeyringProvider,
|
||||
AccessToken, CredentialsCache, KeyringProvider,
|
||||
cache::FetchUrl,
|
||||
credentials::{Credentials, Username},
|
||||
index::{AuthPolicy, Indexes},
|
||||
realm::Realm,
|
||||
};
|
||||
use crate::{Index, TextCredentialStore, TomlCredentialError};
|
||||
use crate::{Index, TextCredentialStore};
|
||||
|
||||
/// Cached check for whether we're running in Dependabot.
|
||||
static IS_DEPENDABOT: LazyLock<bool> =
|
||||
LazyLock::new(|| std::env::var(EnvVars::DEPENDABOT).is_ok_and(|value| value == "true"));
|
||||
|
||||
/// Strategy for loading netrc files.
|
||||
enum NetrcMode {
|
||||
|
|
@ -60,49 +65,55 @@ impl NetrcMode {
|
|||
|
||||
/// Strategy for loading text-based credential files.
|
||||
enum TextStoreMode {
|
||||
Automatic(LazyLock<Option<TextCredentialStore>>),
|
||||
Automatic(tokio::sync::OnceCell<Option<TextCredentialStore>>),
|
||||
Enabled(TextCredentialStore),
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl Default for TextStoreMode {
|
||||
fn default() -> Self {
|
||||
// TODO(zanieb): Reconsider this pattern. We're just mirroring the [`NetrcMode`]
|
||||
// implementation for now.
|
||||
Self::Automatic(LazyLock::new(|| {
|
||||
let path = TextCredentialStore::default_file()
|
||||
.inspect_err(|err| {
|
||||
warn!("Failed to determine credentials file path: {}", err);
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
match TextCredentialStore::read(&path) {
|
||||
Ok((store, _lock)) => {
|
||||
debug!("Loaded credential file {}", path.display());
|
||||
Some(store)
|
||||
}
|
||||
Err(TomlCredentialError::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
debug!("No credentials file found at {}", path.display());
|
||||
None
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"Failed to load credentials from {}: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}))
|
||||
Self::Automatic(tokio::sync::OnceCell::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl TextStoreMode {
|
||||
async fn load_default_store() -> Option<TextCredentialStore> {
|
||||
let path = TextCredentialStore::default_file()
|
||||
.inspect_err(|err| {
|
||||
warn!("Failed to determine credentials file path: {}", err);
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
match TextCredentialStore::read(&path).await {
|
||||
Ok((store, _lock)) => {
|
||||
debug!("Loaded credential file {}", path.display());
|
||||
Some(store)
|
||||
}
|
||||
Err(err)
|
||||
if err
|
||||
.as_io_error()
|
||||
.is_some_and(|err| err.kind() == std::io::ErrorKind::NotFound) =>
|
||||
{
|
||||
debug!("No credentials file found at {}", path.display());
|
||||
None
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"Failed to load credentials from {}: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the parsed credential store, if enabled.
|
||||
fn get(&self) -> Option<&TextCredentialStore> {
|
||||
async fn get(&self) -> Option<&TextCredentialStore> {
|
||||
match self {
|
||||
Self::Automatic(lock) => lock.as_ref(),
|
||||
// TODO(zanieb): Reconsider this pattern. We're just mirroring the [`NetrcMode`]
|
||||
// implementation for now.
|
||||
Self::Automatic(lock) => lock.get_or_init(Self::load_default_store).await.as_ref(),
|
||||
Self::Enabled(store) => Some(store),
|
||||
Self::Disabled => None,
|
||||
}
|
||||
|
|
@ -118,6 +129,15 @@ enum TokenState {
|
|||
Initialized(Option<AccessToken>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum S3CredentialState {
|
||||
/// The S3 credential state has not yet been initialized.
|
||||
Uninitialized,
|
||||
/// The S3 credential state has been initialized, with either a signer or `None` if
|
||||
/// no S3 endpoint is configured.
|
||||
Initialized(Option<Arc<Authentication>>),
|
||||
}
|
||||
|
||||
/// A middleware that adds basic authentication to requests.
|
||||
///
|
||||
/// Uses a cache to propagate credentials from previously seen requests and
|
||||
|
|
@ -126,7 +146,8 @@ pub struct AuthMiddleware {
|
|||
netrc: NetrcMode,
|
||||
text_store: TextStoreMode,
|
||||
keyring: Option<KeyringProvider>,
|
||||
cache: Option<CredentialsCache>,
|
||||
/// Global authentication cache for a uv invocation to share credentials across uv clients.
|
||||
cache: Arc<CredentialsCache>,
|
||||
/// Auth policies for specific URLs.
|
||||
indexes: Indexes,
|
||||
/// Set all endpoints as needing authentication. We never try to send an
|
||||
|
|
@ -138,21 +159,31 @@ pub struct AuthMiddleware {
|
|||
pyx_token_store: Option<PyxTokenStore>,
|
||||
/// Tokens to use for persistent credentials.
|
||||
pyx_token_state: Mutex<TokenState>,
|
||||
/// Cached S3 credentials to avoid running the credential helper multiple times.
|
||||
s3_credential_state: Mutex<S3CredentialState>,
|
||||
preview: Preview,
|
||||
}
|
||||
|
||||
impl Default for AuthMiddleware {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthMiddleware {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
netrc: NetrcMode::default(),
|
||||
text_store: TextStoreMode::default(),
|
||||
keyring: None,
|
||||
cache: None,
|
||||
// TODO(konsti): There shouldn't be a credential cache without that in the initializer.
|
||||
cache: Arc::new(CredentialsCache::default()),
|
||||
indexes: Indexes::new(),
|
||||
only_authenticated: false,
|
||||
base_client: None,
|
||||
pyx_token_store: None,
|
||||
pyx_token_state: Mutex::new(TokenState::Uninitialized),
|
||||
s3_credential_state: Mutex::new(S3CredentialState::Uninitialized),
|
||||
preview: Preview::default(),
|
||||
}
|
||||
}
|
||||
|
|
@ -200,7 +231,14 @@ impl AuthMiddleware {
|
|||
/// Configure the [`CredentialsCache`] to use.
|
||||
#[must_use]
|
||||
pub fn with_cache(mut self, cache: CredentialsCache) -> Self {
|
||||
self.cache = Some(cache);
|
||||
self.cache = Arc::new(cache);
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure the [`CredentialsCache`] to use from an existing [`Arc`].
|
||||
#[must_use]
|
||||
pub fn with_cache_arc(mut self, cache: Arc<CredentialsCache>) -> Self {
|
||||
self.cache = cache;
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -233,17 +271,9 @@ impl AuthMiddleware {
|
|||
self
|
||||
}
|
||||
|
||||
/// Get the configured authentication store.
|
||||
///
|
||||
/// If not set, the global store is used.
|
||||
/// Global authentication cache for a uv invocation to share credentials across uv clients.
|
||||
fn cache(&self) -> &CredentialsCache {
|
||||
self.cache.as_ref().unwrap_or(&CREDENTIALS_CACHE)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AuthMiddleware {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
&self.cache
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -352,11 +382,15 @@ impl Middleware for AuthMiddleware {
|
|||
.is_some_and(|token_store| token_store.is_known_url(request.url()));
|
||||
|
||||
let must_authenticate = self.only_authenticated
|
||||
|| match auth_policy {
|
||||
AuthPolicy::Auto => is_known_url,
|
||||
AuthPolicy::Always => true,
|
||||
AuthPolicy::Never => false,
|
||||
};
|
||||
|| (match auth_policy {
|
||||
AuthPolicy::Auto => is_known_url,
|
||||
AuthPolicy::Always => true,
|
||||
AuthPolicy::Never => false,
|
||||
}
|
||||
// Dependabot intercepts HTTP requests and injects credentials, which means that we
|
||||
// cannot eagerly enforce an `AuthPolicy` as we don't know whether credentials will be
|
||||
// added outside of uv.
|
||||
&& !*IS_DEPENDABOT);
|
||||
|
||||
let (mut retry_request, response) = if !must_authenticate {
|
||||
let url = tracing_url(&request, credentials.as_deref());
|
||||
|
|
@ -656,13 +690,26 @@ impl AuthMiddleware {
|
|||
return Some(credentials);
|
||||
}
|
||||
|
||||
if let Some(credentials) = S3EndpointProvider::credentials_for(url, self.preview)
|
||||
.map(Authentication::from)
|
||||
.map(Arc::new)
|
||||
{
|
||||
debug!("Found S3 credentials for {url}");
|
||||
self.cache().fetches.done(key, Some(credentials.clone()));
|
||||
return Some(credentials);
|
||||
if S3EndpointProvider::is_s3_endpoint(url, self.preview) {
|
||||
let mut s3_state = self.s3_credential_state.lock().await;
|
||||
|
||||
// If the S3 credential state is uninitialized, initialize it.
|
||||
let credentials = match &*s3_state {
|
||||
S3CredentialState::Uninitialized => {
|
||||
trace!("Initializing S3 credentials for {url}");
|
||||
let signer = S3EndpointProvider::create_signer();
|
||||
let credentials = Arc::new(Authentication::from(signer));
|
||||
*s3_state = S3CredentialState::Initialized(Some(credentials.clone()));
|
||||
Some(credentials)
|
||||
}
|
||||
S3CredentialState::Initialized(credentials) => credentials.clone(),
|
||||
};
|
||||
|
||||
if let Some(credentials) = credentials {
|
||||
debug!("Found S3 credentials for {url}");
|
||||
self.cache().fetches.done(key, Some(credentials.clone()));
|
||||
return Some(credentials);
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a known URL, authenticate it via the token store.
|
||||
|
|
@ -720,9 +767,16 @@ impl AuthMiddleware {
|
|||
Some(credentials)
|
||||
|
||||
// Text credential store support.
|
||||
} else if let Some(credentials) = self.text_store.get().and_then(|text_store| {
|
||||
} else if let Some(credentials) = self.text_store.get().await.and_then(|text_store| {
|
||||
debug!("Checking text store for credentials for {url}");
|
||||
text_store.get_credentials(url, credentials.as_ref().and_then(|credentials| credentials.username())).cloned()
|
||||
text_store
|
||||
.get_credentials(
|
||||
url,
|
||||
credentials
|
||||
.as_ref()
|
||||
.and_then(|credentials| credentials.username()),
|
||||
)
|
||||
.cloned()
|
||||
}) {
|
||||
debug!("Found credentials in plaintext store for {url}");
|
||||
Some(credentials)
|
||||
|
|
@ -738,10 +792,16 @@ impl AuthMiddleware {
|
|||
if let Some(index) = index {
|
||||
// N.B. The native store performs an exact look up right now, so we use the root
|
||||
// URL of the index instead of relying on prefix-matching.
|
||||
debug!("Checking native store for credentials for index URL {}{}", display_username, index.root_url);
|
||||
debug!(
|
||||
"Checking native store for credentials for index URL {}{}",
|
||||
display_username, index.root_url
|
||||
);
|
||||
native_store.fetch(&index.root_url, username).await
|
||||
} else {
|
||||
debug!("Checking native store for credentials for URL {}{}", display_username, url);
|
||||
debug!(
|
||||
"Checking native store for credentials for URL {}{}",
|
||||
display_username, url
|
||||
);
|
||||
native_store.fetch(url, username).await
|
||||
}
|
||||
// TODO(zanieb): We should have a realm fallback here too
|
||||
|
|
@ -762,10 +822,18 @@ impl AuthMiddleware {
|
|||
// always authenticate.
|
||||
if let Some(username) = credentials.and_then(|credentials| credentials.username()) {
|
||||
if let Some(index) = index {
|
||||
debug!("Checking keyring for credentials for index URL {}@{}", username, index.url);
|
||||
keyring.fetch(DisplaySafeUrl::ref_cast(&index.url), Some(username)).await
|
||||
debug!(
|
||||
"Checking keyring for credentials for index URL {}@{}",
|
||||
username, index.url
|
||||
);
|
||||
keyring
|
||||
.fetch(DisplaySafeUrl::ref_cast(&index.url), Some(username))
|
||||
.await
|
||||
} else {
|
||||
debug!("Checking keyring for credentials for full URL {}@{}", username, url);
|
||||
debug!(
|
||||
"Checking keyring for credentials for full URL {}@{}",
|
||||
username, url
|
||||
);
|
||||
keyring.fetch(url, Some(username)).await
|
||||
}
|
||||
} else if matches!(auth_policy, AuthPolicy::Always) {
|
||||
|
|
@ -774,12 +842,16 @@ impl AuthMiddleware {
|
|||
"Checking keyring for credentials for index URL {} without username due to `authenticate = always`",
|
||||
index.url
|
||||
);
|
||||
keyring.fetch(DisplaySafeUrl::ref_cast(&index.url), None).await
|
||||
keyring
|
||||
.fetch(DisplaySafeUrl::ref_cast(&index.url), None)
|
||||
.await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
debug!("Skipping keyring fetch for {url} without username; use `authenticate = always` to force");
|
||||
debug!(
|
||||
"Skipping keyring fetch for {url} without username; use `authenticate = always` to force"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
@ -789,9 +861,9 @@ impl AuthMiddleware {
|
|||
Some(credentials)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.map(Authentication::from)
|
||||
.map(Arc::new);
|
||||
};
|
||||
|
||||
let credentials = credentials.map(Authentication::from).map(Arc::new);
|
||||
|
||||
// Register the fetch for this key
|
||||
self.cache().fetches.done(key, credentials.clone());
|
||||
|
|
|
|||
|
|
@ -66,8 +66,8 @@ static S3_ENDPOINT_REALM: LazyLock<Option<Realm>> = LazyLock::new(|| {
|
|||
pub(crate) struct S3EndpointProvider;
|
||||
|
||||
impl S3EndpointProvider {
|
||||
/// Returns the credentials for the S3 endpoint, if available.
|
||||
pub(crate) fn credentials_for(url: &Url, preview: Preview) -> Option<DefaultSigner> {
|
||||
/// Returns `true` if the URL matches the configured S3 endpoint.
|
||||
pub(crate) fn is_s3_endpoint(url: &Url, preview: Preview) -> bool {
|
||||
if let Some(s3_endpoint_realm) = S3_ENDPOINT_REALM.as_ref().map(RealmRef::from) {
|
||||
if !preview.is_enabled(PreviewFeatures::S3_ENDPOINT) {
|
||||
warn_user_once!(
|
||||
|
|
@ -79,19 +79,26 @@ impl S3EndpointProvider {
|
|||
// Treat any URL on the same domain or subdomain as available for S3 signing.
|
||||
let realm = RealmRef::from(url);
|
||||
if realm == s3_endpoint_realm || realm.is_subdomain_of(s3_endpoint_realm) {
|
||||
// TODO(charlie): Can `reqsign` infer the region for us? Profiles, for example,
|
||||
// often have a region set already.
|
||||
let region = std::env::var(EnvVars::AWS_REGION)
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or_else(|_| {
|
||||
std::env::var(EnvVars::AWS_DEFAULT_REGION)
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or_else(|_| Cow::Borrowed("us-east-1"))
|
||||
});
|
||||
let signer = reqsign::aws::default_signer("s3", ®ion);
|
||||
return Some(signer);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
None
|
||||
false
|
||||
}
|
||||
|
||||
/// Creates a new S3 signer with the configured region.
|
||||
///
|
||||
/// This is potentially expensive as it may invoke credential helpers, so the result
|
||||
/// should be cached.
|
||||
pub(crate) fn create_signer() -> DefaultSigner {
|
||||
// TODO(charlie): Can `reqsign` infer the region for us? Profiles, for example,
|
||||
// often have a region set already.
|
||||
let region = std::env::var(EnvVars::AWS_REGION)
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or_else(|_| {
|
||||
std::env::var(EnvVars::AWS_DEFAULT_REGION)
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or_else(|_| Cow::Borrowed("us-east-1"))
|
||||
});
|
||||
reqsign::aws::default_signer("s3", ®ion)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use fs_err as fs;
|
|||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use uv_fs::{LockedFile, with_added_extension};
|
||||
use uv_fs::{LockedFile, LockedFileError, LockedFileMode, with_added_extension};
|
||||
use uv_preview::{Preview, PreviewFeatures};
|
||||
use uv_redacted::DisplaySafeUrl;
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ pub enum AuthBackend {
|
|||
}
|
||||
|
||||
impl AuthBackend {
|
||||
pub fn from_settings(preview: Preview) -> Result<Self, TomlCredentialError> {
|
||||
pub async fn from_settings(preview: Preview) -> Result<Self, TomlCredentialError> {
|
||||
// If preview is enabled, we'll use the system-native store
|
||||
if preview.is_enabled(PreviewFeatures::NATIVE_AUTH) {
|
||||
return Ok(Self::System(KeyringProvider::native()));
|
||||
|
|
@ -36,12 +36,16 @@ impl AuthBackend {
|
|||
|
||||
// Otherwise, we'll use the plaintext credential store
|
||||
let path = TextCredentialStore::default_file()?;
|
||||
match TextCredentialStore::read(&path) {
|
||||
match TextCredentialStore::read(&path).await {
|
||||
Ok((store, lock)) => Ok(Self::TextStore(store, lock)),
|
||||
Err(TomlCredentialError::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
Err(err)
|
||||
if err
|
||||
.as_io_error()
|
||||
.is_some_and(|err| err.kind() == std::io::ErrorKind::NotFound) =>
|
||||
{
|
||||
Ok(Self::TextStore(
|
||||
TextCredentialStore::default(),
|
||||
TextCredentialStore::lock(&path)?,
|
||||
TextCredentialStore::lock(&path).await?,
|
||||
))
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
|
|
@ -69,6 +73,8 @@ pub enum AuthScheme {
|
|||
pub enum TomlCredentialError {
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
LockedFile(#[from] LockedFileError),
|
||||
#[error("Failed to parse TOML credential file: {0}")]
|
||||
ParseError(#[from] toml::de::Error),
|
||||
#[error("Failed to serialize credentials to TOML")]
|
||||
|
|
@ -83,6 +89,21 @@ pub enum TomlCredentialError {
|
|||
TokenNotUnicode(#[from] std::string::FromUtf8Error),
|
||||
}
|
||||
|
||||
impl TomlCredentialError {
|
||||
pub fn as_io_error(&self) -> Option<&std::io::Error> {
|
||||
match self {
|
||||
Self::Io(err) => Some(err),
|
||||
Self::LockedFile(err) => err.as_io_error(),
|
||||
Self::ParseError(_)
|
||||
| Self::SerializeError(_)
|
||||
| Self::BasicAuthError(_)
|
||||
| Self::BearerAuthError(_)
|
||||
| Self::CredentialsDirError
|
||||
| Self::TokenNotUnicode(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum BasicAuthError {
|
||||
#[error("`username` is required with `scheme = basic`")]
|
||||
|
|
@ -233,12 +254,12 @@ impl TextCredentialStore {
|
|||
}
|
||||
|
||||
/// Acquire a lock on the credentials file at the given path.
|
||||
pub fn lock(path: &Path) -> Result<LockedFile, TomlCredentialError> {
|
||||
pub async fn lock(path: &Path) -> Result<LockedFile, TomlCredentialError> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let lock = with_added_extension(path, ".lock");
|
||||
Ok(LockedFile::acquire_blocking(lock, "credentials store")?)
|
||||
Ok(LockedFile::acquire(lock, LockedFileMode::Exclusive, "credentials store").await?)
|
||||
}
|
||||
|
||||
/// Read credentials from a file.
|
||||
|
|
@ -269,8 +290,8 @@ impl TextCredentialStore {
|
|||
/// Returns [`TextCredentialStore`] and a [`LockedFile`] to hold if mutating the store.
|
||||
///
|
||||
/// If the store will not be written to following the read, the lock can be dropped.
|
||||
pub fn read<P: AsRef<Path>>(path: P) -> Result<(Self, LockedFile), TomlCredentialError> {
|
||||
let lock = Self::lock(path.as_ref())?;
|
||||
pub async fn read<P: AsRef<Path>>(path: P) -> Result<(Self, LockedFile), TomlCredentialError> {
|
||||
let lock = Self::lock(path.as_ref()).await?;
|
||||
let store = Self::from_file(path)?;
|
||||
Ok((store, lock))
|
||||
}
|
||||
|
|
@ -450,8 +471,8 @@ mod tests {
|
|||
assert!(store.get_credentials(&url, None).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_operations() {
|
||||
#[tokio::test]
|
||||
async fn test_file_operations() {
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
writeln!(
|
||||
temp_file,
|
||||
|
|
@ -487,7 +508,7 @@ password = "pass2"
|
|||
store
|
||||
.write(
|
||||
temp_output.path(),
|
||||
TextCredentialStore::lock(temp_file.path()).unwrap(),
|
||||
TextCredentialStore::lock(temp_file.path()).await.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
[package]
|
||||
name = "uv-bench"
|
||||
version = "0.0.0"
|
||||
description = "uv Micro-benchmarks"
|
||||
version = "0.0.8"
|
||||
description = "This is an internal component crate of uv"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
|
|
@ -23,14 +22,14 @@ name = "uv"
|
|||
path = "benches/uv.rs"
|
||||
harness = false
|
||||
|
||||
[dependencies]
|
||||
[dev-dependencies]
|
||||
uv-cache = { workspace = true }
|
||||
uv-client = { workspace = true }
|
||||
uv-configuration = { workspace = true }
|
||||
uv-dispatch = { workspace = true }
|
||||
uv-distribution = { workspace = true }
|
||||
uv-distribution-types = { workspace = true }
|
||||
uv-extract = { workspace = true, optional = true }
|
||||
uv-extract = { workspace = true }
|
||||
uv-install-wheel = { workspace = true }
|
||||
uv-pep440 = { workspace = true }
|
||||
uv-pep508 = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- This file is generated. DO NOT EDIT -->
|
||||
|
||||
# uv-bench
|
||||
|
||||
This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here
|
||||
is unstable and will have frequent breaking changes.
|
||||
|
||||
This version (0.0.8) is a component of [uv 0.9.18](https://crates.io/crates/uv/0.9.18). The source
|
||||
can be found [here](https://github.com/astral-sh/uv/blob/0.9.18/crates/uv-bench).
|
||||
|
||||
See uv's
|
||||
[crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning)
|
||||
for details on versioning.
|
||||
|
|
@ -59,7 +59,10 @@ fn setup(manifest: Manifest) -> impl Fn(bool) {
|
|||
.build()
|
||||
.unwrap();
|
||||
|
||||
let cache = Cache::from_path("../../.cache").init().unwrap();
|
||||
let cache = Cache::from_path("../../.cache")
|
||||
.init_no_wait()
|
||||
.expect("No cache contention when running benchmarks")
|
||||
.unwrap();
|
||||
let interpreter = PythonEnvironment::from_root("../../.venv", &cache)
|
||||
.unwrap()
|
||||
.into_interpreter();
|
||||
|
|
@ -131,7 +134,7 @@ mod resolver {
|
|||
);
|
||||
|
||||
static TAGS: LazyLock<Tags> = LazyLock::new(|| {
|
||||
Tags::from_env(&PLATFORM, (3, 11), "cpython", (3, 11), false, false).unwrap()
|
||||
Tags::from_env(&PLATFORM, (3, 11), "cpython", (3, 11), false, false, false).unwrap()
|
||||
});
|
||||
|
||||
pub(crate) async fn resolve(
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
[package]
|
||||
name = "uv-bin-install"
|
||||
version = "0.0.1"
|
||||
version = "0.0.8"
|
||||
description = "This is an internal component crate of uv"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
description = "Binary download and installation utilities for uv"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
|
@ -24,6 +23,7 @@ uv-extract = { workspace = true }
|
|||
uv-pep440 = { workspace = true }
|
||||
uv-platform = { workspace = true }
|
||||
uv-redacted = { workspace = true }
|
||||
|
||||
fs-err = { workspace = true, features = ["tokio"] }
|
||||
futures = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- This file is generated. DO NOT EDIT -->
|
||||
|
||||
# uv-bin-install
|
||||
|
||||
This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here
|
||||
is unstable and will have frequent breaking changes.
|
||||
|
||||
This version (0.0.8) is a component of [uv 0.9.18](https://crates.io/crates/uv/0.9.18). The source
|
||||
can be found [here](https://github.com/astral-sh/uv/blob/0.9.18/crates/uv-bin-install).
|
||||
|
||||
See uv's
|
||||
[crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning)
|
||||
for details on versioning.
|
||||
|
|
@ -19,7 +19,7 @@ use tracing::debug;
|
|||
use url::Url;
|
||||
use uv_distribution_filename::SourceDistExtension;
|
||||
|
||||
use uv_cache::{Cache, CacheBucket, CacheEntry};
|
||||
use uv_cache::{Cache, CacheBucket, CacheEntry, Error as CacheError};
|
||||
use uv_client::{BaseClient, is_transient_network_error};
|
||||
use uv_extract::{Error as ExtractError, stream};
|
||||
use uv_pep440::Version;
|
||||
|
|
@ -135,6 +135,9 @@ pub enum Error {
|
|||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Cache(#[from] CacheError),
|
||||
|
||||
#[error("Failed to detect platform")]
|
||||
Platform(#[from] uv_platform::Error),
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
[package]
|
||||
name = "uv-build-backend"
|
||||
version = "0.1.0"
|
||||
version = "0.0.8"
|
||||
description = "This is an internal component crate of uv"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- This file is generated. DO NOT EDIT -->
|
||||
|
||||
# uv-build-backend
|
||||
|
||||
This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here
|
||||
is unstable and will have frequent breaking changes.
|
||||
|
||||
This version (0.0.8) is a component of [uv 0.9.18](https://crates.io/crates/uv/0.9.18). The source
|
||||
can be found [here](https://github.com/astral-sh/uv/blob/0.9.18/crates/uv-build-backend).
|
||||
|
||||
See uv's
|
||||
[crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning)
|
||||
for details on versioning.
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
use itertools::Itertools;
|
||||
mod metadata;
|
||||
mod serde_verbatim;
|
||||
mod settings;
|
||||
|
|
@ -7,8 +8,10 @@ mod wheel;
|
|||
pub use metadata::{PyProjectToml, check_direct_build};
|
||||
pub use settings::{BuildBackendSettings, WheelDataIncludes};
|
||||
pub use source_dist::{build_source_dist, list_source_dist};
|
||||
use uv_warnings::warn_user_once;
|
||||
pub use wheel::{build_editable, build_wheel, list_wheel, metadata};
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
@ -29,9 +32,9 @@ use crate::settings::ModuleName;
|
|||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
#[error("Invalid pyproject.toml")]
|
||||
Toml(#[from] toml::de::Error),
|
||||
#[error("Invalid pyproject.toml")]
|
||||
#[error("Invalid metadata format in: {}", _0.user_display())]
|
||||
Toml(PathBuf, #[source] toml::de::Error),
|
||||
#[error("Invalid project metadata")]
|
||||
Validation(#[from] ValidationError),
|
||||
#[error("Invalid module name: {0}")]
|
||||
InvalidModuleName(String, #[source] IdentifierParseError),
|
||||
|
|
@ -191,6 +194,60 @@ fn check_metadata_directory(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the list of module names without names which would be included twice
|
||||
///
|
||||
/// In normal cases it should do nothing:
|
||||
///
|
||||
/// * `["aaa"] -> ["aaa"]`
|
||||
/// * `["aaa", "bbb"] -> ["aaa", "bbb"]`
|
||||
///
|
||||
/// Duplicate elements are removed:
|
||||
///
|
||||
/// * `["aaa", "aaa"] -> ["aaa"]`
|
||||
/// * `["bbb", "aaa", "bbb"] -> ["aaa", "bbb"]`
|
||||
///
|
||||
/// Names with more specific paths are removed in favour of more general paths:
|
||||
///
|
||||
/// * `["aaa.foo", "aaa"] -> ["aaa"]`
|
||||
/// * `["bbb", "aaa", "bbb.foo", "ccc.foo", "ccc.foo.bar", "aaa"] -> ["aaa", "bbb.foo", "ccc.foo"]`
|
||||
///
|
||||
/// This does not preserve the order of the elements.
|
||||
fn prune_redundant_modules(mut names: Vec<String>) -> Vec<String> {
|
||||
names.sort();
|
||||
let mut pruned = Vec::with_capacity(names.len());
|
||||
for name in names {
|
||||
if let Some(last) = pruned.last() {
|
||||
if name == *last {
|
||||
continue;
|
||||
}
|
||||
// This is a more specific (narrow) module name than what came before
|
||||
if name
|
||||
.strip_prefix(last)
|
||||
.is_some_and(|suffix| suffix.starts_with('.'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
pruned.push(name);
|
||||
}
|
||||
pruned
|
||||
}
|
||||
|
||||
/// Wraps [`prune_redundant_modules`] with a conditional warning when modules are ignored
|
||||
fn prune_redundant_modules_warn(names: &[String], show_warnings: bool) -> Vec<String> {
|
||||
let pruned = prune_redundant_modules(names.to_vec());
|
||||
if show_warnings && names.len() != pruned.len() {
|
||||
let mut pruned: HashSet<_> = pruned.iter().collect();
|
||||
let ignored: Vec<_> = names.iter().filter(|name| !pruned.remove(name)).collect();
|
||||
let s = if ignored.len() == 1 { "" } else { "s" };
|
||||
warn_user_once!(
|
||||
"Ignoring redundant module name{s} in `tool.uv.build-backend.module-name`: `{}`",
|
||||
ignored.into_iter().join("`, `")
|
||||
);
|
||||
}
|
||||
pruned
|
||||
}
|
||||
|
||||
/// Returns the source root and the module path(s) with the `__init__.py[i]` below to it while
|
||||
/// checking the project layout and names.
|
||||
///
|
||||
|
|
@ -213,6 +270,7 @@ fn find_roots(
|
|||
relative_module_root: &Path,
|
||||
module_name: Option<&ModuleName>,
|
||||
namespace: bool,
|
||||
show_warnings: bool,
|
||||
) -> Result<(PathBuf, Vec<PathBuf>), Error> {
|
||||
let relative_module_root = uv_fs::normalize_path(relative_module_root);
|
||||
// Check that even if a path contains `..`, we only include files below the module root.
|
||||
|
|
@ -231,8 +289,8 @@ fn find_roots(
|
|||
ModuleName::Name(name) => {
|
||||
vec![name.split('.').collect::<PathBuf>()]
|
||||
}
|
||||
ModuleName::Names(names) => names
|
||||
.iter()
|
||||
ModuleName::Names(names) => prune_redundant_modules_warn(names, show_warnings)
|
||||
.into_iter()
|
||||
.map(|name| name.split('.').collect::<PathBuf>())
|
||||
.collect(),
|
||||
}
|
||||
|
|
@ -250,9 +308,9 @@ fn find_roots(
|
|||
let modules_relative = if let Some(module_name) = module_name {
|
||||
match module_name {
|
||||
ModuleName::Name(name) => vec![module_path_from_module_name(&src_root, name)?],
|
||||
ModuleName::Names(names) => names
|
||||
.iter()
|
||||
.map(|name| module_path_from_module_name(&src_root, name))
|
||||
ModuleName::Names(names) => prune_redundant_modules_warn(names, show_warnings)
|
||||
.into_iter()
|
||||
.map(|name| module_path_from_module_name(&src_root, &name))
|
||||
.collect::<Result<_, _>>()?,
|
||||
}
|
||||
} else {
|
||||
|
|
@ -420,19 +478,20 @@ mod tests {
|
|||
fn build(source_root: &Path, dist: &Path) -> Result<BuildResults, Error> {
|
||||
// Build a direct wheel, capture all its properties to compare it with the indirect wheel
|
||||
// latest and remove it since it has the same filename as the indirect wheel.
|
||||
let (_name, direct_wheel_list_files) = list_wheel(source_root, MOCK_UV_VERSION)?;
|
||||
let direct_wheel_filename = build_wheel(source_root, dist, None, MOCK_UV_VERSION)?;
|
||||
let (_name, direct_wheel_list_files) = list_wheel(source_root, MOCK_UV_VERSION, false)?;
|
||||
let direct_wheel_filename = build_wheel(source_root, dist, None, MOCK_UV_VERSION, false)?;
|
||||
let direct_wheel_path = dist.join(direct_wheel_filename.to_string());
|
||||
let direct_wheel_contents = wheel_contents(&direct_wheel_path);
|
||||
let direct_wheel_hash = sha2::Sha256::digest(fs_err::read(&direct_wheel_path)?);
|
||||
fs_err::remove_file(&direct_wheel_path)?;
|
||||
|
||||
// Build a source distribution.
|
||||
let (_name, source_dist_list_files) = list_source_dist(source_root, MOCK_UV_VERSION)?;
|
||||
let (_name, source_dist_list_files) =
|
||||
list_source_dist(source_root, MOCK_UV_VERSION, false)?;
|
||||
// TODO(konsti): This should run in the unpacked source dist tempdir, but we need to
|
||||
// normalize the path.
|
||||
let (_name, wheel_list_files) = list_wheel(source_root, MOCK_UV_VERSION)?;
|
||||
let source_dist_filename = build_source_dist(source_root, dist, MOCK_UV_VERSION)?;
|
||||
let (_name, wheel_list_files) = list_wheel(source_root, MOCK_UV_VERSION, false)?;
|
||||
let source_dist_filename = build_source_dist(source_root, dist, MOCK_UV_VERSION, false)?;
|
||||
let source_dist_path = dist.join(source_dist_filename.to_string());
|
||||
let source_dist_contents = sdist_contents(&source_dist_path);
|
||||
|
||||
|
|
@ -446,7 +505,13 @@ mod tests {
|
|||
source_dist_filename.name.as_dist_info_name(),
|
||||
source_dist_filename.version
|
||||
));
|
||||
let wheel_filename = build_wheel(&sdist_top_level_directory, dist, None, MOCK_UV_VERSION)?;
|
||||
let wheel_filename = build_wheel(
|
||||
&sdist_top_level_directory,
|
||||
dist,
|
||||
None,
|
||||
MOCK_UV_VERSION,
|
||||
false,
|
||||
)?;
|
||||
let wheel_contents = wheel_contents(&dist.join(wheel_filename.to_string()));
|
||||
|
||||
// Check that direct and indirect wheels are identical.
|
||||
|
|
@ -534,7 +599,7 @@ mod tests {
|
|||
/// platform-independent deterministic builds.
|
||||
#[test]
|
||||
fn built_by_uv_building() {
|
||||
let built_by_uv = Path::new("../../scripts/packages/built-by-uv");
|
||||
let built_by_uv = Path::new("../../test/packages/built-by-uv");
|
||||
let src = TempDir::new().unwrap();
|
||||
for dir in [
|
||||
"src",
|
||||
|
|
@ -597,7 +662,7 @@ mod tests {
|
|||
// Check that the source dist is reproducible across platforms.
|
||||
assert_snapshot!(
|
||||
format!("{:x}", sha2::Sha256::digest(fs_err::read(&source_dist_path).unwrap())),
|
||||
@"871d1f859140721b67cbeaca074e7a2740c88c38028d0509eba87d1285f1da9e"
|
||||
@"bb74bff575b135bb39e5c9bce56349441fb0923bb8857e32a5eaf34ec1843967"
|
||||
);
|
||||
// Check both the files we report and the actual files
|
||||
assert_snapshot!(format_file_list(build.source_dist_list_files, src.path()), @r"
|
||||
|
|
@ -756,7 +821,7 @@ mod tests {
|
|||
|
||||
// Build a wheel from a source distribution
|
||||
let output_dir = TempDir::new().unwrap();
|
||||
build_source_dist(src.path(), output_dir.path(), "0.5.15").unwrap();
|
||||
build_source_dist(src.path(), output_dir.path(), "0.5.15", false).unwrap();
|
||||
let sdist_tree = TempDir::new().unwrap();
|
||||
let source_dist_path = output_dir.path().join("pep_pep639_license-1.0.0.tar.gz");
|
||||
let sdist_reader = BufReader::new(File::open(&source_dist_path).unwrap());
|
||||
|
|
@ -767,6 +832,7 @@ mod tests {
|
|||
output_dir.path(),
|
||||
None,
|
||||
"0.5.15",
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let wheel = output_dir
|
||||
|
|
@ -831,6 +897,7 @@ mod tests {
|
|||
output_dir.path(),
|
||||
Some(&metadata_dir.path().join(&dist_info_dir)),
|
||||
"0.5.15",
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let wheel = output_dir
|
||||
|
|
@ -1414,4 +1481,114 @@ mod tests {
|
|||
simple_namespace_part-1.0.0.dist-info/WHEEL
|
||||
");
|
||||
}
|
||||
|
||||
/// `prune_redundant_modules` should remove modules which are already
|
||||
/// included (either directly or via their parent)
|
||||
#[test]
|
||||
fn test_prune_redundant_modules() {
|
||||
fn check(input: &[&str], expect: &[&str]) {
|
||||
let input = input.iter().map(|s| (*s).to_string()).collect();
|
||||
let expect: Vec<_> = expect.iter().map(|s| (*s).to_string()).collect();
|
||||
assert_eq!(prune_redundant_modules(input), expect);
|
||||
}
|
||||
|
||||
// Basic cases
|
||||
check(&[], &[]);
|
||||
check(&["foo"], &["foo"]);
|
||||
check(&["foo", "bar"], &["bar", "foo"]);
|
||||
|
||||
// Deshadowing
|
||||
check(&["foo", "foo.bar"], &["foo"]);
|
||||
check(&["foo.bar", "foo"], &["foo"]);
|
||||
check(
|
||||
&["foo.bar.a", "foo.bar.b", "foo.bar", "foo", "foo.bar.a.c"],
|
||||
&["foo"],
|
||||
);
|
||||
check(
|
||||
&["bar.one", "bar.two", "baz", "bar", "baz.one"],
|
||||
&["bar", "baz"],
|
||||
);
|
||||
|
||||
// Potential false positives
|
||||
check(&["foo", "foobar"], &["foo", "foobar"]);
|
||||
check(
|
||||
&["foo", "foobar", "foo.bar", "foobar.baz"],
|
||||
&["foo", "foobar"],
|
||||
);
|
||||
check(&["foo.bar", "foo.baz"], &["foo.bar", "foo.baz"]);
|
||||
check(&["foo", "foo", "foo.bar", "foo.bar"], &["foo"]);
|
||||
|
||||
// Everything
|
||||
check(
|
||||
&[
|
||||
"foo.inner",
|
||||
"foo.inner.deeper",
|
||||
"foo",
|
||||
"bar",
|
||||
"bar.sub",
|
||||
"bar.sub.deep",
|
||||
"foobar",
|
||||
"baz.baz.bar",
|
||||
"baz.baz",
|
||||
"qux",
|
||||
],
|
||||
&["bar", "baz.baz", "foo", "foobar", "qux"],
|
||||
);
|
||||
}
|
||||
|
||||
/// A package with duplicate module names.
|
||||
#[test]
|
||||
fn duplicate_module_names() {
|
||||
let src = TempDir::new().unwrap();
|
||||
let pyproject_toml = indoc! {r#"
|
||||
[project]
|
||||
name = "duplicate"
|
||||
version = "1.0.0"
|
||||
|
||||
[tool.uv.build-backend]
|
||||
module-name = ["foo", "foo", "bar.baz", "bar.baz.submodule"]
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.5.15,<0.6.0"]
|
||||
build-backend = "uv_build"
|
||||
"#
|
||||
};
|
||||
fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
|
||||
fs_err::create_dir_all(src.path().join("src").join("foo")).unwrap();
|
||||
File::create(src.path().join("src").join("foo").join("__init__.py")).unwrap();
|
||||
fs_err::create_dir_all(src.path().join("src").join("bar").join("baz")).unwrap();
|
||||
File::create(
|
||||
src.path()
|
||||
.join("src")
|
||||
.join("bar")
|
||||
.join("baz")
|
||||
.join("__init__.py"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let dist = TempDir::new().unwrap();
|
||||
let build = build(src.path(), dist.path()).unwrap();
|
||||
assert_snapshot!(build.source_dist_contents.join("\n"), @r"
|
||||
duplicate-1.0.0/
|
||||
duplicate-1.0.0/PKG-INFO
|
||||
duplicate-1.0.0/pyproject.toml
|
||||
duplicate-1.0.0/src
|
||||
duplicate-1.0.0/src/bar
|
||||
duplicate-1.0.0/src/bar/baz
|
||||
duplicate-1.0.0/src/bar/baz/__init__.py
|
||||
duplicate-1.0.0/src/foo
|
||||
duplicate-1.0.0/src/foo/__init__.py
|
||||
");
|
||||
assert_snapshot!(build.wheel_contents.join("\n"), @r"
|
||||
bar/
|
||||
bar/baz/
|
||||
bar/baz/__init__.py
|
||||
duplicate-1.0.0.dist-info/
|
||||
duplicate-1.0.0.dist-info/METADATA
|
||||
duplicate-1.0.0.dist-info/RECORD
|
||||
duplicate-1.0.0.dist-info/WHEEL
|
||||
foo/
|
||||
foo/__init__.py
|
||||
");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::ffi::OsStr;
|
|||
use std::fmt::Display;
|
||||
use std::fmt::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::str::{self, FromStr};
|
||||
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
|
@ -60,6 +60,10 @@ pub enum ValidationError {
|
|||
ReservedGuiScripts,
|
||||
#[error("`project.license` is not a valid SPDX expression: {0}")]
|
||||
InvalidSpdx(String, #[source] spdx::error::ParseError),
|
||||
#[error("`{field}` glob `{glob}` did not match any files")]
|
||||
LicenseGlobNoMatches { field: String, glob: String },
|
||||
#[error("License file `{}` must be UTF-8 encoded", _0)]
|
||||
LicenseFileNotUtf8(String),
|
||||
}
|
||||
|
||||
/// Check if the build backend is matching the currently running uv version.
|
||||
|
|
@ -150,8 +154,11 @@ impl PyProjectToml {
|
|||
&self.project.version
|
||||
}
|
||||
|
||||
pub(crate) fn parse(contents: &str) -> Result<Self, Error> {
|
||||
Ok(toml::from_str(contents)?)
|
||||
pub(crate) fn parse(path: &Path) -> Result<Self, Error> {
|
||||
let contents = fs_err::read_to_string(path)?;
|
||||
let pyproject_toml =
|
||||
toml::from_str(&contents).map_err(|err| Error::Toml(path.to_path_buf(), err))?;
|
||||
Ok(pyproject_toml)
|
||||
}
|
||||
|
||||
pub(crate) fn readme(&self) -> Option<&Readme> {
|
||||
|
|
@ -339,99 +346,7 @@ impl PyProjectToml {
|
|||
"2.3"
|
||||
};
|
||||
|
||||
// TODO(konsti): Issue a warning on old license metadata once PEP 639 is universal.
|
||||
let (license, license_expression, license_files) =
|
||||
if let Some(license_globs) = &self.project.license_files {
|
||||
let license_expression = match &self.project.license {
|
||||
None => None,
|
||||
Some(License::Spdx(license_expression)) => Some(license_expression.clone()),
|
||||
Some(License::Text { .. } | License::File { .. }) => {
|
||||
return Err(ValidationError::MixedLicenseGenerations.into());
|
||||
}
|
||||
};
|
||||
|
||||
let mut license_files = Vec::new();
|
||||
let mut license_globs_parsed = Vec::new();
|
||||
for license_glob in license_globs {
|
||||
let pep639_glob =
|
||||
PortableGlobParser::Pep639
|
||||
.parse(license_glob)
|
||||
.map_err(|err| Error::PortableGlob {
|
||||
field: license_glob.to_owned(),
|
||||
source: err,
|
||||
})?;
|
||||
license_globs_parsed.push(pep639_glob);
|
||||
}
|
||||
let license_globs =
|
||||
GlobDirFilter::from_globs(&license_globs_parsed).map_err(|err| {
|
||||
Error::GlobSetTooLarge {
|
||||
field: "tool.uv.build-backend.source-include".to_string(),
|
||||
source: err,
|
||||
}
|
||||
})?;
|
||||
|
||||
for entry in WalkDir::new(root)
|
||||
.sort_by_file_name()
|
||||
.into_iter()
|
||||
.filter_entry(|entry| {
|
||||
license_globs.match_directory(
|
||||
entry
|
||||
.path()
|
||||
.strip_prefix(root)
|
||||
.expect("walkdir starts with root"),
|
||||
)
|
||||
})
|
||||
{
|
||||
let entry = entry.map_err(|err| Error::WalkDir {
|
||||
root: root.to_path_buf(),
|
||||
err,
|
||||
})?;
|
||||
let relative = entry
|
||||
.path()
|
||||
.strip_prefix(root)
|
||||
.expect("walkdir starts with root");
|
||||
if !license_globs.match_path(relative) {
|
||||
trace!("Not a license files match: {}", relative.user_display());
|
||||
continue;
|
||||
}
|
||||
if !entry.file_type().is_file() {
|
||||
trace!(
|
||||
"Not a file in license files match: {}",
|
||||
relative.user_display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
error_on_venv(entry.file_name(), entry.path())?;
|
||||
|
||||
debug!("License files match: {}", relative.user_display());
|
||||
license_files.push(relative.portable_display().to_string());
|
||||
}
|
||||
|
||||
// The glob order may be unstable
|
||||
license_files.sort();
|
||||
|
||||
(None, license_expression, license_files)
|
||||
} else {
|
||||
match &self.project.license {
|
||||
None => (None, None, Vec::new()),
|
||||
Some(License::Spdx(license_expression)) => {
|
||||
(None, Some(license_expression.clone()), Vec::new())
|
||||
}
|
||||
Some(License::Text { text }) => (Some(text.clone()), None, Vec::new()),
|
||||
Some(License::File { file }) => {
|
||||
let text = fs_err::read_to_string(root.join(file))?;
|
||||
(Some(text), None, Vec::new())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Check that the license expression is a valid SPDX identifier.
|
||||
if let Some(license_expression) = &license_expression {
|
||||
if let Err(err) = spdx::Expression::parse(license_expression) {
|
||||
return Err(ValidationError::InvalidSpdx(license_expression.clone(), err).into());
|
||||
}
|
||||
}
|
||||
let (license, license_expression, license_files) = self.license_metadata(root)?;
|
||||
|
||||
// TODO(konsti): https://peps.python.org/pep-0753/#label-normalization (Draft)
|
||||
let project_urls = self
|
||||
|
|
@ -518,6 +433,156 @@ impl PyProjectToml {
|
|||
})
|
||||
}
|
||||
|
||||
/// Parse and validate the old (PEP 621) and new (PEP 639) license files.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn license_metadata(
|
||||
&self,
|
||||
root: &Path,
|
||||
) -> Result<(Option<String>, Option<String>, Vec<String>), Error> {
|
||||
// TODO(konsti): Issue a warning on old license metadata once PEP 639 is universal.
|
||||
let (license, license_expression, license_files) = if let Some(license_globs) =
|
||||
&self.project.license_files
|
||||
{
|
||||
let license_expression = match &self.project.license {
|
||||
None => None,
|
||||
Some(License::Spdx(license_expression)) => Some(license_expression.clone()),
|
||||
Some(License::Text { .. } | License::File { .. }) => {
|
||||
return Err(ValidationError::MixedLicenseGenerations.into());
|
||||
}
|
||||
};
|
||||
|
||||
let mut license_files = Vec::new();
|
||||
let mut license_globs_parsed = Vec::with_capacity(license_globs.len());
|
||||
let mut license_glob_matchers = Vec::with_capacity(license_globs.len());
|
||||
|
||||
for license_glob in license_globs {
|
||||
let pep639_glob =
|
||||
PortableGlobParser::Pep639
|
||||
.parse(license_glob)
|
||||
.map_err(|err| Error::PortableGlob {
|
||||
field: license_glob.to_owned(),
|
||||
source: err,
|
||||
})?;
|
||||
license_glob_matchers.push(pep639_glob.compile_matcher());
|
||||
license_globs_parsed.push(pep639_glob);
|
||||
}
|
||||
|
||||
// Track whether each user-specified glob matched so we can flag the unmatched ones.
|
||||
let mut license_globs_matched = vec![false; license_globs_parsed.len()];
|
||||
|
||||
let license_globs =
|
||||
GlobDirFilter::from_globs(&license_globs_parsed).map_err(|err| {
|
||||
Error::GlobSetTooLarge {
|
||||
field: "project.license-files".to_string(),
|
||||
source: err,
|
||||
}
|
||||
})?;
|
||||
|
||||
for entry in WalkDir::new(root)
|
||||
.sort_by_file_name()
|
||||
.into_iter()
|
||||
.filter_entry(|entry| {
|
||||
license_globs.match_directory(
|
||||
entry
|
||||
.path()
|
||||
.strip_prefix(root)
|
||||
.expect("walkdir starts with root"),
|
||||
)
|
||||
})
|
||||
{
|
||||
let entry = entry.map_err(|err| Error::WalkDir {
|
||||
root: root.to_path_buf(),
|
||||
err,
|
||||
})?;
|
||||
|
||||
let relative = entry
|
||||
.path()
|
||||
.strip_prefix(root)
|
||||
.expect("walkdir starts with root");
|
||||
|
||||
if !license_globs.match_path(relative) {
|
||||
trace!("Not a license files match: {}", relative.user_display());
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_type = entry.file_type();
|
||||
|
||||
if !(file_type.is_file() || file_type.is_symlink()) {
|
||||
trace!(
|
||||
"Not a file or symlink in license files match: {}",
|
||||
relative.user_display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
error_on_venv(entry.file_name(), entry.path())?;
|
||||
|
||||
debug!("License files match: {}", relative.user_display());
|
||||
|
||||
for (matched, matcher) in license_globs_matched
|
||||
.iter_mut()
|
||||
.zip(license_glob_matchers.iter())
|
||||
{
|
||||
if *matched {
|
||||
continue;
|
||||
}
|
||||
|
||||
if matcher.is_match(relative) {
|
||||
*matched = true;
|
||||
}
|
||||
}
|
||||
|
||||
license_files.push(relative.portable_display().to_string());
|
||||
}
|
||||
|
||||
if let Some((pattern, _)) = license_globs_parsed
|
||||
.into_iter()
|
||||
.zip(license_globs_matched)
|
||||
.find(|(_, matched)| !matched)
|
||||
{
|
||||
return Err(ValidationError::LicenseGlobNoMatches {
|
||||
field: "project.license-files".to_string(),
|
||||
glob: pattern.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
for license_file in &license_files {
|
||||
let file_path = root.join(license_file);
|
||||
let bytes = fs_err::read(&file_path)?;
|
||||
if str::from_utf8(&bytes).is_err() {
|
||||
return Err(ValidationError::LicenseFileNotUtf8(license_file.clone()).into());
|
||||
}
|
||||
}
|
||||
|
||||
// The glob order may be unstable
|
||||
license_files.sort();
|
||||
|
||||
(None, license_expression, license_files)
|
||||
} else {
|
||||
match &self.project.license {
|
||||
None => (None, None, Vec::new()),
|
||||
Some(License::Spdx(license_expression)) => {
|
||||
(None, Some(license_expression.clone()), Vec::new())
|
||||
}
|
||||
Some(License::Text { text }) => (Some(text.clone()), None, Vec::new()),
|
||||
Some(License::File { file }) => {
|
||||
let text = fs_err::read_to_string(root.join(file))?;
|
||||
(Some(text), None, Vec::new())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Check that the license expression is a valid SPDX identifier.
|
||||
if let Some(license_expression) = &license_expression {
|
||||
if let Err(err) = spdx::Expression::parse(license_expression) {
|
||||
return Err(ValidationError::InvalidSpdx(license_expression.clone(), err).into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok((license, license_expression, license_files))
|
||||
}
|
||||
|
||||
/// Validate and convert the entrypoints in `pyproject.toml`, including console and GUI scripts,
|
||||
/// to an `entry_points.txt`.
|
||||
///
|
||||
|
|
@ -887,7 +952,7 @@ mod tests {
|
|||
requires = ["uv_build>=0.4.15,<0.5.0"]
|
||||
build-backend = "uv_build"
|
||||
"#;
|
||||
let pyproject_toml = PyProjectToml::parse(contents).unwrap();
|
||||
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
||||
let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
|
||||
|
|
@ -972,7 +1037,7 @@ mod tests {
|
|||
"#
|
||||
};
|
||||
|
||||
let pyproject_toml = PyProjectToml::parse(contents).unwrap();
|
||||
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
|
||||
let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
|
||||
|
||||
assert_snapshot!(metadata.core_metadata_format(), @r###"
|
||||
|
|
@ -1066,7 +1131,7 @@ mod tests {
|
|||
"#
|
||||
};
|
||||
|
||||
let pyproject_toml = PyProjectToml::parse(contents).unwrap();
|
||||
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
|
||||
let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
|
||||
|
||||
assert_snapshot!(metadata.core_metadata_format(), @r"
|
||||
|
|
@ -1158,7 +1223,7 @@ mod tests {
|
|||
"#
|
||||
};
|
||||
|
||||
let pyproject_toml = PyProjectToml::parse(contents).unwrap();
|
||||
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
|
||||
let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
|
||||
|
||||
assert_snapshot!(metadata.core_metadata_format(), @r###"
|
||||
|
|
@ -1219,7 +1284,7 @@ mod tests {
|
|||
#[test]
|
||||
fn build_system_valid() {
|
||||
let contents = extend_project("");
|
||||
let pyproject_toml = PyProjectToml::parse(&contents).unwrap();
|
||||
let pyproject_toml: PyProjectToml = toml::from_str(&contents).unwrap();
|
||||
assert_snapshot!(
|
||||
pyproject_toml.check_build_system("0.4.15+test").join("\n"),
|
||||
@""
|
||||
|
|
@ -1237,7 +1302,7 @@ mod tests {
|
|||
requires = ["uv_build"]
|
||||
build-backend = "uv_build"
|
||||
"#};
|
||||
let pyproject_toml = PyProjectToml::parse(contents).unwrap();
|
||||
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
|
||||
assert_snapshot!(
|
||||
pyproject_toml.check_build_system("0.4.15+test").join("\n"),
|
||||
@r###"`build_system.requires = ["uv_build"]` is missing an upper bound on the `uv_build` version such as `<0.5`. Without bounding the `uv_build` version, the source distribution will break when a future, breaking version of `uv_build` is released."###
|
||||
|
|
@ -1255,7 +1320,7 @@ mod tests {
|
|||
requires = ["uv_build>=0.4.15,<0.5.0", "wheel"]
|
||||
build-backend = "uv_build"
|
||||
"#};
|
||||
let pyproject_toml = PyProjectToml::parse(contents).unwrap();
|
||||
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
|
||||
assert_snapshot!(
|
||||
pyproject_toml.check_build_system("0.4.15+test").join("\n"),
|
||||
@"Expected a single uv requirement in `build-system.requires`, found ``"
|
||||
|
|
@ -1273,7 +1338,7 @@ mod tests {
|
|||
requires = ["setuptools"]
|
||||
build-backend = "uv_build"
|
||||
"#};
|
||||
let pyproject_toml = PyProjectToml::parse(contents).unwrap();
|
||||
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
|
||||
assert_snapshot!(
|
||||
pyproject_toml.check_build_system("0.4.15+test").join("\n"),
|
||||
@"Expected a single uv requirement in `build-system.requires`, found ``"
|
||||
|
|
@ -1291,7 +1356,7 @@ mod tests {
|
|||
requires = ["uv_build>=0.4.15,<0.5.0"]
|
||||
build-backend = "setuptools"
|
||||
"#};
|
||||
let pyproject_toml = PyProjectToml::parse(contents).unwrap();
|
||||
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
|
||||
assert_snapshot!(
|
||||
pyproject_toml.check_build_system("0.4.15+test").join("\n"),
|
||||
@r###"The value for `build_system.build-backend` should be `"uv_build"`, not `"setuptools"`"###
|
||||
|
|
@ -1302,7 +1367,7 @@ mod tests {
|
|||
fn minimal() {
|
||||
let contents = extend_project("");
|
||||
|
||||
let metadata = PyProjectToml::parse(&contents)
|
||||
let metadata = toml::from_str::<PyProjectToml>(&contents)
|
||||
.unwrap()
|
||||
.to_metadata(Path::new("/do/not/read"))
|
||||
.unwrap();
|
||||
|
|
@ -1321,15 +1386,14 @@ mod tests {
|
|||
"#
|
||||
});
|
||||
|
||||
let err = PyProjectToml::parse(&contents).unwrap_err();
|
||||
assert_snapshot!(format_err(err), @r###"
|
||||
Invalid pyproject.toml
|
||||
Caused by: TOML parse error at line 4, column 10
|
||||
let err = toml::from_str::<PyProjectToml>(&contents).unwrap_err();
|
||||
assert_snapshot!(format_err(err), @r#"
|
||||
TOML parse error at line 4, column 10
|
||||
|
|
||||
4 | readme = { path = "Readme.md" }
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
data did not match any variant of untagged enum Readme
|
||||
"###);
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1339,7 +1403,7 @@ mod tests {
|
|||
"#
|
||||
});
|
||||
|
||||
let err = PyProjectToml::parse(&contents)
|
||||
let err = toml::from_str::<PyProjectToml>(&contents)
|
||||
.unwrap()
|
||||
.to_metadata(Path::new("/do/not/read"))
|
||||
.unwrap_err();
|
||||
|
|
@ -1361,14 +1425,14 @@ mod tests {
|
|||
"#
|
||||
});
|
||||
|
||||
let err = PyProjectToml::parse(&contents)
|
||||
let err = toml::from_str::<PyProjectToml>(&contents)
|
||||
.unwrap()
|
||||
.to_metadata(Path::new("/do/not/read"))
|
||||
.unwrap_err();
|
||||
assert_snapshot!(format_err(err), @r###"
|
||||
Invalid pyproject.toml
|
||||
assert_snapshot!(format_err(err), @r"
|
||||
Invalid project metadata
|
||||
Caused by: `project.description` must be a single line
|
||||
"###);
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1379,14 +1443,14 @@ mod tests {
|
|||
"#
|
||||
});
|
||||
|
||||
let err = PyProjectToml::parse(&contents)
|
||||
let err = toml::from_str::<PyProjectToml>(&contents)
|
||||
.unwrap()
|
||||
.to_metadata(Path::new("/do/not/read"))
|
||||
.unwrap_err();
|
||||
assert_snapshot!(format_err(err), @r###"
|
||||
Invalid pyproject.toml
|
||||
assert_snapshot!(format_err(err), @r"
|
||||
Invalid project metadata
|
||||
Caused by: When `project.license-files` is defined, `project.license` must be an SPDX expression string
|
||||
"###);
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1395,7 +1459,7 @@ mod tests {
|
|||
license = "MIT OR Apache-2.0"
|
||||
"#
|
||||
});
|
||||
let metadata = PyProjectToml::parse(&contents)
|
||||
let metadata = toml::from_str::<PyProjectToml>(&contents)
|
||||
.unwrap()
|
||||
.to_metadata(Path::new("/do/not/read"))
|
||||
.unwrap();
|
||||
|
|
@ -1413,13 +1477,13 @@ mod tests {
|
|||
license = "MIT XOR Apache-2"
|
||||
"#
|
||||
});
|
||||
let err = PyProjectToml::parse(&contents)
|
||||
let err = toml::from_str::<PyProjectToml>(&contents)
|
||||
.unwrap()
|
||||
.to_metadata(Path::new("/do/not/read"))
|
||||
.unwrap_err();
|
||||
// TODO(konsti): We mess up the indentation in the error.
|
||||
assert_snapshot!(format_err(err), @r"
|
||||
Invalid pyproject.toml
|
||||
Invalid project metadata
|
||||
Caused by: `project.license` is not a valid SPDX expression: MIT XOR Apache-2
|
||||
Caused by: MIT XOR Apache-2
|
||||
^^^ unknown term
|
||||
|
|
@ -1433,18 +1497,18 @@ mod tests {
|
|||
"#
|
||||
});
|
||||
|
||||
let err = PyProjectToml::parse(&contents)
|
||||
let err = toml::from_str::<PyProjectToml>(&contents)
|
||||
.unwrap()
|
||||
.to_metadata(Path::new("/do/not/read"))
|
||||
.unwrap_err();
|
||||
assert_snapshot!(format_err(err), @r###"
|
||||
Invalid pyproject.toml
|
||||
assert_snapshot!(format_err(err), @r"
|
||||
Invalid project metadata
|
||||
Caused by: Dynamic metadata is not supported
|
||||
"###);
|
||||
");
|
||||
}
|
||||
|
||||
fn script_error(contents: &str) -> String {
|
||||
let err = PyProjectToml::parse(contents)
|
||||
let err = toml::from_str::<PyProjectToml>(contents)
|
||||
.unwrap()
|
||||
.to_entry_points()
|
||||
.unwrap_err();
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@ pub struct BuildBackendSettings {
|
|||
pub default_excludes: bool,
|
||||
|
||||
/// Glob expressions which files and directories to exclude from the source distribution.
|
||||
///
|
||||
/// These exclusions are also applied to wheels to ensure that a wheel built from a source tree
|
||||
/// is consistent with a wheel built from a source distribution.
|
||||
#[option(
|
||||
default = r#"[]"#,
|
||||
value_type = "list[str]",
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ pub fn build_source_dist(
|
|||
source_tree: &Path,
|
||||
source_dist_directory: &Path,
|
||||
uv_version: &str,
|
||||
show_warnings: bool,
|
||||
) -> Result<SourceDistFilename, Error> {
|
||||
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
|
||||
let pyproject_toml = PyProjectToml::parse(&contents)?;
|
||||
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
|
||||
let filename = SourceDistFilename {
|
||||
name: pyproject_toml.name().clone(),
|
||||
version: pyproject_toml.version().clone(),
|
||||
|
|
@ -34,7 +34,7 @@ pub fn build_source_dist(
|
|||
};
|
||||
let source_dist_path = source_dist_directory.join(filename.to_string());
|
||||
let writer = TarGzWriter::new(&source_dist_path)?;
|
||||
write_source_dist(source_tree, writer, uv_version)?;
|
||||
write_source_dist(source_tree, writer, uv_version, show_warnings)?;
|
||||
Ok(filename)
|
||||
}
|
||||
|
||||
|
|
@ -42,9 +42,9 @@ pub fn build_source_dist(
|
|||
pub fn list_source_dist(
|
||||
source_tree: &Path,
|
||||
uv_version: &str,
|
||||
show_warnings: bool,
|
||||
) -> Result<(SourceDistFilename, FileList), Error> {
|
||||
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
|
||||
let pyproject_toml = PyProjectToml::parse(&contents)?;
|
||||
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
|
||||
let filename = SourceDistFilename {
|
||||
name: pyproject_toml.name().clone(),
|
||||
version: pyproject_toml.version().clone(),
|
||||
|
|
@ -52,7 +52,7 @@ pub fn list_source_dist(
|
|||
};
|
||||
let mut files = FileList::new();
|
||||
let writer = ListWriter::new(&mut files);
|
||||
write_source_dist(source_tree, writer, uv_version)?;
|
||||
write_source_dist(source_tree, writer, uv_version, show_warnings)?;
|
||||
Ok((filename, files))
|
||||
}
|
||||
|
||||
|
|
@ -61,6 +61,7 @@ fn source_dist_matcher(
|
|||
source_tree: &Path,
|
||||
pyproject_toml: &PyProjectToml,
|
||||
settings: BuildBackendSettings,
|
||||
show_warnings: bool,
|
||||
) -> Result<(GlobDirFilter, GlobSet), Error> {
|
||||
// File and directories to include in the source directory
|
||||
let mut include_globs = Vec::new();
|
||||
|
|
@ -75,6 +76,7 @@ fn source_dist_matcher(
|
|||
&settings.module_root,
|
||||
settings.module_name.as_ref(),
|
||||
settings.namespace,
|
||||
show_warnings,
|
||||
)?;
|
||||
for module_relative in modules_relative {
|
||||
// The wheel must not include any files included by the source distribution (at least until we
|
||||
|
|
@ -182,9 +184,9 @@ fn write_source_dist(
|
|||
source_tree: &Path,
|
||||
mut writer: impl DirectoryWriter,
|
||||
uv_version: &str,
|
||||
show_warnings: bool,
|
||||
) -> Result<SourceDistFilename, Error> {
|
||||
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
|
||||
let pyproject_toml = PyProjectToml::parse(&contents)?;
|
||||
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
|
||||
for warning in pyproject_toml.check_build_system(uv_version) {
|
||||
warn_user_once!("{warning}");
|
||||
}
|
||||
|
|
@ -218,7 +220,7 @@ fn write_source_dist(
|
|||
)?;
|
||||
|
||||
let (include_matcher, exclude_matcher) =
|
||||
source_dist_matcher(source_tree, &pyproject_toml, settings)?;
|
||||
source_dist_matcher(source_tree, &pyproject_toml, settings, show_warnings)?;
|
||||
|
||||
let mut files_visited = 0;
|
||||
for entry in WalkDir::new(source_tree)
|
||||
|
|
@ -297,6 +299,10 @@ impl TarGzWriter {
|
|||
impl DirectoryWriter for TarGzWriter {
|
||||
fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error> {
|
||||
let mut header = Header::new_gnu();
|
||||
// Work around bug in Python's std tar module
|
||||
// https://github.com/python/cpython/issues/141707
|
||||
// https://github.com/astral-sh/uv/pull/17043#issuecomment-3636841022
|
||||
header.set_entry_type(EntryType::Regular);
|
||||
header.set_size(bytes.len() as u64);
|
||||
// Reasonable default to avoid 0o000 permissions, the user's umask will be applied on
|
||||
// unpacking.
|
||||
|
|
@ -310,6 +316,10 @@ impl DirectoryWriter for TarGzWriter {
|
|||
fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error> {
|
||||
let metadata = fs_err::metadata(file)?;
|
||||
let mut header = Header::new_gnu();
|
||||
// Work around bug in Python's std tar module
|
||||
// https://github.com/python/cpython/issues/141707
|
||||
// https://github.com/astral-sh/uv/pull/17043#issuecomment-3636841022
|
||||
header.set_entry_type(EntryType::Regular);
|
||||
// Preserve the executable bit, especially for scripts
|
||||
#[cfg(unix)]
|
||||
let executable_bit = {
|
||||
|
|
|
|||
|
|
@ -29,9 +29,9 @@ pub fn build_wheel(
|
|||
wheel_dir: &Path,
|
||||
metadata_directory: Option<&Path>,
|
||||
uv_version: &str,
|
||||
show_warnings: bool,
|
||||
) -> Result<WheelFilename, Error> {
|
||||
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
|
||||
let pyproject_toml = PyProjectToml::parse(&contents)?;
|
||||
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
|
||||
for warning in pyproject_toml.check_build_system(uv_version) {
|
||||
warn_user_once!("{warning}");
|
||||
}
|
||||
|
|
@ -58,6 +58,7 @@ pub fn build_wheel(
|
|||
&filename,
|
||||
uv_version,
|
||||
wheel_writer,
|
||||
show_warnings,
|
||||
)?;
|
||||
|
||||
Ok(filename)
|
||||
|
|
@ -67,9 +68,9 @@ pub fn build_wheel(
|
|||
pub fn list_wheel(
|
||||
source_tree: &Path,
|
||||
uv_version: &str,
|
||||
show_warnings: bool,
|
||||
) -> Result<(WheelFilename, FileList), Error> {
|
||||
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
|
||||
let pyproject_toml = PyProjectToml::parse(&contents)?;
|
||||
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
|
||||
for warning in pyproject_toml.check_build_system(uv_version) {
|
||||
warn_user_once!("{warning}");
|
||||
}
|
||||
|
|
@ -87,7 +88,14 @@ pub fn list_wheel(
|
|||
|
||||
let mut files = FileList::new();
|
||||
let writer = ListWriter::new(&mut files);
|
||||
write_wheel(source_tree, &pyproject_toml, &filename, uv_version, writer)?;
|
||||
write_wheel(
|
||||
source_tree,
|
||||
&pyproject_toml,
|
||||
&filename,
|
||||
uv_version,
|
||||
writer,
|
||||
show_warnings,
|
||||
)?;
|
||||
Ok((filename, files))
|
||||
}
|
||||
|
||||
|
|
@ -97,6 +105,7 @@ fn write_wheel(
|
|||
filename: &WheelFilename,
|
||||
uv_version: &str,
|
||||
mut wheel_writer: impl DirectoryWriter,
|
||||
show_warnings: bool,
|
||||
) -> Result<(), Error> {
|
||||
let settings = pyproject_toml
|
||||
.settings()
|
||||
|
|
@ -132,6 +141,7 @@ fn write_wheel(
|
|||
&settings.module_root,
|
||||
settings.module_name.as_ref(),
|
||||
settings.namespace,
|
||||
show_warnings,
|
||||
)?;
|
||||
|
||||
let mut files_visited = 0;
|
||||
|
|
@ -259,9 +269,9 @@ pub fn build_editable(
|
|||
wheel_dir: &Path,
|
||||
metadata_directory: Option<&Path>,
|
||||
uv_version: &str,
|
||||
show_warnings: bool,
|
||||
) -> Result<WheelFilename, Error> {
|
||||
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
|
||||
let pyproject_toml = PyProjectToml::parse(&contents)?;
|
||||
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
|
||||
for warning in pyproject_toml.check_build_system(uv_version) {
|
||||
warn_user_once!("{warning}");
|
||||
}
|
||||
|
|
@ -295,6 +305,7 @@ pub fn build_editable(
|
|||
&settings.module_root,
|
||||
settings.module_name.as_ref(),
|
||||
settings.namespace,
|
||||
show_warnings,
|
||||
)?;
|
||||
|
||||
wheel_writer.write_bytes(
|
||||
|
|
@ -321,8 +332,7 @@ pub fn metadata(
|
|||
metadata_directory: &Path,
|
||||
uv_version: &str,
|
||||
) -> Result<String, Error> {
|
||||
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
|
||||
let pyproject_toml = PyProjectToml::parse(&contents)?;
|
||||
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
|
||||
for warning in pyproject_toml.check_build_system(uv_version) {
|
||||
warn_user_once!("{warning}");
|
||||
}
|
||||
|
|
@ -830,7 +840,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_prepare_metadata() {
|
||||
let metadata_dir = TempDir::new().unwrap();
|
||||
let built_by_uv = Path::new("../../scripts/packages/built-by-uv");
|
||||
let built_by_uv = Path::new("../../test/packages/built-by-uv");
|
||||
metadata(built_by_uv, metadata_dir.path(), "1.0.0+test").unwrap();
|
||||
|
||||
let mut files: Vec<_> = WalkDir::new(metadata_dir.path())
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
[package]
|
||||
name = "uv-build-frontend"
|
||||
version = "0.0.1"
|
||||
description = "Build wheels from source distributions"
|
||||
version = "0.0.8"
|
||||
description = "This is an internal component crate of uv"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
|
@ -17,6 +16,7 @@ doctest = false
|
|||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
uv-auth = { workspace = true }
|
||||
uv-cache-key = { workspace = true }
|
||||
uv-configuration = { workspace = true }
|
||||
uv-distribution = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- This file is generated. DO NOT EDIT -->
|
||||
|
||||
# uv-build-frontend
|
||||
|
||||
This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here
|
||||
is unstable and will have frequent breaking changes.
|
||||
|
||||
This version (0.0.8) is a component of [uv 0.9.18](https://crates.io/crates/uv/0.9.18). The source
|
||||
can be found [here](https://github.com/astral-sh/uv/blob/0.9.18/crates/uv-build-frontend).
|
||||
|
||||
See uv's
|
||||
[crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning)
|
||||
for details on versioning.
|
||||
|
|
@ -28,7 +28,7 @@ use tokio::io::AsyncBufReadExt;
|
|||
use tokio::process::Command;
|
||||
use tokio::sync::{Mutex, Semaphore};
|
||||
use tracing::{Instrument, debug, info_span, instrument, warn};
|
||||
|
||||
use uv_auth::CredentialsCache;
|
||||
use uv_cache_key::cache_digest;
|
||||
use uv_configuration::{BuildKind, BuildOutput, SourceStrategy};
|
||||
use uv_distribution::BuildRequires;
|
||||
|
|
@ -36,7 +36,7 @@ use uv_distribution_types::{
|
|||
ConfigSettings, ExtraBuildRequirement, ExtraBuildRequires, IndexLocations, Requirement,
|
||||
Resolution,
|
||||
};
|
||||
use uv_fs::LockedFile;
|
||||
use uv_fs::{LockedFile, LockedFileMode};
|
||||
use uv_fs::{PythonExt, Simplified};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::Version;
|
||||
|
|
@ -292,6 +292,7 @@ impl SourceBuild {
|
|||
mut environment_variables: FxHashMap<OsString, OsString>,
|
||||
level: BuildOutput,
|
||||
concurrent_builds: usize,
|
||||
credentials_cache: &CredentialsCache,
|
||||
preview: Preview,
|
||||
) -> Result<Self, Error> {
|
||||
let temp_dir = build_context.cache().venv_dir()?;
|
||||
|
|
@ -302,7 +303,6 @@ impl SourceBuild {
|
|||
source.to_path_buf()
|
||||
};
|
||||
|
||||
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,
|
||||
|
|
@ -311,7 +311,7 @@ impl SourceBuild {
|
|||
locations,
|
||||
source_strategy,
|
||||
workspace_cache,
|
||||
&default_backend,
|
||||
credentials_cache,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| *err)?;
|
||||
|
|
@ -383,7 +383,6 @@ impl SourceBuild {
|
|||
let resolved_requirements = Self::get_resolved_requirements(
|
||||
build_context,
|
||||
source_build_context,
|
||||
&default_backend,
|
||||
&pep517_backend,
|
||||
extra_build_dependencies,
|
||||
build_stack,
|
||||
|
|
@ -455,6 +454,7 @@ impl SourceBuild {
|
|||
&environment_variables,
|
||||
&modified_path,
|
||||
&temp_dir,
|
||||
credentials_cache,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
|
@ -493,12 +493,16 @@ impl SourceBuild {
|
|||
"uv-setuptools-{}.lock",
|
||||
cache_digest(&canonical_source_path)
|
||||
));
|
||||
source_tree_lock = LockedFile::acquire(lock_path, self.source_tree.to_string_lossy())
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
warn!("Failed to acquire build lock: {err}");
|
||||
})
|
||||
.ok();
|
||||
source_tree_lock = LockedFile::acquire(
|
||||
lock_path,
|
||||
LockedFileMode::Exclusive,
|
||||
self.source_tree.to_string_lossy(),
|
||||
)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
warn!("Failed to acquire build lock: {err}");
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Ok(source_tree_lock)
|
||||
}
|
||||
|
|
@ -506,13 +510,12 @@ impl SourceBuild {
|
|||
async fn get_resolved_requirements(
|
||||
build_context: &impl BuildContext,
|
||||
source_build_context: SourceBuildContext,
|
||||
default_backend: &Pep517Backend,
|
||||
pep517_backend: &Pep517Backend,
|
||||
extra_build_dependencies: Vec<Requirement>,
|
||||
build_stack: &BuildStack,
|
||||
) -> Result<Resolution, Error> {
|
||||
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;
|
||||
|
|
@ -520,7 +523,7 @@ impl SourceBuild {
|
|||
resolved_requirements.clone()
|
||||
} else {
|
||||
let resolved_requirements = build_context
|
||||
.resolve(&default_backend.requirements, build_stack)
|
||||
.resolve(&DEFAULT_BACKEND.requirements, build_stack)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
Error::RequirementsResolve("`setup.py` build", err.into())
|
||||
|
|
@ -560,7 +563,7 @@ impl SourceBuild {
|
|||
locations: &IndexLocations,
|
||||
source_strategy: SourceStrategy,
|
||||
workspace_cache: &WorkspaceCache,
|
||||
default_backend: &Pep517Backend,
|
||||
credentials_cache: &CredentialsCache,
|
||||
) -> Result<(Pep517Backend, Option<Project>), Box<Error>> {
|
||||
match fs::read_to_string(source_tree.join("pyproject.toml")) {
|
||||
Ok(toml) => {
|
||||
|
|
@ -589,6 +592,7 @@ impl SourceBuild {
|
|||
locations,
|
||||
source_strategy,
|
||||
workspace_cache,
|
||||
credentials_cache,
|
||||
)
|
||||
.await
|
||||
.map_err(Error::Lowering)?;
|
||||
|
|
@ -658,7 +662,7 @@ impl SourceBuild {
|
|||
}
|
||||
}
|
||||
|
||||
default_backend.clone()
|
||||
DEFAULT_BACKEND.clone()
|
||||
};
|
||||
Ok((backend, pyproject_toml.project))
|
||||
}
|
||||
|
|
@ -674,7 +678,7 @@ impl SourceBuild {
|
|||
// the default backend, to match `build`. `pip` uses `setup.py` directly in this
|
||||
// case, but plans to make PEP 517 builds the default in the future.
|
||||
// See: https://github.com/pypa/pip/issues/9175.
|
||||
Ok((default_backend.clone(), None))
|
||||
Ok((DEFAULT_BACKEND.clone(), None))
|
||||
}
|
||||
Err(err) => Err(Box::new(err.into())),
|
||||
}
|
||||
|
|
@ -961,6 +965,7 @@ async fn create_pep517_build_environment(
|
|||
environment_variables: &FxHashMap<OsString, OsString>,
|
||||
modified_path: &OsString,
|
||||
temp_dir: &TempDir,
|
||||
credentials_cache: &CredentialsCache,
|
||||
) -> Result<(), Error> {
|
||||
// Write the hook output to a file so that we can read it back reliably.
|
||||
let outfile = temp_dir
|
||||
|
|
@ -1055,6 +1060,7 @@ async fn create_pep517_build_environment(
|
|||
locations,
|
||||
source_strategy,
|
||||
workspace_cache,
|
||||
credentials_cache,
|
||||
)
|
||||
.await
|
||||
.map_err(Error::Lowering)?;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
[package]
|
||||
name = "uv-build"
|
||||
version = "0.9.9"
|
||||
version = "0.9.18"
|
||||
description = "A Python build backend"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "uv-build"
|
||||
version = "0.9.9"
|
||||
version = "0.9.18"
|
||||
description = "The uv build backend"
|
||||
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
|
||||
requires-python = ">=3.8"
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ fn main() -> Result<()> {
|
|||
&env::current_dir()?,
|
||||
&sdist_directory,
|
||||
uv_version::version(),
|
||||
false,
|
||||
)?;
|
||||
// Tell the build frontend about the name of the artifact we built
|
||||
writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?;
|
||||
|
|
@ -56,6 +57,7 @@ fn main() -> Result<()> {
|
|||
&wheel_directory,
|
||||
metadata_directory.as_deref(),
|
||||
uv_version::version(),
|
||||
false,
|
||||
)?;
|
||||
// Tell the build frontend about the name of the artifact we built
|
||||
writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?;
|
||||
|
|
@ -68,6 +70,7 @@ fn main() -> Result<()> {
|
|||
&wheel_directory,
|
||||
metadata_directory.as_deref(),
|
||||
uv_version::version(),
|
||||
false,
|
||||
)?;
|
||||
// Tell the build frontend about the name of the artifact we built
|
||||
writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
[package]
|
||||
name = "uv-cache-info"
|
||||
version = "0.0.1"
|
||||
version = "0.0.8"
|
||||
description = "This is an internal component crate of uv"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- This file is generated. DO NOT EDIT -->
|
||||
|
||||
# uv-cache-info
|
||||
|
||||
This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here
|
||||
is unstable and will have frequent breaking changes.
|
||||
|
||||
This version (0.0.8) is a component of [uv 0.9.18](https://crates.io/crates/uv/0.9.18). The source
|
||||
can be found [here](https://github.com/astral-sh/uv/blob/0.9.18/crates/uv-cache-info).
|
||||
|
||||
See uv's
|
||||
[crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning)
|
||||
for details on versioning.
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
[package]
|
||||
name = "uv-cache-key"
|
||||
version = "0.0.1"
|
||||
description = "Generic functionality for caching paths, URLs, and other resources across platforms."
|
||||
version = "0.0.8"
|
||||
description = "This is an internal component crate of uv"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- This file is generated. DO NOT EDIT -->
|
||||
|
||||
# uv-cache-key
|
||||
|
||||
This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here
|
||||
is unstable and will have frequent breaking changes.
|
||||
|
||||
This version (0.0.8) is a component of [uv 0.9.18](https://crates.io/crates/uv/0.9.18). The source
|
||||
can be found [here](https://github.com/astral-sh/uv/blob/0.9.18/crates/uv-cache-key).
|
||||
|
||||
See uv's
|
||||
[crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning)
|
||||
for details on versioning.
|
||||
|
|
@ -139,8 +139,18 @@ impl std::fmt::Display for CanonicalUrl {
|
|||
/// `https://github.com/pypa/package.git#subdirectory=pkg_b` would map to different
|
||||
/// [`CanonicalUrl`] values, but the same [`RepositoryUrl`], since they map to the same
|
||||
/// resource.
|
||||
///
|
||||
/// The additional information it holds should only be used to discriminate between
|
||||
/// sources that hold the exact same commit in their canonical representation,
|
||||
/// but may differ in the contents such as when Git LFS is enabled.
|
||||
///
|
||||
/// A different cache key will be computed when Git LFS is enabled.
|
||||
/// When Git LFS is `false` or `None`, the cache key remains unchanged.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
pub struct RepositoryUrl(DisplaySafeUrl);
|
||||
pub struct RepositoryUrl {
|
||||
repo_url: DisplaySafeUrl,
|
||||
with_lfs: Option<bool>,
|
||||
}
|
||||
|
||||
impl RepositoryUrl {
|
||||
pub fn new(url: &DisplaySafeUrl) -> Self {
|
||||
|
|
@ -161,19 +171,31 @@ impl RepositoryUrl {
|
|||
url.set_fragment(None);
|
||||
url.set_query(None);
|
||||
|
||||
Self(url)
|
||||
Self {
|
||||
repo_url: url,
|
||||
with_lfs: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(url: &str) -> Result<Self, DisplaySafeUrlError> {
|
||||
Ok(Self::new(&DisplaySafeUrl::parse(url)?))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_lfs(mut self, lfs: Option<bool>) -> Self {
|
||||
self.with_lfs = lfs;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheKey for RepositoryUrl {
|
||||
fn cache_key(&self, state: &mut CacheKeyHasher) {
|
||||
// `as_str` gives the serialisation of a url (which has a spec) and so insulates against
|
||||
// possible changes in how the URL crate does hashing.
|
||||
self.0.as_str().cache_key(state);
|
||||
self.repo_url.as_str().cache_key(state);
|
||||
if let Some(true) = self.with_lfs {
|
||||
1u8.cache_key(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -181,7 +203,10 @@ impl Hash for RepositoryUrl {
|
|||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
// `as_str` gives the serialisation of a url (which has a spec) and so insulates against
|
||||
// possible changes in how the URL crate does hashing.
|
||||
self.0.as_str().hash(state);
|
||||
self.repo_url.as_str().hash(state);
|
||||
if let Some(true) = self.with_lfs {
|
||||
1u8.hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -189,13 +214,13 @@ impl Deref for RepositoryUrl {
|
|||
type Target = Url;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
&self.repo_url
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RepositoryUrl {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.0, f)
|
||||
std::fmt::Display::fmt(&self.repo_url, f)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -283,6 +308,14 @@ mod tests {
|
|||
)?,
|
||||
);
|
||||
|
||||
// Two URLs should _not_ be considered equal if they differ in Git LFS enablement.
|
||||
assert_ne!(
|
||||
CanonicalUrl::parse(
|
||||
"git+https://github.com/pypa/sample-namespace-packages.git#lfs=true"
|
||||
)?,
|
||||
CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git")?,
|
||||
);
|
||||
|
||||
// Two URLs should _not_ be considered equal if they request different commit tags.
|
||||
assert_ne!(
|
||||
CanonicalUrl::parse(
|
||||
|
|
@ -378,6 +411,76 @@ mod tests {
|
|||
)?,
|
||||
);
|
||||
|
||||
// Two URLs should be considered equal if they map to the same repository, even if they
|
||||
// differ in Git LFS enablement.
|
||||
assert_eq!(
|
||||
RepositoryUrl::parse(
|
||||
"git+https://github.com/pypa/sample-namespace-packages.git#lfs=true"
|
||||
)?,
|
||||
RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git")?,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repository_url_with_lfs() -> Result<(), DisplaySafeUrlError> {
|
||||
let mut hasher = CacheKeyHasher::new();
|
||||
RepositoryUrl::parse("https://example.com/pypa/sample-namespace-packages.git@2.0.0")?
|
||||
.cache_key(&mut hasher);
|
||||
let repo_url_basic = hasher.finish();
|
||||
|
||||
let mut hasher = CacheKeyHasher::new();
|
||||
RepositoryUrl::parse(
|
||||
"https://user:foo@example.com/pypa/sample-namespace-packages.git@2.0.0#foo=bar",
|
||||
)?
|
||||
.cache_key(&mut hasher);
|
||||
let repo_url_with_fragments = hasher.finish();
|
||||
|
||||
assert_eq!(
|
||||
repo_url_basic, repo_url_with_fragments,
|
||||
"repository urls should have the exact cache keys as fragments are removed",
|
||||
);
|
||||
|
||||
let mut hasher = CacheKeyHasher::new();
|
||||
RepositoryUrl::parse(
|
||||
"https://user:foo@example.com/pypa/sample-namespace-packages.git@2.0.0#foo=bar",
|
||||
)?
|
||||
.with_lfs(None)
|
||||
.cache_key(&mut hasher);
|
||||
let git_url_with_fragments = hasher.finish();
|
||||
|
||||
assert_eq!(
|
||||
repo_url_with_fragments, git_url_with_fragments,
|
||||
"both structs should have the exact cache keys as fragments are still removed",
|
||||
);
|
||||
|
||||
let mut hasher = CacheKeyHasher::new();
|
||||
RepositoryUrl::parse(
|
||||
"https://user:foo@example.com/pypa/sample-namespace-packages.git@2.0.0#foo=bar",
|
||||
)?
|
||||
.with_lfs(Some(false))
|
||||
.cache_key(&mut hasher);
|
||||
let git_url_with_fragments_and_lfs_false = hasher.finish();
|
||||
|
||||
assert_eq!(
|
||||
git_url_with_fragments, git_url_with_fragments_and_lfs_false,
|
||||
"both structs should have the exact cache keys as lfs false should not influence them",
|
||||
);
|
||||
|
||||
let mut hasher = CacheKeyHasher::new();
|
||||
RepositoryUrl::parse(
|
||||
"https://user:foo@example.com/pypa/sample-namespace-packages.git@2.0.0#foo=bar",
|
||||
)?
|
||||
.with_lfs(Some(true))
|
||||
.cache_key(&mut hasher);
|
||||
let git_url_with_fragments_and_lfs_true = hasher.finish();
|
||||
|
||||
assert_ne!(
|
||||
git_url_with_fragments, git_url_with_fragments_and_lfs_true,
|
||||
"both structs should have different cache keys as one has Git LFS enabled",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
[package]
|
||||
name = "uv-cache"
|
||||
version = "0.0.1"
|
||||
description = "Generate stable hash digests across versions and platforms."
|
||||
version = "0.0.8"
|
||||
description = "This is an internal component crate of uv"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
|
@ -35,5 +34,6 @@ rustc-hash = { workspace = true }
|
|||
same-file = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tempfile = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- This file is generated. DO NOT EDIT -->
|
||||
|
||||
# uv-cache
|
||||
|
||||
This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here
|
||||
is unstable and will have frequent breaking changes.
|
||||
|
||||
This version (0.0.8) is a component of [uv 0.9.18](https://crates.io/crates/uv/0.9.18). The source
|
||||
can be found [here](https://github.com/astral-sh/uv/blob/0.9.18/crates/uv-cache).
|
||||
|
||||
See uv's
|
||||
[crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning)
|
||||
for details on versioning.
|
||||
|
|
@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
|
|||
use uv_static::EnvVars;
|
||||
|
||||
use crate::Cache;
|
||||
use clap::Parser;
|
||||
use clap::{Parser, ValueHint};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
|
|
@ -27,7 +27,7 @@ pub struct CacheArgs {
|
|||
/// `%LOCALAPPDATA%\uv\cache` on Windows.
|
||||
///
|
||||
/// To view the location of the cache directory, run `uv cache dir`.
|
||||
#[arg(global = true, long, env = EnvVars::UV_CACHE_DIR)]
|
||||
#[arg(global = true, long, env = EnvVars::UV_CACHE_DIR, value_hint = ValueHint::DirPath)]
|
||||
pub cache_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use rustc_hash::FxHashMap;
|
|||
use tracing::{debug, trace, warn};
|
||||
|
||||
use uv_cache_info::Timestamp;
|
||||
use uv_fs::{LockedFile, Simplified, cachedir, directories};
|
||||
use uv_fs::{LockedFile, LockedFileError, LockedFileMode, Simplified, cachedir, directories};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pypi_types::ResolutionMetadata;
|
||||
|
||||
|
|
@ -35,6 +35,17 @@ mod wheel;
|
|||
/// Must be kept in-sync with the version in [`CacheBucket::to_str`].
|
||||
pub const ARCHIVE_VERSION: u8 = 0;
|
||||
|
||||
/// Error locking a cache entry or shard
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
#[error("Could not make the path absolute")]
|
||||
Absolute(#[source] io::Error),
|
||||
#[error("Could not acquire lock")]
|
||||
Acquire(#[from] LockedFileError),
|
||||
}
|
||||
|
||||
/// A [`CacheEntry`] which may or may not exist yet.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CacheEntry(PathBuf);
|
||||
|
|
@ -80,9 +91,14 @@ impl CacheEntry {
|
|||
}
|
||||
|
||||
/// Acquire the [`CacheEntry`] as an exclusive lock.
|
||||
pub async fn lock(&self) -> Result<LockedFile, io::Error> {
|
||||
pub async fn lock(&self) -> Result<LockedFile, Error> {
|
||||
fs_err::create_dir_all(self.dir())?;
|
||||
LockedFile::acquire(self.path(), self.path().display()).await
|
||||
Ok(LockedFile::acquire(
|
||||
self.path(),
|
||||
LockedFileMode::Exclusive,
|
||||
self.path().display(),
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,9 +125,14 @@ impl CacheShard {
|
|||
}
|
||||
|
||||
/// Acquire the cache entry as an exclusive lock.
|
||||
pub async fn lock(&self) -> Result<LockedFile, io::Error> {
|
||||
pub async fn lock(&self) -> Result<LockedFile, Error> {
|
||||
fs_err::create_dir_all(self.as_ref())?;
|
||||
LockedFile::acquire(self.join(".lock"), self.display()).await
|
||||
Ok(LockedFile::acquire(
|
||||
self.join(".lock"),
|
||||
LockedFileMode::Exclusive,
|
||||
self.display(),
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Return the [`CacheShard`] as a [`PathBuf`].
|
||||
|
|
@ -182,7 +203,7 @@ impl Cache {
|
|||
}
|
||||
|
||||
/// Acquire a lock that allows removing entries from the cache.
|
||||
pub fn with_exclusive_lock(self) -> Result<Self, io::Error> {
|
||||
pub async fn with_exclusive_lock(self) -> Result<Self, LockedFileError> {
|
||||
let Self {
|
||||
root,
|
||||
refresh,
|
||||
|
|
@ -198,8 +219,12 @@ impl Cache {
|
|||
),
|
||||
);
|
||||
}
|
||||
let lock_file =
|
||||
LockedFile::acquire_blocking(root.join(".lock"), root.simplified_display())?;
|
||||
let lock_file = LockedFile::acquire(
|
||||
root.join(".lock"),
|
||||
LockedFileMode::Exclusive,
|
||||
root.simplified_display(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Self {
|
||||
root,
|
||||
|
|
@ -220,7 +245,11 @@ impl Cache {
|
|||
lock_file,
|
||||
} = self;
|
||||
|
||||
match LockedFile::acquire_no_wait(root.join(".lock"), root.simplified_display()) {
|
||||
match LockedFile::acquire_no_wait(
|
||||
root.join(".lock"),
|
||||
LockedFileMode::Exclusive,
|
||||
root.simplified_display(),
|
||||
) {
|
||||
Some(lock_file) => Ok(Self {
|
||||
root,
|
||||
refresh,
|
||||
|
|
@ -372,10 +401,8 @@ impl Cache {
|
|||
self.temp_dir.is_some()
|
||||
}
|
||||
|
||||
/// Initialize the [`Cache`].
|
||||
pub fn init(self) -> Result<Self, io::Error> {
|
||||
let root = &self.root;
|
||||
|
||||
/// Populate the cache scaffold.
|
||||
fn create_base_files(root: &PathBuf) -> io::Result<()> {
|
||||
// Create the cache directory, if it doesn't exist.
|
||||
fs_err::create_dir_all(root)?;
|
||||
|
||||
|
|
@ -421,29 +448,66 @@ impl Cache {
|
|||
.join(".git"),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize the [`Cache`].
|
||||
pub async fn init(self) -> Result<Self, Error> {
|
||||
let root = &self.root;
|
||||
|
||||
Self::create_base_files(root)?;
|
||||
|
||||
// Block cache removal operations from interfering.
|
||||
let lock_file = match LockedFile::acquire_shared_blocking(
|
||||
let lock_file = match LockedFile::acquire(
|
||||
root.join(".lock"),
|
||||
LockedFileMode::Shared,
|
||||
root.simplified_display(),
|
||||
) {
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(lock_file) => Some(Arc::new(lock_file)),
|
||||
Err(err) if err.kind() == io::ErrorKind::Unsupported => {
|
||||
Err(err)
|
||||
if err
|
||||
.as_io_error()
|
||||
.is_some_and(|err| err.kind() == io::ErrorKind::Unsupported) =>
|
||||
{
|
||||
warn!(
|
||||
"Shared locking is not supported by the current platform or filesystem, \
|
||||
reduced parallel process safety with `uv cache clean` and `uv cache prune`."
|
||||
reduced parallel process safety with `uv cache clean` and `uv cache prune`."
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
root: std::path::absolute(root)?,
|
||||
root: std::path::absolute(root).map_err(Error::Absolute)?,
|
||||
lock_file,
|
||||
..self
|
||||
})
|
||||
}
|
||||
|
||||
/// Initialize the [`Cache`], assuming that there are no other uv processes running.
|
||||
pub fn init_no_wait(self) -> Result<Option<Self>, Error> {
|
||||
let root = &self.root;
|
||||
|
||||
Self::create_base_files(root)?;
|
||||
|
||||
// Block cache removal operations from interfering.
|
||||
let Some(lock_file) = LockedFile::acquire_no_wait(
|
||||
root.join(".lock"),
|
||||
LockedFileMode::Shared,
|
||||
root.simplified_display(),
|
||||
) else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(Self {
|
||||
root: std::path::absolute(root).map_err(Error::Absolute)?,
|
||||
lock_file: Some(Arc::new(lock_file)),
|
||||
..self
|
||||
}))
|
||||
}
|
||||
|
||||
/// Clear the cache, removing all entries.
|
||||
pub fn clear(self, reporter: Box<dyn CleanReporter>) -> Result<Removal, io::Error> {
|
||||
// Remove everything but `.lock`, Windows does not allow removal of a locked file
|
||||
|
|
@ -478,7 +542,7 @@ impl Cache {
|
|||
/// Remove a package from the cache.
|
||||
///
|
||||
/// Returns the number of entries removed from the cache.
|
||||
pub fn remove(&self, name: &PackageName) -> Result<Removal, io::Error> {
|
||||
pub fn remove(&self, name: &PackageName) -> io::Result<Removal> {
|
||||
// Collect the set of referenced archives.
|
||||
let references = self.find_archive_references()?;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ pub enum WheelCache<'a> {
|
|||
Path(&'a DisplaySafeUrl),
|
||||
/// An editable dependency, which we key by URL.
|
||||
Editable(&'a DisplaySafeUrl),
|
||||
/// A Git dependency, which we key by URL and SHA.
|
||||
/// A Git dependency, which we key by URL (including LFS state), SHA.
|
||||
///
|
||||
/// Note that this variant only exists for source distributions; wheels can't be delivered
|
||||
/// through Git.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
[package]
|
||||
name = "uv-cli"
|
||||
version = "0.0.1"
|
||||
description = "The command line interface for the uv binary."
|
||||
version = "0.0.8"
|
||||
description = "This is an internal component crate of uv"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- This file is generated. DO NOT EDIT -->
|
||||
|
||||
# uv-cli
|
||||
|
||||
This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here
|
||||
is unstable and will have frequent breaking changes.
|
||||
|
||||
This version (0.0.8) is a component of [uv 0.9.18](https://crates.io/crates/uv/0.9.18). The source
|
||||
can be found [here](https://github.com/astral-sh/uv/blob/0.9.18/crates/uv-cli).
|
||||
|
||||
See uv's
|
||||
[crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning)
|
||||
for details on versioning.
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -366,6 +366,7 @@ pub fn resolver_options(
|
|||
exclude_newer_package.unwrap_or_default(),
|
||||
),
|
||||
link_mode,
|
||||
torch_backend: None,
|
||||
no_build: flag(no_build, build, "build"),
|
||||
no_build_package: Some(no_build_package),
|
||||
no_binary: flag(no_binary, binary, "binary"),
|
||||
|
|
@ -495,5 +496,6 @@ pub fn resolver_installer_options(
|
|||
Some(no_binary_package)
|
||||
},
|
||||
no_sources: if no_sources { Some(true) } else { None },
|
||||
torch_backend: None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
[package]
|
||||
name = "uv-client"
|
||||
version = "0.0.1"
|
||||
version = "0.0.8"
|
||||
description = "This is an internal component crate of uv"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
|
@ -72,6 +72,9 @@ http-body-util = { workspace = true }
|
|||
hyper = { workspace = true }
|
||||
hyper-util = { workspace = true }
|
||||
insta = { workspace = true }
|
||||
rcgen = { workspace = true }
|
||||
rustls = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-rustls = { workspace = true }
|
||||
wiremock = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
# `pypi-client`
|
||||
<!-- This file is generated. DO NOT EDIT -->
|
||||
|
||||
A general-use client for interacting with PyPI.
|
||||
# uv-client
|
||||
|
||||
Loosely modeled after Orogene's `oro-client`.
|
||||
This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here
|
||||
is unstable and will have frequent breaking changes.
|
||||
|
||||
This version (0.0.8) is a component of [uv 0.9.18](https://crates.io/crates/uv/0.9.18). The source
|
||||
can be found [here](https://github.com/astral-sh/uv/blob/0.9.18/crates/uv-client).
|
||||
|
||||
See uv's
|
||||
[crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning)
|
||||
for details on versioning.
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ use tracing::{debug, trace};
|
|||
use url::ParseError;
|
||||
use url::Url;
|
||||
|
||||
use uv_auth::{AuthMiddleware, Credentials, Indexes, PyxTokenStore};
|
||||
use uv_auth::{AuthMiddleware, Credentials, CredentialsCache, Indexes, PyxTokenStore};
|
||||
use uv_configuration::{KeyringProviderType, TrustedHost};
|
||||
use uv_fs::Simplified;
|
||||
use uv_pep508::MarkerEnvironment;
|
||||
|
|
@ -50,7 +50,7 @@ pub const DEFAULT_RETRIES: u32 = 3;
|
|||
/// Maximum number of redirects to follow before giving up.
|
||||
///
|
||||
/// This is the default used by [`reqwest`].
|
||||
const DEFAULT_MAX_REDIRECTS: u32 = 10;
|
||||
pub const DEFAULT_MAX_REDIRECTS: u32 = 10;
|
||||
|
||||
/// Selectively skip parts or the entire auth middleware.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
|
|
@ -78,6 +78,8 @@ pub struct BaseClientBuilder<'a> {
|
|||
markers: Option<&'a MarkerEnvironment>,
|
||||
platform: Option<&'a Platform>,
|
||||
auth_integration: AuthIntegration,
|
||||
/// Global authentication cache for a uv invocation to share credentials across uv clients.
|
||||
credentials_cache: Arc<CredentialsCache>,
|
||||
indexes: Indexes,
|
||||
timeout: Duration,
|
||||
extra_middleware: Option<ExtraMiddleware>,
|
||||
|
|
@ -89,6 +91,8 @@ pub struct BaseClientBuilder<'a> {
|
|||
cross_origin_credential_policy: CrossOriginCredentialsPolicy,
|
||||
/// Optional custom reqwest client to use instead of creating a new one.
|
||||
custom_client: Option<Client>,
|
||||
/// uv subcommand in which this client is being used
|
||||
subcommand: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// The policy for handling HTTP redirects.
|
||||
|
|
@ -100,6 +104,8 @@ pub enum RedirectPolicy {
|
|||
BypassMiddleware,
|
||||
/// Handle redirects manually, re-triggering our custom middleware for each request.
|
||||
RetriggerMiddleware,
|
||||
/// No redirect for non-cloneable (e.g., streaming) requests with custom redirect logic.
|
||||
NoRedirect,
|
||||
}
|
||||
|
||||
impl RedirectPolicy {
|
||||
|
|
@ -107,6 +113,7 @@ impl RedirectPolicy {
|
|||
match self {
|
||||
Self::BypassMiddleware => reqwest::redirect::Policy::default(),
|
||||
Self::RetriggerMiddleware => reqwest::redirect::Policy::none(),
|
||||
Self::NoRedirect => reqwest::redirect::Policy::none(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -136,6 +143,7 @@ impl Default for BaseClientBuilder<'_> {
|
|||
markers: None,
|
||||
platform: None,
|
||||
auth_integration: AuthIntegration::default(),
|
||||
credentials_cache: Arc::new(CredentialsCache::default()),
|
||||
indexes: Indexes::new(),
|
||||
timeout: Duration::from_secs(30),
|
||||
extra_middleware: None,
|
||||
|
|
@ -143,11 +151,12 @@ impl Default for BaseClientBuilder<'_> {
|
|||
redirect_policy: RedirectPolicy::default(),
|
||||
cross_origin_credential_policy: CrossOriginCredentialsPolicy::Secure,
|
||||
custom_client: None,
|
||||
subcommand: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BaseClientBuilder<'_> {
|
||||
impl<'a> BaseClientBuilder<'a> {
|
||||
pub fn new(
|
||||
connectivity: Connectivity,
|
||||
native_tls: bool,
|
||||
|
|
@ -166,9 +175,7 @@ impl BaseClientBuilder<'_> {
|
|||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> BaseClientBuilder<'a> {
|
||||
/// Use a custom reqwest client instead of creating a new one.
|
||||
///
|
||||
/// This allows you to provide your own reqwest client with custom configuration.
|
||||
|
|
@ -276,6 +283,26 @@ impl<'a> BaseClientBuilder<'a> {
|
|||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn subcommand(mut self, subcommand: Vec<String>) -> Self {
|
||||
self.subcommand = Some(subcommand);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn credentials_cache(&self) -> &CredentialsCache {
|
||||
&self.credentials_cache
|
||||
}
|
||||
|
||||
/// See [`CredentialsCache::store_credentials_from_url`].
|
||||
pub fn store_credentials_from_url(&self, url: &DisplaySafeUrl) -> bool {
|
||||
self.credentials_cache.store_credentials_from_url(url)
|
||||
}
|
||||
|
||||
/// See [`CredentialsCache::store_credentials`].
|
||||
pub fn store_credentials(&self, url: &DisplaySafeUrl, credentials: Credentials) {
|
||||
self.credentials_cache.store_credentials(url, credentials);
|
||||
}
|
||||
|
||||
pub fn is_native_tls(&self) -> bool {
|
||||
self.native_tls
|
||||
}
|
||||
|
|
@ -324,6 +351,7 @@ impl<'a> BaseClientBuilder<'a> {
|
|||
dangerous_client,
|
||||
raw_dangerous_client,
|
||||
timeout,
|
||||
credentials_cache: self.credentials_cache.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -350,6 +378,7 @@ impl<'a> BaseClientBuilder<'a> {
|
|||
raw_client: existing.raw_client.clone(),
|
||||
raw_dangerous_client: existing.raw_dangerous_client.clone(),
|
||||
timeout: existing.timeout,
|
||||
credentials_cache: existing.credentials_cache.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -358,12 +387,14 @@ impl<'a> BaseClientBuilder<'a> {
|
|||
let mut user_agent_string = format!("uv/{}", version());
|
||||
|
||||
// Add linehaul metadata.
|
||||
let linehaul = LineHaul::new(self.markers, self.platform);
|
||||
let linehaul = LineHaul::new(self.markers, self.platform, self.subcommand.clone());
|
||||
if let Ok(output) = serde_json::to_string(&linehaul) {
|
||||
let _ = write!(user_agent_string, " {output}");
|
||||
}
|
||||
|
||||
// Check for the presence of an `SSL_CERT_FILE`.
|
||||
// Checks for the presence of `SSL_CERT_FILE`.
|
||||
// Certificate loading support is delegated to `rustls-native-certs`.
|
||||
// See https://github.com/rustls/rustls-native-certs/blob/813790a297ad4399efe70a8e5264ca1b420acbec/src/lib.rs#L118-L125
|
||||
let ssl_cert_file_exists = env::var_os(EnvVars::SSL_CERT_FILE).is_some_and(|path| {
|
||||
let path_exists = Path::new(&path).exists();
|
||||
if !path_exists {
|
||||
|
|
@ -375,11 +406,61 @@ impl<'a> BaseClientBuilder<'a> {
|
|||
path_exists
|
||||
});
|
||||
|
||||
// Checks for the presence of `SSL_CERT_DIR`.
|
||||
// Certificate loading support is delegated to `rustls-native-certs`.
|
||||
// See https://github.com/rustls/rustls-native-certs/blob/813790a297ad4399efe70a8e5264ca1b420acbec/src/lib.rs#L118-L125
|
||||
let ssl_cert_dir_exists = env::var_os(EnvVars::SSL_CERT_DIR)
|
||||
.filter(|v| !v.is_empty())
|
||||
.is_some_and(|dirs| {
|
||||
// Parse `SSL_CERT_DIR`, with support for multiple entries using
|
||||
// a platform-specific delimiter (`:` on Unix, `;` on Windows)
|
||||
let (existing, missing): (Vec<_>, Vec<_>) =
|
||||
env::split_paths(&dirs).partition(|p| p.exists());
|
||||
|
||||
if existing.is_empty() {
|
||||
let end_note = if missing.len() == 1 {
|
||||
"The directory does not exist."
|
||||
} else {
|
||||
"The entries do not exist."
|
||||
};
|
||||
warn_user_once!(
|
||||
"Ignoring invalid `SSL_CERT_DIR`. {end_note}: {}.",
|
||||
missing
|
||||
.iter()
|
||||
.map(Simplified::simplified_display)
|
||||
.join(", ")
|
||||
.cyan()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Warn on any missing entries
|
||||
if !missing.is_empty() {
|
||||
let end_note = if missing.len() == 1 {
|
||||
"The following directory does not exist:"
|
||||
} else {
|
||||
"The following entries do not exist:"
|
||||
};
|
||||
warn_user_once!(
|
||||
"Invalid entries in `SSL_CERT_DIR`. {end_note}: {}.",
|
||||
missing
|
||||
.iter()
|
||||
.map(Simplified::simplified_display)
|
||||
.join(", ")
|
||||
.cyan()
|
||||
);
|
||||
}
|
||||
|
||||
// Proceed while ignoring missing entries
|
||||
true
|
||||
});
|
||||
|
||||
// Create a secure client that validates certificates.
|
||||
let raw_client = self.create_client(
|
||||
&user_agent_string,
|
||||
timeout,
|
||||
ssl_cert_file_exists,
|
||||
ssl_cert_dir_exists,
|
||||
Security::Secure,
|
||||
self.redirect_policy,
|
||||
);
|
||||
|
|
@ -389,6 +470,7 @@ impl<'a> BaseClientBuilder<'a> {
|
|||
&user_agent_string,
|
||||
timeout,
|
||||
ssl_cert_file_exists,
|
||||
ssl_cert_dir_exists,
|
||||
Security::Insecure,
|
||||
self.redirect_policy,
|
||||
);
|
||||
|
|
@ -401,6 +483,7 @@ impl<'a> BaseClientBuilder<'a> {
|
|||
user_agent: &str,
|
||||
timeout: Duration,
|
||||
ssl_cert_file_exists: bool,
|
||||
ssl_cert_dir_exists: bool,
|
||||
security: Security,
|
||||
redirect_policy: RedirectPolicy,
|
||||
) -> Client {
|
||||
|
|
@ -419,7 +502,7 @@ impl<'a> BaseClientBuilder<'a> {
|
|||
Security::Insecure => client_builder.danger_accept_invalid_certs(true),
|
||||
};
|
||||
|
||||
let client_builder = if self.native_tls || ssl_cert_file_exists {
|
||||
let client_builder = if self.native_tls || ssl_cert_file_exists || ssl_cert_dir_exists {
|
||||
client_builder.tls_built_in_native_certs(true)
|
||||
} else {
|
||||
client_builder.tls_built_in_webpki_certs(true)
|
||||
|
|
@ -500,6 +583,7 @@ impl<'a> BaseClientBuilder<'a> {
|
|||
match self.auth_integration {
|
||||
AuthIntegration::Default => {
|
||||
let mut auth_middleware = AuthMiddleware::new()
|
||||
.with_cache_arc(self.credentials_cache.clone())
|
||||
.with_base_client(base_client)
|
||||
.with_indexes(self.indexes.clone())
|
||||
.with_keyring(self.keyring.to_provider())
|
||||
|
|
@ -511,6 +595,7 @@ impl<'a> BaseClientBuilder<'a> {
|
|||
}
|
||||
AuthIntegration::OnlyAuthenticated => {
|
||||
let mut auth_middleware = AuthMiddleware::new()
|
||||
.with_cache_arc(self.credentials_cache.clone())
|
||||
.with_base_client(base_client)
|
||||
.with_indexes(self.indexes.clone())
|
||||
.with_keyring(self.keyring.to_provider())
|
||||
|
|
@ -554,6 +639,8 @@ pub struct BaseClient {
|
|||
allow_insecure_host: Vec<TrustedHost>,
|
||||
/// The number of retries to attempt on transient errors.
|
||||
retries: u32,
|
||||
/// Global authentication cache for a uv invocation to share credentials across uv clients.
|
||||
credentials_cache: Arc<CredentialsCache>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
|
|
@ -605,6 +692,10 @@ impl BaseClient {
|
|||
}
|
||||
builder.build_with_max_retries(self.retries)
|
||||
}
|
||||
|
||||
pub fn credentials_cache(&self) -> &CredentialsCache {
|
||||
&self.credentials_cache
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around [`ClientWithMiddleware`] that manages redirects.
|
||||
|
|
@ -641,6 +732,7 @@ impl RedirectClientWithMiddleware {
|
|||
match self.redirect_policy {
|
||||
RedirectPolicy::BypassMiddleware => self.client.execute(req).await,
|
||||
RedirectPolicy::RetriggerMiddleware => self.execute_with_redirect_handling(req).await,
|
||||
RedirectPolicy::NoRedirect => self.client.execute(req).await,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use std::fmt::{Display, Formatter};
|
|||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use uv_cache::Error as CacheError;
|
||||
use uv_distribution_filename::{WheelFilename, WheelFilenameError};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_redacted::DisplaySafeUrl;
|
||||
|
|
@ -337,6 +338,9 @@ pub enum ErrorKind {
|
|||
#[error("Failed to write to the client cache")]
|
||||
CacheWrite(#[source] std::io::Error),
|
||||
|
||||
#[error("Failed to acquire lock on the client cache")]
|
||||
CacheLock(#[source] CacheError),
|
||||
|
||||
#[error(transparent)]
|
||||
Io(std::io::Error),
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
pub use base_client::{
|
||||
AuthIntegration, BaseClient, BaseClientBuilder, DEFAULT_RETRIES, ExtraMiddleware,
|
||||
RedirectClientWithMiddleware, RequestBuilder, RetryParsingError, UvRetryableStrategy,
|
||||
is_transient_network_error,
|
||||
AuthIntegration, BaseClient, BaseClientBuilder, DEFAULT_MAX_REDIRECTS, DEFAULT_RETRIES,
|
||||
ExtraMiddleware, RedirectClientWithMiddleware, RedirectPolicy, RequestBuilder,
|
||||
RetryParsingError, UvRetryableStrategy, is_transient_network_error,
|
||||
};
|
||||
pub use cached_client::{CacheControl, CachedClient, CachedClientError, DataWithCachePolicy};
|
||||
pub use error::{Error, ErrorKind, WrappedReqwestError};
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use uv_version::version;
|
|||
pub struct Installer {
|
||||
pub name: Option<String>,
|
||||
pub version: Option<String>,
|
||||
pub subcommand: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
|
|
@ -63,7 +64,11 @@ pub struct LineHaul {
|
|||
impl LineHaul {
|
||||
/// Initializes Linehaul information based on PEP 508 markers.
|
||||
#[instrument(name = "linehaul", skip_all)]
|
||||
pub fn new(markers: Option<&MarkerEnvironment>, platform: Option<&Platform>) -> Self {
|
||||
pub fn new(
|
||||
markers: Option<&MarkerEnvironment>,
|
||||
platform: Option<&Platform>,
|
||||
subcommand: Option<Vec<String>>,
|
||||
) -> Self {
|
||||
// https://github.com/pypa/pip/blob/24.0/src/pip/_internal/network/session.py#L87
|
||||
let looks_like_ci = [
|
||||
EnvVars::BUILD_BUILDID,
|
||||
|
|
@ -123,6 +128,7 @@ impl LineHaul {
|
|||
installer: Option::from(Installer {
|
||||
name: Some("uv".to_string()),
|
||||
version: Some(version().to_string()),
|
||||
subcommand,
|
||||
}),
|
||||
python: markers.map(|markers| markers.python_full_version().version.to_string()),
|
||||
implementation: Option::from(Implementation {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use tokio::sync::{Mutex, Semaphore};
|
|||
use tracing::{Instrument, debug, info_span, instrument, trace, warn};
|
||||
use url::Url;
|
||||
|
||||
use uv_auth::{Indexes, PyxTokenStore};
|
||||
use uv_auth::{CredentialsCache, Indexes, PyxTokenStore};
|
||||
use uv_cache::{Cache, CacheBucket, CacheEntry, WheelCache};
|
||||
use uv_configuration::IndexStrategy;
|
||||
use uv_configuration::KeyringProviderType;
|
||||
|
|
@ -148,8 +148,30 @@ impl<'a> RegistryClientBuilder<'a> {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> RegistryClient {
|
||||
self.index_locations.cache_index_credentials();
|
||||
/// Add all authenticated sources to the cache.
|
||||
pub fn cache_index_credentials(&mut self) {
|
||||
for index in self.index_locations.known_indexes() {
|
||||
if let Some(credentials) = index.credentials() {
|
||||
trace!(
|
||||
"Read credentials for index {}",
|
||||
index
|
||||
.name
|
||||
.as_ref()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| index.url.to_string())
|
||||
);
|
||||
if let Some(root_url) = index.root_url() {
|
||||
self.base_client_builder
|
||||
.store_credentials(&root_url, credentials.clone());
|
||||
}
|
||||
self.base_client_builder
|
||||
.store_credentials(index.raw_url(), credentials);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build(mut self) -> RegistryClient {
|
||||
self.cache_index_credentials();
|
||||
let index_urls = self.index_locations.index_urls();
|
||||
|
||||
// Build a base client
|
||||
|
|
@ -180,8 +202,8 @@ impl<'a> RegistryClientBuilder<'a> {
|
|||
}
|
||||
|
||||
/// Share the underlying client between two different middleware configurations.
|
||||
pub fn wrap_existing(self, existing: &BaseClient) -> RegistryClient {
|
||||
self.index_locations.cache_index_credentials();
|
||||
pub fn wrap_existing(mut self, existing: &BaseClient) -> RegistryClient {
|
||||
self.cache_index_credentials();
|
||||
let index_urls = self.index_locations.index_urls();
|
||||
|
||||
// Wrap in any relevant middleware and handle connectivity.
|
||||
|
|
@ -269,6 +291,10 @@ impl RegistryClient {
|
|||
self.timeout
|
||||
}
|
||||
|
||||
pub fn credentials_cache(&self) -> &CredentialsCache {
|
||||
self.client.uncached().credentials_cache()
|
||||
}
|
||||
|
||||
/// Return the appropriate index URLs for the given [`PackageName`].
|
||||
fn index_urls_for(
|
||||
&self,
|
||||
|
|
@ -513,7 +539,7 @@ impl RegistryClient {
|
|||
#[cfg(windows)]
|
||||
let _lock = {
|
||||
let lock_entry = cache_entry.with_file(format!("{package_name}.lock"));
|
||||
lock_entry.lock().await.map_err(ErrorKind::CacheWrite)?
|
||||
lock_entry.lock().await.map_err(ErrorKind::CacheLock)?
|
||||
};
|
||||
|
||||
let result = if matches!(index, IndexUrl::Path(_)) {
|
||||
|
|
@ -1005,7 +1031,7 @@ impl RegistryClient {
|
|||
#[cfg(windows)]
|
||||
let _lock = {
|
||||
let lock_entry = cache_entry.with_file(format!("{}.lock", filename.stem()));
|
||||
lock_entry.lock().await.map_err(ErrorKind::CacheWrite)?
|
||||
lock_entry.lock().await.map_err(ErrorKind::CacheLock)?
|
||||
};
|
||||
|
||||
let response_callback = async |response: Response| {
|
||||
|
|
@ -1089,7 +1115,7 @@ impl RegistryClient {
|
|||
#[cfg(windows)]
|
||||
let _lock = {
|
||||
let lock_entry = cache_entry.with_file(format!("{}.lock", filename.stem()));
|
||||
lock_entry.lock().await.map_err(ErrorKind::CacheWrite)?
|
||||
lock_entry.lock().await.map_err(ErrorKind::CacheLock)?
|
||||
};
|
||||
|
||||
// Attempt to fetch via a range request.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,382 @@
|
|||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use futures::future;
|
||||
use http_body_util::combinators::BoxBody;
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::body::{Bytes, Incoming};
|
||||
use hyper::header::USER_AGENT;
|
||||
use hyper::service::service_fn;
|
||||
use hyper::{Request, Response};
|
||||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||
use hyper_util::server::conn::auto::Builder;
|
||||
use rcgen::{
|
||||
BasicConstraints, Certificate, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa,
|
||||
Issuer, KeyPair, KeyUsagePurpose, SanType, date_time_ymd,
|
||||
};
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
|
||||
use rustls::server::WebPkiClientVerifier;
|
||||
use rustls::{RootCertStore, ServerConfig};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
|
||||
use uv_fs::Simplified;
|
||||
|
||||
/// An issued certificate, together with the subject keypair.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SelfSigned {
|
||||
/// An issued certificate.
|
||||
pub public: Certificate,
|
||||
/// The certificate's subject signing key.
|
||||
pub private: KeyPair,
|
||||
}
|
||||
|
||||
/// Defines the base location for temporary generated certs.
|
||||
///
|
||||
/// See [`TestContext::test_bucket_dir`] for implementation rationale.
|
||||
pub(crate) fn test_cert_dir() -> PathBuf {
|
||||
std::env::temp_dir()
|
||||
.simple_canonicalize()
|
||||
.expect("failed to canonicalize temp dir")
|
||||
.join("uv")
|
||||
.join("tests")
|
||||
.join("certs")
|
||||
}
|
||||
|
||||
/// Generates a self-signed server certificate for `uv-test-server`, `localhost` and `127.0.0.1`.
|
||||
/// This certificate is standalone and not issued by a self-signed Root CA.
|
||||
///
|
||||
/// Use sparingly as generation of certs is a slow operation.
|
||||
pub(crate) fn generate_self_signed_certs() -> Result<SelfSigned> {
|
||||
let mut params = CertificateParams::default();
|
||||
params.is_ca = IsCa::NoCa;
|
||||
params.not_before = date_time_ymd(1975, 1, 1);
|
||||
params.not_after = date_time_ymd(4096, 1, 1);
|
||||
params.key_usages.push(KeyUsagePurpose::DigitalSignature);
|
||||
params.key_usages.push(KeyUsagePurpose::KeyEncipherment);
|
||||
params
|
||||
.extended_key_usages
|
||||
.push(ExtendedKeyUsagePurpose::ServerAuth);
|
||||
params
|
||||
.distinguished_name
|
||||
.push(DnType::OrganizationName, "Astral Software Inc.");
|
||||
params
|
||||
.distinguished_name
|
||||
.push(DnType::CommonName, "uv-test-server");
|
||||
params
|
||||
.subject_alt_names
|
||||
.push(SanType::DnsName("uv-test-server".try_into()?));
|
||||
params
|
||||
.subject_alt_names
|
||||
.push(SanType::DnsName("localhost".try_into()?));
|
||||
params
|
||||
.subject_alt_names
|
||||
.push(SanType::IpAddress("127.0.0.1".parse()?));
|
||||
let private = KeyPair::generate()?;
|
||||
let public = params.self_signed(&private)?;
|
||||
|
||||
Ok(SelfSigned { public, private })
|
||||
}
|
||||
|
||||
/// Generates a self-signed root CA, server certificate, and client certificate.
|
||||
/// There are no intermediate certs generated as part of this function.
|
||||
/// The server certificate is for `uv-test-server`, `localhost` and `127.0.0.1` issued by this CA.
|
||||
/// The client certificate is for `uv-test-client` issued by this CA.
|
||||
///
|
||||
/// Use sparingly as generation of these certs is a very slow operation.
|
||||
pub(crate) fn generate_self_signed_certs_with_ca() -> Result<(SelfSigned, SelfSigned, SelfSigned)> {
|
||||
// Generate the CA
|
||||
let mut ca_params = CertificateParams::default();
|
||||
ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); // root cert
|
||||
ca_params.not_before = date_time_ymd(1975, 1, 1);
|
||||
ca_params.not_after = date_time_ymd(4096, 1, 1);
|
||||
ca_params.key_usages.push(KeyUsagePurpose::DigitalSignature);
|
||||
ca_params.key_usages.push(KeyUsagePurpose::KeyCertSign);
|
||||
ca_params.key_usages.push(KeyUsagePurpose::CrlSign);
|
||||
ca_params
|
||||
.distinguished_name
|
||||
.push(DnType::OrganizationName, "Astral Software Inc.");
|
||||
ca_params
|
||||
.distinguished_name
|
||||
.push(DnType::CommonName, "uv-test-ca");
|
||||
ca_params
|
||||
.subject_alt_names
|
||||
.push(SanType::DnsName("uv-test-ca".try_into()?));
|
||||
let ca_private_key = KeyPair::generate()?;
|
||||
let ca_public_cert = ca_params.self_signed(&ca_private_key)?;
|
||||
let ca_cert_issuer = Issuer::new(ca_params, &ca_private_key);
|
||||
|
||||
// Generate server cert issued by this CA
|
||||
let mut server_params = CertificateParams::default();
|
||||
server_params.is_ca = IsCa::NoCa;
|
||||
server_params.not_before = date_time_ymd(1975, 1, 1);
|
||||
server_params.not_after = date_time_ymd(4096, 1, 1);
|
||||
server_params.use_authority_key_identifier_extension = true;
|
||||
server_params
|
||||
.key_usages
|
||||
.push(KeyUsagePurpose::DigitalSignature);
|
||||
server_params
|
||||
.key_usages
|
||||
.push(KeyUsagePurpose::KeyEncipherment);
|
||||
server_params
|
||||
.extended_key_usages
|
||||
.push(ExtendedKeyUsagePurpose::ServerAuth);
|
||||
server_params
|
||||
.distinguished_name
|
||||
.push(DnType::OrganizationName, "Astral Software Inc.");
|
||||
server_params
|
||||
.distinguished_name
|
||||
.push(DnType::CommonName, "uv-test-server");
|
||||
server_params
|
||||
.subject_alt_names
|
||||
.push(SanType::DnsName("uv-test-server".try_into()?));
|
||||
server_params
|
||||
.subject_alt_names
|
||||
.push(SanType::DnsName("localhost".try_into()?));
|
||||
server_params
|
||||
.subject_alt_names
|
||||
.push(SanType::IpAddress("127.0.0.1".parse()?));
|
||||
let server_private_key = KeyPair::generate()?;
|
||||
let server_public_cert = server_params.signed_by(&server_private_key, &ca_cert_issuer)?;
|
||||
|
||||
// Generate client cert issued by this CA
|
||||
let mut client_params = CertificateParams::default();
|
||||
client_params.is_ca = IsCa::NoCa;
|
||||
client_params.not_before = date_time_ymd(1975, 1, 1);
|
||||
client_params.not_after = date_time_ymd(4096, 1, 1);
|
||||
client_params.use_authority_key_identifier_extension = true;
|
||||
client_params
|
||||
.key_usages
|
||||
.push(KeyUsagePurpose::DigitalSignature);
|
||||
client_params
|
||||
.extended_key_usages
|
||||
.push(ExtendedKeyUsagePurpose::ClientAuth);
|
||||
client_params
|
||||
.distinguished_name
|
||||
.push(DnType::OrganizationName, "Astral Software Inc.");
|
||||
client_params
|
||||
.distinguished_name
|
||||
.push(DnType::CommonName, "uv-test-client");
|
||||
client_params
|
||||
.subject_alt_names
|
||||
.push(SanType::DnsName("uv-test-client".try_into()?));
|
||||
let client_private_key = KeyPair::generate()?;
|
||||
let client_public_cert = client_params.signed_by(&client_private_key, &ca_cert_issuer)?;
|
||||
|
||||
let ca_self_signed = SelfSigned {
|
||||
public: ca_public_cert,
|
||||
private: ca_private_key,
|
||||
};
|
||||
let server_self_signed = SelfSigned {
|
||||
public: server_public_cert,
|
||||
private: server_private_key,
|
||||
};
|
||||
let client_self_signed = SelfSigned {
|
||||
public: client_public_cert,
|
||||
private: client_private_key,
|
||||
};
|
||||
|
||||
Ok((ca_self_signed, server_self_signed, client_self_signed))
|
||||
}
|
||||
|
||||
// Plain is fine for now; Arc/Box could be used later if we need to support move.
|
||||
type ServerSvcFn =
|
||||
fn(
|
||||
Request<Incoming>,
|
||||
) -> future::Ready<Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct TestServerBuilder<'a> {
|
||||
// Custom server response function
|
||||
svc_fn: Option<ServerSvcFn>,
|
||||
// CA certificate
|
||||
ca_cert: Option<&'a SelfSigned>,
|
||||
// Server certificate
|
||||
server_cert: Option<&'a SelfSigned>,
|
||||
// Enable mTLS Verification
|
||||
mutual_tls: bool,
|
||||
}
|
||||
|
||||
impl<'a> TestServerBuilder<'a> {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
svc_fn: None,
|
||||
server_cert: None,
|
||||
ca_cert: None,
|
||||
mutual_tls: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(unused)]
|
||||
/// Provide a custom server response function.
|
||||
pub(crate) fn with_svc_fn(mut self, svc_fn: ServerSvcFn) -> Self {
|
||||
self.svc_fn = Some(svc_fn);
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide the server certificate. This will enable TLS (HTTPS).
|
||||
pub(crate) fn with_server_cert(mut self, server_cert: &'a SelfSigned) -> Self {
|
||||
self.server_cert = Some(server_cert);
|
||||
self
|
||||
}
|
||||
|
||||
/// CA certificate used to build the `RootCertStore` for client verification.
|
||||
/// Requires `with_server_cert`.
|
||||
pub(crate) fn with_ca_cert(mut self, ca_cert: &'a SelfSigned) -> Self {
|
||||
self.ca_cert = Some(ca_cert);
|
||||
self
|
||||
}
|
||||
|
||||
/// Enforce mutual TLS (client cert auth).
|
||||
/// Requires `with_server_cert` and `with_ca_cert`.
|
||||
pub(crate) fn with_mutual_tls(mut self, mutual: bool) -> Self {
|
||||
self.mutual_tls = mutual;
|
||||
self
|
||||
}
|
||||
|
||||
/// Starts the HTTP(S) server with optional mTLS enforcement.
|
||||
pub(crate) async fn start(self) -> Result<(JoinHandle<Result<()>>, SocketAddr)> {
|
||||
// Validate builder input combinations
|
||||
if self.ca_cert.is_some() && self.server_cert.is_none() {
|
||||
anyhow::bail!("server certificate is required when CA certificate is provided");
|
||||
}
|
||||
if self.mutual_tls && (self.ca_cert.is_none() || self.server_cert.is_none()) {
|
||||
anyhow::bail!("ca certificate is required for mTLS");
|
||||
}
|
||||
|
||||
// Set up the TCP listener on a random available port
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
let addr = listener.local_addr()?;
|
||||
|
||||
// Setup TLS Config (if any)
|
||||
let tls_acceptor = if let Some(server_cert) = self.server_cert {
|
||||
// Prepare Server Cert and KeyPair
|
||||
let server_key = PrivateKeyDer::try_from(server_cert.private.serialize_der()).unwrap();
|
||||
let server_cert = vec![CertificateDer::from(server_cert.public.der().to_vec())];
|
||||
|
||||
// Setup CA Verifier
|
||||
let client_verifier = if let Some(ca_cert) = self.ca_cert {
|
||||
let mut root_store = RootCertStore::empty();
|
||||
root_store
|
||||
.add(CertificateDer::from(ca_cert.public.der().to_vec()))
|
||||
.expect("failed to add CA cert");
|
||||
if self.mutual_tls {
|
||||
// Setup mTLS CA config
|
||||
WebPkiClientVerifier::builder(root_store.into())
|
||||
.build()
|
||||
.expect("failed to setup client verifier")
|
||||
} else {
|
||||
// Only load the CA roots
|
||||
WebPkiClientVerifier::builder(root_store.into())
|
||||
.allow_unauthenticated()
|
||||
.build()
|
||||
.expect("failed to setup client verifier")
|
||||
}
|
||||
} else {
|
||||
WebPkiClientVerifier::no_client_auth()
|
||||
};
|
||||
|
||||
let mut tls_config = ServerConfig::builder()
|
||||
.with_client_cert_verifier(client_verifier)
|
||||
.with_single_cert(server_cert, server_key)?;
|
||||
tls_config.alpn_protocols = vec![b"http/1.1".to_vec(), b"http/1.0".to_vec()];
|
||||
|
||||
Some(TlsAcceptor::from(Arc::new(tls_config)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Setup Response Handler
|
||||
let svc_fn = if let Some(custom_svc_fn) = self.svc_fn {
|
||||
custom_svc_fn
|
||||
} else {
|
||||
|req: Request<Incoming>| {
|
||||
// Get User Agent Header and send it back in the response
|
||||
let user_agent = req
|
||||
.headers()
|
||||
.get(USER_AGENT)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_default(); // Empty Default
|
||||
let response_content = Full::new(Bytes::from(user_agent))
|
||||
.map_err(|_| unreachable!())
|
||||
.boxed();
|
||||
// If we ever want a true echo server, we can use instead
|
||||
// let response_content = req.into_body().boxed();
|
||||
// although uv-client doesn't expose post currently.
|
||||
future::ok::<_, hyper::Error>(Response::new(response_content))
|
||||
}
|
||||
};
|
||||
|
||||
// Spawn the server loop in a background task
|
||||
let server_task = tokio::spawn(async move {
|
||||
let svc = service_fn(move |req: Request<Incoming>| svc_fn(req));
|
||||
|
||||
let (tcp_stream, _remote_addr) = listener
|
||||
.accept()
|
||||
.await
|
||||
.context("Failed to accept TCP connection")?;
|
||||
|
||||
// Start Server (not wrapped in loop {} since we want a single response server)
|
||||
// If we want server to accept multiple connections, we can wrap it in loop {}
|
||||
// but we'll need to ensure to handle termination signals in the tests otherwise
|
||||
// it may never stop.
|
||||
if let Some(tls_acceptor) = tls_acceptor {
|
||||
let tls_stream = tls_acceptor
|
||||
.accept(tcp_stream)
|
||||
.await
|
||||
.context("Failed to accept TLS connection")?;
|
||||
let socket = TokioIo::new(tls_stream);
|
||||
tokio::task::spawn(async move {
|
||||
Builder::new(TokioExecutor::new())
|
||||
.serve_connection(socket, svc)
|
||||
.await
|
||||
.expect("HTTPS Server Started");
|
||||
});
|
||||
} else {
|
||||
let socket = TokioIo::new(tcp_stream);
|
||||
tokio::task::spawn(async move {
|
||||
Builder::new(TokioExecutor::new())
|
||||
.serve_connection(socket, svc)
|
||||
.await
|
||||
.expect("HTTP Server Started");
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
Ok((server_task, addr))
|
||||
}
|
||||
}
|
||||
|
||||
/// Single Request HTTP server that echoes the User Agent Header.
|
||||
pub(crate) async fn start_http_user_agent_server() -> Result<(JoinHandle<Result<()>>, SocketAddr)> {
|
||||
TestServerBuilder::new().start().await
|
||||
}
|
||||
|
||||
/// Single Request HTTPS server that echoes the User Agent Header.
|
||||
pub(crate) async fn start_https_user_agent_server(
|
||||
server_cert: &SelfSigned,
|
||||
) -> Result<(JoinHandle<Result<()>>, SocketAddr)> {
|
||||
TestServerBuilder::new()
|
||||
.with_server_cert(server_cert)
|
||||
.start()
|
||||
.await
|
||||
}
|
||||
|
||||
/// Single Request HTTPS mTLS server that echoes the User Agent Header.
|
||||
pub(crate) async fn start_https_mtls_user_agent_server(
|
||||
ca_cert: &SelfSigned,
|
||||
server_cert: &SelfSigned,
|
||||
) -> Result<(JoinHandle<Result<()>>, SocketAddr)> {
|
||||
TestServerBuilder::new()
|
||||
.with_ca_cert(ca_cert)
|
||||
.with_server_cert(server_cert)
|
||||
.with_mutual_tls(true)
|
||||
.start()
|
||||
.await
|
||||
}
|
||||
|
|
@ -1,2 +1,4 @@
|
|||
mod http_util;
|
||||
mod remote_metadata;
|
||||
mod ssl_certs;
|
||||
mod user_agent_version;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use uv_redacted::DisplaySafeUrl;
|
|||
|
||||
#[tokio::test]
|
||||
async fn remote_metadata_with_and_without_cache() -> Result<()> {
|
||||
let cache = Cache::temp()?.init()?;
|
||||
let cache = Cache::temp()?.init().await?;
|
||||
let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build();
|
||||
|
||||
// The first run is without cache (the tempdir is empty), the second has the cache from the
|
||||
|
|
@ -21,11 +21,11 @@ async fn remote_metadata_with_and_without_cache() -> Result<()> {
|
|||
let filename = WheelFilename::from_str(url.rsplit_once('/').unwrap().1)?;
|
||||
let dist = BuiltDist::DirectUrl(DirectUrlBuiltDist {
|
||||
filename,
|
||||
location: Box::new(DisplaySafeUrl::parse(url).unwrap()),
|
||||
url: VerbatimUrl::from_str(url).unwrap(),
|
||||
location: Box::new(DisplaySafeUrl::parse(url)?),
|
||||
url: VerbatimUrl::from_str(url)?,
|
||||
});
|
||||
let capabilities = IndexCapabilities::default();
|
||||
let metadata = client.wheel_metadata(&dist, &capabilities).await.unwrap();
|
||||
let metadata = client.wheel_metadata(&dist, &capabilities).await?;
|
||||
assert_eq!(metadata.version.to_string(), "4.66.1");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,333 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use rustls::AlertDescription;
|
||||
use url::Url;
|
||||
|
||||
use uv_cache::Cache;
|
||||
use uv_client::BaseClientBuilder;
|
||||
use uv_client::RegistryClientBuilder;
|
||||
use uv_redacted::DisplaySafeUrl;
|
||||
use uv_static::EnvVars;
|
||||
|
||||
use crate::http_util::{
|
||||
generate_self_signed_certs, generate_self_signed_certs_with_ca,
|
||||
start_https_mtls_user_agent_server, start_https_user_agent_server, test_cert_dir,
|
||||
};
|
||||
|
||||
// SAFETY: This test is meant to run with single thread configuration
|
||||
#[tokio::test]
|
||||
#[allow(unsafe_code)]
|
||||
async fn ssl_env_vars() -> Result<()> {
|
||||
// Ensure our environment is not polluted with anything that may affect `rustls-native-certs`
|
||||
unsafe {
|
||||
std::env::remove_var(EnvVars::UV_NATIVE_TLS);
|
||||
std::env::remove_var(EnvVars::SSL_CERT_FILE);
|
||||
std::env::remove_var(EnvVars::SSL_CERT_DIR);
|
||||
std::env::remove_var(EnvVars::SSL_CLIENT_CERT);
|
||||
}
|
||||
|
||||
// Create temporary cert dirs
|
||||
let cert_dir = test_cert_dir();
|
||||
fs_err::create_dir_all(&cert_dir).expect("Failed to create test cert bucket");
|
||||
let cert_dir =
|
||||
tempfile::TempDir::new_in(cert_dir).expect("Failed to create test cert directory");
|
||||
let does_not_exist_cert_dir = cert_dir.path().join("does_not_exist");
|
||||
|
||||
// Generate self-signed standalone cert
|
||||
let standalone_server_cert = generate_self_signed_certs()?;
|
||||
let standalone_public_pem_path = cert_dir.path().join("standalone_public.pem");
|
||||
let standalone_private_pem_path = cert_dir.path().join("standalone_private.pem");
|
||||
|
||||
// Generate self-signed CA, server, and client certs
|
||||
let (ca_cert, server_cert, client_cert) = generate_self_signed_certs_with_ca()?;
|
||||
let ca_public_pem_path = cert_dir.path().join("ca_public.pem");
|
||||
let ca_private_pem_path = cert_dir.path().join("ca_private.pem");
|
||||
let server_public_pem_path = cert_dir.path().join("server_public.pem");
|
||||
let server_private_pem_path = cert_dir.path().join("server_private.pem");
|
||||
let client_combined_pem_path = cert_dir.path().join("client_combined.pem");
|
||||
|
||||
// Persist the certs in PKCS8 format as the env vars expect a path on disk
|
||||
fs_err::write(
|
||||
standalone_public_pem_path.as_path(),
|
||||
standalone_server_cert.public.pem(),
|
||||
)?;
|
||||
fs_err::write(
|
||||
standalone_private_pem_path.as_path(),
|
||||
standalone_server_cert.private.serialize_pem(),
|
||||
)?;
|
||||
fs_err::write(ca_public_pem_path.as_path(), ca_cert.public.pem())?;
|
||||
fs_err::write(
|
||||
ca_private_pem_path.as_path(),
|
||||
ca_cert.private.serialize_pem(),
|
||||
)?;
|
||||
fs_err::write(server_public_pem_path.as_path(), server_cert.public.pem())?;
|
||||
fs_err::write(
|
||||
server_private_pem_path.as_path(),
|
||||
server_cert.private.serialize_pem(),
|
||||
)?;
|
||||
fs_err::write(
|
||||
client_combined_pem_path.as_path(),
|
||||
// SSL_CLIENT_CERT expects a "combined" cert with the public and private key.
|
||||
format!(
|
||||
"{}\n{}",
|
||||
client_cert.public.pem(),
|
||||
client_cert.private.serialize_pem()
|
||||
),
|
||||
)?;
|
||||
|
||||
// ** Set SSL_CERT_FILE to non-existent location
|
||||
// ** Then verify our request fails to establish a connection
|
||||
|
||||
unsafe {
|
||||
std::env::set_var(EnvVars::SSL_CERT_FILE, does_not_exist_cert_dir.as_os_str());
|
||||
}
|
||||
let (server_task, addr) = start_https_user_agent_server(&standalone_server_cert).await?;
|
||||
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
|
||||
let cache = Cache::temp()?.init().await?;
|
||||
let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build();
|
||||
let res = client
|
||||
.cached_client()
|
||||
.uncached()
|
||||
.for_host(&url)
|
||||
.get(Url::from(url))
|
||||
.send()
|
||||
.await;
|
||||
unsafe {
|
||||
std::env::remove_var(EnvVars::SSL_CERT_FILE);
|
||||
}
|
||||
|
||||
// Validate the client error
|
||||
let Some(reqwest_middleware::Error::Middleware(middleware_error)) = res.err() else {
|
||||
panic!("expected middleware error");
|
||||
};
|
||||
let reqwest_error = middleware_error
|
||||
.chain()
|
||||
.find_map(|err| {
|
||||
err.downcast_ref::<reqwest_middleware::Error>().map(|err| {
|
||||
if let reqwest_middleware::Error::Reqwest(inner) = err {
|
||||
inner
|
||||
} else {
|
||||
panic!("expected reqwest error")
|
||||
}
|
||||
})
|
||||
})
|
||||
.expect("expected reqwest error");
|
||||
assert!(reqwest_error.is_connect());
|
||||
|
||||
// Validate the server error
|
||||
let server_res = server_task.await?;
|
||||
let expected_err = if let Err(anyhow_err) = server_res
|
||||
&& let Some(io_err) = anyhow_err.downcast_ref::<std::io::Error>()
|
||||
&& let Some(wrapped_err) = io_err.get_ref()
|
||||
&& let Some(tls_err) = wrapped_err.downcast_ref::<rustls::Error>()
|
||||
&& matches!(
|
||||
tls_err,
|
||||
rustls::Error::AlertReceived(AlertDescription::UnknownCA)
|
||||
) {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
assert!(expected_err);
|
||||
|
||||
// ** Set SSL_CERT_FILE to our public certificate
|
||||
// ** Then verify our request successfully establishes a connection
|
||||
|
||||
unsafe {
|
||||
std::env::set_var(
|
||||
EnvVars::SSL_CERT_FILE,
|
||||
standalone_public_pem_path.as_os_str(),
|
||||
);
|
||||
}
|
||||
let (server_task, addr) = start_https_user_agent_server(&standalone_server_cert).await?;
|
||||
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
|
||||
let cache = Cache::temp()?.init().await?;
|
||||
let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build();
|
||||
let res = client
|
||||
.cached_client()
|
||||
.uncached()
|
||||
.for_host(&url)
|
||||
.get(Url::from(url))
|
||||
.send()
|
||||
.await;
|
||||
assert!(res.is_ok());
|
||||
let _ = server_task.await?; // wait for server shutdown
|
||||
unsafe {
|
||||
std::env::remove_var(EnvVars::SSL_CERT_FILE);
|
||||
}
|
||||
|
||||
// ** Set SSL_CERT_DIR to our cert dir as well as some other dir that does not exist
|
||||
// ** Then verify our request still successfully establishes a connection
|
||||
|
||||
unsafe {
|
||||
std::env::set_var(
|
||||
EnvVars::SSL_CERT_DIR,
|
||||
std::env::join_paths(vec![
|
||||
cert_dir.path().as_os_str(),
|
||||
does_not_exist_cert_dir.as_os_str(),
|
||||
])?,
|
||||
);
|
||||
}
|
||||
let (server_task, addr) = start_https_user_agent_server(&standalone_server_cert).await?;
|
||||
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
|
||||
let cache = Cache::temp()?.init().await?;
|
||||
let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build();
|
||||
let res = client
|
||||
.cached_client()
|
||||
.uncached()
|
||||
.for_host(&url)
|
||||
.get(Url::from(url))
|
||||
.send()
|
||||
.await;
|
||||
assert!(res.is_ok());
|
||||
let _ = server_task.await?; // wait for server shutdown
|
||||
unsafe {
|
||||
std::env::remove_var(EnvVars::SSL_CERT_DIR);
|
||||
}
|
||||
|
||||
// ** Set SSL_CERT_DIR to only the dir that does not exist
|
||||
// ** Then verify our request fails to establish a connection
|
||||
|
||||
unsafe {
|
||||
std::env::set_var(EnvVars::SSL_CERT_DIR, does_not_exist_cert_dir.as_os_str());
|
||||
}
|
||||
let (server_task, addr) = start_https_user_agent_server(&standalone_server_cert).await?;
|
||||
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
|
||||
let cache = Cache::temp()?.init().await?;
|
||||
let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build();
|
||||
let res = client
|
||||
.cached_client()
|
||||
.uncached()
|
||||
.for_host(&url)
|
||||
.get(Url::from(url))
|
||||
.send()
|
||||
.await;
|
||||
unsafe {
|
||||
std::env::remove_var(EnvVars::SSL_CERT_DIR);
|
||||
}
|
||||
|
||||
// Validate the client error
|
||||
let Some(reqwest_middleware::Error::Middleware(middleware_error)) = res.err() else {
|
||||
panic!("expected middleware error");
|
||||
};
|
||||
let reqwest_error = middleware_error
|
||||
.chain()
|
||||
.find_map(|err| {
|
||||
err.downcast_ref::<reqwest_middleware::Error>().map(|err| {
|
||||
if let reqwest_middleware::Error::Reqwest(inner) = err {
|
||||
inner
|
||||
} else {
|
||||
panic!("expected reqwest error")
|
||||
}
|
||||
})
|
||||
})
|
||||
.expect("expected reqwest error");
|
||||
assert!(reqwest_error.is_connect());
|
||||
|
||||
// Validate the server error
|
||||
let server_res = server_task.await?;
|
||||
let expected_err = if let Err(anyhow_err) = server_res
|
||||
&& let Some(io_err) = anyhow_err.downcast_ref::<std::io::Error>()
|
||||
&& let Some(wrapped_err) = io_err.get_ref()
|
||||
&& let Some(tls_err) = wrapped_err.downcast_ref::<rustls::Error>()
|
||||
&& matches!(
|
||||
tls_err,
|
||||
rustls::Error::AlertReceived(AlertDescription::UnknownCA)
|
||||
) {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
assert!(expected_err);
|
||||
|
||||
// *** mTLS Tests
|
||||
|
||||
// ** Set SSL_CERT_FILE to our CA and SSL_CLIENT_CERT to our client cert
|
||||
// ** Then verify our request still successfully establishes a connection
|
||||
|
||||
// We need to set SSL_CERT_FILE or SSL_CERT_DIR to our CA as we need to tell
|
||||
// our HTTP client that we trust certificates issued by our self-signed CA.
|
||||
// This inherently also tests that our server cert is also validated as part
|
||||
// of the certificate path validation algorithm.
|
||||
unsafe {
|
||||
std::env::set_var(EnvVars::SSL_CERT_FILE, ca_public_pem_path.as_os_str());
|
||||
std::env::set_var(
|
||||
EnvVars::SSL_CLIENT_CERT,
|
||||
client_combined_pem_path.as_os_str(),
|
||||
);
|
||||
}
|
||||
let (server_task, addr) = start_https_mtls_user_agent_server(&ca_cert, &server_cert).await?;
|
||||
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
|
||||
let cache = Cache::temp()?.init().await?;
|
||||
let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build();
|
||||
let res = client
|
||||
.cached_client()
|
||||
.uncached()
|
||||
.for_host(&url)
|
||||
.get(Url::from(url))
|
||||
.send()
|
||||
.await;
|
||||
assert!(res.is_ok());
|
||||
let _ = server_task.await?; // wait for server shutdown
|
||||
unsafe {
|
||||
std::env::remove_var(EnvVars::SSL_CERT_FILE);
|
||||
std::env::remove_var(EnvVars::SSL_CLIENT_CERT);
|
||||
}
|
||||
|
||||
// ** Set SSL_CERT_FILE to our CA and unset SSL_CLIENT_CERT
|
||||
// ** Then verify our request fails to establish a connection
|
||||
|
||||
unsafe {
|
||||
std::env::set_var(EnvVars::SSL_CERT_FILE, ca_public_pem_path.as_os_str());
|
||||
}
|
||||
let (server_task, addr) = start_https_mtls_user_agent_server(&ca_cert, &server_cert).await?;
|
||||
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
|
||||
let cache = Cache::temp()?.init().await?;
|
||||
let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build();
|
||||
let res = client
|
||||
.cached_client()
|
||||
.uncached()
|
||||
.for_host(&url)
|
||||
.get(Url::from(url))
|
||||
.send()
|
||||
.await;
|
||||
unsafe {
|
||||
std::env::remove_var(EnvVars::SSL_CERT_FILE);
|
||||
}
|
||||
|
||||
// Validate the client error
|
||||
let Some(reqwest_middleware::Error::Middleware(middleware_error)) = res.err() else {
|
||||
panic!("expected middleware error");
|
||||
};
|
||||
let reqwest_error = middleware_error
|
||||
.chain()
|
||||
.find_map(|err| {
|
||||
err.downcast_ref::<reqwest_middleware::Error>().map(|err| {
|
||||
if let reqwest_middleware::Error::Reqwest(inner) = err {
|
||||
inner
|
||||
} else {
|
||||
panic!("expected reqwest error")
|
||||
}
|
||||
})
|
||||
})
|
||||
.expect("expected reqwest error");
|
||||
assert!(reqwest_error.is_connect());
|
||||
|
||||
// Validate the server error
|
||||
let server_res = server_task.await?;
|
||||
let expected_err = if let Err(anyhow_err) = server_res
|
||||
&& let Some(io_err) = anyhow_err.downcast_ref::<std::io::Error>()
|
||||
&& let Some(wrapped_err) = io_err.get_ref()
|
||||
&& let Some(tls_err) = wrapped_err.downcast_ref::<rustls::Error>()
|
||||
&& matches!(tls_err, rustls::Error::NoCertificatesPresented)
|
||||
{
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
assert!(expected_err);
|
||||
|
||||
// Fin.
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,16 +1,9 @@
|
|||
use anyhow::Result;
|
||||
use futures::future;
|
||||
use http_body_util::Full;
|
||||
use hyper::body::Bytes;
|
||||
use hyper::header::USER_AGENT;
|
||||
use hyper::server::conn::http1;
|
||||
use hyper::service::service_fn;
|
||||
use hyper::{Request, Response};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use insta::{assert_json_snapshot, assert_snapshot, with_settings};
|
||||
use std::str::FromStr;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use anyhow::Result;
|
||||
use insta::{assert_json_snapshot, assert_snapshot, with_settings};
|
||||
use url::Url;
|
||||
|
||||
use uv_cache::Cache;
|
||||
use uv_client::RegistryClientBuilder;
|
||||
use uv_client::{BaseClientBuilder, LineHaul};
|
||||
|
|
@ -19,39 +12,15 @@ use uv_platform_tags::{Arch, Os, Platform};
|
|||
use uv_redacted::DisplaySafeUrl;
|
||||
use uv_version::version;
|
||||
|
||||
use crate::http_util::start_http_user_agent_server;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_agent_has_version() -> Result<()> {
|
||||
// Set up the TCP listener on a random available port
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
let addr = listener.local_addr()?;
|
||||
|
||||
// Spawn the server loop in a background task
|
||||
let server_task = tokio::spawn(async move {
|
||||
let svc = service_fn(move |req: Request<hyper::body::Incoming>| {
|
||||
// Get User Agent Header and send it back in the response
|
||||
let user_agent = req
|
||||
.headers()
|
||||
.get(USER_AGENT)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_default(); // Empty Default
|
||||
future::ok::<_, hyper::Error>(Response::new(Full::new(Bytes::from(user_agent))))
|
||||
});
|
||||
// Start Server (not wrapped in loop {} since we want a single response server)
|
||||
// If you want server to accept multiple connections, wrap it in loop {}
|
||||
let (socket, _) = listener.accept().await.unwrap();
|
||||
let socket = TokioIo::new(socket);
|
||||
tokio::task::spawn(async move {
|
||||
http1::Builder::new()
|
||||
.serve_connection(socket, svc)
|
||||
.with_upgrades()
|
||||
.await
|
||||
.expect("Server Started");
|
||||
});
|
||||
});
|
||||
// Initialize dummy http server
|
||||
let (server_task, addr) = start_http_user_agent_server().await?;
|
||||
|
||||
// Initialize uv-client
|
||||
let cache = Cache::temp()?.init()?;
|
||||
let cache = Cache::temp()?.init().await?;
|
||||
let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build();
|
||||
|
||||
// Send request to our dummy server
|
||||
|
|
@ -88,47 +57,84 @@ async fn test_user_agent_has_version() -> Result<()> {
|
|||
assert_json_snapshot!(&linehaul.installer, @r#"
|
||||
{
|
||||
"name": "uv",
|
||||
"version": "[VERSION]"
|
||||
"version": "[VERSION]",
|
||||
"subcommand": null
|
||||
}
|
||||
"#);
|
||||
});
|
||||
|
||||
// Wait for the server task to complete, to be a good citizen.
|
||||
server_task.await?;
|
||||
let _ = server_task.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_agent_has_subcommand() -> Result<()> {
|
||||
// Initialize dummy http server
|
||||
let (server_task, addr) = start_http_user_agent_server().await?;
|
||||
|
||||
// Initialize uv-client
|
||||
let cache = Cache::temp()?.init().await?;
|
||||
let client = RegistryClientBuilder::new(
|
||||
BaseClientBuilder::default().subcommand(vec!["foo".to_owned(), "bar".to_owned()]),
|
||||
cache,
|
||||
)
|
||||
.build();
|
||||
|
||||
// Send request to our dummy server
|
||||
let url = DisplaySafeUrl::from_str(&format!("http://{addr}"))?;
|
||||
let res = client
|
||||
.cached_client()
|
||||
.uncached()
|
||||
.for_host(&url)
|
||||
.get(Url::from(url))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
// Check the HTTP status
|
||||
assert!(res.status().is_success());
|
||||
|
||||
// Check User Agent
|
||||
let body = res.text().await?;
|
||||
|
||||
let (uv_version, uv_linehaul) = body
|
||||
.split_once(' ')
|
||||
.expect("Failed to split User-Agent header");
|
||||
|
||||
// Deserializing Linehaul
|
||||
let linehaul: LineHaul = serde_json::from_str(uv_linehaul)?;
|
||||
|
||||
// Assert linehaul user agent
|
||||
let filters = vec![(version(), "[VERSION]")];
|
||||
with_settings!({
|
||||
filters => filters
|
||||
}, {
|
||||
// Assert uv version
|
||||
assert_snapshot!(uv_version, @"uv/[VERSION]");
|
||||
// Assert linehaul json
|
||||
assert_json_snapshot!(&linehaul.installer, @r#"
|
||||
{
|
||||
"name": "uv",
|
||||
"version": "[VERSION]",
|
||||
"subcommand": [
|
||||
"foo",
|
||||
"bar"
|
||||
]
|
||||
}
|
||||
"#);
|
||||
});
|
||||
|
||||
// Wait for the server task to complete, to be a good citizen.
|
||||
let _ = server_task.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_agent_has_linehaul() -> Result<()> {
|
||||
// Set up the TCP listener on a random available port
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
let addr = listener.local_addr()?;
|
||||
|
||||
// Spawn the server loop in a background task
|
||||
let server_task = tokio::spawn(async move {
|
||||
let svc = service_fn(move |req: Request<hyper::body::Incoming>| {
|
||||
// Get User Agent Header and send it back in the response
|
||||
let user_agent = req
|
||||
.headers()
|
||||
.get(USER_AGENT)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_default(); // Empty Default
|
||||
future::ok::<_, hyper::Error>(Response::new(Full::new(Bytes::from(user_agent))))
|
||||
});
|
||||
// Start Server (not wrapped in loop {} since we want a single response server)
|
||||
// If you want server to accept multiple connections, wrap it in loop {}
|
||||
let (socket, _) = listener.accept().await.unwrap();
|
||||
let socket = TokioIo::new(socket);
|
||||
tokio::task::spawn(async move {
|
||||
http1::Builder::new()
|
||||
.serve_connection(socket, svc)
|
||||
.with_upgrades()
|
||||
.await
|
||||
.expect("Server Started");
|
||||
});
|
||||
});
|
||||
// Initialize dummy http server
|
||||
let (server_task, addr) = start_http_user_agent_server().await?;
|
||||
|
||||
// Add some representative markers for an Ubuntu CI runner
|
||||
let markers = MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
|
||||
|
|
@ -143,11 +149,10 @@ async fn test_user_agent_has_linehaul() -> Result<()> {
|
|||
python_full_version: "3.12.2",
|
||||
python_version: "3.12",
|
||||
sys_platform: "linux",
|
||||
})
|
||||
.unwrap();
|
||||
})?;
|
||||
|
||||
// Initialize uv-client
|
||||
let cache = Cache::temp()?.init()?;
|
||||
let cache = Cache::temp()?.init().await?;
|
||||
let mut builder =
|
||||
RegistryClientBuilder::new(BaseClientBuilder::default(), cache).markers(&markers);
|
||||
|
||||
|
|
@ -189,7 +194,7 @@ async fn test_user_agent_has_linehaul() -> Result<()> {
|
|||
let body = res.text().await?;
|
||||
|
||||
// Wait for the server task to complete, to be a good citizen.
|
||||
server_task.await?;
|
||||
let _ = server_task.await?;
|
||||
|
||||
// Unpack User-Agent with linehaul
|
||||
let (uv_version, uv_linehaul) = body
|
||||
|
|
@ -210,11 +215,12 @@ async fn test_user_agent_has_linehaul() -> Result<()> {
|
|||
assert_json_snapshot!(&linehaul, {
|
||||
".distro" => "[distro]",
|
||||
".ci" => "[ci]"
|
||||
}, @r###"
|
||||
}, @r#"
|
||||
{
|
||||
"installer": {
|
||||
"name": "uv",
|
||||
"version": "[VERSION]"
|
||||
"version": "[VERSION]",
|
||||
"subcommand": null
|
||||
},
|
||||
"python": "3.12.2",
|
||||
"implementation": {
|
||||
|
|
@ -232,7 +238,7 @@ async fn test_user_agent_has_linehaul() -> Result<()> {
|
|||
"rustc_version": null,
|
||||
"ci": "[ci]"
|
||||
}
|
||||
"###);
|
||||
"#);
|
||||
});
|
||||
|
||||
// Assert distro
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
[package]
|
||||
name = "uv-configuration"
|
||||
version = "0.0.1"
|
||||
version = "0.0.8"
|
||||
description = "This is an internal component crate of uv"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- This file is generated. DO NOT EDIT -->
|
||||
|
||||
# uv-configuration
|
||||
|
||||
This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here
|
||||
is unstable and will have frequent breaking changes.
|
||||
|
||||
This version (0.0.8) is a component of [uv 0.9.18](https://crates.io/crates/uv/0.9.18). The source
|
||||
can be found [here](https://github.com/astral-sh/uv/blob/0.9.18/crates/uv-configuration).
|
||||
|
||||
See uv's
|
||||
[crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning)
|
||||
for details on versioning.
|
||||
|
|
@ -15,4 +15,30 @@ pub enum ExportFormat {
|
|||
#[serde(rename = "pylock.toml", alias = "pylock-toml")]
|
||||
#[cfg_attr(feature = "clap", clap(name = "pylock.toml", alias = "pylock-toml"))]
|
||||
PylockToml,
|
||||
/// Export in `CycloneDX` v1.5 JSON format.
|
||||
#[serde(rename = "cyclonedx1.5")]
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(name = "cyclonedx1.5", alias = "cyclonedx1.5+json")
|
||||
)]
|
||||
CycloneDX1_5,
|
||||
}
|
||||
|
||||
/// The output format to use in `uv pip compile`.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
||||
pub enum PipCompileFormat {
|
||||
/// Export in `requirements.txt` format.
|
||||
#[default]
|
||||
#[serde(rename = "requirements.txt", alias = "requirements-txt")]
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(name = "requirements.txt", alias = "requirements-txt")
|
||||
)]
|
||||
RequirementsTxt,
|
||||
/// Export in `pylock.toml` format.
|
||||
#[serde(rename = "pylock.toml", alias = "pylock-toml")]
|
||||
#[cfg_attr(feature = "clap", clap(name = "pylock.toml", alias = "pylock-toml"))]
|
||||
PylockToml,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,3 +94,32 @@ wheels/
|
|||
# Virtual environments
|
||||
.venv
|
||||
";
|
||||
|
||||
/// Setting for Git LFS (Large File Storage) support.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum GitLfsSetting {
|
||||
/// Git LFS is disabled (default).
|
||||
#[default]
|
||||
Disabled,
|
||||
/// Git LFS is enabled. Tracks whether it came from an environment variable.
|
||||
Enabled { from_env: bool },
|
||||
}
|
||||
|
||||
impl GitLfsSetting {
|
||||
pub fn new(from_arg: Option<bool>, from_env: Option<bool>) -> Self {
|
||||
match (from_arg, from_env) {
|
||||
(Some(true), _) => Self::Enabled { from_env: false },
|
||||
(_, Some(true)) => Self::Enabled { from_env: true },
|
||||
_ => Self::Disabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GitLfsSetting> for Option<bool> {
|
||||
fn from(setting: GitLfsSetting) -> Self {
|
||||
match setting {
|
||||
GitLfsSetting::Enabled { .. } => Some(true),
|
||||
GitLfsSetting::Disabled => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
[package]
|
||||
name = "uv-console"
|
||||
version = "0.0.1"
|
||||
description = "Utilities for interacting with the terminal"
|
||||
version = "0.0.8"
|
||||
description = "This is an internal component crate of uv"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- This file is generated. DO NOT EDIT -->
|
||||
|
||||
# uv-console
|
||||
|
||||
This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here
|
||||
is unstable and will have frequent breaking changes.
|
||||
|
||||
This version (0.0.8) is a component of [uv 0.9.18](https://crates.io/crates/uv/0.9.18). The source
|
||||
can be found [here](https://github.com/astral-sh/uv/blob/0.9.18/crates/uv-console).
|
||||
|
||||
See uv's
|
||||
[crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning)
|
||||
for details on versioning.
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
[package]
|
||||
name = "uv-dev"
|
||||
version = "0.0.1"
|
||||
description = "Build wheels from source distributions"
|
||||
version = "0.0.8"
|
||||
description = "This is an internal component crate of uv"
|
||||
publish = false
|
||||
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
|
@ -80,4 +79,4 @@ performance-memory-allocator = ["dep:uv-performance-memory-allocator"]
|
|||
render = ["poloto", "resvg", "tagu"]
|
||||
|
||||
[package.metadata.cargo-shear]
|
||||
ignored = ["flate2", "uv-extract", "uv-performance-memory-allocator"]
|
||||
ignored = ["uv-performance-memory-allocator"]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- This file is generated. DO NOT EDIT -->
|
||||
|
||||
# uv-dev
|
||||
|
||||
This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here
|
||||
is unstable and will have frequent breaking changes.
|
||||
|
||||
This version (0.0.8) is a component of [uv 0.9.18](https://crates.io/crates/uv/0.9.18). The source
|
||||
can be found [here](https://github.com/astral-sh/uv/blob/0.9.18/crates/uv-dev).
|
||||
|
||||
See uv's
|
||||
[crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning)
|
||||
for details on versioning.
|
||||
|
|
@ -18,7 +18,7 @@ pub(crate) struct CompileArgs {
|
|||
}
|
||||
|
||||
pub(crate) async fn compile(args: CompileArgs) -> anyhow::Result<()> {
|
||||
let cache = Cache::try_from(args.cache_args)?.init()?;
|
||||
let cache = Cache::try_from(args.cache_args)?.init().await?;
|
||||
|
||||
let interpreter = if let Some(python) = args.python {
|
||||
python
|
||||
|
|
|
|||
|
|
@ -342,31 +342,3 @@ fn emit_possible_options(opt: &clap::Arg, output: &mut String) {
|
|||
output.push_str(&markdown::to_html(&value));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use uv_static::EnvVars;
|
||||
|
||||
use crate::generate_all::Mode;
|
||||
|
||||
use super::{Args, main};
|
||||
|
||||
#[test]
|
||||
fn test_generate_cli_reference() -> Result<()> {
|
||||
// Skip this test in CI to avoid redundancy with the dedicated CI job
|
||||
if env::var_os(EnvVars::CI).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mode = if env::var(EnvVars::UV_UPDATE_SCHEMA).as_deref() == Ok("1") {
|
||||
Mode::Write
|
||||
} else {
|
||||
Mode::Check
|
||||
};
|
||||
main(&Args { mode })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,31 +106,3 @@ fn render(var: &str, doc: &str, added_in: Option<&str>) -> String {
|
|||
format!("### `{var}`\n\n{doc}\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use uv_static::EnvVars;
|
||||
|
||||
use crate::generate_all::Mode;
|
||||
|
||||
use super::{Args, main};
|
||||
|
||||
#[test]
|
||||
fn test_generate_env_vars_reference() -> Result<()> {
|
||||
// Skip this test in CI to avoid redundancy with the dedicated CI job
|
||||
if env::var_os(EnvVars::CI).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mode = if env::var(EnvVars::UV_UPDATE_SCHEMA).as_deref() == Ok("1") {
|
||||
Mode::Write
|
||||
} else {
|
||||
Mode::Check
|
||||
};
|
||||
main(&Args { mode })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -387,31 +387,3 @@ impl Visit for CollectOptionsVisitor {
|
|||
self.fields.push((name.to_owned(), field));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use uv_static::EnvVars;
|
||||
|
||||
use crate::generate_all::Mode;
|
||||
|
||||
use super::{Args, main};
|
||||
|
||||
#[test]
|
||||
fn test_generate_options_reference() -> Result<()> {
|
||||
// Skip this test in CI to avoid redundancy with the dedicated CI job
|
||||
if env::var_os(EnvVars::CI).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mode = if env::var(EnvVars::UV_UPDATE_SCHEMA).as_deref() == Ok("1") {
|
||||
Mode::Write
|
||||
} else {
|
||||
Mode::Check
|
||||
};
|
||||
main(&Args { mode })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use crate::ROOT_DIR;
|
|||
use crate::generate_all::Mode;
|
||||
|
||||
/// Contains current supported targets
|
||||
const TARGETS_YML_URL: &str = "https://raw.githubusercontent.com/astral-sh/python-build-standalone/refs/tags/20251031/cpython-unix/targets.yml";
|
||||
const TARGETS_YML_URL: &str = "https://raw.githubusercontent.com/astral-sh/python-build-standalone/refs/tags/20251209/cpython-unix/targets.yml";
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub(crate) struct Args {
|
||||
|
|
@ -130,7 +130,7 @@ async fn generate() -> Result<String> {
|
|||
output.push_str("//! DO NOT EDIT\n");
|
||||
output.push_str("//!\n");
|
||||
output.push_str("//! Generated with `cargo run dev generate-sysconfig-metadata`\n");
|
||||
output.push_str("//! Targets from <https://github.com/astral-sh/python-build-standalone/blob/20251031/cpython-unix/targets.yml>\n");
|
||||
output.push_str("//! Targets from <https://github.com/astral-sh/python-build-standalone/blob/20251209/cpython-unix/targets.yml>\n");
|
||||
output.push_str("//!\n");
|
||||
|
||||
// Disable clippy/fmt
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ pub(crate) async fn list_packages(
|
|||
args: ListPackagesArgs,
|
||||
environment: EnvironmentOptions,
|
||||
) -> Result<()> {
|
||||
let cache = Cache::try_from(args.cache_args)?.init()?;
|
||||
let cache = Cache::try_from(args.cache_args)?.init().await?;
|
||||
let client = RegistryClientBuilder::new(
|
||||
BaseClientBuilder::default().timeout(environment.http_timeout),
|
||||
cache,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ pub(crate) async fn validate_zip(
|
|||
args: ValidateZipArgs,
|
||||
environment: EnvironmentOptions,
|
||||
) -> Result<()> {
|
||||
let cache = Cache::try_from(args.cache_args)?.init()?;
|
||||
let cache = Cache::try_from(args.cache_args)?.init().await?;
|
||||
let client = RegistryClientBuilder::new(
|
||||
BaseClientBuilder::default().timeout(environment.http_timeout),
|
||||
cache,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ pub(crate) async fn wheel_metadata(
|
|||
args: WheelMetadataArgs,
|
||||
environment: EnvironmentOptions,
|
||||
) -> Result<()> {
|
||||
let cache = Cache::try_from(args.cache_args)?.init()?;
|
||||
let cache = Cache::try_from(args.cache_args)?.init().await?;
|
||||
let client = RegistryClientBuilder::new(
|
||||
BaseClientBuilder::default().timeout(environment.http_timeout),
|
||||
cache,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
[package]
|
||||
name = "uv-dirs"
|
||||
version = "0.0.1"
|
||||
description = "Resolution of directories for storage of uv state"
|
||||
version = "0.0.8"
|
||||
description = "This is an internal component crate of uv"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- This file is generated. DO NOT EDIT -->
|
||||
|
||||
# uv-dirs
|
||||
|
||||
This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here
|
||||
is unstable and will have frequent breaking changes.
|
||||
|
||||
This version (0.0.8) is a component of [uv 0.9.18](https://crates.io/crates/uv/0.9.18). The source
|
||||
can be found [here](https://github.com/astral-sh/uv/blob/0.9.18/crates/uv-dirs).
|
||||
|
||||
See uv's
|
||||
[crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning)
|
||||
for details on versioning.
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
[package]
|
||||
name = "uv-dispatch"
|
||||
version = "0.0.1"
|
||||
description = "Avoid cyclic crate dependencies between resolver, installer and builder"
|
||||
version = "0.0.8"
|
||||
description = "This is an internal component crate of uv"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- This file is generated. DO NOT EDIT -->
|
||||
|
||||
# uv-dispatch
|
||||
|
||||
This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here
|
||||
is unstable and will have frequent breaking changes.
|
||||
|
||||
This version (0.0.8) is a component of [uv 0.9.18](https://crates.io/crates/uv/0.9.18). The source
|
||||
can be found [here](https://github.com/astral-sh/uv/blob/0.9.18/crates/uv-dispatch).
|
||||
|
||||
See uv's
|
||||
[crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning)
|
||||
for details on versioning.
|
||||
|
|
@ -492,6 +492,7 @@ impl BuildContext for BuildDispatch<'_> {
|
|||
environment_variables,
|
||||
build_output,
|
||||
self.concurrency.builds,
|
||||
self.client.credentials_cache(),
|
||||
self.preview,
|
||||
)
|
||||
.boxed_local()
|
||||
|
|
@ -504,6 +505,7 @@ impl BuildContext for BuildDispatch<'_> {
|
|||
source: &'data Path,
|
||||
subdirectory: Option<&'data Path>,
|
||||
output_dir: &'data Path,
|
||||
sources: SourceStrategy,
|
||||
build_kind: BuildKind,
|
||||
version_id: Option<&'data str>,
|
||||
) -> Result<Option<DistFilename>, BuildDispatchError> {
|
||||
|
|
@ -532,6 +534,7 @@ impl BuildContext for BuildDispatch<'_> {
|
|||
&output_dir,
|
||||
None,
|
||||
uv_version::version(),
|
||||
sources == SourceStrategy::Enabled,
|
||||
)?;
|
||||
DistFilename::WheelFilename(wheel)
|
||||
}
|
||||
|
|
@ -540,6 +543,7 @@ impl BuildContext for BuildDispatch<'_> {
|
|||
&source_tree,
|
||||
&output_dir,
|
||||
uv_version::version(),
|
||||
sources == SourceStrategy::Enabled,
|
||||
)?;
|
||||
DistFilename::SourceDistFilename(source_dist)
|
||||
}
|
||||
|
|
@ -549,6 +553,7 @@ impl BuildContext for BuildDispatch<'_> {
|
|||
&output_dir,
|
||||
None,
|
||||
uv_version::version(),
|
||||
sources == SourceStrategy::Enabled,
|
||||
)?;
|
||||
DistFilename::WheelFilename(wheel)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
[package]
|
||||
name = "uv-distribution-filename"
|
||||
version = "0.0.1"
|
||||
version = "0.0.8"
|
||||
description = "This is an internal component crate of uv"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- This file is generated. DO NOT EDIT -->
|
||||
|
||||
# uv-distribution-filename
|
||||
|
||||
This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here
|
||||
is unstable and will have frequent breaking changes.
|
||||
|
||||
This version (0.0.8) is a component of [uv 0.9.18](https://crates.io/crates/uv/0.9.18). The source
|
||||
can be found [here](https://github.com/astral-sh/uv/blob/0.9.18/crates/uv-distribution-filename).
|
||||
|
||||
See uv's
|
||||
[crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning)
|
||||
for details on versioning.
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
[package]
|
||||
name = "uv-distribution-types"
|
||||
version = "0.0.1"
|
||||
version = "0.0.8"
|
||||
description = "This is an internal component crate of uv"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- This file is generated. DO NOT EDIT -->
|
||||
|
||||
# uv-distribution-types
|
||||
|
||||
This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here
|
||||
is unstable and will have frequent breaking changes.
|
||||
|
||||
This version (0.0.8) is a component of [uv 0.9.18](https://crates.io/crates/uv/0.9.18). The source
|
||||
can be found [here](https://github.com/astral-sh/uv/blob/0.9.18/crates/uv-distribution-types).
|
||||
|
||||
See uv's
|
||||
[crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning)
|
||||
for details on versioning.
|
||||
|
|
@ -8,7 +8,6 @@ use std::sync::{Arc, LazyLock, RwLock};
|
|||
use itertools::Either;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use thiserror::Error;
|
||||
use tracing::trace;
|
||||
use url::{ParseError, Url};
|
||||
use uv_auth::RealmRef;
|
||||
use uv_cache_key::CanonicalUrl;
|
||||
|
|
@ -440,26 +439,6 @@ impl<'a> IndexLocations {
|
|||
}
|
||||
}
|
||||
|
||||
/// Add all authenticated sources to the cache.
|
||||
pub fn cache_index_credentials(&self) {
|
||||
for index in self.known_indexes() {
|
||||
if let Some(credentials) = index.credentials() {
|
||||
trace!(
|
||||
"Read credentials for index {}",
|
||||
index
|
||||
.name
|
||||
.as_ref()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| index.url.to_string())
|
||||
);
|
||||
if let Some(root_url) = index.root_url() {
|
||||
uv_auth::store_credentials(&root_url, credentials.clone());
|
||||
}
|
||||
uv_auth::store_credentials(index.raw_url(), credentials);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the Simple API cache control header for an [`IndexUrl`], if configured.
|
||||
pub fn simple_api_cache_control_for(&self, url: &IndexUrl) -> Option<&str> {
|
||||
for index in &self.indexes {
|
||||
|
|
|
|||
|
|
@ -159,9 +159,9 @@ pub enum InstalledVersion<'a> {
|
|||
Url(&'a DisplaySafeUrl, &'a Version),
|
||||
}
|
||||
|
||||
impl InstalledVersion<'_> {
|
||||
impl<'a> InstalledVersion<'a> {
|
||||
/// If it is a URL, return its value.
|
||||
pub fn url(&self) -> Option<&DisplaySafeUrl> {
|
||||
pub fn url(&self) -> Option<&'a DisplaySafeUrl> {
|
||||
match self {
|
||||
Self::Version(_) => None,
|
||||
Self::Url(url, _) => Some(url),
|
||||
|
|
@ -169,7 +169,7 @@ impl InstalledVersion<'_> {
|
|||
}
|
||||
|
||||
/// If it is a version, return its value.
|
||||
pub fn version(&self) -> &Version {
|
||||
pub fn version(&self) -> &'a Version {
|
||||
match self {
|
||||
Self::Version(version) => version,
|
||||
Self::Url(_, version) => version,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use thiserror::Error;
|
|||
use uv_cache_key::{CacheKey, CacheKeyHasher};
|
||||
use uv_distribution_filename::DistExtension;
|
||||
use uv_fs::{CWD, PortablePath, PortablePathBuf, relative_to};
|
||||
use uv_git_types::{GitOid, GitReference, GitUrl, GitUrlParseError, OidParseError};
|
||||
use uv_git_types::{GitLfs, GitOid, GitReference, GitUrl, GitUrlParseError, OidParseError};
|
||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||
use uv_pep440::VersionSpecifiers;
|
||||
use uv_pep508::{
|
||||
|
|
@ -350,6 +350,13 @@ impl Display for Requirement {
|
|||
if let Some(subdirectory) = subdirectory {
|
||||
writeln!(f, "#subdirectory={}", subdirectory.display())?;
|
||||
}
|
||||
if git.lfs().enabled() {
|
||||
writeln!(
|
||||
f,
|
||||
"{}lfs=true",
|
||||
if subdirectory.is_some() { "&" } else { "#" }
|
||||
)?;
|
||||
}
|
||||
}
|
||||
RequirementSource::Path { url, .. } => {
|
||||
write!(f, " @ {url}")?;
|
||||
|
|
@ -436,6 +443,9 @@ impl CacheKey for Requirement {
|
|||
} else {
|
||||
0u8.cache_key(state);
|
||||
}
|
||||
if git.lfs().enabled() {
|
||||
1u8.cache_key(state);
|
||||
}
|
||||
url.cache_key(state);
|
||||
}
|
||||
RequirementSource::Path {
|
||||
|
|
@ -765,6 +775,13 @@ impl Display for RequirementSource {
|
|||
if let Some(subdirectory) = subdirectory {
|
||||
writeln!(f, "#subdirectory={}", subdirectory.display())?;
|
||||
}
|
||||
if git.lfs().enabled() {
|
||||
writeln!(
|
||||
f,
|
||||
"{}lfs=true",
|
||||
if subdirectory.is_some() { "&" } else { "#" }
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Self::Path { url, .. } => {
|
||||
write!(f, "{url}")?;
|
||||
|
|
@ -856,6 +873,11 @@ impl From<RequirementSource> for RequirementSourceWire {
|
|||
.append_pair("subdirectory", &subdirectory);
|
||||
}
|
||||
|
||||
// Persist lfs=true in the distribution metadata only when explicitly enabled.
|
||||
if git.lfs().enabled() {
|
||||
url.query_pairs_mut().append_pair("lfs", "true");
|
||||
}
|
||||
|
||||
// Put the requested reference in the query.
|
||||
match git.reference() {
|
||||
GitReference::Branch(branch) => {
|
||||
|
|
@ -932,6 +954,7 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
|
|||
|
||||
let mut reference = GitReference::DefaultBranch;
|
||||
let mut subdirectory: Option<PortablePathBuf> = None;
|
||||
let mut lfs = GitLfs::Disabled;
|
||||
for (key, val) in repository.query_pairs() {
|
||||
match &*key {
|
||||
"tag" => reference = GitReference::Tag(val.into_owned()),
|
||||
|
|
@ -940,6 +963,7 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
|
|||
"subdirectory" => {
|
||||
subdirectory = Some(PortablePathBuf::from(val.as_ref()));
|
||||
}
|
||||
"lfs" => lfs = GitLfs::from(val.eq_ignore_ascii_case("true")),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -959,13 +983,22 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
|
|||
let path = format!("{}@{}", url.path(), rev);
|
||||
url.set_path(&path);
|
||||
}
|
||||
let mut frags: Vec<String> = Vec::new();
|
||||
if let Some(subdirectory) = subdirectory.as_ref() {
|
||||
url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
|
||||
frags.push(format!("subdirectory={subdirectory}"));
|
||||
}
|
||||
// Preserve that we're using Git LFS in the Verbatim Url representations
|
||||
if lfs.enabled() {
|
||||
frags.push("lfs=true".to_string());
|
||||
}
|
||||
if !frags.is_empty() {
|
||||
url.set_fragment(Some(&frags.join("&")));
|
||||
}
|
||||
|
||||
let url = VerbatimUrl::from_url(url);
|
||||
|
||||
Ok(Self::Git {
|
||||
git: GitUrl::from_fields(repository, reference, precise)?,
|
||||
git: GitUrl::from_fields(repository, reference, precise, lfs)?,
|
||||
subdirectory: subdirectory.map(Box::<Path>::from),
|
||||
url,
|
||||
})
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue