Compare commits

..

No commits in common. "main" and "0.9.9" have entirely different histories.
main ... 0.9.9

617 changed files with 17008 additions and 29987 deletions

View File

@ -1,81 +0,0 @@
# /// 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()

View File

@ -1,15 +0,0 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "uv run .claude/hooks/post-edit-format.py"
}
]
}
]
}
}

View File

@ -3,14 +3,15 @@
dependencyDashboard: true, dependencyDashboard: true,
suppressNotifications: ["prEditedNotification"], suppressNotifications: ["prEditedNotification"],
extends: [ extends: [
"github>astral-sh/renovate-config", "config:recommended",
// For tool versions defined in GitHub Actions: // For tool versions defined in GitHub Actions:
"customManagers:githubActionsVersions", "customManagers:githubActionsVersions",
], ],
labels: ["internal"], labels: ["internal"],
schedule: ["* 0-3 * * 1"], schedule: ["before 4am on Monday"],
semanticCommits: "disabled", semanticCommits: "disabled",
separateMajorMinor: false, separateMajorMinor: false,
prHourlyLimit: 10,
enabledManagers: ["github-actions", "pre-commit", "cargo", "custom.regex"], enabledManagers: ["github-actions", "pre-commit", "cargo", "custom.regex"],
cargo: { cargo: {
// See https://docs.renovatebot.com/configuration-options/#rangestrategy // See https://docs.renovatebot.com/configuration-options/#rangestrategy

View File

@ -98,7 +98,7 @@ jobs:
macos-x86_64: macos-x86_64:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: depot-macos-14 runs-on: macos-14
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
@ -157,7 +157,7 @@ jobs:
macos-aarch64: macos-aarch64:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: depot-macos-14 runs-on: macos-14
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
@ -417,7 +417,7 @@ jobs:
linux-arm: linux-arm:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: depot-ubuntu-22.04-8 runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
strategy: strategy:
matrix: matrix:
@ -956,7 +956,7 @@ jobs:
musllinux-cross: musllinux-cross:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: depot-ubuntu-22.04-8 runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
platform: platform:

View File

@ -184,13 +184,13 @@ jobs:
- buildpack-deps:trixie,trixie,debian - buildpack-deps:trixie,trixie,debian
- debian:bookworm-slim,bookworm-slim - debian:bookworm-slim,bookworm-slim
- buildpack-deps:bookworm,bookworm - buildpack-deps:bookworm,bookworm
- python:3.14-alpine3.23,python3.14-alpine3.23,python3.14-alpine - python:3.14-alpine,python3.14-alpine
- python:3.13-alpine3.23,python3.13-alpine3.23,python3.13-alpine - python:3.13-alpine,python3.13-alpine
- python:3.12-alpine3.23,python3.12-alpine3.23,python3.12-alpine - python:3.12-alpine,python3.12-alpine
- python:3.11-alpine3.23,python3.11-alpine3.23,python3.11-alpine - python:3.11-alpine,python3.11-alpine
- python:3.10-alpine3.23,python3.10-alpine3.23,python3.10-alpine - python:3.10-alpine,python3.10-alpine
- python:3.9-alpine3.22,python3.9-alpine3.22,python3.9-alpine - python:3.9-alpine,python3.9-alpine
- python:3.8-alpine3.20,python3.8-alpine3.20,python3.8-alpine - python:3.8-alpine,python3.8-alpine
- python:3.14-trixie,python3.14-trixie - python:3.14-trixie,python3.14-trixie
- python:3.13-trixie,python3.13-trixie - python:3.13-trixie,python3.13-trixie
- python:3.12-trixie,python3.12-trixie - python:3.12-trixie,python3.12-trixie

View File

@ -27,8 +27,6 @@ jobs:
outputs: outputs:
# Flag that is raised when any code is changed # Flag that is raised when any code is changed
code: ${{ steps.changed.outputs.code_any_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: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
@ -42,16 +40,10 @@ jobs:
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha || 'origin/main' }}...HEAD) CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha || 'origin/main' }}...HEAD)
CODE_CHANGED=false CODE_CHANGED=false
SCHEMA_CHANGED=false
while IFS= read -r file; do while IFS= read -r file; do
# Check if the schema file changed (e.g., in a release PR) # Generated markdown and JSON files are checked during test runs.
if [[ "${file}" == "uv.schema.json" ]]; then if [[ "${file}" =~ ^docs/ && ! "${file}" =~ ^docs/reference/(cli|settings).md && ! "${file}" =~ ^docs/reference/environment.md ]]; then
echo "Detected schema change: ${file}"
SCHEMA_CHANGED=true
fi
if [[ "${file}" =~ ^docs/ ]]; then
echo "Skipping ${file} (matches docs/ pattern)" echo "Skipping ${file} (matches docs/ pattern)"
continue continue
fi fi
@ -78,7 +70,6 @@ jobs:
done <<< "${CHANGED_FILES}" done <<< "${CHANGED_FILES}"
echo "code_any_changed=${CODE_CHANGED}" >> "${GITHUB_OUTPUT}" echo "code_any_changed=${CODE_CHANGED}" >> "${GITHUB_OUTPUT}"
echo "schema_changed=${SCHEMA_CHANGED}" >> "${GITHUB_OUTPUT}"
lint: lint:
timeout-minutes: 10 timeout-minutes: 10
name: "lint" name: "lint"
@ -98,7 +89,7 @@ jobs:
- name: "Install uv" - name: "Install uv"
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with: with:
version: "0.9.13" version: "0.9.8"
- name: "rustfmt" - name: "rustfmt"
run: cargo fmt --all --check run: cargo fmt --all --check
@ -144,7 +135,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Check uv_build dependencies" - name: "Check uv_build dependencies"
@ -176,7 +167,7 @@ jobs:
run: | run: |
Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with: with:
workspaces: ${{ env.UV_WORKSPACE }} workspaces: ${{ env.UV_WORKSPACE }}
@ -187,22 +178,6 @@ jobs:
working-directory: ${{ env.UV_WORKSPACE }} working-directory: ${{ env.UV_WORKSPACE }}
run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings 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: cargo-dev-generate-all:
timeout-minutes: 10 timeout-minutes: 10
needs: determine_changes needs: determine_changes
@ -213,16 +188,11 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Generate all" - name: "Generate all"
run: cargo dev generate-all --mode dry-run run: cargo dev generate-all --mode check
- 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: cargo-shear:
timeout-minutes: 10 timeout-minutes: 10
@ -233,7 +203,7 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: "Install cargo shear" - name: "Install cargo shear"
uses: taiki-e/install-action@d850aa816998e5cf15f67a78c7b933f2a5033f8a # v2.63.3 uses: taiki-e/install-action@a416ddeedbd372e614cc1386e8b642692f66865e # v2.57.1
with: with:
tool: cargo-shear tool: cargo-shear
- run: cargo shear - run: cargo shear
@ -255,14 +225,14 @@ jobs:
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
run: rustup show run: rustup show
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with: with:
version: "0.9.13" version: "0.9.8"
- name: "Install required Python versions" - name: "Install required Python versions"
run: uv python install run: uv python install
@ -289,13 +259,12 @@ jobs:
UV_HTTP_RETRIES: 5 UV_HTTP_RETRIES: 5
run: | run: |
cargo nextest run \ cargo nextest run \
--cargo-profile fast-build \
--features python-patch,native-auth,secret-service \ --features python-patch,native-auth,secret-service \
--workspace \ --workspace \
--status-level skip --failure-output immediate-final --no-fail-fast -j 20 --final-status-level slow --status-level skip --failure-output immediate-final --no-fail-fast -j 20 --final-status-level slow
cargo-test-macos: cargo-test-macos:
timeout-minutes: 20 timeout-minutes: 15
needs: determine_changes needs: determine_changes
# Only run macOS tests on main without opt-in # Only run macOS tests on main without opt-in
if: ${{ contains(github.event.pull_request.labels.*.name, 'test:macos') || github.ref == 'refs/heads/main' }} if: ${{ contains(github.event.pull_request.labels.*.name, 'test:macos') || github.ref == 'refs/heads/main' }}
@ -308,14 +277,14 @@ jobs:
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
run: rustup show run: rustup show
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with: with:
version: "0.9.13" version: "0.9.8"
- name: "Install required Python versions" - name: "Install required Python versions"
run: uv python install run: uv python install
@ -331,9 +300,8 @@ jobs:
UV_HTTP_RETRIES: 5 UV_HTTP_RETRIES: 5
run: | run: |
cargo nextest run \ cargo nextest run \
--cargo-profile fast-build \
--no-default-features \ --no-default-features \
--features python,python-managed,pypi,git,git-lfs,performance,crates-io,native-auth,apple-native \ --features python,python-managed,pypi,git,performance,crates-io,native-auth,apple-native \
--workspace \ --workspace \
--status-level skip --failure-output immediate-final --no-fail-fast -j 12 --final-status-level slow --status-level skip --failure-output immediate-final --no-fail-fast -j 12 --final-status-level slow
@ -358,12 +326,12 @@ jobs:
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with: with:
version: "0.9.13" version: "0.9.8"
- name: "Install required Python versions" - name: "Install required Python versions"
run: uv python install run: uv python install
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with: with:
workspaces: ${{ env.UV_WORKSPACE }} workspaces: ${{ env.UV_WORKSPACE }}
@ -387,7 +355,6 @@ jobs:
shell: bash shell: bash
run: | run: |
cargo nextest run \ cargo nextest run \
--cargo-profile fast-build \
--no-default-features \ --no-default-features \
--features python,pypi,python-managed,native-auth,windows-native \ --features python,pypi,python-managed,native-auth,windows-native \
--workspace \ --workspace \
@ -417,7 +384,7 @@ jobs:
run: | run: |
Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with: with:
workspaces: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline workspaces: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline
@ -477,7 +444,7 @@ jobs:
- name: Copy Git Repo to Dev Drive - name: Copy Git Repo to Dev Drive
run: | run: |
Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with: with:
workspaces: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline workspaces: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
@ -495,8 +462,8 @@ jobs:
working-directory: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline working-directory: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline
run: | run: |
cargo build --target ${{ matrix.target-arch }}-pc-windows-msvc cargo build --target ${{ matrix.target-arch }}-pc-windows-msvc
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-console.exe 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 cp target/${{ matrix.target-arch }}-pc-windows-msvc/debug/uv-trampoline-gui.exe trampolines/uv-trampoline-${{ matrix.target-arch }}-gui.exe
- name: "Test new binaries" - name: "Test new binaries"
working-directory: ${{ env.UV_WORKSPACE }} working-directory: ${{ env.UV_WORKSPACE }}
run: | run: |
@ -524,17 +491,9 @@ jobs:
persist-credentials: false persist-credentials: false
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with: with:
version: "0.9.13" version: "0.9.8"
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - 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" - name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
@ -561,18 +520,18 @@ jobs:
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Build" - name: "Build"
run: cargo build --profile no-debug run: cargo build
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: uv-linux-libc-${{ github.sha }} name: uv-linux-libc-${{ github.sha }}
path: | path: |
./target/no-debug/uv ./target/debug/uv
./target/no-debug/uvx ./target/debug/uvx
retention-days: 1 retention-days: 1
build-binary-linux-aarch64: build-binary-linux-aarch64:
@ -588,18 +547,18 @@ jobs:
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Build" - name: "Build"
run: cargo build --profile no-debug run: cargo build
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: uv-linux-aarch64-${{ github.sha }} name: uv-linux-aarch64-${{ github.sha }}
path: | path: |
./target/no-debug/uv ./target/debug/uv
./target/no-debug/uvx ./target/debug/uvx
retention-days: 1 retention-days: 1
build-binary-linux-musl: build-binary-linux-musl:
@ -620,18 +579,18 @@ jobs:
sudo apt-get install musl-tools sudo apt-get install musl-tools
rustup target add x86_64-unknown-linux-musl rustup target add x86_64-unknown-linux-musl
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Build" - name: "Build"
run: cargo build --profile no-debug --target x86_64-unknown-linux-musl --bin uv --bin uvx run: cargo build --target x86_64-unknown-linux-musl --bin uv --bin uvx
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: uv-linux-musl-${{ github.sha }} name: uv-linux-musl-${{ github.sha }}
path: | path: |
./target/x86_64-unknown-linux-musl/no-debug/uv ./target/x86_64-unknown-linux-musl/debug/uv
./target/x86_64-unknown-linux-musl/no-debug/uvx ./target/x86_64-unknown-linux-musl/debug/uvx
retention-days: 1 retention-days: 1
build-binary-macos-aarch64: build-binary-macos-aarch64:
@ -647,17 +606,17 @@ jobs:
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Build" - name: "Build"
run: cargo build --profile no-debug --bin uv --bin uvx run: cargo build --bin uv --bin uvx
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: uv-macos-aarch64-${{ github.sha }} name: uv-macos-aarch64-${{ github.sha }}
path: | path: |
./target/no-debug/uv ./target/debug/uv
./target/no-debug/uvx ./target/debug/uvx
retention-days: 1 retention-days: 1
build-binary-macos-x86_64: build-binary-macos-x86_64:
@ -673,17 +632,17 @@ jobs:
- uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Build" - name: "Build"
run: cargo build --profile no-debug --bin uv --bin uvx run: cargo build --bin uv --bin uvx
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: uv-macos-x86_64-${{ github.sha }} name: uv-macos-x86_64-${{ github.sha }}
path: | path: |
./target/no-debug/uv ./target/debug/uv
./target/no-debug/uvx ./target/debug/uvx
retention-days: 1 retention-days: 1
build-binary-windows-x86_64: build-binary-windows-x86_64:
@ -705,21 +664,21 @@ jobs:
run: | run: |
Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with: with:
workspaces: ${{ env.UV_WORKSPACE }} workspaces: ${{ env.UV_WORKSPACE }}
- name: "Build" - name: "Build"
working-directory: ${{ env.UV_WORKSPACE }} working-directory: ${{ env.UV_WORKSPACE }}
run: cargo build --profile no-debug --bin uv --bin uvx run: cargo build --bin uv --bin uvx
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: uv-windows-x86_64-${{ github.sha }} name: uv-windows-x86_64-${{ github.sha }}
path: | path: |
${{ env.UV_WORKSPACE }}/target/no-debug/uv.exe ${{ env.UV_WORKSPACE }}/target/debug/uv.exe
${{ env.UV_WORKSPACE }}/target/no-debug/uvx.exe ${{ env.UV_WORKSPACE }}/target/debug/uvx.exe
retention-days: 1 retention-days: 1
build-binary-windows-aarch64: build-binary-windows-aarch64:
@ -742,7 +701,7 @@ jobs:
run: | run: |
Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with: with:
workspaces: ${{ env.UV_WORKSPACE }} workspaces: ${{ env.UV_WORKSPACE }}
@ -751,15 +710,15 @@ jobs:
- name: "Build" - name: "Build"
working-directory: ${{ env.UV_WORKSPACE }} working-directory: ${{ env.UV_WORKSPACE }}
run: cargo build --profile no-debug --target aarch64-pc-windows-msvc run: cargo build --target aarch64-pc-windows-msvc
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: uv-windows-aarch64-${{ github.sha }} name: uv-windows-aarch64-${{ github.sha }}
path: | path: |
${{ env.UV_WORKSPACE }}/target/aarch64-pc-windows-msvc/no-debug/uv.exe ${{ env.UV_WORKSPACE }}/target/aarch64-pc-windows-msvc/debug/uv.exe
${{ env.UV_WORKSPACE }}/target/aarch64-pc-windows-msvc/no-debug/uvx.exe ${{ env.UV_WORKSPACE }}/target/aarch64-pc-windows-msvc/debug/uvx.exe
retention-days: 1 retention-days: 1
build-binary-msrv: build-binary-msrv:
@ -783,11 +742,11 @@ jobs:
MSRV: ${{ steps.msrv.outputs.value }} MSRV: ${{ steps.msrv.outputs.value }}
- name: "Install mold" - name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- run: cargo +${MSRV} build --profile no-debug - run: cargo +${MSRV} build
env: env:
MSRV: ${{ steps.msrv.outputs.value }} MSRV: ${{ steps.msrv.outputs.value }}
- run: ./target/no-debug/uv --version - run: ./target/debug/uv --version
build-binary-freebsd: build-binary-freebsd:
needs: determine_changes needs: determine_changes
@ -800,7 +759,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Cross build" - name: "Cross build"
run: | run: |
# Install cross from `freebsd-firecracker` # Install cross from `freebsd-firecracker`
@ -808,7 +767,7 @@ jobs:
chmod +x cross chmod +x cross
mv cross /usr/local/bin/cross mv cross /usr/local/bin/cross
cross build --target x86_64-unknown-freebsd --profile no-debug cross build --target x86_64-unknown-freebsd
- name: Test in Firecracker VM - name: Test in Firecracker VM
uses: acj/freebsd-firecracker-action@a5a3fc1709c5b5368141a5699f10259aca3cd965 # v0.6.0 uses: acj/freebsd-firecracker-action@a5a3fc1709c5b5368141a5699f10259aca3cd965 # v0.6.0
@ -822,8 +781,8 @@ jobs:
cat <<EOF > $include_path cat <<EOF > $include_path
target target
target/x86_64-unknown-freebsd target/x86_64-unknown-freebsd
target/x86_64-unknown-freebsd/no-debug target/x86_64-unknown-freebsd/debug
target/x86_64-unknown-freebsd/no-debug/uv target/x86_64-unknown-freebsd/debug/uv
EOF EOF
rsync -r -e "ssh" \ rsync -r -e "ssh" \
@ -833,7 +792,7 @@ jobs:
--exclude "*" \ --exclude "*" \
. firecracker: . firecracker:
run-in-vm: | run-in-vm: |
mv target/x86_64-unknown-freebsd/no-debug/uv uv mv target/x86_64-unknown-freebsd/debug/uv uv
chmod +x uv chmod +x uv
./uv --version ./uv --version
@ -1873,7 +1832,7 @@ jobs:
run: chmod +x ./uv run: chmod +x ./uv
- name: "Configure AWS credentials" - name: "Configure AWS credentials"
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 uses: aws-actions/configure-aws-credentials@a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 # v5.0.0
with: with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
@ -2032,7 +1991,6 @@ jobs:
UV_TEST_PUBLISH_GITLAB_PAT: ${{ secrets.UV_TEST_PUBLISH_GITLAB_PAT }} 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_CODEBERG_TOKEN: ${{ secrets.UV_TEST_PUBLISH_CODEBERG_TOKEN }}
UV_TEST_PUBLISH_CLOUDSMITH_TOKEN: ${{ secrets.UV_TEST_PUBLISH_CLOUDSMITH_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 }} UV_TEST_PUBLISH_PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
integration-uv-build-backend: integration-uv-build-backend:
@ -2067,22 +2025,22 @@ jobs:
# Test the main path (`build_wheel`) through pip # Test the main path (`build_wheel`) through pip
./uv venv -v --seed ./uv venv -v --seed
./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 -m pip install -v scripts/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())" ./uv run --no-project python -c "from built_by_uv import greet; print(greet())"
# Test both `build_wheel` and `build_sdist` through uv # Test both `build_wheel` and `build_sdist` through uv
./uv venv -c -v ./uv venv -c -v
./uv build -v --force-pep517 test/packages/built-by-uv --find-links crates/uv-build/dist --offline ./uv build -v --force-pep517 scripts/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 pip install -v scripts/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())" ./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` # Test both `build_wheel` and `build_sdist` through the official `build`
rm -rf test/packages/built-by-uv/dist/ rm -rf scripts/packages/built-by-uv/dist/
./uv venv -c -v ./uv venv -c -v
./uv pip install build ./uv pip install build
# Add the uv binary to PATH for `build` to find # 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 test/packages/built-by-uv 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 test/packages/built-by-uv/dist/*.tar.gz --find-links crates/uv-build/dist --offline --no-deps ./uv pip install -v scripts/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())" ./uv run --no-project python -c "from built_by_uv import greet; print(greet())"
cache-test-ubuntu: cache-test-ubuntu:
@ -2924,14 +2882,14 @@ jobs:
runs-on: codspeed-macro runs-on: codspeed-macro
needs: determine_changes 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') }} 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: 25 timeout-minutes: 20
steps: steps:
- name: "Checkout Branch" - name: "Checkout Branch"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
run: rustup show run: rustup show
@ -2946,8 +2904,8 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y libsasl2-dev libldap2-dev libkrb5-dev sudo apt-get install -y libsasl2-dev libldap2-dev libkrb5-dev
cargo run --bin uv -- venv --cache-dir .cache cargo run --bin uv -- venv --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 scripts/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 cargo run --bin uv -- pip compile scripts/requirements/airflow.in --universal --exclude-newer 2024-08-08 --cache-dir .cache
- name: "Build benchmarks" - name: "Build benchmarks"
run: cargo codspeed build --profile profiling -p uv-bench run: cargo codspeed build --profile profiling -p uv-bench
@ -2971,7 +2929,7 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
run: rustup show run: rustup show
@ -2986,8 +2944,8 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y libsasl2-dev libldap2-dev libkrb5-dev sudo apt-get install -y libsasl2-dev libldap2-dev libkrb5-dev
cargo run --bin uv -- venv --cache-dir .cache cargo run --bin uv -- venv --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 scripts/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 cargo run --bin uv -- pip compile scripts/requirements/airflow.in --universal --exclude-newer 2024-08-08 --cache-dir .cache
- name: "Build benchmarks" - name: "Build benchmarks"
run: cargo codspeed build --profile profiling -p uv-bench run: cargo codspeed build --profile profiling -p uv-bench

