name: CI

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
  cancel-in-progress: true

env:
  CARGO_INCREMENTAL: 0
  CARGO_NET_RETRY: 10
  CARGO_TERM_COLOR: always
  RUSTUP_MAX_RETRIES: 10
  PYTHON_VERSION: "3.12"

jobs:
  determine_changes:
    name: "Determine changes"
    runs-on: ubuntu-latest
    outputs:
      # Flag that is raised when any code is changed
      code: ${{ steps.changed.outputs.code_any_changed }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: tj-actions/changed-files@v45
        id: changed
        with:
          files_yaml: |
            code:
              - "**/*"
              - "!docs/**/*"
              - "!mkdocs.*.yml"
              - "!**/*.md"
              - "!bin/**"
              - "!assets/**"
              # Generated markdown and JSON files are checked during test runs
              - "docs/reference/cli.md"
              - "docs/reference/settings.md"
              - "docs/configuration/environment.md"
              - "uv.schema.json"
  lint:
    timeout-minutes: 10
    name: "lint"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: 3.12

      - name: "Install Rustfmt"
        run: rustup component add rustfmt

      - name: "Install uv"
        uses: astral-sh/setup-uv@v5

      - name: "rustfmt"
        run: cargo fmt --all --check

      - name: "Prettier"
        run: |
          npx prettier --check "**/*.{json5,yaml,yml}"
          npx prettier --prose-wrap always --check "**/*.md"

      - name: "README check"
        run: python scripts/transform_readme.py --target pypi

      - name: "Python format"
        run: uvx ruff format --diff .

      - name: "Python lint"
        run: uvx ruff check .

      - name: "Python type check"
        run: uvx mypy

      - name: "Validate project metadata"
        run: uvx --from 'validate-pyproject[all,store]' validate-pyproject pyproject.toml

      - name: "Lint shell scripts"
        uses: ludeeus/action-shellcheck@2.0.0
        env:
          # renovate: datasource=github-tags depName=koalaman/shellcheck
          SHELLCHECK_VERSION: "v0.10.0"
          SHELLCHECK_OPTS: --shell bash
        with:
          version: ${{ env.SHELLCHECK_VERSION }}
          severity: style
          check_together: "yes"

  cargo-clippy:
    timeout-minutes: 10
    needs: determine_changes
    if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
    runs-on: ubuntu-latest
    name: "cargo clippy | ubuntu"
    steps:
      - uses: actions/checkout@v4
      - uses: Swatinem/rust-cache@v2
        with:
          save-if: ${{ github.ref == 'refs/heads/main' }}
      - name: "Install Rust toolchain"
        run: rustup component add clippy
      - name: "Clippy"
        run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings

  cargo-clippy-windows:
    timeout-minutes: 15
    needs: determine_changes
    if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
    runs-on: windows-latest
    name: "cargo clippy | windows"
    steps:
      - uses: actions/checkout@v4

      - 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@v2
        with:
          workspaces: ${{ env.UV_WORKSPACE }}

      - 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

  cargo-dev-generate-all:
    timeout-minutes: 10
    needs: determine_changes
    if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
    runs-on: ubuntu-latest
    name: "cargo dev generate-all"
    steps:
      - uses: actions/checkout@v4
      - uses: Swatinem/rust-cache@v2
        with:
          save-if: ${{ github.ref == 'refs/heads/main' }}
      - name: "Generate all"
        run: cargo dev generate-all --mode check

  cargo-shear:
    timeout-minutes: 10
    name: "cargo shear"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: "Install cargo shear"
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-shear
      - run: cargo shear

  # We use the large GitHub actions runners
  # For Ubuntu and Windows, this requires Organization-level configuration
  # See: https://docs.github.com/en/actions/using-github-hosted-runners/about-larger-runners/about-larger-runners#about-ubuntu-and-windows-larger-runners

  cargo-test-linux:
    timeout-minutes: 10
    needs: determine_changes
    if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
    runs-on: depot-ubuntu-22.04-16
    name: "cargo test | ubuntu"
    steps:
      - uses: actions/checkout@v4

      - uses: rui314/setup-mold@v1

      - uses: Swatinem/rust-cache@v2

      - name: "Install Rust toolchain"
        run: rustup show

      - uses: astral-sh/setup-uv@v5
      - name: "Install required Python versions"
        run: uv python install

      - name: "Install cargo nextest"
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-nextest

      - name: "Cargo test"
        run: |
          cargo nextest run \
            --features python-patch \
            --workspace \
            --status-level skip --failure-output immediate-final --no-fail-fast -j 20 --final-status-level slow

  cargo-test-macos:
    timeout-minutes: 10
    needs: determine_changes
    if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
    runs-on: macos-latest-xlarge # github-macos-14-aarch64-6
    name: "cargo test | macos"
    steps:
      - uses: actions/checkout@v4

      - uses: rui314/setup-mold@v1

      - uses: Swatinem/rust-cache@v2

      - name: "Install Rust toolchain"
        run: rustup show

      - uses: astral-sh/setup-uv@v5
      - name: "Install required Python versions"
        run: uv python install

      - name: "Install cargo nextest"
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-nextest

      - name: "Cargo test"
        run: |
          cargo nextest run \
            --no-default-features \
            --features python,python-managed,pypi,git,performance,crates-io \
            --workspace \
            --status-level skip --failure-output immediate-final --no-fail-fast -j 12 --final-status-level slow

  cargo-test-windows:
    timeout-minutes: 15
    needs: determine_changes
    if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
    runs-on: github-windows-2025-x86_64-16
    name: "cargo test | windows"
    steps:
      - uses: actions/checkout@v4

      - 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: astral-sh/setup-uv@v5
      - name: "Install required Python versions"
        run: uv python install

      - uses: Swatinem/rust-cache@v2
        with:
          workspaces: ${{ env.UV_WORKSPACE }}

      - name: "Install Rust toolchain"
        working-directory: ${{ env.UV_WORKSPACE }}
        run: rustup show

      - name: "Install cargo nextest"
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-nextest

      - name: "Cargo test"
        working-directory: ${{ env.UV_WORKSPACE }}
        env:
          # Avoid permission errors during concurrent tests
          # See https://github.com/astral-sh/uv/issues/6940
          UV_LINK_MODE: copy
        shell: bash
        run: |
          cargo nextest run \
            --no-default-features \
            --features python,pypi,python-managed \
            --workspace \
            --status-level skip --failure-output immediate-final --no-fail-fast -j 20 --final-status-level slow

  # Separate jobs for the nightly crate
  windows-trampoline-check:
    timeout-minutes: 15
    needs: determine_changes
    if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
    runs-on: windows-latest
    name: "check windows trampoline | ${{ matrix.target-arch }}"
    strategy:
      fail-fast: false
      matrix:
        target-arch: ["x86_64", "i686", "aarch64"]
    steps:
      - uses: actions/checkout@v4

      - 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@v2
        with:
          workspaces: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline

      - name: "Install Rust toolchain"
        working-directory: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline
        run: |
          rustup target add ${{ matrix.target-arch }}-pc-windows-msvc
          rustup component add rust-src --target ${{ matrix.target-arch }}-pc-windows-msvc

      - name: "Install cargo-bloat"
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-bloat

      - name: "Clippy"
        working-directory: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline
        run: cargo clippy --all-features --locked --target x86_64-pc-windows-msvc --tests -- -D warnings

      - name: "Bloat Check"
        working-directory: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline
        run: |
          $output = cargo bloat --release --target x86_64-pc-windows-msvc
          $filteredOutput = $output | Select-String -Pattern 'core::fmt::write|core::fmt::getcount' -NotMatch
          $containsPatterns = $filteredOutput | Select-String -Pattern 'core::fmt|std::panicking|std::backtrace_rs'

          if ($containsPatterns) {
              Exit 1
          } else {
              Exit 0
          }

  # Separate jobs for the nightly crate
  windows-trampoline-test:
    timeout-minutes: 10
    needs: determine_changes
    if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
    runs-on: windows-latest
    name: "test windows trampoline | ${{ matrix.target-arch }}"
    strategy:
      fail-fast: false
      matrix:
        # Note, we exclude `aarch64` because it's not supported by the GitHub runner
        target-arch: ["x86_64", "i686"]
    steps:
      - uses: actions/checkout@v4
      - uses: Swatinem/rust-cache@v2
        with:
          workspaces: ${{ github.workspace }}/crates/uv-trampoline
      - name: "Install Rust toolchain"
        working-directory: ${{ github.workspace }}/crates/uv-trampoline
        run: |
          rustup target add ${{ matrix.target-arch }}-pc-windows-msvc
          rustup component add rust-src --target ${{ matrix.target-arch }}-pc-windows-msvc
      - name: "Test committed binaries"
        working-directory: ${{ github.workspace }}
        run: |
          rustup target add ${{ matrix.target-arch }}-pc-windows-msvc
          cargo test -p uv-trampoline-builder --target ${{ matrix.target-arch }}-pc-windows-msvc
      # Build and copy the new binaries
      - name: "Build"
        working-directory: ${{ github.workspace }}/crates/uv-trampoline
        run: |
          cargo build --target ${{ matrix.target-arch }}-pc-windows-msvc
          cp target/${{ matrix.target-arch }}-pc-windows-msvc/debug/uv-trampoline-console.exe trampolines/uv-trampoline-${{ matrix.target-arch }}-console.exe
          cp target/${{ matrix.target-arch }}-pc-windows-msvc/debug/uv-trampoline-gui.exe trampolines/uv-trampoline-${{ matrix.target-arch }}-gui.exe
      - name: "Test new binaries"
        working-directory: ${{ github.workspace }}
        run: |
          # We turn off the default "production" test feature since these are debug binaries
          cargo test -p uv-trampoline-builder --target ${{ matrix.target-arch }}-pc-windows-msvc --no-default-features

  typos:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: crate-ci/typos@master

  docs:
    timeout-minutes: 10
    name: "mkdocs"
    runs-on: ubuntu-latest
    env:
      MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v5
      - uses: actions/setup-python@v5
      - name: "Add SSH key"
        if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}

      - name: "Build docs (public)"
        run: uvx --with-requirements docs/requirements.txt mkdocs build --strict -f mkdocs.public.yml

      - name: "Build docs (insiders)"
        if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
        run: uvx --with-requirements docs/requirements.txt mkdocs build --strict -f mkdocs.insiders.yml

  build-binary-linux-libc:
    timeout-minutes: 10
    needs: determine_changes
    if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
    runs-on: github-ubuntu-24.04-x86_64-8
    name: "build binary | linux libc"
    steps:
      - uses: actions/checkout@v4

      - uses: rui314/setup-mold@v1

      - uses: Swatinem/rust-cache@v2

      - name: "Build"
        run: cargo build

      - name: "Upload binary"
        uses: actions/upload-artifact@v4
        with:
          name: uv-linux-libc-${{ github.sha }}
          path: |
            ./target/debug/uv
            ./target/debug/uvx
          retention-days: 1

  build-binary-linux-musl:
    timeout-minutes: 10
    needs: determine_changes
    if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
    runs-on: github-ubuntu-24.04-x86_64-8
    name: "build binary | linux musl"
    steps:
      - uses: actions/checkout@v4

      - uses: rui314/setup-mold@v1

      - name: "Setup musl"
        run: |
          sudo apt-get install musl-tools
          rustup target add x86_64-unknown-linux-musl

      - uses: Swatinem/rust-cache@v2

      - name: "Build"
        run: cargo build --target x86_64-unknown-linux-musl --bin uv --bin uvx

      - name: "Upload binary"
        uses: actions/upload-artifact@v4
        with:
          name: uv-linux-musl-${{ github.sha }}
          path: |
            ./target/x86_64-unknown-linux-musl/debug/uv
            ./target/x86_64-unknown-linux-musl/debug/uvx
          retention-days: 1

  build-binary-macos-aarch64:
    timeout-minutes: 10
    needs: determine_changes
    if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
    runs-on: macos-14 # github-macos-14-aarch64-3
    name: "build binary | macos aarch64"
    steps:
      - uses: actions/checkout@v4

      - uses: rui314/setup-mold@v1

      - uses: Swatinem/rust-cache@v2
      - name: "Build"
        run: cargo build --bin uv --bin uvx

      - name: "Upload binary"
        uses: actions/upload-artifact@v4
        with:
          name: uv-macos-aarch64-${{ github.sha }}
          path: |
            ./target/debug/uv
            ./target/debug/uvx
          retention-days: 1

  build-binary-macos-x86_64:
    timeout-minutes: 10
    needs: determine_changes
    if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
    runs-on: macos-latest-large # github-macos-14-x86_64-12
    name: "build binary | macos x86_64"
    steps:
      - uses: actions/checkout@v4

      - uses: rui314/setup-mold@v1

      - uses: Swatinem/rust-cache@v2
      - name: "Build"
        run: cargo build --bin uv --bin uvx

      - name: "Upload binary"
        uses: actions/upload-artifact@v4
        with:
          name: uv-macos-x86_64-${{ github.sha }}
          path: |
            ./target/debug/uv
            ./target/debug/uvx
          retention-days: 1

  build-binary-windows-x86_64:
    needs: determine_changes
    timeout-minutes: 10
    if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
    runs-on: windows-latest
    name: "build binary | windows x86_64"
    steps:
      - uses: actions/checkout@v4

      - 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@v2
        with:
          workspaces: ${{ env.UV_WORKSPACE }}

      - name: "Build"
        working-directory: ${{ env.UV_WORKSPACE }}
        run: cargo build --bin uv --bin uvx

      - name: "Upload binary"
        uses: actions/upload-artifact@v4
        with:
          name: uv-windows-x86_64-${{ github.sha }}
          path: |
            ${{ env.UV_WORKSPACE }}/target/debug/uv.exe
            ${{ env.UV_WORKSPACE }}/target/debug/uvx.exe
          retention-days: 1

  build-binary-windows-aarch64:
    needs: determine_changes
    timeout-minutes: 25
    if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
    runs-on:
      labels: windows-latest
    name: "build binary | windows aarch64"
    steps:
      - uses: actions/checkout@v4

      - name: Create Dev Drive using ReFS
        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@v2
        with:
          workspaces: ${{ env.UV_WORKSPACE }}

      - name: "Install cross target"
        run: rustup target add aarch64-pc-windows-msvc

      - name: "Build"
        working-directory: ${{ env.UV_WORKSPACE }}
        run: cargo build --target aarch64-pc-windows-msvc

      - name: "Upload binary"
        uses: actions/upload-artifact@v4
        with:
          name: uv-windows-aarch64-${{ github.sha }}
          path: |
            ${{ env.UV_WORKSPACE }}/target/aarch64-pc-windows-msvc/debug/uv.exe
            ${{ env.UV_WORKSPACE }}/target/aarch64-pc-windows-msvc/debug/uvx.exe
          retention-days: 1

  cargo-build-msrv:
    name: "cargo build (msrv)"
    needs: determine_changes
    if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
    runs-on: github-ubuntu-24.04-x86_64-8
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
      - uses: SebRollen/toml-action@v1.2.0
        id: msrv
        with:
          file: "Cargo.toml"
          field: "workspace.package.rust-version"
      - name: "Install Rust toolchain"
        run: rustup default ${{ steps.msrv.outputs.value }}
      - name: "Install mold"
        uses: rui314/setup-mold@v1
      - uses: Swatinem/rust-cache@v2
      - run: cargo +${{ steps.msrv.outputs.value }} build
      - run: ./target/debug/uv --version

  build-binary-freebsd:
    needs: determine_changes
    timeout-minutes: 10
    if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
    runs-on: ubuntu-latest
    name: "build binary | freebsd"

    steps:
      - uses: actions/checkout@v4
      - uses: Swatinem/rust-cache@v2
      - name: "Cross build"
        run: |
          # Install cross from `freebsd-firecracker`
          wget -q -O cross https://github.com/acj/freebsd-firecracker/releases/download/v0.0.10/cross
          chmod +x cross
          mv cross /usr/local/bin/cross

          cross build --target x86_64-unknown-freebsd

      - name: Test in Firecracker VM
        uses: acj/freebsd-firecracker-action@v0.3.0
        with:
          verbose: false
          checkout: false
          pre-run: |
            # The exclude `*` prevents examination of directories so we need to
            # include each parent directory of the binary
            include_path="$(mktemp)"
            cat <<EOF > $include_path
            target
            target/x86_64-unknown-freebsd
            target/x86_64-unknown-freebsd/debug
            target/x86_64-unknown-freebsd/debug/uv
            EOF

            rsync -r -e "ssh" \
              --relative \
              --copy-links \
              --include-from "$include_path" \
              --exclude "*" \
              . firecracker:
          run-in-vm: |
            mv target/x86_64-unknown-freebsd/debug/uv uv
            chmod +x uv
            ./uv --version

  ecosystem-test:
    timeout-minutes: 10
    needs: build-binary-linux-libc
    name: "ecosystem test | ${{ matrix.repo }}"
    runs-on: ubuntu-latest
    strategy:
      matrix:
        include:
          - repo: "prefecthq/prefect"
            commands:
              - "uv venv"
              - "uv pip install -e '.[dev]'"
            python: "3.9"
          - repo: "pallets/flask"
            commands:
              - "uv venv"
              - "uv pip install -r requirements/dev.txt"
            python: "3.12"
          - repo: "pydantic/pydantic-core"
            commands:
              - "uv sync --group all"
              - "uv lock --upgrade"
            python: "3.12"
      fail-fast: false
    steps:
      - uses: actions/checkout@v4
        with:
          repository: ${{ matrix.repo }}

      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python }}

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-libc-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Test"
        run: |
          echo '${{ toJSON(matrix.commands) }}' | jq -r '.[]' | while read cmd; do
            echo "+ $cmd" >&2
            if [[ $cmd == uv* ]]; then
              ./$cmd
            else
              $cmd
            fi
          done

  smoke-test-linux:
    timeout-minutes: 10
    needs: build-binary-linux-libc
    name: "smoke test | linux"
    runs-on: ubuntu-latest
    steps:
      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-libc-${{ github.sha }}

      - name: "Prepare binary"
        run: |
          chmod +x ./uv
          chmod +x ./uvx

      - name: "Smoke test"
        run: |
          ./uv venv -v
          ./uv pip install ruff -v
          ./uvx -v ruff --version
          eval "$(./uv generate-shell-completion bash)"
          eval "$(./uvx --generate-shell-completion bash)"

  smoke-test-macos:
    timeout-minutes: 10
    needs: build-binary-macos-x86_64
    name: "smoke test | macos"
    runs-on: macos-latest
    steps:
      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-macos-x86_64-${{ github.sha }}

      - name: "Prepare binary"
        run: |
          chmod +x ./uv
          chmod +x ./uvx

      - name: "Smoke test"
        run: |
          ./uv venv -v
          ./uv pip install ruff -v
          ./uvx -v ruff --version
          eval "$(./uv generate-shell-completion bash)"
          eval "$(./uvx --generate-shell-completion bash)"

  smoke-test-windows-x86_64:
    timeout-minutes: 10
    needs: build-binary-windows-x86_64
    name: "smoke test | windows x86_64"
    runs-on: windows-latest
    steps:
      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-windows-x86_64-${{ github.sha }}

      - name: "Smoke test"
        working-directory: ${{ env.UV_WORKSPACE }}
        run: |
          ./uv venv -v
          ./uv pip install ruff -v
          ./uvx -v ruff --version
          (& ./uv generate-shell-completion powershell) | Out-String | Invoke-Expression
          (& ./uvx --generate-shell-completion powershell) | Out-String | Invoke-Expression

  smoke-test-windows-aarch64:
    timeout-minutes: 10
    needs: build-binary-windows-aarch64
    name: "smoke test | windows aarch64"
    runs-on: github-windows-11-aarch64-4
    steps:
      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-windows-aarch64-${{ github.sha }}

      - name: "Smoke test"
        working-directory: ${{ env.UV_WORKSPACE }}
        run: |
          $ErrorActionPreference = "Stop"
          $PSNativeCommandUseErrorActionPreference = $true

          ./uv venv -v
          ./uv pip install ruff -v
          ./uvx -v ruff --version
          (& ./uv generate-shell-completion powershell) | Out-String | Invoke-Expression
          (& ./uvx --generate-shell-completion powershell) | Out-String | Invoke-Expression

  integration-test-conda:
    timeout-minutes: 10
    needs: build-binary-linux-libc
    name: "integration test | conda on ubuntu"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: conda-incubator/setup-miniconda@v3
        with:
          miniconda-version: latest
          activate-environment: uv
          python-version: "3.12"

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-libc-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: Conda info
        shell: bash -el {0}
        run: conda info

      - name: "Install a package"
        shell: bash -el {0}
        run: |
          echo "$CONDA_PREFIX"
          ./uv pip install anyio

  integration-test-deadsnakes-39-linux:
    timeout-minutes: 5
    needs: build-binary-linux-libc
    name: "integration test | deadsnakes python3.9 on ubuntu"
    runs-on: ubuntu-latest
    steps:
      - name: "Install python3.9"
        run: |
          sudo add-apt-repository ppa:deadsnakes
          sudo apt-get update
          sudo apt-get install python3.9

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-libc-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Check missing distutils"
        run: |
          ./uv venv -p 3.9 --python-preference only-system -v 2>&1 | tee log.txt || true
          # We should report that distutils is missing
          grep 'Python installation is missing `distutils`' log.txt

      - name: "Install distutils"
        run: |
          sudo apt-get install python3.9-distutils

      - name: "Create a virtualenv"
        run: |
          ./uv venv -p 3.9 --python-preference only-system -v

      - name: "Check version"
        run: |
          .venv/bin/python --version

      - name: "Check install"
        run: |
          ./uv pip install -v anyio

  integration-test-free-threaded-linux:
    timeout-minutes: 5
    needs: build-binary-linux-libc
    name: "integration test | free-threaded on linux"
    runs-on: ubuntu-latest
    steps:
      - name: "Install python3.13-nogil"
        run: |
          sudo add-apt-repository ppa:deadsnakes
          sudo apt-get update
          sudo apt-get install python3.13-nogil

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-libc-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Create a virtual environment"
        run: |
          ./uv venv -p 3.13t --python-preference only-system

      - name: "Check version"
        run: |
          .venv/bin/python --version

      - name: "Check is free-threaded"
        run: |
          .venv/bin/python -c "import sys; exit(1) if sys._is_gil_enabled() else exit(0)"

      - name: "Check install"
        run: |
          ./uv pip install -v anyio

      - name: "Install free-threaded Python via uv"
        run: |
          ./uv python install -v 3.13t
          ./uv venv -p 3.13t --python-preference only-managed

      - name: "Check version"
        run: |
          .venv/bin/python --version

      - name: "Check is free-threaded"
        run: |
          .venv/bin/python -c "import sys; exit(1) if sys._is_gil_enabled() else exit(0)"

      - name: "Check install"
        run: |
          ./uv pip install -v anyio

  integration-test-free-threaded-windows-x86_64:
    timeout-minutes: 10
    needs: build-binary-windows-x86_64
    name: "integration test | free-threaded on windows"
    runs-on: windows-latest

    steps:
      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-windows-x86_64-${{ github.sha }}

      - name: "Install free-threaded Python via uv"
        run: |
          ./uv python install -v 3.13t

      - name: "Create a virtual environment (stdlib)"
        run: |
          & (./uv python find 3.13t) -m venv .venv

      - name: "Check version (stdlib)"
        run: |
          .venv/Scripts/python --version

      - name: "Create a virtual environment (uv)"
        run: |
          ./uv venv -p 3.13t --python-preference only-managed

      - name: "Check version (uv)"
        run: |
          .venv/Scripts/python --version

      - name: "Check is free-threaded"
        run: |
          .venv/Scripts/python -c "import sys; exit(1) if sys._is_gil_enabled() else exit(0)"

      - name: "Check install"
        run: |
          ./uv pip install -v anyio

      - name: "Check uv run"
        run: |
          ./uv run python -c ""
          ./uv run -p 3.13t python -c ""

  integration-test-pypy-linux:
    timeout-minutes: 10
    needs: build-binary-linux-libc
    name: "integration test | pypy on ubuntu"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-libc-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Install PyPy"
        run: ./uv python install -v pypy3.9

      - name: "Create a virtual environment"
        run: |
          ./uv venv -p pypy3.9 --python-preference only-managed

      - name: "Check for executables"
        run: |
          check_in_bin() {
              local executable_name=$1
              local bin_path=".venv/bin"

              if [[ -x "$bin_path/$executable_name" ]]; then
                  return 0
              else
                  echo "Executable '$executable_name' not found in folder '$bin_path'."
                  return 1
              fi
          }

          executables=("pypy" "pypy3" "python")

          all_found=true
          for executable_name in "${executables[@]}"; do
              check_in_bin "$executable_name" "$folder_path"
              result=$?

              if [[ $result -ne 0 ]]; then
                  all_found=false
              fi
          done

          if ! $all_found; then
            echo "One or more expected executables were not found."
            exit 1
          fi

      - name: "Check version"
        run: |
          .venv/bin/pypy --version
          .venv/bin/pypy3 --version
          .venv/bin/python --version

      - name: "Check install"
        run: |
          ./uv pip install anyio

  integration-test-pypy-windows-x86_64:
    timeout-minutes: 10
    needs: build-binary-windows-x86_64
    name: "integration test | pypy on windows"
    runs-on: windows-latest

    steps:
      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-windows-x86_64-${{ github.sha }}

      - name: "Install PyPy"
        run: .\uv.exe python install pypy3.9

      - name: "Create a virtual environment"
        run: |
          .\uv.exe venv -p pypy3.9 --python-preference only-managed

      - name: "Check for executables"
        shell: python
        run: |
          import sys
          from pathlib import Path

          def binary_exist(binary):
            binaries_path = Path(".venv\\Scripts")
            if (binaries_path / binary).exists():
              return True
            print(f"Executable '{binary}' not found in folder '{binaries_path}'.")

          all_found = True
          expected_binaries = [
              "pypy3.9.exe",
              "pypy3.9w.exe",
              "pypy3.exe",
              "pypyw.exe",
              "python.exe",
              "python3.9.exe",
              "python3.exe",
              "pythonw.exe",
          ]
          for binary in expected_binaries:
            if not binary_exist(binary):
              all_found = False

          if not all_found:
            print("One or more expected executables were not found.")
            sys.exit(1)

      - name: "Check version"
        run: |
          & .venv\Scripts\pypy3.9.exe --version
          & .venv\Scripts\pypy3.exe --version
          & .venv\Scripts\python.exe --version

      - name: "Check install"
        env:
          # Avoid debug build stack overflows.
          UV_STACK_SIZE: 3000000 # 3 megabyte, triple the default on windows
        run: |
          .\uv.exe pip install anyio

  integration-test-graalpy-linux:
    timeout-minutes: 10
    needs: build-binary-linux-libc
    name: "integration test | graalpy on ubuntu"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "graalpy24.1"

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-libc-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: Graalpy info
        run: |
          which graalpy

      - name: "Create a virtual environment"
        run: |
          ./uv venv -p $(which graalpy)

      - name: "Check for executables"
        run: |
          check_in_bin() {
              local executable_name=$1
              local bin_path=".venv/bin"

              if [[ -x "$bin_path/$executable_name" ]]; then
                  return 0
              else
                  echo "Executable '$executable_name' not found in folder '$bin_path'."
                  return 1
              fi
          }

          executables=("graalpy" "python3" "python")

          all_found=true
          for executable_name in "${executables[@]}"; do
              check_in_bin "$executable_name" "$folder_path"
              result=$?

              if [[ $result -ne 0 ]]; then
                  all_found=false
              fi
          done

          if ! $all_found; then
            echo "One or more expected executables were not found."
            exit 1
          fi

      - name: "Check version"
        run: |
          .venv/bin/graalpy --version
          .venv/bin/python3 --version
          .venv/bin/python --version

      - name: "Check install"
        run: |
          ./uv pip install anyio

  integration-test-graalpy-windows-x86_64:
    timeout-minutes: 10
    needs: build-binary-windows-x86_64
    name: "integration test | graalpy on windows"
    runs-on: windows-latest

    steps:
      - uses: timfel/setup-python@fc9bcb4a04f5b1ea7d678c2ca7ea1c479a2468d7
        with:
          python-version: "graalpy24.1"

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-windows-x86_64-${{ github.sha }}

      - name: Graalpy info
        run: Get-Command graalpy

      - name: "Create a virtual environment"
        run: |
          $graalpy = (Get-Command graalpy).source
          .\uv.exe venv -p $graalpy

      - name: "Check for executables"
        shell: python
        run: |
          import sys
          from pathlib import Path

          def binary_exist(binary):
            binaries_path = Path(".venv\\Scripts")
            if (binaries_path / binary).exists():
              return True
            print(f"Executable '{binary}' not found in folder '{binaries_path}'.")

          all_found = True
          expected_binaries = [
              "graalpy.exe",
              "python.exe",
              "python3.exe",
          ]
          for binary in expected_binaries:
            if not binary_exist(binary):
              all_found = False

          if not all_found:
            print("One or more expected executables were not found.")
            sys.exit(1)

      - name: "Check version"
        run: |
          & .venv\Scripts\graalpy.exe --version
          & .venv\Scripts\python3.exe --version
          & .venv\Scripts\python.exe --version

      - name: "Check install"
        env:
          # Avoid debug build stack overflows.
          UV_STACK_SIZE: 3000000 # 3 megabyte, triple the default on windows
        run: |
          .\uv.exe pip install anyio

  integration-test-github-actions:
    timeout-minutes: 10
    needs: build-binary-linux-libc
    name: "integration test | github actions"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12.7"

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-libc-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Install a package without system opt-in"
        run: |
          ./uv pip install anyio && exit 1 || echo "Failed as expected"

      - name: "Install a package with system opt-in"
        run: |
          ./uv pip install anyio --system

      - name: Configure uv to use the system Python by default
        run: echo "UV_SYSTEM_PYTHON=1" >> $GITHUB_ENV

      - name: "Install a package with system opt-in via the environment"
        run: |
          ./uv pip install anyio --reinstall

      - name: "Create a project"
        run: |
          # Use Python 3.11 as the minimum required version
          ./uv init --python 3.11
          ./uv add anyio

      - name: "Sync to the system Python"
        run: ./uv sync -v --python 3.12
        env:
          UV_PROJECT_ENVIRONMENT: "/opt/hostedtoolcache/Python/3.12.7/x64"

      - name: "Attempt to sync to the system Python with an incompatible version"
        run: |
          ./uv sync -v --python 3.11 && { echo "ci: Error; should not succeed"; exit 1; } || { echo "ci: Ok; expected failure"; exit 0; }
        env:
          UV_PROJECT_ENVIRONMENT: "/opt/hostedtoolcache/Python/3.12.7/x64"

      - name: "Attempt to sync to a non-Python environment directory"
        run: |
          mkdir -p /home/runner/example
          touch /home/runner/example/some-file
          ./uv sync -v && { echo "ci: Error; should not succeed"; exit 1; } || { echo "ci: Ok; expected failure"; exit 0; }
        env:
          UV_PROJECT_ENVIRONMENT: "/home/runner/example"

  integration-test-publish-changed:
    timeout-minutes: 10
    needs: build-binary-linux-libc
    name: "integration test | determine publish changes"
    runs-on: ubuntu-latest
    outputs:
      # Flag that is raised when any code is changed
      code: ${{ steps.changed.outputs.code_any_changed }}
    # Only the main repository is a trusted publisher
    if: github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test')
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      # Only publish a new release if the publishing code changed
      - uses: tj-actions/changed-files@v45
        id: changed
        with:
          files_yaml: |
            code:
              - "crates/uv-publish/**/*"
              - "scripts/publish/**/*"
              - ".github/workflows/ci.yml"

  integration-test-publish:
    timeout-minutes: 10
    needs: integration-test-publish-changed
    name: "integration test | uv publish"
    runs-on: ubuntu-latest
    if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event.pull_request.head.repo.fork != true && (needs.integration-test-publish-changed.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
    environment: uv-test-publish
    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@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-python@v5
        with:
          python-version: "${{ env.PYTHON_VERSION }}"

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-libc-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - 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: "Publish test packages"
        # `-p 3.12` prefers the python we just installed over the one locked in `.python_version`.
        run: ./uv run -p ${{ env.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_PYTHON_VERSION: ${{ env.PYTHON_VERSION }}

  cache-test-ubuntu:
    timeout-minutes: 10
    needs: build-binary-linux-libc
    name: "check cache | ubuntu"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-libc-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Download binary for last version"
        run: curl -LsSf "https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-unknown-linux-gnu.tar.gz" | tar -xvz

      - name: "Check cache compatibility"
        run: python scripts/check_cache_compat.py --uv-current ./uv --uv-previous ./uv-x86_64-unknown-linux-gnu/uv

  cache-test-macos-aarch64:
    timeout-minutes: 10
    needs: build-binary-macos-aarch64
    name: "check cache | macos aarch64"
    runs-on: macos-14 # github-macos-14-aarch64-3
    steps:
      - uses: actions/checkout@v4

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-macos-aarch64-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Download binary for last version"
        run: curl -LsSf "https://github.com/astral-sh/uv/releases/latest/download/uv-aarch64-apple-darwin.tar.gz" | tar -xvz

      - name: "Check cache compatibility"
        run: python scripts/check_cache_compat.py --uv-current ./uv --uv-previous ./uv-aarch64-apple-darwin/uv

  system-test-debian:
    timeout-minutes: 10
    needs: build-binary-linux-musl
    name: "check system | python on debian"
    runs-on: ubuntu-latest
    container: debian:bookworm
    steps:
      - uses: actions/checkout@v4

      - name: "Install Python"
        run: apt-get update && apt-get install -y python3.11 python3-pip python3.11-venv

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-musl-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3.11)

      - name: "Validate global Python install"
        run: python3.11 scripts/check_system_python.py --uv ./uv --externally-managed

  system-test-fedora:
    timeout-minutes: 10
    needs: build-binary-linux-libc
    name: "check system | python on fedora"
    runs-on: ubuntu-latest
    container: fedora:42
    steps:
      - uses: actions/checkout@v4

      - name: "Install Python"
        run: dnf install python3 which -y && python3 -m ensurepip

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-libc-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3)

      - name: "Validate global Python install"
        run: python3 scripts/check_system_python.py --uv ./uv

  system-test-ubuntu:
    timeout-minutes: 10
    needs: build-binary-linux-libc
    name: "check system | python on ubuntu"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-libc-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python)

      - name: "Validate global Python install"
        run: python scripts/check_system_python.py --uv ./uv

  system-test-opensuse:
    timeout-minutes: 5
    needs: build-binary-linux-libc
    name: "check system | python on opensuse"
    runs-on: ubuntu-latest
    container: opensuse/tumbleweed
    steps:
      - uses: actions/checkout@v4

      - name: "Install Python"
        run: >
          until
          zypper install -y python310 which && python3.10 -m ensurepip && mv /usr/bin/python3.10 /usr/bin/python3;
          do sleep 10;
          done

          # We retry because `zypper` can fail during remote repository updates
          # The above will not sleep forever due to the job level timeout

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-libc-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3)

      - name: "Validate global Python install"
        run: python3 scripts/check_system_python.py --uv ./uv

  # Note: rockylinux is a 1-1 code compatible distro to rhel
  # rockylinux mimics centos but with added maintenance stability
  # and avoids issues with centos stream uptime concerns
  system-test-rocky-linux:
    timeout-minutes: 10
    needs: build-binary-linux-musl
    name: "check system | python on rocky linux ${{ matrix.rocky-version }}"
    runs-on: ubuntu-latest
    container: rockylinux:${{ matrix.rocky-version }}
    strategy:
      fail-fast: false
      matrix:
        rocky-version: ["8", "9"]
    steps:
      - uses: actions/checkout@v4

      - name: "Install Python"
        if: matrix.rocky-version == '8'
        run: |
          dnf install python39 python39-pip which -y

      - name: "Install Python"
        if: matrix.rocky-version == '9'
        run: |
          dnf install python3.9 python3.9-pip which -y

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-musl-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3)

      - name: "Validate global Python install"
        run: python3 scripts/check_system_python.py --uv ./uv

  system-test-pypy:
    timeout-minutes: 10
    needs: build-binary-linux-libc
    name: "check system | pypy on ubuntu"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "pypy3.9"

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-libc-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which pypy)

      - name: "Validate global Python install"
        run: pypy scripts/check_system_python.py --uv ./uv

  system-test-pyston:
    timeout-minutes: 10
    needs: build-binary-linux-musl
    name: "check system | pyston"
    runs-on: ubuntu-latest
    container: pyston/pyston:2.3.5
    steps:
      - uses: actions/checkout@v4

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-musl-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which pyston)

      - name: "Validate global Python install"
        run: pyston scripts/check_system_python.py --uv ./uv

  system-test-alpine:
    timeout-minutes: 10
    needs: build-binary-linux-musl
    name: "check system | alpine"
    runs-on: ubuntu-latest
    container: alpine:latest
    steps:
      - uses: actions/checkout@v4

      - name: "Install Python"
        run: apk add --update --no-cache python3 py3-pip

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-musl-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3)

      - name: "Validate global Python install"
        run: python3 scripts/check_system_python.py --uv ./uv --externally-managed

  system-test-macos-aarch64:
    timeout-minutes: 10
    needs: build-binary-macos-aarch64
    name: "check system | python on macos aarch64"
    runs-on: macos-14 # github-macos-14-aarch64-3
    steps:
      - uses: actions/checkout@v4

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-macos-aarch64-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      # This should be the macOS system Python
      # We'd like to test with Homebrew but this Python takes precedence in system Python discovery
      - name: "Print Python path"
        run: echo $(which python3)

      - name: "Validate global Python install"
        run: python3 scripts/check_system_python.py --uv ./uv --externally-managed

  system-test-macos-aarch64-homebrew:
    timeout-minutes: 10
    needs: build-binary-macos-aarch64
    name: "check system | homebrew python on macos aarch64"
    runs-on: macos-14 # github-macos-14-aarch64-3
    steps:
      - uses: actions/checkout@v4

      - name: "Install Python"
        run: brew install python3

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-macos-aarch64-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3)

      - name: "Validate global Python install"
        run: python3 scripts/check_system_python.py --uv ./uv --externally-managed

  system-test-macos-x86_64:
    timeout-minutes: 10
    needs: build-binary-macos-x86_64
    name: "check system | python on macos x86-64"
    runs-on: macos-13 # github-macos-13-x86_64-4
    steps:
      - uses: actions/checkout@v4

      # We test with GitHub's Python as a regression test for
      # https://github.com/astral-sh/uv/issues/2450
      - uses: actions/setup-python@v5

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-macos-x86_64-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3)

      - name: "Validate global Python install"
        run: python3 scripts/check_system_python.py --uv ./uv

  system-test-windows-python-310:
    timeout-minutes: 10
    needs: build-binary-windows-x86_64
    name: "check system | python3.10 on windows x86-64"
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.10"

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-windows-x86_64-${{ github.sha }}

      - name: "Print Python path"
        run: echo $(which python)

      - name: "Validate global Python install"
        run: py -3.10 ./scripts/check_system_python.py --uv ./uv.exe

  system-test-windows-x86_64-python-310:
    timeout-minutes: 10
    needs: build-binary-windows-x86_64
    name: "check system | python3.10 on windows x86"
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.10"
          architecture: "x86"

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-windows-x86_64-${{ github.sha }}

      - name: "Print Python path"
        run: echo $(which python)

      - name: "Validate global Python install"
        run: python ./scripts/check_system_python.py --uv ./uv.exe

  system-test-windows-x86_64-python-313:
    timeout-minutes: 10
    needs: build-binary-windows-x86_64
    name: "check system | python3.13 on windows x86-64"
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.13"
          allow-prereleases: true
          cache: pip

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-windows-x86_64-${{ github.sha }}

      - name: "Print Python path"
        run: echo $(which python)

      - name: "Validate global Python install"
        run: py -3.13 ./scripts/check_system_python.py --uv ./uv.exe

  # Test our PEP 514 integration that installs Python into the Windows registry.
  system-test-windows-registry:
    timeout-minutes: 10
    needs: build-binary-windows-x86_64
    name: "check system | windows registry"
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-windows-x86_64-${{ github.sha }}

      # NB: Run this last, we are modifying the registry
      - name: "Test PEP 514 registration"
        run: python ./scripts/check_registry.py --uv ./uv.exe

  system-test-choco:
    timeout-minutes: 10
    needs: build-binary-windows-x86_64
    name: "check system | python3.12 via chocolatey"
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4

      - name: "Install Python"
        run: choco install python3 --verbose --version=3.9.13

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-windows-x86_64-${{ github.sha }}

      - name: "Print Python path"
        run: echo $(which python3)

      - name: "Validate global Python install"
        run: py -3.9 ./scripts/check_system_python.py --uv ./uv.exe

  system-test-pyenv:
    timeout-minutes: 10
    needs: build-binary-linux-libc
    name: "check system | python3.9 via pyenv"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: "Install pyenv"
        run: |
          # Install pyenv
          curl https://pyenv.run | bash

          # Set up environment variables for current step
          export PYENV_ROOT="$HOME/.pyenv"
          export PATH="$PYENV_ROOT/bin:$PATH"
          eval "$(pyenv init -)"

          # Install Python 3.9
          pyenv install 3.9
          pyenv global 3.9

          # Make environment variables persist across steps
          echo "PYENV_ROOT=$HOME/.pyenv" >> $GITHUB_ENV
          echo "$HOME/.pyenv/bin" >> $GITHUB_PATH
          echo "$HOME/.pyenv/shims" >> $GITHUB_PATH

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-libc-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3.9)

      - name: "Validate global Python install"
        run: python3.9 scripts/check_system_python.py --uv ./uv

  system-test-linux-313:
    timeout-minutes: 10
    needs: build-binary-linux-libc
    name: "check system | python3.13"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: 3.13
          allow-prereleases: true

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-libc-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3.13)

      - name: "Validate global Python install"
        run: python3.13 scripts/check_system_python.py --uv ./uv

  system-test-conda:
    timeout-minutes: 10
    needs:
      [
        build-binary-windows-x86_64,
        build-binary-macos-aarch64,
        build-binary-linux-libc,
      ]
    name: check system | conda${{ matrix.python-version }} on ${{ matrix.os }} ${{ matrix.arch }}
    runs-on: ${{ matrix.runner }}
    strategy:
      fail-fast: false
      matrix:
        os: ["linux", "windows", "macos"]
        python-version: ["3.8", "3.11"]
        include:
          - {
              os: "linux",
              target: "linux-libc",
              runner: "ubuntu-latest",
              arch: "x86-64",
            }
          - {
              os: "windows",
              target: "windows-x86_64",
              runner: "windows-latest",
              arch: "x86-64",
            }
          - {
              os: "macos",
              target: "macos-aarch64",
              runner: "macos-14",
              arch: "aarch64",
            }
    steps:
      - uses: actions/checkout@v4

      - uses: conda-incubator/setup-miniconda@v3
        with:
          miniconda-version: "latest"
          activate-environment: uv
          python-version: ${{ matrix.python-version }}

      - name: Conda info
        shell: bash -el {0}
        run: conda info

      - name: Conda list
        shell: pwsh
        run: conda list

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-${{ matrix.target }}-${{ github.sha }}

      - name: "Prepare binary"
        if: ${{ matrix.os != 'windows' }}
        run: chmod +x ./uv

      - name: "Print Python path"
        shell: bash -el {0}
        run: echo $(which python)

      - name: "Validate global Python install"
        shell: bash -el {0}
        run: python ./scripts/check_system_python.py --uv ./uv

  system-test-amazonlinux:
    timeout-minutes: 10
    needs: build-binary-linux-musl
    name: "check system | amazonlinux"
    runs-on: ubuntu-latest
    container: amazonlinux:2023
    steps:
      - name: "Install base requirements"
        run: |
          # Needed for `actions/checkout`
          yum install tar gzip which -y
      - uses: actions/checkout@v4
      - name: "Install Python"
        run: yum install python3 python3-pip -y

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-musl-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3)

      - name: "Validate global Python install"
        run: python3 scripts/check_system_python.py --uv ./uv

  system-test-windows-embedded-python-310:
    timeout-minutes: 10
    needs: build-binary-windows-x86_64
    name: "check system | embedded python3.10 on windows x86-64"
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-windows-x86_64-${{ github.sha }}

      # Download embedded Python.
      - name: "Download embedded Python"
        run: curl -LsSf https://www.python.org/ftp/python/3.11.8/python-3.11.8-embed-amd64.zip -o python-3.11.8-embed-amd64.zip

      - name: "Unzip embedded Python"
        run: 7z x python-3.11.8-embed-amd64.zip -oembedded-python

      - name: "Show embedded Python contents"
        run: ls embedded-python

      - name: "Set PATH"
        run: echo "${{ github.workspace }}\embedded-python" >> $env:GITHUB_PATH

      - name: "Print Python path"
        run: echo $(which python)

      - name: "Validate embedded Python install"
        run: python ./scripts/check_embedded_python.py --uv ./uv.exe

  benchmarks:
    runs-on: ubuntu-latest
    needs: determine_changes
    if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
    timeout-minutes: 20
    steps:
      - name: "Checkout Branch"
        uses: actions/checkout@v4

      - uses: Swatinem/rust-cache@v2

      - name: "Install Rust toolchain"
        run: rustup show

      - name: "Install codspeed"
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-codspeed

      - name: "Install requirements and prime cache"
        run: |
          sudo apt-get update
          sudo apt-get install -y libsasl2-dev libldap2-dev libkrb5-dev
          cargo run --bin uv -- venv --cache-dir .cache
          cargo run --bin uv -- pip compile scripts/requirements/jupyter.in --universal --exclude-newer 2024-08-08 --cache-dir .cache
          cargo run --bin uv -- pip compile scripts/requirements/airflow.in --universal --exclude-newer 2024-08-08 --cache-dir .cache

      - name: "Build benchmarks"
        run: cargo codspeed build --profile profiling --features "codspeed,performance" -p uv-bench

      - name: "Run benchmarks"
        uses: CodSpeedHQ/action@v3
        with:
          run: cargo codspeed run
          token: ${{ secrets.CODSPEED_TOKEN }}
