diff --git a/.github/actions/setup-safe-chain/action.yml b/.github/actions/setup-safe-chain/action.yml new file mode 100644 index 0000000000000..8d7ff92aee354 --- /dev/null +++ b/.github/actions/setup-safe-chain/action.yml @@ -0,0 +1,68 @@ +name: "Setup Aikido Safe Chain (CI)" +description: > + Installs a pinned safe-chain release after verifying the installer script SHA-256 (no curl-to-sh). + Sets SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS so npm/PyPI installs are blocked when versions are too new. + Bump SAFE_CHAIN_VERSION and SAFE_CHAIN_INSTALL_SCRIPT_SHA256 together (digest from GitHub release asset API). + +inputs: + minimum_package_age_hours: + description: "Minimum age in hours before a package version may be installed (npm and PyPI per safe-chain)." + required: false + default: "72" + safe_chain_version: + description: "The version of safe-chain to install." + required: false + default: "1.4.6" + safe_chain_install_script_sha256: + description: "The SHA-256 of the safe-chain install script." + required: false + default: "1c49baa0d40285cf249364c5274ae2d0f11a5c65fb3aae23750dd06f91d9a356" + +runs: + using: composite + steps: + - name: Cache Aikido Safe Chain binary + id: safe-chain-cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + env: + SAFE_CHAIN_VERSION: ${{ inputs.safe_chain_version }} + with: + path: ~/.safe-chain + key: safe-chain-${{ runner.os }}-${{ runner.arch }}-${{ env.SAFE_CHAIN_VERSION }} + + - name: Install Aikido Safe Chain (pinned + SHA-256 verified) + if: steps.safe-chain-cache.outputs.cache-hit != 'true' + shell: bash + env: + SAFE_CHAIN_VERSION: ${{ inputs.safe_chain_version }} + SAFE_CHAIN_INSTALL_SCRIPT_SHA256: ${{ inputs.safe_chain_install_script_sha256 }} + GITHUB_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + url="https://github.com/AikidoSec/safe-chain/releases/download/${SAFE_CHAIN_VERSION}/install-safe-chain.sh" + tmp="$(mktemp)" + curl -fsSL "$url" -H "Authorization: Bearer $GITHUB_TOKEN" -o "$tmp" + if command -v sha256sum &>/dev/null; then + actual=$(sha256sum "$tmp" | awk '{print $1}') + else + actual=$(shasum -a 256 "$tmp" | awk '{print $1}') + fi + if [ "$actual" != "$SAFE_CHAIN_INSTALL_SCRIPT_SHA256" ]; then + echo "::error::safe-chain install script SHA-256 mismatch (expected ${SAFE_CHAIN_INSTALL_SCRIPT_SHA256}, got ${actual}). Refusing to run." + rm -f "$tmp" + exit 1 + fi + bash "$tmp" --ci + rm -f "$tmp" + + - name: Configure Aikido Safe Chain + shell: bash + env: + SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS: ${{ inputs.minimum_package_age_hours }} + run: | + echo "$HOME/.safe-chain/shims" >> "$GITHUB_PATH" + echo "$HOME/.safe-chain/bin" >> "$GITHUB_PATH" + { + echo "SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS=${SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS}" + echo "SAFE_CHAIN_LOGGING=silent" + } >> "$GITHUB_ENV" diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml index bee6b878e1941..092ebc8b5a071 100644 --- a/.github/workflows/build-release-binaries.yml +++ b/.github/workflows/build-release-binaries.yml @@ -1,27 +1,17 @@ -# Build uv on all platforms. -# -# Generates both wheels (for PyPI) and archived binaries (for GitHub releases). +# Build uv release binaries for our target platforms. # # Called from: -# - .github/workflows/ci.yml (when release-relevant files change) -# - .github/workflows/release.yml (as a local artifacts job within `cargo-dist`) +# - .github/workflows/release.yml name: "Build release binaries" on: workflow_call: - inputs: - plan: - required: false - type: string concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: - PACKAGE_NAME: uv - MODULE_NAME: uv - PYTHON_VERSION: "3.11" CARGO_INCREMENTAL: 0 CARGO_NET_RETRY: 10 CARGO_TERM_COLOR: always @@ -30,378 +20,26 @@ env: permissions: {} jobs: - sdist: - name: sdist - runs-on: depot-ubuntu-24.04-4 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - - # uv - - name: "Prep README.md" - run: python scripts/transform_readme.py --target pypi - - name: "Build sdist" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - command: sdist - args: --out dist - - name: "Test sdist" - run: | - # We can't use `--find-links` here, since we need maturin, which means no `--no-index`, and without that option - # we run the risk that pip pull uv from PyPI instead. - pip install dist/${PACKAGE_NAME}-*.tar.gz --force-reinstall - ${MODULE_NAME} --help - python -m ${MODULE_NAME} --help - uvx --help - - name: "Upload sdist" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv-sdist - path: dist - - # uv-build - - name: "Build sdist uv-build" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - command: sdist - args: --out crates/uv-build/dist -m crates/uv-build/Cargo.toml - - name: "Fix Cargo.lock in sdist uv-build" - run: python scripts/repair-sdist-cargo-lock.py crates/uv-build/dist/${PACKAGE_NAME}_build-*.tar.gz - - name: "Test sdist uv-build" - run: | - pip install crates/uv-build/dist/${PACKAGE_NAME}_build-*.tar.gz --force-reinstall - ${MODULE_NAME}-build --help - python -m ${MODULE_NAME}_build --help - - name: "Upload sdist uv-build" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv_build-sdist - path: crates/uv-build/dist - - macos-x86_64: - name: x86_64-apple-darwin - runs-on: depot-macos-14 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - architecture: x64 - - name: "Prep README.md" - run: python scripts/transform_readme.py --target pypi - - name: "Install cargo extensions" - shell: bash - run: scripts/install-cargo-extensions.sh - - # uv - - name: "Build wheels - x86_64" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: x86_64 - args: --release --locked --out dist --features self-update --compatibility pypi - env: - CARGO: ${{ github.workspace }}/scripts/cargo.sh - - name: "Upload wheels" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv-macos-x86_64 - path: dist - - name: "Archive binary" - run: | - TARGET=x86_64-apple-darwin - ARCHIVE_NAME=uv-$TARGET - ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz - - mkdir -p $ARCHIVE_NAME - cp target/$TARGET/release/uv $ARCHIVE_NAME/uv - cp target/$TARGET/release/uvx $ARCHIVE_NAME/uvx - tar czvf $ARCHIVE_FILE $ARCHIVE_NAME - shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - - name: "Upload binary" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: artifacts-macos-x86_64 - path: | - *.tar.gz - *.sha256 - - # uv-build - - name: "Build wheels uv-build - x86_64" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: x86_64 - args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi - env: - CARGO: ${{ github.workspace }}/scripts/cargo.sh - - name: "Upload wheels uv-build" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv_build-macos-x86_64 - path: crates/uv-build/dist - macos-aarch64: name: aarch64-apple-darwin - runs-on: depot-macos-14 + runs-on: macos-14 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - architecture: arm64 - - name: "Prep README.md" - run: python scripts/transform_readme.py --target pypi - - name: "Install cargo extensions" - shell: bash - run: scripts/install-cargo-extensions.sh + - name: "Setup Aikido Safe Chain" + uses: ./.github/actions/setup-safe-chain - # uv - - name: "Build wheels - aarch64" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: aarch64 - manylinux: 2_17 - args: --release --locked --out dist --features self-update --compatibility pypi - env: - CARGO: ${{ github.workspace }}/scripts/cargo.sh - - name: "Test wheel - aarch64" - run: | - pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall - ${MODULE_NAME} --help - python -m ${MODULE_NAME} --help - uvx --help - - name: "Upload wheels" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv-aarch64-apple-darwin - path: dist - - name: "Archive binary" - run: | - TARGET=aarch64-apple-darwin - ARCHIVE_NAME=uv-$TARGET - ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz + - name: "Install Rust toolchain" + run: rustup target add aarch64-apple-darwin - mkdir -p $ARCHIVE_NAME - cp target/$TARGET/release/uv $ARCHIVE_NAME/uv - cp target/$TARGET/release/uvx $ARCHIVE_NAME/uvx - tar czvf $ARCHIVE_FILE $ARCHIVE_NAME - shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - - name: "Upload binary" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: artifacts-aarch64-apple-darwin - path: | - *.tar.gz - *.sha256 + - name: "Build" + run: cargo build --release --locked --target aarch64-apple-darwin --features self-update - # uv-build - - name: "Build wheels uv-build - aarch64" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: aarch64 - args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi - env: - CARGO: ${{ github.workspace }}/scripts/cargo.sh - - name: "Test wheel - aarch64" - run: | - pip install ${PACKAGE_NAME}_build --no-index --find-links crates/uv-build/dist --force-reinstall - ${MODULE_NAME}-build --help - python -m ${MODULE_NAME}_build --help - - name: "Upload wheels uv-build" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv_build-aarch64-apple-darwin - path: crates/uv-build/dist - - windows: - name: ${{ matrix.platform.target }} - runs-on: ${{ matrix.platform.runner }} - strategy: - matrix: - platform: - - target: x86_64-pc-windows-msvc - arch: x64 - runner: github-windows-2025-x86_64-8 - - target: i686-pc-windows-msvc - arch: x86 - runner: github-windows-2025-x86_64-8 - - target: aarch64-pc-windows-msvc - arch: arm64 - runner: github-windows-11-aarch64-8 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - architecture: ${{ matrix.platform.arch }} - - name: "Prep README.md" - run: python scripts/transform_readme.py --target pypi - - name: "Install cargo extensions" - shell: bash - run: scripts/install-cargo-extensions.sh - - name: "Install NASM" - # NASM is required for x86/x86-64 Windows targets by aws-lc-sys. - # On aarch64-pc-windows-msvc, it uses clang-cl instead. - # See: https://aws.github.io/aws-lc-rs/requirements/windows.html#build-requirements - if: contains(matrix.platform.target, 'x86') || contains(matrix.platform.target, 'i686') - run: | - winget install NASM.NASM --accept-source-agreements --accept-package-agreements - echo "C:\Program Files\NASM" | Out-File -FilePath $env:GITHUB_PATH -Append - # uv - - name: "Build wheels" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: ${{ matrix.platform.target }} - args: --release --locked --out dist --features self-update,windows-gui-bin --compatibility pypi - env: - CARGO: ${{ github.workspace }}/scripts/cargo.cmd - # Disable prebuilt NASM objects so we always compile assembly from source. - AWS_LC_SYS_PREBUILT_NASM: "0" - - name: "Test wheel" - shell: bash - run: | - pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall - ${MODULE_NAME} --help - python -m ${MODULE_NAME} --help - uvx --help - uvw --help - - name: "Upload wheels" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv-${{ matrix.platform.target }} - path: dist - - name: "Archive binary" - shell: bash - run: | - ARCHIVE_FILE=uv-${PLATFORM_TARGET}.zip - 7z a $ARCHIVE_FILE ./target/${PLATFORM_TARGET}/release/uv.exe - 7z a $ARCHIVE_FILE ./target/${PLATFORM_TARGET}/release/uvx.exe - 7z a $ARCHIVE_FILE ./target/${PLATFORM_TARGET}/release/uvw.exe - sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - env: - PLATFORM_TARGET: ${{ matrix.platform.target }} - - name: "Upload binary" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: artifacts-${{ matrix.platform.target }} - path: | - *.zip - *.sha256 - - # uv-build - - name: "Build wheels uv-build" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: ${{ matrix.platform.target }} - args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi - env: - CARGO: ${{ github.workspace }}/scripts/cargo.cmd - - name: "Test wheel uv-build" - shell: bash - run: | - pip install ${PACKAGE_NAME}_build --no-index --find-links crates/uv-build/dist --force-reinstall - ${MODULE_NAME}-build --help - python -m ${MODULE_NAME}_build --help - - name: "Upload wheels uv-build" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv_build-${{ matrix.platform.target }} - path: crates/uv-build/dist - - linux: - name: ${{ matrix.target }} - runs-on: depot-ubuntu-latest-4 - strategy: - matrix: - include: - - { target: "i686-unknown-linux-gnu", cc: "gcc -m32" } - - { target: "x86_64-unknown-linux-gnu", cc: "gcc" } - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - architecture: x64 - - name: "Prep README.md" - run: python scripts/transform_readme.py --target pypi - - # uv - - name: "Build wheels" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: ${{ matrix.target }} - # Generally, we try to build in a target docker container. In this case however, a - # 32-bit compiler runs out of memory (4GB memory limit for 32-bit), so we cross compile - # from 64-bit version of the container, breaking the pattern from other builds. - container: quay.io/pypa/manylinux2014 - manylinux: 2_17 - docker-options: -e CARGO - args: --release --locked --out dist --features self-update --compatibility pypi - before-script-linux: | - # Install the 32-bit cross target on 64-bit (noop if we're already on 64-bit) - rustup target add ${{ matrix.target }} - # If we're running on rhel centos, install needed packages. - if command -v yum &> /dev/null; then - yum update -y && yum install -y pkgconfig libatomic - - # Install cross build requirements - if [[ "${{ matrix.target }}" == "i686-unknown-linux-gnu" ]]; then - yum install -y glibc-devel.i686 libstdc++-devel.i686 libatomic.i686 - fi - - # Symlink libatomic so the linker can find it with -latomic. - if [[ -f "/usr/lib/libatomic.so.1" && ! -f "/usr/lib/libatomic.so" ]]; then - ln -s /usr/lib/libatomic.so.1 /usr/lib/libatomic.so - fi - else - # If we're running on debian-based system. - apt update -y && apt-get install -y pkg-config - fi - # Install cargo extensions as a static musl binary so it runs in any container. - scripts/install-cargo-extensions.sh - env: - CC: ${{ matrix.cc }} - CARGO: ${{ github.workspace }}/scripts/cargo.sh - - name: "Test wheel" - if: ${{ startsWith(matrix.target, 'x86_64') }} - run: | - pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall - ${MODULE_NAME} --help - python -m ${MODULE_NAME} --help - uvx --help - - name: "Upload wheels" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv-${{ matrix.target }} - path: dist - name: "Archive binary" - shell: bash run: | + TARGET=aarch64-apple-darwin ARCHIVE_NAME=uv-$TARGET ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz @@ -410,237 +48,38 @@ jobs: cp target/$TARGET/release/uvx $ARCHIVE_NAME/uvx tar czvf $ARCHIVE_FILE $ARCHIVE_NAME shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - env: - TARGET: ${{ matrix.target }} - - name: "Upload binary" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: artifacts-${{ matrix.target }} - path: | - *.tar.gz - *.sha256 - - # uv-build - - name: "Build wheels uv-build" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: ${{ matrix.target }} - manylinux: 2_17 - docker-options: -e CARGO - args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi - before-script-linux: | - scripts/install-cargo-extensions.sh - env: - CARGO: ${{ github.workspace }}/scripts/cargo.sh - - name: "Test wheel uv-build" - if: ${{ startsWith(matrix.target, 'x86_64') }} - run: | - pip install ${PACKAGE_NAME}_build --no-index --find-links crates/uv-build/dist --force-reinstall - ${MODULE_NAME}-build --help - python -m ${MODULE_NAME}_build --help - - name: "Upload wheels uv-build" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv_build-${{ matrix.target }} - path: crates/uv-build/dist - - linux-arm: - name: ${{ matrix.platform.target }} - runs-on: depot-ubuntu-24.04-8 - timeout-minutes: 30 - strategy: - matrix: - platform: - - target: aarch64-unknown-linux-gnu - arch: aarch64 - # see https://github.com/astral-sh/ruff/issues/3791 - # and https://github.com/gnzlbg/jemallocator/issues/170#issuecomment-1503228963 - maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16 - # Build fails with 2_17 container: https://github.com/astral-sh/uv/actions/runs/20850906093/job/59905482208?pr=17358 - manylinux: 2_28 - - target: armv7-unknown-linux-gnueabihf - arch: armv7 - manylinux: 2_17 - - target: arm-unknown-linux-musleabihf - arch: arm - # Special case: armv6l is linux_armv6l, no manylinux or musllinux. - # "auto" instead of "off" to get the cross container - manylinux: auto - - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - - name: "Prep README.md" - run: python scripts/transform_readme.py --target pypi - # uv - - name: "Build wheels" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: ${{ matrix.platform.target }} - manylinux: ${{ matrix.platform.manylinux }} - docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} - args: --release --locked --out dist --features self-update --compatibility pypi - before-script-linux: | - scripts/install-cargo-extensions.sh - env: - CARGO: ${{ github.workspace }}/scripts/cargo.sh - # TODO(zanieb): Find an alternative for this action; it uses EOL Node 20 - - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 - name: "Test wheel" - with: - arch: ${{ matrix.platform.arch == 'arm' && 'armv6' || matrix.platform.arch }} - distro: ${{ matrix.platform.arch == 'arm' && 'bullseye' || 'ubuntu20.04' }} - install: | - apt-get update - apt-get install -y --no-install-recommends python3 python3-pip python-is-python3 - pip3 install -U pip - run: | - pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall - ${MODULE_NAME} --help - # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. - # python -m ${MODULE_NAME} --help - uvx --help - env: | - PACKAGE_NAME: ${{ env.PACKAGE_NAME }} - MODULE_NAME: ${{ env.MODULE_NAME }} - - name: "Upload wheels" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv-${{ matrix.platform.target }} - path: dist - - name: "Archive binary" - shell: bash - run: | - ARCHIVE_NAME=uv-$TARGET - ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz + - name: "Test binary" + run: ./uv-aarch64-apple-darwin/uv --version - mkdir -p $ARCHIVE_NAME - cp target/$TARGET/release/uv $ARCHIVE_NAME/uv - cp target/$TARGET/release/uvx $ARCHIVE_NAME/uvx - tar czvf $ARCHIVE_FILE $ARCHIVE_NAME - shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - env: - TARGET: ${{ matrix.platform.target }} - name: "Upload binary" uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: - name: artifacts-${{ matrix.platform.target }} + name: artifacts-aarch64-apple-darwin path: | *.tar.gz *.sha256 - # uv-build - - name: "Build wheels uv-build" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: ${{ matrix.platform.target }} - manylinux: ${{ matrix.platform.manylinux }} - docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} - args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi - before-script-linux: | - scripts/install-cargo-extensions.sh - env: - CARGO: ${{ github.workspace }}/scripts/cargo.sh - # TODO(zanieb): Find an alternative for this action; it uses EOL Node 20 - - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 - name: "Test wheel uv-build" - with: - arch: ${{ matrix.platform.arch == 'arm' && 'armv6' || matrix.platform.arch }} - distro: ${{ matrix.platform.arch == 'arm' && 'bullseye' || 'ubuntu20.04' }} - install: | - apt-get update - apt-get install -y --no-install-recommends python3 python3-pip python-is-python3 - pip3 install -U pip - run: | - pip install ${PACKAGE_NAME}_build --no-index --find-links crates/uv-build/dist --force-reinstall - ${MODULE_NAME}-build --help - # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. - # python -m ${MODULE_NAME}_build --help - env: | - PACKAGE_NAME: ${{ env.PACKAGE_NAME }} - MODULE_NAME: ${{ env.MODULE_NAME }} - - name: "Upload wheels uv-build" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv_build-${{ matrix.platform.target }} - path: crates/uv-build/dist - - # Like `linux-arm`. - linux-s390x: - name: ${{ matrix.platform.target }} - timeout-minutes: 30 - runs-on: depot-ubuntu-latest-4 - strategy: - matrix: - platform: - - target: s390x-unknown-linux-gnu - arch: s390x - + linux-x86_64: + name: x86_64-unknown-linux-gnu + runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - - name: "Prep README.md" - run: python scripts/transform_readme.py --target pypi + - name: "Setup Aikido Safe Chain" + uses: ./.github/actions/setup-safe-chain - # uv - - name: "Build wheels" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: ${{ matrix.platform.target }} - manylinux: 2_17 - docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} - args: --release --locked --out dist --features self-update --compatibility pypi - rust-toolchain: ${{ matrix.platform.toolchain || null }} - before-script-linux: | - scripts/install-cargo-extensions.sh - # Install the s390x cross target on x86_64 - rustup target add ${{ matrix.platform.target }} - apt-get update && apt-get install -y gcc-s390x-linux-gnu binutils-s390x-linux-gnu - env: - CARGO: ${{ github.workspace }}/scripts/cargo.sh + - name: "Install Rust toolchain" + run: rustup target add x86_64-unknown-linux-gnu + + - name: "Build" + run: cargo build --release --locked --target x86_64-unknown-linux-gnu --features self-update - # TODO(zanieb): Find an alternative for this action; it uses EOL Node 20 - - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 - name: "Test wheel" - with: - arch: ${{ matrix.platform.arch }} - distro: ubuntu20.04 - install: | - apt-get update - apt-get install -y --no-install-recommends python3 python3-pip python-is-python3 - pip3 install -U pip - run: | - pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall - ${MODULE_NAME} --help - # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. - # python -m ${MODULE_NAME} --help - uvx --help - env: | - PACKAGE_NAME: ${{ env.PACKAGE_NAME }} - MODULE_NAME: ${{ env.MODULE_NAME }} - - name: "Upload wheels" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv-${{ matrix.platform.target }} - path: dist - name: "Archive binary" - shell: bash run: | + TARGET=x86_64-unknown-linux-gnu ARCHIVE_NAME=uv-$TARGET ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz @@ -648,337 +87,48 @@ jobs: cp target/$TARGET/release/uv $ARCHIVE_NAME/uv cp target/$TARGET/release/uvx $ARCHIVE_NAME/uvx tar czvf $ARCHIVE_FILE $ARCHIVE_NAME - shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - env: - TARGET: ${{ matrix.platform.target }} - - name: "Upload binary" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: artifacts-${{ matrix.platform.target }} - path: | - *.tar.gz - *.sha256 - - # uv-build - - name: "Build wheels uv-build" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: ${{ matrix.platform.target }} - manylinux: 2_17 - docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} - args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi - before-script-linux: | - scripts/install-cargo-extensions.sh - env: - CARGO: ${{ github.workspace }}/scripts/cargo.sh - # TODO(zanieb): Find an alternative for this action; it uses EOL Node 20 - - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 - name: "Test wheel uv-build" - with: - arch: ${{ matrix.platform.arch }} - distro: ubuntu20.04 - install: | - apt-get update - apt-get install -y --no-install-recommends python3 python3-pip python-is-python3 - pip3 install -U pip - run: | - pip install ${PACKAGE_NAME}-build --no-index --find-links crates/uv-build/dist --force-reinstall - ${MODULE_NAME}-build --help - # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. - # python -m ${MODULE_NAME}-build --help - env: | - PACKAGE_NAME: ${{ env.PACKAGE_NAME }} - MODULE_NAME: ${{ env.MODULE_NAME }} - - name: "Upload wheels uv-build" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv_build-${{ matrix.platform.target }} - path: crates/uv-build/dist - - # Like `linux-arm`, but install the `gcc-powerpc64-linux-gnu` package. - linux-powerpc: - name: ${{ matrix.platform.target }} - runs-on: depot-ubuntu-24.04-4 - strategy: - matrix: - platform: - - target: powerpc64le-unknown-linux-gnu - arch: ppc64le - # see https://github.com/astral-sh/uv/issues/6528 - maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16 - - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - - name: "Prep README.md" - run: python scripts/transform_readme.py --target pypi + - name: "Test binary" + run: ./uv-x86_64-unknown-linux-gnu/uv --version - # uv - - name: "Build wheels" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: ${{ matrix.platform.target }} - manylinux: 2_17 - docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} - args: --release --locked --out dist --features self-update --compatibility pypi - before-script-linux: | - if command -v yum &> /dev/null; then - yum update -y - yum -y install epel-release - yum repolist - yum install -y gcc-powerpc64-linux-gnu - fi - scripts/install-cargo-extensions.sh - env: - CARGO: ${{ github.workspace }}/scripts/cargo.sh - # TODO(charlie): Re-enable testing for PPC wheels. - # - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 - # name: "Test wheel" - # with: - # arch: ${{ matrix.platform.arch }} - # distro: ubuntu20.04 - # install: | - # apt-get update - # apt-get install -y --no-install-recommends python3 python3-pip python-is-python3 - # pip3 install -U pip - # run: | - # pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall - # ${MODULE_NAME} --help - # #(konsti) TODO: Enable this test on all platforms,currently `find_uv_bin` is failingto discover uv here. - # # python -m ${MODULE_NAME} --helppython -m ${MODULE_NAME} --help - # uvx --help - - name: "Upload wheels" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv-${{ matrix.platform.target }} - path: dist - - name: "Archive binary" - shell: bash - run: | - ARCHIVE_NAME=uv-$TARGET - ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz - - mkdir -p $ARCHIVE_NAME - cp target/$TARGET/release/uv $ARCHIVE_NAME/uv - cp target/$TARGET/release/uvx $ARCHIVE_NAME/uvx - tar czvf $ARCHIVE_FILE $ARCHIVE_NAME - shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - env: - TARGET: ${{ matrix.platform.target }} - name: "Upload binary" uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: - name: artifacts-${{ matrix.platform.target }} + name: artifacts-x86_64-unknown-linux-gnu path: | *.tar.gz *.sha256 - # uv-build - - name: "Build wheels uv-build" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: ${{ matrix.platform.target }} - manylinux: 2_17 - docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} - args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi - before-script-linux: | - if command -v yum &> /dev/null; then - yum update -y - yum -y install epel-release - yum repolist - yum install -y gcc-powerpc64-linux-gnu - fi - scripts/install-cargo-extensions.sh - env: - CARGO: ${{ github.workspace }}/scripts/cargo.sh - # TODO(charlie): Re-enable testing for PPC wheels. - - name: "Upload wheels uv-build" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv_build-${{ matrix.platform.target }} - path: crates/uv-build/dist - - # Like `linux-arm`. - linux-riscv64: - name: ${{ matrix.platform.target }} - timeout-minutes: 30 - runs-on: depot-ubuntu-latest-4 - strategy: - matrix: - platform: - - target: riscv64gc-unknown-linux-gnu - arch: riscv64 - + linux-aarch64: + name: aarch64-unknown-linux-gnu + runs-on: ubuntu-24.04 + timeout-minutes: 60 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - - name: "Prep README.md" - run: python scripts/transform_readme.py --target pypi + - name: "Setup Aikido Safe Chain" + uses: ./.github/actions/setup-safe-chain - # uv - - name: "Build wheels" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: ${{ matrix.platform.target }} - manylinux: 2_31 - docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} - args: --release --locked --out dist --features self-update --compatibility pypi - before-script-linux: | - scripts/install-cargo-extensions.sh - env: - CARGO: ${{ github.workspace }}/scripts/cargo.sh - # TODO(zanieb): Find an alternative for this action; it uses EOL Node 20 - - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 - name: "Test wheel" - with: - arch: ${{ matrix.platform.arch }} - distro: ubuntu20.04 - githubToken: ${{ github.token }} - install: | - apt-get update - apt-get install -y --no-install-recommends python3 python3-pip python-is-python3 - pip3 install -U pip - run: | - pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall - ${MODULE_NAME} --help - # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. - # python -m ${MODULE_NAME} --help - uvx --help - env: | - PACKAGE_NAME: ${{ env.PACKAGE_NAME }} - MODULE_NAME: ${{ env.MODULE_NAME }} - - name: "Upload wheels" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv-${{ matrix.platform.target }} - path: dist - - name: "Archive binary" - shell: bash + - name: "Install cross-compilation toolchain" run: | - ARCHIVE_NAME=uv-$TARGET - ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu - mkdir -p $ARCHIVE_NAME - cp target/$TARGET/release/uv $ARCHIVE_NAME/uv - cp target/$TARGET/release/uvx $ARCHIVE_NAME/uvx - tar czvf $ARCHIVE_FILE $ARCHIVE_NAME - shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - env: - TARGET: ${{ matrix.platform.target }} - - name: "Upload binary" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: artifacts-${{ matrix.platform.target }} - path: | - *.tar.gz - *.sha256 + - name: "Install Rust toolchain" + run: rustup target add aarch64-unknown-linux-gnu - # uv-build - - name: "Build wheels uv-build" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: ${{ matrix.platform.target }} - manylinux: 2_31 - docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} - args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi - before-script-linux: | - scripts/install-cargo-extensions.sh + - name: "Build" env: - CARGO: ${{ github.workspace }}/scripts/cargo.sh - # TODO(zanieb): Find an alternative for this action; it uses EOL Node 20 - - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 - name: "Test wheel uv-build" - with: - arch: ${{ matrix.platform.arch }} - distro: ubuntu20.04 - githubToken: ${{ github.token }} - install: | - apt-get update - apt-get install -y --no-install-recommends python3 python3-pip python-is-python3 - pip3 install -U pip - run: | - pip install ${PACKAGE_NAME}-build --no-index --find-links crates/uv-build/dist --force-reinstall - ${MODULE_NAME}-build --help - # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. - # python -m ${MODULE_NAME}-build --help - env: | - PACKAGE_NAME: ${{ env.PACKAGE_NAME }} - MODULE_NAME: ${{ env.MODULE_NAME }} - - name: "Upload wheels uv-build" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv_build-${{ matrix.platform.target }} - path: crates/uv-build/dist - - musllinux: - name: ${{ matrix.target }} - runs-on: depot-ubuntu-24.04-4 - strategy: - matrix: - target: - - x86_64-unknown-linux-musl - - i686-unknown-linux-musl - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - architecture: x64 - - name: "Prep README.md" - run: python scripts/transform_readme.py --target pypi + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + JEMALLOC_SYS_WITH_LG_PAGE: "16" + run: cargo build --release --locked --target aarch64-unknown-linux-gnu --features self-update - # uv - - name: "Build wheels" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: ${{ matrix.target }} - manylinux: musllinux_1_1 - docker-options: -e CARGO - args: --release --locked --out dist --features self-update --compatibility pypi - before-script-linux: | - scripts/install-cargo-extensions.sh - env: - CARGO: ${{ github.workspace }}/scripts/cargo.sh - - name: "Test wheel" - if: matrix.target == 'x86_64-unknown-linux-musl' - run: | - docker run --rm -v ${{ github.workspace }}:/io -w /io --env MODULE_NAME --env PACKAGE_NAME alpine:3.12 sh -c " - apk add python3 py3-pip; - python3 -m venv .venv; - .venv/bin/pip install --upgrade pip; - .venv/bin/pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall; - .venv/bin/${MODULE_NAME} --help; - # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. - # .venv/bin/python -m ${MODULE_NAME} --help; - .venv/bin/uvx --help; - " - - name: "Upload wheels" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv-${{ matrix.target }} - path: dist - name: "Archive binary" - shell: bash run: | + TARGET=aarch64-unknown-linux-gnu ARCHIVE_NAME=uv-$TARGET ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz @@ -986,236 +136,12 @@ jobs: cp target/$TARGET/release/uv $ARCHIVE_NAME/uv cp target/$TARGET/release/uvx $ARCHIVE_NAME/uvx tar czvf $ARCHIVE_FILE $ARCHIVE_NAME - shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - env: - TARGET: ${{ matrix.target }} - - name: "Upload binary" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: artifacts-${{ matrix.target }} - path: | - *.tar.gz - *.sha256 - - # uv-build - - name: "Build wheels uv-build" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: ${{ matrix.target }} - manylinux: musllinux_1_1 - docker-options: -e CARGO - args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi - before-script-linux: | - scripts/install-cargo-extensions.sh - env: - CARGO: ${{ github.workspace }}/scripts/cargo.sh - - name: "Test wheel uv-build" - if: matrix.target == 'x86_64-unknown-linux-musl' - run: | - docker run --rm -v ${{ github.workspace }}:/io -w /io --env MODULE_NAME --env PACKAGE_NAME alpine:3.12 sh -c " - apk add python3 py3-pip; - python3 -m venv .venv; - .venv/bin/pip install --upgrade pip; - .venv/bin/pip install ${PACKAGE_NAME}-build --no-index --find-links crates/uv-build/dist --force-reinstall; - .venv/bin/${MODULE_NAME}-build --help; - # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. - # .venv/bin/python -m ${MODULE_NAME}_build --help; - " - - name: "Upload wheels uv-build" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv_build-${{ matrix.target }} - path: crates/uv-build/dist - - musllinux-cross: - name: ${{ matrix.platform.target }} - runs-on: depot-ubuntu-24.04-8 - env: - CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_MUSL_RUSTFLAGS: "-C target-feature=+crt-static" - strategy: - matrix: - platform: - - target: aarch64-unknown-linux-musl - arch: aarch64 - maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16 - - target: armv7-unknown-linux-musleabihf - arch: armv7 - - target: riscv64gc-unknown-linux-musl - arch: riscv64 - fail-fast: false - - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - - name: "Prep README.md" - run: python scripts/transform_readme.py --target pypi - - # uv - - name: "Build wheels" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: ${{ matrix.platform.target }} - manylinux: musllinux_1_1 - # Tag the musl builds as manylinux 2_17 fallback cause the aarch64 build only support 2_28 - args: --release --locked --out dist --features self-update --compatibility ${{ matrix.platform.arch == 'riscv64' && '2_31' || '2_17'}} --compatibility pypi - docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} - rust-toolchain: ${{ matrix.platform.toolchain || null }} - before-script-linux: | - scripts/install-cargo-extensions.sh - env: - CARGO: ${{ github.workspace }}/scripts/cargo.sh - # TODO(zanieb): Find an alternative for this action; it uses EOL Node 20 - - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 - name: "Test wheel" - with: - arch: ${{ matrix.platform.arch }} - distro: alpine_latest - install: | - apk add python3 - run: | - python -m venv .venv - .venv/bin/pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall - .venv/bin/${MODULE_NAME} --help - # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. - # .venv/bin/python -m ${MODULE_NAME} --help - .venv/bin/uvx --help - env: | - PACKAGE_NAME: ${{ env.PACKAGE_NAME }} - MODULE_NAME: ${{ env.MODULE_NAME }} - # TODO(zanieb): Find an alternative for this action; it uses EOL Node 20 - - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 - name: "Test wheel (manylinux)" - if: matrix.platform.arch == 'aarch64' - with: - arch: aarch64 - distro: ubuntu20.04 - install: | - apt-get update - apt-get install -y --no-install-recommends python3 python3-pip python-is-python3 - pip3 install -U pip - run: | - pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall - ${MODULE_NAME} --help - # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. - # python -m ${MODULE_NAME} --help - uvx --help - env: | - PACKAGE_NAME: ${{ env.PACKAGE_NAME }} - MODULE_NAME: ${{ env.MODULE_NAME }} - - name: "Upload wheels" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv-${{ matrix.platform.target }} - path: dist - - name: "Archive binary" - shell: bash - run: | - ARCHIVE_NAME=uv-$TARGET - ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz + sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - mkdir -p $ARCHIVE_NAME - cp target/$TARGET/$PROFILE/uv $ARCHIVE_NAME/uv - cp target/$TARGET/$PROFILE/uvx $ARCHIVE_NAME/uvx - tar czvf $ARCHIVE_FILE $ARCHIVE_NAME - shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - env: - TARGET: ${{ matrix.platform.target }} - PROFILE: ${{ matrix.platform.arch == 'ppc64le' && 'release-no-lto' || 'release' }} - name: "Upload binary" uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: - name: artifacts-${{ matrix.platform.target }} + name: artifacts-aarch64-unknown-linux-gnu path: | *.tar.gz *.sha256 - - # uv-build - - name: "Build wheels" - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 - with: - maturin-version: v1.12.6 - target: ${{ matrix.platform.target }} - manylinux: musllinux_1_1 - args: --profile minimal-size --locked ${{ matrix.platform.arch == 'aarch64' && '--compatibility 2_17' || ''}} --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi - docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} - rust-toolchain: ${{ matrix.platform.toolchain || null }} - before-script-linux: | - scripts/install-cargo-extensions.sh - env: - CARGO: ${{ github.workspace }}/scripts/cargo.sh - # TODO(zanieb): Find an alternative for this action; it uses EOL Node 20 - - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 - name: "Test wheel" - with: - arch: ${{ matrix.platform.arch }} - distro: alpine_latest - install: | - apk add python3 - run: | - python -m venv .venv - .venv/bin/pip install ${PACKAGE_NAME}-build --no-index --find-links crates/uv-build/dist --force-reinstall - .venv/bin/${MODULE_NAME}-build --help - # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. - # .venv/bin/python -m ${MODULE_NAME}_build --help - env: | - PACKAGE_NAME: ${{ env.PACKAGE_NAME }} - MODULE_NAME: ${{ env.MODULE_NAME }} - # TODO(zanieb): Find an alternative for this action; it uses EOL Node 20 - - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 - name: "Test wheel (manylinux)" - if: matrix.platform.arch == 'aarch64' - with: - arch: aarch64 - distro: ubuntu20.04 - install: | - apt-get update - apt-get install -y --no-install-recommends python3 python3-pip python-is-python3 - pip3 install -U pip - run: | - pip install ${PACKAGE_NAME}-build --no-index --find-links crates/uv-build/dist --force-reinstall - ${MODULE_NAME}-build --help - # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. - # python -m ${MODULE_NAME}_build --help - env: | - PACKAGE_NAME: ${{ env.PACKAGE_NAME }} - MODULE_NAME: ${{ env.MODULE_NAME }} - - name: "Upload wheels" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wheels_uv_build-${{ matrix.platform.target }} - path: crates/uv-build/dist - - check-wheels: - name: "Check wheel contents" - runs-on: ubuntu-slim - needs: - - macos-x86_64 - - macos-aarch64 - - windows - - linux - - linux-arm - - linux-s390x - - linux-powerpc - - linux-riscv64 - - musllinux - - musllinux-cross - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - name: "Install uv" - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: wheels_*-* - path: wheels - merge-multiple: true - - name: "Check wheel contents" - run: uv run --no-project scripts/check_uv_wheel_contents.py wheels/* diff --git a/.github/workflows/check-lint.yml b/.github/workflows/check-lint.yml index 40e8024a98010..9bfda28e0c20b 100644 --- a/.github/workflows/check-lint.yml +++ b/.github/workflows/check-lint.yml @@ -112,36 +112,6 @@ jobs: - name: "Clippy" run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings - clippy-windows: - name: "clippy on windows" - timeout-minutes: 15 - if: ${{ inputs.code-changed == 'true' || github.ref == 'refs/heads/main' }} - runs-on: windows-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Setup Dev Drive - run: ${{ github.workspace }}/.github/workflows/setup-dev-drive.ps1 - - # actions/checkout does not let us clone into anywhere outside ${{ github.workspace }}, so we have to copy the clone... - - name: Copy Git Repo to Dev Drive - run: | - Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse - - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - workspaces: ${{ env.UV_WORKSPACE }} - save-if: ${{ inputs.save-rust-cache == 'true' }} - - - name: "Install Rust toolchain" - run: rustup component add clippy - - - name: "Clippy" - working-directory: ${{ env.UV_WORKSPACE }} - run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings - shear: name: "cargo shear" timeout-minutes: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7be34842b729e..aa0e220c2ec11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,407 +13,11 @@ concurrency: cancel-in-progress: true jobs: - plan: - runs-on: depot-ubuntu-24.04 - outputs: - test-code: ${{ steps.plan.outputs.test_code }} - check-schema: ${{ steps.plan.outputs.check_schema }} - build-release-binaries: ${{ steps.plan.outputs.build_release_binaries }} - run-checks: ${{ steps.plan.outputs.run_checks }} - test-publish: ${{ steps.plan.outputs.test_publish }} - test-windows-trampoline: ${{ steps.plan.outputs.test_windows_trampoline }} - save-rust-cache: ${{ steps.plan.outputs.save_rust_cache }} - run-bench: ${{ steps.plan.outputs.run_bench }} - test-smoke: ${{ steps.plan.outputs.test_smoke }} - test-ecosystem: ${{ steps.plan.outputs.test_ecosystem }} - test-integration: ${{ steps.plan.outputs.test_integration }} - test-system: ${{ steps.plan.outputs.test_system }} - test-macos: ${{ steps.plan.outputs.test_macos }} - build-docker: ${{ steps.plan.outputs.build_docker }} - push-docker: ${{ steps.plan.outputs.push_docker }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - persist-credentials: false - - - name: "Plan" - id: plan - shell: bash - env: - GH_REF: ${{ github.ref }} - HAS_SKIP_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'test:skip') }} - HAS_INTEGRATION_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'test:integration') }} - HAS_SYSTEM_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'test:system') }} - HAS_EXTENDED_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'test:extended') }} - HAS_MACOS_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'test:macos') }} - HAS_PUBLISH_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'test:publish') }} - HAS_BUILD_SKIP_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'build:skip') }} - HAS_BUILD_SKIP_DOCKER_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'build:skip-docker') }} - HAS_BUILD_SKIP_RELEASE_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'build:skip-release') }} - HAS_BUILD_RELEASE_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'build:release') }} - HAS_BUILD_PUSH_DOCKER_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'build:push-docker') }} - BASE_SHA: ${{ github.event.pull_request.base.sha }} - run: | - [[ "$GH_REF" == "refs/heads/main" ]] && on_main_branch=1 - [[ "$HAS_SKIP_LABEL" == "true" ]] && has_skip_label=1 - [[ "$HAS_INTEGRATION_LABEL" == "true" ]] && has_integration_label=1 - [[ "$HAS_SYSTEM_LABEL" == "true" ]] && has_system_label=1 - [[ "$HAS_EXTENDED_LABEL" == "true" ]] && has_extended_label=1 - [[ "$HAS_MACOS_LABEL" == "true" ]] && has_macos_label=1 - [[ "$HAS_PUBLISH_LABEL" == "true" ]] && has_publish_label=1 - [[ "$HAS_BUILD_SKIP_LABEL" == "true" ]] && has_build_skip_label=1 - [[ "$HAS_BUILD_SKIP_DOCKER_LABEL" == "true" ]] && has_build_skip_docker_label=1 - [[ "$HAS_BUILD_SKIP_RELEASE_LABEL" == "true" ]] && has_build_skip_release_label=1 - [[ "$HAS_BUILD_RELEASE_LABEL" == "true" ]] && has_build_release_label=1 - [[ "$HAS_BUILD_PUSH_DOCKER_LABEL" == "true" ]] && has_build_push_docker_label=1 - - # Detect changed files - while IFS= read -r file; do - [[ -z "$file" ]] && continue - [[ "$file" =~ \.rs$ ]] && rust_code_changed=1 - [[ "$file" == "Cargo.toml" || "$file" == "Cargo.lock" || "$file" =~ ^crates/.*/Cargo\.toml$ ]] && rust_deps_changed=1 - [[ "$file" == "rust-toolchain.toml" || "$file" =~ ^\.cargo/ ]] && rust_config_changed=1 - [[ "$file" == "pyproject.toml" || "$file" =~ ^crates/.*/pyproject\.toml$ ]] && python_config_changed=1 - [[ "$file" =~ ^\.github/workflows/.*\.yml$ ]] && workflow_changed=1 - [[ "$file" == ".github/workflows/build-release-binaries.yml" || "$file" == ".github/workflows/release.yml" ]] && release_workflow_changed=1 - [[ "$file" == "scripts/check_uv_wheel_contents.py" || "$file" == "scripts/patch-dist-manifest-checksums.py" || "$file" == "scripts/repair-sdist-cargo-lock.py" ]] && release_build_changed=1 - [[ "$file" == ".github/workflows/ci.yml" ]] && ci_workflow_changed=1 - [[ "$file" == "uv.schema.json" ]] && schema_changed=1 - [[ "$file" =~ ^crates/uv-publish/ || "$file" =~ ^scripts/publish/ || "$file" == "crates/uv/src/commands/publish.rs" ]] && publish_code_changed=1 - [[ "$file" == ".github/workflows/test-windows-trampolines.yml" ]] && trampoline_workflow_changed=1 - [[ "$file" =~ ^crates/uv-trampoline/ || "$file" =~ ^crates/uv-trampoline-builder/ ]] && trampoline_code_changed=1 - [[ "$file" == "scripts/build-trampolines.sh" || "$file" == "scripts/check-trampoline-version-consistency.py" ]] && trampoline_scripts_changed=1 - [[ "$file" =~ ^crates/uv-build/ ]] && uv_build_changed=1 - [[ "$file" == "Dockerfile" ]] && dockerfile_changed=1 - [[ "$file" == ".github/workflows/build-docker.yml" ]] && docker_workflow_changed=1 - [[ "$file" == ".github/workflows/bench.yml" ]] && bench_workflow_changed=1 - [[ "$file" == ".github/workflows/test-integration.yml" || "$file" =~ ^test/integration/ || "$file" == "scripts/check_registry.py" || "$file" == "scripts/check_cache_compat.py" || "$file" == "scripts/registries-test.py" ]] && integration_changed=1 - [[ "$file" == ".github/workflows/test-system.yml" ]] && system_workflow_changed=1 - [[ "$file" == "scripts/check_system_python.py" || "$file" == "scripts/check_embedded_python.py" ]] && system_test_changed=1 - [[ "$file" =~ ^docs/ || "$file" =~ ^mkdocs.*\.yml$ || "$file" =~ \.md$ || "$file" =~ ^bin/ || "$file" =~ ^assets/ ]] && continue - any_code_changed=1 - done <<< "$(git diff --name-only "${BASE_SHA:-origin/main}...HEAD")" - - # Derived groups - [[ $rust_code_changed || $rust_deps_changed || $rust_config_changed ]] && any_rust_changed=1 - [[ $python_config_changed || $rust_deps_changed || $rust_config_changed || $uv_build_changed || $release_workflow_changed ]] && release_build_changed=1 - [[ $publish_code_changed || $ci_workflow_changed ]] && publish_changed=1 - [[ $rust_deps_changed || $rust_config_changed || $workflow_changed ]] && cache_relevant_changed=1 - [[ $python_config_changed || $rust_deps_changed || $rust_config_changed || $dockerfile_changed || $docker_workflow_changed ]] && docker_build_changed=1 - - # Decisions - [[ ! $has_skip_label && ($any_code_changed || $on_main_branch) ]] && test_code=1 - [[ $schema_changed ]] && check_schema=1 - [[ ! $has_skip_label && ! $has_build_skip_label && ! $has_build_skip_release_label && ($release_build_changed || $has_build_release_label) ]] && build_release_binaries=1 - [[ ! $has_skip_label ]] && run_checks=1 - [[ $publish_changed || $has_publish_label || $has_extended_label || $on_main_branch ]] && test_publish=1 - [[ ! $has_skip_label && ($trampoline_code_changed || $trampoline_scripts_changed || $trampoline_workflow_changed || $rust_deps_changed || $on_main_branch) ]] && test_windows_trampoline=1 - [[ $on_main_branch || $cache_relevant_changed ]] && save_rust_cache=1 - [[ ! $has_skip_label && ($any_rust_changed || $bench_workflow_changed || $on_main_branch) ]] && run_bench=1 - [[ ! $has_skip_label ]] && test_smoke=1 - [[ ! $has_skip_label ]] && test_ecosystem=1 - [[ $has_integration_label || $has_extended_label || $on_main_branch || $integration_changed ]] && test_integration=1 - [[ $has_system_label || $has_extended_label || $on_main_branch || $system_workflow_changed || $system_test_changed ]] && test_system=1 - [[ $has_macos_label || $has_extended_label || $on_main_branch || $build_release_binaries ]] && test_macos=1 - [[ ! $has_build_skip_label && ! $has_build_skip_docker_label && ($docker_build_changed || $has_build_push_docker_label) ]] && build_docker=1 - [[ $has_build_push_docker_label ]] && push_docker=1 - - # Output (convert 1/empty to true/false for GHA) - out() { [[ "$2" ]] && echo "$1=true" || echo "$1=false"; } - { - out test_code "$test_code" - out check_schema "$check_schema" - out build_release_binaries "$build_release_binaries" - out run_checks "$run_checks" - out test_publish "$test_publish" - out test_windows_trampoline "$test_windows_trampoline" - out save_rust_cache "$save_rust_cache" - out run_bench "$run_bench" - out test_smoke "$test_smoke" - out test_ecosystem "$test_ecosystem" - out test_integration "$test_integration" - out test_system "$test_system" - out test_macos "$test_macos" - out build_docker "$build_docker" - out push_docker "$push_docker" - } >> "$GITHUB_OUTPUT" - check-fmt: uses: ./.github/workflows/check-fmt.yml check-lint: - needs: plan uses: ./.github/workflows/check-lint.yml with: - code-changed: ${{ needs.plan.outputs.test-code }} - save-rust-cache: ${{ needs.plan.outputs.save-rust-cache }} - - check-docs: - needs: plan - if: ${{ needs.plan.outputs.run-checks == 'true' }} - uses: ./.github/workflows/check-docs.yml - secrets: inherit - - check-zizmor: - needs: plan - if: ${{ needs.plan.outputs.run-checks == 'true' }} - uses: ./.github/workflows/check-zizmor.yml - permissions: - contents: read - security-events: write - - check-publish: - needs: plan - if: ${{ needs.plan.outputs.test-code == 'true' }} - uses: ./.github/workflows/check-publish.yml - - check-release: - needs: plan - if: ${{ needs.plan.outputs.run-checks == 'true' }} - uses: ./.github/workflows/check-release.yml - - check-generated-files: - needs: plan - if: ${{ needs.plan.outputs.test-code == 'true' }} - uses: ./.github/workflows/check-generated-files.yml - with: - schema-changed: ${{ needs.plan.outputs.check-schema }} - save-rust-cache: ${{ needs.plan.outputs.save-rust-cache }} - - test: - needs: plan - if: ${{ needs.plan.outputs.test-code == 'true' }} - uses: ./.github/workflows/test.yml - with: - save-rust-cache: ${{ needs.plan.outputs.save-rust-cache }} - test-macos: ${{ needs.plan.outputs.test-macos }} - - test-windows-trampolines: - needs: plan - if: ${{ needs.plan.outputs.test-windows-trampoline == 'true' }} - uses: ./.github/workflows/test-windows-trampolines.yml - - build-dev-binaries: - needs: plan - if: ${{ needs.plan.outputs.test-code == 'true' }} - uses: ./.github/workflows/build-dev-binaries.yml - with: - save-rust-cache: ${{ needs.plan.outputs.save-rust-cache }} - - test-smoke: - needs: - - plan - - build-dev-binaries - if: ${{ needs.plan.outputs.test-smoke == 'true' }} - uses: ./.github/workflows/test-smoke.yml - with: - sha: ${{ github.sha }} - - test-integration: - needs: - - plan - - build-dev-binaries - if: ${{ needs.plan.outputs.test-integration == 'true' }} - uses: ./.github/workflows/test-integration.yml - secrets: inherit - permissions: - id-token: write - with: - sha: ${{ github.sha }} - - test-system: - needs: - - plan - - build-dev-binaries - if: ${{ needs.plan.outputs.test-system == 'true' }} - uses: ./.github/workflows/test-system.yml - with: - sha: ${{ github.sha }} - - test-ecosystem: - needs: - - plan - - build-dev-binaries - if: ${{ needs.plan.outputs.test-ecosystem == 'true' }} - uses: ./.github/workflows/test-ecosystem.yml - with: - sha: ${{ github.sha }} - - build-release-binaries: - needs: plan - if: ${{ needs.plan.outputs.build-release-binaries == 'true' }} - uses: ./.github/workflows/build-release-binaries.yml - secrets: inherit - - build-docker: - needs: plan - if: ${{ needs.plan.outputs.build-docker == 'true' }} - uses: ./.github/workflows/build-docker.yml - with: - push-dev: ${{ needs.plan.outputs.push-docker == 'true' }} - secrets: inherit - permissions: - contents: read - id-token: write - packages: write - attestations: write - - bench: - needs: plan - if: ${{ needs.plan.outputs.run-bench == 'true' }} - uses: ./.github/workflows/bench.yml - secrets: inherit - with: - save-rust-cache: ${{ needs.plan.outputs.save-rust-cache }} - - # This job cannot be moved into a reusable workflow because it includes coverage for uploading - # attestations and PyPI does not support attestations in reusable workflows. - test-publish: - name: "test uv publish" - timeout-minutes: 20 - needs: - - plan - - build-dev-binaries - runs-on: ubuntu-latest - # Only the main repository is a trusted publisher - if: ${{ github.repository == 'astral-sh/uv' && github.event.pull_request.head.repo.fork != true && needs.plan.outputs.test-publish == 'true' }} - environment: - name: uv-test-publish - deployment: false - env: - # No dbus in GitHub Actions - PYTHON_KEYRING_BACKEND: keyrings.alt.file.PlaintextKeyring - PYTHON_VERSION: 3.12 - permissions: - # For trusted publishing - id-token: write - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - persist-credentials: false - - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: "${{ env.PYTHON_VERSION }}" - - - name: "Download binary" - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: uv-linux-libc-${{ github.sha }} - - - name: "Prepare binary" - run: chmod +x ./uv - - - name: "Build astral-test-pypa-gh-action" - shell: bash -eo pipefail {0} - run: | - # Build a yet unused version of `astral-test-pypa-gh-action` - mkdir astral-test-pypa-gh-action - cd astral-test-pypa-gh-action - ../uv init --package --no-workspace - # Get the latest patch version - patch_version=$(curl https://test.pypi.org/simple/astral-test-pypa-gh-action/?format=application/vnd.pypi.simple.v1+json | jq --raw-output '[.files[].filename | select(endswith(".tar.gz"))] | last' | grep -oP '(?<=astral_test_pypa_gh_action-0\.1\.)\d+(?=\.tar\.gz)') - # Set the current version to one higher (which should be unused) - sed -i "s/0.1.0/0.1.$((patch_version + 1))/g" pyproject.toml - ../uv build - - - name: "Publish astral-test-pypa-gh-action" - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 - with: - # With this GitHub action, we can't do as rigid checks as with our custom Python script, so we publish more - # leniently - skip-existing: "true" - verbose: "true" - repository-url: "https://test.pypi.org/legacy/" - packages-dir: "astral-test-pypa-gh-action/dist" - - - name: "Request GitLab OIDC tokens for impersonation" - uses: digital-blueprint/gitlab-pipeline-trigger-action@c59b56e9d2688ab42c1304322ac8831a4ef6f7d2 # v1.4.0 - with: - host: gitlab.com - id: astral-test-publish/astral-test-gitlab-pypi-tp - ref: main - trigger_token: ${{ secrets.GITLAB_TEST_PUBLISH_TRIGGER_TOKEN }} - access_token: ${{ secrets.GITLAB_TEST_PUBLISH_ACCESS_TOKEN }} - download_artifacts: true - fail_if_no_artifacts: true - download_path: ./gitlab-artifacts - - - name: "Load GitLab OIDC tokens from GitLab job artifacts" - id: load-gitlab-oidc-token - run: | - # we expect ./gitlab-artifacts/*/artifacts/pypi-id-token to exist - pypi_id_token_file=$(find ./gitlab-artifacts -type f -name pypi-id-token | head -n 1) - if [ -z "${pypi_id_token_file}" ]; then - echo "No pypi-id-token file found in GitLab artifacts" - exit 1 - fi - GITLAB_PYPI_OIDC_TOKEN=$(cat "${pypi_id_token_file}") - - # we expect ./gitlab-artifacts/*/artifacts/pyx-id-token to exist - pyx_id_token_file=$(find ./gitlab-artifacts -type f -name pyx-id-token | head -n 1) - if [ -z "${pyx_id_token_file}" ]; then - echo "No pyx-id-token file found in GitLab artifacts" - exit 1 - fi - GITLAB_PYX_OIDC_TOKEN=$(cat "${pyx_id_token_file}") - - # Add secret masks for the tokens. - echo "::add-mask::$GITLAB_PYPI_OIDC_TOKEN" - echo "::add-mask::$GITLAB_PYX_OIDC_TOKEN" - - echo "GITLAB_PYPI_OIDC_TOKEN=${GITLAB_PYPI_OIDC_TOKEN}" >> "${GITHUB_OUTPUT}" - echo "GITLAB_PYX_OIDC_TOKEN=${GITLAB_PYX_OIDC_TOKEN}" >> "${GITHUB_OUTPUT}" - - - name: "Add password to keyring" - run: | - # `keyrings.alt` contains the plaintext keyring - ./uv tool install --with keyrings.alt keyring - echo $UV_TEST_PUBLISH_KEYRING | keyring set https://test.pypi.org/legacy/?astral-test-keyring __token__ - env: - UV_TEST_PUBLISH_KEYRING: ${{ secrets.UV_TEST_PUBLISH_KEYRING }} - - - name: "Add password to uv text store" - run: | - ./uv auth login https://test.pypi.org/legacy/?astral-test-text-store --token ${UV_TEST_PUBLISH_TEXT_STORE} - env: - UV_TEST_PUBLISH_TEXT_STORE: ${{ secrets.UV_TEST_PUBLISH_TEXT_STORE }} - - - name: "Publish test packages" - # `-p 3.12` prefers the python we just installed over the one locked in `.python_version`. - run: ./uv run --no-project -p "${PYTHON_VERSION}" scripts/publish/test_publish.py --uv ./uv all - env: - RUST_LOG: uv=debug,uv_publish=trace - UV_TEST_PUBLISH_TOKEN: ${{ secrets.UV_TEST_PUBLISH_TOKEN }} - UV_TEST_PUBLISH_PASSWORD: ${{ secrets.UV_TEST_PUBLISH_PASSWORD }} - UV_TEST_PUBLISH_GITLAB_PAT: ${{ secrets.UV_TEST_PUBLISH_GITLAB_PAT }} - UV_TEST_PUBLISH_CODEBERG_TOKEN: ${{ secrets.UV_TEST_PUBLISH_CODEBERG_TOKEN }} - UV_TEST_PUBLISH_CLOUDSMITH_TOKEN: ${{ secrets.UV_TEST_PUBLISH_CLOUDSMITH_TOKEN }} - UV_TEST_PUBLISH_PYX_TOKEN: ${{ secrets.UV_TEST_PUBLISH_PYX_TOKEN }} - UV_TEST_PUBLISH_PYTHON_VERSION: ${{ env.PYTHON_VERSION }} - UV_TEST_PUBLISH_GITLAB_PYPI_OIDC_TOKEN: ${{ steps.load-gitlab-oidc-token.outputs.GITLAB_PYPI_OIDC_TOKEN }} - UV_TEST_PUBLISH_GITLAB_PYX_OIDC_TOKEN: ${{ steps.load-gitlab-oidc-token.outputs.GITLAB_PYX_OIDC_TOKEN }} - - required-checks-passed: - name: "all required jobs passed" - if: always() - needs: - - check-fmt - - check-lint - - check-docs - - check-generated-files - - test - - build-dev-binaries - runs-on: ubuntu-slim - steps: - - name: "Check required jobs passed" - run: | - failing=$(echo "$NEEDS_JSON" | jq -r 'to_entries[] | select(.value.result != "success" and .value.result != "skipped") | "\(.key): \(.value.result)"') - if [ -n "$failing" ]; then - echo "$failing" - exit 1 - fi - env: - NEEDS_JSON: ${{ toJSON(needs) }} + code-changed: "true" + save-rust-cache: ${{ github.ref == 'refs/heads/main' }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 83c4787476d80..9ed848ad490db 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,388 +1,47 @@ -# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist -# -# Copyright 2022-2024, axodotdev -# SPDX-License-Identifier: MIT or Apache-2.0 -# -# CI that: -# -# * checks for a Git Tag that looks like a release -# * builds artifacts with dist (archives, installers, hashes) -# * uploads those artifacts to temporary workflow zip -# * on success, uploads the artifacts to a GitHub Release -# -# Note that the GitHub Release will be created with a generated -# title/body based on your changelogs. - name: Release permissions: - "contents": "write" + contents: write -# This task will run whenever you workflow_dispatch with a tag that looks like a version -# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. -# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where -# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION -# must be a Cargo-style SemVer Version (must have at least major.minor.patch). -# -# If PACKAGE_NAME is specified, then the announcement will be for that -# package (erroring out if it doesn't have the given version or isn't dist-able). -# -# If PACKAGE_NAME isn't specified, then the announcement will be for all -# (dist-able) packages in the workspace with that version (this mode is -# intended for workspaces with only one dist-able package, or with all dist-able -# packages versioned/released in lockstep). -# -# If you push multiple tags at once, separate instances of this workflow will -# spin up, creating an independent announcement for each one. However, GitHub -# will hard limit this to 3 tags per commit, as it will assume more tags is a -# mistake. -# -# If there's a prerelease-style suffix to the version, then the release(s) -# will be marked as a prerelease. on: workflow_dispatch: inputs: tag: - description: Release Tag + description: Release Tag (e.g. 0.11.5-dev.0) required: true default: dry-run type: string -env: - CARGO_DIST_VERSION: "0.31.0" - CARGO_DIST_CHECKSUM: "cd355dab0b4c02fb59038fef87655550021d07f45f1d82f947a34ef98560abb8" - jobs: - release-gate: - # N.B. This name should not change, it is used for downstream checks. - name: release-gate - if: ${{ inputs.tag != 'dry-run' }} - runs-on: ubuntu-latest - # This environment requires a 2-factor approval, i.e., the workflow must be approved by another - # team member. GitHub fires approval events on every job that deploys to an environment, so we - # have a dedicated environment for this purpose instead of using the `release` environment. - # We use a GitHub App with a deployment protection rule webhook to ensure that the `release` - # environment is only approved when the `release-gate` job succeeds. - environment: - name: release-gate - deployment: true - steps: - - run: echo "Release approved" - - # Run 'dist plan' (or host) to determine what tasks we need to do - plan: - runs-on: "depot-ubuntu-latest-4" - outputs: - val: ${{ steps.plan.outputs.manifest }} - tag: ${{ (inputs.tag != 'dry-run' && inputs.tag) || '' }} - tag-flag: ${{ inputs.tag && inputs.tag != 'dry-run' && format('--tag={0}', inputs.tag) || '' }} - publishing: ${{ inputs.tag && inputs.tag != 'dry-run' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - persist-credentials: false - submodules: recursive - - name: Install dist - shell: bash - run: | - curl --proto '=https' --tlsv1.2 -LsSf "https://github.com/axodotdev/cargo-dist/releases/download/v${CARGO_DIST_VERSION}/cargo-dist-x86_64-unknown-linux-gnu.tar.xz" -o /tmp/cargo-dist.tar.xz - echo "${CARGO_DIST_CHECKSUM} /tmp/cargo-dist.tar.xz" | sha256sum -c - - tar -xf /tmp/cargo-dist.tar.xz -C /tmp - install /tmp/cargo-dist-x86_64-unknown-linux-gnu/dist ~/.cargo/bin/ - - name: Cache dist - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: cargo-dist-cache - path: ~/.cargo/bin/dist - # sure would be cool if github gave us proper conditionals... - # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible - # functionality based on whether this is a pull_request, and whether it's from a fork. - # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* - # but also really annoying to build CI around when it needs secrets to work right.) - - id: plan - run: | - dist ${{ (inputs.tag && inputs.tag != 'dry-run' && format('host --steps=create --tag={0}', inputs.tag)) || 'plan' }} --output-format=json > plan-dist-manifest.json - echo "dist ran successfully" - cat plan-dist-manifest.json - echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - - name: "Upload dist-manifest.json" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: artifacts-plan-dist-manifest - path: plan-dist-manifest.json - - custom-build-release-binaries: - needs: - - plan - if: ${{ needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run' }} + build-release-binaries: + if: ${{ inputs.tag != '' }} uses: ./.github/workflows/build-release-binaries.yml - with: - plan: ${{ needs.plan.outputs.val }} secrets: inherit - custom-build-docker: - needs: - - plan - - release-gate - if: ${{ always() && needs.plan.result == 'success' && (needs.release-gate.result == 'success' || needs.release-gate.result == 'skipped') && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run') }} - uses: ./.github/workflows/build-docker.yml - with: - plan: ${{ needs.plan.outputs.val }} - secrets: inherit - permissions: - "attestations": "write" - "contents": "read" - "id-token": "write" - "packages": "write" - - synthesize-local-dist-manifest: - needs: - - plan - - custom-build-release-binaries - if: ${{ needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run' }} - runs-on: "depot-ubuntu-latest-4" - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - persist-credentials: false - submodules: recursive - - name: Install cached dist - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: cargo-dist-cache - path: ~/.cargo/bin/ - - run: chmod +x ~/.cargo/bin/dist - - name: Fetch local artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: artifacts-* - path: target/distrib/ - merge-multiple: true - - name: Generate local dist manifest - shell: bash - env: - TAG: ${{ needs.plan.outputs.tag-flag }} - run: | - temp_manifest=target/local-dist-manifest.json.tmp - dist manifest "$TAG" --output-format=json --no-local-paths --artifacts=local > "$temp_manifest" - python3 scripts/patch-dist-manifest-checksums.py --manifest "$temp_manifest" --artifacts-dir target/distrib - mv "$temp_manifest" target/distrib/local-dist-manifest.json - - name: Upload synthesized local dist manifest - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: artifacts-build-local-manifest - path: target/distrib/local-dist-manifest.json - - # Build and package all the platform-agnostic(ish) things - build-global-artifacts: - needs: - - plan - - custom-build-release-binaries - - custom-build-docker - - synthesize-local-dist-manifest - runs-on: "depot-ubuntu-latest-4" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - persist-credentials: false - submodules: recursive - - name: Install cached dist - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: cargo-dist-cache - path: ~/.cargo/bin/ - - run: chmod +x ~/.cargo/bin/dist - # Get all the local artifacts for the global tasks to use (for e.g. checksums) - - name: Fetch local artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: artifacts-* - path: target/distrib/ - merge-multiple: true - - id: cargo-dist - shell: bash - run: | - dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json - echo "dist ran successfully" - - # Parse out what we just built and upload it to scratch storage - echo "paths<> "$GITHUB_OUTPUT" - jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - cp dist-manifest.json "$BUILD_MANIFEST_NAME" - - name: "Upload artifacts" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: artifacts-build-global - path: | - ${{ steps.cargo-dist.outputs.paths }} - ${{ env.BUILD_MANIFEST_NAME }} - # Determines if we should publish/announce - host: - needs: - - plan - - custom-build-release-binaries - - custom-build-docker - - build-global-artifacts - # Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine) - if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.custom-build-release-binaries.result == 'skipped' || needs.custom-build-release-binaries.result == 'success') && (needs.custom-build-docker.result == 'skipped' || needs.custom-build-docker.result == 'success') }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - runs-on: "depot-ubuntu-latest-4" - outputs: - val: ${{ steps.host.outputs.manifest }} + release: + needs: build-release-binaries + if: ${{ inputs.tag != 'dry-run' }} + runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false - submodules: recursive - - name: Install cached dist - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: cargo-dist-cache - path: ~/.cargo/bin/ - - run: chmod +x ~/.cargo/bin/dist - # Fetch artifacts from scratch-storage - - name: Fetch artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: artifacts-* - path: target/distrib/ - merge-multiple: true - # This is a harmless no-op for GitHub Releases, hosting for that happens in "announce" - - id: host - shell: bash - run: | - dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json - echo "artifacts uploaded and released successfully" - cat dist-manifest.json - echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" - - name: "Upload dist-manifest.json" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - # Overwrite the previous copy - name: artifacts-dist-manifest - path: dist-manifest.json - custom-publish-pypi: - needs: - - plan - - host - - release-gate - if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} - uses: ./.github/workflows/publish-pypi.yml - with: - plan: ${{ needs.plan.outputs.val }} - secrets: inherit - # publish jobs get escalated permissions - permissions: - "id-token": "write" - "packages": "write" - - custom-publish-crates: - needs: - - plan - - host - - release-gate - - 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" - "id-token": "write" - - # Create a GitHub Release while uploading all files to it - announce: - needs: - - plan - - host - - release-gate - - custom-publish-pypi - - custom-publish-crates - # use "always() && ..." to allow us to wait for all publish jobs while - # still allowing individual publish jobs to skip themselves (for prereleases). - # "host" however must run to completion, no skipping allowed! - if: ${{ always() && needs.host.result == 'success' && needs.release-gate.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') && (needs.custom-publish-crates.result == 'skipped' || needs.custom-publish-crates.result == 'success') }} - runs-on: "depot-ubuntu-latest-4" - environment: - name: release - permissions: - "attestations": "write" - "contents": "write" - "id-token": "write" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - persist-credentials: false - submodules: recursive - # Create a GitHub Release while uploading all files to it - - name: "Download GitHub Artifacts" + - name: Download artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: artifacts-* path: artifacts merge-multiple: true - - name: Cleanup - run: | - # Remove the granular manifests - rm -f artifacts/*-dist-manifest.json - - name: Attest - uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 - with: - subject-path: | - artifacts/*.json - artifacts/*.sh - artifacts/*.ps1 - artifacts/*.zip - artifacts/*.tar.gz + - name: Create GitHub Release env: - PRERELEASE_FLAG: "${{ fromJson(needs.host.outputs.val).announcement_is_prerelease && '--prerelease' || '' }}" - ANNOUNCEMENT_TITLE: "${{ fromJson(needs.host.outputs.val).announcement_title }}" - ANNOUNCEMENT_BODY: "${{ fromJson(needs.host.outputs.val).announcement_github_body }}" - RELEASE_COMMIT: "${{ github.sha }}" + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ inputs.tag }} + SHA: ${{ github.sha }} run: | - # Write and read notes from a file to avoid quoting breaking things - echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt - - gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* - - custom-publish-docs: - needs: - - plan - - announce - uses: ./.github/workflows/publish-docs.yml - with: - plan: ${{ needs.plan.outputs.val }} - secrets: inherit - - custom-publish-versions: - needs: - - plan - - announce - uses: ./.github/workflows/publish-versions.yml - with: - plan: ${{ needs.plan.outputs.val }} - secrets: inherit - - custom-publish-mirror: - needs: - - plan - - announce - uses: ./.github/workflows/publish-mirror.yml - with: - plan: ${{ needs.plan.outputs.val }} - secrets: inherit - permissions: - "contents": "read" + gh release create "$TAG" \ + --target "$SHA" \ + --title "uv $TAG" \ + --generate-notes \ + --prerelease \ + artifacts/* diff --git a/CHANGELOG.md b/CHANGELOG.md index dcbfa2e5ab747..abdf27a175e84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ +## 0.11.6 + +Released on 2026-04-09. + +This release resolves a low severity security advisory in which wheels with malformed RECORD entries could delete arbitrary files on uninstall. See [GHSA-pjjw-68hj-v9mw](https://github.com/astral-sh/uv/security/advisories/GHSA-pjjw-68hj-v9mw) for details. + +### Bug fixes + +- Do not remove files outside the venv on uninstall ([#18942](https://github.com/astral-sh/uv/pull/18942)) +- Validate and heal wheel `RECORD` during installation ([#18943](https://github.com/astral-sh/uv/pull/18943)) +- Avoid `uv cache clean` errors due to Win32 path normalization ([#18856](https://github.com/astral-sh/uv/pull/18856)) + ## 0.11.5 Released on 2026-04-08. @@ -17,7 +29,6 @@ Released on 2026-04-08. - Remove trailing path separators in path normalization ([#18915](https://github.com/astral-sh/uv/pull/18915)) - Improve error messages for unsupported or invalid TLS certificates ([#18924](https://github.com/astral-sh/uv/pull/18924)) - ### Preview features - Add `exclude-newer` to `[[tool.uv.index]]` ([#18839](https://github.com/astral-sh/uv/pull/18839)) @@ -143,28 +154,28 @@ The changes are largely driven by the upgrade of reqwest, which powers uv's HTTP The following changes are included: - [`rustls-platform-verifier`](https://github.com/rustls/rustls-platform-verifier) is used instead of [`rustls-native-certs`](https://github.com/rustls/rustls-native-certs) and [`webpki`](https://github.com/rustls/webpki) for certificate verification - + **This change should have no effect unless you are using the `native-tls` option to enable reading system certificates.** - + `rustls-platform-verifier` delegates to the system for certificate validation (e.g., `Security.framework` on macOS) instead of eagerly loading certificates from the system and verifying them via `webpki`. The effects of this change will vary based on the operating system. In general, uv's certificate validation should now be more consistent with browsers and other native applications. However, this is the most likely cause of breaking changes in this release. Some previously failing certificate chains may succeed, and some previously accepted certificate chains may fail. In either case, we expect the validation to be more correct and welcome reports of regressions. - + In particular, because more responsibility for validating the certificate is transferred to your system's security library, some features like [CA constraints](https://support.apple.com/en-us/103255) or [revocation of certificates](https://en.wikipedia.org/wiki/Certificate_revocation) via OCSP and CRLs may now be used. - + This change should improve performance when using system certificate on macOS, as uv no longer needs to load all certificates from the keychain at startup. - [`aws-lc`](https://github.com/aws/aws-lc) is used instead of `ring` for a cryptography backend - + There should not be breaking changes from this change. We expect this to expand support for certificate signature algorithms. - `--native-tls` is deprecated in favor of a new `--system-certs` flag - + The `--native-tls` flag is still usable and has identical behavior to `--system-certs.` - + This change was made to reduce confusion about the TLS implementation uv uses. uv always uses `rustls` not `native-tls`. - Building uv on x86-64 and i686 Windows requires NASM - + NASM is required by `aws-lc`. If not found on the system, a prebuilt blob provided by `aws-lc-sys` will be used. - + If you are not building uv from source, this change has no effect. - + See the [CONTRIBUTING](https://github.com/astral-sh/uv/blob/b6854d77bfd0cb78157fecaf8b30126c6f16bc11/CONTRIBUTING.md#setup) guide for details. - Empty `SSL_CERT_FILE` values are ignored (for consistency with `SSL_CERT_DIR`) @@ -530,86 +541,86 @@ There are no breaking changes to [`uv_build`](https://docs.astral.sh/uv/concepts ### Breaking changes - **Require `--clear` to remove existing virtual environments in `uv venv`** ([#17757](https://github.com/astral-sh/uv/pull/17757)) - + Previously, `uv venv` would prompt for confirmation before removing an existing virtual environment in interactive contexts, and remove it without confirmation in non-interactive contexts. Now, `uv venv` requires the `--clear` flag to remove an existing virtual environment. A warning for this change was added in [uv 0.8](https://github.com/astral-sh/uv/blob/main/changelogs/0.8.x.md#breaking-changes). - + You can opt out of this behavior by passing the `--clear` flag or setting `UV_VENV_CLEAR=1`. - **Error if multiple indexes include `default = true`** ([#17011](https://github.com/astral-sh/uv/pull/17011)) - + Previously, uv would silently accept multiple indexes with `default = true` and use the first one. Now, uv will error if multiple indexes are marked as the default. - + You cannot opt out of this behavior. Remove `default = true` from all but one index. - **Error when an `explicit` index is unnamed** ([#17777](https://github.com/astral-sh/uv/pull/17777)) - + Explicit indexes can only be used via the `[tool.uv.sources]` table, which requires referencing the index by name. Previously, uv would silently accept unnamed explicit indexes, which could never be referenced. Now, uv will error if an explicit index does not have a name. - + You cannot opt out of this behavior. Add a `name` to the explicit index or remove the entry. - **Install alternative Python executables using their implementation name** ([#17756](https://github.com/astral-sh/uv/pull/17756), [#17760](https://github.com/astral-sh/uv/pull/17760)) - + Previously, `uv python install` would install PyPy, GraalPy, and Pyodide executables with names like `python3.10` into the bin directory. Now, these executables will be named using their implementation name, e.g., `pypy3.10`, `graalpy3.10`, and `pyodide3.12`, to avoid conflicting with CPython installations. - + You cannot opt out of this behavior. - **Respect global Python version pins in `uv tool run` and `uv tool install`** ([#14112](https://github.com/astral-sh/uv/pull/14112)) - + Previously, `uv tool run` and `uv tool install` did not respect the global Python version pin (set via `uv python pin --global`). Now, these commands will use the global Python version when no explicit version is requested. - + For `uv tool install`, if the tool is already installed, the Python version will not change unless `--reinstall` or `--python` is provided. If the tool was previously installed with an explicit `--python` flag, the global pin will not override it. - + You can opt out of this behavior by providing an explicit `--python` flag. - **Remove Debian Bookworm, Alpine 3.21, and Python 3.8 Docker images** ([#17755](https://github.com/astral-sh/uv/pull/17755)) - + The Debian Bookworm and Alpine 3.21 images were replaced by Debian Trixie and Alpine 3.22 as defaults in [uv 0.9](https://github.com/astral-sh/uv/pull/15352). These older images are now removed. Python 3.8 images are also removed, as Python 3.8 is no longer supported in the Trixie or Alpine base images. - + The following image tags are no longer published: - `uv:bookworm`, `uv:bookworm-slim` - `uv:alpine3.21` - `uv:python3.8-*` - + Use `uv:debian` or `uv:trixie` instead of `uv:bookworm`, `uv:alpine` or `uv:alpine3.22` instead of `uv:alpine3.21`, and a newer Python version instead of `uv:python3.8-*`. - **Drop PPC64 (big endian) builds** ([#17626](https://github.com/astral-sh/uv/pull/17626)) - + uv no longer provides pre-built binaries for PPC64 (big endian). This platform appears to be largely unused and is only supported on a single manylinux version. PPC64LE (little endian) builds are unaffected. - + Building uv from source is still supported for this platform. - **Skip generating `activate.csh` for relocatable virtual environments** ([#17759](https://github.com/astral-sh/uv/pull/17759)) - + Previously, `uv venv --relocatable` would generate an `activate.csh` script that contained hardcoded paths, making it incompatible with relocation. Now, the `activate.csh` script is not generated for relocatable virtual environments. - + You cannot opt out of this behavior. - **Require username when multiple credentials match a URL** ([#16983](https://github.com/astral-sh/uv/pull/16983)) - + When using `uv auth login` to store credentials, you can register multiple username and password combinations for the same host. Previously, when uv needed to authenticate and multiple credentials matched the URL (e.g., when retrieving a token with `uv auth token`), uv would pick the first match. Now, uv will error instead. - + You cannot opt out of this behavior. Include the username in the request, e.g., `uv auth token --username foo example.com`. - **Avoid invalidating the lockfile versions after an `exclude-newer` change** ([#17721](https://github.com/astral-sh/uv/pull/17721)) - + Previously, changing the `exclude-newer` setting would cause package versions to be upgraded, ignoring the lockfile entirely. Now, uv will only change package versions if they are no longer within the `exclude-newer` range. - + You can restore the previous behavior by using `--upgrade` or `--upgrade-package` to opt-in to package version changes. - **Upgrade `uv format` to Ruff 0.15.0** ([#17838](https://github.com/astral-sh/uv/pull/17838)) - + `uv format` now uses [Ruff 0.15.0](https://github.com/astral-sh/ruff/releases/tag/0.15.0), which uses the [2026 style guide](https://astral.sh/blog/ruff-v0.15.0#the-ruff-2026-style-guide). See the blog post for details. - + The formatting of code is likely to change. You can opt out of this behavior by requesting an older Ruff version, e.g., `uv format --version 0.14.14`. - **Update uv crate test features to use `test-` as a prefix** ([#17860](https://github.com/astral-sh/uv/pull/17860)) - + This change only affects redistributors of uv. The Cargo features used to gate test dependencies, e.g., `pypi`, have been renamed with a `test-` prefix for clarity, e.g., `test-pypi`. ### Stabilizations - **`uv python upgrade` and `uv python install --upgrade`** ([#17766](https://github.com/astral-sh/uv/pull/17766)) - + When installing Python versions, an [intermediary directory](https://docs.astral.sh/uv/concepts/python-versions/#minor-version-directories) without the patch version attached will be created, and virtual environments will be transparently upgraded to new patch versions. - + See the [Python version documentation](https://docs.astral.sh/uv/concepts/python-versions/#upgrading-python-versions) for more details. - **`uv add --bounds` and the `add-bounds` configuration option** ([#17660](https://github.com/astral-sh/uv/pull/17660)) - + This does not come with any behavior changes. You will no longer see an experimental warning when using `uv add --bounds` or `add-bounds` in configuration. - **`uv workspace list` and `uv workspace dir`** ([#17768](https://github.com/astral-sh/uv/pull/17768)) - + This does not come with any behavior changes. You will no longer see an experimental warning when using these commands. - **`extra-build-dependencies`** ([#17767](https://github.com/astral-sh/uv/pull/17767)) - + This does not come with any behavior changes. You will no longer see an experimental warning when using `extra-build-dependencies` in configuration. ### Enhancements diff --git a/Cargo.lock b/Cargo.lock index 6275afd7baf53..16f87465c2e9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5754,7 +5754,7 @@ dependencies = [ [[package]] name = "uv" -version = "0.11.5" +version = "0.11.6-dev.0" dependencies = [ "anstream", "anyhow", @@ -5881,7 +5881,7 @@ dependencies = [ [[package]] name = "uv-audit" -version = "0.0.38" +version = "0.0.39" dependencies = [ "astral-reqwest-middleware", "clap", @@ -5905,7 +5905,7 @@ dependencies = [ [[package]] name = "uv-auth" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anyhow", "arcstr", @@ -5948,7 +5948,7 @@ dependencies = [ [[package]] name = "uv-bench" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anyhow", "codspeed-criterion-compat", @@ -5975,7 +5975,7 @@ dependencies = [ [[package]] name = "uv-bin-install" -version = "0.0.38" +version = "0.0.39" dependencies = [ "astral-reqwest-middleware", "astral-reqwest-retry", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "uv-build" -version = "0.11.5" +version = "0.11.6-dev.0" dependencies = [ "anstream", "anyhow", @@ -6017,7 +6017,7 @@ dependencies = [ [[package]] name = "uv-build-backend" -version = "0.0.38" +version = "0.0.39" dependencies = [ "astral-version-ranges", "base64 0.22.1", @@ -6059,7 +6059,7 @@ dependencies = [ [[package]] name = "uv-build-frontend" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anstream", "fs-err", @@ -6096,7 +6096,7 @@ dependencies = [ [[package]] name = "uv-cache" -version = "0.0.38" +version = "0.0.39" dependencies = [ "clap", "fs-err", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "uv-cache-info" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anyhow", "fs-err", @@ -6139,7 +6139,7 @@ dependencies = [ [[package]] name = "uv-cache-key" -version = "0.0.38" +version = "0.0.39" dependencies = [ "hex", "memchr", @@ -6151,7 +6151,7 @@ dependencies = [ [[package]] name = "uv-cli" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anstream", "anyhow", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "uv-client" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anyhow", "astral-reqwest-middleware", @@ -6254,7 +6254,7 @@ dependencies = [ [[package]] name = "uv-configuration" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anyhow", "clap", @@ -6288,14 +6288,14 @@ dependencies = [ [[package]] name = "uv-console" -version = "0.0.38" +version = "0.0.39" dependencies = [ "console", ] [[package]] name = "uv-dev" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anstream", "anyhow", @@ -6344,7 +6344,7 @@ dependencies = [ [[package]] name = "uv-dirs" -version = "0.0.38" +version = "0.0.39" dependencies = [ "assert_fs", "etcetera", @@ -6356,7 +6356,7 @@ dependencies = [ [[package]] name = "uv-dispatch" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anyhow", "futures", @@ -6388,7 +6388,7 @@ dependencies = [ [[package]] name = "uv-distribution" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anyhow", "astral-reqwest-middleware", @@ -6422,6 +6422,7 @@ dependencies = [ "uv-fs", "uv-git", "uv-git-types", + "uv-install-wheel", "uv-metadata", "uv-normalize", "uv-pep440", @@ -6439,7 +6440,7 @@ dependencies = [ [[package]] name = "uv-distribution-filename" -version = "0.0.38" +version = "0.0.39" dependencies = [ "insta", "memchr", @@ -6456,7 +6457,7 @@ dependencies = [ [[package]] name = "uv-distribution-types" -version = "0.0.38" +version = "0.0.39" dependencies = [ "arcstr", "astral-version-ranges", @@ -6496,7 +6497,7 @@ dependencies = [ [[package]] name = "uv-extract" -version = "0.0.38" +version = "0.0.39" dependencies = [ "astral-tokio-tar", "astral_async_zip", @@ -6527,14 +6528,14 @@ dependencies = [ [[package]] name = "uv-flags" -version = "0.0.38" +version = "0.0.39" dependencies = [ "bitflags 2.11.0", ] [[package]] name = "uv-fs" -version = "0.0.38" +version = "0.0.39" dependencies = [ "backon", "clap", @@ -6563,7 +6564,7 @@ dependencies = [ [[package]] name = "uv-git" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anyhow", "astral-reqwest-middleware", @@ -6588,7 +6589,7 @@ dependencies = [ [[package]] name = "uv-git-types" -version = "0.0.38" +version = "0.0.39" dependencies = [ "serde", "thiserror 2.0.18", @@ -6601,7 +6602,7 @@ dependencies = [ [[package]] name = "uv-globfilter" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anstream", "fs-err", @@ -6618,7 +6619,7 @@ dependencies = [ [[package]] name = "uv-install-wheel" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anyhow", "assert_fs", @@ -6655,7 +6656,7 @@ dependencies = [ [[package]] name = "uv-installer" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anstream", "anyhow", @@ -6698,7 +6699,7 @@ dependencies = [ [[package]] name = "uv-keyring" -version = "0.0.38" +version = "0.0.39" dependencies = [ "async-trait", "byteorder", @@ -6714,7 +6715,7 @@ dependencies = [ [[package]] name = "uv-logging" -version = "0.0.38" +version = "0.0.39" dependencies = [ "jiff", "owo-colors", @@ -6724,7 +6725,7 @@ dependencies = [ [[package]] name = "uv-macros" -version = "0.0.38" +version = "0.0.39" dependencies = [ "proc-macro2", "quote", @@ -6734,7 +6735,7 @@ dependencies = [ [[package]] name = "uv-metadata" -version = "0.0.38" +version = "0.0.39" dependencies = [ "astral_async_zip", "fs-err", @@ -6751,7 +6752,7 @@ dependencies = [ [[package]] name = "uv-normalize" -version = "0.0.38" +version = "0.0.39" dependencies = [ "rkyv", "schemars", @@ -6761,7 +6762,7 @@ dependencies = [ [[package]] name = "uv-once-map" -version = "0.0.38" +version = "0.0.39" dependencies = [ "dashmap", "futures", @@ -6770,14 +6771,14 @@ dependencies = [ [[package]] name = "uv-options-metadata" -version = "0.0.38" +version = "0.0.39" dependencies = [ "serde", ] [[package]] name = "uv-pep440" -version = "0.0.38" +version = "0.0.39" dependencies = [ "astral-version-ranges", "indoc", @@ -6791,7 +6792,7 @@ dependencies = [ [[package]] name = "uv-pep508" -version = "0.0.38" +version = "0.0.39" dependencies = [ "arcstr", "astral-version-ranges", @@ -6821,7 +6822,7 @@ dependencies = [ [[package]] name = "uv-performance-memory-allocator" -version = "0.0.38" +version = "0.0.39" dependencies = [ "mimalloc", "tikv-jemallocator", @@ -6829,7 +6830,7 @@ dependencies = [ [[package]] name = "uv-platform" -version = "0.0.38" +version = "0.0.39" dependencies = [ "fs-err", "goblin", @@ -6850,7 +6851,7 @@ dependencies = [ [[package]] name = "uv-platform-tags" -version = "0.0.38" +version = "0.0.39" dependencies = [ "bitflags 2.11.0", "insta", @@ -6864,7 +6865,7 @@ dependencies = [ [[package]] name = "uv-preview" -version = "0.0.38" +version = "0.0.39" dependencies = [ "enumflags2", "itertools 0.14.0", @@ -6875,7 +6876,7 @@ dependencies = [ [[package]] name = "uv-publish" -version = "0.0.38" +version = "0.0.39" dependencies = [ "ambient-id", "anstream", @@ -6917,7 +6918,7 @@ dependencies = [ [[package]] name = "uv-pypi-types" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anyhow", "hashbrown 0.16.1", @@ -6950,7 +6951,7 @@ dependencies = [ [[package]] name = "uv-python" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anyhow", "assert_fs", @@ -7013,7 +7014,7 @@ dependencies = [ [[package]] name = "uv-redacted" -version = "0.0.38" +version = "0.0.39" dependencies = [ "ref-cast", "schemars", @@ -7024,7 +7025,7 @@ dependencies = [ [[package]] name = "uv-requirements" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anyhow", "configparser", @@ -7058,7 +7059,7 @@ dependencies = [ [[package]] name = "uv-requirements-txt" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anyhow", "assert_fs", @@ -7091,7 +7092,7 @@ dependencies = [ [[package]] name = "uv-resolver" -version = "0.0.38" +version = "0.0.39" dependencies = [ "arcstr", "astral-pubgrub", @@ -7157,7 +7158,7 @@ dependencies = [ [[package]] name = "uv-scripts" -version = "0.0.38" +version = "0.0.39" dependencies = [ "fs-err", "indoc", @@ -7182,7 +7183,7 @@ dependencies = [ [[package]] name = "uv-settings" -version = "0.0.38" +version = "0.0.39" dependencies = [ "clap", "fs-err", @@ -7217,7 +7218,7 @@ dependencies = [ [[package]] name = "uv-shell" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anyhow", "fs-err", @@ -7234,7 +7235,7 @@ dependencies = [ [[package]] name = "uv-small-str" -version = "0.0.38" +version = "0.0.39" dependencies = [ "arcstr", "rkyv", @@ -7244,7 +7245,7 @@ dependencies = [ [[package]] name = "uv-state" -version = "0.0.38" +version = "0.0.39" dependencies = [ "fs-err", "tempfile", @@ -7253,7 +7254,7 @@ dependencies = [ [[package]] name = "uv-static" -version = "0.0.38" +version = "0.0.39" dependencies = [ "thiserror 2.0.18", "uv-macros", @@ -7261,7 +7262,7 @@ dependencies = [ [[package]] name = "uv-test" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anyhow", "assert_cmd", @@ -7291,7 +7292,7 @@ dependencies = [ [[package]] name = "uv-tool" -version = "0.0.38" +version = "0.0.39" dependencies = [ "fs-err", "owo-colors", @@ -7320,7 +7321,7 @@ dependencies = [ [[package]] name = "uv-torch" -version = "0.0.38" +version = "0.0.39" dependencies = [ "clap", "either", @@ -7340,7 +7341,7 @@ dependencies = [ [[package]] name = "uv-trampoline-builder" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anyhow", "assert_cmd", @@ -7358,7 +7359,7 @@ dependencies = [ [[package]] name = "uv-types" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anyhow", "dashmap", @@ -7380,7 +7381,7 @@ dependencies = [ [[package]] name = "uv-unix" -version = "0.0.38" +version = "0.0.39" dependencies = [ "nix 0.31.2", "thiserror 2.0.18", @@ -7388,11 +7389,11 @@ dependencies = [ [[package]] name = "uv-version" -version = "0.11.5" +version = "0.11.6-dev.0" [[package]] name = "uv-virtualenv" -version = "0.0.38" +version = "0.0.39" dependencies = [ "console", "fs-err", @@ -7413,7 +7414,7 @@ dependencies = [ [[package]] name = "uv-warnings" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anstream", "anyhow", @@ -7425,14 +7426,14 @@ dependencies = [ [[package]] name = "uv-windows" -version = "0.0.38" +version = "0.0.39" dependencies = [ "windows", ] [[package]] name = "uv-workspace" -version = "0.0.38" +version = "0.0.39" dependencies = [ "anyhow", "assert_fs", diff --git a/Cargo.toml b/Cargo.toml index aa09fc96e7164..bf416ecca57a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,77 +16,77 @@ authors = ["uv"] license = "MIT OR Apache-2.0" [workspace.dependencies] -uv-audit = { version = "0.0.38", path = "crates/uv-audit" } -uv-auth = { version = "0.0.38", path = "crates/uv-auth" } -uv-bin-install = { version = "0.0.38", path = "crates/uv-bin-install" } -uv-build-backend = { version = "0.0.38", path = "crates/uv-build-backend" } -uv-build-frontend = { version = "0.0.38", path = "crates/uv-build-frontend" } -uv-cache = { version = "0.0.38", path = "crates/uv-cache" } -uv-cache-info = { version = "0.0.38", path = "crates/uv-cache-info" } -uv-cache-key = { version = "0.0.38", path = "crates/uv-cache-key" } -uv-cli = { version = "0.0.38", path = "crates/uv-cli" } -uv-client = { version = "0.0.38", path = "crates/uv-client" } -uv-configuration = { version = "0.0.38", path = "crates/uv-configuration" } -uv-console = { version = "0.0.38", path = "crates/uv-console" } -uv-dirs = { version = "0.0.38", path = "crates/uv-dirs" } -uv-dispatch = { version = "0.0.38", path = "crates/uv-dispatch" } -uv-distribution = { version = "0.0.38", path = "crates/uv-distribution" } -uv-distribution-filename = { version = "0.0.38", path = "crates/uv-distribution-filename" } -uv-distribution-types = { version = "0.0.38", path = "crates/uv-distribution-types" } -uv-extract = { version = "0.0.38", path = "crates/uv-extract" } -uv-flags = { version = "0.0.38", path = "crates/uv-flags" } -uv-fs = { version = "0.0.38", path = "crates/uv-fs", features = [ +uv-audit = { version = "0.0.39", path = "crates/uv-audit" } +uv-auth = { version = "0.0.39", path = "crates/uv-auth" } +uv-bin-install = { version = "0.0.39", path = "crates/uv-bin-install" } +uv-build-backend = { version = "0.0.39", path = "crates/uv-build-backend" } +uv-build-frontend = { version = "0.0.39", path = "crates/uv-build-frontend" } +uv-cache = { version = "0.0.39", path = "crates/uv-cache" } +uv-cache-info = { version = "0.0.39", path = "crates/uv-cache-info" } +uv-cache-key = { version = "0.0.39", path = "crates/uv-cache-key" } +uv-cli = { version = "0.0.39", path = "crates/uv-cli" } +uv-client = { version = "0.0.39", path = "crates/uv-client" } +uv-configuration = { version = "0.0.39", path = "crates/uv-configuration" } +uv-console = { version = "0.0.39", path = "crates/uv-console" } +uv-dirs = { version = "0.0.39", path = "crates/uv-dirs" } +uv-dispatch = { version = "0.0.39", path = "crates/uv-dispatch" } +uv-distribution = { version = "0.0.39", path = "crates/uv-distribution" } +uv-distribution-filename = { version = "0.0.39", path = "crates/uv-distribution-filename" } +uv-distribution-types = { version = "0.0.39", path = "crates/uv-distribution-types" } +uv-extract = { version = "0.0.39", path = "crates/uv-extract" } +uv-flags = { version = "0.0.39", path = "crates/uv-flags" } +uv-fs = { version = "0.0.39", path = "crates/uv-fs", features = [ "serde", "tokio", ] } -uv-git = { version = "0.0.38", path = "crates/uv-git" } -uv-git-types = { version = "0.0.38", path = "crates/uv-git-types" } -uv-globfilter = { version = "0.0.38", path = "crates/uv-globfilter" } -uv-install-wheel = { version = "0.0.38", path = "crates/uv-install-wheel", default-features = false } -uv-installer = { version = "0.0.38", path = "crates/uv-installer" } -uv-keyring = { version = "0.0.38", path = "crates/uv-keyring" } -uv-logging = { version = "0.0.38", path = "crates/uv-logging" } -uv-macros = { version = "0.0.38", path = "crates/uv-macros" } -uv-metadata = { version = "0.0.38", path = "crates/uv-metadata" } -uv-normalize = { version = "0.0.38", path = "crates/uv-normalize" } -uv-once-map = { version = "0.0.38", path = "crates/uv-once-map" } -uv-options-metadata = { version = "0.0.38", path = "crates/uv-options-metadata" } -uv-performance-memory-allocator = { version = "0.0.38", path = "crates/uv-performance-memory-allocator" } -uv-pep440 = { version = "0.0.38", path = "crates/uv-pep440", features = [ +uv-git = { version = "0.0.39", path = "crates/uv-git" } +uv-git-types = { version = "0.0.39", path = "crates/uv-git-types" } +uv-globfilter = { version = "0.0.39", path = "crates/uv-globfilter" } +uv-install-wheel = { version = "0.0.39", path = "crates/uv-install-wheel", default-features = false } +uv-installer = { version = "0.0.39", path = "crates/uv-installer" } +uv-keyring = { version = "0.0.39", path = "crates/uv-keyring" } +uv-logging = { version = "0.0.39", path = "crates/uv-logging" } +uv-macros = { version = "0.0.39", path = "crates/uv-macros" } +uv-metadata = { version = "0.0.39", path = "crates/uv-metadata" } +uv-normalize = { version = "0.0.39", path = "crates/uv-normalize" } +uv-once-map = { version = "0.0.39", path = "crates/uv-once-map" } +uv-options-metadata = { version = "0.0.39", path = "crates/uv-options-metadata" } +uv-performance-memory-allocator = { version = "0.0.39", path = "crates/uv-performance-memory-allocator" } +uv-pep440 = { version = "0.0.39", path = "crates/uv-pep440", features = [ "tracing", "rkyv", "version-ranges", ] } -uv-pep508 = { version = "0.0.38", path = "crates/uv-pep508", features = [ +uv-pep508 = { version = "0.0.39", path = "crates/uv-pep508", features = [ "non-pep508-extensions", ] } -uv-platform = { version = "0.0.38", path = "crates/uv-platform" } -uv-platform-tags = { version = "0.0.38", path = "crates/uv-platform-tags" } -uv-preview = { version = "0.0.38", path = "crates/uv-preview" } -uv-publish = { version = "0.0.38", path = "crates/uv-publish" } -uv-pypi-types = { version = "0.0.38", path = "crates/uv-pypi-types" } -uv-python = { version = "0.0.38", path = "crates/uv-python" } -uv-redacted = { version = "0.0.38", path = "crates/uv-redacted" } -uv-requirements = { version = "0.0.38", path = "crates/uv-requirements" } -uv-requirements-txt = { version = "0.0.38", path = "crates/uv-requirements-txt" } -uv-resolver = { version = "0.0.38", path = "crates/uv-resolver" } -uv-scripts = { version = "0.0.38", path = "crates/uv-scripts" } -uv-settings = { version = "0.0.38", path = "crates/uv-settings" } -uv-shell = { version = "0.0.38", path = "crates/uv-shell" } -uv-small-str = { version = "0.0.38", path = "crates/uv-small-str" } -uv-state = { version = "0.0.38", path = "crates/uv-state" } -uv-static = { version = "0.0.38", path = "crates/uv-static" } -uv-test = { version = "0.0.38", path = "crates/uv-test" } -uv-tool = { version = "0.0.38", path = "crates/uv-tool" } -uv-torch = { version = "0.0.38", path = "crates/uv-torch" } -uv-trampoline-builder = { version = "0.0.38", path = "crates/uv-trampoline-builder" } -uv-types = { version = "0.0.38", path = "crates/uv-types" } -uv-unix = { version = "0.0.38", path = "crates/uv-unix" } -uv-version = { version = "0.11.5", path = "crates/uv-version" } -uv-virtualenv = { version = "0.0.38", path = "crates/uv-virtualenv" } -uv-warnings = { version = "0.0.38", path = "crates/uv-warnings" } -uv-windows = { version = "0.0.38", path = "crates/uv-windows" } -uv-workspace = { version = "0.0.38", path = "crates/uv-workspace" } +uv-platform = { version = "0.0.39", path = "crates/uv-platform" } +uv-platform-tags = { version = "0.0.39", path = "crates/uv-platform-tags" } +uv-preview = { version = "0.0.39", path = "crates/uv-preview" } +uv-publish = { version = "0.0.39", path = "crates/uv-publish" } +uv-pypi-types = { version = "0.0.39", path = "crates/uv-pypi-types" } +uv-python = { version = "0.0.39", path = "crates/uv-python" } +uv-redacted = { version = "0.0.39", path = "crates/uv-redacted" } +uv-requirements = { version = "0.0.39", path = "crates/uv-requirements" } +uv-requirements-txt = { version = "0.0.39", path = "crates/uv-requirements-txt" } +uv-resolver = { version = "0.0.39", path = "crates/uv-resolver" } +uv-scripts = { version = "0.0.39", path = "crates/uv-scripts" } +uv-settings = { version = "0.0.39", path = "crates/uv-settings" } +uv-shell = { version = "0.0.39", path = "crates/uv-shell" } +uv-small-str = { version = "0.0.39", path = "crates/uv-small-str" } +uv-state = { version = "0.0.39", path = "crates/uv-state" } +uv-static = { version = "0.0.39", path = "crates/uv-static" } +uv-test = { version = "0.0.39", path = "crates/uv-test" } +uv-tool = { version = "0.0.39", path = "crates/uv-tool" } +uv-torch = { version = "0.0.39", path = "crates/uv-torch" } +uv-trampoline-builder = { version = "0.0.39", path = "crates/uv-trampoline-builder" } +uv-types = { version = "0.0.39", path = "crates/uv-types" } +uv-unix = { version = "0.0.39", path = "crates/uv-unix" } +uv-version = { version = "0.11.6-dev.0", path = "crates/uv-version" } +uv-virtualenv = { version = "0.0.39", path = "crates/uv-virtualenv" } +uv-warnings = { version = "0.0.39", path = "crates/uv-warnings" } +uv-windows = { version = "0.0.39", path = "crates/uv-windows" } +uv-workspace = { version = "0.0.39", path = "crates/uv-workspace" } ambient-id = { version = "0.0.11", default-features = false, features = [ "reqwest-middleware", diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000000000..844f5c2775083 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,95 @@ +# Releasing the Doppel uv fork + +This is a fork of [astral-sh/uv](https://github.com/astral-sh/uv) maintained at +[doppelxyz/uv](https://github.com/doppelxyz/uv). It adds midnight-truncated `exclude-newer` +timestamps to reduce lockfile churn. + +## Quick release + +```bash +# 1. Set the version (appends -doppel automatically) +./scripts/doppel-set-version.sh 0.12.0 + +# 2. Commit +git add -A && git commit -m "Bump version to 0.12.0-doppel" + +# 3. Tag and push +git tag 0.12.0-doppel +git push origin main --tags +``` + +The release workflow triggers on `workflow_dispatch` with a tag input. It builds binaries for: + +| Target | Runner | +| --------------------------- | ---------------- | +| `aarch64-apple-darwin` | macOS 14 (arm64) | +| `x86_64-unknown-linux-gnu` | Ubuntu latest | +| `aarch64-unknown-linux-gnu` | Ubuntu 24.04 | + +## Syncing with upstream + +```bash +# Add upstream remote (one-time) +git remote add upstream https://github.com/astral-sh/uv.git + +# Fetch the upstream release tag +git fetch upstream --tags + +# Rebase the Doppel branch onto the new release +git rebase + +# Resolve any conflicts in exclude_newer.rs, then set the version +./scripts/doppel-set-version.sh + +# Commit, tag, push +git add -A && git commit -m "Bump version to -doppel" +git tag -doppel +git push origin main --tags +``` + +### What to watch for during rebase + +- **`crates/uv-distribution-types/src/exclude_newer.rs`** — our `start_of_day()` truncation in + `recompute()` and `FromStr`. If upstream changes these methods, re-apply the truncation. +- **`.github/workflows/build-release-binaries.yml`** — upstream may add new platforms or change + runner names. We only keep 3 targets + Safe Chain. +- **`crates/uv-version/Cargo.toml`** — upstream bumps the version here. Run `doppel-set-version.sh` + after rebasing to re-apply the `-doppel` suffix. + +## Version scheme + +Cargo (SemVer): `-dev.0` (e.g., `0.11.5-dev.0`) Python (PEP 440): +`.dev0` (e.g., `0.11.5.dev0`) + +The `-dev.0` suffix is valid in both SemVer and PEP 440 (maturin auto-converts). `uv --version` +prints `uv 0.11.5-dev.0` to identify the fork. The script `scripts/doppel-set-version.sh` updates 6 +files: + +- `Cargo.toml` (workspace `uv-version` dependency) +- `crates/uv/Cargo.toml` +- `crates/uv-build/Cargo.toml` +- `crates/uv-version/Cargo.toml` +- `pyproject.toml` +- `crates/uv-build/pyproject.toml` + +## Using the fork in the monorepo + +The Doppel monorepo uses `mise` to manage uv. To use the fork locally: + +```bash +# Install the built binary as a custom mise version +mkdir -p ~/.local/share/mise/installs/uv/0.11.5-doppel/uv-aarch64-apple-darwin +cp target/release/uv ~/.local/share/mise/installs/uv/0.11.5-doppel/uv-aarch64-apple-darwin/uv +ln -s uv ~/.local/share/mise/installs/uv/0.11.5-doppel/uv-aarch64-apple-darwin/uvx + +# Override in .mise.local.toml (gitignored) +echo '[tools]\nuv = "0.11.5-doppel"' > .mise.local.toml +``` + +## CI + +CI runs `check-fmt` and `check-lint` (Linux clippy only, no Windows) on every push/PR. Release +binaries are only built via the Release workflow. + +All build jobs include [Aikido Safe Chain](https://github.com/AikidoSec/safe-chain) which blocks pip +installs of packages published less than 72 hours ago. diff --git a/crates/uv-audit/Cargo.toml b/crates/uv-audit/Cargo.toml index 820d4cd838dbc..515e5b96fef74 100644 --- a/crates/uv-audit/Cargo.toml +++ b/crates/uv-audit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-audit" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition.workspace = true rust-version.workspace = true diff --git a/crates/uv-audit/README.md b/crates/uv-audit/README.md index c3aa324b75d66..100d9d31f4b45 100644 --- a/crates/uv-audit/README.md +++ b/crates/uv-audit/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-audit). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-audit). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-audit/src/service/osv.rs b/crates/uv-audit/src/service/osv.rs index ba0913ce0ccdb..b1e09a687288a 100644 --- a/crates/uv-audit/src/service/osv.rs +++ b/crates/uv-audit/src/service/osv.rs @@ -180,6 +180,25 @@ struct QueryBatchResponse { results: Vec, } +/// Filter for OSV queries. +#[derive(Debug, Copy, Clone)] +pub enum Filter { + /// Return all vulnerabilities. + All, + /// Return only vulnerabilities matching the `MAL-` prefix. + Malware, +} + +impl Filter { + /// Returns `true` if the given vulnerability ID matches this filter. + fn matches(self, id: &str) -> bool { + match self { + Self::All => true, + Self::Malware => id.starts_with("MAL-"), + } + } +} + /// Represents [OSV](https://osv.dev/), an open-source vulnerability database. pub struct Osv { base_url: DisplaySafeUrl, @@ -208,6 +227,7 @@ impl Osv { pub async fn query_batch( &self, dependencies: &[types::Dependency], + filter: Filter, ) -> Result, Error> { if dependencies.is_empty() { return Ok(vec![]); @@ -253,7 +273,13 @@ impl Osv { let mut next_pending = Vec::new(); for ((dep, _), result) in pending.iter().zip(batch_response.results.iter()) { - dep_vuln_ids.extend(result.vulns.iter().map(|v| (*dep, v.id.clone()))); + dep_vuln_ids.extend( + result + .vulns + .iter() + .filter(|v| filter.matches(&v.id)) + .map(|v| (*dep, v.id.clone())), + ); if let Some(token) = &result.next_page_token { next_pending.push((*dep, Some(token.clone()))); } @@ -403,7 +429,7 @@ mod tests { use wiremock::matchers::{body_json, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; - use crate::service::osv::RangeType; + use crate::service::osv::{Filter, RangeType}; use crate::types::{Dependency, Finding}; use super::Event; @@ -517,7 +543,7 @@ mod tests { ]; let findings = osv - .query_batch(&dependencies) + .query_batch(&dependencies, Filter::All) .await .expect("Failed to query batch"); @@ -711,7 +737,7 @@ mod tests { ]; let findings = osv - .query_batch(&dependencies) + .query_batch(&dependencies, Filter::All) .await .expect("Failed to query batch"); @@ -735,4 +761,74 @@ mod tests { "Expected two querybatch requests and three vuln detail requests" ); } + + /// Ensure that `query_batch` with `Filter::Malware` only fetches full records for `MAL-` + /// prefixed vulnerability IDs, skipping non-malware vulnerabilities entirely. + #[tokio::test] + async fn test_query_batch_malware_filter() { + let server = MockServer::start().await; + + // Querybatch returns both a MAL- and a non-MAL vulnerability. + Mock::given(method("POST")) + .and(path("/v1/querybatch")) + .and(body_json(json!({ + "queries": [ + { + "package": { "name": "package-a", "ecosystem": "PyPI" }, + "version": "1.0.0", + } + ] + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "results": [ + { + "vulns": [ + { "id": "MAL-2026-1234", "modified": "2026-01-01T00:00:00Z" }, + { "id": "GHSA-xxxx-yyyy", "modified": "2026-01-02T00:00:00Z" } + ] + } + ] + }))) + .mount(&server) + .await; + + // Only the MAL- vuln should be fetched. + Mock::given(method("GET")) + .and(path("/v1/vulns/MAL-2026-1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "id": "MAL-2026-1234", + "modified": "2026-01-01T00:00:00Z", + }))) + .mount(&server) + .await; + + let osv = Osv::new( + ClientWithMiddleware::default(), + Some(DisplaySafeUrl::parse(&server.uri()).unwrap()), + Concurrency::default(), + ); + + let dependencies = vec![Dependency::new( + PackageName::from_str("package-a").unwrap(), + Version::from_str("1.0.0").unwrap(), + )]; + + let findings = osv + .query_batch(&dependencies, Filter::Malware) + .await + .expect("Failed to query batch"); + + let [Finding::Vulnerability(v)] = findings.as_slice() else { + panic!("Expected exactly one vulnerability finding"); + }; + + assert_eq!(v.id.as_str(), "MAL-2026-1234"); + + // 1 querybatch + 1 vuln detail fetch (GHSA- was skipped). + assert_eq!( + server.received_requests().await.unwrap().len(), + 2, + "Expected one querybatch request and one vuln detail request (non-MAL skipped)" + ); + } } diff --git a/crates/uv-auth/Cargo.toml b/crates/uv-auth/Cargo.toml index fba19984c5d1f..8f8f1b83f7e00 100644 --- a/crates/uv-auth/Cargo.toml +++ b/crates/uv-auth/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-auth" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-auth/README.md b/crates/uv-auth/README.md index 0834d59fd7dfa..a8c397303b5f2 100644 --- a/crates/uv-auth/README.md +++ b/crates/uv-auth/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-auth). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-auth). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-bench/Cargo.toml b/crates/uv-bench/Cargo.toml index 99685c122e0dd..0d71219a26b02 100644 --- a/crates/uv-bench/Cargo.toml +++ b/crates/uv-bench/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-bench" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" publish = false authors = { workspace = true } diff --git a/crates/uv-bench/README.md b/crates/uv-bench/README.md index 9e7ab2994fdb3..e78aa150c43b6 100644 --- a/crates/uv-bench/README.md +++ b/crates/uv-bench/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-bench). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-bench). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-bin-install/Cargo.toml b/crates/uv-bin-install/Cargo.toml index 0dedb78a58181..d9b1b53564847 100644 --- a/crates/uv-bin-install/Cargo.toml +++ b/crates/uv-bin-install/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-bin-install" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-bin-install/README.md b/crates/uv-bin-install/README.md index a00324890b7ca..35c0b59602cbe 100644 --- a/crates/uv-bin-install/README.md +++ b/crates/uv-bin-install/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-bin-install). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-bin-install). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-build-backend/Cargo.toml b/crates/uv-build-backend/Cargo.toml index fb9770f83d4d8..730e50ba44a3c 100644 --- a/crates/uv-build-backend/Cargo.toml +++ b/crates/uv-build-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-build-backend" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-build-backend/README.md b/crates/uv-build-backend/README.md index b9f8bd3b29b42..6131de6d7a8ed 100644 --- a/crates/uv-build-backend/README.md +++ b/crates/uv-build-backend/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-build-backend). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-build-backend). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-build-backend/src/wheel.rs b/crates/uv-build-backend/src/wheel.rs index 2f0d12fe9c827..9fde50a5b3e59 100644 --- a/crates/uv-build-backend/src/wheel.rs +++ b/crates/uv-build-backend/src/wheel.rs @@ -397,7 +397,7 @@ struct RecordEntry { /// The urlsafe-base64-nopad encoded SHA256 of the files. hash: String, /// The size of the file in bytes. - size: usize, + size: u64, } /// Read the input file and write it both to the hasher and the target file. @@ -410,7 +410,7 @@ fn write_hashed( writer: &mut dyn Write, ) -> Result { let mut hasher = Sha256::new(); - let mut size = 0; + let mut size: u64 = 0; // 8KB is the default defined in `std::sys_common::io`. let mut buffer = vec![0; 8 * 1024]; loop { @@ -425,7 +425,7 @@ fn write_hashed( } hasher.update(&buffer[..read]); writer.write_all(&buffer[..read])?; - size += read; + size += read as u64; } Ok(RecordEntry { path: path.to_string(), @@ -736,7 +736,7 @@ impl DirectoryWriter for ZipDirectoryWriter { self.record.push(RecordEntry { path: path.to_string(), hash, - size: bytes.len(), + size: bytes.len() as u64, }); Ok(()) @@ -816,7 +816,7 @@ impl DirectoryWriter for FilesystemWriter { self.record.push(RecordEntry { path: path.to_string(), hash, - size: bytes.len(), + size: bytes.len() as u64, }); Ok(fs_err::write(self.root.join(path), bytes)?) diff --git a/crates/uv-build-frontend/Cargo.toml b/crates/uv-build-frontend/Cargo.toml index 729e14cfaf0e0..a92c4aa0e431f 100644 --- a/crates/uv-build-frontend/Cargo.toml +++ b/crates/uv-build-frontend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-build-frontend" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-build-frontend/README.md b/crates/uv-build-frontend/README.md index 253fbb97a1f1c..bfb97467be774 100644 --- a/crates/uv-build-frontend/README.md +++ b/crates/uv-build-frontend/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-build-frontend). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-build-frontend). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-build/Cargo.toml b/crates/uv-build/Cargo.toml index be8800adc9f04..4bdb423cee20c 100644 --- a/crates/uv-build/Cargo.toml +++ b/crates/uv-build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-build" -version = "0.11.5" +version = "0.11.6-dev.0" description = "A Python build backend" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-build/pyproject.toml b/crates/uv-build/pyproject.toml index 62abc5091ea62..134aee52eb513 100644 --- a/crates/uv-build/pyproject.toml +++ b/crates/uv-build/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uv-build" -version = "0.11.5" +version = "0.11.6-dev.0" description = "The uv build backend" authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" diff --git a/crates/uv-cache-info/Cargo.toml b/crates/uv-cache-info/Cargo.toml index 29d223867bc77..47fde75f3a197 100644 --- a/crates/uv-cache-info/Cargo.toml +++ b/crates/uv-cache-info/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-cache-info" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-cache-info/README.md b/crates/uv-cache-info/README.md index ba5a91f8acf28..d778154da52bf 100644 --- a/crates/uv-cache-info/README.md +++ b/crates/uv-cache-info/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-cache-info). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-cache-info). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-cache-key/Cargo.toml b/crates/uv-cache-key/Cargo.toml index f1cc6a5ac1364..cd43ce792c03f 100644 --- a/crates/uv-cache-key/Cargo.toml +++ b/crates/uv-cache-key/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-cache-key" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-cache-key/README.md b/crates/uv-cache-key/README.md index df0c3b6ce91c0..bda64c9beb4cf 100644 --- a/crates/uv-cache-key/README.md +++ b/crates/uv-cache-key/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-cache-key). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-cache-key). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-cache/Cargo.toml b/crates/uv-cache/Cargo.toml index af4d4ada8d53c..dbd6bd7e86c19 100644 --- a/crates/uv-cache/Cargo.toml +++ b/crates/uv-cache/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-cache" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-cache/README.md b/crates/uv-cache/README.md index cdd954218682f..a4e3d9ba900b7 100644 --- a/crates/uv-cache/README.md +++ b/crates/uv-cache/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-cache). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-cache). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-cache/src/removal.rs b/crates/uv-cache/src/removal.rs index 8fbf041fb1c04..5fdd4c58193ae 100644 --- a/crates/uv-cache/src/removal.rs +++ b/crates/uv-cache/src/removal.rs @@ -62,7 +62,9 @@ impl Removal { reporter: Option<&dyn CleanReporter>, skip_locked_file: bool, ) -> io::Result<()> { - let metadata = match fs_err::symlink_metadata(path) { + let path = uv_fs::verbatim_path(path); + + let metadata = match fs_err::symlink_metadata(&path) { Ok(metadata) => metadata, Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()), Err(err) => return Err(err), @@ -79,18 +81,18 @@ impl Removal { use std::os::windows::fs::FileTypeExt; if metadata.file_type().is_symlink_dir() { - remove_dir(path)?; + remove_dir(&path)?; } else { - remove_file(path)?; + remove_file(&path)?; } } #[cfg(not(windows))] { - remove_file(path)?; + remove_file(&path)?; } } else { - remove_file(path)?; + remove_file(&path)?; } reporter.map(CleanReporter::on_clean); @@ -98,7 +100,7 @@ impl Removal { return Ok(()); } - for entry in walkdir::WalkDir::new(path).contents_first(true) { + for entry in walkdir::WalkDir::new(&path).contents_first(true) { // If we hit a directory that lacks read permissions, try to make it readable. if let Err(ref err) = entry { if err @@ -109,7 +111,7 @@ impl Removal { if set_readable(dir).unwrap_or(false) { // Retry the operation; if we _just_ `self.rm_rf(dir)` and continue, // `walkdir` may give us duplicate entries for the directory. - return self.rm_rf(path, reporter, skip_locked_file); + return self.rm_rf(&path, reporter, skip_locked_file); } } } @@ -122,7 +124,7 @@ impl Removal { && entry.file_name() == ".lock" && entry .path() - .strip_prefix(path) + .strip_prefix(&path) .is_ok_and(|suffix| suffix == Path::new(".lock")) { continue; @@ -143,7 +145,7 @@ impl Removal { remove_dir(entry.path())?; } else if entry.file_type().is_dir() { // Remove the directory with the exclusive lock last. - if skip_locked_file && entry.path() == path { + if skip_locked_file && entry.path() == path.as_ref() { continue; } diff --git a/crates/uv-cli/Cargo.toml b/crates/uv-cli/Cargo.toml index 947fea9c95047..9c21400f019e2 100644 --- a/crates/uv-cli/Cargo.toml +++ b/crates/uv-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-cli" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-cli/README.md b/crates/uv-cli/README.md index 04afab14af952..3f48a05379654 100644 --- a/crates/uv-cli/README.md +++ b/crates/uv-cli/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-cli). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-cli). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-client/Cargo.toml b/crates/uv-client/Cargo.toml index 36901b4976e1b..c3da34034eb16 100644 --- a/crates/uv-client/Cargo.toml +++ b/crates/uv-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-client" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-client/README.md b/crates/uv-client/README.md index 4a7279251caec..dff2a9b4b3934 100644 --- a/crates/uv-client/README.md +++ b/crates/uv-client/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-client). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-client). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-configuration/Cargo.toml b/crates/uv-configuration/Cargo.toml index 3cbf2816b1f8f..8b395e6adfacc 100644 --- a/crates/uv-configuration/Cargo.toml +++ b/crates/uv-configuration/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-configuration" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-configuration/README.md b/crates/uv-configuration/README.md index 7df876f02539e..addd36b1c7f0c 100644 --- a/crates/uv-configuration/README.md +++ b/crates/uv-configuration/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-configuration). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-configuration). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-console/Cargo.toml b/crates/uv-console/Cargo.toml index 49b73bb9384af..60ca55d486a6b 100644 --- a/crates/uv-console/Cargo.toml +++ b/crates/uv-console/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-console" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-console/README.md b/crates/uv-console/README.md index 3d4adcef0227b..41a7dce00974f 100644 --- a/crates/uv-console/README.md +++ b/crates/uv-console/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-console). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-console). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-dev/Cargo.toml b/crates/uv-dev/Cargo.toml index 10e98f8d66810..5910e04d34bf7 100644 --- a/crates/uv-dev/Cargo.toml +++ b/crates/uv-dev/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-dev" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" publish = false diff --git a/crates/uv-dev/README.md b/crates/uv-dev/README.md index 7787ac2bdcd06..b5ea03d75dd99 100644 --- a/crates/uv-dev/README.md +++ b/crates/uv-dev/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-dev). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-dev). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-dirs/Cargo.toml b/crates/uv-dirs/Cargo.toml index 034314f6d3429..055f7f580f763 100644 --- a/crates/uv-dirs/Cargo.toml +++ b/crates/uv-dirs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-dirs" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-dirs/README.md b/crates/uv-dirs/README.md index fe932ed121262..75aaf3e969c85 100644 --- a/crates/uv-dirs/README.md +++ b/crates/uv-dirs/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-dirs). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-dirs). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-dispatch/Cargo.toml b/crates/uv-dispatch/Cargo.toml index d31a701cd64bc..07256787d2c1e 100644 --- a/crates/uv-dispatch/Cargo.toml +++ b/crates/uv-dispatch/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-dispatch" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-dispatch/README.md b/crates/uv-dispatch/README.md index 6c84a9bcf4bc7..bceb5d6a38b8c 100644 --- a/crates/uv-dispatch/README.md +++ b/crates/uv-dispatch/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-dispatch). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-dispatch). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 94bc9ad055cb9..498ce513156dc 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -378,8 +378,9 @@ impl BuildContext for BuildDispatch<'_> { // Remove any unnecessary packages. if !reinstalls.is_empty() { + let layout = venv.interpreter().layout(); for dist_info in &reinstalls { - let summary = uv_installer::uninstall(dist_info) + let summary = uv_installer::uninstall(dist_info, &layout) .await .context("Failed to uninstall build dependencies")?; debug!( diff --git a/crates/uv-distribution-filename/Cargo.toml b/crates/uv-distribution-filename/Cargo.toml index 81e6b66362d30..743e15d890782 100644 --- a/crates/uv-distribution-filename/Cargo.toml +++ b/crates/uv-distribution-filename/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-distribution-filename" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-distribution-filename/README.md b/crates/uv-distribution-filename/README.md index c1ce3062e443a..79cbf201eb038 100644 --- a/crates/uv-distribution-filename/README.md +++ b/crates/uv-distribution-filename/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-distribution-filename). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-distribution-filename). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-distribution-types/Cargo.toml b/crates/uv-distribution-types/Cargo.toml index 1b06fd4525630..7fc8457a11382 100644 --- a/crates/uv-distribution-types/Cargo.toml +++ b/crates/uv-distribution-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-distribution-types" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-distribution-types/README.md b/crates/uv-distribution-types/README.md index 97160502b9be2..967b75d0fa548 100644 --- a/crates/uv-distribution-types/README.md +++ b/crates/uv-distribution-types/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-distribution-types). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-distribution-types). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-distribution-types/src/exclude_newer.rs b/crates/uv-distribution-types/src/exclude_newer.rs index d2d3311499096..929c1e24c1d39 100644 --- a/crates/uv-distribution-types/src/exclude_newer.rs +++ b/crates/uv-distribution-types/src/exclude_newer.rs @@ -116,8 +116,14 @@ impl ExcludeNewerValue { }; }; + // Truncate to the start of the day (midnight UTC) to avoid lockfile churn + // from second/millisecond differences when recomputing from a relative span. + let truncated = cutoff + .start_of_day() + .expect("start_of_day should not fail for valid dates"); + Self { - timestamp: cutoff.into(), + timestamp: truncated.into(), span: Some(span), } } @@ -269,7 +275,11 @@ impl FromStr for ExcludeNewerValue { let cutoff = now.checked_sub(span.abs()).map_err(|err| { format!("Duration `{input}` is too large to subtract from current time: {err}") })?; - return Ok(Self::new(cutoff.into(), Some(ExcludeNewerSpan(span)))); + // Truncate to midnight UTC to avoid lockfile churn from time-of-day differences. + let truncated = cutoff + .start_of_day() + .map_err(|err| format!("Failed to truncate cutoff to start of day: {err}"))?; + return Ok(Self::new(truncated.into(), Some(ExcludeNewerSpan(span)))); } Err(err) => err, }; diff --git a/crates/uv-distribution-types/src/lib.rs b/crates/uv-distribution-types/src/lib.rs index b2ee398a6b849..f50cc5b3e9995 100644 --- a/crates/uv-distribution-types/src/lib.rs +++ b/crates/uv-distribution-types/src/lib.rs @@ -33,6 +33,7 @@ //! //! Since we read this information from [`direct_url.json`](https://packaging.python.org/en/latest/specifications/direct-url-data-structure/), it doesn't match the information [`Dist`] exactly. use std::borrow::Cow; +use std::fmt::Display; use std::path; use std::path::Path; use std::str::FromStr; @@ -204,6 +205,15 @@ pub enum DistRef<'a> { Source(&'a SourceDist), } +impl Display for DistRef<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Built(built_dist) => Display::fmt(&built_dist, f), + Self::Source(source_dist) => Display::fmt(&source_dist, f), + } + } +} + /// A wheel, with its three possible origins (index, url, path) #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum BuiltDist { diff --git a/crates/uv-distribution/Cargo.toml b/crates/uv-distribution/Cargo.toml index 2d55c42fbf323..5717f02339890 100644 --- a/crates/uv-distribution/Cargo.toml +++ b/crates/uv-distribution/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-distribution" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } @@ -28,6 +28,7 @@ uv-flags = { workspace = true } uv-fs = { workspace = true, features = ["tokio"] } uv-git = { workspace = true } uv-git-types = { workspace = true } +uv-install-wheel = { workspace = true } uv-metadata = { workspace = true } uv-normalize = { workspace = true } uv-pep440 = { workspace = true } diff --git a/crates/uv-distribution/README.md b/crates/uv-distribution/README.md index 32ac4bb539a9b..71fef333b8507 100644 --- a/crates/uv-distribution/README.md +++ b/crates/uv-distribution/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-distribution). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-distribution). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 528a2cc78502f..cff84342fd0d6 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -6,7 +6,6 @@ use std::sync::Arc; use std::task::{Context, Poll}; use futures::{FutureExt, TryStreamExt}; -use tempfile::TempDir; use tokio::io::{AsyncRead, AsyncSeekExt, ReadBuf}; use tokio::sync::Semaphore; use tokio_util::compat::FuturesAsyncReadCompatExt; @@ -20,11 +19,12 @@ use uv_client::{ }; use uv_distribution_filename::{SourceDistExtension, WheelFilename}; use uv_distribution_types::{ - BuildInfo, BuildableSource, BuiltDist, Dist, File, HashPolicy, Hashed, IndexUrl, InstalledDist, - Name, SourceDist, ToUrlError, + BuildInfo, BuildableSource, BuiltDist, Dist, DistRef, File, HashPolicy, Hashed, IndexUrl, + InstalledDist, Name, SourceDist, ToUrlError, }; use uv_extract::hash::Hasher; use uv_fs::write_atomic; +use uv_install_wheel::validate_and_heal_record; use uv_platform_tags::Tags; use uv_pypi_types::{HashDigest, HashDigests, PyProjectToml}; use uv_redacted::DisplaySafeUrl; @@ -491,7 +491,11 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { // Otherwise, unzip the wheel. let id = self - .unzip_wheel(&built_wheel.path, &built_wheel.target) + .unzip_wheel( + &built_wheel.path, + &built_wheel.target, + DistRef::Source(dist), + ) .await?; Ok(LocalWheel { @@ -683,19 +687,19 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { let temp_dir = tempfile::tempdir_in(self.build_context.cache().root()) .map_err(Error::CacheWrite)?; - match progress { + let files = match progress { Some((reporter, progress)) => { let mut reader = ProgressReader::new(&mut hasher, progress, &**reporter); match extension { WheelExtension::Whl => { uv_extract::stream::unzip(query_url, &mut reader, temp_dir.path()) .await - .map_err(|err| Error::Extract(filename.to_string(), err))?; + .map_err(|err| Error::Extract(filename.to_string(), err))? } WheelExtension::WhlZst => { uv_extract::stream::untar_zst(&mut reader, temp_dir.path()) .await - .map_err(|err| Error::Extract(filename.to_string(), err))?; + .map_err(|err| Error::Extract(filename.to_string(), err))? } } } @@ -703,21 +707,25 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { WheelExtension::Whl => { uv_extract::stream::unzip(query_url, &mut hasher, temp_dir.path()) .await - .map_err(|err| Error::Extract(filename.to_string(), err))?; + .map_err(|err| Error::Extract(filename.to_string(), err))? } WheelExtension::WhlZst => { uv_extract::stream::untar_zst(&mut hasher, temp_dir.path()) .await - .map_err(|err| Error::Extract(filename.to_string(), err))?; + .map_err(|err| Error::Extract(filename.to_string(), err))? } }, - } - + }; // If necessary, exhaust the reader to compute the hash. if !hashes.is_none() { hasher.finish().await.map_err(Error::HashExhaustion)?; } + // Before we make the wheel accessible by persisting it, ensure that the RECORD is + // valid. + validate_and_heal_record(temp_dir.path(), files.iter(), dist) + .map_err(Error::InstallWheelError)?; + // Persist the temporary directory to the directory store. let id = self .build_context @@ -883,52 +891,49 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { .map_err(Error::CacheWrite)?; // If no hashes are required, parallelize the unzip operation. - let hashes = if hashes.is_none() { + let (files, hashes) = if hashes.is_none() { + // Unzip the wheel into a temporary directory. let file = file.into_std().await; - tokio::task::spawn_blocking({ - let target = temp_dir.path().to_owned(); - move || -> Result<(), uv_extract::Error> { - // Unzip the wheel into a temporary directory. - match extension { - WheelExtension::Whl => { - uv_extract::unzip(file, &target)?; - } - WheelExtension::WhlZst => { - uv_extract::stream::untar_zst_file(file, &target)?; - } - } - Ok(()) - } + let target = temp_dir.path().to_owned(); + let files = tokio::task::spawn_blocking(move || match extension { + WheelExtension::Whl => uv_extract::unzip(file, &target), + WheelExtension::WhlZst => uv_extract::stream::untar_zst_file(file, &target), }) .await? .map_err(|err| Error::Extract(filename.to_string(), err))?; - HashDigests::empty() + (files, HashDigests::empty()) } else { // Create a hasher for each hash algorithm. let algorithms = hashes.algorithms(); let mut hashers = algorithms.into_iter().map(Hasher::from).collect::>(); let mut hasher = uv_extract::hash::HashReader::new(file, &mut hashers); - match extension { + let files = match extension { WheelExtension::Whl => { uv_extract::stream::unzip(query_url, &mut hasher, temp_dir.path()) .await - .map_err(|err| Error::Extract(filename.to_string(), err))?; + .map_err(|err| Error::Extract(filename.to_string(), err))? } WheelExtension::WhlZst => { uv_extract::stream::untar_zst(&mut hasher, temp_dir.path()) .await - .map_err(|err| Error::Extract(filename.to_string(), err))?; + .map_err(|err| Error::Extract(filename.to_string(), err))? } - } + }; // If necessary, exhaust the reader to compute the hash. hasher.finish().await.map_err(Error::HashExhaustion)?; + let hashes = hashers.into_iter().map(HashDigest::from).collect(); - hashers.into_iter().map(HashDigest::from).collect() + (files, hashes) }; + // Before we make the wheel accessible by persisting it, ensure that the RECORD is + // valid. + validate_and_heal_record(temp_dir.path(), files.iter(), dist) + .map_err(Error::InstallWheelError)?; + // Persist the temporary directory to the directory store. let id = self .build_context @@ -1062,7 +1067,8 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { } else if hashes.is_none() { // Otherwise, unzip the wheel. let archive = Archive::new( - self.unzip_wheel(path, wheel_entry.path()).await?, + self.unzip_wheel(path, wheel_entry.path(), DistRef::Built(dist)) + .await?, HashDigests::empty(), filename.clone(), ); @@ -1100,24 +1106,29 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { let mut hasher = uv_extract::hash::HashReader::new(file, &mut hashers); // Unzip the wheel to a temporary directory. - match extension { + let files = match extension { WheelExtension::Whl => { uv_extract::stream::unzip(path.display(), &mut hasher, temp_dir.path()) .await - .map_err(|err| Error::Extract(filename.to_string(), err))?; + .map_err(|err| Error::Extract(filename.to_string(), err))? } WheelExtension::WhlZst => { uv_extract::stream::untar_zst(&mut hasher, temp_dir.path()) .await - .map_err(|err| Error::Extract(filename.to_string(), err))?; + .map_err(|err| Error::Extract(filename.to_string(), err))? } - } + }; // Exhaust the reader to compute the hash. hasher.finish().await.map_err(Error::HashExhaustion)?; let hashes = hashers.into_iter().map(HashDigest::from).collect(); + // Before we make the wheel accessible by persisting it, ensure that the RECORD is + // valid. + validate_and_heal_record(temp_dir.path(), files.iter(), dist) + .map_err(Error::InstallWheelError)?; + // Persist the temporary directory to the directory store. let id = self .build_context @@ -1152,21 +1163,30 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { } /// Unzip a wheel into the cache, returning the path to the unzipped directory. - async fn unzip_wheel(&self, path: &Path, target: &Path) -> Result { - let temp_dir = tokio::task::spawn_blocking({ + async fn unzip_wheel( + &self, + path: &Path, + target: &Path, + dist: DistRef<'_>, + ) -> Result { + let (temp_dir, files) = tokio::task::spawn_blocking({ let path = path.to_owned(); let root = self.build_context.cache().root().to_path_buf(); - move || -> Result { + move || -> Result<_, Error> { // Unzip the wheel into a temporary directory. let temp_dir = tempfile::tempdir_in(root).map_err(Error::CacheWrite)?; let reader = fs_err::File::open(&path).map_err(Error::CacheWrite)?; - uv_extract::unzip(reader, temp_dir.path()) + let files = uv_extract::unzip(reader, temp_dir.path()) .map_err(|err| Error::Extract(path.to_string_lossy().into_owned(), err))?; - Ok(temp_dir) + Ok((temp_dir, files)) } }) .await??; + // Before we make the wheel accessible by persisting it, ensure that the RECORD is valid. + validate_and_heal_record(temp_dir.path(), files.iter(), dist) + .map_err(Error::InstallWheelError)?; + // Persist the temporary directory to the directory store. let id = self .build_context diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index b885fc1ef4262..b9f85c98929ea 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -196,6 +196,9 @@ pub enum Error { #[error("Hash-checking is not supported for Git repositories: `{0}`")] HashesNotSupportedGit(String), + + #[error(transparent)] + InstallWheelError(uv_install_wheel::Error), } impl From for Error { diff --git a/crates/uv-extract/Cargo.toml b/crates/uv-extract/Cargo.toml index 6219e881b4a3a..9d6f2d6278d17 100644 --- a/crates/uv-extract/Cargo.toml +++ b/crates/uv-extract/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-extract" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-extract/README.md b/crates/uv-extract/README.md index 3cb7b05c1d6e8..e034cc7aef6ad 100644 --- a/crates/uv-extract/README.md +++ b/crates/uv-extract/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-extract). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-extract). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-extract/src/stream.rs b/crates/uv-extract/src/stream.rs index cc824380e36f1..9a5c91f8109cb 100644 --- a/crates/uv-extract/src/stream.rs +++ b/crates/uv-extract/src/stream.rs @@ -48,11 +48,13 @@ struct ComputedEntry { /// /// `source_hint` is used for warning messages, to identify the source of the ZIP archive /// beneath the reader. It might be a URL, a file path, or something else. +/// +/// Returns the list of unpacked files and their sizes. pub async fn unzip( source_hint: D, reader: R, target: impl AsRef, -) -> Result<(), Error> { +) -> Result, Error> { /// Ensure the file path is safe to use as a [`Path`]. /// /// See: @@ -82,6 +84,7 @@ pub async fn unzip( let mut directories = FxHashSet::default(); let mut local_headers = FxHashMap::default(); + let mut files = Vec::new(); let mut offset = 0; while let Some(mut entry) = zip.next_with_entry().await? { @@ -272,6 +275,9 @@ pub async fn unzip( } } + // Collect file paths (excluding directories). + files.push((relpath.clone(), actual_uncompressed_size)); + ComputedEntry { crc32: actual_crc32, uncompressed_size: actual_uncompressed_size, @@ -577,22 +583,26 @@ pub async fn unzip( } } - Ok(()) + Ok(files) } /// Unpack the given tar archive into the destination directory. /// /// This is equivalent to `archive.unpack_in(dst)`, but it also preserves the executable bit. +/// +/// Returns the list of unpacked files and their sizes. async fn untar_in( mut archive: tokio_tar::Archive<&'_ mut (dyn tokio::io::AsyncRead + Unpin)>, dst: &Path, -) -> std::io::Result<()> { +) -> std::io::Result> { // Like `tokio-tar`, canonicalize the destination prior to unpacking. let dst = fs_err::tokio::canonicalize(dst).await?; // Memoize filesystem calls to canonicalize paths. let mut memo = FxHashSet::default(); + let mut files = Vec::new(); + let mut entries = archive.entries()?; let mut pinned = Pin::new(&mut entries); while let Some(entry) = pinned.next().await { @@ -609,6 +619,14 @@ async fn untar_in( continue; } + // Collect file paths (excluding directories). + let entry_type = file.header().entry_type(); + if entry_type.is_file() || entry_type.is_hard_link() { + let relpath = file.path()?.into_owned(); + let size = file.header().size()?; + files.push((relpath, size)); + } + // Unpack the file into the destination directory. #[cfg_attr(not(unix), allow(unused_variables))] let unpacked_at = file.unpack_in_raw(&dst, &mut memo).await?; @@ -619,7 +637,6 @@ async fn untar_in( use std::fs::Permissions; use std::os::unix::fs::PermissionsExt; - let entry_type = file.header().entry_type(); if entry_type.is_file() || entry_type.is_hard_link() { let mode = file.header().mode()?; let has_any_executable_bit = mode & 0o111; @@ -639,16 +656,18 @@ async fn untar_in( } } - Ok(()) + Ok(files) } /// Unpack a `.tar.gz` archive into the target directory, without requiring `Seek`. /// /// This is useful for unpacking files as they're being downloaded. +/// +/// Returns the list of unpacked files and their sizes. pub async fn untar_gz( reader: R, target: impl AsRef, -) -> Result<(), Error> { +) -> Result, Error> { let reader = tokio::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader); let mut decompressed_bytes = async_compression::tokio::bufread::GzipDecoder::new(reader); @@ -667,10 +686,12 @@ pub async fn untar_gz( /// Unpack a `.tar.bz2` archive into the target directory, without requiring `Seek`. /// /// This is useful for unpacking files as they're being downloaded. +/// +/// Returns the list of unpacked files and their sizes. pub async fn untar_bz2( reader: R, target: impl AsRef, -) -> Result<(), Error> { +) -> Result, Error> { let reader = tokio::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader); let mut decompressed_bytes = async_compression::tokio::bufread::BzDecoder::new(reader); @@ -689,10 +710,12 @@ pub async fn untar_bz2( /// Unpack a `.tar.zst` archive into the target directory, without requiring `Seek`. /// /// This is useful for unpacking files as they're being downloaded. +/// +/// Returns the list of unpacked files and their sizes. pub async fn untar_zst( reader: R, target: impl AsRef, -) -> Result<(), Error> { +) -> Result, Error> { let reader = tokio::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader); let mut decompressed_bytes = async_compression::tokio::bufread::ZstdDecoder::new(reader); @@ -709,22 +732,70 @@ pub async fn untar_zst( } /// Unpack a `.tar.zst` archive from a file on disk into the target directory. -pub fn untar_zst_file(reader: R, target: impl AsRef) -> Result<(), Error> { +/// +/// Returns the list of unpacked files and their sizes. +pub fn untar_zst_file( + reader: R, + target: impl AsRef, +) -> Result, Error> { let reader = std::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader); let decompressed = zstd::Decoder::new(reader).map_err(Error::Io)?; let mut archive = tar::Archive::new(decompressed); archive.set_preserve_mtime(false); - archive.unpack(target).map_err(Error::io_or_compression)?; - Ok(()) + + // The logic below is `Archive::unpack`, with slight simplifications as we know the target is + // a real directory, using our error handling and adding file recording. + let mut files = Vec::new(); + + // Canonicalizing the dst directory will prepend the path with '\\?\' + // on windows which will allow windows APIs to treat the path as an + // extended-length path with a 32,767 character limit. Otherwise all + // unpacked paths over 260 characters will fail on creation with a + // NotFound exception. + let dst = fs_err::canonicalize(&target).unwrap_or(target.as_ref().to_path_buf()); + + // Delay any directory entries until the end (they will be created if needed by + // descendants), to ensure that directory permissions do not interfere with descendant + // extraction. + let mut directories = Vec::new(); + for entry in archive.entries().map_err(Error::io_or_compression)? { + let mut file = entry.map_err(Error::io_or_compression)?; + if file.header().entry_type() == tar::EntryType::Directory { + directories.push(file); + } else { + let entry_type = file.header().entry_type(); + let path = file.path().map_err(Error::io_or_compression)?.into_owned(); + let size = file.header().size().map_err(Error::io_or_compression)?; + if entry_type.is_file() || entry_type.is_hard_link() { + files.push((path, size)); + } + file.unpack_in(&dst).map_err(Error::io_or_compression)?; + } + } + + // Apply the directories. + // + // Note: the order of application is important to permissions. That is, we must traverse + // the filesystem graph in topological ordering or else we risk not being able to create + // child directories within those of more restrictive permissions. See [0] for details. + // + // [0]: + directories.sort_by(|a, b| b.path_bytes().cmp(&a.path_bytes())); + for mut dir in directories { + dir.unpack_in(&dst).map_err(Error::io_or_compression)?; + } + Ok(files) } /// Unpack a `.tar.xz` archive into the target directory, without requiring `Seek`. /// /// This is useful for unpacking files as they're being downloaded. +/// +/// Returns the list of unpacked files and their sizes. pub async fn untar_xz( reader: R, target: impl AsRef, -) -> Result<(), Error> { +) -> Result, Error> { let reader = tokio::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader); let mut decompressed_bytes = async_compression::tokio::bufread::XzDecoder::new(reader); @@ -737,17 +808,18 @@ pub async fn untar_xz( .build(); untar_in(archive, target.as_ref()) .await - .map_err(Error::io_or_compression)?; - Ok(()) + .map_err(Error::io_or_compression) } /// Unpack a `.tar` archive into the target directory, without requiring `Seek`. /// /// This is useful for unpacking files as they're being downloaded. +/// +/// Returns the list of unpacked files and their sizes. pub async fn untar( reader: R, target: impl AsRef, -) -> Result<(), Error> { +) -> Result, Error> { let mut reader = tokio::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader); let archive = @@ -758,8 +830,7 @@ pub async fn untar( .build(); untar_in(archive, target.as_ref()) .await - .map_err(Error::io_or_compression)?; - Ok(()) + .map_err(Error::io_or_compression) } /// Unpack a `.zip`, `.tar.gz`, `.tar.bz2`, `.tar.zst`, or `.tar.xz` archive into the target directory, @@ -767,35 +838,24 @@ pub async fn untar( /// /// `source_hint` is used for warning messages, to identify the source of the archive /// beneath the reader. It might be a URL, a file path, or something else. +/// +/// Returns the list of unpacked files and their sizes. pub async fn archive( source_hint: D, reader: R, ext: SourceDistExtension, target: impl AsRef, -) -> Result<(), Error> { +) -> Result, Error> { match ext { - SourceDistExtension::Zip => { - unzip(source_hint, reader, target).await?; - } - SourceDistExtension::Tar => { - untar(reader, target).await?; - } - SourceDistExtension::Tgz | SourceDistExtension::TarGz => { - untar_gz(reader, target).await?; - } - SourceDistExtension::Tbz | SourceDistExtension::TarBz2 => { - untar_bz2(reader, target).await?; - } + SourceDistExtension::Zip => unzip(source_hint, reader, target).await, + SourceDistExtension::Tar => untar(reader, target).await, + SourceDistExtension::Tgz | SourceDistExtension::TarGz => untar_gz(reader, target).await, + SourceDistExtension::Tbz | SourceDistExtension::TarBz2 => untar_bz2(reader, target).await, SourceDistExtension::Txz | SourceDistExtension::TarXz | SourceDistExtension::Tlz | SourceDistExtension::TarLz - | SourceDistExtension::TarLzma => { - untar_xz(reader, target).await?; - } - SourceDistExtension::TarZst => { - untar_zst(reader, target).await?; - } + | SourceDistExtension::TarLzma => untar_xz(reader, target).await, + SourceDistExtension::TarZst => untar_zst(reader, target).await, } - Ok(()) } diff --git a/crates/uv-extract/src/sync.rs b/crates/uv-extract/src/sync.rs index c84ed65f0ff2f..225c0c23f574c 100644 --- a/crates/uv-extract/src/sync.rs +++ b/crates/uv-extract/src/sync.rs @@ -11,7 +11,9 @@ use uv_warnings::warn_user_once; use zip::ZipArchive; /// Unzip a `.zip` archive into the target directory. -pub fn unzip(reader: fs_err::File, target: &Path) -> Result<(), Error> { +/// +/// Returns the list of unpacked files and their sizes. +pub fn unzip(reader: fs_err::File, target: &Path) -> Result, Error> { let (reader, filename) = reader.into_parts(); // Unzip in parallel. @@ -47,17 +49,17 @@ pub fn unzip(reader: fs_err::File, target: &Path) -> Result<(), Error> { // Determine the path of the file within the wheel. let Some(enclosed_name) = file.enclosed_name() else { warn!("Skipping unsafe file name: {}", file.name()); - return Ok(()); + return Ok(None); }; // Create necessary parent directories. - let path = target.join(enclosed_name); + let path = target.join(&enclosed_name); if file.is_dir() { let mut directories = directories.lock().unwrap(); if directories.insert(path.clone()) { fs_err::create_dir_all(path).map_err(Error::Io)?; } - return Ok(()); + return Ok(None); } if let Some(parent) = path.parent() { @@ -102,8 +104,10 @@ pub fn unzip(reader: fs_err::File, target: &Path) -> Result<(), Error> { } } - Ok(()) + Ok(Some((enclosed_name, size))) }) + // Filter out directories and skipped dangerous paths, we only want to collect the files. + .filter_map(Result::transpose) .collect::>() } diff --git a/crates/uv-flags/Cargo.toml b/crates/uv-flags/Cargo.toml index b68a51c67a660..91b50dc3af318 100644 --- a/crates/uv-flags/Cargo.toml +++ b/crates/uv-flags/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-flags" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-flags/README.md b/crates/uv-flags/README.md index 4e0acbdca324a..e6701ed2fb53d 100644 --- a/crates/uv-flags/README.md +++ b/crates/uv-flags/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-flags). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-flags). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-fs/Cargo.toml b/crates/uv-fs/Cargo.toml index 35db7508e4d27..e0b32bbb6fafe 100644 --- a/crates/uv-fs/Cargo.toml +++ b/crates/uv-fs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-fs" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-fs/README.md b/crates/uv-fs/README.md index 6733a3221ab9a..bb9b6bc90e666 100644 --- a/crates/uv-fs/README.md +++ b/crates/uv-fs/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-fs). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-fs). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-fs/src/path.rs b/crates/uv-fs/src/path.rs index 9605d82fb3e78..58f1c882190cc 100644 --- a/crates/uv-fs/src/path.rs +++ b/crates/uv-fs/src/path.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; -use std::path::{Component, Path, PathBuf}; +use std::ffi::OsString; +use std::path::{Component, Path, PathBuf, Prefix}; use std::sync::LazyLock; use either::Either; @@ -351,6 +352,107 @@ pub fn try_relative_to_if( } } +/// Convert a [`Path`] to a Windows `verbatim` path (prefixed with `\\?\`) when possible to bypass +/// Win32 path normalization such as [`MAX_PATH`] and removed trailing characters (dot, space). +/// Other characters as defined by [`Path.GetInvalidFileNameChars`] are still prohibited. This +/// function will attempt to perform path normalization similar to Win32 default normalization +/// without triggering the existing Win32 limitations. +/// +/// Only [`Prefix::UNC`] and [`Prefix::Disk`] conversion compatible components are supported. +/// * [`Prefix::UNC`] `\\server\share` becomes `\\?\UNC\server\share` +/// * [`Prefix::Disk`] `DriveLetter:` becomes `\\?\DriveLetter:` +/// +/// Other representations do not yield a `verbatim` path. The following cases are returned as-is: +/// * Non-Windows systems. +/// * Device paths such as those starting with `\\.\`. +/// * Paths already prefixed with `\\?\` or `\\?\UNC\`. +/// +/// WARNING: Adding the `\\?\` prefix effectively skips Win32 default path normalization. Even +/// though it allows operations on paths that are normally unavailable, it can also be used to +/// create entries that can potentially lead to further issues with operations that expect +/// normalization such as symbolic links, junctions or reparse points. +/// +/// [`MAX_PATH`]: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation +/// [`Path.GetInvalidFileNameChars`]: https://learn.microsoft.com/en-us/dotnet/api/system.io.path.getinvalidfilenamechars +/// +/// See: +/// * +/// * +pub fn verbatim_path(path: &Path) -> Cow<'_, Path> { + if !cfg!(windows) { + return Cow::Borrowed(path); + } + + // Attempt to resolve a fully qualified path just like Win32 path normalization would. + // std::path::absolute calls GetFullPathNameW which defeats the purpose of this function + // as it results in Win32 default path normalization. + let resolved_path = if path.is_relative() { + Cow::Owned(CWD.join(path)) + } else { + Cow::Borrowed(path) + }; + + // Fast Path: we only support verbatim conversion for Prefix::UNC and Prefix::Disk + if let Some(Component::Prefix(prefix)) = resolved_path.components().next() { + match prefix.kind() { + Prefix::UNC(..) | Prefix::Disk(_) => {}, + // return as-is as there's no verbatim equivalent for `\\.\device` + Prefix::DeviceNS(_) + // return as-is as its already verbatim + | Prefix::Verbatim(_) + | Prefix::VerbatimDisk(_) + | Prefix::VerbatimUNC(..) => return Cow::Borrowed(path) + } + } + + // Resolve relative directory components while avoiding default Win32 path normalization + let normalized_path = normalized(&resolved_path); + + let mut components = normalized_path.components(); + let Some(Component::Prefix(prefix)) = components.next() else { + return Cow::Borrowed(path); + }; + + match prefix.kind() { + // `DriveLetter:` -> `\\?\DriveLetter:` + Prefix::Disk(_) => { + let mut result = OsString::from(r"\\?\"); + result.push(normalized_path.as_os_str()); // e.g. "C:" + Cow::Owned(PathBuf::from(result)) + } + // `\\server\share` -> `\\?\UNC\server\share` + Prefix::UNC(server, share) => { + let mut result = OsString::from(r"\\?\UNC\"); + result.push(server); + result.push(r"\"); + result.push(share); + for component in components { + match component { + Component::RootDir => {} // being cautious + Component::Prefix(_) => { + debug_assert!(false, "prefix already consumed"); + } + Component::CurDir | Component::ParentDir => { + debug_assert!(false, "path already normalized"); + } + Component::Normal(_) => { + result.push(r"\"); + result.push(component.as_os_str()); + } + } + } + Cow::Owned(PathBuf::from(result)) + } + Prefix::DeviceNS(_) + | Prefix::Verbatim(_) + | Prefix::VerbatimDisk(_) + | Prefix::VerbatimUNC(..) => { + debug_assert!(false, "skipped via fast path"); + Cow::Borrowed(path) + } + } +} + /// A path that can be serialized and deserialized in a portable way by converting Windows-style /// backslashes to forward slashes, and using a `.` for an empty path. /// @@ -610,4 +712,60 @@ mod tests { assert_eq!(normalize_path(Path::new(input)), Path::new(expected)); } } + + #[cfg(windows)] + #[test] + fn test_verbatim_path() { + let relative_path = format!(r"\\?\{}\path\to\logging.", CWD.simplified_display()); + let relative_root = format!( + r"\\?\{}\path\to\logging.", + CWD.components() + .next() + .expect("expected a drive letter prefix") + .simplified_display() + ); + let cases = [ + // Non-Verbatim disk + (r"C:\path\to\logging.", r"\\?\C:\path\to\logging."), + (r"C:\path\to\.\logging.", r"\\?\C:\path\to\logging."), + (r"C:\path\to\..\to\logging.", r"\\?\C:\path\to\logging."), + (r"C:/path/to/../to/./logging.", r"\\?\C:\path\to\logging."), + (r"C:path\to\..\to\logging.", r"\\?\C:path\to\logging."), // @TODO(samypr100) we do not support expanding drive-relative paths + (r".\path\to\.\logging.", relative_path.as_str()), + (r"path\to\..\to\logging.", relative_path.as_str()), + (r"./path/to/logging.", relative_path.as_str()), + (r"\path\to\logging.", relative_root.as_str()), + // Non-Verbatim UNC + ( + r"\\127.0.0.1\c$\path\to\logging.", + r"\\?\UNC\127.0.0.1\c$\path\to\logging.", + ), + ( + r"\\127.0.0.1\c$\path\to\.\logging.", + r"\\?\UNC\127.0.0.1\c$\path\to\logging.", + ), + ( + r"\\127.0.0.1\c$\path\to\..\to\logging.", + r"\\?\UNC\127.0.0.1\c$\path\to\logging.", + ), + ( + r"//127.0.0.1/c$/path/to/../to/./logging.", + r"\\?\UNC\127.0.0.1\c$\path\to\logging.", + ), + // Verbatim Disk + (r"\\?\C:\path\to\logging.", r"\\?\C:\path\to\logging."), + // Verbatim UNC + ( + r"\\?\UNC\127.0.0.1\c$\path\to\logging.", + r"\\?\UNC\127.0.0.1\c$\path\to\logging.", + ), + // Device Namespace + (r"\\.\PhysicalDrive0", r"\\.\PhysicalDrive0"), + (r"\\.\NUL", r"\\.\NUL"), + ]; + + for (input, expected) in cases { + assert_eq!(verbatim_path(Path::new(input)), Path::new(expected)); + } + } } diff --git a/crates/uv-git-types/Cargo.toml b/crates/uv-git-types/Cargo.toml index 656fa6e523cbb..f1c0a8710056c 100644 --- a/crates/uv-git-types/Cargo.toml +++ b/crates/uv-git-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-git-types" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-git-types/README.md b/crates/uv-git-types/README.md index 07231bb4a758f..f0112442cf46a 100644 --- a/crates/uv-git-types/README.md +++ b/crates/uv-git-types/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-git-types). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-git-types). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-git/Cargo.toml b/crates/uv-git/Cargo.toml index 6f8ca9fff9ced..e6d8d172fde94 100644 --- a/crates/uv-git/Cargo.toml +++ b/crates/uv-git/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-git" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-git/README.md b/crates/uv-git/README.md index 8891836cedcb4..8ee4bbfff39c7 100644 --- a/crates/uv-git/README.md +++ b/crates/uv-git/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-git). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-git). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-globfilter/Cargo.toml b/crates/uv-globfilter/Cargo.toml index 00f2780fea293..35a27a309e605 100644 --- a/crates/uv-globfilter/Cargo.toml +++ b/crates/uv-globfilter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-globfilter" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" readme = "README.md" edition = { workspace = true } diff --git a/crates/uv-install-wheel/Cargo.toml b/crates/uv-install-wheel/Cargo.toml index 98380aa854558..cb4624b7775d1 100644 --- a/crates/uv-install-wheel/Cargo.toml +++ b/crates/uv-install-wheel/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-install-wheel" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" keywords = ["wheel", "python"] diff --git a/crates/uv-install-wheel/README.md b/crates/uv-install-wheel/README.md index 9f32e9ea205fc..87a5a4a0a1b92 100644 --- a/crates/uv-install-wheel/README.md +++ b/crates/uv-install-wheel/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-install-wheel). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-install-wheel). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-install-wheel/src/install.rs b/crates/uv-install-wheel/src/install.rs index e1d05c9394ed8..e14031afbe3da 100644 --- a/crates/uv-install-wheel/src/install.rs +++ b/crates/uv-install-wheel/src/install.rs @@ -14,7 +14,7 @@ use uv_pypi_types::{DirectUrl, Metadata10}; use crate::linker::{InstallState, LinkMode, link_wheel_files}; use crate::wheel::{ LibKind, WheelFile, dist_info_metadata, find_dist_info, install_data, parse_scripts, - read_record_file, write_installer_metadata, write_script_entrypoints, + read_record, write_installer_metadata, write_record, write_script_entrypoints, }; use crate::{Error, Layout}; @@ -83,7 +83,7 @@ pub fn install_wheel( .as_ref() .join(format!("{dist_info_prefix}.dist-info/RECORD")), )?; - let mut record = read_record_file(&mut record_file)?; + let mut record = read_record(&mut record_file)?; let (console_scripts, gui_scripts) = parse_scripts(&wheel, &dist_info_prefix, None, layout.python_version.1)?; @@ -149,14 +149,7 @@ pub fn install_wheel( } trace!(?name, "Writing record"); - let mut record_writer = csv::WriterBuilder::new() - .has_headers(false) - .escape(b'"') - .from_path(site_packages.join(format!("{dist_info_prefix}.dist-info/RECORD")))?; - record.sort(); - for entry in record { - record_writer.serialize(entry)?; - } + write_record(site_packages, &dist_info_prefix, record)?; Ok(()) } diff --git a/crates/uv-install-wheel/src/lib.rs b/crates/uv-install-wheel/src/lib.rs index 3ba45b1e94099..0632d1d5fcf21 100644 --- a/crates/uv-install-wheel/src/lib.rs +++ b/crates/uv-install-wheel/src/lib.rs @@ -13,8 +13,9 @@ use uv_pypi_types::Scheme; pub use install::install_wheel; pub use linker::{InstallState, LinkMode, link_wheel_files}; +pub use record::RecordEntry; pub use uninstall::{Uninstall, uninstall_egg, uninstall_legacy_editable, uninstall_wheel}; -pub use wheel::{LibKind, WheelFile, read_record_file}; +pub use wheel::{LibKind, WheelFile, read_record, validate_and_heal_record}; mod install; mod linker; @@ -47,10 +48,20 @@ pub enum Error { /// Doesn't follow file name schema #[error("Failed to move data files")] WalkDir(#[from] walkdir::Error), - #[error("RECORD file doesn't match wheel contents: {0}")] - RecordFile(String), + // This shouldn't be possible anymore, we keep it for better error reporting. + #[error( + "RECORD file doesn't match wheel contents, could not find entry for: {} ({})", + relative.simplified_display(), + absolute.simplified_display() + )] + RecordFile { + relative: PathBuf, + absolute: PathBuf, + }, #[error("RECORD file is invalid")] RecordCsv(#[from] csv::Error), + #[error("Non-UTF8 path in {0}: {1:?}")] + NonUtf8WheelPath(String, PathBuf), #[error("Broken virtual environment: {0}")] BrokenVenv(String), #[error( diff --git a/crates/uv-install-wheel/src/uninstall.rs b/crates/uv-install-wheel/src/uninstall.rs index 9caefba0f401e..d3e5aa90c8d04 100644 --- a/crates/uv-install-wheel/src/uninstall.rs +++ b/crates/uv-install-wheel/src/uninstall.rs @@ -1,15 +1,22 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashSet}; +use std::fmt::Display; use std::path::{Component, Path, PathBuf}; +use std::sync::{LazyLock, Mutex, OnceLock}; -use std::sync::{LazyLock, Mutex}; use tracing::trace; + use uv_fs::write_atomic_sync; +use uv_warnings::warn_user; -use crate::Error; -use crate::wheel::read_record_file; +use crate::wheel::read_record; +use crate::{Error, Layout}; /// Uninstall the wheel represented by the given `.dist-info` directory. -pub fn uninstall_wheel(dist_info: &Path) -> Result { +pub fn uninstall_wheel( + dist_info: &Path, + distribution: impl Display, + layout: &Layout, +) -> Result { let Some(site_packages) = dist_info.parent() else { return Err(Error::BrokenVenv( "dist-info directory is not in a site-packages directory".to_string(), @@ -26,7 +33,7 @@ pub fn uninstall_wheel(dist_info: &Path) -> Result { } Err(err) => return Err(err.into()), }; - read_record_file(&mut record_file)? + read_record(&mut record_file)? }; let mut file_count = 0usize; @@ -40,6 +47,10 @@ pub fn uninstall_wheel(dist_info: &Path) -> Result { for entry in &record { let path = site_packages.join(&entry.path); + if !is_path_in_scheme(&entry.path, site_packages, &distribution, layout) { + continue; + } + // On Windows, deleting the current executable is a special case. #[cfg(windows)] if let Some(itself) = itself.as_ref() { @@ -148,10 +159,65 @@ pub fn uninstall_wheel(dist_info: &Path) -> Result { }) } +static WARNED_FOR_PACKAGE: OnceLock>> = OnceLock::new(); + +/// Check if the path is inside the venv or a system interpreter path, and warn if it isn't. +/// +/// Returns `false` is a path is outside the paths that files from a wheel can be installed into, +/// so that the caller can reject RECORD entries that escape site-packages via path traversal (e.g., +/// `../../../etc/passwd`). A malicious wheel could otherwise include such entries to cause deletion +/// of arbitrary files on uninstall. +fn is_path_in_scheme( + path: &str, + site_packages: &Path, + distribution: impl Display, + layout: &Layout, +) -> bool { + let normalized = normalize_path(&site_packages.join(path)); + + // `purelib` or `platlib` are site-packages (depending on `Root-Is-Purelib`). As + // `.data/*` goes into the directories of `scheme`, `.dist-info` goes into site-packages + // and all other content goes into site-packages, the condition below covers all valid + // directories, in venvs, system interpreters and custom installation schemes. + // + // For a venv, `data` is the venv root: A wheel can write into the entire venv through + // `.data/data`. For a system environment, wheels are allowed to write to + // whole system directories, for example `data` is `/usr/local` for system Python on + // Ubuntu 24.04. + if normalized.starts_with(&layout.scheme.data) + || normalized.starts_with(&layout.scheme.purelib) + || normalized.starts_with(&layout.scheme.platlib) + || normalized.starts_with(&layout.scheme.scripts) + || normalized.starts_with(&layout.scheme.include) + { + true + } else { + // A package that does this is malformed to the point of being a risk to the user, be + // annoying about it, but only once per package. + if WARNED_FOR_PACKAGE + .get_or_init(|| Mutex::new(HashSet::new())) + .lock() + .expect("The mutex is broken, did some other thread panic?") + .insert(distribution.to_string()) + { + warn_user!( + "Invalid RECORD entry in {} that escapes the Python environment, skipping: {}", + distribution, + path + ); + } + false + } +} + /// Uninstall the egg represented by the `.egg-info` directory. /// /// See: -pub fn uninstall_egg(egg_info: &Path) -> Result { +pub fn uninstall_egg( + egg_info: &Path, + distribution: impl Display, + layout: &Layout, +) -> Result { let mut file_count = 0usize; let mut dir_count = 0usize; @@ -194,6 +260,10 @@ pub fn uninstall_egg(egg_info: &Path) -> Result { for entry in top_level { let path = dist_location.join(&entry); + if !is_path_in_scheme(&entry, dist_location, &distribution, layout) { + continue; + } + // Remove as a directory. match fs_err::remove_dir_all(&path) { Ok(()) => { @@ -347,3 +417,122 @@ fn normalize_path(path: &Path) -> PathBuf { } ret } + +#[cfg(test)] +mod tests { + use assert_fs::prelude::*; + + use uv_pypi_types::Scheme; + + use crate::Layout; + use crate::uninstall::{uninstall_egg, uninstall_wheel}; + + /// Uninstall must not remove files outside the install scheme. + #[test] + fn test_uninstall_record_path_traversal() { + let venv = assert_fs::TempDir::new().unwrap(); + let site_packages = venv.child("lib/python3.12/site-packages"); + let outside_dir = assert_fs::TempDir::new().unwrap(); + + // Create a file outside site-packages that a malicious RECORD might target. + let target_file = outside_dir.child("traversal_target.txt"); + target_file.write_str("I should not be deleted").unwrap(); + + // Build a relative traversal path from site-packages to the target file. + let dist_info = site_packages.child("evilpkg-0.1.0.dist-info"); + dist_info.create_dir_all().unwrap(); + let target_path = pathdiff::diff_paths(target_file.path(), site_packages.path()).unwrap(); + assert!(site_packages.join(&target_path).exists()); + + // Add the invalid path to the RECORD. + let record_content = format!( + "evilpkg/__init__.py,,0\n\ + evilpkg-0.1.0.dist-info/METADATA,,0\n\ + evilpkg-0.1.0.dist-info/RECORD,,\n\ + {},,0\n", + target_path.display() + ); + dist_info + .child("RECORD") + .write_str(&record_content) + .unwrap(); + + // Also create the legitimate files so uninstall can remove them. + let init_py = site_packages.child("evilpkg/__init__.py"); + init_py.touch().unwrap(); + let metadata = dist_info.child("METADATA"); + metadata.touch().unwrap(); + + // Something that looks sufficiently like a Unix venv. + let layout = Layout { + sys_executable: venv.path().join("bin/python"), + python_version: (3, 13), + os_name: "posix".to_string(), + scheme: Scheme { + purelib: site_packages.to_path_buf(), + platlib: site_packages.to_path_buf(), + scripts: venv.path().join("bin"), + data: venv.path().to_path_buf(), + include: venv.path().join("include/python3.12"), + }, + }; + + uninstall_wheel(dist_info.path(), "evilpkg 0.1.0", &layout).unwrap(); + + // The regular package files have been removed, while the file outside the scheme still + // exists. + assert!(target_file.exists()); + assert!(!metadata.exists()); + assert!(!init_py.exists()); + } + + #[test] + fn test_uninstall_egg_info_path_traversal() { + let venv = assert_fs::TempDir::new().unwrap(); + let site_packages = venv.child("lib/python3.12/site-packages"); + let outside_dir = assert_fs::TempDir::new().unwrap(); + + // Create a directory outside site-packages that a malicious top_level.txt might target. + let target_dir = outside_dir.child("traversal_target"); + let target_file = target_dir.child("secret.txt"); + target_file.write_str("I should not be deleted").unwrap(); + + // Build a relative traversal path from site-packages to the target directory. + let egg_info = site_packages.child("evilpkg-0.1.0.egg-info"); + egg_info.create_dir_all().unwrap(); + let target_path = pathdiff::diff_paths(target_dir.path(), site_packages.path()).unwrap(); + assert!(site_packages.join(&target_path).exists()); + + // Create a fake egg-info directory with a top_level.txt containing a path traversal entry. + egg_info + .child("top_level.txt") + .write_str(&format!("evilpkg\n{}\n", target_path.display())) + .unwrap(); + + // Also create the legitimate package directory so uninstall can remove it. + let init_py = site_packages.child("evilpkg").child("__init__.py"); + init_py.touch().unwrap(); + + // Something that looks sufficiently like a Unix venv. + let layout = Layout { + sys_executable: venv.path().join("bin/python"), + python_version: (3, 13), + os_name: "posix".to_string(), + scheme: Scheme { + purelib: site_packages.to_path_buf(), + platlib: site_packages.to_path_buf(), + scripts: venv.path().join("bin"), + data: venv.path().to_path_buf(), + include: venv.path().join("include/python3.12"), + }, + }; + + uninstall_egg(egg_info.path(), "evilpkg 0.1.0", &layout).unwrap(); + + // The regular package directory has been removed, while the directory outside the scheme still exists. + assert!(target_dir.exists()); + assert!(target_file.exists()); + assert!(!init_py.exists()); + assert!(!egg_info.exists()); + } +} diff --git a/crates/uv-install-wheel/src/wheel.rs b/crates/uv-install-wheel/src/wheel.rs index 4037567e26773..ed008340e8edc 100644 --- a/crates/uv-install-wheel/src/wheel.rs +++ b/crates/uv-install-wheel/src/wheel.rs @@ -1,4 +1,5 @@ -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; +use std::fmt::Display; use std::io; use std::io::{BufReader, Read, Write}; use std::path::{Path, PathBuf}; @@ -6,13 +7,14 @@ use std::path::{Path, PathBuf}; use data_encoding::BASE64URL_NOPAD; use fs_err as fs; use fs_err::{DirEntry, File}; +use itertools::Itertools; use mailparse::parse_headers; use rustc_hash::FxHashMap; use sha2::{Digest, Sha256}; use tracing::{debug, instrument, trace, warn}; use walkdir::WalkDir; -use uv_fs::{Simplified, persist_with_retry_sync, relative_to}; +use uv_fs::{PortablePath, Simplified, persist_with_retry_sync, relative_to}; use uv_normalize::PackageName; use uv_pypi_types::DirectUrl; use uv_shell::escape_posix_for_single_quotes; @@ -366,12 +368,9 @@ pub(crate) fn move_folder_recorded( let entry = record .iter_mut() .find(|entry| Path::new(&entry.path) == relative_to_site_packages) - .ok_or_else(|| { - Error::RecordFile(format!( - "Could not find entry for {} ({})", - relative_to_site_packages.simplified_display(), - src.simplified_display() - )) + .ok_or_else(|| Error::RecordFile { + relative: relative_to_site_packages.to_path_buf(), + absolute: src.to_path_buf(), })?; entry.path = relative_to(&target, site_packages)? .portable_display() @@ -569,12 +568,11 @@ fn install_script( .iter_mut() .find(|entry| Path::new(&entry.path) == relative_to_site_packages) .ok_or_else(|| { - // This should be possible to occur at this point, but filesystems and such - Error::RecordFile(format!( - "Could not find entry for {} ({})", - relative_to_site_packages.simplified_display(), - path.simplified_display() - )) + // It should not be possible to error at this point, but filesystems and such. + Error::RecordFile { + relative: relative_to_site_packages.to_path_buf(), + absolute: path.clone(), + } })?; // Update the entry in the `RECORD`. @@ -793,7 +791,7 @@ pub(crate) fn get_relocatable_executable( /// Reads the record file /// -pub fn read_record_file(record: &mut impl Read) -> Result, Error> { +pub fn read_record(record: impl Read) -> Result, Error> { csv::ReaderBuilder::new() .has_headers(false) .escape(Some(b'"')) @@ -810,6 +808,113 @@ pub fn read_record_file(record: &mut impl Read) -> Result, Erro .collect() } +pub(crate) fn write_record( + site_packages: &Path, + dist_info_prefix: &str, + mut record: Vec, +) -> Result<(), Error> { + let record_file = site_packages.join(format!("{dist_info_prefix}.dist-info/RECORD")); + let mut record_writer = csv::WriterBuilder::new() + .has_headers(false) + .escape(b'"') + .from_path(record_file)?; + record.sort(); + for entry in record { + record_writer.serialize(entry)?; + } + Ok(()) +} + +/// Validate the RECORD and heal invalid RECORD files. +/// +/// This ensures that all unpacked wheels have record that matches the contents, and uninstall can't +/// remove files that don't belong to the wheel. +/// +/// This function is given both the location of the unpacked wheel and the list of files from the +/// wheel that were unpacked to avoid a walkdir for this check. +pub fn validate_and_heal_record<'a>( + wheel_dir: &Path, + unpacked_wheel: impl IntoIterator, + dist: impl Display, +) -> Result<(), Error> { + // On the filesystem: The unpacked files of the wheel. + let mut files: BTreeMap<&Path, u64> = unpacked_wheel + .into_iter() + .map(|(path, size)| (path.as_path(), *size)) + .collect(); + + // In the record: The files we expect in the wheel. + let dist_info_prefix = find_dist_info(wheel_dir)?; + let dist_info_dir = format!("{dist_info_prefix}.dist-info"); + let record_path = wheel_dir.join(&dist_info_dir).join("RECORD"); + let mut record_file = File::open(&record_path)?; + let mut record = read_record(&mut record_file)?; + + // Remove matching files from both collections. + let mut extra_record_entries = Vec::new(); + record.retain(|entry| { + let path = Path::new(&entry.path); + if files.remove(path).is_some() { + return true; + } + // Allow non-canonical spellings such as `./foo`. + if files.remove(uv_fs::normalize_path(path).as_ref()).is_some() { + return true; + } + extra_record_entries.push(path.to_path_buf()); + false + }); + + if !files.is_empty() { + // Deprecated, but not listed in RECORD if used. + files.remove(Path::new(&dist_info_dir).join("RECORD.jws").as_path()); + files.remove(Path::new(&dist_info_dir).join("RECORD.p7s").as_path()); + } + + // If the RECORD was correct, there were no extra entries in the record and no missing entries + // that weren't removed from files. + if !extra_record_entries.is_empty() { + debug!( + "RECORD contains files not in wheel archive for {}: `{}`", + dist, + extra_record_entries + .iter() + .map(Simplified::simplified_display) + .join("`, `") + ); + } + if !files.is_empty() { + debug!( + "Wheel archive contains files not in RECORD for {}: `{}`", + dist, + files + .keys() + .map(Simplified::simplified_display) + .join("`, `") + ); + } + if !extra_record_entries.is_empty() || !files.is_empty() { + debug!("Rewriting RECORD to match actual wheel contents for {dist}"); + // We already removed RECORD entries with no matching unpacked file, now add files that + // were unpacked but not listed in the archive. + for (path, size) in files { + record.push(RecordEntry { + // RECORD entries always use forward slashes, even on Windows. + path: PortablePath::from(path).to_string(), + // We don't heal the hash. It's not validated anyway (pip doesn't), and by rules of + // the spec the wheel would have been rejected anyway (if the spec would have been + // enforced). + hash: None, + size: Some(size), + }); + } + + write_record(wheel_dir, &dist_info_prefix, record)?; + } + + Ok(()) +} + /// Parse a file with email message format such as WHEEL and METADATA fn parse_email_message_file( file: impl Read, @@ -842,7 +947,7 @@ fn parse_email_message_file( Ok(data) } -/// Find the `dist-info` directory in an unzipped wheel. +/// Find the prefix of the `dist-info` directory in an unzipped wheel. /// /// See: /// @@ -953,7 +1058,7 @@ mod test { use super::{ Error, RecordEntry, Script, WheelFile, format_shebang, get_script_executable, - parse_email_message_file, read_record_file, write_installer_metadata, + parse_email_message_file, read_record, write_installer_metadata, }; #[test] @@ -1040,7 +1145,7 @@ mod test { selenium-4.1.0.dist-info/RECORD,, "}; - let entries = read_record_file(&mut record.as_bytes()).unwrap(); + let entries = read_record(&mut record.as_bytes()).unwrap(); let expected = [ "selenium/__init__.py", "selenium/common/exceptions.py", diff --git a/crates/uv-installer/Cargo.toml b/crates/uv-installer/Cargo.toml index de7f0c1d4f16f..47e45a5d5644b 100644 --- a/crates/uv-installer/Cargo.toml +++ b/crates/uv-installer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-installer" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-installer/README.md b/crates/uv-installer/README.md index 665ff73b3242b..c2d91d02541fa 100644 --- a/crates/uv-installer/README.md +++ b/crates/uv-installer/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-installer). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-installer). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-installer/src/uninstall.rs b/crates/uv-installer/src/uninstall.rs index cfbea683083ce..eace930091c9b 100644 --- a/crates/uv-installer/src/uninstall.rs +++ b/crates/uv-installer/src/uninstall.rs @@ -1,18 +1,23 @@ use uv_distribution_types::{InstalledDist, InstalledDistKind, InstalledEggInfoFile}; +use uv_install_wheel::Layout; /// Uninstall a package from the specified Python environment. pub async fn uninstall( dist: &InstalledDist, + layout: &Layout, ) -> Result { let uninstall = tokio::task::spawn_blocking({ let dist = dist.clone(); + let layout = layout.clone(); move || match dist.kind { - InstalledDistKind::Registry(_) | InstalledDistKind::Url(_) => { - Ok(uv_install_wheel::uninstall_wheel(dist.install_path())?) - } - InstalledDistKind::EggInfoDirectory(_) => { - Ok(uv_install_wheel::uninstall_egg(dist.install_path())?) - } + InstalledDistKind::Registry(_) | InstalledDistKind::Url(_) => Ok( + uv_install_wheel::uninstall_wheel(dist.install_path(), &dist, &layout)?, + ), + InstalledDistKind::EggInfoDirectory(_) => Ok(uv_install_wheel::uninstall_egg( + dist.install_path(), + &dist, + &layout, + )?), InstalledDistKind::LegacyEditable(dist) => { Ok(uv_install_wheel::uninstall_legacy_editable(&dist.egg_link)?) } diff --git a/crates/uv-keyring/Cargo.toml b/crates/uv-keyring/Cargo.toml index 5ba2e893ecee9..cd75e4ce6b470 100644 --- a/crates/uv-keyring/Cargo.toml +++ b/crates/uv-keyring/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-keyring" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-logging/Cargo.toml b/crates/uv-logging/Cargo.toml index b63b8e1f9ba2a..1eac47a069b0f 100644 --- a/crates/uv-logging/Cargo.toml +++ b/crates/uv-logging/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-logging" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-logging/README.md b/crates/uv-logging/README.md index 9429575fdb8d6..1ab7ff7f04eef 100644 --- a/crates/uv-logging/README.md +++ b/crates/uv-logging/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-logging). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-logging). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-macros/Cargo.toml b/crates/uv-macros/Cargo.toml index 9a7b216ad76eb..ba7d21e87b003 100644 --- a/crates/uv-macros/Cargo.toml +++ b/crates/uv-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-macros" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-macros/README.md b/crates/uv-macros/README.md index 7c14e1f5d75f2..2297f6bf752e2 100644 --- a/crates/uv-macros/README.md +++ b/crates/uv-macros/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-macros). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-macros). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-metadata/Cargo.toml b/crates/uv-metadata/Cargo.toml index 6bda9a6cdc052..933e0c4476371 100644 --- a/crates/uv-metadata/Cargo.toml +++ b/crates/uv-metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-metadata" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-metadata/README.md b/crates/uv-metadata/README.md index 6d7a9c8ff0d7b..e3a82ea4307c9 100644 --- a/crates/uv-metadata/README.md +++ b/crates/uv-metadata/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-metadata). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-metadata). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-normalize/Cargo.toml b/crates/uv-normalize/Cargo.toml index fbe1c60b2297e..74fc767bf3afa 100644 --- a/crates/uv-normalize/Cargo.toml +++ b/crates/uv-normalize/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-normalize" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-normalize/README.md b/crates/uv-normalize/README.md index 4adcfe42e64c3..6b0d88730253d 100644 --- a/crates/uv-normalize/README.md +++ b/crates/uv-normalize/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-normalize). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-normalize). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-once-map/Cargo.toml b/crates/uv-once-map/Cargo.toml index 861b5376bd26f..91239417ebd8f 100644 --- a/crates/uv-once-map/Cargo.toml +++ b/crates/uv-once-map/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-once-map" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-once-map/README.md b/crates/uv-once-map/README.md index 2252eac2cb694..7a07f385b57ec 100644 --- a/crates/uv-once-map/README.md +++ b/crates/uv-once-map/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-once-map). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-once-map). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-options-metadata/Cargo.toml b/crates/uv-options-metadata/Cargo.toml index 626fc113e2056..61d6f0cf4ac6f 100644 --- a/crates/uv-options-metadata/Cargo.toml +++ b/crates/uv-options-metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-options-metadata" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-options-metadata/README.md b/crates/uv-options-metadata/README.md index 7c86e90b7da6c..34b3bf80cfde0 100644 --- a/crates/uv-options-metadata/README.md +++ b/crates/uv-options-metadata/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-options-metadata). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-options-metadata). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-pep440/Cargo.toml b/crates/uv-pep440/Cargo.toml index 4ece062a618c1..0a737c39eeeba 100644 --- a/crates/uv-pep440/Cargo.toml +++ b/crates/uv-pep440/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-pep440" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" license = "Apache-2.0 OR BSD-2-Clause" include = ["/src", "Changelog.md", "License-Apache", "License-BSD", "Readme.md", "pyproject.toml"] diff --git a/crates/uv-pep440/README.md b/crates/uv-pep440/README.md index 0993383eb6d95..673eb7c28c1e9 100644 --- a/crates/uv-pep440/README.md +++ b/crates/uv-pep440/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-pep440). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-pep440). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-pep508/Cargo.toml b/crates/uv-pep508/Cargo.toml index 4d3e620245970..6cc087d9dc890 100644 --- a/crates/uv-pep508/Cargo.toml +++ b/crates/uv-pep508/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-pep508" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" include = ["/src", "Changelog.md", "License-Apache", "License-BSD", "Readme.md", "pyproject.toml"] license = "Apache-2.0 OR BSD-2-Clause" diff --git a/crates/uv-pep508/README.md b/crates/uv-pep508/README.md index fcbef1fee996f..b5699294805c0 100644 --- a/crates/uv-pep508/README.md +++ b/crates/uv-pep508/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-pep508). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-pep508). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-performance-memory-allocator/Cargo.toml b/crates/uv-performance-memory-allocator/Cargo.toml index 9d248188dfb1a..2e4e6e3ee2fee 100644 --- a/crates/uv-performance-memory-allocator/Cargo.toml +++ b/crates/uv-performance-memory-allocator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-performance-memory-allocator" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-performance-memory-allocator/README.md b/crates/uv-performance-memory-allocator/README.md index 5e33d0c394ca5..7901b832ec825 100644 --- a/crates/uv-performance-memory-allocator/README.md +++ b/crates/uv-performance-memory-allocator/README.md @@ -5,9 +5,9 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source can be found -[here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-performance-memory-allocator). +[here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-performance-memory-allocator). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-platform-tags/Cargo.toml b/crates/uv-platform-tags/Cargo.toml index aa48d1ecfc790..b5e08b63a3e05 100644 --- a/crates/uv-platform-tags/Cargo.toml +++ b/crates/uv-platform-tags/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-platform-tags" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-platform-tags/README.md b/crates/uv-platform-tags/README.md index 218d3a9aba185..9131b8fcbb8ba 100644 --- a/crates/uv-platform-tags/README.md +++ b/crates/uv-platform-tags/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-platform-tags). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-platform-tags). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-platform/Cargo.toml b/crates/uv-platform/Cargo.toml index 7cec5481d84b6..b9e6a2e403a33 100644 --- a/crates/uv-platform/Cargo.toml +++ b/crates/uv-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-platform" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-platform/README.md b/crates/uv-platform/README.md index 368ef4a47f12e..f9915e4c37236 100644 --- a/crates/uv-platform/README.md +++ b/crates/uv-platform/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-platform). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-platform). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-preview/Cargo.toml b/crates/uv-preview/Cargo.toml index ee7d26555ebfc..9972325ffc3e0 100644 --- a/crates/uv-preview/Cargo.toml +++ b/crates/uv-preview/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-preview" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-preview/README.md b/crates/uv-preview/README.md index fe676b635e421..0233e02059ee9 100644 --- a/crates/uv-preview/README.md +++ b/crates/uv-preview/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-preview). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-preview). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-publish/Cargo.toml b/crates/uv-publish/Cargo.toml index 2459ca08aa059..8df4a86ce56f4 100644 --- a/crates/uv-publish/Cargo.toml +++ b/crates/uv-publish/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-publish" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-publish/README.md b/crates/uv-publish/README.md index a96e4730e18a9..60072da0dad91 100644 --- a/crates/uv-publish/README.md +++ b/crates/uv-publish/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-publish). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-publish). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-pypi-types/Cargo.toml b/crates/uv-pypi-types/Cargo.toml index 20e2a084b76e7..86258569860e0 100644 --- a/crates/uv-pypi-types/Cargo.toml +++ b/crates/uv-pypi-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-pypi-types" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-pypi-types/README.md b/crates/uv-pypi-types/README.md index 00d977577b616..2c76d99847bf6 100644 --- a/crates/uv-pypi-types/README.md +++ b/crates/uv-pypi-types/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-pypi-types). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-pypi-types). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-python/Cargo.toml b/crates/uv-python/Cargo.toml index 309cd9efcdc0f..f3dabd1bb94a2 100644 --- a/crates/uv-python/Cargo.toml +++ b/crates/uv-python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-python" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-python/README.md b/crates/uv-python/README.md index 899af6221e164..3555551ecacdc 100644 --- a/crates/uv-python/README.md +++ b/crates/uv-python/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-python). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-python). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-redacted/Cargo.toml b/crates/uv-redacted/Cargo.toml index afe4087035766..561dfe34dc538 100644 --- a/crates/uv-redacted/Cargo.toml +++ b/crates/uv-redacted/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-redacted" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-redacted/README.md b/crates/uv-redacted/README.md index 8b745a2e1a26f..9392fb14b8873 100644 --- a/crates/uv-redacted/README.md +++ b/crates/uv-redacted/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-redacted). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-redacted). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-requirements-txt/Cargo.toml b/crates/uv-requirements-txt/Cargo.toml index 5dfb9bdc18e65..5b7b87651b32a 100644 --- a/crates/uv-requirements-txt/Cargo.toml +++ b/crates/uv-requirements-txt/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-requirements-txt" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-requirements-txt/README.md b/crates/uv-requirements-txt/README.md index 7dd8a47712cf8..e0c0cb6a1ae13 100644 --- a/crates/uv-requirements-txt/README.md +++ b/crates/uv-requirements-txt/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-requirements-txt). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-requirements-txt). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-requirements/Cargo.toml b/crates/uv-requirements/Cargo.toml index fa7539697042d..c10edd2754133 100644 --- a/crates/uv-requirements/Cargo.toml +++ b/crates/uv-requirements/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-requirements" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-requirements/README.md b/crates/uv-requirements/README.md index fba457a928a71..f6c50ff5bf3d0 100644 --- a/crates/uv-requirements/README.md +++ b/crates/uv-requirements/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-requirements). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-requirements). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index 791c57db8a663..205209bbbdc06 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-resolver" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-resolver/README.md b/crates/uv-resolver/README.md index 36be083ba7ede..e579006ba6c61 100644 --- a/crates/uv-resolver/README.md +++ b/crates/uv-resolver/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-resolver). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-resolver). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-scripts/Cargo.toml b/crates/uv-scripts/Cargo.toml index 44ca5cca375f6..00f0d973cfa2e 100644 --- a/crates/uv-scripts/Cargo.toml +++ b/crates/uv-scripts/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-scripts" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-scripts/README.md b/crates/uv-scripts/README.md index e015659317628..864f33ac50a5b 100644 --- a/crates/uv-scripts/README.md +++ b/crates/uv-scripts/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-scripts). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-scripts). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-settings/Cargo.toml b/crates/uv-settings/Cargo.toml index 58ba03bd0aba4..fc82235044110 100644 --- a/crates/uv-settings/Cargo.toml +++ b/crates/uv-settings/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-settings" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-settings/README.md b/crates/uv-settings/README.md index c47c8ea26be59..bd0204d5be675 100644 --- a/crates/uv-settings/README.md +++ b/crates/uv-settings/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-settings). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-settings). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-shell/Cargo.toml b/crates/uv-shell/Cargo.toml index 82b138be96ff2..dfcf132f7a2a7 100644 --- a/crates/uv-shell/Cargo.toml +++ b/crates/uv-shell/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-shell" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-shell/README.md b/crates/uv-shell/README.md index bc7c03bf98671..1d8c0f3f398e6 100644 --- a/crates/uv-shell/README.md +++ b/crates/uv-shell/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-shell). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-shell). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-small-str/Cargo.toml b/crates/uv-small-str/Cargo.toml index 4e8a7a8f699c2..e00b40b99449f 100644 --- a/crates/uv-small-str/Cargo.toml +++ b/crates/uv-small-str/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-small-str" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-small-str/README.md b/crates/uv-small-str/README.md index 5e2b2c76450fa..863e56b9a5254 100644 --- a/crates/uv-small-str/README.md +++ b/crates/uv-small-str/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-small-str). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-small-str). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-state/Cargo.toml b/crates/uv-state/Cargo.toml index 26bd6c6d4b4e6..e04e1b3305d04 100644 --- a/crates/uv-state/Cargo.toml +++ b/crates/uv-state/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-state" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-state/README.md b/crates/uv-state/README.md index 89ea6d4beeb90..88d016f976ec7 100644 --- a/crates/uv-state/README.md +++ b/crates/uv-state/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-state). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-state). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-static/Cargo.toml b/crates/uv-static/Cargo.toml index d1fbdf5e679d5..89ee84c8e24eb 100644 --- a/crates/uv-static/Cargo.toml +++ b/crates/uv-static/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-static" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-static/README.md b/crates/uv-static/README.md index e42af47d31f79..8c89e750e4cec 100644 --- a/crates/uv-static/README.md +++ b/crates/uv-static/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-static). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-static). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-test/Cargo.toml b/crates/uv-test/Cargo.toml index 3fb681332f6fa..e4e87d7c8ee57 100644 --- a/crates/uv-test/Cargo.toml +++ b/crates/uv-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-test" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-test/README.md b/crates/uv-test/README.md index 1917ba51a3190..003a90d7ff1e7 100644 --- a/crates/uv-test/README.md +++ b/crates/uv-test/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-test). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-test). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-tool/Cargo.toml b/crates/uv-tool/Cargo.toml index 7bd874f17dcaf..6fd16ef637b1f 100644 --- a/crates/uv-tool/Cargo.toml +++ b/crates/uv-tool/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-tool" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-tool/README.md b/crates/uv-tool/README.md index c1c93ca826c9c..39a4310fb03f1 100644 --- a/crates/uv-tool/README.md +++ b/crates/uv-tool/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-tool). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-tool). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-tool/src/lib.rs b/crates/uv-tool/src/lib.rs index 336f3e9a39843..3e0a11bae4d53 100644 --- a/crates/uv-tool/src/lib.rs +++ b/crates/uv-tool/src/lib.rs @@ -11,7 +11,7 @@ use tracing::{debug, warn}; use uv_cache::Cache; use uv_dirs::user_executable_directory; use uv_fs::{LockedFile, LockedFileError, LockedFileMode, Simplified}; -use uv_install_wheel::read_record_file; +use uv_install_wheel::read_record; use uv_installer::SitePackages; use uv_normalize::{InvalidNameError, PackageName}; use uv_pep440::Version; @@ -459,7 +459,7 @@ pub fn entrypoint_paths( ); // Read the RECORD file. - let record = read_record_file(&mut File::open(dist_info_path.join("RECORD"))?)?; + let record = read_record(File::open(dist_info_path.join("RECORD"))?)?; // The RECORD file uses relative paths, so we're looking for the relative path to be a prefix. let layout = site_packages.interpreter().layout(); diff --git a/crates/uv-torch/Cargo.toml b/crates/uv-torch/Cargo.toml index 19d1b46d1080d..c9467095c4bfd 100644 --- a/crates/uv-torch/Cargo.toml +++ b/crates/uv-torch/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-torch" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-torch/README.md b/crates/uv-torch/README.md index 89b7c83816f31..9ab172ba15d8a 100644 --- a/crates/uv-torch/README.md +++ b/crates/uv-torch/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-torch). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-torch). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-trampoline-builder/Cargo.toml b/crates/uv-trampoline-builder/Cargo.toml index 54c6b4a3ff4d8..5d16548a3f1f1 100644 --- a/crates/uv-trampoline-builder/Cargo.toml +++ b/crates/uv-trampoline-builder/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-trampoline-builder" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } diff --git a/crates/uv-trampoline-builder/README.md b/crates/uv-trampoline-builder/README.md index d97a0cc916bae..dcbdba87a271c 100644 --- a/crates/uv-trampoline-builder/README.md +++ b/crates/uv-trampoline-builder/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-trampoline-builder). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-trampoline-builder). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-trampoline/Cargo.lock b/crates/uv-trampoline/Cargo.lock index ebba601bbd825..1bb7161584c8b 100644 --- a/crates/uv-trampoline/Cargo.lock +++ b/crates/uv-trampoline/Cargo.lock @@ -138,7 +138,7 @@ checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "uv-macros" -version = "0.0.38" +version = "0.0.39" dependencies = [ "proc-macro2", "quote", @@ -148,7 +148,7 @@ dependencies = [ [[package]] name = "uv-static" -version = "0.0.38" +version = "0.0.39" dependencies = [ "thiserror", "uv-macros", @@ -169,7 +169,7 @@ dependencies = [ [[package]] name = "uv-windows" -version = "0.0.38" +version = "0.0.39" dependencies = [ "windows", ] diff --git a/crates/uv-types/Cargo.toml b/crates/uv-types/Cargo.toml index 6a9ad313033f5..96a297a37b640 100644 --- a/crates/uv-types/Cargo.toml +++ b/crates/uv-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-types" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-types/README.md b/crates/uv-types/README.md index c1d23412463db..b015f74177fa1 100644 --- a/crates/uv-types/README.md +++ b/crates/uv-types/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-types). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-types). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-unix/Cargo.toml b/crates/uv-unix/Cargo.toml index d0c401c0a4cb7..f1d41f199f7d2 100644 --- a/crates/uv-unix/Cargo.toml +++ b/crates/uv-unix/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-unix" -version = "0.0.38" +version = "0.0.39" description = "Unix-specific functionality for uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-unix/README.md b/crates/uv-unix/README.md index b48e7e725c835..f7772f543dbdc 100644 --- a/crates/uv-unix/README.md +++ b/crates/uv-unix/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-unix). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-unix). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml index 89b135ef99a23..5ca59ea739f50 100644 --- a/crates/uv-version/Cargo.toml +++ b/crates/uv-version/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-version" -version = "0.11.5" +version = "0.11.6-dev.0" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-version/README.md b/crates/uv-version/README.md index 02df15fcd343f..e5f30b681d07c 100644 --- a/crates/uv-version/README.md +++ b/crates/uv-version/README.md @@ -5,8 +5,8 @@ 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.11.5) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-version). +This version (0.11.6) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-version). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-virtualenv/Cargo.toml b/crates/uv-virtualenv/Cargo.toml index a409baa737634..1355b738bb7e2 100644 --- a/crates/uv-virtualenv/Cargo.toml +++ b/crates/uv-virtualenv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-virtualenv" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" keywords = ["virtualenv", "venv", "python"] diff --git a/crates/uv-warnings/Cargo.toml b/crates/uv-warnings/Cargo.toml index 3507b1b329a1a..be1bfb6818ee8 100644 --- a/crates/uv-warnings/Cargo.toml +++ b/crates/uv-warnings/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-warnings" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-warnings/README.md b/crates/uv-warnings/README.md index a15377a9b941c..aced41624f574 100644 --- a/crates/uv-warnings/README.md +++ b/crates/uv-warnings/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-warnings). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-warnings). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-windows/Cargo.toml b/crates/uv-windows/Cargo.toml index 6e267ff56e604..0dc7b3daa3419 100644 --- a/crates/uv-windows/Cargo.toml +++ b/crates/uv-windows/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-windows" -version = "0.0.38" +version = "0.0.39" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/crates/uv-windows/README.md b/crates/uv-windows/README.md index f7f0da09566b5..403e40a4dacac 100644 --- a/crates/uv-windows/README.md +++ b/crates/uv-windows/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-windows). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-windows). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-workspace/Cargo.toml b/crates/uv-workspace/Cargo.toml index 6060ee2499b5c..fe6a1ce1a7e9b 100644 --- a/crates/uv-workspace/Cargo.toml +++ b/crates/uv-workspace/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-workspace" -version = "0.0.38" +version = "0.0.39" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-workspace/README.md b/crates/uv-workspace/README.md index 2af53e50e6585..0571d508f7994 100644 --- a/crates/uv-workspace/README.md +++ b/crates/uv-workspace/README.md @@ -5,8 +5,8 @@ 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.38) is a component of [uv 0.11.5](https://crates.io/crates/uv/0.11.5). The source -can be found [here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv-workspace). +This version (0.0.39) is a component of [uv 0.11.6](https://crates.io/crates/uv/0.11.6). The source +can be found [here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv-workspace). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index f792eabc287af..6200f71cf0d17 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv" -version = "0.11.5" +version = "0.11.6-dev.0" description = "A Python package and project manager" edition = { workspace = true } rust-version = { workspace = true } @@ -70,8 +70,8 @@ uv-workspace = { workspace = true, features = ["clap"] } anstream = { workspace = true } anyhow = { workspace = true } axoupdater = { workspace = true, features = [ - "github_releases", - "tokio", + "github_releases", + "tokio", ], optional = true } clap = { workspace = true, features = ["derive", "string", "wrap_help"] } console = { workspace = true } @@ -175,16 +175,16 @@ tracing-durations-export = ["dep:tracing-durations-export", "uv-resolver/tracing # Features that only apply when running tests, no-ops otherwise. test-defaults = [ - "test-crates-io", - "test-git", - "test-git-lfs", - "test-pypi", - "test-r2", - "test-python", - "test-python-managed", - "test-python-eol", - "test-slow", - "test-ecosystem" + "test-crates-io", + "test-git", + "test-git-lfs", + "test-pypi", + "test-r2", + "test-python", + "test-python-managed", + "test-python-eol", + "test-slow", + "test-ecosystem" ] # Introduces a testing dependency on crates.io. test-crates-io = [] diff --git a/crates/uv/README.md b/crates/uv/README.md index d2905c3e847c2..a9ca403541a74 100644 --- a/crates/uv/README.md +++ b/crates/uv/README.md @@ -10,8 +10,8 @@ for more information. This crate is the entry point to the uv command-line interface. The Rust API exposed here is not considered public interface. -This is version 0.11.5. The source can be found -[here](https://github.com/astral-sh/uv/blob/0.11.5/crates/uv). +This is version 0.11.6. The source can be found +[here](https://github.com/astral-sh/uv/blob/0.11.6/crates/uv). The following uv workspace members are also available: diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index bc91600ec9202..d3bd8c4c9f9ec 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -806,8 +806,9 @@ async fn execute_plan( if !uninstalls.is_empty() { let start = std::time::Instant::now(); + let layout = venv.interpreter().layout(); for dist_info in &uninstalls { - match uv_installer::uninstall(dist_info).await { + match uv_installer::uninstall(dist_info, &layout).await { Ok(summary) => { debug!( "Uninstalled {} ({} file{}, {} director{})", diff --git a/crates/uv/src/commands/pip/show.rs b/crates/uv/src/commands/pip/show.rs index a3d5186e8eea5..1dd5d8403966a 100644 --- a/crates/uv/src/commands/pip/show.rs +++ b/crates/uv/src/commands/pip/show.rs @@ -10,7 +10,7 @@ use tracing::debug; use uv_cache::Cache; use uv_distribution_types::{DependencyMetadata, Diagnostic, Name}; use uv_fs::Simplified; -use uv_install_wheel::read_record_file; +use uv_install_wheel::read_record; use uv_installer::SitePackages; use uv_normalize::PackageName; use uv_preview::Preview; @@ -218,7 +218,7 @@ pub(crate) fn pip_show( // If requests, show the list of installed files. if files { let path = distribution.install_path().join("RECORD"); - let record = read_record_file(&mut File::open(path)?)?; + let record = read_record(&mut File::open(path)?)?; writeln!(printer.stdout(), "Files:")?; for entry in record { writeln!(printer.stdout(), " {}", entry.path)?; diff --git a/crates/uv/src/commands/pip/uninstall.rs b/crates/uv/src/commands/pip/uninstall.rs index 354083081161a..502ec335501d1 100644 --- a/crates/uv/src/commands/pip/uninstall.rs +++ b/crates/uv/src/commands/pip/uninstall.rs @@ -201,8 +201,9 @@ pub(crate) async fn pip_uninstall( // Uninstall each package. if !dry_run.enabled() { + let layout = environment.interpreter().layout(); for distribution in &distributions { - let summary = uv_installer::uninstall(distribution).await?; + let summary = uv_installer::uninstall(distribution, &layout).await?; debug!( "Uninstalled {} ({} file{}, {} director{})", distribution.name(), diff --git a/crates/uv/src/commands/project/audit.rs b/crates/uv/src/commands/project/audit.rs index 334b86a5bbbe0..1ec5aec7ae515 100644 --- a/crates/uv/src/commands/project/audit.rs +++ b/crates/uv/src/commands/project/audit.rs @@ -211,7 +211,7 @@ pub(crate) async fn audit( let client = base_client.for_host(&osv_url).raw_client().clone(); let service = osv::Osv::new(client, Some(osv_url), concurrency); trace!("Auditing {n} dependencies against OSV", n = auditable.len()); - service.query_batch(&dependencies).await? + service.query_batch(&dependencies, osv::Filter::All).await? } } }; diff --git a/crates/uv/tests/it/cache_clean.rs b/crates/uv/tests/it/cache_clean.rs index 03f812bd0138c..e380a7ef271c3 100644 --- a/crates/uv/tests/it/cache_clean.rs +++ b/crates/uv/tests/it/cache_clean.rs @@ -275,3 +275,48 @@ async fn cache_timeout() { error: Timeout ([TIME]) when waiting for lock on `[CACHE_DIR]/` at `[CACHE_DIR]/.lock`, is another uv process running? You can set `UV_LOCK_TIMEOUT` to increase the timeout. "); } + +/// `cache clean` should handle file paths normally restricted by Win32 path normalization. +#[cfg(windows)] +#[test] +fn clean_handles_verbatim_paths() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + // Clean slate + fs_err::remove_dir_all(&context.cache_dir)?; + + // Cached sdist path resembling the uwsgi==2.0.31 build failure. + let uwsgi_shard = context + .cache_dir + .child("sdists-v9") + .child("pypi") + .child("uwsgi") + .child("2.0.31") + .child("QxDIp0qpjbsWjWURKmegK") + .child("src") + .child("core"); + + // Attempt to create a file with a trailing dot (we need to make it verbatim to do so) + uwsgi_shard.create_dir_all()?; + let invalid_path = uwsgi_shard.child("logging.").to_path_buf(); + let invalid_file = uv_fs::verbatim_path(invalid_path.as_path()); + fs_err::write(&invalid_file, b"")?; + + // Confirm Win32 normalized path causes an os error when attempting to remove + let remove_err = fs_err::remove_file(&invalid_path).expect_err("expected to fail"); + assert_eq!(remove_err.kind(), std::io::ErrorKind::NotFound); + + // Tests cache clean leverages verbatim conversion + uv_snapshot!(context.filters(), context.clean().arg("--verbose"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + DEBUG uv [VERSION] ([COMMIT] DATE) + Clearing cache at: [CACHE_DIR]/ + Removed 2 files + "); + + Ok(()) +} diff --git a/crates/uv/tests/it/extract.rs b/crates/uv/tests/it/extract.rs index bfdd4b2ffd5ef..1bf3479445002 100644 --- a/crates/uv/tests/it/extract.rs +++ b/crates/uv/tests/it/extract.rs @@ -23,7 +23,8 @@ async fn unzip(url: &str) -> anyhow::Result<(), uv_extract::Error> { .into_async_read(); let target = tempfile::TempDir::new().map_err(uv_extract::Error::Io)?; - uv_extract::stream::unzip(url, reader.compat(), target.path()).await + uv_extract::stream::unzip(url, reader.compat(), target.path()).await?; + Ok(()) } #[tokio::test] diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index e7d84758c2a3c..a6878ac8a7038 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -1,3 +1,4 @@ +use std::io; use std::io::Cursor; use std::path::PathBuf; use std::process::Command; @@ -9,20 +10,24 @@ use flate2::write::GzEncoder; use fs_err as fs; use fs_err::File; use indoc::{formatdoc, indoc}; +use insta::assert_snapshot; use predicates::prelude::predicate; use url::Url; +use walkdir::WalkDir; use wiremock::{ Mock, MockServer, ResponseTemplate, matchers::{basic_auth, method, path}, }; +use zip::write::SimpleFileOptions; +use zip::{ZipArchive, ZipWriter}; -use uv_fs::Simplified; +use uv_fs::{PortablePath, Simplified}; use uv_static::EnvVars; #[cfg(feature = "test-git")] use uv_test::decode_token; use uv_test::{ - DEFAULT_PYTHON_VERSION, TestContext, build_vendor_links_url, download_to_disk, get_bin, - packse_index_url, uv_snapshot, venv_bin_path, + DEFAULT_PYTHON_VERSION, TestContext, apply_filters, build_vendor_links_url, download_to_disk, + get_bin, packse_index_url, uv_snapshot, venv_bin_path, }; #[test] @@ -14918,3 +14923,106 @@ fn upgrade_group_not_supported() { error: `--upgrade-group` is not supported in `uv pip` commands "); } + +/// Check that the RECORD in a wheel with that doesn't match its contents gets fixed before +/// installation. +#[test] +fn handle_record_mismatches() -> Result<()> { + let context = uv_test::test_context!("3.12") + .with_filter(( + regex::escape(r"foo-0.1.0.dist-info/uv_cache.json,sha256=") + ".*", + r"foo-0.1.0.dist-info/uv_cache.json,sha256=[SHA256],[SIZE]", + )) + .with_filter(( + regex::escape(r"foo-0.1.0.dist-info/WHEEL,sha256=") + ".*", + r"foo-0.1.0.dist-info/WHEEL,sha256=[SHA256],[SIZE]", + )); + + // Build a small wheel and unpack it for modification. + context.init().arg("--lib").arg("foo").assert().success(); + context.build().arg("--wheel").arg("foo").assert().success(); + let built_wheel = context.temp_dir.join("foo/dist/foo-0.1.0-py3-none-any.whl"); + let unpacked = context.temp_dir.join("foo-unpacked"); + ZipArchive::new(File::open(built_wheel)?)?.extract(&unpacked)?; + + // Snapshot the current (correct) RECORD. + let record = unpacked.join("foo-0.1.0.dist-info/RECORD"); + let correct_record = fs_err::read_to_string(&record)?; + let correct_record = apply_filters(correct_record, context.filters()); + assert_snapshot!(correct_record, @" + foo/__init__.py,sha256=jv2QBpHSNajIRNeADSmtqOWL9QcdUddyMK277kbp06o,49 + foo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 + foo-0.1.0.dist-info/WHEEL,sha256=[SHA256],[SIZE] + foo-0.1.0.dist-info/METADATA,sha256=d-PGjuBKXweF5NgWUW5yDnAjBY0lg4uFZBqcnmGtNgY,147 + foo-0.1.0.dist-info/RECORD,, + "); + + // Create a broken RECORD: Remove 2 files, and add a bogus one. + fs_err::write( + &record, + indoc! {" + foo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 + foo-0.1.0.dist-info/WHEEL,sha256=hbX8mDThv1n7VEIpQRy6c2yAFTw4iAQlEC53gDAhHSo,80 + foo-0.1.0.dist-info/RECORD,, + ../../../../etc/passwd,,0 + "}, + )?; + + // Repack the wheel. + let repacked_wheel = context.temp_dir.join("foo-0.1.0-py3-none-any.whl"); + let mut writer = ZipWriter::new(File::create(&repacked_wheel)?); + let options = SimpleFileOptions::default(); + for entry in WalkDir::new(&unpacked) { + let entry = entry?; + let path = entry.path(); + let name = path.strip_prefix(&unpacked)?; + if name.as_os_str().is_empty() { + continue; + } + // Zip entries must use forward slashes, even on Windows. + let name = PortablePath::from(name).to_string(); + if path.is_dir() { + writer.add_directory(&name, options)?; + } else { + writer.start_file(&name, options)?; + io::copy(&mut File::open(path)?, &mut writer)?; + } + } + writer.finish()?; + + uv_snapshot!(context.filters(), context.pip_install() + .arg("--find-links") + .arg(context.temp_dir.as_ref()) + .arg("--offline") + .arg("foo"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + foo==0.1.0 + " + ); + + // Read the healed RECORD. + let installed_record = + fs_err::read_to_string(context.site_packages().join("foo-0.1.0.dist-info/RECORD"))?; + let snapshot = apply_filters(installed_record, context.filters()); + + // Ensure that all expected files are present. + assert_snapshot!(&snapshot, @" + foo-0.1.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 + foo-0.1.0.dist-info/METADATA,,147 + foo-0.1.0.dist-info/RECORD,, + foo-0.1.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 + foo-0.1.0.dist-info/WHEEL,sha256=[SHA256],[SIZE] + foo-0.1.0.dist-info/uv_cache.json,sha256=[SHA256],[SIZE] + foo/__init__.py,,49 + foo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 + "); + + Ok(()) +} diff --git a/crates/uv/tests/it/pip_uninstall.rs b/crates/uv/tests/it/pip_uninstall.rs index aa4574b8a6177..4cd6b26c06d0c 100644 --- a/crates/uv/tests/it/pip_uninstall.rs +++ b/crates/uv/tests/it/pip_uninstall.rs @@ -509,3 +509,73 @@ fn dry_run_uninstall_egg_info() -> Result<()> { Ok(()) } + +/// Uninstall must not remove files outside the install scheme. +/// +/// A malformed or malicious wheel can include path-traversal entries +/// (e.g. `../../../../../etc/passwd`) in its RECORD file. During uninstall those entries are joined +/// with the site-packages directory and could cause deletion of files outside the installation +/// scheme. +#[test] +fn uninstall_record_path_traversal() -> Result<()> { + // The traversal-depth count differs between Unix (`.venv/lib/pythonX.Y/site-packages`) + // and Windows (`.venv/Lib/site-packages`), so normalize the `../` sequence in the warning. + let context = uv_test::test_context!("3.12").with_filter(( + r"(\.\./)+traversal_target\.txt", + "[..]/traversal_target.txt", + )); + + context + .init() + .arg("--lib") + .arg("evilpkg") + .assert() + .success(); + context.pip_install().arg("./evilpkg").assert().success(); + + // Build the relative traversal path from site-packages to a target file outside + // site-packages but inside the test temp dir. RECORD uses forward slashes, even on + // Windows, and the venv layout (and thus the traversal depth) differs by platform, + // so we construct the path manually and filter the leading `../` sequence out of the + // snapshot above. + let target_file = context.temp_dir.child("traversal_target.txt"); + target_file.write_str("I should not be deleted")?; + // Canonicalize the temp dir, since `site_packages` is built from a canonicalized path + // (with `\\?\`), which would otherwise make `strip_prefix` fail. + let canonical_temp_dir = context.temp_dir.canonicalize()?; + let depth = context + .site_packages() + .strip_prefix(&canonical_temp_dir)? + .components() + .count(); + let traversal_record = format!("{}traversal_target.txt", "../".repeat(depth)); + + let record_file = context + .site_packages() + .join("evilpkg-0.1.0.dist-info/RECORD"); + let record = fs_err::read_to_string(&record_file)?; + let record = format!("{}\n{},,0\n", record.trim(), traversal_record); + fs_err::write(record_file, &record)?; + + let init_py = context.site_packages().join("evilpkg/__init__.py"); + assert!(context.site_packages().join(&traversal_record).exists()); + assert!(init_py.exists()); + + uv_snapshot!(context.filters(), context.pip_uninstall() + .arg("evilpkg"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: Invalid RECORD entry in evilpkg==0.1.0 (from file://[TEMP_DIR]/evilpkg) that escapes the Python environment, skipping: [..]/traversal_target.txt + Uninstalled 1 package in [TIME] + - evilpkg==0.1.0 (from file://[TEMP_DIR]/evilpkg) + "); + + // The regular package files have been removed, while the file outside the scheme still exists. + assert!(target_file.exists()); + assert!(!init_py.exists()); + + Ok(()) +} diff --git a/docs/concepts/build-backend.md b/docs/concepts/build-backend.md index 5aa790814917a..5a600d91c8ffb 100644 --- a/docs/concepts/build-backend.md +++ b/docs/concepts/build-backend.md @@ -31,7 +31,7 @@ To use uv as a build backend in an existing project, add `uv_build` to the ```toml title="pyproject.toml" [build-system] -requires = ["uv_build>=0.11.5,<0.12"] +requires = ["uv_build>=0.11.6,<0.12"] build-backend = "uv_build" ``` diff --git a/docs/concepts/projects/init.md b/docs/concepts/projects/init.md index 1ad7886308197..7a8306c627f5c 100644 --- a/docs/concepts/projects/init.md +++ b/docs/concepts/projects/init.md @@ -113,7 +113,7 @@ dependencies = [] example-pkg = "example_pkg:main" [build-system] -requires = ["uv_build>=0.11.5,<0.12"] +requires = ["uv_build>=0.11.6,<0.12"] build-backend = "uv_build" ``` @@ -136,7 +136,7 @@ dependencies = [] example-pkg = "example_pkg:main" [build-system] -requires = ["uv_build>=0.11.5,<0.12"] +requires = ["uv_build>=0.11.6,<0.12"] build-backend = "uv_build" ``` @@ -197,7 +197,7 @@ requires-python = ">=3.11" dependencies = [] [build-system] -requires = ["uv_build>=0.11.5,<0.12"] +requires = ["uv_build>=0.11.6,<0.12"] build-backend = "uv_build" ``` diff --git a/docs/concepts/projects/workspaces.md b/docs/concepts/projects/workspaces.md index a07f922174167..0b3a6ed321337 100644 --- a/docs/concepts/projects/workspaces.md +++ b/docs/concepts/projects/workspaces.md @@ -75,7 +75,7 @@ bird-feeder = { workspace = true } members = ["packages/*"] [build-system] -requires = ["uv_build>=0.11.5,<0.12"] +requires = ["uv_build>=0.11.6,<0.12"] build-backend = "uv_build" ``` @@ -106,7 +106,7 @@ tqdm = { git = "https://github.com/tqdm/tqdm" } members = ["packages/*"] [build-system] -requires = ["uv_build>=0.11.5,<0.12"] +requires = ["uv_build>=0.11.6,<0.12"] build-backend = "uv_build" ``` @@ -188,7 +188,7 @@ dependencies = ["bird-feeder", "tqdm>=4,<5"] bird-feeder = { path = "packages/bird-feeder" } [build-system] -requires = ["uv_build>=0.11.5,<0.12"] +requires = ["uv_build>=0.11.6,<0.12"] build-backend = "uv_build" ``` diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 4a3ede0445253..be03ecea3b33b 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -25,7 +25,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```console - $ curl -LsSf https://astral.sh/uv/0.11.5/install.sh | sh + $ curl -LsSf https://astral.sh/uv/0.11.6/install.sh | sh ``` === "Windows" @@ -41,7 +41,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```pwsh-session - PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.11.5/install.ps1 | iex" + PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.11.6/install.ps1 | iex" ``` !!! tip diff --git a/docs/guides/integration/aws-lambda.md b/docs/guides/integration/aws-lambda.md index 429feeba53b12..a29f3b59cd05c 100644 --- a/docs/guides/integration/aws-lambda.md +++ b/docs/guides/integration/aws-lambda.md @@ -92,7 +92,7 @@ the second stage, we'll copy this directory over to the final image, omitting th other unnecessary files. ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.11.5 AS uv +FROM ghcr.io/astral-sh/uv:0.11.6 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder @@ -334,7 +334,7 @@ And confirm that opening http://127.0.0.1:8000/ in a web browser displays, "Hell Finally, we'll update the Dockerfile to include the local library in the deployment package: ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.11.5 AS uv +FROM ghcr.io/astral-sh/uv:0.11.6 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder diff --git a/docs/guides/integration/docker.md b/docs/guides/integration/docker.md index fc56534871700..0de224d1c6b0a 100644 --- a/docs/guides/integration/docker.md +++ b/docs/guides/integration/docker.md @@ -31,7 +31,7 @@ $ docker run --rm -it ghcr.io/astral-sh/uv:debian uv --help The following distroless images are available: - `ghcr.io/astral-sh/uv:latest` -- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.11.5` +- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.11.6` - `ghcr.io/astral-sh/uv:{major}.{minor}`, e.g., `ghcr.io/astral-sh/uv:0.8` (the latest patch version) @@ -92,7 +92,7 @@ And the following derived images are available: As with the distroless image, each derived image is published with uv version tags as `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}-{base}` and -`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.11.5-alpine`. +`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.11.6-alpine`. In addition, starting with `0.8` each derived image also sets `UV_TOOL_BIN_DIR` to `/usr/local/bin` to allow `uv tool install` to work as expected with the default user. @@ -133,7 +133,7 @@ Note this requires `curl` to be available. In either case, it is best practice to pin to a specific uv version, e.g., with: ```dockerfile -COPY --from=ghcr.io/astral-sh/uv:0.11.5 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.11.6 /uv /uvx /bin/ ``` !!! tip @@ -151,7 +151,7 @@ COPY --from=ghcr.io/astral-sh/uv:0.11.5 /uv /uvx /bin/ Or, with the installer: ```dockerfile -ADD https://astral.sh/uv/0.11.5/install.sh /uv-installer.sh +ADD https://astral.sh/uv/0.11.6/install.sh /uv-installer.sh ``` ### Installing a project @@ -619,5 +619,5 @@ Verified OK !!! tip These examples use `latest`, but best practice is to verify the attestation for a specific - version tag, e.g., `ghcr.io/astral-sh/uv:0.11.5`, or (even better) the specific image digest, + version tag, e.g., `ghcr.io/astral-sh/uv:0.11.6`, or (even better) the specific image digest, such as `ghcr.io/astral-sh/uv:0.5.27@sha256:5adf09a5a526f380237408032a9308000d14d5947eafa687ad6c6a2476787b4f`. diff --git a/docs/guides/integration/github.md b/docs/guides/integration/github.md index 9a0bfbd77a6a6..6cf71b3ba2542 100644 --- a/docs/guides/integration/github.md +++ b/docs/guides/integration/github.md @@ -47,7 +47,7 @@ jobs: uses: astral-sh/setup-uv@v7 with: # Install a specific version of uv. - version: "0.11.5" + version: "0.11.6" ``` ## Setting up Python diff --git a/docs/guides/integration/gitlab.md b/docs/guides/integration/gitlab.md index 3e64e35dba78c..831ba480d59cd 100644 --- a/docs/guides/integration/gitlab.md +++ b/docs/guides/integration/gitlab.md @@ -13,7 +13,7 @@ Select a variant that is suitable for your workflow. ```yaml title=".gitlab-ci.yml" variables: - UV_VERSION: "0.11.5" + UV_VERSION: "0.11.6" PYTHON_VERSION: "3.12" BASE_LAYER: trixie-slim # GitLab CI creates a separate mountpoint for the build directory, diff --git a/docs/guides/integration/pre-commit.md b/docs/guides/integration/pre-commit.md index 0e0bf69ea92eb..cb82137788ba4 100644 --- a/docs/guides/integration/pre-commit.md +++ b/docs/guides/integration/pre-commit.md @@ -19,7 +19,7 @@ To make sure your `uv.lock` file is up to date even if your `pyproject.toml` fil repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.11.5 + rev: 0.11.6 hooks: - id: uv-lock ``` @@ -30,7 +30,7 @@ To keep a `requirements.txt` file in sync with your `uv.lock` file: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.11.5 + rev: 0.11.6 hooks: - id: uv-export ``` @@ -41,7 +41,7 @@ To compile requirements files: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.11.5 + rev: 0.11.6 hooks: # Compile requirements - id: pip-compile @@ -54,7 +54,7 @@ To compile alternative requirements files, modify `args` and `files`: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.11.5 + rev: 0.11.6 hooks: # Compile requirements - id: pip-compile @@ -68,7 +68,7 @@ To run the hook over multiple files at the same time, add additional entries: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.11.5 + rev: 0.11.6 hooks: # Compile requirements - id: pip-compile diff --git a/pyproject.toml b/pyproject.toml index c0e46df3759a9..191869f7236ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "uv" -version = "0.11.5" +version = "0.11.6-dev.0" description = "An extremely fast Python package and project manager, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" diff --git a/scripts/doppel-set-version.sh b/scripts/doppel-set-version.sh new file mode 100755 index 0000000000000..00d33303c2e12 --- /dev/null +++ b/scripts/doppel-set-version.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# +# Set the Doppel fork version across all crates. +# +# Usage: +# ./scripts/doppel-set-version.sh 0.12.0 # sets 0.12.0-dev.0 in Cargo, 0.12.0.dev0 in pyproject +# +# Maturin auto-converts SemVer "0.12.0-dev.0" → PEP 440 "0.12.0.dev0" so the +# same Cargo.toml version works for both the Rust binary and Python wheels. +# `uv --version` prints "uv 0.12.0-dev.0". +# +# What it updates: +# Cargo (SemVer: X.Y.Z-dev.0): +# - Cargo.toml (workspace uv-version dep) +# - crates/uv/Cargo.toml +# - crates/uv-build/Cargo.toml +# - crates/uv-version/Cargo.toml +# - Cargo.lock (via cargo update) +# +# Python (PEP 440: X.Y.Z.dev0) — auto-converted by maturin from the Cargo version: +# - pyproject.toml +# - crates/uv-build/pyproject.toml + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 " + echo " e.g. $0 0.12.0 → Cargo: 0.12.0-dev.0, Python: 0.12.0.dev0" + exit 1 +fi + +INPUT="$1" + +# Strip any existing suffix to get base version +BASE="${INPUT%-dev.0}" +BASE="${BASE%-doppel}" +BASE="${BASE%.dev0}" + +if ! [[ "$BASE" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "ERROR: '$BASE' is not a valid semver (expected X.Y.Z)" >&2 + exit 1 +fi + +# SemVer for Cargo — maturin converts this to PEP 440 automatically +CARGO_VERSION="${BASE}-dev.0" +# PEP 440 for pyproject.toml (maturin reads Cargo.toml but pyproject.toml +# also carries the version for tools that read it directly) +PEP440_VERSION="${BASE}.dev0" + +echo "Setting versions:" +echo " Cargo (SemVer): $CARGO_VERSION (uv --version)" +echo " Python (PEP 440): $PEP440_VERSION (pyproject.toml / wheels)" +echo "" + +_sed() { + if [[ "$(uname)" == "Darwin" ]]; then + sed -i '' "$@" + else + sed -i "$@" + fi +} + +# --- Cargo files (SemVer: X.Y.Z-dev.0) --- + +CRATE_FILES=( + "$REPO_ROOT/crates/uv/Cargo.toml" + "$REPO_ROOT/crates/uv-build/Cargo.toml" + "$REPO_ROOT/crates/uv-version/Cargo.toml" +) + +for file in "${CRATE_FILES[@]}"; do + if [[ ! -f "$file" ]]; then + echo "WARNING: $file not found, skipping" >&2 + continue + fi + _sed "s/^version = \"[0-9][^\"]*\"/version = \"${CARGO_VERSION}\"/" "$file" + echo " Updated: $file" +done + +_sed "s/uv-version = { version = \"[0-9][^\"]*\"/uv-version = { version = \"${CARGO_VERSION}\"/" "$REPO_ROOT/Cargo.toml" +echo " Updated: Cargo.toml (workspace dep)" + +# --- Python files (PEP 440: X.Y.Z.dev0) --- + +PYPROJECT_FILES=( + "$REPO_ROOT/pyproject.toml" + "$REPO_ROOT/crates/uv-build/pyproject.toml" +) + +for file in "${PYPROJECT_FILES[@]}"; do + if [[ ! -f "$file" ]]; then + echo "WARNING: $file not found, skipping" >&2 + continue + fi + _sed "s/^version = \"[0-9][^\"]*\"/version = \"${PEP440_VERSION}\"/" "$file" + echo " Updated: $file (PEP 440)" +done + +# --- Cargo.lock --- + +echo "" +echo "Updating Cargo.lock..." +(cd "$REPO_ROOT" && cargo update -p uv-version -p uv -p uv-build 2>&1 | grep -v "^$") + +echo "" +echo "Done. Verify with:" +echo " cargo check -p uv-version" +echo " grep '^version' crates/uv-version/Cargo.toml pyproject.toml" diff --git a/uv.lock b/uv.lock index 12d86872c7f6a..5a3cc8b701477 100644 --- a/uv.lock +++ b/uv.lock @@ -934,7 +934,7 @@ wheels = [ [[package]] name = "uv" -version = "0.11.5" +version = "0.11.6" source = { editable = "." } [package.dev-dependencies]