View File

@ -1,33 +0,0 @@
# 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 }}

View File

@ -36,14 +36,6 @@ jobs:
with: with:
python-version: 3.12 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" - name: "Set docs display name"
run: | run: |
version="${VERSION}" version="${VERSION}"

View File

@ -18,7 +18,8 @@ jobs:
environment: environment:
name: release name: release
permissions: permissions:
id-token: write # For PyPI's trusted publishing # For PyPI's trusted publishing.
id-token: write
steps: steps:
- name: "Install uv" - name: "Install uv"
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
@ -36,7 +37,8 @@ jobs:
environment: environment:
name: release name: release
permissions: permissions:
id-token: write # For PyPI's trusted publishing # For PyPI's trusted publishing.
id-token: write
steps: steps:
- name: "Install uv" - name: "Install uv"
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0

View File

@ -222,31 +222,16 @@ jobs:
"id-token": "write" "id-token": "write"
"packages": "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 # Create a GitHub Release while uploading all files to it
announce: announce:
needs: needs:
- plan - plan
- host - host
- custom-publish-pypi - custom-publish-pypi
- custom-publish-crates
# use "always() && ..." to allow us to wait for all publish jobs while # use "always() && ..." to allow us to wait for all publish jobs while
# still allowing individual publish jobs to skip themselves (for prereleases). # still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed! # "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') && (needs.custom-publish-crates.result == 'skipped' || needs.custom-publish-crates.result == 'success') }} if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') }}
runs-on: "depot-ubuntu-latest-4" runs-on: "depot-ubuntu-latest-4"
permissions: permissions:
"attestations": "write" "attestations": "write"

View File

@ -49,4 +49,3 @@ jobs:
title: "Sync latest Python releases" title: "Sync latest Python releases"
body: "Automated update for Python releases." body: "Automated update for Python releases."
base: "main" base: "main"
draft: true

5
.gitignore vendored
View File

@ -37,11 +37,6 @@ profile.json.gz
# MkDocs # MkDocs
/site /site
# Generated reference docs (use `cargo dev generate-all` to regenerate)
/docs/reference/cli.md
/docs/reference/environment.md
/docs/reference/settings.md
# macOS # macOS
**/.DS_Store **/.DS_Store

View File

@ -4,5 +4,5 @@ PREVIEW-CHANGELOG.md
docs/reference/cli.md docs/reference/cli.md
docs/reference/settings.md docs/reference/settings.md
docs/reference/environment.md docs/reference/environment.md
test/ecosystem/home-assistant-core/LICENSE.md ecosystem/home-assistant-core/LICENSE.md
docs/guides/integration/gitlab.md docs/guides/integration/gitlab.md

View File

@ -3,253 +3,6 @@
<!-- prettier-ignore-start --> <!-- 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 UTF8-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 ## 0.9.9
Released on 2025-11-12. Released on 2025-11-12.
@ -285,6 +38,7 @@ Released on 2025-11-12.
- Fix `CMD` path in FastAPI Dockerfile ([#16701](https://github.com/astral-sh/uv/pull/16701)) - Fix `CMD` path in FastAPI Dockerfile ([#16701](https://github.com/astral-sh/uv/pull/16701))
## 0.9.8 ## 0.9.8
Released on 2025-11-07. Released on 2025-11-07.

View File

@ -1,125 +0,0 @@
# 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

View File

@ -86,13 +86,6 @@ cargo test --package <package> --test <test> -- <test_name> -- --exact
cargo insta review 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 ### Local testing
You can invoke your development version of uv with `cargo run -- <args>`. For example: You can invoke your development version of uv with `cargo run -- <args>`. For example:
@ -102,15 +95,6 @@ cargo run -- venv
cargo run -- pip install requests 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 ## Running inside a Docker container
Source distributions can run arbitrary code on build and can make unwanted modifications to your Source distributions can run arbitrary code on build and can make unwanted modifications to your
@ -136,7 +120,7 @@ Please refer to Ruff's
it applies to uv, too. it applies to uv, too.
We provide diverse sets of requirements for testing and benchmarking the resolver in We provide diverse sets of requirements for testing and benchmarking the resolver in
`test/requirements` and for the installer in `test/requirements/compiled`. `scripts/requirements` and for the installer in `scripts/requirements/compiled`.
You can use `scripts/benchmark` to benchmark predefined workloads between uv versions and with other You can use `scripts/benchmark` to benchmark predefined workloads between uv versions and with other
tools, e.g., from the `scripts/benchmark` directory: tools, e.g., from the `scripts/benchmark` directory:
@ -147,7 +131,7 @@ uv run resolver \
--poetry \ --poetry \
--benchmark \ --benchmark \
resolve-cold \ resolve-cold \
../test/requirements/trio.in ../scripts/requirements/trio.in
``` ```
### Analyzing concurrency ### Analyzing concurrency
@ -157,7 +141,7 @@ visualize parallel requests and find any spots where uv is CPU-bound. Example us
`uv-dev` respectively: `uv-dev` respectively:
```shell ```shell
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 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
``` ```
```shell ```shell

616
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,9 @@ exclude = [
"scripts", "scripts",
# Needs nightly # Needs nightly
"crates/uv-trampoline", "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" resolver = "2"
@ -11,71 +14,71 @@ resolver = "2"
edition = "2024" edition = "2024"
rust-version = "1.89" rust-version = "1.89"
homepage = "https://pypi.org/project/uv/" homepage = "https://pypi.org/project/uv/"
documentation = "https://pypi.org/project/uv/"
repository = "https://github.com/astral-sh/uv" repository = "https://github.com/astral-sh/uv"
authors = ["uv"] authors = ["uv"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
[workspace.dependencies] [workspace.dependencies]
uv-auth = { version = "0.0.8", path = "crates/uv-auth" } uv-auth = { path = "crates/uv-auth" }
uv-bin-install = { version = "0.0.8", path = "crates/uv-bin-install" } uv-bin-install = { path = "crates/uv-bin-install" }
uv-build-backend = { version = "0.0.8", path = "crates/uv-build-backend" } uv-build-backend = { path = "crates/uv-build-backend" }
uv-build-frontend = { version = "0.0.8", path = "crates/uv-build-frontend" } uv-build-frontend = { path = "crates/uv-build-frontend" }
uv-cache = { version = "0.0.8", path = "crates/uv-cache" } uv-cache = { path = "crates/uv-cache" }
uv-cache-info = { version = "0.0.8", path = "crates/uv-cache-info" } uv-cache-info = { path = "crates/uv-cache-info" }
uv-cache-key = { version = "0.0.8", path = "crates/uv-cache-key" } uv-cache-key = { path = "crates/uv-cache-key" }
uv-cli = { version = "0.0.8", path = "crates/uv-cli" } uv-cli = { path = "crates/uv-cli" }
uv-client = { version = "0.0.8", path = "crates/uv-client" } uv-client = { path = "crates/uv-client" }
uv-configuration = { version = "0.0.8", path = "crates/uv-configuration" } uv-configuration = { path = "crates/uv-configuration" }
uv-console = { version = "0.0.8", path = "crates/uv-console" } uv-console = { path = "crates/uv-console" }
uv-dirs = { version = "0.0.8", path = "crates/uv-dirs" } uv-dirs = { path = "crates/uv-dirs" }
uv-dispatch = { version = "0.0.8", path = "crates/uv-dispatch" } uv-dispatch = { path = "crates/uv-dispatch" }
uv-distribution = { version = "0.0.8", path = "crates/uv-distribution" } uv-distribution = { path = "crates/uv-distribution" }
uv-distribution-filename = { version = "0.0.8", path = "crates/uv-distribution-filename" } uv-distribution-filename = { path = "crates/uv-distribution-filename" }
uv-distribution-types = { version = "0.0.8", path = "crates/uv-distribution-types" } uv-distribution-types = { path = "crates/uv-distribution-types" }
uv-extract = { version = "0.0.8", path = "crates/uv-extract" } uv-extract = { path = "crates/uv-extract" }
uv-flags = { version = "0.0.8", path = "crates/uv-flags" } uv-flags = { path = "crates/uv-flags" }
uv-fs = { version = "0.0.8", path = "crates/uv-fs", features = ["serde", "tokio"] } uv-fs = { path = "crates/uv-fs", features = ["serde", "tokio"] }
uv-git = { version = "0.0.8", path = "crates/uv-git" } uv-git = { path = "crates/uv-git" }
uv-git-types = { version = "0.0.8", path = "crates/uv-git-types" } uv-git-types = { path = "crates/uv-git-types" }
uv-globfilter = { version = "0.0.8", path = "crates/uv-globfilter" } uv-globfilter = { path = "crates/uv-globfilter" }
uv-install-wheel = { version = "0.0.8", path = "crates/uv-install-wheel", default-features = false } uv-install-wheel = { path = "crates/uv-install-wheel", default-features = false }
uv-installer = { version = "0.0.8", path = "crates/uv-installer" } uv-installer = { path = "crates/uv-installer" }
uv-keyring = { version = "0.0.8", path = "crates/uv-keyring" } uv-keyring = { path = "crates/uv-keyring" }
uv-logging = { version = "0.0.8", path = "crates/uv-logging" } uv-logging = { path = "crates/uv-logging" }
uv-macros = { version = "0.0.8", path = "crates/uv-macros" } uv-macros = { path = "crates/uv-macros" }
uv-metadata = { version = "0.0.8", path = "crates/uv-metadata" } uv-metadata = { path = "crates/uv-metadata" }
uv-normalize = { version = "0.0.8", path = "crates/uv-normalize" } uv-normalize = { path = "crates/uv-normalize" }
uv-once-map = { version = "0.0.8", path = "crates/uv-once-map" } uv-once-map = { path = "crates/uv-once-map" }
uv-options-metadata = { version = "0.0.8", path = "crates/uv-options-metadata" } uv-options-metadata = { path = "crates/uv-options-metadata" }
uv-performance-memory-allocator = { version = "0.0.8", path = "crates/uv-performance-memory-allocator" } uv-pep440 = { path = "crates/uv-pep440", features = ["tracing", "rkyv", "version-ranges"] }
uv-pep440 = { version = "0.0.8", path = "crates/uv-pep440", features = ["tracing", "rkyv", "version-ranges"] } uv-pep508 = { path = "crates/uv-pep508", features = ["non-pep508-extensions"] }
uv-pep508 = { version = "0.0.8", path = "crates/uv-pep508", features = ["non-pep508-extensions"] } uv-platform = { path = "crates/uv-platform" }
uv-platform = { version = "0.0.8", path = "crates/uv-platform" } uv-platform-tags = { path = "crates/uv-platform-tags" }
uv-platform-tags = { version = "0.0.8", path = "crates/uv-platform-tags" } uv-preview = { path = "crates/uv-preview" }
uv-preview = { version = "0.0.8", path = "crates/uv-preview" } uv-publish = { path = "crates/uv-publish" }
uv-publish = { version = "0.0.8", path = "crates/uv-publish" } uv-pypi-types = { path = "crates/uv-pypi-types" }
uv-pypi-types = { version = "0.0.8", path = "crates/uv-pypi-types" } uv-python = { path = "crates/uv-python" }
uv-python = { version = "0.0.8", path = "crates/uv-python" } uv-redacted = { path = "crates/uv-redacted" }
uv-redacted = { version = "0.0.8", path = "crates/uv-redacted" } uv-requirements = { path = "crates/uv-requirements" }
uv-requirements = { version = "0.0.8", path = "crates/uv-requirements" } uv-requirements-txt = { path = "crates/uv-requirements-txt" }
uv-requirements-txt = { version = "0.0.8", path = "crates/uv-requirements-txt" } uv-resolver = { path = "crates/uv-resolver" }
uv-resolver = { version = "0.0.8", path = "crates/uv-resolver" } uv-scripts = { path = "crates/uv-scripts" }
uv-scripts = { version = "0.0.8", path = "crates/uv-scripts" } uv-settings = { path = "crates/uv-settings" }
uv-settings = { version = "0.0.8", path = "crates/uv-settings" } uv-shell = { path = "crates/uv-shell" }
uv-shell = { version = "0.0.8", path = "crates/uv-shell" } uv-small-str = { path = "crates/uv-small-str" }
uv-small-str = { version = "0.0.8", path = "crates/uv-small-str" } uv-state = { path = "crates/uv-state" }
uv-state = { version = "0.0.8", path = "crates/uv-state" } uv-static = { path = "crates/uv-static" }
uv-static = { version = "0.0.8", path = "crates/uv-static" } uv-tool = { path = "crates/uv-tool" }
uv-tool = { version = "0.0.8", path = "crates/uv-tool" } uv-torch = { path = "crates/uv-torch" }
uv-torch = { version = "0.0.8", path = "crates/uv-torch" } uv-trampoline-builder = { path = "crates/uv-trampoline-builder" }
uv-trampoline-builder = { version = "0.0.8", path = "crates/uv-trampoline-builder" } uv-types = { path = "crates/uv-types" }
uv-types = { version = "0.0.8", path = "crates/uv-types" } uv-version = { path = "crates/uv-version" }
uv-version = { version = "0.9.18", path = "crates/uv-version" } uv-virtualenv = { path = "crates/uv-virtualenv" }
uv-virtualenv = { version = "0.0.8", path = "crates/uv-virtualenv" } uv-warnings = { path = "crates/uv-warnings" }
uv-warnings = { version = "0.0.8", path = "crates/uv-warnings" } uv-workspace = { path = "crates/uv-workspace" }
uv-workspace = { version = "0.0.8", path = "crates/uv-workspace" }
ambient-id = { version = "0.0.7", default-features = false, features = ["astral-reqwest-middleware"] } ambient-id = { version = "0.0.5" }
anstream = { version = "0.6.15" } anstream = { version = "0.6.15" }
anyhow = { version = "1.0.89" } anyhow = { version = "1.0.89" }
arcstr = { version = "1.2.0" } arcstr = { version = "1.2.0" }
@ -84,8 +87,8 @@ astral-tokio-tar = { version = "0.5.6" }
async-channel = { version = "2.3.1" } async-channel = { version = "2.3.1" }
async-compression = { version = "0.4.12", features = ["bzip2", "gzip", "xz", "zstd"] } async-compression = { version = "0.4.12", features = ["bzip2", "gzip", "xz", "zstd"] }
async-trait = { version = "0.1.82" } async-trait = { version = "0.1.82" }
async_http_range_reader = { version = "0.9.1", package = "astral_async_http_range_reader" } async_http_range_reader = { version = "0.9.1" }
async_zip = { version = "0.0.17", package = "astral_async_zip", features = ["bzip2", "deflate", "lzma", "tokio", "xz", "zstd"] } async_zip = { git = "https://github.com/astral-sh/rs-async-zip", rev = "f6a41d32866003c868d03ed791a89c794f61b703", features = ["bzip2", "deflate", "lzma", "tokio", "xz", "zstd"] }
axoupdater = { version = "0.9.0", default-features = false } axoupdater = { version = "0.9.0", default-features = false }
backon = { version = "1.3.0" } backon = { version = "1.3.0" }
base64 = { version = "0.22.1" } base64 = { version = "0.22.1" }
@ -100,17 +103,13 @@ configparser = { version = "3.1.0" }
console = { version = "0.16.0", default-features = false, features = ["std"] } console = { version = "0.16.0", default-features = false, features = ["std"] }
csv = { version = "1.3.0" } csv = { version = "1.3.0" }
ctrlc = { version = "3.4.5" } ctrlc = { version = "3.4.5" }
cyclonedx-bom = { version = "0.8.0" }
dashmap = { version = "6.1.0" } dashmap = { version = "6.1.0" }
data-encoding = { version = "2.6.0" } data-encoding = { version = "2.6.0" }
diskus = { version = "0.9.0", default-features = false }
dotenvy = { version = "0.15.7" } dotenvy = { version = "0.15.7" }
dunce = { version = "1.0.5" } dunce = { version = "1.0.5" }
either = { version = "1.13.0" } either = { version = "1.13.0" }
encoding_rs_io = { version = "0.1.7" } encoding_rs_io = { version = "0.1.7" }
embed-manifest = { version = "1.5.0" }
etcetera = { version = "0.11.0" } etcetera = { version = "0.11.0" }
fastrand = { version = "2.3.0" }
flate2 = { version = "1.0.33", default-features = false, features = ["zlib-rs"] } flate2 = { version = "1.0.33", default-features = false, features = ["zlib-rs"] }
fs-err = { version = "3.0.0", features = ["tokio"] } fs-err = { version = "3.0.0", features = ["tokio"] }
futures = { version = "0.3.30" } futures = { version = "0.3.30" }
@ -135,6 +134,7 @@ memchr = { version = "2.7.4" }
miette = { version = "7.2.0", features = ["fancy-no-backtrace"] } miette = { version = "7.2.0", features = ["fancy-no-backtrace"] }
nanoid = { version = "0.4.0" } nanoid = { version = "0.4.0" }
nix = { version = "0.30.0", features = ["signal"] } nix = { version = "0.30.0", features = ["signal"] }
once_cell = { version = "1.20.2" }
open = { version = "5.3.2" } open = { version = "5.3.2" }
owo-colors = { version = "4.1.0" } owo-colors = { version = "4.1.0" }
path-slash = { version = "0.2.1" } path-slash = { version = "0.2.1" }
@ -143,7 +143,7 @@ percent-encoding = { version = "2.3.1" }
petgraph = { version = "0.8.0" } petgraph = { version = "0.8.0" }
proc-macro2 = { version = "1.0.86" } proc-macro2 = { version = "1.0.86" }
procfs = { version = "0.17.0", default-features = false, features = ["flate2"] } procfs = { version = "0.17.0", default-features = false, features = ["flate2"] }
pubgrub = { version = "0.3.3" , package = "astral-pubgrub" } pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "d8efd77673c9a90792da9da31b6c0da7ea8a324b" }
quote = { version = "1.0.37" } quote = { version = "1.0.37" }
rayon = { version = "1.10.0" } rayon = { version = "1.10.0" }
ref-cast = { version = "1.0.24" } 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"] } 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 } 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 = { 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 = { version = "0.4.2", package = "astral-reqwest-middleware", features = ["multipart"] } reqwest-middleware = { git = "https://github.com/astral-sh/reqwest-middleware", rev = "7650ed76215a962a96d94a79be71c27bffde7ab2", features = ["multipart"] }
reqwest-retry = { version = "0.7.0", package = "astral-reqwest-retry" } reqwest-retry = { git = "https://github.com/astral-sh/reqwest-middleware", rev = "7650ed76215a962a96d94a79be71c27bffde7ab2" }
rkyv = { version = "0.8.8", features = ["bytecheck"] } rkyv = { version = "0.8.8", features = ["bytecheck"] }
rmp-serde = { version = "1.3.0" } rmp-serde = { version = "1.3.0" }
rust-netrc = { version = "0.1.2" } rust-netrc = { version = "0.1.2" }
@ -170,7 +170,7 @@ serde-untagged = { version = "0.1.6" }
serde_json = { version = "1.0.128" } serde_json = { version = "1.0.128" }
sha2 = { version = "0.10.8" } sha2 = { version = "0.10.8" }
smallvec = { version = "1.13.2" } smallvec = { version = "1.13.2" }
spdx = { version = "0.13.0" } spdx = { version = "0.12.0" }
syn = { version = "2.0.77" } syn = { version = "2.0.77" }
sys-info = { version = "0.9.1" } sys-info = { version = "0.9.1" }
tar = { version = "0.4.43" } tar = { version = "0.4.43" }
@ -178,8 +178,8 @@ target-lexicon = { version = "0.13.0" }
tempfile = { version = "3.14.0" } tempfile = { version = "3.14.0" }
textwrap = { version = "0.16.1" } textwrap = { version = "0.16.1" }
thiserror = { version = "2.0.0" } thiserror = { version = "2.0.0" }
astral-tl = { version = "0.7.11" } astral-tl = { version = "0.7.9" }
tokio = { version = "1.40.0", features = ["fs", "io-util", "macros", "process", "rt", "signal", "sync", "time"] } tokio = { version = "1.40.0", features = ["fs", "io-util", "macros", "process", "rt", "signal", "sync"] }
tokio-stream = { version = "0.1.16" } tokio-stream = { version = "0.1.16" }
tokio-util = { version = "0.7.12", features = ["compat", "io"] } tokio-util = { version = "0.7.12", features = ["compat", "io"] }
toml = { version = "0.9.2", features = ["fast_hash"] } toml = { version = "0.9.2", features = ["fast_hash"] }
@ -193,7 +193,7 @@ unicode-width = { version = "0.2.0" }
unscanny = { version = "0.1.0" } unscanny = { version = "0.1.0" }
url = { version = "2.5.2", features = ["serde"] } url = { version = "2.5.2", features = ["serde"] }
uuid = { version = "1.16.0" } uuid = { version = "1.16.0" }
version-ranges = { version = "0.1.3", package = "astral-version-ranges" } version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "d8efd77673c9a90792da9da31b6c0da7ea8a324b" }
walkdir = { version = "2.5.0" } walkdir = { version = "2.5.0" }
which = { version = "8.0.0", features = ["regex"] } 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"] } 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,19 +212,20 @@ byteorder = { version = "1.5.0" }
filetime = { version = "0.2.25" } filetime = { version = "0.2.25" }
http-body-util = { version = "0.1.2" } http-body-util = { version = "0.1.2" }
hyper = { version = "1.4.1", features = ["server", "http1"] } hyper = { version = "1.4.1", features = ["server", "http1"] }
hyper-util = { version = "0.1.8", features = ["tokio", "server", "http1"] } hyper-util = { version = "0.1.8", features = ["tokio"] }
ignore = { version = "0.4.23" } ignore = { version = "0.4.23" }
insta = { version = "1.40.0", features = ["json", "filters", "redactions"] } insta = { version = "1.40.0", features = ["json", "filters", "redactions"] }
predicates = { version = "3.1.2" } predicates = { version = "3.1.2" }
rcgen = { version = "0.14.5", features = ["crypto", "pem", "ring"], default-features = false } 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" } similar = { version = "2.6.0" }
temp-env = { version = "0.3.6" } temp-env = { version = "0.3.6" }
test-case = { version = "3.3.1" } test-case = { version = "3.3.1" }
test-log = { version = "0.2.16", features = ["trace"], default-features = false } 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" } whoami = { version = "1.6.0" }
[workspace.metadata.cargo-shear]
ignored = ["flate2", "xz2", "h2"]
[workspace.lints.rust] [workspace.lints.rust]
unsafe_code = "warn" unsafe_code = "warn"
unreachable_pub = "warn" unreachable_pub = "warn"
@ -310,21 +311,12 @@ strip = false
debug = "full" debug = "full"
lto = false 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] [profile.fast-build]
inherits = "dev" inherits = "dev"
opt-level = 1 opt-level = 1
debug = 0 debug = 0
strip = "debuginfo" 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 to build a minimally sized binary for uv-build
[profile.minimal-size] [profile.minimal-size]
inherits = "release" inherits = "release"
@ -336,3 +328,7 @@ codegen-units = 1
# The profile that 'cargo dist' will build with. # The profile that 'cargo dist' will build with.
[profile.dist] [profile.dist]
inherits = "release" 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" }

View File

@ -42,7 +42,7 @@ An extremely fast Python package and project manager, written in Rust.
- 🖥️ Supports macOS, Linux, and Windows. - 🖥️ Supports macOS, Linux, and Windows.
uv is backed by [Astral](https://astral.sh), the creators of uv is backed by [Astral](https://astral.sh), the creators of
[Ruff](https://github.com/astral-sh/ruff) and [ty](https://github.com/astral-sh/ty). [Ruff](https://github.com/astral-sh/ruff).
## Installation ## Installation
@ -192,12 +192,14 @@ uv installs Python and allows quickly switching between versions.
Install multiple Python versions: Install multiple Python versions:
```console ```console
$ uv python install 3.12 3.13 3.14 $ uv python install 3.10 3.11 3.12
Installed 3 versions in 972ms Searching for Python versions matching: Python 3.10
+ cpython-3.12.12-macos-aarch64-none (python3.12) Searching for Python versions matching: Python 3.11
+ cpython-3.13.9-macos-aarch64-none (python3.13) Searching for Python versions matching: Python 3.12
+ cpython-3.14.0-macos-aarch64-none (python3.14) 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
``` ```
Download Python versions as needed: Download Python versions as needed:
@ -268,6 +270,14 @@ Installed 43 packages in 208ms
See the [pip interface documentation](https://docs.astral.sh/uv/pip/index/) to get started. 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 ## Contributing
We are passionate about supporting contributors of all levels of experience and would love to see We are passionate about supporting contributors of all levels of experience and would love to see
@ -284,15 +294,6 @@ 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. 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 ## Acknowledgements
uv's dependency resolver uses [PubGrub](https://github.com/pubgrub-rs/pubgrub) under the hood. We're uv's dependency resolver uses [PubGrub](https://github.com/pubgrub-rs/pubgrub) under the hood. We're

View File

@ -1,8 +1,8 @@
[files] [files]
extend-exclude = [ extend-exclude = [
"**/snapshots/", "**/snapshots/",
"test/ecosystem/**", "ecosystem/**",
"test/requirements/**/*.in", "scripts/**/*.in",
"crates/uv-build-frontend/src/pipreqs/mapping", "crates/uv-build-frontend/src/pipreqs/mapping",
] ]
ignore-hidden = false ignore-hidden = false

View File

@ -1,10 +1,10 @@
[package] [package]
name = "uv-auth" name = "uv-auth"
version = "0.0.8" version = "0.0.1"
description = "This is an internal component crate of uv"
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
homepage = { workspace = true } homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }

View File

@ -1,13 +0,0 @@
<!-- 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.

View File

@ -11,8 +11,8 @@ use url::Url;
use uv_once_map::OnceMap; use uv_once_map::OnceMap;
use uv_redacted::DisplaySafeUrl; use uv_redacted::DisplaySafeUrl;
use crate::Realm;
use crate::credentials::{Authentication, Username}; use crate::credentials::{Authentication, Username};
use crate::{Credentials, Realm};
type FxOnceMap<K, V> = OnceMap<K, V, BuildHasherDefault<FxHasher>>; type FxOnceMap<K, V> = OnceMap<K, V, BuildHasherDefault<FxHasher>>;
@ -33,7 +33,6 @@ impl Display for FetchUrl {
} }
} }
#[derive(Debug)] // All internal types are redacted.
pub struct CredentialsCache { pub struct CredentialsCache {
/// A cache per realm and username /// A cache per realm and username
realms: RwLock<FxHashMap<(Realm, Username), Arc<Authentication>>>, realms: RwLock<FxHashMap<(Realm, Username), Arc<Authentication>>>,
@ -59,27 +58,6 @@ 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. /// Return the credentials that should be used for a realm and username, if any.
pub(crate) fn get_realm( pub(crate) fn get_realm(
&self, &self,

View File

@ -1,5 +1,12 @@
use std::sync::{Arc, LazyLock};
use tracing::trace;
use uv_redacted::DisplaySafeUrl;
use crate::credentials::Authentication;
pub use access_token::AccessToken; pub use access_token::AccessToken;
pub use cache::CredentialsCache; use cache::CredentialsCache;
pub use credentials::{Credentials, Username}; pub use credentials::{Credentials, Username};
pub use index::{AuthPolicy, Index, Indexes}; pub use index::{AuthPolicy, Index, Indexes};
pub use keyring::KeyringProvider; pub use keyring::KeyringProvider;
@ -22,3 +29,32 @@ mod pyx;
mod realm; mod realm;
mod service; mod service;
mod store; 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)));
}

View File

@ -10,24 +10,19 @@ use tracing::{debug, trace, warn};
use uv_preview::{Preview, PreviewFeatures}; use uv_preview::{Preview, PreviewFeatures};
use uv_redacted::DisplaySafeUrl; use uv_redacted::DisplaySafeUrl;
use uv_static::EnvVars;
use uv_warnings::owo_colors::OwoColorize; use uv_warnings::owo_colors::OwoColorize;
use crate::credentials::Authentication; use crate::credentials::Authentication;
use crate::providers::{HuggingFaceProvider, S3EndpointProvider}; use crate::providers::{HuggingFaceProvider, S3EndpointProvider};
use crate::pyx::{DEFAULT_TOLERANCE_SECS, PyxTokenStore}; use crate::pyx::{DEFAULT_TOLERANCE_SECS, PyxTokenStore};
use crate::{ use crate::{
AccessToken, CredentialsCache, KeyringProvider, AccessToken, CREDENTIALS_CACHE, CredentialsCache, KeyringProvider,
cache::FetchUrl, cache::FetchUrl,
credentials::{Credentials, Username}, credentials::{Credentials, Username},
index::{AuthPolicy, Indexes}, index::{AuthPolicy, Indexes},
realm::Realm, realm::Realm,
}; };
use crate::{Index, TextCredentialStore}; use crate::{Index, TextCredentialStore, TomlCredentialError};
/// 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. /// Strategy for loading netrc files.
enum NetrcMode { enum NetrcMode {
@ -65,55 +60,49 @@ impl NetrcMode {
/// Strategy for loading text-based credential files. /// Strategy for loading text-based credential files.
enum TextStoreMode { enum TextStoreMode {
Automatic(tokio::sync::OnceCell<Option<TextCredentialStore>>), Automatic(LazyLock<Option<TextCredentialStore>>),
Enabled(TextCredentialStore), Enabled(TextCredentialStore),
Disabled, Disabled,
} }
impl Default for TextStoreMode { impl Default for TextStoreMode {
fn default() -> Self { fn default() -> Self {
Self::Automatic(tokio::sync::OnceCell::new()) // 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
}
}
}))
} }
} }
impl TextStoreMode { 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. /// Get the parsed credential store, if enabled.
async fn get(&self) -> Option<&TextCredentialStore> { fn get(&self) -> Option<&TextCredentialStore> {
match self { match self {
// TODO(zanieb): Reconsider this pattern. We're just mirroring the [`NetrcMode`] Self::Automatic(lock) => lock.as_ref(),
// implementation for now.
Self::Automatic(lock) => lock.get_or_init(Self::load_default_store).await.as_ref(),
Self::Enabled(store) => Some(store), Self::Enabled(store) => Some(store),
Self::Disabled => None, Self::Disabled => None,
} }
@ -129,15 +118,6 @@ enum TokenState {
Initialized(Option<AccessToken>), 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. /// A middleware that adds basic authentication to requests.
/// ///
/// Uses a cache to propagate credentials from previously seen requests and /// Uses a cache to propagate credentials from previously seen requests and
@ -146,8 +126,7 @@ pub struct AuthMiddleware {
netrc: NetrcMode, netrc: NetrcMode,
text_store: TextStoreMode, text_store: TextStoreMode,
keyring: Option<KeyringProvider>, keyring: Option<KeyringProvider>,
/// Global authentication cache for a uv invocation to share credentials across uv clients. cache: Option<CredentialsCache>,
cache: Arc<CredentialsCache>,
/// Auth policies for specific URLs. /// Auth policies for specific URLs.
indexes: Indexes, indexes: Indexes,
/// Set all endpoints as needing authentication. We never try to send an /// Set all endpoints as needing authentication. We never try to send an
@ -159,31 +138,21 @@ pub struct AuthMiddleware {
pyx_token_store: Option<PyxTokenStore>, pyx_token_store: Option<PyxTokenStore>,
/// Tokens to use for persistent credentials. /// Tokens to use for persistent credentials.
pyx_token_state: Mutex<TokenState>, pyx_token_state: Mutex<TokenState>,
/// Cached S3 credentials to avoid running the credential helper multiple times.
s3_credential_state: Mutex<S3CredentialState>,
preview: Preview, preview: Preview,
} }
impl Default for AuthMiddleware {
fn default() -> Self {
Self::new()
}
}
impl AuthMiddleware { impl AuthMiddleware {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
netrc: NetrcMode::default(), netrc: NetrcMode::default(),
text_store: TextStoreMode::default(), text_store: TextStoreMode::default(),
keyring: None, keyring: None,
// TODO(konsti): There shouldn't be a credential cache without that in the initializer. cache: None,
cache: Arc::new(CredentialsCache::default()),
indexes: Indexes::new(), indexes: Indexes::new(),
only_authenticated: false, only_authenticated: false,
base_client: None, base_client: None,
pyx_token_store: None, pyx_token_store: None,
pyx_token_state: Mutex::new(TokenState::Uninitialized), pyx_token_state: Mutex::new(TokenState::Uninitialized),
s3_credential_state: Mutex::new(S3CredentialState::Uninitialized),
preview: Preview::default(), preview: Preview::default(),
} }
} }
@ -231,14 +200,7 @@ impl AuthMiddleware {
/// Configure the [`CredentialsCache`] to use. /// Configure the [`CredentialsCache`] to use.
#[must_use] #[must_use]
pub fn with_cache(mut self, cache: CredentialsCache) -> Self { pub fn with_cache(mut self, cache: CredentialsCache) -> Self {
self.cache = Arc::new(cache); self.cache = Some(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 self
} }
@ -271,9 +233,17 @@ impl AuthMiddleware {
self self
} }
/// Global authentication cache for a uv invocation to share credentials across uv clients. /// Get the configured authentication store.
///
/// If not set, the global store is used.
fn cache(&self) -> &CredentialsCache { fn cache(&self) -> &CredentialsCache {
&self.cache self.cache.as_ref().unwrap_or(&CREDENTIALS_CACHE)
}
}
impl Default for AuthMiddleware {
fn default() -> Self {
Self::new()
} }
} }
@ -382,15 +352,11 @@ impl Middleware for AuthMiddleware {
.is_some_and(|token_store| token_store.is_known_url(request.url())); .is_some_and(|token_store| token_store.is_known_url(request.url()));
let must_authenticate = self.only_authenticated let must_authenticate = self.only_authenticated
|| (match auth_policy { || match auth_policy {
AuthPolicy::Auto => is_known_url, AuthPolicy::Auto => is_known_url,
AuthPolicy::Always => true, AuthPolicy::Always => true,
AuthPolicy::Never => false, 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 (mut retry_request, response) = if !must_authenticate {
let url = tracing_url(&request, credentials.as_deref()); let url = tracing_url(&request, credentials.as_deref());
@ -690,26 +656,13 @@ impl AuthMiddleware {
return Some(credentials); return Some(credentials);
} }
if S3EndpointProvider::is_s3_endpoint(url, self.preview) { if let Some(credentials) = S3EndpointProvider::credentials_for(url, self.preview)
let mut s3_state = self.s3_credential_state.lock().await; .map(Authentication::from)
.map(Arc::new)
// If the S3 credential state is uninitialized, initialize it. {
let credentials = match &*s3_state { debug!("Found S3 credentials for {url}");
S3CredentialState::Uninitialized => { self.cache().fetches.done(key, Some(credentials.clone()));
trace!("Initializing S3 credentials for {url}"); return Some(credentials);
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. // If this is a known URL, authenticate it via the token store.
@ -767,16 +720,9 @@ impl AuthMiddleware {
Some(credentials) Some(credentials)
// Text credential store support. // Text credential store support.
} else if let Some(credentials) = self.text_store.get().await.and_then(|text_store| { } else if let Some(credentials) = self.text_store.get().and_then(|text_store| {
debug!("Checking text store for credentials for {url}"); debug!("Checking text store for credentials for {url}");
text_store text_store.get_credentials(url, credentials.as_ref().and_then(|credentials| credentials.username())).cloned()
.get_credentials(
url,
credentials
.as_ref()
.and_then(|credentials| credentials.username()),
)
.cloned()
}) { }) {
debug!("Found credentials in plaintext store for {url}"); debug!("Found credentials in plaintext store for {url}");
Some(credentials) Some(credentials)
@ -792,16 +738,10 @@ impl AuthMiddleware {
if let Some(index) = index { if let Some(index) = index {
// N.B. The native store performs an exact look up right now, so we use the root // 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. // URL of the index instead of relying on prefix-matching.
debug!( debug!("Checking native store for credentials for index URL {}{}", display_username, index.root_url);
"Checking native store for credentials for index URL {}{}",
display_username, index.root_url
);
native_store.fetch(&index.root_url, username).await native_store.fetch(&index.root_url, username).await
} else { } else {
debug!( debug!("Checking native store for credentials for URL {}{}", display_username, url);
"Checking native store for credentials for URL {}{}",
display_username, url
);
native_store.fetch(url, username).await native_store.fetch(url, username).await
} }
// TODO(zanieb): We should have a realm fallback here too // TODO(zanieb): We should have a realm fallback here too
@ -822,18 +762,10 @@ impl AuthMiddleware {
// always authenticate. // always authenticate.
if let Some(username) = credentials.and_then(|credentials| credentials.username()) { if let Some(username) = credentials.and_then(|credentials| credentials.username()) {
if let Some(index) = index { if let Some(index) = index {
debug!( debug!("Checking keyring for credentials for index URL {}@{}", username, index.url);
"Checking keyring for credentials for index URL {}@{}", keyring.fetch(DisplaySafeUrl::ref_cast(&index.url), Some(username)).await
username, index.url
);
keyring
.fetch(DisplaySafeUrl::ref_cast(&index.url), Some(username))
.await
} else { } else {
debug!( debug!("Checking keyring for credentials for full URL {}@{}", username, url);
"Checking keyring for credentials for full URL {}@{}",
username, url
);
keyring.fetch(url, Some(username)).await keyring.fetch(url, Some(username)).await
} }
} else if matches!(auth_policy, AuthPolicy::Always) { } else if matches!(auth_policy, AuthPolicy::Always) {
@ -842,16 +774,12 @@ impl AuthMiddleware {
"Checking keyring for credentials for index URL {} without username due to `authenticate = always`", "Checking keyring for credentials for index URL {} without username due to `authenticate = always`",
index.url index.url
); );
keyring keyring.fetch(DisplaySafeUrl::ref_cast(&index.url), None).await
.fetch(DisplaySafeUrl::ref_cast(&index.url), None)
.await
} else { } else {
None None
} }
} else { } else {
debug!( debug!("Skipping keyring fetch for {url} without username; use `authenticate = always` to force");
"Skipping keyring fetch for {url} without username; use `authenticate = always` to force"
);
None None
} }
} }
@ -861,9 +789,9 @@ impl AuthMiddleware {
Some(credentials) Some(credentials)
} else { } else {
None None
}; }
.map(Authentication::from)
let credentials = credentials.map(Authentication::from).map(Arc::new); .map(Arc::new);
// Register the fetch for this key // Register the fetch for this key
self.cache().fetches.done(key, credentials.clone()); self.cache().fetches.done(key, credentials.clone());

View File

@ -66,8 +66,8 @@ static S3_ENDPOINT_REALM: LazyLock<Option<Realm>> = LazyLock::new(|| {
pub(crate) struct S3EndpointProvider; pub(crate) struct S3EndpointProvider;
impl S3EndpointProvider { impl S3EndpointProvider {
/// Returns `true` if the URL matches the configured S3 endpoint. /// Returns the credentials for the S3 endpoint, if available.
pub(crate) fn is_s3_endpoint(url: &Url, preview: Preview) -> bool { pub(crate) fn credentials_for(url: &Url, preview: Preview) -> Option<DefaultSigner> {
if let Some(s3_endpoint_realm) = S3_ENDPOINT_REALM.as_ref().map(RealmRef::from) { if let Some(s3_endpoint_realm) = S3_ENDPOINT_REALM.as_ref().map(RealmRef::from) {
if !preview.is_enabled(PreviewFeatures::S3_ENDPOINT) { if !preview.is_enabled(PreviewFeatures::S3_ENDPOINT) {
warn_user_once!( warn_user_once!(
@ -79,26 +79,19 @@ impl S3EndpointProvider {
// Treat any URL on the same domain or subdomain as available for S3 signing. // Treat any URL on the same domain or subdomain as available for S3 signing.
let realm = RealmRef::from(url); let realm = RealmRef::from(url);
if realm == s3_endpoint_realm || realm.is_subdomain_of(s3_endpoint_realm) { if realm == s3_endpoint_realm || realm.is_subdomain_of(s3_endpoint_realm) {
return true; // 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", &region);
return Some(signer);
} }
} }
false None
}
/// 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", &region)
} }
} }

View File

@ -5,7 +5,7 @@ use fs_err as fs;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use uv_fs::{LockedFile, LockedFileError, LockedFileMode, with_added_extension}; use uv_fs::{LockedFile, with_added_extension};
use uv_preview::{Preview, PreviewFeatures}; use uv_preview::{Preview, PreviewFeatures};
use uv_redacted::DisplaySafeUrl; use uv_redacted::DisplaySafeUrl;
@ -28,7 +28,7 @@ pub enum AuthBackend {
} }
impl AuthBackend { impl AuthBackend {
pub async fn from_settings(preview: Preview) -> Result<Self, TomlCredentialError> { pub fn from_settings(preview: Preview) -> Result<Self, TomlCredentialError> {
// If preview is enabled, we'll use the system-native store // If preview is enabled, we'll use the system-native store
if preview.is_enabled(PreviewFeatures::NATIVE_AUTH) { if preview.is_enabled(PreviewFeatures::NATIVE_AUTH) {
return Ok(Self::System(KeyringProvider::native())); return Ok(Self::System(KeyringProvider::native()));
@ -36,16 +36,12 @@ impl AuthBackend {
// Otherwise, we'll use the plaintext credential store // Otherwise, we'll use the plaintext credential store
let path = TextCredentialStore::default_file()?; let path = TextCredentialStore::default_file()?;
match TextCredentialStore::read(&path).await { match TextCredentialStore::read(&path) {
Ok((store, lock)) => Ok(Self::TextStore(store, lock)), Ok((store, lock)) => Ok(Self::TextStore(store, lock)),
Err(err) Err(TomlCredentialError::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
if err
.as_io_error()
.is_some_and(|err| err.kind() == std::io::ErrorKind::NotFound) =>
{
Ok(Self::TextStore( Ok(Self::TextStore(
TextCredentialStore::default(), TextCredentialStore::default(),
TextCredentialStore::lock(&path).await?, TextCredentialStore::lock(&path)?,
)) ))
} }
Err(err) => Err(err), Err(err) => Err(err),
@ -73,8 +69,6 @@ pub enum AuthScheme {
pub enum TomlCredentialError { pub enum TomlCredentialError {
#[error(transparent)] #[error(transparent)]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error(transparent)]
LockedFile(#[from] LockedFileError),
#[error("Failed to parse TOML credential file: {0}")] #[error("Failed to parse TOML credential file: {0}")]
ParseError(#[from] toml::de::Error), ParseError(#[from] toml::de::Error),
#[error("Failed to serialize credentials to TOML")] #[error("Failed to serialize credentials to TOML")]
@ -89,21 +83,6 @@ pub enum TomlCredentialError {
TokenNotUnicode(#[from] std::string::FromUtf8Error), 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)] #[derive(Debug, Error)]
pub enum BasicAuthError { pub enum BasicAuthError {
#[error("`username` is required with `scheme = basic`")] #[error("`username` is required with `scheme = basic`")]
@ -254,12 +233,12 @@ impl TextCredentialStore {
} }
/// Acquire a lock on the credentials file at the given path. /// Acquire a lock on the credentials file at the given path.
pub async fn lock(path: &Path) -> Result<LockedFile, TomlCredentialError> { pub fn lock(path: &Path) -> Result<LockedFile, TomlCredentialError> {
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?; fs::create_dir_all(parent)?;
} }
let lock = with_added_extension(path, ".lock"); let lock = with_added_extension(path, ".lock");
Ok(LockedFile::acquire(lock, LockedFileMode::Exclusive, "credentials store").await?) Ok(LockedFile::acquire_blocking(lock, "credentials store")?)
} }
/// Read credentials from a file. /// Read credentials from a file.
@ -290,8 +269,8 @@ impl TextCredentialStore {
/// Returns [`TextCredentialStore`] and a [`LockedFile`] to hold if mutating the store. /// 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. /// If the store will not be written to following the read, the lock can be dropped.
pub async fn read<P: AsRef<Path>>(path: P) -> Result<(Self, LockedFile), TomlCredentialError> { pub fn read<P: AsRef<Path>>(path: P) -> Result<(Self, LockedFile), TomlCredentialError> {
let lock = Self::lock(path.as_ref()).await?; let lock = Self::lock(path.as_ref())?;
let store = Self::from_file(path)?; let store = Self::from_file(path)?;
Ok((store, lock)) Ok((store, lock))
} }
@ -471,8 +450,8 @@ mod tests {
assert!(store.get_credentials(&url, None).is_none()); assert!(store.get_credentials(&url, None).is_none());
} }
#[tokio::test] #[test]
async fn test_file_operations() { fn test_file_operations() {
let mut temp_file = NamedTempFile::new().unwrap(); let mut temp_file = NamedTempFile::new().unwrap();
writeln!( writeln!(
temp_file, temp_file,
@ -508,7 +487,7 @@ password = "pass2"
store store
.write( .write(
temp_output.path(), temp_output.path(),
TextCredentialStore::lock(temp_file.path()).await.unwrap(), TextCredentialStore::lock(temp_file.path()).unwrap(),
) )
.unwrap(); .unwrap();

View File

@ -1,12 +1,13 @@
[package] [package]
name = "uv-bench" name = "uv-bench"
version = "0.0.8" version = "0.0.0"
description = "This is an internal component crate of uv" description = "uv Micro-benchmarks"
publish = false publish = false
authors = { workspace = true } authors = { workspace = true }
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
homepage = { workspace = true } homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
license = { workspace = true } license = { workspace = true }
@ -22,14 +23,14 @@ name = "uv"
path = "benches/uv.rs" path = "benches/uv.rs"
harness = false harness = false
[dev-dependencies] [dependencies]
uv-cache = { workspace = true } uv-cache = { workspace = true }
uv-client = { workspace = true } uv-client = { workspace = true }
uv-configuration = { workspace = true } uv-configuration = { workspace = true }
uv-dispatch = { workspace = true } uv-dispatch = { workspace = true }
uv-distribution = { workspace = true } uv-distribution = { workspace = true }
uv-distribution-types = { workspace = true } uv-distribution-types = { workspace = true }
uv-extract = { workspace = true } uv-extract = { workspace = true, optional = true }
uv-install-wheel = { workspace = true } uv-install-wheel = { workspace = true }
uv-pep440 = { workspace = true } uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true } uv-pep508 = { workspace = true }

View File

@ -1,13 +0,0 @@
<!-- 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.

View File

@ -59,10 +59,7 @@ fn setup(manifest: Manifest) -> impl Fn(bool) {
.build() .build()
.unwrap(); .unwrap();
let cache = Cache::from_path("../../.cache") let cache = Cache::from_path("../../.cache").init().unwrap();
.init_no_wait()
.expect("No cache contention when running benchmarks")
.unwrap();
let interpreter = PythonEnvironment::from_root("../../.venv", &cache) let interpreter = PythonEnvironment::from_root("../../.venv", &cache)
.unwrap() .unwrap()
.into_interpreter(); .into_interpreter();
@ -134,7 +131,7 @@ mod resolver {
); );
static TAGS: LazyLock<Tags> = LazyLock::new(|| { static TAGS: LazyLock<Tags> = LazyLock::new(|| {
Tags::from_env(&PLATFORM, (3, 11), "cpython", (3, 11), false, false, false).unwrap() Tags::from_env(&PLATFORM, (3, 11), "cpython", (3, 11), false, false).unwrap()
}); });
pub(crate) async fn resolve( pub(crate) async fn resolve(

View File

@ -1,13 +1,14 @@
[package] [package]
name = "uv-bin-install" name = "uv-bin-install"
version = "0.0.8" version = "0.0.1"
description = "This is an internal component crate of uv"
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
homepage = { workspace = true } homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }
description = "Binary download and installation utilities for uv"
[lib] [lib]
doctest = false doctest = false
@ -23,7 +24,6 @@ uv-extract = { workspace = true }
uv-pep440 = { workspace = true } uv-pep440 = { workspace = true }
uv-platform = { workspace = true } uv-platform = { workspace = true }
uv-redacted = { workspace = true } uv-redacted = { workspace = true }
fs-err = { workspace = true, features = ["tokio"] } fs-err = { workspace = true, features = ["tokio"] }
futures = { workspace = true } futures = { workspace = true }
reqwest = { workspace = true } reqwest = { workspace = true }

View File

@ -1,13 +0,0 @@
<!-- 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.

View File

@ -19,7 +19,7 @@ use tracing::debug;
use url::Url; use url::Url;
use uv_distribution_filename::SourceDistExtension; use uv_distribution_filename::SourceDistExtension;
use uv_cache::{Cache, CacheBucket, CacheEntry, Error as CacheError}; use uv_cache::{Cache, CacheBucket, CacheEntry};
use uv_client::{BaseClient, is_transient_network_error}; use uv_client::{BaseClient, is_transient_network_error};
use uv_extract::{Error as ExtractError, stream}; use uv_extract::{Error as ExtractError, stream};
use uv_pep440::Version; use uv_pep440::Version;
@ -135,9 +135,6 @@ pub enum Error {
#[error(transparent)] #[error(transparent)]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error(transparent)]
Cache(#[from] CacheError),
#[error("Failed to detect platform")] #[error("Failed to detect platform")]
Platform(#[from] uv_platform::Error), Platform(#[from] uv_platform::Error),

View File

@ -1,10 +1,10 @@
[package] [package]
name = "uv-build-backend" name = "uv-build-backend"
version = "0.0.8" version = "0.1.0"
description = "This is an internal component crate of uv"
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
homepage = { workspace = true } homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }

View File

@ -1,13 +0,0 @@
<!-- 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.

View File

@ -1,4 +1,3 @@
use itertools::Itertools;
mod metadata; mod metadata;
mod serde_verbatim; mod serde_verbatim;
mod settings; mod settings;
@ -8,10 +7,8 @@ mod wheel;
pub use metadata::{PyProjectToml, check_direct_build}; pub use metadata::{PyProjectToml, check_direct_build};
pub use settings::{BuildBackendSettings, WheelDataIncludes}; pub use settings::{BuildBackendSettings, WheelDataIncludes};
pub use source_dist::{build_source_dist, list_source_dist}; 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}; pub use wheel::{build_editable, build_wheel, list_wheel, metadata};
use std::collections::HashSet;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -32,9 +29,9 @@ use crate::settings::ModuleName;
pub enum Error { pub enum Error {
#[error(transparent)] #[error(transparent)]
Io(#[from] io::Error), Io(#[from] io::Error),
#[error("Invalid metadata format in: {}", _0.user_display())] #[error("Invalid pyproject.toml")]
Toml(PathBuf, #[source] toml::de::Error), Toml(#[from] toml::de::Error),
#[error("Invalid project metadata")] #[error("Invalid pyproject.toml")]
Validation(#[from] ValidationError), Validation(#[from] ValidationError),
#[error("Invalid module name: {0}")] #[error("Invalid module name: {0}")]
InvalidModuleName(String, #[source] IdentifierParseError), InvalidModuleName(String, #[source] IdentifierParseError),
@ -194,60 +191,6 @@ fn check_metadata_directory(
Ok(()) 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 /// Returns the source root and the module path(s) with the `__init__.py[i]` below to it while
/// checking the project layout and names. /// checking the project layout and names.
/// ///
@ -270,7 +213,6 @@ fn find_roots(
relative_module_root: &Path, relative_module_root: &Path,
module_name: Option<&ModuleName>, module_name: Option<&ModuleName>,
namespace: bool, namespace: bool,
show_warnings: bool,
) -> Result<(PathBuf, Vec<PathBuf>), Error> { ) -> Result<(PathBuf, Vec<PathBuf>), Error> {
let relative_module_root = uv_fs::normalize_path(relative_module_root); 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. // Check that even if a path contains `..`, we only include files below the module root.
@ -289,8 +231,8 @@ fn find_roots(
ModuleName::Name(name) => { ModuleName::Name(name) => {
vec![name.split('.').collect::<PathBuf>()] vec![name.split('.').collect::<PathBuf>()]
} }
ModuleName::Names(names) => prune_redundant_modules_warn(names, show_warnings) ModuleName::Names(names) => names
.into_iter() .iter()
.map(|name| name.split('.').collect::<PathBuf>()) .map(|name| name.split('.').collect::<PathBuf>())
.collect(), .collect(),
} }
@ -308,9 +250,9 @@ fn find_roots(
let modules_relative = if let Some(module_name) = module_name { let modules_relative = if let Some(module_name) = module_name {
match module_name { match module_name {
ModuleName::Name(name) => vec![module_path_from_module_name(&src_root, name)?], ModuleName::Name(name) => vec![module_path_from_module_name(&src_root, name)?],
ModuleName::Names(names) => prune_redundant_modules_warn(names, show_warnings) ModuleName::Names(names) => names
.into_iter() .iter()
.map(|name| module_path_from_module_name(&src_root, &name)) .map(|name| module_path_from_module_name(&src_root, name))
.collect::<Result<_, _>>()?, .collect::<Result<_, _>>()?,
} }
} else { } else {
@ -478,20 +420,19 @@ mod tests {
fn build(source_root: &Path, dist: &Path) -> Result<BuildResults, Error> { 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 // 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. // 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, false)?; 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, false)?; let direct_wheel_filename = build_wheel(source_root, dist, None, MOCK_UV_VERSION)?;
let direct_wheel_path = dist.join(direct_wheel_filename.to_string()); let direct_wheel_path = dist.join(direct_wheel_filename.to_string());
let direct_wheel_contents = wheel_contents(&direct_wheel_path); let direct_wheel_contents = wheel_contents(&direct_wheel_path);
let direct_wheel_hash = sha2::Sha256::digest(fs_err::read(&direct_wheel_path)?); let direct_wheel_hash = sha2::Sha256::digest(fs_err::read(&direct_wheel_path)?);
fs_err::remove_file(&direct_wheel_path)?; fs_err::remove_file(&direct_wheel_path)?;
// Build a source distribution. // Build a source distribution.
let (_name, source_dist_list_files) = let (_name, source_dist_list_files) = list_source_dist(source_root, MOCK_UV_VERSION)?;
list_source_dist(source_root, MOCK_UV_VERSION, false)?;
// TODO(konsti): This should run in the unpacked source dist tempdir, but we need to // TODO(konsti): This should run in the unpacked source dist tempdir, but we need to
// normalize the path. // normalize the path.
let (_name, wheel_list_files) = list_wheel(source_root, MOCK_UV_VERSION, false)?; 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, false)?; let source_dist_filename = build_source_dist(source_root, dist, MOCK_UV_VERSION)?;
let source_dist_path = dist.join(source_dist_filename.to_string()); let source_dist_path = dist.join(source_dist_filename.to_string());
let source_dist_contents = sdist_contents(&source_dist_path); let source_dist_contents = sdist_contents(&source_dist_path);
@ -505,13 +446,7 @@ mod tests {
source_dist_filename.name.as_dist_info_name(), source_dist_filename.name.as_dist_info_name(),
source_dist_filename.version source_dist_filename.version
)); ));
let wheel_filename = build_wheel( let wheel_filename = build_wheel(&sdist_top_level_directory, dist, None, MOCK_UV_VERSION)?;
&sdist_top_level_directory,
dist,
None,
MOCK_UV_VERSION,
false,
)?;
let wheel_contents = wheel_contents(&dist.join(wheel_filename.to_string())); let wheel_contents = wheel_contents(&dist.join(wheel_filename.to_string()));
// Check that direct and indirect wheels are identical. // Check that direct and indirect wheels are identical.
@ -599,7 +534,7 @@ mod tests {
/// platform-independent deterministic builds. /// platform-independent deterministic builds.
#[test] #[test]
fn built_by_uv_building() { fn built_by_uv_building() {
let built_by_uv = Path::new("../../test/packages/built-by-uv"); let built_by_uv = Path::new("../../scripts/packages/built-by-uv");
let src = TempDir::new().unwrap(); let src = TempDir::new().unwrap();
for dir in [ for dir in [
"src", "src",
@ -662,7 +597,7 @@ mod tests {
// Check that the source dist is reproducible across platforms. // Check that the source dist is reproducible across platforms.
assert_snapshot!( assert_snapshot!(
format!("{:x}", sha2::Sha256::digest(fs_err::read(&source_dist_path).unwrap())), format!("{:x}", sha2::Sha256::digest(fs_err::read(&source_dist_path).unwrap())),
@"bb74bff575b135bb39e5c9bce56349441fb0923bb8857e32a5eaf34ec1843967" @"871d1f859140721b67cbeaca074e7a2740c88c38028d0509eba87d1285f1da9e"
); );
// Check both the files we report and the actual files // Check both the files we report and the actual files
assert_snapshot!(format_file_list(build.source_dist_list_files, src.path()), @r" assert_snapshot!(format_file_list(build.source_dist_list_files, src.path()), @r"
@ -821,7 +756,7 @@ mod tests {
// Build a wheel from a source distribution // Build a wheel from a source distribution
let output_dir = TempDir::new().unwrap(); let output_dir = TempDir::new().unwrap();
build_source_dist(src.path(), output_dir.path(), "0.5.15", false).unwrap(); build_source_dist(src.path(), output_dir.path(), "0.5.15").unwrap();
let sdist_tree = TempDir::new().unwrap(); let sdist_tree = TempDir::new().unwrap();
let source_dist_path = output_dir.path().join("pep_pep639_license-1.0.0.tar.gz"); 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()); let sdist_reader = BufReader::new(File::open(&source_dist_path).unwrap());
@ -832,7 +767,6 @@ mod tests {
output_dir.path(), output_dir.path(),
None, None,
"0.5.15", "0.5.15",
false,
) )
.unwrap(); .unwrap();
let wheel = output_dir let wheel = output_dir
@ -897,7 +831,6 @@ mod tests {
output_dir.path(), output_dir.path(),
Some(&metadata_dir.path().join(&dist_info_dir)), Some(&metadata_dir.path().join(&dist_info_dir)),
"0.5.15", "0.5.15",
false,
) )
.unwrap(); .unwrap();
let wheel = output_dir let wheel = output_dir
@ -1481,114 +1414,4 @@ mod tests {
simple_namespace_part-1.0.0.dist-info/WHEEL 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
");
}
} }

View File

@ -3,7 +3,7 @@ use std::ffi::OsStr;
use std::fmt::Display; use std::fmt::Display;
use std::fmt::Write; use std::fmt::Write;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::{self, FromStr}; use std::str::FromStr;
use itertools::Itertools; use itertools::Itertools;
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
@ -60,10 +60,6 @@ pub enum ValidationError {
ReservedGuiScripts, ReservedGuiScripts,
#[error("`project.license` is not a valid SPDX expression: {0}")] #[error("`project.license` is not a valid SPDX expression: {0}")]
InvalidSpdx(String, #[source] spdx::error::ParseError), 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. /// Check if the build backend is matching the currently running uv version.
@ -154,11 +150,8 @@ impl PyProjectToml {
&self.project.version &self.project.version
} }
pub(crate) fn parse(path: &Path) -> Result<Self, Error> { pub(crate) fn parse(contents: &str) -> Result<Self, Error> {
let contents = fs_err::read_to_string(path)?; Ok(toml::from_str(contents)?)
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> { pub(crate) fn readme(&self) -> Option<&Readme> {
@ -346,7 +339,99 @@ impl PyProjectToml {
"2.3" "2.3"
}; };
let (license, license_expression, license_files) = self.license_metadata(root)?; // 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());
}
}
// TODO(konsti): https://peps.python.org/pep-0753/#label-normalization (Draft) // TODO(konsti): https://peps.python.org/pep-0753/#label-normalization (Draft)
let project_urls = self let project_urls = self
@ -433,156 +518,6 @@ 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, /// Validate and convert the entrypoints in `pyproject.toml`, including console and GUI scripts,
/// to an `entry_points.txt`. /// to an `entry_points.txt`.
/// ///
@ -952,7 +887,7 @@ mod tests {
requires = ["uv_build>=0.4.15,<0.5.0"] requires = ["uv_build>=0.4.15,<0.5.0"]
build-backend = "uv_build" build-backend = "uv_build"
"#; "#;
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); let pyproject_toml = PyProjectToml::parse(contents).unwrap();
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap(); let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
@ -1037,7 +972,7 @@ mod tests {
"# "#
}; };
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); let pyproject_toml = PyProjectToml::parse(contents).unwrap();
let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap(); let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
assert_snapshot!(metadata.core_metadata_format(), @r###" assert_snapshot!(metadata.core_metadata_format(), @r###"
@ -1131,7 +1066,7 @@ mod tests {
"# "#
}; };
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); let pyproject_toml = PyProjectToml::parse(contents).unwrap();
let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap(); let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
assert_snapshot!(metadata.core_metadata_format(), @r" assert_snapshot!(metadata.core_metadata_format(), @r"
@ -1223,7 +1158,7 @@ mod tests {
"# "#
}; };
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); let pyproject_toml = PyProjectToml::parse(contents).unwrap();
let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap(); let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
assert_snapshot!(metadata.core_metadata_format(), @r###" assert_snapshot!(metadata.core_metadata_format(), @r###"
@ -1284,7 +1219,7 @@ mod tests {
#[test] #[test]
fn build_system_valid() { fn build_system_valid() {
let contents = extend_project(""); let contents = extend_project("");
let pyproject_toml: PyProjectToml = toml::from_str(&contents).unwrap(); let pyproject_toml = PyProjectToml::parse(&contents).unwrap();
assert_snapshot!( assert_snapshot!(
pyproject_toml.check_build_system("0.4.15+test").join("\n"), pyproject_toml.check_build_system("0.4.15+test").join("\n"),
@"" @""
@ -1302,7 +1237,7 @@ mod tests {
requires = ["uv_build"] requires = ["uv_build"]
build-backend = "uv_build" build-backend = "uv_build"
"#}; "#};
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); let pyproject_toml = PyProjectToml::parse(contents).unwrap();
assert_snapshot!( assert_snapshot!(
pyproject_toml.check_build_system("0.4.15+test").join("\n"), 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."### @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."###
@ -1320,7 +1255,7 @@ mod tests {
requires = ["uv_build>=0.4.15,<0.5.0", "wheel"] requires = ["uv_build>=0.4.15,<0.5.0", "wheel"]
build-backend = "uv_build" build-backend = "uv_build"
"#}; "#};
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); let pyproject_toml = PyProjectToml::parse(contents).unwrap();
assert_snapshot!( assert_snapshot!(
pyproject_toml.check_build_system("0.4.15+test").join("\n"), pyproject_toml.check_build_system("0.4.15+test").join("\n"),
@"Expected a single uv requirement in `build-system.requires`, found ``" @"Expected a single uv requirement in `build-system.requires`, found ``"
@ -1338,7 +1273,7 @@ mod tests {
requires = ["setuptools"] requires = ["setuptools"]
build-backend = "uv_build" build-backend = "uv_build"
"#}; "#};
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); let pyproject_toml = PyProjectToml::parse(contents).unwrap();
assert_snapshot!( assert_snapshot!(
pyproject_toml.check_build_system("0.4.15+test").join("\n"), pyproject_toml.check_build_system("0.4.15+test").join("\n"),
@"Expected a single uv requirement in `build-system.requires`, found ``" @"Expected a single uv requirement in `build-system.requires`, found ``"
@ -1356,7 +1291,7 @@ mod tests {
requires = ["uv_build>=0.4.15,<0.5.0"] requires = ["uv_build>=0.4.15,<0.5.0"]
build-backend = "setuptools" build-backend = "setuptools"
"#}; "#};
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); let pyproject_toml = PyProjectToml::parse(contents).unwrap();
assert_snapshot!( assert_snapshot!(
pyproject_toml.check_build_system("0.4.15+test").join("\n"), 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"`"### @r###"The value for `build_system.build-backend` should be `"uv_build"`, not `"setuptools"`"###
@ -1367,7 +1302,7 @@ mod tests {
fn minimal() { fn minimal() {
let contents = extend_project(""); let contents = extend_project("");
let metadata = toml::from_str::<PyProjectToml>(&contents) let metadata = PyProjectToml::parse(&contents)
.unwrap() .unwrap()
.to_metadata(Path::new("/do/not/read")) .to_metadata(Path::new("/do/not/read"))
.unwrap(); .unwrap();
@ -1386,14 +1321,15 @@ mod tests {
"# "#
}); });
let err = toml::from_str::<PyProjectToml>(&contents).unwrap_err(); let err = PyProjectToml::parse(&contents).unwrap_err();
assert_snapshot!(format_err(err), @r#" assert_snapshot!(format_err(err), @r###"
TOML parse error at line 4, column 10 Invalid pyproject.toml
Caused by: TOML parse error at line 4, column 10
| |
4 | readme = { path = "Readme.md" } 4 | readme = { path = "Readme.md" }
| ^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^
data did not match any variant of untagged enum Readme data did not match any variant of untagged enum Readme
"#); "###);
} }
#[test] #[test]
@ -1403,7 +1339,7 @@ mod tests {
"# "#
}); });
let err = toml::from_str::<PyProjectToml>(&contents) let err = PyProjectToml::parse(&contents)
.unwrap() .unwrap()
.to_metadata(Path::new("/do/not/read")) .to_metadata(Path::new("/do/not/read"))
.unwrap_err(); .unwrap_err();
@ -1425,14 +1361,14 @@ mod tests {
"# "#
}); });
let err = toml::from_str::<PyProjectToml>(&contents) let err = PyProjectToml::parse(&contents)
.unwrap() .unwrap()
.to_metadata(Path::new("/do/not/read")) .to_metadata(Path::new("/do/not/read"))
.unwrap_err(); .unwrap_err();
assert_snapshot!(format_err(err), @r" assert_snapshot!(format_err(err), @r###"
Invalid project metadata Invalid pyproject.toml
Caused by: `project.description` must be a single line Caused by: `project.description` must be a single line
"); "###);
} }
#[test] #[test]
@ -1443,14 +1379,14 @@ mod tests {
"# "#
}); });
let err = toml::from_str::<PyProjectToml>(&contents) let err = PyProjectToml::parse(&contents)
.unwrap() .unwrap()
.to_metadata(Path::new("/do/not/read")) .to_metadata(Path::new("/do/not/read"))
.unwrap_err(); .unwrap_err();
assert_snapshot!(format_err(err), @r" assert_snapshot!(format_err(err), @r###"
Invalid project metadata Invalid pyproject.toml
Caused by: When `project.license-files` is defined, `project.license` must be an SPDX expression string Caused by: When `project.license-files` is defined, `project.license` must be an SPDX expression string
"); "###);
} }
#[test] #[test]
@ -1459,7 +1395,7 @@ mod tests {
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
"# "#
}); });
let metadata = toml::from_str::<PyProjectToml>(&contents) let metadata = PyProjectToml::parse(&contents)
.unwrap() .unwrap()
.to_metadata(Path::new("/do/not/read")) .to_metadata(Path::new("/do/not/read"))
.unwrap(); .unwrap();
@ -1477,13 +1413,13 @@ mod tests {
license = "MIT XOR Apache-2" license = "MIT XOR Apache-2"
"# "#
}); });
let err = toml::from_str::<PyProjectToml>(&contents) let err = PyProjectToml::parse(&contents)
.unwrap() .unwrap()
.to_metadata(Path::new("/do/not/read")) .to_metadata(Path::new("/do/not/read"))
.unwrap_err(); .unwrap_err();
// TODO(konsti): We mess up the indentation in the error. // TODO(konsti): We mess up the indentation in the error.
assert_snapshot!(format_err(err), @r" assert_snapshot!(format_err(err), @r"
Invalid project metadata Invalid pyproject.toml
Caused by: `project.license` is not a valid SPDX expression: MIT XOR Apache-2 Caused by: `project.license` is not a valid SPDX expression: MIT XOR Apache-2
Caused by: MIT XOR Apache-2 Caused by: MIT XOR Apache-2
^^^ unknown term ^^^ unknown term
@ -1497,18 +1433,18 @@ mod tests {
"# "#
}); });
let err = toml::from_str::<PyProjectToml>(&contents) let err = PyProjectToml::parse(&contents)
.unwrap() .unwrap()
.to_metadata(Path::new("/do/not/read")) .to_metadata(Path::new("/do/not/read"))
.unwrap_err(); .unwrap_err();
assert_snapshot!(format_err(err), @r" assert_snapshot!(format_err(err), @r###"
Invalid project metadata Invalid pyproject.toml
Caused by: Dynamic metadata is not supported Caused by: Dynamic metadata is not supported
"); "###);
} }
fn script_error(contents: &str) -> String { fn script_error(contents: &str) -> String {
let err = toml::from_str::<PyProjectToml>(contents) let err = PyProjectToml::parse(contents)
.unwrap() .unwrap()
.to_entry_points() .to_entry_points()
.unwrap_err(); .unwrap_err();

View File

@ -70,9 +70,6 @@ pub struct BuildBackendSettings {
pub default_excludes: bool, pub default_excludes: bool,
/// Glob expressions which files and directories to exclude from the source distribution. /// 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( #[option(
default = r#"[]"#, default = r#"[]"#,
value_type = "list[str]", value_type = "list[str]",

View File

@ -24,9 +24,9 @@ pub fn build_source_dist(
source_tree: &Path, source_tree: &Path,
source_dist_directory: &Path, source_dist_directory: &Path,
uv_version: &str, uv_version: &str,
show_warnings: bool,
) -> Result<SourceDistFilename, Error> { ) -> Result<SourceDistFilename, Error> {
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?;
let filename = SourceDistFilename { let filename = SourceDistFilename {
name: pyproject_toml.name().clone(), name: pyproject_toml.name().clone(),
version: pyproject_toml.version().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 source_dist_path = source_dist_directory.join(filename.to_string());
let writer = TarGzWriter::new(&source_dist_path)?; let writer = TarGzWriter::new(&source_dist_path)?;
write_source_dist(source_tree, writer, uv_version, show_warnings)?; write_source_dist(source_tree, writer, uv_version)?;
Ok(filename) Ok(filename)
} }
@ -42,9 +42,9 @@ pub fn build_source_dist(
pub fn list_source_dist( pub fn list_source_dist(
source_tree: &Path, source_tree: &Path,
uv_version: &str, uv_version: &str,
show_warnings: bool,
) -> Result<(SourceDistFilename, FileList), Error> { ) -> Result<(SourceDistFilename, FileList), Error> {
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?;
let filename = SourceDistFilename { let filename = SourceDistFilename {
name: pyproject_toml.name().clone(), name: pyproject_toml.name().clone(),
version: pyproject_toml.version().clone(), version: pyproject_toml.version().clone(),
@ -52,7 +52,7 @@ pub fn list_source_dist(
}; };
let mut files = FileList::new(); let mut files = FileList::new();
let writer = ListWriter::new(&mut files); let writer = ListWriter::new(&mut files);
write_source_dist(source_tree, writer, uv_version, show_warnings)?; write_source_dist(source_tree, writer, uv_version)?;
Ok((filename, files)) Ok((filename, files))
} }
@ -61,7 +61,6 @@ fn source_dist_matcher(
source_tree: &Path, source_tree: &Path,
pyproject_toml: &PyProjectToml, pyproject_toml: &PyProjectToml,
settings: BuildBackendSettings, settings: BuildBackendSettings,
show_warnings: bool,
) -> Result<(GlobDirFilter, GlobSet), Error> { ) -> Result<(GlobDirFilter, GlobSet), Error> {
// File and directories to include in the source directory // File and directories to include in the source directory
let mut include_globs = Vec::new(); let mut include_globs = Vec::new();
@ -76,7 +75,6 @@ fn source_dist_matcher(
&settings.module_root, &settings.module_root,
settings.module_name.as_ref(), settings.module_name.as_ref(),
settings.namespace, settings.namespace,
show_warnings,
)?; )?;
for module_relative in modules_relative { for module_relative in modules_relative {
// The wheel must not include any files included by the source distribution (at least until we // The wheel must not include any files included by the source distribution (at least until we
@ -184,9 +182,9 @@ fn write_source_dist(
source_tree: &Path, source_tree: &Path,
mut writer: impl DirectoryWriter, mut writer: impl DirectoryWriter,
uv_version: &str, uv_version: &str,
show_warnings: bool,
) -> Result<SourceDistFilename, Error> { ) -> Result<SourceDistFilename, Error> {
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?;
for warning in pyproject_toml.check_build_system(uv_version) { for warning in pyproject_toml.check_build_system(uv_version) {
warn_user_once!("{warning}"); warn_user_once!("{warning}");
} }
@ -220,7 +218,7 @@ fn write_source_dist(
)?; )?;
let (include_matcher, exclude_matcher) = let (include_matcher, exclude_matcher) =
source_dist_matcher(source_tree, &pyproject_toml, settings, show_warnings)?; source_dist_matcher(source_tree, &pyproject_toml, settings)?;
let mut files_visited = 0; let mut files_visited = 0;
for entry in WalkDir::new(source_tree) for entry in WalkDir::new(source_tree)
@ -299,10 +297,6 @@ impl TarGzWriter {
impl DirectoryWriter for TarGzWriter { impl DirectoryWriter for TarGzWriter {
fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error> { fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error> {
let mut header = Header::new_gnu(); 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); header.set_size(bytes.len() as u64);
// Reasonable default to avoid 0o000 permissions, the user's umask will be applied on // Reasonable default to avoid 0o000 permissions, the user's umask will be applied on
// unpacking. // unpacking.
@ -316,10 +310,6 @@ impl DirectoryWriter for TarGzWriter {
fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error> { fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error> {
let metadata = fs_err::metadata(file)?; let metadata = fs_err::metadata(file)?;
let mut header = Header::new_gnu(); 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 // Preserve the executable bit, especially for scripts
#[cfg(unix)] #[cfg(unix)]
let executable_bit = { let executable_bit = {

View File

@ -29,9 +29,9 @@ pub fn build_wheel(
wheel_dir: &Path, wheel_dir: &Path,
metadata_directory: Option<&Path>, metadata_directory: Option<&Path>,
uv_version: &str, uv_version: &str,
show_warnings: bool,
) -> Result<WheelFilename, Error> { ) -> Result<WheelFilename, Error> {
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?;
for warning in pyproject_toml.check_build_system(uv_version) { for warning in pyproject_toml.check_build_system(uv_version) {
warn_user_once!("{warning}"); warn_user_once!("{warning}");
} }
@ -58,7 +58,6 @@ pub fn build_wheel(
&filename, &filename,
uv_version, uv_version,
wheel_writer, wheel_writer,
show_warnings,
)?; )?;
Ok(filename) Ok(filename)
@ -68,9 +67,9 @@ pub fn build_wheel(
pub fn list_wheel( pub fn list_wheel(
source_tree: &Path, source_tree: &Path,
uv_version: &str, uv_version: &str,
show_warnings: bool,
) -> Result<(WheelFilename, FileList), Error> { ) -> Result<(WheelFilename, FileList), Error> {
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?;
for warning in pyproject_toml.check_build_system(uv_version) { for warning in pyproject_toml.check_build_system(uv_version) {
warn_user_once!("{warning}"); warn_user_once!("{warning}");
} }
@ -88,14 +87,7 @@ pub fn list_wheel(
let mut files = FileList::new(); let mut files = FileList::new();
let writer = ListWriter::new(&mut files); let writer = ListWriter::new(&mut files);
write_wheel( write_wheel(source_tree, &pyproject_toml, &filename, uv_version, writer)?;
source_tree,
&pyproject_toml,
&filename,
uv_version,
writer,
show_warnings,
)?;
Ok((filename, files)) Ok((filename, files))
} }
@ -105,7 +97,6 @@ fn write_wheel(
filename: &WheelFilename, filename: &WheelFilename,
uv_version: &str, uv_version: &str,
mut wheel_writer: impl DirectoryWriter, mut wheel_writer: impl DirectoryWriter,
show_warnings: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
let settings = pyproject_toml let settings = pyproject_toml
.settings() .settings()
@ -141,7 +132,6 @@ fn write_wheel(
&settings.module_root, &settings.module_root,
settings.module_name.as_ref(), settings.module_name.as_ref(),
settings.namespace, settings.namespace,
show_warnings,
)?; )?;
let mut files_visited = 0; let mut files_visited = 0;
@ -269,9 +259,9 @@ pub fn build_editable(
wheel_dir: &Path, wheel_dir: &Path,
metadata_directory: Option<&Path>, metadata_directory: Option<&Path>,
uv_version: &str, uv_version: &str,
show_warnings: bool,
) -> Result<WheelFilename, Error> { ) -> Result<WheelFilename, Error> {
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?;
for warning in pyproject_toml.check_build_system(uv_version) { for warning in pyproject_toml.check_build_system(uv_version) {
warn_user_once!("{warning}"); warn_user_once!("{warning}");
} }
@ -305,7 +295,6 @@ pub fn build_editable(
&settings.module_root, &settings.module_root,
settings.module_name.as_ref(), settings.module_name.as_ref(),
settings.namespace, settings.namespace,
show_warnings,
)?; )?;
wheel_writer.write_bytes( wheel_writer.write_bytes(
@ -332,7 +321,8 @@ pub fn metadata(
metadata_directory: &Path, metadata_directory: &Path,
uv_version: &str, uv_version: &str,
) -> Result<String, Error> { ) -> Result<String, Error> {
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?;
for warning in pyproject_toml.check_build_system(uv_version) { for warning in pyproject_toml.check_build_system(uv_version) {
warn_user_once!("{warning}"); warn_user_once!("{warning}");
} }
@ -840,7 +830,7 @@ mod test {
#[test] #[test]
fn test_prepare_metadata() { fn test_prepare_metadata() {
let metadata_dir = TempDir::new().unwrap(); let metadata_dir = TempDir::new().unwrap();
let built_by_uv = Path::new("../../test/packages/built-by-uv"); let built_by_uv = Path::new("../../scripts/packages/built-by-uv");
metadata(built_by_uv, metadata_dir.path(), "1.0.0+test").unwrap(); metadata(built_by_uv, metadata_dir.path(), "1.0.0+test").unwrap();
let mut files: Vec<_> = WalkDir::new(metadata_dir.path()) let mut files: Vec<_> = WalkDir::new(metadata_dir.path())

View File

@ -1,10 +1,11 @@
[package] [package]
name = "uv-build-frontend" name = "uv-build-frontend"
version = "0.0.8" version = "0.0.1"
description = "This is an internal component crate of uv" description = "Build wheels from source distributions"
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
homepage = { workspace = true } homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }
@ -16,7 +17,6 @@ doctest = false
workspace = true workspace = true
[dependencies] [dependencies]
uv-auth = { workspace = true }
uv-cache-key = { workspace = true } uv-cache-key = { workspace = true }
uv-configuration = { workspace = true } uv-configuration = { workspace = true }
uv-distribution = { workspace = true } uv-distribution = { workspace = true }

View File

@ -1,13 +0,0 @@
<!-- 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.

View File

@ -28,7 +28,7 @@ use tokio::io::AsyncBufReadExt;
use tokio::process::Command; use tokio::process::Command;
use tokio::sync::{Mutex, Semaphore}; use tokio::sync::{Mutex, Semaphore};
use tracing::{Instrument, debug, info_span, instrument, warn}; use tracing::{Instrument, debug, info_span, instrument, warn};
use uv_auth::CredentialsCache;
use uv_cache_key::cache_digest; use uv_cache_key::cache_digest;
use uv_configuration::{BuildKind, BuildOutput, SourceStrategy}; use uv_configuration::{BuildKind, BuildOutput, SourceStrategy};
use uv_distribution::BuildRequires; use uv_distribution::BuildRequires;
@ -36,7 +36,7 @@ use uv_distribution_types::{
ConfigSettings, ExtraBuildRequirement, ExtraBuildRequires, IndexLocations, Requirement, ConfigSettings, ExtraBuildRequirement, ExtraBuildRequires, IndexLocations, Requirement,
Resolution, Resolution,
}; };
use uv_fs::{LockedFile, LockedFileMode}; use uv_fs::LockedFile;
use uv_fs::{PythonExt, Simplified}; use uv_fs::{PythonExt, Simplified};
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pep440::Version; use uv_pep440::Version;
@ -292,7 +292,6 @@ impl SourceBuild {
mut environment_variables: FxHashMap<OsString, OsString>, mut environment_variables: FxHashMap<OsString, OsString>,
level: BuildOutput, level: BuildOutput,
concurrent_builds: usize, concurrent_builds: usize,
credentials_cache: &CredentialsCache,
preview: Preview, preview: Preview,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let temp_dir = build_context.cache().venv_dir()?; let temp_dir = build_context.cache().venv_dir()?;
@ -303,6 +302,7 @@ impl SourceBuild {
source.to_path_buf() source.to_path_buf()
}; };
let default_backend: Pep517Backend = DEFAULT_BACKEND.clone();
// Check if we have a PEP 517 build backend. // Check if we have a PEP 517 build backend.
let (pep517_backend, project) = Self::extract_pep517_backend( let (pep517_backend, project) = Self::extract_pep517_backend(
&source_tree, &source_tree,
@ -311,7 +311,7 @@ impl SourceBuild {
locations, locations,
source_strategy, source_strategy,
workspace_cache, workspace_cache,
credentials_cache, &default_backend,
) )
.await .await
.map_err(|err| *err)?; .map_err(|err| *err)?;
@ -383,6 +383,7 @@ impl SourceBuild {
let resolved_requirements = Self::get_resolved_requirements( let resolved_requirements = Self::get_resolved_requirements(
build_context, build_context,
source_build_context, source_build_context,
&default_backend,
&pep517_backend, &pep517_backend,
extra_build_dependencies, extra_build_dependencies,
build_stack, build_stack,
@ -454,7 +455,6 @@ impl SourceBuild {
&environment_variables, &environment_variables,
&modified_path, &modified_path,
&temp_dir, &temp_dir,
credentials_cache,
) )
.await?; .await?;
} }
@ -493,16 +493,12 @@ impl SourceBuild {
"uv-setuptools-{}.lock", "uv-setuptools-{}.lock",
cache_digest(&canonical_source_path) cache_digest(&canonical_source_path)
)); ));
source_tree_lock = LockedFile::acquire( source_tree_lock = LockedFile::acquire(lock_path, self.source_tree.to_string_lossy())
lock_path, .await
LockedFileMode::Exclusive, .inspect_err(|err| {
self.source_tree.to_string_lossy(), warn!("Failed to acquire build lock: {err}");
) })
.await .ok();
.inspect_err(|err| {
warn!("Failed to acquire build lock: {err}");
})
.ok();
} }
Ok(source_tree_lock) Ok(source_tree_lock)
} }
@ -510,12 +506,13 @@ impl SourceBuild {
async fn get_resolved_requirements( async fn get_resolved_requirements(
build_context: &impl BuildContext, build_context: &impl BuildContext,
source_build_context: SourceBuildContext, source_build_context: SourceBuildContext,
default_backend: &Pep517Backend,
pep517_backend: &Pep517Backend, pep517_backend: &Pep517Backend,
extra_build_dependencies: Vec<Requirement>, extra_build_dependencies: Vec<Requirement>,
build_stack: &BuildStack, build_stack: &BuildStack,
) -> Result<Resolution, Error> { ) -> Result<Resolution, Error> {
Ok( Ok(
if pep517_backend.requirements == DEFAULT_BACKEND.requirements if pep517_backend.requirements == default_backend.requirements
&& extra_build_dependencies.is_empty() && extra_build_dependencies.is_empty()
{ {
let mut resolution = source_build_context.default_resolution.lock().await; let mut resolution = source_build_context.default_resolution.lock().await;
@ -523,7 +520,7 @@ impl SourceBuild {
resolved_requirements.clone() resolved_requirements.clone()
} else { } else {
let resolved_requirements = build_context let resolved_requirements = build_context
.resolve(&DEFAULT_BACKEND.requirements, build_stack) .resolve(&default_backend.requirements, build_stack)
.await .await
.map_err(|err| { .map_err(|err| {
Error::RequirementsResolve("`setup.py` build", err.into()) Error::RequirementsResolve("`setup.py` build", err.into())
@ -563,7 +560,7 @@ impl SourceBuild {
locations: &IndexLocations, locations: &IndexLocations,
source_strategy: SourceStrategy, source_strategy: SourceStrategy,
workspace_cache: &WorkspaceCache, workspace_cache: &WorkspaceCache,
credentials_cache: &CredentialsCache, default_backend: &Pep517Backend,
) -> Result<(Pep517Backend, Option<Project>), Box<Error>> { ) -> Result<(Pep517Backend, Option<Project>), Box<Error>> {
match fs::read_to_string(source_tree.join("pyproject.toml")) { match fs::read_to_string(source_tree.join("pyproject.toml")) {
Ok(toml) => { Ok(toml) => {
@ -592,7 +589,6 @@ impl SourceBuild {
locations, locations,
source_strategy, source_strategy,
workspace_cache, workspace_cache,
credentials_cache,
) )
.await .await
.map_err(Error::Lowering)?; .map_err(Error::Lowering)?;
@ -662,7 +658,7 @@ impl SourceBuild {
} }
} }
DEFAULT_BACKEND.clone() default_backend.clone()
}; };
Ok((backend, pyproject_toml.project)) Ok((backend, pyproject_toml.project))
} }
@ -678,7 +674,7 @@ impl SourceBuild {
// the default backend, to match `build`. `pip` uses `setup.py` directly in this // 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. // case, but plans to make PEP 517 builds the default in the future.
// See: https://github.com/pypa/pip/issues/9175. // 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())), Err(err) => Err(Box::new(err.into())),
} }
@ -965,7 +961,6 @@ async fn create_pep517_build_environment(
environment_variables: &FxHashMap<OsString, OsString>, environment_variables: &FxHashMap<OsString, OsString>,
modified_path: &OsString, modified_path: &OsString,
temp_dir: &TempDir, temp_dir: &TempDir,
credentials_cache: &CredentialsCache,
) -> Result<(), Error> { ) -> Result<(), Error> {
// Write the hook output to a file so that we can read it back reliably. // Write the hook output to a file so that we can read it back reliably.
let outfile = temp_dir let outfile = temp_dir
@ -1060,7 +1055,6 @@ async fn create_pep517_build_environment(
locations, locations,
source_strategy, source_strategy,
workspace_cache, workspace_cache,
credentials_cache,
) )
.await .await
.map_err(Error::Lowering)?; .map_err(Error::Lowering)?;

View File

@ -1,10 +1,10 @@
[package] [package]
name = "uv-build" name = "uv-build"
version = "0.9.18" version = "0.9.9"
description = "A Python build backend"
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
homepage = { workspace = true } homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }

View File

@ -1,6 +1,6 @@
[project] [project]
name = "uv-build" name = "uv-build"
version = "0.9.18" version = "0.9.9"
description = "The uv build backend" description = "The uv build backend"
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
requires-python = ">=3.8" requires-python = ">=3.8"

View File

@ -44,7 +44,6 @@ fn main() -> Result<()> {
&env::current_dir()?, &env::current_dir()?,
&sdist_directory, &sdist_directory,
uv_version::version(), uv_version::version(),
false,
)?; )?;
// Tell the build frontend about the name of the artifact we built // Tell the build frontend about the name of the artifact we built
writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?;
@ -57,7 +56,6 @@ fn main() -> Result<()> {
&wheel_directory, &wheel_directory,
metadata_directory.as_deref(), metadata_directory.as_deref(),
uv_version::version(), uv_version::version(),
false,
)?; )?;
// Tell the build frontend about the name of the artifact we built // Tell the build frontend about the name of the artifact we built
writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?;
@ -70,7 +68,6 @@ fn main() -> Result<()> {
&wheel_directory, &wheel_directory,
metadata_directory.as_deref(), metadata_directory.as_deref(),
uv_version::version(), uv_version::version(),
false,
)?; )?;
// Tell the build frontend about the name of the artifact we built // Tell the build frontend about the name of the artifact we built
writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?;

View File

@ -1,10 +1,10 @@
[package] [package]
name = "uv-cache-info" name = "uv-cache-info"
version = "0.0.8" version = "0.0.1"
description = "This is an internal component crate of uv"
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
homepage = { workspace = true } homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }

View File

@ -1,13 +0,0 @@
<!-- 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.

View File

@ -1,10 +1,11 @@
[package] [package]
name = "uv-cache-key" name = "uv-cache-key"
version = "0.0.8" version = "0.0.1"
description = "This is an internal component crate of uv" description = "Generic functionality for caching paths, URLs, and other resources across platforms."
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
homepage = { workspace = true } homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }

View File

@ -1,13 +0,0 @@
<!-- 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.

View File

@ -139,18 +139,8 @@ impl std::fmt::Display for CanonicalUrl {
/// `https://github.com/pypa/package.git#subdirectory=pkg_b` would map to different /// `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 /// [`CanonicalUrl`] values, but the same [`RepositoryUrl`], since they map to the same
/// resource. /// 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)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct RepositoryUrl { pub struct RepositoryUrl(DisplaySafeUrl);
repo_url: DisplaySafeUrl,
with_lfs: Option<bool>,
}
impl RepositoryUrl { impl RepositoryUrl {
pub fn new(url: &DisplaySafeUrl) -> Self { pub fn new(url: &DisplaySafeUrl) -> Self {
@ -171,31 +161,19 @@ impl RepositoryUrl {
url.set_fragment(None); url.set_fragment(None);
url.set_query(None); url.set_query(None);
Self { Self(url)
repo_url: url,
with_lfs: None,
}
} }
pub fn parse(url: &str) -> Result<Self, DisplaySafeUrlError> { pub fn parse(url: &str) -> Result<Self, DisplaySafeUrlError> {
Ok(Self::new(&DisplaySafeUrl::parse(url)?)) 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 { impl CacheKey for RepositoryUrl {
fn cache_key(&self, state: &mut CacheKeyHasher) { fn cache_key(&self, state: &mut CacheKeyHasher) {
// `as_str` gives the serialisation of a url (which has a spec) and so insulates against // `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. // possible changes in how the URL crate does hashing.
self.repo_url.as_str().cache_key(state); self.0.as_str().cache_key(state);
if let Some(true) = self.with_lfs {
1u8.cache_key(state);
}
} }
} }
@ -203,10 +181,7 @@ impl Hash for RepositoryUrl {
fn hash<H: Hasher>(&self, state: &mut H) { fn hash<H: Hasher>(&self, state: &mut H) {
// `as_str` gives the serialisation of a url (which has a spec) and so insulates against // `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. // possible changes in how the URL crate does hashing.
self.repo_url.as_str().hash(state); self.0.as_str().hash(state);
if let Some(true) = self.with_lfs {
1u8.hash(state);
}
} }
} }
@ -214,13 +189,13 @@ impl Deref for RepositoryUrl {
type Target = Url; type Target = Url;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.repo_url &self.0
} }
} }
impl std::fmt::Display for RepositoryUrl { impl std::fmt::Display for RepositoryUrl {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.repo_url, f) std::fmt::Display::fmt(&self.0, f)
} }
} }
@ -308,14 +283,6 @@ 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. // Two URLs should _not_ be considered equal if they request different commit tags.
assert_ne!( assert_ne!(
CanonicalUrl::parse( CanonicalUrl::parse(
@ -411,76 +378,6 @@ 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(()) Ok(())
} }
} }

View File

@ -1,10 +1,11 @@
[package] [package]
name = "uv-cache" name = "uv-cache"
version = "0.0.8" version = "0.0.1"
description = "This is an internal component crate of uv" description = "Generate stable hash digests across versions and platforms."
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
homepage = { workspace = true } homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }
@ -34,6 +35,5 @@ rustc-hash = { workspace = true }
same-file = { workspace = true } same-file = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
tempfile = { workspace = true } tempfile = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
walkdir = { workspace = true } walkdir = { workspace = true }

View File

@ -1,13 +0,0 @@
<!-- 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.

View File

@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
use uv_static::EnvVars; use uv_static::EnvVars;
use crate::Cache; use crate::Cache;
use clap::{Parser, ValueHint}; use clap::Parser;
use tracing::{debug, warn}; use tracing::{debug, warn};
#[derive(Parser, Debug, Clone)] #[derive(Parser, Debug, Clone)]
@ -27,7 +27,7 @@ pub struct CacheArgs {
/// `%LOCALAPPDATA%\uv\cache` on Windows. /// `%LOCALAPPDATA%\uv\cache` on Windows.
/// ///
/// To view the location of the cache directory, run `uv cache dir`. /// To view the location of the cache directory, run `uv cache dir`.
#[arg(global = true, long, env = EnvVars::UV_CACHE_DIR, value_hint = ValueHint::DirPath)] #[arg(global = true, long, env = EnvVars::UV_CACHE_DIR)]
pub cache_dir: Option<PathBuf>, pub cache_dir: Option<PathBuf>,
} }

View File

@ -10,7 +10,7 @@ use rustc_hash::FxHashMap;
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
use uv_cache_info::Timestamp; use uv_cache_info::Timestamp;
use uv_fs::{LockedFile, LockedFileError, LockedFileMode, Simplified, cachedir, directories}; use uv_fs::{LockedFile, Simplified, cachedir, directories};
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pypi_types::ResolutionMetadata; use uv_pypi_types::ResolutionMetadata;
@ -35,17 +35,6 @@ mod wheel;
/// Must be kept in-sync with the version in [`CacheBucket::to_str`]. /// Must be kept in-sync with the version in [`CacheBucket::to_str`].
pub const ARCHIVE_VERSION: u8 = 0; 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. /// A [`CacheEntry`] which may or may not exist yet.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CacheEntry(PathBuf); pub struct CacheEntry(PathBuf);
@ -91,14 +80,9 @@ impl CacheEntry {
} }
/// Acquire the [`CacheEntry`] as an exclusive lock. /// Acquire the [`CacheEntry`] as an exclusive lock.
pub async fn lock(&self) -> Result<LockedFile, Error> { pub async fn lock(&self) -> Result<LockedFile, io::Error> {
fs_err::create_dir_all(self.dir())?; fs_err::create_dir_all(self.dir())?;
Ok(LockedFile::acquire( LockedFile::acquire(self.path(), self.path().display()).await
self.path(),
LockedFileMode::Exclusive,
self.path().display(),
)
.await?)
} }
} }
@ -125,14 +109,9 @@ impl CacheShard {
} }
/// Acquire the cache entry as an exclusive lock. /// Acquire the cache entry as an exclusive lock.
pub async fn lock(&self) -> Result<LockedFile, Error> { pub async fn lock(&self) -> Result<LockedFile, io::Error> {
fs_err::create_dir_all(self.as_ref())?; fs_err::create_dir_all(self.as_ref())?;
Ok(LockedFile::acquire( LockedFile::acquire(self.join(".lock"), self.display()).await
self.join(".lock"),
LockedFileMode::Exclusive,
self.display(),
)
.await?)
} }
/// Return the [`CacheShard`] as a [`PathBuf`]. /// Return the [`CacheShard`] as a [`PathBuf`].
@ -203,7 +182,7 @@ impl Cache {
} }
/// Acquire a lock that allows removing entries from the cache. /// Acquire a lock that allows removing entries from the cache.
pub async fn with_exclusive_lock(self) -> Result<Self, LockedFileError> { pub fn with_exclusive_lock(self) -> Result<Self, io::Error> {
let Self { let Self {
root, root,
refresh, refresh,
@ -219,12 +198,8 @@ impl Cache {
), ),
); );
} }
let lock_file = LockedFile::acquire( let lock_file =
root.join(".lock"), LockedFile::acquire_blocking(root.join(".lock"), root.simplified_display())?;
LockedFileMode::Exclusive,
root.simplified_display(),
)
.await?;
Ok(Self { Ok(Self {
root, root,
@ -245,11 +220,7 @@ impl Cache {
lock_file, lock_file,
} = self; } = self;
match LockedFile::acquire_no_wait( match LockedFile::acquire_no_wait(root.join(".lock"), root.simplified_display()) {
root.join(".lock"),
LockedFileMode::Exclusive,
root.simplified_display(),
) {
Some(lock_file) => Ok(Self { Some(lock_file) => Ok(Self {
root, root,
refresh, refresh,
@ -401,8 +372,10 @@ impl Cache {
self.temp_dir.is_some() self.temp_dir.is_some()
} }
/// Populate the cache scaffold. /// Initialize the [`Cache`].
fn create_base_files(root: &PathBuf) -> io::Result<()> { pub fn init(self) -> Result<Self, io::Error> {
let root = &self.root;
// Create the cache directory, if it doesn't exist. // Create the cache directory, if it doesn't exist.
fs_err::create_dir_all(root)?; fs_err::create_dir_all(root)?;
@ -448,66 +421,29 @@ impl Cache {
.join(".git"), .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. // Block cache removal operations from interfering.
let lock_file = match LockedFile::acquire( let lock_file = match LockedFile::acquire_shared_blocking(
root.join(".lock"), root.join(".lock"),
LockedFileMode::Shared,
root.simplified_display(), root.simplified_display(),
) ) {
.await
{
Ok(lock_file) => Some(Arc::new(lock_file)), Ok(lock_file) => Some(Arc::new(lock_file)),
Err(err) Err(err) if err.kind() == io::ErrorKind::Unsupported => {
if err
.as_io_error()
.is_some_and(|err| err.kind() == io::ErrorKind::Unsupported) =>
{
warn!( warn!(
"Shared locking is not supported by the current platform or filesystem, \ "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 None
} }
Err(err) => return Err(err.into()), Err(err) => return Err(err),
}; };
Ok(Self { Ok(Self {
root: std::path::absolute(root).map_err(Error::Absolute)?, root: std::path::absolute(root)?,
lock_file, lock_file,
..self ..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. /// Clear the cache, removing all entries.
pub fn clear(self, reporter: Box<dyn CleanReporter>) -> Result<Removal, io::Error> { 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 // Remove everything but `.lock`, Windows does not allow removal of a locked file
@ -542,7 +478,7 @@ impl Cache {
/// Remove a package from the cache. /// Remove a package from the cache.
/// ///
/// Returns the number of entries removed from the cache. /// Returns the number of entries removed from the cache.
pub fn remove(&self, name: &PackageName) -> io::Result<Removal> { pub fn remove(&self, name: &PackageName) -> Result<Removal, io::Error> {
// Collect the set of referenced archives. // Collect the set of referenced archives.
let references = self.find_archive_references()?; let references = self.find_archive_references()?;

View File

@ -15,7 +15,7 @@ pub enum WheelCache<'a> {
Path(&'a DisplaySafeUrl), Path(&'a DisplaySafeUrl),
/// An editable dependency, which we key by URL. /// An editable dependency, which we key by URL.
Editable(&'a DisplaySafeUrl), Editable(&'a DisplaySafeUrl),
/// A Git dependency, which we key by URL (including LFS state), SHA. /// A Git dependency, which we key by URL and SHA.
/// ///
/// Note that this variant only exists for source distributions; wheels can't be delivered /// Note that this variant only exists for source distributions; wheels can't be delivered
/// through Git. /// through Git.

View File

@ -1,10 +1,11 @@
[package] [package]
name = "uv-cli" name = "uv-cli"
version = "0.0.8" version = "0.0.1"
description = "This is an internal component crate of uv" description = "The command line interface for the uv binary."
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
homepage = { workspace = true } homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }

View File

@ -1,13 +0,0 @@
<!-- 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

View File

@ -366,7 +366,6 @@ pub fn resolver_options(
exclude_newer_package.unwrap_or_default(), exclude_newer_package.unwrap_or_default(),
), ),
link_mode, link_mode,
torch_backend: None,
no_build: flag(no_build, build, "build"), no_build: flag(no_build, build, "build"),
no_build_package: Some(no_build_package), no_build_package: Some(no_build_package),
no_binary: flag(no_binary, binary, "binary"), no_binary: flag(no_binary, binary, "binary"),
@ -496,6 +495,5 @@ pub fn resolver_installer_options(
Some(no_binary_package) Some(no_binary_package)
}, },
no_sources: if no_sources { Some(true) } else { None }, no_sources: if no_sources { Some(true) } else { None },
torch_backend: None,
} }
} }

View File

@ -1,10 +1,10 @@
[package] [package]
name = "uv-client" name = "uv-client"
version = "0.0.8" version = "0.0.1"
description = "This is an internal component crate of uv"
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
homepage = { workspace = true } homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }
@ -72,9 +72,6 @@ http-body-util = { workspace = true }
hyper = { workspace = true } hyper = { workspace = true }
hyper-util = { workspace = true } hyper-util = { workspace = true }
insta = { workspace = true } insta = { workspace = true }
rcgen = { workspace = true }
rustls = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tokio-rustls = { workspace = true }
wiremock = { workspace = true } wiremock = { workspace = true }
tempfile = { workspace = true } tempfile = { workspace = true }

View File

@ -1,13 +1,5 @@
<!-- This file is generated. DO NOT EDIT --> # `pypi-client`
# uv-client A general-use client for interacting with PyPI.
This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here Loosely modeled after Orogene's `oro-client`.
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.

View File

@ -28,7 +28,7 @@ use tracing::{debug, trace};
use url::ParseError; use url::ParseError;
use url::Url; use url::Url;
use uv_auth::{AuthMiddleware, Credentials, CredentialsCache, Indexes, PyxTokenStore}; use uv_auth::{AuthMiddleware, Credentials, Indexes, PyxTokenStore};
use uv_configuration::{KeyringProviderType, TrustedHost}; use uv_configuration::{KeyringProviderType, TrustedHost};
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_pep508::MarkerEnvironment; use uv_pep508::MarkerEnvironment;
@ -50,7 +50,7 @@ pub const DEFAULT_RETRIES: u32 = 3;
/// Maximum number of redirects to follow before giving up. /// Maximum number of redirects to follow before giving up.
/// ///
/// This is the default used by [`reqwest`]. /// This is the default used by [`reqwest`].
pub const DEFAULT_MAX_REDIRECTS: u32 = 10; const DEFAULT_MAX_REDIRECTS: u32 = 10;
/// Selectively skip parts or the entire auth middleware. /// Selectively skip parts or the entire auth middleware.
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
@ -78,8 +78,6 @@ pub struct BaseClientBuilder<'a> {
markers: Option<&'a MarkerEnvironment>, markers: Option<&'a MarkerEnvironment>,
platform: Option<&'a Platform>, platform: Option<&'a Platform>,
auth_integration: AuthIntegration, auth_integration: AuthIntegration,
/// Global authentication cache for a uv invocation to share credentials across uv clients.
credentials_cache: Arc<CredentialsCache>,
indexes: Indexes, indexes: Indexes,
timeout: Duration, timeout: Duration,
extra_middleware: Option<ExtraMiddleware>, extra_middleware: Option<ExtraMiddleware>,
@ -91,8 +89,6 @@ pub struct BaseClientBuilder<'a> {
cross_origin_credential_policy: CrossOriginCredentialsPolicy, cross_origin_credential_policy: CrossOriginCredentialsPolicy,
/// Optional custom reqwest client to use instead of creating a new one. /// Optional custom reqwest client to use instead of creating a new one.
custom_client: Option<Client>, custom_client: Option<Client>,
/// uv subcommand in which this client is being used
subcommand: Option<Vec<String>>,
} }
/// The policy for handling HTTP redirects. /// The policy for handling HTTP redirects.
@ -104,8 +100,6 @@ pub enum RedirectPolicy {
BypassMiddleware, BypassMiddleware,
/// Handle redirects manually, re-triggering our custom middleware for each request. /// Handle redirects manually, re-triggering our custom middleware for each request.
RetriggerMiddleware, RetriggerMiddleware,
/// No redirect for non-cloneable (e.g., streaming) requests with custom redirect logic.
NoRedirect,
} }
impl RedirectPolicy { impl RedirectPolicy {
@ -113,7 +107,6 @@ impl RedirectPolicy {
match self { match self {
Self::BypassMiddleware => reqwest::redirect::Policy::default(), Self::BypassMiddleware => reqwest::redirect::Policy::default(),
Self::RetriggerMiddleware => reqwest::redirect::Policy::none(), Self::RetriggerMiddleware => reqwest::redirect::Policy::none(),
Self::NoRedirect => reqwest::redirect::Policy::none(),
} }
} }
} }
@ -143,7 +136,6 @@ impl Default for BaseClientBuilder<'_> {
markers: None, markers: None,
platform: None, platform: None,
auth_integration: AuthIntegration::default(), auth_integration: AuthIntegration::default(),
credentials_cache: Arc::new(CredentialsCache::default()),
indexes: Indexes::new(), indexes: Indexes::new(),
timeout: Duration::from_secs(30), timeout: Duration::from_secs(30),
extra_middleware: None, extra_middleware: None,
@ -151,12 +143,11 @@ impl Default for BaseClientBuilder<'_> {
redirect_policy: RedirectPolicy::default(), redirect_policy: RedirectPolicy::default(),
cross_origin_credential_policy: CrossOriginCredentialsPolicy::Secure, cross_origin_credential_policy: CrossOriginCredentialsPolicy::Secure,
custom_client: None, custom_client: None,
subcommand: None,
} }
} }
} }
impl<'a> BaseClientBuilder<'a> { impl BaseClientBuilder<'_> {
pub fn new( pub fn new(
connectivity: Connectivity, connectivity: Connectivity,
native_tls: bool, native_tls: bool,
@ -175,7 +166,9 @@ impl<'a> BaseClientBuilder<'a> {
..Self::default() ..Self::default()
} }
} }
}
impl<'a> BaseClientBuilder<'a> {
/// Use a custom reqwest client instead of creating a new one. /// Use a custom reqwest client instead of creating a new one.
/// ///
/// This allows you to provide your own reqwest client with custom configuration. /// This allows you to provide your own reqwest client with custom configuration.
@ -283,26 +276,6 @@ impl<'a> BaseClientBuilder<'a> {
self 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 { pub fn is_native_tls(&self) -> bool {
self.native_tls self.native_tls
} }
@ -351,7 +324,6 @@ impl<'a> BaseClientBuilder<'a> {
dangerous_client, dangerous_client,
raw_dangerous_client, raw_dangerous_client,
timeout, timeout,
credentials_cache: self.credentials_cache.clone(),
} }
} }
@ -378,7 +350,6 @@ impl<'a> BaseClientBuilder<'a> {
raw_client: existing.raw_client.clone(), raw_client: existing.raw_client.clone(),
raw_dangerous_client: existing.raw_dangerous_client.clone(), raw_dangerous_client: existing.raw_dangerous_client.clone(),
timeout: existing.timeout, timeout: existing.timeout,
credentials_cache: existing.credentials_cache.clone(),
} }
} }
@ -387,14 +358,12 @@ impl<'a> BaseClientBuilder<'a> {
let mut user_agent_string = format!("uv/{}", version()); let mut user_agent_string = format!("uv/{}", version());
// Add linehaul metadata. // Add linehaul metadata.
let linehaul = LineHaul::new(self.markers, self.platform, self.subcommand.clone()); let linehaul = LineHaul::new(self.markers, self.platform);
if let Ok(output) = serde_json::to_string(&linehaul) { if let Ok(output) = serde_json::to_string(&linehaul) {
let _ = write!(user_agent_string, " {output}"); let _ = write!(user_agent_string, " {output}");
} }
// Checks for the presence of `SSL_CERT_FILE`. // Check for the presence of an `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 ssl_cert_file_exists = env::var_os(EnvVars::SSL_CERT_FILE).is_some_and(|path| {
let path_exists = Path::new(&path).exists(); let path_exists = Path::new(&path).exists();
if !path_exists { if !path_exists {
@ -406,61 +375,11 @@ impl<'a> BaseClientBuilder<'a> {
path_exists 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. // Create a secure client that validates certificates.
let raw_client = self.create_client( let raw_client = self.create_client(
&user_agent_string, &user_agent_string,
timeout, timeout,
ssl_cert_file_exists, ssl_cert_file_exists,
ssl_cert_dir_exists,
Security::Secure, Security::Secure,
self.redirect_policy, self.redirect_policy,
); );
@ -470,7 +389,6 @@ impl<'a> BaseClientBuilder<'a> {
&user_agent_string, &user_agent_string,
timeout, timeout,
ssl_cert_file_exists, ssl_cert_file_exists,
ssl_cert_dir_exists,
Security::Insecure, Security::Insecure,
self.redirect_policy, self.redirect_policy,
); );
@ -483,7 +401,6 @@ impl<'a> BaseClientBuilder<'a> {
user_agent: &str, user_agent: &str,
timeout: Duration, timeout: Duration,
ssl_cert_file_exists: bool, ssl_cert_file_exists: bool,
ssl_cert_dir_exists: bool,
security: Security, security: Security,
redirect_policy: RedirectPolicy, redirect_policy: RedirectPolicy,
) -> Client { ) -> Client {
@ -502,7 +419,7 @@ impl<'a> BaseClientBuilder<'a> {
Security::Insecure => client_builder.danger_accept_invalid_certs(true), Security::Insecure => client_builder.danger_accept_invalid_certs(true),
}; };
let client_builder = if self.native_tls || ssl_cert_file_exists || ssl_cert_dir_exists { let client_builder = if self.native_tls || ssl_cert_file_exists {
client_builder.tls_built_in_native_certs(true) client_builder.tls_built_in_native_certs(true)
} else { } else {
client_builder.tls_built_in_webpki_certs(true) client_builder.tls_built_in_webpki_certs(true)
@ -583,7 +500,6 @@ impl<'a> BaseClientBuilder<'a> {
match self.auth_integration { match self.auth_integration {
AuthIntegration::Default => { AuthIntegration::Default => {
let mut auth_middleware = AuthMiddleware::new() let mut auth_middleware = AuthMiddleware::new()
.with_cache_arc(self.credentials_cache.clone())
.with_base_client(base_client) .with_base_client(base_client)
.with_indexes(self.indexes.clone()) .with_indexes(self.indexes.clone())
.with_keyring(self.keyring.to_provider()) .with_keyring(self.keyring.to_provider())
@ -595,7 +511,6 @@ impl<'a> BaseClientBuilder<'a> {
} }
AuthIntegration::OnlyAuthenticated => { AuthIntegration::OnlyAuthenticated => {
let mut auth_middleware = AuthMiddleware::new() let mut auth_middleware = AuthMiddleware::new()
.with_cache_arc(self.credentials_cache.clone())
.with_base_client(base_client) .with_base_client(base_client)
.with_indexes(self.indexes.clone()) .with_indexes(self.indexes.clone())
.with_keyring(self.keyring.to_provider()) .with_keyring(self.keyring.to_provider())
@ -639,8 +554,6 @@ pub struct BaseClient {
allow_insecure_host: Vec<TrustedHost>, allow_insecure_host: Vec<TrustedHost>,
/// The number of retries to attempt on transient errors. /// The number of retries to attempt on transient errors.
retries: u32, retries: u32,
/// Global authentication cache for a uv invocation to share credentials across uv clients.
credentials_cache: Arc<CredentialsCache>,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -692,10 +605,6 @@ impl BaseClient {
} }
builder.build_with_max_retries(self.retries) builder.build_with_max_retries(self.retries)
} }
pub fn credentials_cache(&self) -> &CredentialsCache {
&self.credentials_cache
}
} }
/// Wrapper around [`ClientWithMiddleware`] that manages redirects. /// Wrapper around [`ClientWithMiddleware`] that manages redirects.
@ -732,7 +641,6 @@ impl RedirectClientWithMiddleware {
match self.redirect_policy { match self.redirect_policy {
RedirectPolicy::BypassMiddleware => self.client.execute(req).await, RedirectPolicy::BypassMiddleware => self.client.execute(req).await,
RedirectPolicy::RetriggerMiddleware => self.execute_with_redirect_handling(req).await, RedirectPolicy::RetriggerMiddleware => self.execute_with_redirect_handling(req).await,
RedirectPolicy::NoRedirect => self.client.execute(req).await,
} }
} }

View File

@ -5,7 +5,6 @@ use std::fmt::{Display, Formatter};
use std::ops::Deref; use std::ops::Deref;
use std::path::PathBuf; use std::path::PathBuf;
use uv_cache::Error as CacheError;
use uv_distribution_filename::{WheelFilename, WheelFilenameError}; use uv_distribution_filename::{WheelFilename, WheelFilenameError};
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_redacted::DisplaySafeUrl; use uv_redacted::DisplaySafeUrl;
@ -338,9 +337,6 @@ pub enum ErrorKind {
#[error("Failed to write to the client cache")] #[error("Failed to write to the client cache")]
CacheWrite(#[source] std::io::Error), CacheWrite(#[source] std::io::Error),
#[error("Failed to acquire lock on the client cache")]
CacheLock(#[source] CacheError),
#[error(transparent)] #[error(transparent)]
Io(std::io::Error), Io(std::io::Error),

View File

@ -1,7 +1,7 @@
pub use base_client::{ pub use base_client::{
AuthIntegration, BaseClient, BaseClientBuilder, DEFAULT_MAX_REDIRECTS, DEFAULT_RETRIES, AuthIntegration, BaseClient, BaseClientBuilder, DEFAULT_RETRIES, ExtraMiddleware,
ExtraMiddleware, RedirectClientWithMiddleware, RedirectPolicy, RequestBuilder, RedirectClientWithMiddleware, RequestBuilder, RetryParsingError, UvRetryableStrategy,
RetryParsingError, UvRetryableStrategy, is_transient_network_error, is_transient_network_error,
}; };
pub use cached_client::{CacheControl, CachedClient, CachedClientError, DataWithCachePolicy}; pub use cached_client::{CacheControl, CachedClient, CachedClientError, DataWithCachePolicy};
pub use error::{Error, ErrorKind, WrappedReqwestError}; pub use error::{Error, ErrorKind, WrappedReqwestError};

View File

@ -12,7 +12,6 @@ use uv_version::version;
pub struct Installer { pub struct Installer {
pub name: Option<String>, pub name: Option<String>,
pub version: Option<String>, pub version: Option<String>,
pub subcommand: Option<Vec<String>>,
} }
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
@ -64,11 +63,7 @@ pub struct LineHaul {
impl LineHaul { impl LineHaul {
/// Initializes Linehaul information based on PEP 508 markers. /// Initializes Linehaul information based on PEP 508 markers.
#[instrument(name = "linehaul", skip_all)] #[instrument(name = "linehaul", skip_all)]
pub fn new( pub fn new(markers: Option<&MarkerEnvironment>, platform: Option<&Platform>) -> Self {
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 // https://github.com/pypa/pip/blob/24.0/src/pip/_internal/network/session.py#L87
let looks_like_ci = [ let looks_like_ci = [
EnvVars::BUILD_BUILDID, EnvVars::BUILD_BUILDID,
@ -128,7 +123,6 @@ impl LineHaul {
installer: Option::from(Installer { installer: Option::from(Installer {
name: Some("uv".to_string()), name: Some("uv".to_string()),
version: Some(version().to_string()), version: Some(version().to_string()),
subcommand,
}), }),
python: markers.map(|markers| markers.python_full_version().version.to_string()), python: markers.map(|markers| markers.python_full_version().version.to_string()),
implementation: Option::from(Implementation { implementation: Option::from(Implementation {

View File

@ -15,7 +15,7 @@ use tokio::sync::{Mutex, Semaphore};
use tracing::{Instrument, debug, info_span, instrument, trace, warn}; use tracing::{Instrument, debug, info_span, instrument, trace, warn};
use url::Url; use url::Url;
use uv_auth::{CredentialsCache, Indexes, PyxTokenStore}; use uv_auth::{Indexes, PyxTokenStore};
use uv_cache::{Cache, CacheBucket, CacheEntry, WheelCache}; use uv_cache::{Cache, CacheBucket, CacheEntry, WheelCache};
use uv_configuration::IndexStrategy; use uv_configuration::IndexStrategy;
use uv_configuration::KeyringProviderType; use uv_configuration::KeyringProviderType;
@ -148,30 +148,8 @@ impl<'a> RegistryClientBuilder<'a> {
self self
} }
/// Add all authenticated sources to the cache. pub fn build(self) -> RegistryClient {
pub fn cache_index_credentials(&mut self) { self.index_locations.cache_index_credentials();
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(); let index_urls = self.index_locations.index_urls();
// Build a base client // Build a base client
@ -202,8 +180,8 @@ impl<'a> RegistryClientBuilder<'a> {
} }
/// Share the underlying client between two different middleware configurations. /// Share the underlying client between two different middleware configurations.
pub fn wrap_existing(mut self, existing: &BaseClient) -> RegistryClient { pub fn wrap_existing(self, existing: &BaseClient) -> RegistryClient {
self.cache_index_credentials(); self.index_locations.cache_index_credentials();
let index_urls = self.index_locations.index_urls(); let index_urls = self.index_locations.index_urls();
// Wrap in any relevant middleware and handle connectivity. // Wrap in any relevant middleware and handle connectivity.
@ -291,10 +269,6 @@ impl RegistryClient {
self.timeout self.timeout
} }
pub fn credentials_cache(&self) -> &CredentialsCache {
self.client.uncached().credentials_cache()
}
/// Return the appropriate index URLs for the given [`PackageName`]. /// Return the appropriate index URLs for the given [`PackageName`].
fn index_urls_for( fn index_urls_for(
&self, &self,
@ -539,7 +513,7 @@ impl RegistryClient {
#[cfg(windows)] #[cfg(windows)]
let _lock = { let _lock = {
let lock_entry = cache_entry.with_file(format!("{package_name}.lock")); let lock_entry = cache_entry.with_file(format!("{package_name}.lock"));
lock_entry.lock().await.map_err(ErrorKind::CacheLock)? lock_entry.lock().await.map_err(ErrorKind::CacheWrite)?
}; };
let result = if matches!(index, IndexUrl::Path(_)) { let result = if matches!(index, IndexUrl::Path(_)) {
@ -1031,7 +1005,7 @@ impl RegistryClient {
#[cfg(windows)] #[cfg(windows)]
let _lock = { let _lock = {
let lock_entry = cache_entry.with_file(format!("{}.lock", filename.stem())); let lock_entry = cache_entry.with_file(format!("{}.lock", filename.stem()));
lock_entry.lock().await.map_err(ErrorKind::CacheLock)? lock_entry.lock().await.map_err(ErrorKind::CacheWrite)?
}; };
let response_callback = async |response: Response| { let response_callback = async |response: Response| {
@ -1115,7 +1089,7 @@ impl RegistryClient {
#[cfg(windows)] #[cfg(windows)]
let _lock = { let _lock = {
let lock_entry = cache_entry.with_file(format!("{}.lock", filename.stem())); let lock_entry = cache_entry.with_file(format!("{}.lock", filename.stem()));
lock_entry.lock().await.map_err(ErrorKind::CacheLock)? lock_entry.lock().await.map_err(ErrorKind::CacheWrite)?
}; };
// Attempt to fetch via a range request. // Attempt to fetch via a range request.

View File

@ -1,382 +0,0 @@
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
}

View File

@ -1,4 +1,2 @@
mod http_util;
mod remote_metadata; mod remote_metadata;
mod ssl_certs;
mod user_agent_version; mod user_agent_version;

View File

@ -11,7 +11,7 @@ use uv_redacted::DisplaySafeUrl;
#[tokio::test] #[tokio::test]
async fn remote_metadata_with_and_without_cache() -> Result<()> { async fn remote_metadata_with_and_without_cache() -> Result<()> {
let cache = Cache::temp()?.init().await?; let cache = Cache::temp()?.init()?;
let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build(); 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 // 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 filename = WheelFilename::from_str(url.rsplit_once('/').unwrap().1)?;
let dist = BuiltDist::DirectUrl(DirectUrlBuiltDist { let dist = BuiltDist::DirectUrl(DirectUrlBuiltDist {
filename, filename,
location: Box::new(DisplaySafeUrl::parse(url)?), location: Box::new(DisplaySafeUrl::parse(url).unwrap()),
url: VerbatimUrl::from_str(url)?, url: VerbatimUrl::from_str(url).unwrap(),
}); });
let capabilities = IndexCapabilities::default(); let capabilities = IndexCapabilities::default();
let metadata = client.wheel_metadata(&dist, &capabilities).await?; let metadata = client.wheel_metadata(&dist, &capabilities).await.unwrap();
assert_eq!(metadata.version.to_string(), "4.66.1"); assert_eq!(metadata.version.to_string(), "4.66.1");
} }

View File

@ -1,333 +0,0 @@
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(())
}

View File

@ -1,9 +1,16 @@
use std::str::FromStr;
use anyhow::Result; 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 insta::{assert_json_snapshot, assert_snapshot, with_settings};
use std::str::FromStr;
use tokio::net::TcpListener;
use url::Url; use url::Url;
use uv_cache::Cache; use uv_cache::Cache;
use uv_client::RegistryClientBuilder; use uv_client::RegistryClientBuilder;
use uv_client::{BaseClientBuilder, LineHaul}; use uv_client::{BaseClientBuilder, LineHaul};
@ -12,15 +19,39 @@ use uv_platform_tags::{Arch, Os, Platform};
use uv_redacted::DisplaySafeUrl; use uv_redacted::DisplaySafeUrl;
use uv_version::version; use uv_version::version;
use crate::http_util::start_http_user_agent_server;
#[tokio::test] #[tokio::test]
async fn test_user_agent_has_version() -> Result<()> { async fn test_user_agent_has_version() -> Result<()> {
// Initialize dummy http server // Set up the TCP listener on a random available port
let (server_task, addr) = start_http_user_agent_server().await?; 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 uv-client // Initialize uv-client
let cache = Cache::temp()?.init().await?; let cache = Cache::temp()?.init()?;
let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build(); let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build();
// Send request to our dummy server // Send request to our dummy server
@ -57,84 +88,47 @@ async fn test_user_agent_has_version() -> Result<()> {
assert_json_snapshot!(&linehaul.installer, @r#" assert_json_snapshot!(&linehaul.installer, @r#"
{ {
"name": "uv", "name": "uv",
"version": "[VERSION]", "version": "[VERSION]"
"subcommand": null
} }
"#); "#);
}); });
// Wait for the server task to complete, to be a good citizen. // Wait for the server task to complete, to be a good citizen.
let _ = server_task.await?; 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(()) Ok(())
} }
#[tokio::test] #[tokio::test]
async fn test_user_agent_has_linehaul() -> Result<()> { async fn test_user_agent_has_linehaul() -> Result<()> {
// Initialize dummy http server // Set up the TCP listener on a random available port
let (server_task, addr) = start_http_user_agent_server().await?; 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");
});
});
// Add some representative markers for an Ubuntu CI runner // Add some representative markers for an Ubuntu CI runner
let markers = MarkerEnvironment::try_from(MarkerEnvironmentBuilder { let markers = MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
@ -149,10 +143,11 @@ async fn test_user_agent_has_linehaul() -> Result<()> {
python_full_version: "3.12.2", python_full_version: "3.12.2",
python_version: "3.12", python_version: "3.12",
sys_platform: "linux", sys_platform: "linux",
})?; })
.unwrap();
// Initialize uv-client // Initialize uv-client
let cache = Cache::temp()?.init().await?; let cache = Cache::temp()?.init()?;
let mut builder = let mut builder =
RegistryClientBuilder::new(BaseClientBuilder::default(), cache).markers(&markers); RegistryClientBuilder::new(BaseClientBuilder::default(), cache).markers(&markers);
@ -194,7 +189,7 @@ async fn test_user_agent_has_linehaul() -> Result<()> {
let body = res.text().await?; let body = res.text().await?;
// Wait for the server task to complete, to be a good citizen. // Wait for the server task to complete, to be a good citizen.
let _ = server_task.await?; server_task.await?;
// Unpack User-Agent with linehaul // Unpack User-Agent with linehaul
let (uv_version, uv_linehaul) = body let (uv_version, uv_linehaul) = body
@ -215,12 +210,11 @@ async fn test_user_agent_has_linehaul() -> Result<()> {
assert_json_snapshot!(&linehaul, { assert_json_snapshot!(&linehaul, {
".distro" => "[distro]", ".distro" => "[distro]",
".ci" => "[ci]" ".ci" => "[ci]"
}, @r#" }, @r###"
{ {
"installer": { "installer": {
"name": "uv", "name": "uv",
"version": "[VERSION]", "version": "[VERSION]"
"subcommand": null
}, },
"python": "3.12.2", "python": "3.12.2",
"implementation": { "implementation": {
@ -238,7 +232,7 @@ async fn test_user_agent_has_linehaul() -> Result<()> {
"rustc_version": null, "rustc_version": null,
"ci": "[ci]" "ci": "[ci]"
} }
"#); "###);
}); });
// Assert distro // Assert distro

View File

@ -1,10 +1,10 @@
[package] [package]
name = "uv-configuration" name = "uv-configuration"
version = "0.0.8" version = "0.0.1"
description = "This is an internal component crate of uv"
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
homepage = { workspace = true } homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }

View File

@ -1,13 +0,0 @@
<!-- 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.

View File

@ -15,30 +15,4 @@ pub enum ExportFormat {
#[serde(rename = "pylock.toml", alias = "pylock-toml")] #[serde(rename = "pylock.toml", alias = "pylock-toml")]
#[cfg_attr(feature = "clap", clap(name = "pylock.toml", alias = "pylock-toml"))] #[cfg_attr(feature = "clap", clap(name = "pylock.toml", alias = "pylock-toml"))]
PylockToml, 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,
} }

View File

@ -94,32 +94,3 @@ wheels/
# Virtual environments # Virtual environments
.venv .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,
}
}
}

View File

@ -1,10 +1,11 @@
[package] [package]
name = "uv-console" name = "uv-console"
version = "0.0.8" version = "0.0.1"
description = "This is an internal component crate of uv" description = "Utilities for interacting with the terminal"
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
homepage = { workspace = true } homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }

View File

@ -1,13 +0,0 @@
<!-- 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.

View File

@ -1,12 +1,13 @@
[package] [package]
name = "uv-dev" name = "uv-dev"
version = "0.0.8" version = "0.0.1"
description = "This is an internal component crate of uv" description = "Build wheels from source distributions"
publish = false publish = false
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
homepage = { workspace = true } homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }
@ -79,4 +80,4 @@ performance-memory-allocator = ["dep:uv-performance-memory-allocator"]
render = ["poloto", "resvg", "tagu"] render = ["poloto", "resvg", "tagu"]
[package.metadata.cargo-shear] [package.metadata.cargo-shear]
ignored = ["uv-performance-memory-allocator"] ignored = ["flate2", "uv-extract", "uv-performance-memory-allocator"]

View File

@ -1,13 +0,0 @@
<!-- 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.

View File

@ -18,7 +18,7 @@ pub(crate) struct CompileArgs {
} }
pub(crate) async fn compile(args: CompileArgs) -> anyhow::Result<()> { pub(crate) async fn compile(args: CompileArgs) -> anyhow::Result<()> {
let cache = Cache::try_from(args.cache_args)?.init().await?; let cache = Cache::try_from(args.cache_args)?.init()?;
let interpreter = if let Some(python) = args.python { let interpreter = if let Some(python) = args.python {
python python

View File

@ -342,3 +342,31 @@ fn emit_possible_options(opt: &clap::Arg, output: &mut String) {
output.push_str(&markdown::to_html(&value)); 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 })
}
}

View File

@ -106,3 +106,31 @@ fn render(var: &str, doc: &str, added_in: Option<&str>) -> String {
format!("### `{var}`\n\n{doc}\n\n") 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 })
}
}

View File

@ -387,3 +387,31 @@ impl Visit for CollectOptionsVisitor {
self.fields.push((name.to_owned(), field)); 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 })
}
}

View File

@ -11,7 +11,7 @@ use crate::ROOT_DIR;
use crate::generate_all::Mode; use crate::generate_all::Mode;
/// Contains current supported targets /// Contains current supported targets
const TARGETS_YML_URL: &str = "https://raw.githubusercontent.com/astral-sh/python-build-standalone/refs/tags/20251209/cpython-unix/targets.yml"; const TARGETS_YML_URL: &str = "https://raw.githubusercontent.com/astral-sh/python-build-standalone/refs/tags/20251031/cpython-unix/targets.yml";
#[derive(clap::Args)] #[derive(clap::Args)]
pub(crate) struct Args { pub(crate) struct Args {
@ -130,7 +130,7 @@ async fn generate() -> Result<String> {
output.push_str("//! DO NOT EDIT\n"); output.push_str("//! DO NOT EDIT\n");
output.push_str("//!\n"); output.push_str("//!\n");
output.push_str("//! Generated with `cargo run dev generate-sysconfig-metadata`\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/20251209/cpython-unix/targets.yml>\n"); output.push_str("//! Targets from <https://github.com/astral-sh/python-build-standalone/blob/20251031/cpython-unix/targets.yml>\n");
output.push_str("//!\n"); output.push_str("//!\n");
// Disable clippy/fmt // Disable clippy/fmt

View File

@ -19,7 +19,7 @@ pub(crate) async fn list_packages(
args: ListPackagesArgs, args: ListPackagesArgs,
environment: EnvironmentOptions, environment: EnvironmentOptions,
) -> Result<()> { ) -> Result<()> {
let cache = Cache::try_from(args.cache_args)?.init().await?; let cache = Cache::try_from(args.cache_args)?.init()?;
let client = RegistryClientBuilder::new( let client = RegistryClientBuilder::new(
BaseClientBuilder::default().timeout(environment.http_timeout), BaseClientBuilder::default().timeout(environment.http_timeout),
cache, cache,

View File

@ -22,7 +22,7 @@ pub(crate) async fn validate_zip(
args: ValidateZipArgs, args: ValidateZipArgs,
environment: EnvironmentOptions, environment: EnvironmentOptions,
) -> Result<()> { ) -> Result<()> {
let cache = Cache::try_from(args.cache_args)?.init().await?; let cache = Cache::try_from(args.cache_args)?.init()?;
let client = RegistryClientBuilder::new( let client = RegistryClientBuilder::new(
BaseClientBuilder::default().timeout(environment.http_timeout), BaseClientBuilder::default().timeout(environment.http_timeout),
cache, cache,

View File

@ -23,7 +23,7 @@ pub(crate) async fn wheel_metadata(
args: WheelMetadataArgs, args: WheelMetadataArgs,
environment: EnvironmentOptions, environment: EnvironmentOptions,
) -> Result<()> { ) -> Result<()> {
let cache = Cache::try_from(args.cache_args)?.init().await?; let cache = Cache::try_from(args.cache_args)?.init()?;
let client = RegistryClientBuilder::new( let client = RegistryClientBuilder::new(
BaseClientBuilder::default().timeout(environment.http_timeout), BaseClientBuilder::default().timeout(environment.http_timeout),
cache, cache,

View File

@ -1,10 +1,11 @@
[package] [package]
name = "uv-dirs" name = "uv-dirs"
version = "0.0.8" version = "0.0.1"
description = "This is an internal component crate of uv" description = "Resolution of directories for storage of uv state"
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
homepage = { workspace = true } homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }

View File

@ -1,13 +0,0 @@
<!-- 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.

View File

@ -1,10 +1,11 @@
[package] [package]
name = "uv-dispatch" name = "uv-dispatch"
version = "0.0.8" version = "0.0.1"
description = "This is an internal component crate of uv" description = "Avoid cyclic crate dependencies between resolver, installer and builder"
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
homepage = { workspace = true } homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }

View File

@ -1,13 +0,0 @@
<!-- 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.

View File

@ -492,7 +492,6 @@ impl BuildContext for BuildDispatch<'_> {
environment_variables, environment_variables,
build_output, build_output,
self.concurrency.builds, self.concurrency.builds,
self.client.credentials_cache(),
self.preview, self.preview,
) )
.boxed_local() .boxed_local()
@ -505,7 +504,6 @@ impl BuildContext for BuildDispatch<'_> {
source: &'data Path, source: &'data Path,
subdirectory: Option<&'data Path>, subdirectory: Option<&'data Path>,
output_dir: &'data Path, output_dir: &'data Path,
sources: SourceStrategy,
build_kind: BuildKind, build_kind: BuildKind,
version_id: Option<&'data str>, version_id: Option<&'data str>,
) -> Result<Option<DistFilename>, BuildDispatchError> { ) -> Result<Option<DistFilename>, BuildDispatchError> {
@ -534,7 +532,6 @@ impl BuildContext for BuildDispatch<'_> {
&output_dir, &output_dir,
None, None,
uv_version::version(), uv_version::version(),
sources == SourceStrategy::Enabled,
)?; )?;
DistFilename::WheelFilename(wheel) DistFilename::WheelFilename(wheel)
} }
@ -543,7 +540,6 @@ impl BuildContext for BuildDispatch<'_> {
&source_tree, &source_tree,
&output_dir, &output_dir,
uv_version::version(), uv_version::version(),
sources == SourceStrategy::Enabled,
)?; )?;
DistFilename::SourceDistFilename(source_dist) DistFilename::SourceDistFilename(source_dist)
} }
@ -553,7 +549,6 @@ impl BuildContext for BuildDispatch<'_> {
&output_dir, &output_dir,
None, None,
uv_version::version(), uv_version::version(),
sources == SourceStrategy::Enabled,
)?; )?;
DistFilename::WheelFilename(wheel) DistFilename::WheelFilename(wheel)
} }

View File

@ -1,10 +1,10 @@
[package] [package]
name = "uv-distribution-filename" name = "uv-distribution-filename"
version = "0.0.8" version = "0.0.1"
description = "This is an internal component crate of uv"
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
homepage = { workspace = true } homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }

View File

@ -1,13 +0,0 @@
<!-- 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.

View File

@ -1,10 +1,10 @@
[package] [package]
name = "uv-distribution-types" name = "uv-distribution-types"
version = "0.0.8" version = "0.0.1"
description = "This is an internal component crate of uv"
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
homepage = { workspace = true } homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }

View File

@ -1,13 +0,0 @@
<!-- 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.

View File

@ -8,6 +8,7 @@ use std::sync::{Arc, LazyLock, RwLock};
use itertools::Either; use itertools::Either;
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use thiserror::Error; use thiserror::Error;
use tracing::trace;
use url::{ParseError, Url}; use url::{ParseError, Url};
use uv_auth::RealmRef; use uv_auth::RealmRef;
use uv_cache_key::CanonicalUrl; use uv_cache_key::CanonicalUrl;
@ -439,6 +440,26 @@ 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. /// Return the Simple API cache control header for an [`IndexUrl`], if configured.
pub fn simple_api_cache_control_for(&self, url: &IndexUrl) -> Option<&str> { pub fn simple_api_cache_control_for(&self, url: &IndexUrl) -> Option<&str> {
for index in &self.indexes { for index in &self.indexes {

View File

@ -159,9 +159,9 @@ pub enum InstalledVersion<'a> {
Url(&'a DisplaySafeUrl, &'a Version), Url(&'a DisplaySafeUrl, &'a Version),
} }
impl<'a> InstalledVersion<'a> { impl InstalledVersion<'_> {
/// If it is a URL, return its value. /// If it is a URL, return its value.
pub fn url(&self) -> Option<&'a DisplaySafeUrl> { pub fn url(&self) -> Option<&DisplaySafeUrl> {
match self { match self {
Self::Version(_) => None, Self::Version(_) => None,
Self::Url(url, _) => Some(url), Self::Url(url, _) => Some(url),
@ -169,7 +169,7 @@ impl<'a> InstalledVersion<'a> {
} }
/// If it is a version, return its value. /// If it is a version, return its value.
pub fn version(&self) -> &'a Version { pub fn version(&self) -> &Version {
match self { match self {
Self::Version(version) => version, Self::Version(version) => version,
Self::Url(_, version) => version, Self::Url(_, version) => version,

View File

@ -7,7 +7,7 @@ use thiserror::Error;
use uv_cache_key::{CacheKey, CacheKeyHasher}; use uv_cache_key::{CacheKey, CacheKeyHasher};
use uv_distribution_filename::DistExtension; use uv_distribution_filename::DistExtension;
use uv_fs::{CWD, PortablePath, PortablePathBuf, relative_to}; use uv_fs::{CWD, PortablePath, PortablePathBuf, relative_to};
use uv_git_types::{GitLfs, GitOid, GitReference, GitUrl, GitUrlParseError, OidParseError}; use uv_git_types::{GitOid, GitReference, GitUrl, GitUrlParseError, OidParseError};
use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep440::VersionSpecifiers; use uv_pep440::VersionSpecifiers;
use uv_pep508::{ use uv_pep508::{
@ -350,13 +350,6 @@ impl Display for Requirement {
if let Some(subdirectory) = subdirectory { if let Some(subdirectory) = subdirectory {
writeln!(f, "#subdirectory={}", subdirectory.display())?; writeln!(f, "#subdirectory={}", subdirectory.display())?;
} }
if git.lfs().enabled() {
writeln!(
f,
"{}lfs=true",
if subdirectory.is_some() { "&" } else { "#" }
)?;
}
} }
RequirementSource::Path { url, .. } => { RequirementSource::Path { url, .. } => {
write!(f, " @ {url}")?; write!(f, " @ {url}")?;
@ -443,9 +436,6 @@ impl CacheKey for Requirement {
} else { } else {
0u8.cache_key(state); 0u8.cache_key(state);
} }
if git.lfs().enabled() {
1u8.cache_key(state);
}
url.cache_key(state); url.cache_key(state);
} }
RequirementSource::Path { RequirementSource::Path {
@ -775,13 +765,6 @@ impl Display for RequirementSource {
if let Some(subdirectory) = subdirectory { if let Some(subdirectory) = subdirectory {
writeln!(f, "#subdirectory={}", subdirectory.display())?; writeln!(f, "#subdirectory={}", subdirectory.display())?;
} }
if git.lfs().enabled() {
writeln!(
f,
"{}lfs=true",
if subdirectory.is_some() { "&" } else { "#" }
)?;
}
} }
Self::Path { url, .. } => { Self::Path { url, .. } => {
write!(f, "{url}")?; write!(f, "{url}")?;
@ -873,11 +856,6 @@ impl From<RequirementSource> for RequirementSourceWire {
.append_pair("subdirectory", &subdirectory); .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. // Put the requested reference in the query.
match git.reference() { match git.reference() {
GitReference::Branch(branch) => { GitReference::Branch(branch) => {
@ -954,7 +932,6 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
let mut reference = GitReference::DefaultBranch; let mut reference = GitReference::DefaultBranch;
let mut subdirectory: Option<PortablePathBuf> = None; let mut subdirectory: Option<PortablePathBuf> = None;
let mut lfs = GitLfs::Disabled;
for (key, val) in repository.query_pairs() { for (key, val) in repository.query_pairs() {
match &*key { match &*key {
"tag" => reference = GitReference::Tag(val.into_owned()), "tag" => reference = GitReference::Tag(val.into_owned()),
@ -963,7 +940,6 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
"subdirectory" => { "subdirectory" => {
subdirectory = Some(PortablePathBuf::from(val.as_ref())); subdirectory = Some(PortablePathBuf::from(val.as_ref()));
} }
"lfs" => lfs = GitLfs::from(val.eq_ignore_ascii_case("true")),
_ => {} _ => {}
} }
} }
@ -983,22 +959,13 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
let path = format!("{}@{}", url.path(), rev); let path = format!("{}@{}", url.path(), rev);
url.set_path(&path); url.set_path(&path);
} }
let mut frags: Vec<String> = Vec::new();
if let Some(subdirectory) = subdirectory.as_ref() { if let Some(subdirectory) = subdirectory.as_ref() {
frags.push(format!("subdirectory={subdirectory}")); url.set_fragment(Some(&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); let url = VerbatimUrl::from_url(url);
Ok(Self::Git { Ok(Self::Git {
git: GitUrl::from_fields(repository, reference, precise, lfs)?, git: GitUrl::from_fields(repository, reference, precise)?,
subdirectory: subdirectory.map(Box::<Path>::from), subdirectory: subdirectory.map(Box::<Path>::from),
url, url,
}) })

Some files were not shown because too many files have changed in this diff Show More