diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 00000000..a5c8a4cf --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,6 @@ +[test-groups] +rate-limited = { max-threads = 1 } + +[[profile.default.overrides]] +filter = 'test(rate_limited::)' +test-group = 'rate-limited' diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..377bc158 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[tests/snapshots/*] +trim_trailing_whitespace = false + +[*.{cff,yml}] +indent_size = 2 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..632f801f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [NobodyXu] diff --git a/.github/actions/just-setup/action.yml b/.github/actions/just-setup/action.yml new file mode 100644 index 00000000..f57a949f --- /dev/null +++ b/.github/actions/just-setup/action.yml @@ -0,0 +1,156 @@ +name: Setup tools and cache +inputs: + tools: + description: Extra tools + required: false + default: "" + indexcache: + description: Enable index cache + required: true + default: true + type: boolean + buildcache: + description: Enable build cache + required: true + default: true + type: boolean + +runs: + using: composite + steps: + - name: Enable macOS developer mode for better + if: runner.os == 'macOS' + run: sudo spctl developer-mode enable-terminal + shell: bash + + - name: Enable transparent huge page + if: runner.os == 'Linux' + run: echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/enabled + shell: bash + + - name: Configure jemalloc (used by rustc) to use transparent huge page + if: runner.os == 'Linux' + run: echo "MALLOC_CONF=thp:always,metadata_thp:always" >> "$GITHUB_ENV" + shell: bash + + - name: Exclude workspace and cargo/rustup home from windows defender + if: runner.os == 'Windows' + run: | + Add-MpPreference -ExclusionPath '${{ github.workspace }}' + shell: pwsh + + - name: Add just to tools to install + run: echo "tools=just" >>"$GITHUB_ENV" + shell: bash + + - name: Add inputs.tools to tools to install + if: inputs.tools != '' + env: + inputs_tools: ${{ inputs.tools }} + run: echo "tools=$tools,$inputs_tools" >>"$GITHUB_ENV" + shell: bash + + - name: Determine native target + run: | + if [ "$RUNNER_OS" = "Linux" ]; then RUNNER_TARGET=x86_64-unknown-linux-gnu; fi + if [ "$RUNNER_OS" = "macOS" ]; then RUNNER_TARGET=aarch64-apple-darwin; fi + if [ "$RUNNER_OS" = "Windows" ]; then RUNNER_TARGET=x86_64-pc-windows-msvc; fi + echo "RUNNER_TARGET=$RUNNER_TARGET" >>"$GITHUB_ENV" + shell: bash + + - name: Install tools + uses: taiki-e/install-action@v2 + with: + tool: ${{ env.tools }} + env: + CARGO_BUILD_TARGET: ${{ env.RUNNER_TARGET }} + + - name: Install rust toolchains + run: just toolchain + shell: bash + + - name: rustc version + run: rustc -vV + shell: bash + + - name: Retrieve RUSTFLAGS for caching + if: inputs.indexcache || inputs.buildcache + id: retrieve-rustflags + run: | + if [ -n "${{ inputs.buildcache }}" ]; then + echo RUSTFLAGS="$(just print-rustflags)" >> "$GITHUB_OUTPUT" + else + echo RUSTFLAGS= >> "$GITHUB_OUTPUT" + fi + shell: bash + + - run: just ci-install-deps + shell: bash + + - if: inputs.indexcache || inputs.buildcache + uses: Swatinem/rust-cache@v2 + with: + env-vars: "CARGO CC CFLAGS CXX CMAKE RUST JUST" + env: + RUSTFLAGS: ${{ steps.retrieve-rustflags.outputs.RUSTFLAGS }} + + - name: Find zig location and create symlink to it in ~/.local/bin + if: env.JUST_USE_CARGO_ZIGBUILD + run: | + python_package_path=$(python3 -m site --user-site) + ln -s "${python_package_path}/ziglang/zig" "$HOME/.local/bin/zig" + shell: bash + + - name: Calculate zig cache key + if: env.JUST_USE_CARGO_ZIGBUILD + run: | + ZIG_VERSION=$(zig version) + SYS_CRATE_HASHSUM=$(cargo tree --all-features --prefix none --no-dedupe --target "$CARGO_BUILD_TARGET" | grep -e '-sys' -e '^ring' | sort -u | sha1sum | sed 's/[ -]*//g') + PREFIX=v0-${JOB_ID}-zig-${ZIG_VERSION}-${CARGO_BUILD_TARGET}- + echo "ZIG_CACHE_KEY=${PREFIX}${SYS_CRATE_HASHSUM}" >> "$GITHUB_ENV" + echo -e "ZIG_CACHE_RESTORE_KEY=$PREFIX" >> "$GITHUB_ENV" + shell: bash + env: + RUSTFLAGS: ${{ steps.retrieve-rustflags.outputs.RUSTFLAGS }} + JOB_ID: ${{ github.job }} + + - name: Get zig global cache dir + if: env.JUST_USE_CARGO_ZIGBUILD + id: zig_global_cache_dir_path + run: | + cache_dir=$(zig env | jq -r '.global_cache_dir') + echo "cache_dir=$cache_dir" >> "$GITHUB_OUTPUT" + shell: bash + + - name: Cache zig compilation + if: env.JUST_USE_CARGO_ZIGBUILD + uses: actions/cache@v4 + with: + path: ${{ steps.zig_global_cache_dir_path.outputs.cache_dir }} + key: ${{ env.ZIG_CACHE_KEY }} + restore-keys: | + ${{ env.ZIG_CACHE_RESTORE_KEY }} + + - name: Cache make compiled + if: runner.os == 'macOS' + id: cache-make + uses: actions/cache@v4 + with: + path: /usr/local/bin/make + key: ${{ runner.os }}-make-4.4.1 + + - name: Build and use make 4.4.1 on macOS, since cc requires make >=4.3 + if: runner.os == 'macOS' && steps.cache-make.outputs.cache-hit != 'true' + run: | + curl "https://ftp.gnu.org/gnu/make/make-${MAKE_VERSION}.tar.gz" | tar xz + pushd "make-${MAKE_VERSION}" + ./configure + make -j 4 + popd + cp -p "make-${MAKE_VERSION}/make" /usr/local/bin + env: + MAKE_VERSION: 4.4.1 + shell: bash + + - run: make -v + shell: bash diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5f8b768d..8239740f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,8 +2,20 @@ version: 2 updates: - - package-ecosystem: "cargo" + - package-ecosystem: "github-actions" + # Workflow files stored in the + # default location of `.github/workflows` directory: "/" schedule: interval: "daily" - + - package-ecosystem: "cargo" + directory: "/" + schedule: + # Only run dependabot after all compatible upgrades and transitive deps + # are done to reduce amount of PRs opened. + interval: "weekly" + day: "saturday" + groups: + deps: + patterns: + - "*" diff --git a/.github/scripts/ephemeral-crate.sh b/.github/scripts/ephemeral-crate.sh new file mode 100755 index 00000000..3636ca71 --- /dev/null +++ b/.github/scripts/ephemeral-crate.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +cat >> crates/bin/Cargo.toml <> age.key <<< "$AGE_KEY_SECRET" + +set -x + +rage --decrypt --identity age.key --output minisign.key minisign.key.age + +ts=$(node -e 'console.log((new Date).toISOString())') +git=$(git rev-parse HEAD) +comment="gh=$GITHUB_REPOSITORY git=$git ts=$ts run=$GITHUB_RUN_ID" + +for file in "$@"; do + rsign sign -W -s minisign.key -x "$file.sig" -t "$comment" "$file" +done + +rm age.key minisign.key diff --git a/.github/scripts/release-pr-template.ejs b/.github/scripts/release-pr-template.ejs new file mode 100644 index 00000000..d94c008b --- /dev/null +++ b/.github/scripts/release-pr-template.ejs @@ -0,0 +1,41 @@ +<% if (pr.metaComment) { %> + +<% } %> + +This is a release PR for **<%= crate.name %>** version **<%= version.actual %>**<% + if (version.actual != version.desired) { +%> (performing a <%= version.desired %> bump).<% + } else { +%>.<% + } +%> + +**Use squash merge.** + +<% if (crate.name == "cargo-binstall") { %> +Upon merging, this will automatically create the tag `v<%= version.actual %>`, build the CLI, +create a GitHub release with the release notes below +<% } else { %> +Upon merging, this will create the tag `<%= crate.name %>-v<%= version.actual %>` +<% } %>, and CI will publish to crates.io on merge of this PR. + +**To trigger builds initially, close and then immediately re-open this PR once.** + +<% if (pr.releaseNotes) { %> +--- + +_Edit release notes into the section below:_ + + +### Release notes + +_Binstall is a tool to fetch and install Rust-based executables as binaries. It aims to be a drop-in replacement for `cargo install` in most cases. Install it today with `cargo install cargo-binstall`, from the binaries below, or if you already have it, upgrade with `cargo binstall cargo-binstall`._ + +#### In this release: + +- + +#### Other changes: + +- +<% } %> diff --git a/.github/scripts/test-detect-targets-musl.sh b/.github/scripts/test-detect-targets-musl.sh new file mode 100755 index 00000000..e8f404b1 --- /dev/null +++ b/.github/scripts/test-detect-targets-musl.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -exuo pipefail + +TARGET=${1?} + +[ "$(detect-targets)" = "$TARGET" ] + +apk update +apk add gcompat + +ls -lsha /lib + +GNU_TARGET=${TARGET//musl/gnu} + +[ "$(detect-targets)" = "$(printf '%s\n%s' "$GNU_TARGET" "$TARGET")" ] + +echo diff --git a/.github/workflows/cache-cleanup.yml b/.github/workflows/cache-cleanup.yml new file mode 100644 index 00000000..92f430b8 --- /dev/null +++ b/.github/workflows/cache-cleanup.yml @@ -0,0 +1,40 @@ +name: Cleanup caches for closed PRs + +on: + # Run twice every day to remove the cache so that the caches from the closed prs + # are removed. + schedule: + - cron: "0 17 * * *" + - cron: "30 18 * * *" + workflow_dispatch: + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Cleanup + run: | + set -euxo pipefail + + gh extension install actions/gh-actions-cache + + export REPO="${{ github.repository }}" + + # Setting this to not fail the workflow while deleting cache keys. + set +e + + # Remove pull requests cache, since they cannot be reused + gh pr list --state closed -L 20 --json number --jq '.[]|.number' | ( + while IFS='$\n' read -r closed_pr; do + BRANCH="refs/pull/${closed_pr}/merge" ./cleanup-cache.sh + done + ) + # Remove merge queue cache, since they cannot be reused + gh actions-cache list -L 100 | cut -f 3 | (grep 'gh-readonly-queue' || true) | sort -u | ( + while IFS='$\n' read -r branch; do + BRANCH="$branch" ./cleanup-cache.sh + done + ) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..d3c07336 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,415 @@ +name: CI + +on: + workflow_dispatch: + workflow_call: + inputs: + additional_key: + required: true + type: string + default: "" + merge_group: + pull_request: + types: + - opened + - reopened + - synchronize + push: + branches: + - main + paths: + - 'Cargo.lock' + - 'Cargo.toml' + - '**/Cargo.toml' + + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.event.pull_request.number || github.sha }}-${{ inputs.additional_key }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse + JUST_ENABLE_H3: true + CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 4 + CARGO_PROFILE_DEV_CODEGEN_UNITS: 4 + CARGO_PROFILE_CHECK_ONLY_CODEGEN_UNITS: 4 + +jobs: + changed-files: + runs-on: ubuntu-latest + name: Test changed-files + permissions: + pull-requests: read + + outputs: + crates_changed: ${{ steps.list-changed-files.outputs.crates_changed }} + has_detect_target_changed: ${{ steps.list-changed-files.outputs.has_detect_target_changed }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@4140eb99d2cced9bfd78375c2088371853262f79 + with: + dir_names: true + dir_names_exclude_current_dir: true + dir_names_max_depth: 2 + + - name: List all changed files + id: list-changed-files + env: + ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} + run: | + set -euxo pipefail + crates_changed="$(for file in $ALL_CHANGED_FILES; do echo $file; done | grep crates | cut -d / -f 2 | sed 's/^bin$/cargo-binstall/' || echo)" + has_detect_target_changed="$(echo "$crates_changed" | grep -q detect-targets && echo true || echo false)" + echo "crates_changed=${crates_changed//$'\n'/ }" | tee -a "$GITHUB_OUTPUT" + echo "has_detect_target_changed=$has_detect_target_changed" | tee -a "$GITHUB_OUTPUT" + + unit-tests: + needs: changed-files + runs-on: ubuntu-latest + env: + CARGO_BUILD_TARGET: x86_64-unknown-linux-gnu + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/just-setup + env: + # just-setup use binstall to install sccache, + # which works better when we provide it with GITHUB_TOKEN. + GITHUB_TOKEN: ${{ secrets.CI_RELEASE_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + tools: cargo-nextest + + - name: Decide crates to test + shell: bash + env: + CRATES_CHANGED: ${{ needs.changed-files.outputs.crates_changed }} + run: | + ARGS="" + for crate in $CRATES_CHANGED; do + ARGS="$ARGS -p $crate" + done + echo "CARGO_NEXTEST_ADDITIONAL_ARGS=$ARGS" | tee -a "$GITHUB_ENV" + + - run: just unit-tests + if: env.CARGO_NEXTEST_ADDITIONAL_ARGS != '' + env: + GITHUB_TOKEN: ${{ secrets.CI_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + CI_UNIT_TEST_GITHUB_TOKEN: ${{ secrets.CI_UNIT_TEST_GITHUB_TOKEN }} + + e2e-tests: + if: github.event_name != 'pull_request' + strategy: + fail-fast: false + matrix: + include: + - target: aarch64-apple-darwin + os: macos-latest + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: x86_64-pc-windows-msvc + os: windows-latest + + runs-on: ${{ matrix.os }} + env: + CARGO_BUILD_TARGET: ${{ matrix.target }} + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/just-setup + env: + # just-setup use binstall to install sccache, + # which works better when we provide it with GITHUB_TOKEN. + GITHUB_TOKEN: ${{ secrets.CI_RELEASE_TEST_GITHUB_TOKEN }} + + - run: just build + - run: just e2e-tests + env: + GITHUB_TOKEN: ${{ secrets.CI_TEST_GITHUB_TOKEN }} + + cross-check: + strategy: + fail-fast: false + matrix: + include: + - target: armv7-unknown-linux-musleabihf + os: ubuntu-latest + - target: armv7-unknown-linux-gnueabihf + os: ubuntu-latest + - target: aarch64-unknown-linux-musl + os: ubuntu-latest + - target: aarch64-unknown-linux-gnu + os: ubuntu-latest + - target: x86_64-unknown-linux-musl + os: ubuntu-latest + - target: x86_64-apple-darwin + os: macos-latest + - target: aarch64-pc-windows-msvc + os: windows-latest + runs-on: ${{ matrix.os }} + env: + CARGO_BUILD_TARGET: ${{ matrix.target }} + steps: + - uses: actions/checkout@v4 + + - name: Enable cargo-zigbuild + if: matrix.os == 'ubuntu-latest' + run: echo JUST_USE_CARGO_ZIGBUILD=true >> "$GITHUB_ENV" + + - uses: ./.github/actions/just-setup + with: + tools: cargo-hack@0.6.10 + env: + # just-setup use binstall to install sccache, + # which works better when we provide it with GITHUB_TOKEN. + GITHUB_TOKEN: ${{ secrets.CI_RELEASE_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + + - run: just avoid-dev-deps + - run: just check + + lint: + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-apple-darwin + os: macos-latest + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: x86_64-pc-windows-msvc + os: windows-latest + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/just-setup + env: + # just-setup use binstall to install sccache, + # which works better when we provide it with GITHUB_TOKEN. + GITHUB_TOKEN: ${{ secrets.CI_RELEASE_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + + - run: just toolchain rustfmt,clippy + - run: just avoid-dev-deps + - run: just lint + + pr-info: + outputs: + is-release: ${{ steps.meta.outputs.is-release }} + crate: ${{ steps.meta.outputs.crates-names }} + + runs-on: ubuntu-latest + steps: + - id: meta + if: github.event_name == 'pull_request' + uses: cargo-bins/release-meta@v1 + with: + event-data: ${{ toJSON(github.event) }} + extract-notes-under: "### Release notes" + + release-dry-run: + needs: pr-info + uses: ./.github/workflows/release-cli.yml + if: github.event_name != 'pull_request' + secrets: inherit + with: + info: | + { + "is-release": false, + "crate": "${{ needs.pr-info.outputs.crate }}", + "version": "0.0.0", + "notes": "" + } + CARGO_PROFILE_RELEASE_LTO: no + CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 4 + + detect-targets-build: + needs: changed-files + if: needs.changed-files.outputs.has_detect_target_changed == 'true' + runs-on: ubuntu-latest + env: + CARGO_BUILD_TARGET: x86_64-unknown-linux-musl + steps: + - uses: actions/checkout@v4 + - name: Install ${{ env.CARGO_BUILD_TARGET }} target + run: | + rustup target add $CARGO_BUILD_TARGET + pip3 install -r zigbuild-requirements.txt + - uses: Swatinem/rust-cache@v2 + with: + cache-all-crates: true + - name: Build detect-targets + run: | + cargo zigbuild --features cli-logging --target $CARGO_BUILD_TARGET + # Set working directory here, otherwise `cargo-zigbuild` would download + # and build quite a few unused dependencies. + working-directory: crates/detect-targets + - uses: actions/upload-artifact@v4 + with: + name: detect-targets + path: target/${{ env.CARGO_BUILD_TARGET }}/debug/detect-targets + + detect-targets-alpine-test: + runs-on: ubuntu-latest + needs: + - detect-targets-build + - changed-files + if: needs.changed-files.outputs.has_detect_target_changed == 'true' + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: detect-targets + - run: chmod +x detect-targets + + - name: Run test in alpine + run: | + docker run --rm \ + --mount src="$PWD/detect-targets",dst=/usr/local/bin/detect-targets,type=bind \ + --mount src="$PWD/.github/scripts/test-detect-targets-musl.sh",dst=/usr/local/bin/test.sh,type=bind \ + alpine /bin/ash -c "apk update && apk add bash && test.sh x86_64-unknown-linux-musl" + + detect-targets-ubuntu-arm-test: + runs-on: ubuntu-24.04-arm + steps: + - uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 + with: + cache-all-crates: true + - name: Build and run detect-targets tests + run: | + set -euxo pipefail + output="$(cargo run --features cli-logging --bin detect-targets)" + [ "$output" = "$(printf 'aarch64-unknown-linux-gnu\naarch64-unknown-linux-musl')" ] + # Set working directory here, otherwise `cargo` would download + # and build quite a few unused dependencies. + working-directory: crates/detect-targets + + detect-targets-ubuntu-x86_64-test: + needs: + - detect-targets-build + - changed-files + if: needs.changed-files.outputs.has_detect_target_changed == 'true' + strategy: + fail-fast: false + matrix: + os: + - ubuntu-22.04 + - ubuntu-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/download-artifact@v4 + with: + name: detect-targets + - run: chmod +x detect-targets + + - name: Run test in ubuntu + run: | + set -exuo pipefail + [ "$(./detect-targets)" = "$(printf 'x86_64-unknown-linux-gnu\nx86_64-unknown-linux-musl')" ] + + detect-targets-more-glibc-test: + needs: + - detect-targets-build + - changed-files + if: needs.changed-files.outputs.has_detect_target_changed == 'true' + strategy: + fail-fast: false + matrix: + container: + - archlinux + - fedora:37 + - fedora:38 + - fedora:39 + - fedora + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: detect-targets + - run: chmod +x detect-targets + + - name: Run test + run: | + set -exuo pipefail + [ "$(docker run --rm \ + --mount src="$PWD/detect-targets",dst=/usr/local/bin/detect-targets,type=bind \ + ${{ matrix.container }} detect-targets )" = "$(printf 'x86_64-unknown-linux-gnu\nx86_64-unknown-linux-musl')" ] + + detect-targets-nix-test: + needs: + - detect-targets-build + - changed-files + if: needs.changed-files.outputs.has_detect_target_changed == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: detect-targets + - run: chmod +x detect-targets + + - name: Run test + run: | + set -exuo pipefail + [ "$(docker run --rm \ + --mount src="$PWD/detect-targets",dst=/detect-targets,type=bind \ + nixos/nix /detect-targets )" = x86_64-unknown-linux-musl ] + + detect-targets-android-check: + needs: changed-files + if: needs.changed-files.outputs.has_detect_target_changed == 'true' + strategy: + fail-fast: false + matrix: + include: + - target: aarch64-linux-android + + runs-on: ubuntu-latest + env: + CARGO_BUILD_TARGET: ${{ matrix.target }} + + steps: + - uses: actions/checkout@v4 + + - name: Add ${{ matrix.target }} + run: rustup target add ${{ matrix.target }} + + - uses: Swatinem/rust-cache@v2 + with: + cache-all-crates: true + - name: Build detect-targets + run: | + cargo check --target ${{ matrix.target }} + # Set working directory here, otherwise `cargo-check` would download + # and build quite a few unused dependencies. + working-directory: crates/detect-targets + + # Dummy job to have a stable name for the "all tests pass" requirement + tests-pass: + name: Tests pass + needs: + - unit-tests + - e2e-tests + - cross-check + - lint + - release-dry-run + - detect-targets-build + - detect-targets-alpine-test + - detect-targets-ubuntu-arm-test + - detect-targets-ubuntu-x86_64-test + - detect-targets-more-glibc-test + - detect-targets-nix-test + - detect-targets-android-check + if: always() # always run even if dependencies fail + runs-on: ubuntu-latest + steps: + # fail if ANY dependency has failed or cancelled + - if: "contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')" + run: exit 1 + - run: exit 0 diff --git a/.github/workflows/gh-action.yml b/.github/workflows/gh-action.yml new file mode 100644 index 00000000..5d2280a2 --- /dev/null +++ b/.github/workflows/gh-action.yml @@ -0,0 +1,40 @@ +name: Test GitHub Action installer +on: + merge_group: + pull_request: + paths: + - install-from-binstall-release.ps1 + - install-from-binstall-release.sh + - action.yml + push: + branches: + - main + paths: + - install-from-binstall-release.ps1 + - install-from-binstall-release.sh + - action.yml + +jobs: + test-gha-installer: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + + - name: Install cargo-binstall + uses: ./ # uses action.yml from root of the repo + env: + GITHUB_TOKEN: ${{ secrets.CI_RELEASE_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Verify successful installation - display cargo-binstall's help + run: cargo binstall --help + + - name: Verify successful installation - install example binary using cargo-binstall + run: cargo binstall -y ripgrep + env: + GITHUB_TOKEN: ${{ secrets.CI_RELEASE_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Verify successful installation - display help of installed binary + run: rg --help diff --git a/.github/workflows/install-script.yml b/.github/workflows/install-script.yml new file mode 100644 index 00000000..b6ac7095 --- /dev/null +++ b/.github/workflows/install-script.yml @@ -0,0 +1,154 @@ +name: Test install-script + +on: + merge_group: + pull_request: + types: + - opened + - reopened + - synchronize + paths: + - install-from-binstall-release.ps1 + - install-from-binstall-release.sh + - .github/workflows/install-script.yml + push: + branches: + - main + paths: + - install-from-binstall-release.ps1 + - install-from-binstall-release.sh + - .github/workflows/install-script.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + +jobs: + unix: + strategy: + fail-fast: false + matrix: + os: [macos-latest, ubuntu-latest] + set_cargo_home: [t, f] + set_binstall_version: ['no', 'with-v', 'without-v'] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Set `CARGO_HOME` + if: matrix.set_cargo_home == 't' + run: | + CARGO_HOME="$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home')" + mkdir -p "${CARGO_HOME}/bin" + echo "CARGO_HOME=$CARGO_HOME" >> "$GITHUB_ENV" + + - name: Set `BINSTALL_VERSION` + if: matrix.set_binstall_version != 'no' + env: + STRIP_V: ${{ matrix.set_binstall_version }} + GH_TOKEN: ${{ github.token }} + run: | + # fetch most recent release tag. + BINSTALL_VERSION="$(gh release list --json name --jq '[.[] | select(.name | startswith("v")) | .name] | first')" + if [[ $STRIP_V == 'without-v' ]]; then BINSTALL_VERSION="${BINSTALL_VERSION#v*}"; fi + echo "Setting BINSTALL_VERSION=$BINSTALL_VERSION" + echo "BINSTALL_VERSION=$BINSTALL_VERSION" >> "$GITHUB_ENV" + + - name: Install `cargo-binstall` using scripts + run: ./install-from-binstall-release.sh + env: + GITHUB_TOKEN: ${{ secrets.CI_RELEASE_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Verify `cargo-binstall` installation + run: | + which cargo-binstall + cargo binstall -vV + + windows: + strategy: + fail-fast: false + matrix: + set_cargo_home: [t, f] + set_binstall_version: ['no', 'with-v', 'without-v'] + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set `CARGO_HOME` + if: matrix.set_cargo_home == 't' + shell: bash + run: | + CARGO_HOME="$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home')" + mkdir -p "${CARGO_HOME}/bin" + echo "CARGO_HOME=$CARGO_HOME" >> "$GITHUB_ENV" + + - name: Set `BINSTALL_VERSION` + if: matrix.set_binstall_version != 'no' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + STRIP_V: ${{ matrix.set_binstall_version }} + run: | + # fetch most recent release name. + BINSTALL_VERSION="$(gh release list --json name --jq '[.[] | select(.name | startswith("v")) | .name] | first')" + if [[ $STRIP_V == 'without-v' ]]; then BINSTALL_VERSION="${BINSTALL_VERSION#v*}"; fi + echo "Setting BINSTALL_VERSION=$BINSTALL_VERSION" + echo "BINSTALL_VERSION=$BINSTALL_VERSION" >> "$GITHUB_ENV" + + - name: Install `cargo-binstall` using scripts + run: ./install-from-binstall-release.ps1 + env: + GITHUB_TOKEN: ${{ secrets.CI_RELEASE_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Verify `cargo-binstall` installation + run: cargo binstall -vV + + windows-bash: + strategy: + fail-fast: false + matrix: + set_cargo_home: [t, f] + set_binstall_version: ['no', 'with-v', 'without-v'] + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set `CARGO_HOME` + if: matrix.set_cargo_home == 't' + shell: bash + run: | + CARGO_HOME="$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home')" + mkdir -p "${CARGO_HOME}/bin" + echo "CARGO_HOME=$CARGO_HOME" >> "$GITHUB_ENV" + + - name: Set `BINSTALL_VERSION` + if: matrix.set_binstall_version != 'no' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + STRIP_V: ${{ matrix.set_binstall_version }} + run: | + # fetch most recent release name. + BINSTALL_VERSION="$(gh release list --json name --jq '[.[] | select(.name | startswith("v")) | .name] | first')" + if [[ $STRIP_V == 'without-v' ]]; then BINSTALL_VERSION="${BINSTALL_VERSION#v*}"; fi + echo "Setting BINSTALL_VERSION=$BINSTALL_VERSION" + echo "BINSTALL_VERSION=$BINSTALL_VERSION" >> "$GITHUB_ENV" + + - name: Install `cargo-binstall` using scripts + shell: bash + run: ./install-from-binstall-release.sh + env: + GITHUB_TOKEN: ${{ secrets.CI_RELEASE_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Verify `cargo-binstall` installation + shell: bash + run: cargo binstall -vV diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml new file mode 100644 index 00000000..39be23c1 --- /dev/null +++ b/.github/workflows/release-cli.yml @@ -0,0 +1,135 @@ +name: Release CLI +on: + workflow_call: + inputs: + info: + description: "The release metadata JSON" + required: true + type: string + CARGO_PROFILE_RELEASE_LTO: + description: "Used to speed up CI" + required: false + type: string + CARGO_PROFILE_RELEASE_CODEGEN_UNITS: + description: "Used to speed up CI" + required: false + type: string + +jobs: + tag: + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - if: fromJSON(inputs.info).is-release == 'true' + name: Push cli release tag + uses: mathieudutour/github-tag-action@v6.2 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + custom_tag: ${{ fromJSON(inputs.info).version }} + tag_prefix: v + + keygen: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: cargo-bins/cargo-binstall@main + env: + GITHUB_TOKEN: ${{ secrets.CI_RELEASE_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + - name: Install binaries required + run: cargo binstall -y --force rsign2 rage + env: + GITHUB_TOKEN: ${{ secrets.CI_RELEASE_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Create ephemeral keypair + id: keypair + env: + AGE_KEY_PUBLIC: ${{ vars.AGE_KEY_PUBLIC }} + run: .github/scripts/ephemeral-gen.sh + - uses: actions/upload-artifact@v4 + with: + name: minisign.pub + path: minisign.pub + - uses: actions/upload-artifact@v4 + with: + name: minisign.key.age + path: minisign.key.age + retention-days: 1 + - name: Check that key can be decrypted + env: + AGE_KEY_SECRET: ${{ secrets.AGE_KEY_SECRET }} + shell: bash + run: .github/scripts/ephemeral-sign.sh minisign.pub + + package: + needs: + - tag + - keygen + uses: ./.github/workflows/release-packages.yml + secrets: inherit + with: + publish: ${{ inputs.info }} + CARGO_PROFILE_RELEASE_LTO: ${{ inputs.CARGO_PROFILE_RELEASE_LTO }} + CARGO_PROFILE_RELEASE_CODEGEN_UNITS: ${{ inputs.CARGO_PROFILE_RELEASE_CODEGEN_UNITS }} + + publish: + needs: package + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: minisign.pub + + - run: rustup toolchain install stable --no-self-update --profile minimal + + - run: .github/scripts/ephemeral-crate.sh + + - if: fromJSON(inputs.info).is-release != 'true' && fromJSON(inputs.info).crate != '' + name: DRY-RUN Publish to crates.io + env: + crate: ${{ fromJSON(inputs.info).crate }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish --dry-run -p "$crate" --allow-dirty --no-default-features + - if: fromJSON(inputs.info).is-release != 'true' && fromJSON(inputs.info).crate != '' + name: Upload crate package as artifact + uses: actions/upload-artifact@v4 + with: + name: crate-package + path: target/package/*.crate + + - if: fromJSON(inputs.info).is-release == 'true' + name: Publish to crates.io + env: + crate: ${{ fromJSON(inputs.info).crate }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish -p "$crate" --allow-dirty --no-default-features + + - if: fromJSON(inputs.info).is-release == 'true' + name: Upload minisign.pub + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + release_name: v${{ fromJSON(inputs.info).version }} + tag: v${{ fromJSON(inputs.info).version }} + body: ${{ fromJSON(inputs.info).notes }} + promote: true + file: minisign.pub + + - if: fromJSON(inputs.info).is-release == 'true' + name: Make release latest + run: gh release edit v${{ fromJSON(inputs.info).version }} --latest --draft=false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - if: fromJSON(inputs.info).is-release == 'true' + name: Delete signing key artifact + uses: geekyeggo/delete-artifact@v5 + with: + name: minisign.key.age + failOnError: false + diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml new file mode 100644 index 00000000..61fef550 --- /dev/null +++ b/.github/workflows/release-packages.yml @@ -0,0 +1,201 @@ +name: Build packages for release + +on: + workflow_call: + inputs: + publish: + description: "The release metadata JSON" + required: true + type: string + CARGO_PROFILE_RELEASE_LTO: + description: "Used to speed up CI" + required: false + type: string + CARGO_PROFILE_RELEASE_CODEGEN_UNITS: + description: "Used to speed up CI" + required: false + type: string + +env: + CARGO_TERM_COLOR: always + CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse + JUST_TIMINGS: true + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - { o: macos-latest, t: x86_64-apple-darwin } + - { o: macos-latest, t: x86_64h-apple-darwin } + - { o: macos-latest, t: aarch64-apple-darwin, r: true } + - { + o: ubuntu-latest, + t: x86_64-unknown-linux-gnu, + g: 2.17, + r: true, + c: true, + } + - { + o: ubuntu-latest, + t: armv7-unknown-linux-gnueabihf, + g: 2.17, + c: true, + } + - { o: ubuntu-latest, t: aarch64-unknown-linux-gnu, g: 2.17, c: true } + - { o: ubuntu-latest, t: x86_64-unknown-linux-musl, r: true, c: true } + - { o: ubuntu-latest, t: armv7-unknown-linux-musleabihf, c: true } + - { o: ubuntu-latest, t: aarch64-unknown-linux-musl, c: true } + - { o: windows-latest, t: x86_64-pc-windows-msvc, r: true } + - { o: windows-latest, t: aarch64-pc-windows-msvc } + + name: ${{ matrix.t }} + runs-on: ${{ matrix.o }} + permissions: + contents: write + env: + CARGO_BUILD_TARGET: ${{ matrix.t }} + GLIBC_VERSION: ${{ matrix.g }} + JUST_USE_CARGO_ZIGBUILD: ${{ matrix.c }} + JUST_FOR_RELEASE: true + JUST_ENABLE_H3: true + + steps: + - uses: actions/checkout@v4 + + - name: Override release profile lto settings + if: inputs.CARGO_PROFILE_RELEASE_LTO + run: echo "CARGO_PROFILE_RELEASE_LTO=${{ inputs.CARGO_PROFILE_RELEASE_LTO }}" >> "$GITHUB_ENV" + shell: bash + + - name: Override release profile codegen-units settings + if: inputs.CARGO_PROFILE_RELEASE_CODEGEN_UNITS + run: echo "CARGO_PROFILE_RELEASE_CODEGEN_UNITS=${{ inputs.CARGO_PROFILE_RELEASE_CODEGEN_UNITS }}" >> "$GITHUB_ENV" + shell: bash + + - uses: ./.github/actions/just-setup + with: + tools: rsign2,rage + env: + # just-setup use binstall to install sccache, + # which works better when we provide it with GITHUB_TOKEN. + GITHUB_TOKEN: ${{ secrets.CI_RELEASE_TEST_GITHUB_TOKEN }} + + - run: just toolchain rust-src + + - uses: actions/download-artifact@v4 + with: + name: minisign.pub + - run: just package + - if: runner.os == 'Windows' + run: Get-ChildItem packages/ + - if: runner.os != 'Windows' + run: ls -shal packages/ + + - name: Ensure release binary is runnable + if: "matrix.r" + run: just e2e-tests + env: + GITHUB_TOKEN: ${{ secrets.CI_RELEASE_TEST_GITHUB_TOKEN }} + + - uses: actions/download-artifact@v4 + with: + name: minisign.key.age + - name: Sign package + env: + AGE_KEY_SECRET: ${{ secrets.AGE_KEY_SECRET }} + shell: bash + run: .github/scripts/ephemeral-sign.sh packages/cargo-binstall-* + + - if: fromJSON(inputs.publish).is-release == 'true' + name: Upload to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + release_name: v${{ fromJSON(inputs.publish).version }} + tag: v${{ fromJSON(inputs.publish).version }} + body: ${{ fromJSON(inputs.publish).notes }} + file: packages/cargo-binstall-* + file_glob: true + prerelease: true + - if: "fromJSON(inputs.publish).is-release != 'true' || runner.os == 'macOS'" + name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.t }} + path: packages/cargo-binstall-* + retention-days: 1 + + - name: Upload timings + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.t }}-cargo-timings + path: target/cargo-timings + retention-days: 1 + + lipo: + needs: build + name: universal-apple-darwin + permissions: + contents: write + runs-on: macos-latest + env: + JUST_FOR_RELEASE: true + + steps: + - uses: actions/checkout@v4 + + - uses: taiki-e/install-action@v2 + with: + tool: just,rsign2,rage + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/download-artifact@v4 + with: + name: x86_64h-apple-darwin + path: packages/ + - uses: actions/download-artifact@v4 + with: + name: x86_64-apple-darwin + path: packages/ + - uses: actions/download-artifact@v4 + with: + name: aarch64-apple-darwin + path: packages/ + + - uses: actions/download-artifact@v4 + with: + name: minisign.pub + - run: ls -shalr packages/ + - run: just repackage-lipo + - run: ls -shal packages/ + + - uses: actions/download-artifact@v4 + with: + name: minisign.key.age + - env: + AGE_KEY_SECRET: ${{ secrets.AGE_KEY_SECRET }} + shell: bash + run: .github/scripts/ephemeral-sign.sh packages/cargo-binstall-universal-* + + - if: fromJSON(inputs.publish).is-release == 'true' + name: Upload to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: v${{ fromJSON(inputs.publish).version }} + release_name: v${{ fromJSON(inputs.publish).version }} + body: ${{ fromJSON(inputs.publish).notes }} + file: packages/cargo-binstall-universal-* + file_glob: true + overwrite: true + prerelease: true + - if: fromJSON(inputs.publish).is-release != 'true' + name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: universal-apple-darwin + path: packages/cargo-binstall-universal-* + retention-days: 1 diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml new file mode 100644 index 00000000..db2b1e0a --- /dev/null +++ b/.github/workflows/release-plz.yml @@ -0,0 +1,27 @@ +name: Release-plz + +permissions: + pull-requests: write + contents: write + +on: + push: + branches: + - main + +jobs: + release-plz: + name: Release-plz + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install Rust toolchain + run: rustup toolchain install stable --no-self-update --profile minimal + - name: Run release-plz + uses: MarcoIeni/release-plz-action@v0.5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml new file mode 100644 index 00000000..0a1943ef --- /dev/null +++ b/.github/workflows/release-pr.yml @@ -0,0 +1,47 @@ +name: Open cargo-binstall release PR +on: + workflow_dispatch: + inputs: + version: + description: Version to release + required: true + type: string + default: patch + +permissions: + pull-requests: write + +jobs: + make-release-pr: + permissions: + id-token: write # Enable OIDC + pull-requests: write + contents: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Configure toolchain + run: | + rustup toolchain install --profile minimal --no-self-update nightly + rustup default nightly + - uses: chainguard-dev/actions/setup-gitsign@main + - name: Install cargo-release + uses: taiki-e/install-action@v2 + with: + tool: cargo-release,cargo-semver-checks + env: + GITHUB_TOKEN: ${{ secrets.CI_RELEASE_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + + - run: rustup toolchain install stable --no-self-update --profile minimal + - uses: cargo-bins/release-pr@v2.1.3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + version: ${{ inputs.version }} + crate-path: crates/bin + pr-label: release + pr-release-notes: true + pr-template-file: .github/scripts/release-pr-template.ejs + check-semver: false + check-package: true + env: + RUSTFLAGS: --cfg reqwest_unstable diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..c8131abb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,54 @@ +name: On release +on: + pull_request: + types: closed + branches: [main] # target branch of release PRs + +jobs: + info: + if: github.event.pull_request.merged + + outputs: + is-release: ${{ steps.meta.outputs.is-release }} + crate: ${{ steps.meta.outputs.crates-names }} + version: ${{ steps.meta.outputs.version-actual }} + notes: ${{ steps.meta.outputs.notes }} + + runs-on: ubuntu-latest + steps: + - id: meta + uses: cargo-bins/release-meta@v1 + with: + event-data: ${{ toJSON(github.event) }} + extract-notes-under: '### Release notes' + + release-lib: + if: needs.info.outputs.is-release == 'true' && needs.info.outputs.crate != 'cargo-binstall' + needs: info + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: rustup toolchain install stable --no-self-update --profile minimal + - name: Push lib release tag + if: needs.info.outputs.crate != 'cargo-binstall' + uses: mathieudutour/github-tag-action@v6.2 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + custom_tag: ${{ needs.info.outputs.version }} + tag_prefix: ${{ needs.info.outputs.crate }}-v + - name: Publish to crates.io + run: | + cargo publish -p '${{ needs.info.outputs.crate }}' + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + release-cli: + if: needs.info.outputs.crate == 'cargo-binstall' + needs: info + uses: ./.github/workflows/release-cli.yml + secrets: inherit + with: + info: ${{ toJSON(needs.info.outputs) }} + diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index 98c1cf23..00000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,127 +0,0 @@ -name: Rust - -on: - push: - branches: [ main ] - tags: [ 'v*' ] - pull_request: - branches: [ main ] - -env: - CARGO_TERM_COLOR: always - -jobs: - build: - - runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.experimental }} - - strategy: - fail-fast: false - matrix: - include: - - target: x86_64-unknown-linux-gnu - os: ubuntu-latest - output: cargo-binstall - experimental: false - - target: x86_64-apple-darwin - os: macos-latest - output: cargo-binstall - experimental: false - - target: armv7-unknown-linux-gnueabihf - os: ubuntu-latest - output: cargo-binstall - experimental: true - - target: x86_64-pc-windows-msvc - os: windows-latest - output: cargo-binstall.exe - experimental: true - - steps: - - uses: actions/checkout@v2 - - uses: FranzDiebold/github-env-vars-action@v1.2.1 - - - name: Configure toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - target: ${{ matrix.target }} - override: true - - - name: Configure caching - uses: actions/cache@v2 - if: ${{ matrix.os != 'macos-latest' }} - # Caching disabled on macos due to https://github.com/actions/cache/issues/403 - with: - key: ${{ matrix.os }}-${{ matrix.target }} - path: | - ${{ env.HOME }}/.cargo" - target - - - name: Install openssl (apt armv7) - if: ${{ matrix.os == 'ubuntu-latest' && matrix.target == 'armv7-unknown-linux-gnueabihf' }} - run: | - sudo dpkg --add-architecture armhf - sudo apt update - sudo apt install openssl-dev:armhf - - - name: Install openssl (vcpkg) - if: ${{ matrix.os == 'windows-latest' }} - timeout-minutes: 30 - run: | - vcpkg integrate install - vcpkg install openssl:x64-windows-static - echo "OPENSSL_DIR=C:/vcpkg/installed/x64-windows-static/" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append - echo "OPENSSL_ROOT_DIR=C:/vcpkg/installed/x64-windows-static/" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append - echo "OPENSSL_STATIC=1" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append - - - name: Cache vcpkg - if: ${{ matrix.os == 'windows-latest' }} - uses: actions/cache@v2 - with: - key: ${{ matrix.os }}-${{ matrix.target }} - path: C:/vcpkg/installed - - - name: Build release - uses: actions-rs/cargo@v1 - with: - command: build - args: --target ${{ matrix.target }} --release - - - name: Copy / Rename utility - run: | - cp target/${{ matrix.target }}/release/${{ matrix.output }} ${{ matrix.output }} - tar -czvf cargo-binstall-${{ matrix.target }}.tgz ${{ matrix.output }} - - - name: Upload artifacts - uses: actions/upload-artifact@v1 - with: - name: cargo-binstall-${{ matrix.target }}.tgz - path: cargo-binstall-${{ matrix.target }}.tgz - - - name: Upload binary to release - if: ${{ startsWith(github.ref, 'refs/tags/v') }} - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: cargo-binstall-${{ matrix.target }}.tgz - asset_name: cargo-binstall-${{ matrix.target }}.tgz - tag: ${{ github.ref }} - overwrite: true - - release: - name: Upload firmware artifacts to release - runs-on: ubuntu-latest - if: ${{ startsWith(github.ref, 'refs/tags/v') }} - steps: - - - name: Create Release - uses: actions/create-release@v1 - id: create_release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - body: Release ${{ github.ref }} - diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml new file mode 100644 index 00000000..b7145e64 --- /dev/null +++ b/.github/workflows/shellcheck.yml @@ -0,0 +1,32 @@ +name: Shellcheck + +on: + merge_group: + pull_request: + types: + - opened + - reopened + - synchronize + paths: + - '**.sh' + push: + branches: + - main + paths: + - '**.sh' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +jobs: + shellcheck: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: taiki-e/install-action@v2 + with: + tool: fd-find + - name: shellcheck + run: fd -e sh -t f -X shellcheck diff --git a/.github/workflows/upgrade-transitive-deps.yml b/.github/workflows/upgrade-transitive-deps.yml new file mode 100644 index 00000000..8d9aad41 --- /dev/null +++ b/.github/workflows/upgrade-transitive-deps.yml @@ -0,0 +1,48 @@ +name: Upgrade transitive dependencies + +on: + workflow_dispatch: # Allow running on-demand + schedule: + - cron: "0 3 * * 5" + +jobs: + upgrade: + name: Upgrade & Open Pull Request + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: true + + - name: Generate branch name + run: | + git checkout -b deps/transitive/${{ github.run_id }} + + - name: Install rust + run: | + rustup toolchain install stable --no-self-update --profile minimal + + - name: Upgrade transitive dependencies + run: cargo update --aggressive + + - name: Detect changes + id: changes + run: + # This output boolean tells us if the dependencies have actually changed + echo "count=$(git status --porcelain=v1 | wc -l)" >> $GITHUB_OUTPUT + + - name: Commit and push changes + # Only push if changes exist + if: steps.changes.outputs.count > 0 + run: | + git config user.name github-actions + git config user.email github-actions@github.com + git commit -am "dep: Upgrade transitive dependencies" + git push origin HEAD + + - name: Open pull request if needed + if: steps.changes.outputs.count > 0 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr create --base main --label 'PR: dependencies' --title 'dep: Upgrade transitive dependencies' --body 'Update dependencies' --head $(git branch --show-current) diff --git a/.gitignore b/.gitignore index ea8c4bf7..b8aaf2b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ /target +.DS_Store +/packages +/e2e-tests/cargo-binstall* diff --git a/Cargo.lock b/Cargo.lock index f26c3e60..46ea5c00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,238 +1,790 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -[[package]] -name = "adler" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" +version = 3 [[package]] -name = "ansi_term" -version = "0.11.0" +name = "addr2line" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ - "winapi 0.3.9", + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.35" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c0df63cb2955042487fad3aefd2c6e3ae7389ac5dc1beb28921de0b69f779d4" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] -name = "arrayref" -version = "0.3.6" +name = "arc-swap" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "arrayvec" -version = "0.5.2" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] -name = "atty" -version = "0.2.14" +name = "async-compression" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "06575e6a9673580f52661c92107baabffbf41e2141373441cbcdc47cb733003c" dependencies = [ - "hermit-abi", - "libc", - "winapi 0.3.9", + "brotli", + "bzip2", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "xz2", + "zstd", + "zstd-safe", ] +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-file-install" +version = "1.0.11" +dependencies = [ + "reflink-copy", + "tempfile", + "tracing", + "windows 0.61.3", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" -version = "1.0.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] -name = "base64" -version = "0.13.0" +name = "backtrace" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" - -[[package]] -name = "bitflags" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" - -[[package]] -name = "blake2b_simd" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ - "arrayref", - "arrayvec", - "constant_time_eq", + "addr2line", + "cfg-if", + "libc", + "miniz_oxide 0.8.9", + "object", + "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] -name = "block-buffer" -version = "0.7.3" +name = "backtrace-ext" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[package]] +name = "base16" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" +dependencies = [ + "serde", +] + +[[package]] +name = "binstalk" +version = "0.28.36" +dependencies = [ + "binstalk-bins", + "binstalk-downloader", + "binstalk-fetchers", + "binstalk-git-repo-api", + "binstalk-registry", + "binstalk-types", + "cargo-toml-workspace", + "command-group", + "compact_str", + "detect-targets", + "either", + "itertools", + "jobslot", + "leon", + "maybe-owned", + "miette", + "semver", + "simple-git", + "strum", + "target-lexicon", + "tempfile", + "thiserror 2.0.12", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "binstalk-bins" +version = "0.6.14" +dependencies = [ + "atomic-file-install", + "binstalk-types", + "compact_str", + "leon", + "miette", + "normalize-path", + "thiserror 2.0.12", + "tracing", +] + +[[package]] +name = "binstalk-downloader" +version = "0.13.20" +dependencies = [ + "async-compression", + "async-trait", + "binstalk-types", + "binstall-tar", + "bytes", + "bzip2", + "cfg-if", + "compact_str", + "default-net", + "flate2", + "futures-io", + "futures-util", + "hickory-resolver", + "httpdate", + "ipconfig", + "native-tls", + "once_cell", + "rc-zip-sync", + "reqwest", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.12", + "tokio", + "tokio-tar", + "tokio-util", + "tracing", + "url", + "xz2", + "zstd", +] + +[[package]] +name = "binstalk-fetchers" +version = "0.10.21" +dependencies = [ + "async-trait", + "binstalk-downloader", + "binstalk-git-repo-api", + "binstalk-types", + "bytes", + "compact_str", + "either", + "itertools", + "leon", + "leon-macros", + "miette", + "minisign-verify", + "once_cell", + "strum", + "thiserror 2.0.12", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "binstalk-git-repo-api" +version = "0.5.22" +dependencies = [ + "binstalk-downloader", + "compact_str", + "once_cell", + "percent-encoding", + "serde", + "serde-tuple-vec-map", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tracing", + "tracing-subscriber", + "url", + "zeroize", +] + +[[package]] +name = "binstalk-manifests" +version = "0.16.1" +dependencies = [ + "beef", + "binstalk-types", + "compact_str", + "detect-targets", + "fs-lock", + "home", + "miette", + "semver", + "serde", + "serde-tuple-vec-map", + "serde_json", + "tempfile", + "thiserror 2.0.12", + "toml_edit", + "url", +] + +[[package]] +name = "binstalk-registry" +version = "0.11.21" +dependencies = [ + "async-trait", + "base16", + "binstalk-downloader", + "binstalk-types", + "cargo-toml-workspace", + "compact_str", + "leon", + "miette", + "normalize-path", + "once_cell", + "semver", + "serde", + "serde_json", + "sha2", + "simple-git", + "tempfile", + "thiserror 2.0.12", + "tokio", + "toml_edit", + "tracing", + "url", +] + +[[package]] +name = "binstalk-types" +version = "0.10.0" +dependencies = [ + "compact_str", + "maybe-owned", + "once_cell", + "semver", + "serde", + "serde_json", + "strum", + "strum_macros", + "url", +] + +[[package]] +name = "binstall-tar" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3620d72763b5d8df3384f3b2ec47dc5885441c2abbd94dd32197167d08b014a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "block-padding", - "byte-tools", - "byteorder", "generic-array", ] [[package]] -name = "block-padding" -version = "0.1.5" +name = "brotli" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" dependencies = [ - "byte-tools", + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "regex-automata 0.4.9", + "serde", ] [[package]] name = "bumpalo" -version = "3.4.0" +version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" - -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" [[package]] name = "byteorder" -version = "1.3.4" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "0.5.6" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "bytesize" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" + +[[package]] +name = "bytesize" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba" + +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", + "libbz2-rs-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "camino" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" +dependencies = [ + "serde", +] [[package]] name = "cargo-binstall" -version = "0.1.0" +version = "1.13.0" dependencies = [ - "anyhow", - "cargo_metadata", - "cargo_toml", - "crates_io_api", + "atomic-file-install", + "binstalk", + "binstalk-manifests", + "clap", + "clap-cargo", + "compact_str", "dirs", - "flate2", + "embed-resource", + "file-format", + "home", "log", - "reqwest", - "serde", - "serde_derive", - "simplelog", - "structopt", + "miette", + "mimalloc", + "once_cell", + "semver", "strum", "strum_macros", - "tar", - "tempdir", - "tinytemplate", + "supports-color", + "tempfile", "tokio", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "vergen", + "zeroize", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-toml-workspace" +version = "7.0.6" +dependencies = [ + "cargo_toml", + "compact_str", + "glob", + "normalize-path", + "serde", + "tempfile", + "thiserror 2.0.12", + "tracing", ] [[package]] name = "cargo_metadata" -version = "0.12.1" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f95cf4bf0dda0ac2e65371ae7215d0dce3c187613a9dbf23aaa9374186f97a" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" dependencies = [ + "camino", + "cargo-platform", "semver", - "semver-parser", "serde", "serde_json", + "thiserror 1.0.69", ] [[package]] name = "cargo_toml" -version = "0.8.1" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513d17226888c7b8283ac02a1c1b0d8a9d4cbf6db65dfadb79f598f5d7966fe9" +checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257" dependencies = [ "serde", - "serde_derive", "toml", ] +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" -version = "1.0.66" +version = "1.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" +dependencies = [ + "jobserver", + "libc", + "shlex", +] [[package]] name = "cfg-if" -version = "0.1.10" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] -name = "cfg-if" -version = "1.0.0" +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chardetng" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea" +dependencies = [ + "cfg-if", + "encoding_rs", + "memchr", +] [[package]] name = "chrono" -version = "0.4.19" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ - "libc", - "num-integer", "num-traits", - "serde", - "time", - "winapi 0.3.9", ] [[package]] name = "clap" -version = "2.33.3" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" dependencies = [ - "ansi_term", - "atty", - "bitflags", + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap-cargo" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d546f0e84ff2bfa4da1ce9b54be42285767ba39c688572ca32412a09a73851e5" +dependencies = [ + "anstyle", + "clap", +] + +[[package]] +name = "clap_builder" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", "strsim", - "textwrap", - "unicode-width", - "vec_map", + "terminal_size", ] [[package]] -name = "console_error_panic_hook" -version = "0.1.6" +name = "clap_derive" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" dependencies = [ - "cfg-if 0.1.10", - "wasm-bindgen", + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "constant_time_eq" -version = "0.1.5" +name = "clap_lex" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "clru" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "command-group" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68fa787550392a9d58f44c21a3022cfb3ea3e2458b7f85d3b399d0ceeccf409" +dependencies = [ + "async-trait", + "nix", + "tokio", + "winapi", +] + +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "serde", + "static_assertions", +] [[package]] name = "core-foundation" -version = "0.9.1" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -240,113 +792,341 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.2" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "crates_io_api" -version = "0.6.1" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fc34993d70c88226800d0f23bb51c96c7dfb285e6a1f96134b7a4d8b81d5a4" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ - "chrono", - "futures", - "log", - "reqwest", - "serde", - "serde_derive", - "serde_json", - "tokio", - "url", + "libc", ] [[package]] -name = "crc32fast" -version = "1.2.1" +name = "crc" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" dependencies = [ - "cfg-if 1.0.0", + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.1" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "autocfg", - "cfg-if 1.0.0", - "lazy_static", + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "default-net" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c5a6569a908354d49b10db3c516d69aca1eccd97562fd31c98b13f00b73ca66" +dependencies = [ + "dlopen2", + "libc", + "memalloc", + "netlink-packet-core", + "netlink-packet-route", + "netlink-sys", + "once_cell", + "system-configuration 0.5.1", + "windows 0.48.0", +] + +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_destructure2" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64b697ac90ff296f0fc031ee5a61c7ac31fb9fff50e3fb32873b09223613fc0c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "detect-targets" +version = "0.1.52" +dependencies = [ + "cfg-if", + "guess_host_triple", + "tokio", + "tracing", + "tracing-subscriber", + "windows-sys 0.60.2", +] + +[[package]] +name = "detect-wasi" +version = "1.0.31" +dependencies = [ + "tempfile", ] [[package]] name = "digest" -version = "0.8.1" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "generic-array", + "block-buffer", + "crypto-common", ] [[package]] name = "dirs" -version = "3.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.3.5" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", + "option-ext", "redox_users", - "winapi 0.3.9", + "windows-sys 0.60.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embed-resource" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8fe7d068ca6b3a5782ca5ec9afc244acd99dd441e4686a83b1c3973aba1d489" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml", + "vswhom", + "winreg 0.55.0", ] [[package]] name = "encoding_rs" -version = "0.8.26" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "801bbab217d7f79c0062f4f7205b5d4427c6d1a7bd7aafdd1475f7c59d62b283" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] -name = "fake-simd" +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "errno-dragonfly" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "faster-hex" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" +dependencies = [ + "serde", +] + +[[package]] +name = "faster-hex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless", + "serde", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "file-format" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ff8badf6e72ff15e42c9ade15d01375837173b17d10c228ab41d821082619db" [[package]] name = "filetime" -version = "0.2.13" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c122a393ea57648015bf06fbd3d372378992e86b9ff5a7a497b076a28c79efe" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", - "redox_syscall", - "winapi 0.3.9", + "libredox", + "windows-sys 0.59.0", ] [[package]] name = "flate2" -version = "1.0.19" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7411863d55df97a419aa64cb4d2f167103ea9d767e2c54a1868b7ac3f6b47129" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ - "cfg-if 1.0.0", "crc32fast", - "libc", - "miniz_oxide", + "libz-ng-sys", + "libz-rs-sys", + "miniz_oxide 0.8.9", ] [[package]] @@ -372,41 +1152,36 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.0.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ - "matches", "percent-encoding", ] [[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - -[[package]] -name = "fuchsia-zircon" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +name = "fs-lock" +version = "0.1.10" dependencies = [ - "bitflags", - "fuchsia-zircon-sys", + "fs4", + "tracing", ] [[package]] -name = "fuchsia-zircon-sys" -version = "0.3.3" +name = "fs4" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" +dependencies = [ + "rustix 1.0.7", + "windows-sys 0.59.0", +] [[package]] name = "futures" -version = "0.3.8" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b3b0c040a1fe6529d30b3c5944b280c7f0dcb2930d2c3062bca967b602583d0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -419,9 +1194,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.8" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b7109687aa4e177ef6fe84553af6280ef2778bdb7783ba44c9dc3399110fe64" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -429,15 +1204,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.8" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "847ce131b72ffb13b6109a221da9ad97a64cbe48feb1028356b836b47b8f1748" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.8" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4caa2b2b68b880003057c1dd49f1ed937e38f22fcf6c212188a121f08cf40a65" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -446,17 +1221,16 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.8" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611834ce18aaa1bd13c4b374f5d653e1027cf99b6b502584ff8c9a64413b30bb" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.8" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77408a692f1f97bcc61dc001d752e00643408fbc922e4d634c655df50d595556" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ - "proc-macro-hack", "proc-macro2", "quote", "syn", @@ -464,24 +1238,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.8" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f878195a49cee50e006b02b93cf7e0a95a38ac7b776b4c4d9cc1207cd20fcb3d" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.8" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c554eb5bf48b2426c4771ab68c6b14468b6e76cc90996f528c3338d761a4d0d" -dependencies = [ - "once_cell", -] +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.8" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d304cff4a7b99cfb7986f7d43fbe93d175e72e704a8860787cc95e9ffd85cbd2" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -490,82 +1261,1112 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project 1.0.2", + "pin-project-lite", "pin-utils", - "proc-macro-hack", - "proc-macro-nested", "slab", ] [[package]] -name = "generic-array" -version = "0.12.3" +name = "generator" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" +checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows 0.61.3", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", + "version_check", ] [[package]] name = "getrandom" -version = "0.1.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", + "js-sys", "libc", - "wasi 0.9.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gix" +version = "0.71.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a61e71ec6817fc3c9f12f812682cfe51ee6ea0d2e27e02fc3849c35524617435" +dependencies = [ + "gix-actor", + "gix-attributes", + "gix-command", + "gix-commitgraph", + "gix-config", + "gix-credentials", + "gix-date", + "gix-diff", + "gix-discover", + "gix-features 0.41.1", + "gix-filter", + "gix-fs 0.14.0", + "gix-glob", + "gix-hash 0.17.0", + "gix-hashtable", + "gix-ignore", + "gix-index", + "gix-lock", + "gix-negotiate", + "gix-object", + "gix-odb", + "gix-pack", + "gix-path", + "gix-pathspec", + "gix-prompt", + "gix-protocol", + "gix-ref", + "gix-refspec", + "gix-revision", + "gix-revwalk", + "gix-sec", + "gix-shallow", + "gix-submodule", + "gix-tempfile", + "gix-trace", + "gix-transport", + "gix-traverse", + "gix-url", + "gix-utils 0.2.0", + "gix-validate 0.9.4", + "gix-worktree", + "gix-worktree-state", + "once_cell", + "smallvec", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-actor" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f438c87d4028aca4b82f82ba8d8ab1569823cfb3e5bc5fa8456a71678b2a20e7" +dependencies = [ + "bstr", + "gix-date", + "gix-utils 0.2.0", + "itoa", + "thiserror 2.0.12", + "winnow 0.7.11", +] + +[[package]] +name = "gix-attributes" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e25825e0430aa11096f8b65ced6780d4a96a133f81904edceebb5344c8dd7f" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-quote", + "gix-trace", + "kstring", + "smallvec", + "thiserror 2.0.12", + "unicode-bom", +] + +[[package]] +name = "gix-bitmap" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1db9765c69502650da68f0804e3dc2b5f8ccc6a2d104ca6c85bc40700d37540" +dependencies = [ + "thiserror 2.0.12", +] + +[[package]] +name = "gix-chunk" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1f1d8764958699dc764e3f727cef280ff4d1bd92c107bbf8acd85b30c1bd6f" +dependencies = [ + "thiserror 2.0.12", +] + +[[package]] +name = "gix-command" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0378995847773a697f8e157fe2963ecf3462fe64be05b7b3da000b3b472def8" +dependencies = [ + "bstr", + "gix-path", + "gix-quote", + "gix-trace", + "shell-words", +] + +[[package]] +name = "gix-commitgraph" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043cbe49b7a7505150db975f3cb7c15833335ac1e26781f615454d9d640a28fe" +dependencies = [ + "bstr", + "gix-chunk", + "gix-hash 0.17.0", + "memmap2", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-config" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6f830bf746604940261b49abf7f655d2c19cadc9f4142ae9379e3a316e8cfa" +dependencies = [ + "bstr", + "gix-config-value", + "gix-features 0.41.1", + "gix-glob", + "gix-path", + "gix-ref", + "gix-sec", + "memchr", + "once_cell", + "smallvec", + "thiserror 2.0.12", + "unicode-bom", + "winnow 0.7.11", +] + +[[package]] +name = "gix-config-value" +version = "0.14.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc2c844c4cf141884678cabef736fd91dd73068b9146e6f004ba1a0457944b6" +dependencies = [ + "bitflags 2.9.1", + "bstr", + "gix-path", + "libc", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-credentials" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25322308aaf65789536b860d21137c3f7b69004ac4971c15c1abb08d3951c062" +dependencies = [ + "bstr", + "gix-command", + "gix-config-value", + "gix-path", + "gix-prompt", + "gix-sec", + "gix-trace", + "gix-url", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-date" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa30058ec7d3511fbc229e4f9e696a35abd07ec5b82e635eff864a2726217e4" +dependencies = [ + "bstr", + "itoa", + "jiff", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-diff" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c975dad2afc85e4e233f444d1efbe436c3cdcf3a07173984509c436d00a3f8" +dependencies = [ + "bstr", + "gix-hash 0.17.0", + "gix-object", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-discover" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fb8a4349b854506a3915de18d3341e5f1daa6b489c8affc9ca0d69efe86781" +dependencies = [ + "bstr", + "dunce", + "gix-fs 0.14.0", + "gix-hash 0.17.0", + "gix-path", + "gix-ref", + "gix-sec", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-features" +version = "0.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016d6050219458d14520fe22bdfdeb9cb71631dec9bc2724767c983f60109634" +dependencies = [ + "bytes", + "bytesize 1.3.3", + "crc32fast", + "crossbeam-channel", + "flate2", + "gix-path", + "gix-trace", + "gix-utils 0.2.0", + "libc", + "once_cell", + "parking_lot", + "prodash", + "thiserror 2.0.12", + "walkdir", +] + +[[package]] +name = "gix-features" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f4399af6ec4fd9db84dd4cf9656c5c785ab492ab40a7c27ea92b4241923fed" +dependencies = [ + "gix-trace", + "gix-utils 0.3.0", + "libc", + "prodash", +] + +[[package]] +name = "gix-filter" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb2b2bbffdc5cc9b2b82fc82da1b98163c9b423ac2b45348baa83a947ac9ab89" +dependencies = [ + "bstr", + "encoding_rs", + "gix-attributes", + "gix-command", + "gix-hash 0.17.0", + "gix-object", + "gix-packetline-blocking", + "gix-path", + "gix-quote", + "gix-trace", + "gix-utils 0.2.0", + "smallvec", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-fs" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951e886120dc5fa8cac053e5e5c89443f12368ca36811b2e43d1539081f9c111" +dependencies = [ + "bstr", + "fastrand", + "gix-features 0.41.1", + "gix-path", + "gix-utils 0.2.0", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-fs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a0637149b4ef24d3ea55f81f77231401c8463fae6da27331c987957eb597c7" +dependencies = [ + "bstr", + "fastrand", + "gix-features 0.42.1", + "gix-path", + "gix-utils 0.3.0", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-glob" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20972499c03473e773a2099e5fd0c695b9b72465837797a51a43391a1635a030" +dependencies = [ + "bitflags 2.9.1", + "bstr", + "gix-features 0.41.1", + "gix-path", +] + +[[package]] +name = "gix-hash" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "834e79722063958b03342edaa1e17595cd2939bb2b3306b3225d0815566dcb49" +dependencies = [ + "faster-hex 0.9.0", + "gix-features 0.41.1", + "sha1-checked", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-hash" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d4900562c662852a6b42e2ef03442eccebf24f047d8eab4f23bc12ef0d785d8" +dependencies = [ + "faster-hex 0.10.0", + "gix-features 0.42.1", + "sha1-checked", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-hashtable" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b5cb3c308b4144f2612ff64e32130e641279fcf1a84d8d40dad843b4f64904" +dependencies = [ + "gix-hash 0.18.0", + "hashbrown 0.14.5", + "parking_lot", +] + +[[package]] +name = "gix-ignore" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a27c8380f493a10d1457f756a3f81924d578fc08d6535e304dfcafbf0261d18" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-trace", + "unicode-bom", +] + +[[package]] +name = "gix-index" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "855bece2d4153453aa5d0a80d51deea1ce8cd6a3b4cf213da85ac344ccb908a7" +dependencies = [ + "bitflags 2.9.1", + "bstr", + "filetime", + "fnv", + "gix-bitmap", + "gix-features 0.41.1", + "gix-fs 0.14.0", + "gix-hash 0.17.0", + "gix-lock", + "gix-object", + "gix-traverse", + "gix-utils 0.2.0", + "gix-validate 0.9.4", + "hashbrown 0.14.5", + "itoa", + "libc", + "memmap2", + "rustix 0.38.44", + "smallvec", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-lock" +version = "17.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "570f8b034659f256366dc90f1a24924902f20acccd6a15be96d44d1269e7a796" +dependencies = [ + "gix-tempfile", + "gix-utils 0.3.0", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-negotiate" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad912acf5a68a7defa4836014337ff4381af8c3c098f41f818a8c524285e57b" +dependencies = [ + "bitflags 2.9.1", + "gix-commitgraph", + "gix-date", + "gix-hash 0.17.0", + "gix-object", + "gix-revwalk", + "smallvec", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-object" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4943fcdae6ffc135920c9ea71e0362ed539182924ab7a85dd9dac8d89b0dd69a" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-features 0.41.1", + "gix-hash 0.17.0", + "gix-hashtable", + "gix-path", + "gix-utils 0.2.0", + "gix-validate 0.9.4", + "itoa", + "smallvec", + "thiserror 2.0.12", + "winnow 0.7.11", +] + +[[package]] +name = "gix-odb" +version = "0.68.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50306d40dcc982eb6b7593103f066ea6289c7b094cb9db14f3cd2be0b9f5e610" +dependencies = [ + "arc-swap", + "gix-date", + "gix-features 0.41.1", + "gix-fs 0.14.0", + "gix-hash 0.17.0", + "gix-hashtable", + "gix-object", + "gix-pack", + "gix-path", + "gix-quote", + "parking_lot", + "tempfile", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-pack" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b65fffb09393c26624ca408d32cfe8776fb94cd0a5cdf984905e1d2f39779cb" +dependencies = [ + "clru", + "gix-chunk", + "gix-features 0.41.1", + "gix-hash 0.17.0", + "gix-hashtable", + "gix-object", + "gix-path", + "gix-tempfile", + "memmap2", + "parking_lot", + "smallvec", + "thiserror 2.0.12", + "uluru", +] + +[[package]] +name = "gix-packetline" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123844a70cf4d5352441dc06bab0da8aef61be94ec239cb631e0ba01dc6d3a04" +dependencies = [ + "bstr", + "faster-hex 0.9.0", + "gix-trace", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-packetline-blocking" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecf3ea2e105c7e45587bac04099824301262a6c43357fad5205da36dbb233b3" +dependencies = [ + "bstr", + "faster-hex 0.9.0", + "gix-trace", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-path" +version = "0.10.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567f65fec4ef10dfab97ae71f26a27fd4d7fe7b8e3f90c8a58551c41ff3fb65b" +dependencies = [ + "bstr", + "gix-trace", + "gix-validate 0.10.0", + "home", + "once_cell", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-pathspec" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef8422c3c9066d649074b24025125963f85232bfad32d6d16aea9453b82ec14" +dependencies = [ + "bitflags 2.9.1", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-glob", + "gix-path", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-prompt" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf9cbf6239fd32f2c2c9c57eeb4e9b28fa1c9b779fa0e3b7c455eb1ca49d5f0" +dependencies = [ + "gix-command", + "gix-config-value", + "parking_lot", + "rustix 0.38.44", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-protocol" +version = "0.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5678ddae1d62880bc30e2200be1b9387af3372e0e88e21f81b4e7f8367355b5a" +dependencies = [ + "bstr", + "gix-credentials", + "gix-date", + "gix-features 0.41.1", + "gix-hash 0.17.0", + "gix-lock", + "gix-negotiate", + "gix-object", + "gix-ref", + "gix-refspec", + "gix-revwalk", + "gix-shallow", + "gix-trace", + "gix-transport", + "gix-utils 0.2.0", + "maybe-async", + "thiserror 2.0.12", + "winnow 0.7.11", +] + +[[package]] +name = "gix-quote" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b005c550bf84de3b24aa5e540a23e6146a1c01c7d30470e35d75a12f827f969" +dependencies = [ + "bstr", + "gix-utils 0.2.0", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-ref" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e1f7eb6b7ce82d2d19961f74bd637bab3ea79b1bc7bfb23dbefc67b0415d8b" +dependencies = [ + "gix-actor", + "gix-features 0.41.1", + "gix-fs 0.14.0", + "gix-hash 0.17.0", + "gix-lock", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-utils 0.2.0", + "gix-validate 0.9.4", + "memmap2", + "thiserror 2.0.12", + "winnow 0.7.11", +] + +[[package]] +name = "gix-refspec" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8587b21e2264a6e8938d940c5c99662779c13a10741a5737b15fc85c252ffc" +dependencies = [ + "bstr", + "gix-hash 0.17.0", + "gix-revision", + "gix-validate 0.9.4", + "smallvec", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-revision" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "342caa4e158df3020cadf62f656307c3948fe4eacfdf67171d7212811860c3e9" +dependencies = [ + "bstr", + "gix-commitgraph", + "gix-date", + "gix-hash 0.17.0", + "gix-object", + "gix-revwalk", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-revwalk" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc7c3d7e5cdc1ab8d35130106e4af0a4f9f9eca0c81f4312b690780e92bde0d" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-hash 0.17.0", + "gix-hashtable", + "gix-object", + "smallvec", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-sec" +version = "0.10.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47aeb0f13de9ef2f3033f5ff218de30f44db827ac9f1286f9ef050aacddd5888" +dependencies = [ + "bitflags 2.9.1", + "gix-path", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "gix-shallow" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc0598aacfe1d52575a21c9492fee086edbb21e228ec36c819c42ab923f434c3" +dependencies = [ + "bstr", + "gix-hash 0.17.0", + "gix-lock", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-submodule" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c7390c2059505c365e9548016d4edc9f35749c6a9112b7b1214400bbc68da2" +dependencies = [ + "bstr", + "gix-config", + "gix-path", + "gix-pathspec", + "gix-refspec", + "gix-url", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-tempfile" +version = "17.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c750e8c008453a2dba67a2b0d928b7716e05da31173a3f5e351d5457ad4470aa" +dependencies = [ + "gix-fs 0.15.0", + "libc", + "once_cell", + "parking_lot", + "tempfile", +] + +[[package]] +name = "gix-trace" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c396a2036920c69695f760a65e7f2677267ccf483f25046977d87e4cb2665f7" + +[[package]] +name = "gix-transport" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3f68c2870bfca8278389d2484a7f2215b67d0b0cc5277d3c72ad72acf41787e" +dependencies = [ + "base64", + "bstr", + "gix-command", + "gix-credentials", + "gix-features 0.41.1", + "gix-packetline", + "gix-quote", + "gix-sec", + "gix-url", + "reqwest", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-traverse" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c0b049f8bdb61b20016694102f7b507f2e1727e83e9c5e6dad4f7d84ff7384" +dependencies = [ + "bitflags 2.9.1", + "gix-commitgraph", + "gix-date", + "gix-hash 0.17.0", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "smallvec", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-url" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48dfe23f93f1ddb84977d80bb0dd7aa09d1bf5d5afc0c9b6820cccacc25ae860" +dependencies = [ + "bstr", + "gix-features 0.41.1", + "gix-path", + "percent-encoding", + "thiserror 2.0.12", + "url", +] + +[[package]] +name = "gix-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "189f8724cf903e7fd57cfe0b7bc209db255cacdcb22c781a022f52c3a774f8d0" +dependencies = [ + "fastrand", + "unicode-normalization", +] + +[[package]] +name = "gix-utils" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5351af2b172caf41a3728eb4455326d84e0d70fe26fc4de74ab0bd37df4191c5" +dependencies = [ + "fastrand", + "unicode-normalization", +] + +[[package]] +name = "gix-validate" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b5f1253109da6c79ed7cf6e1e38437080bb6d704c76af14c93e2f255234084" +dependencies = [ + "bstr", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-validate" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77b9e00cacde5b51388d28ed746c493b18a6add1f19b5e01d686b3b9ece66d4d" +dependencies = [ + "bstr", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-worktree" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7760dbc4b79aa274fed30adc0d41dca6b917641f26e7867c4071b1fb4dc727b" +dependencies = [ + "bstr", + "gix-attributes", + "gix-features 0.41.1", + "gix-fs 0.14.0", + "gix-glob", + "gix-hash 0.17.0", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-validate 0.9.4", +] + +[[package]] +name = "gix-worktree-state" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490eb4d38ec2735b3466840aa3881b44ec1a4c180d6a658abfab03910380e18b" +dependencies = [ + "bstr", + "gix-features 0.41.1", + "gix-filter", + "gix-fs 0.14.0", + "gix-glob", + "gix-hash 0.17.0", + "gix-index", + "gix-object", + "gix-path", + "gix-worktree", + "io-close", + "thiserror 2.0.12", +] + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "guess_host_triple" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd62763349a2c83ed2ce9ce5429158085fa43e0cdd8c60011a7843765d04e18" +dependencies = [ + "errno 0.2.8", + "libc", + "log", + "winapi", ] [[package]] name = "h2" -version = "0.2.7" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", "http", "indexmap", "slab", "tokio", "tokio-util", "tracing", - "tracing-futures", +] + +[[package]] +name = "h3" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfb059a4f28a66f186ed16ad912d142f490676acba59353831d7cb45a96b0d3" +dependencies = [ + "bytes", + "fastrand", + "futures-util", + "http", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "h3" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10872b55cfb02a821b69dc7cf8dc6a71d6af25eb9a79662bec4a9d016056b3be" +dependencies = [ + "bytes", + "fastrand", + "futures-util", + "http", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "h3-quinn" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d482318ae94198fc8e3cbb0b7ba3099c865d744e6ec7c62039ca7b6b6c66fbf" +dependencies = [ + "bytes", + "futures", + "h3 0.0.7", + "quinn", + "tokio", + "tokio-util", +] + +[[package]] +name = "h3-quinn" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2e732c8d91a74731663ac8479ab505042fbf547b9a207213ab7fbcbfc4f8b4" +dependencies = [ + "bytes", + "futures", + "h3 0.0.8", + "quinn", + "tokio", + "tokio-util", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", ] [[package]] name = "hashbrown" -version = "0.9.1" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" - -[[package]] -name = "heck" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "unicode-segmentation", + "ahash", + "allocator-api2", ] [[package]] -name = "hermit-abi" -version = "0.1.17" +name = "hashbrown" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" dependencies = [ - "libc", + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "bitflags 2.9.1", + "bytes", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "h2", + "h3 0.0.7", + "h3-quinn 0.0.9", + "http", + "idna", + "ipnet", + "once_cell", + "pin-project-lite", + "quinn", + "rand 0.9.1", + "ring", + "rustls", + "rustls-pki-types", + "thiserror 2.0.12", + "time", + "tinyvec", + "tokio", + "tokio-rustls", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "quinn", + "rand 0.9.1", + "resolv-conf", + "rustls", + "smallvec", + "thiserror 2.0.12", + "tokio", + "tokio-rustls", + "tracing", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", ] [[package]] name = "http" -version = "0.2.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -574,423 +2375,1044 @@ dependencies = [ [[package]] name = "http-body" -version = "0.3.1" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", ] [[package]] -name = "httparse" -version = "1.3.4" +name = "http-body-util" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" -version = "0.3.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "human_format" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3b1f728c459d27b12448862017b96ad4767b1ec2ec5e6434e99f1577f085b8" [[package]] name = "hyper" -version = "0.13.9" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ad767baac13b44d4529fcf58ba2cd0995e36e7b435bc5b039de6f47e880dbf" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", - "futures-core", "futures-util", "h2", "http", "http-body", "httparse", - "httpdate", "itoa", - "pin-project 1.0.2", - "socket2", + "pin-project-lite", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] [[package]] -name = "hyper-tls" -version = "0.4.3" +name = "hyper-rustls" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", + "http-body-util", "hyper", + "hyper-util", "native-tls", "tokio", - "tokio-tls", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration 0.6.1", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", ] [[package]] name = "idna" -version = "0.2.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] name = "indexmap" -version = "1.6.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ - "autocfg", - "hashbrown", + "equivalent", + "hashbrown 0.15.4", ] [[package]] -name = "iovec" -version = "0.1.4" +name = "io-close" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc" dependencies = [ "libc", + "winapi", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2", + "widestring", + "windows-sys 0.48.0", + "winreg 0.50.0", ] [[package]] name = "ipnet" -version = "2.3.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] [[package]] name = "itoa" -version = "0.4.6" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", + "windows-sys 0.52.0", +] + +[[package]] +name = "jiff-static" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "jobslot" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "571ee549d772b43a0d8f8836f74944b67038cf31c710830da36d7dac8f4da565" +dependencies = [ + "cfg-if", + "derive_destructure2", + "getrandom 0.3.3", + "libc", + "scopeguard", + "tokio", + "windows-sys 0.60.2", +] [[package]] name = "js-sys" -version = "0.3.46" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d7383929f7c9c7c2d0fa596f325832df98c3704f2c60553080f7127a58175" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] -name = "kernel32-sys" -version = "0.2.2" +name = "kstring" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" dependencies = [ - "winapi 0.2.8", - "winapi-build", + "static_assertions", ] [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "libc" -version = "0.2.81" +name = "leon" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" - -[[package]] -name = "log" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +checksum = "42a865ffec5587961f5afc6d365bccb304f4feaa1928f4fe94c91c9d210d7310" dependencies = [ - "cfg-if 0.1.10", + "miette", + "thiserror 2.0.12", ] [[package]] -name = "maplit" -version = "1.0.2" +name = "leon-macros" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +checksum = "e0c7ff357e507638488e191166c6e396ff53f921664547d40fe63aa38e5014b0" +dependencies = [ + "leon", + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "matches" -version = "0.1.8" +name = "libbz2-rs-sys" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +checksum = "0864a00c8d019e36216b69c2c4ce50b83b7bd966add3cf5ba554ec44f8bebcf5" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libmimalloc-sys" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.1", + "libc", + "redox_syscall 0.5.13", +] + +[[package]] +name = "libz-ng-sys" +version = "1.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7118c2c2a3c7b6edc279a8b19507672b9c4d716f95e671172dfa4e23f9fd824" +dependencies = [ + "cmake", + "libc", +] + +[[package]] +name = "libz-rs-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +dependencies = [ + "zlib-rs", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "maybe-async" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" +dependencies = [ + "serde", +] + +[[package]] +name = "memalloc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1" [[package]] name = "memchr" -version = "2.3.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "mimalloc" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af" +dependencies = [ + "libmimalloc-sys", +] [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] -name = "mime_guess" -version = "2.0.3" +name = "minisign-verify" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ - "mime", - "unicase", + "adler", ] [[package]] name = "miniz_oxide" -version = "0.4.3" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ - "adler", - "autocfg", + "adler2", ] [[package]] name = "mio" -version = "0.6.23" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ - "cfg-if 0.1.10", - "fuchsia-zircon", - "fuchsia-zircon-sys", - "iovec", - "kernel32-sys", "libc", - "log", - "miow", - "net2", - "slab", - "winapi 0.2.8", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] -name = "miow" -version = "0.2.2" +name = "moka" +version = "0.12.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" dependencies = [ - "kernel32-sys", - "net2", - "winapi 0.2.8", - "ws2_32-sys", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "loom", + "parking_lot", + "portable-atomic", + "rustc_version", + "smallvec", + "tagptr", + "thiserror 1.0.69", + "uuid", ] [[package]] name = "native-tls" -version = "0.2.6" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fcc7939b5edc4e4f86b1b4a04bb1498afaaf871b1a6691838ed06fcb48d3a3f" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ - "lazy_static", "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] [[package]] -name = "net2" -version = "0.2.37" +name = "netlink-packet-core" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" dependencies = [ - "cfg-if 0.1.10", - "libc", - "winapi 0.3.9", + "anyhow", + "byteorder", + "netlink-packet-utils", ] [[package]] -name = "num-integer" -version = "0.1.44" +name = "netlink-packet-route" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +checksum = "053998cea5a306971f88580d0829e90f270f940befd7cf928da179d4187a5a66" dependencies = [ - "autocfg", - "num-traits", + "anyhow", + "bitflags 1.3.2", + "byteorder", + "libc", + "netlink-packet-core", + "netlink-packet-utils", ] +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror 1.0.69", +] + +[[package]] +name = "netlink-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" +dependencies = [ + "bytes", + "libc", + "log", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + +[[package]] +name = "normalize-path" +version = "0.2.1" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] -name = "num_cpus" -version = "1.13.0" +name = "num_enum" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ - "hermit-abi", "libc", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "oem_cp" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330138902ab4dab09a86e6b7ab7ddeffb5f8435d52fe0df1bce8b06a17b10ee4" +dependencies = [ + "phf", + "phf_codegen", + "serde", + "serde_json", +] + [[package]] name = "once_cell" -version = "1.5.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] -name = "opaque-debug" -version = "0.2.3" +name = "once_cell_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "openssl" -version = "0.10.31" +version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d008f51b1acffa0d3450a68606e6a51c123012edaacb0f4e1426bd978869187" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags", - "cfg-if 1.0.0", + "bitflags 2.9.1", + "cfg-if", "foreign-types", - "lazy_static", "libc", + "once_cell", + "openssl-macros", "openssl-sys", ] [[package]] -name = "openssl-probe" -version = "0.1.2" +name = "openssl-macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-src" +version = "300.5.0+3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f" +dependencies = [ + "cc", +] [[package]] name = "openssl-sys" -version = "0.9.59" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de52d8eabd217311538a39bba130d7dea1f1e118010fee7a033d966845e7d5fe" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ - "autocfg", "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] [[package]] -name = "percent-encoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" - -[[package]] -name = "pest" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" -dependencies = [ - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" -dependencies = [ - "maplit", - "pest", - "sha-1", -] - -[[package]] -name = "pin-project" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffbc8e94b38ea3d2d8ba92aea2983b503cd75d0888d75b86bb37970b5698e15" -dependencies = [ - "pin-project-internal 0.4.27", -] - -[[package]] -name = "pin-project" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ccc2237c2c489783abd8c4c80e5450fc0e98644555b1364da68cc29aa151ca7" -dependencies = [ - "pin-project-internal 1.0.2", -] - -[[package]] -name = "pin-project-internal" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pin-project-internal" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8e8d2bf0b23038a4424865103a4df472855692821aab4e4f5c3312d461d9e5f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pin-project-lite" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b" - -[[package]] -name = "pin-project-lite" +name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b063f57ec186e6140e2b8b6921e5f1bd89c7356dda5b33acc5401203ca6131c" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "oval" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135cef32720c6746450d910890b0b69bcba2bbf6f85c9f4583df13fe415de828" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "ownable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcba94d1536fcc470287d96fd26356c38da8215fdb9a74285b09621f35d9350" +dependencies = [ + "ownable-macro", +] + +[[package]] +name = "ownable-macro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2c91d2781624dec1234581a1a01e63638f36546ad72ee82873ac1b84f41117b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "owo-colors" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.13", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -1000,15 +3422,68 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.19" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "positioned-io" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8078ce4d22da5e8f57324d985cc9befe40c49ab0507a192d6be9e59584495c9" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.10" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] [[package]] name = "proc-macro-error" @@ -1019,7 +3494,6 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", "version_check", ] @@ -1034,219 +3508,494 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - -[[package]] -name = "proc-macro-nested" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" - [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ - "unicode-xid", + "unicode-ident", +] + +[[package]] +name = "prodash" +version = "29.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04bb108f648884c23b98a0e940ebc2c93c0c3b89f04dbaf7eb8256ce617d1bc" +dependencies = [ + "bytesize 2.0.1", + "human_format", + "log", + "parking_lot", +] + +[[package]] +name = "quinn" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +dependencies = [ + "bytes", + "cfg_aliases", + "futures-io", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.1", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", ] [[package]] name = "quote" -version = "1.0.7" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] -name = "rand" -version = "0.4.6" +name = "r-efi" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi 0.3.9", + "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.7.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ - "getrandom", - "libc", "rand_chacha", - "rand_core 0.5.1", - "rand_hc", + "rand_core 0.9.3", ] [[package]] name = "rand_chacha" -version = "0.2.2" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.5.1", + "rand_core 0.9.3", ] [[package]] name = "rand_core" -version = "0.3.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "rand_core" -version = "0.4.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom", + "getrandom 0.3.3", ] [[package]] -name = "rand_hc" -version = "0.2.0" +name = "rc-zip" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "ebde715984a68b306e5b41884cbcb8158e0e1dbe6e2841212983333b1662c416" dependencies = [ - "rand_core 0.5.1", + "bzip2", + "chardetng", + "chrono", + "crc32fast", + "deflate64", + "encoding_rs", + "lzma-rs", + "miniz_oxide 0.7.4", + "num_enum", + "oem_cp", + "oval", + "ownable", + "thiserror 1.0.69", + "tracing", + "winnow 0.5.40", + "zstd", ] [[package]] -name = "rdrand" -version = "0.4.0" +name = "rc-zip-sync" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +checksum = "dc9658d8ace392ab7f2b149ae6c40bea7a3d70ebc39aaa6c7076ddc3f11c9c32" dependencies = [ - "rand_core 0.3.1", + "oval", + "positioned-io", + "rc-zip", + "tracing", ] [[package]] name = "redox_syscall" -version = "0.1.57" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +dependencies = [ + "bitflags 2.9.1", +] [[package]] name = "redox_users" -version = "0.3.5" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom", - "redox_syscall", - "rust-argon2", + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.12", ] [[package]] -name = "remove_dir_all" -version = "0.5.3" +name = "reflink-copy" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +checksum = "78c81d000a2c524133cc00d2f92f019d399e57906c3b7119271a2495354fe895" dependencies = [ - "winapi 0.3.9", + "cfg-if", + "libc", + "rustix 1.0.7", + "windows 0.61.3", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "reqwest" -version = "0.10.9" +version = "0.12.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb15d6255c792356a0f578d8a645c677904dc02e862bebe2ecc18e0c01b9a0ce" +checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" dependencies = [ + "async-compression", "base64", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", + "h2", + "h3 0.0.8", + "h3-quinn 0.0.10", "http", "http-body", + "http-body-util", "hyper", + "hyper-rustls", "hyper-tls", - "ipnet", + "hyper-util", "js-sys", - "lazy_static", "log", "mime", - "mime_guess", "native-tls", "percent-encoding", - "pin-project-lite 0.2.0", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", + "slab", + "sync_wrapper", "tokio", - "tokio-tls", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-bindgen-test", + "wasm-streams", "web-sys", - "winreg", + "webpki-roots", ] [[package]] -name = "rust-argon2" -version = "0.8.3" +name = "resolv-conf" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ - "base64", - "blake2b_simd", - "constant_time_eq", - "crossbeam-utils", + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", ] +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.1", + "errno 0.3.12", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.1", + "errno 0.3.12", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.2.0", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + [[package]] name = "ryu" -version = "1.0.5" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] [[package]] name = "schannel" -version = "0.1.19" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ - "lazy_static", - "winapi 0.3.9", + "windows-sys 0.59.0", ] [[package]] name = "scoped-tls" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.0.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1759c2e3c8580017a484a7ac56d3abc5a6c1feadf88db2f3633f12ae4268c69" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", - "core-foundation", + "bitflags 2.9.1", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -1254,9 +4003,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.0.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f99b9d5e26d2a71633cc4f2ebae7cc9f874044e0c351a27e17892d76dce5678b" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -1264,38 +4013,36 @@ dependencies = [ [[package]] name = "semver" -version = "0.11.0" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" dependencies = [ - "semver-parser", "serde", ] -[[package]] -name = "semver-parser" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e012c6c5380fb91897ba7b9261a0f565e624e869d42fe1a1d03fa0d68a083d5" -dependencies = [ - "pest", - "pest_derive", -] - [[package]] name = "serde" -version = "1.0.118" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] -name = "serde_derive" -version = "1.0.118" +name = "serde-tuple-vec-map" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" +checksum = "a04d0ebe0de77d7d445bb729a895dcb0a288854b267ca85f030ce51cdc578c82" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -1304,20 +4051,30 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.60" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] [[package]] -name = "serde_urlencoded" -version = "0.7.0" +name = "serde_spanned" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", @@ -1326,219 +4083,423 @@ dependencies = [ ] [[package]] -name = "sha-1" -version = "0.8.2" +name = "sha1" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "block-buffer", + "cfg-if", + "cpufeatures", "digest", - "fake-simd", - "opaque-debug", ] [[package]] -name = "simplelog" -version = "0.8.0" +name = "sha1-checked" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2736f58087298a448859961d3f4a0850b832e72619d75adc69da7993c2cd3c" +checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" dependencies = [ - "chrono", - "log", - "termcolor", + "digest", + "sha1", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "simple-git" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532b9541e0f6fe54f7a721953d81371b58b601d618294bd8eba284df79acefff" +dependencies = [ + "compact_str", + "derive_destructure2", + "gix", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" -version = "0.4.2" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.3.17" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c29947abdee2a218277abeca306f25789c938e500ea5a9d4b12a5a504466902" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ - "cfg-if 1.0.0", "libc", - "redox_syscall", - "winapi 0.3.9", + "windows-sys 0.52.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" -version = "0.8.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[package]] -name = "structopt" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c" -dependencies = [ - "clap", - "lazy_static", - "structopt-derive", -] - -[[package]] -name = "structopt-derive" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.20.0" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" [[package]] name = "strum_macros" -version = "0.20.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" dependencies = [ "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + +[[package]] +name = "syn" +version = "2.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] -name = "syn" -version = "1.0.54" +name = "system-configuration" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", + "bitflags 1.3.2", + "core-foundation 0.9.4", + "system-configuration-sys 0.5.0", ] [[package]] -name = "tar" -version = "0.4.30" +name = "system-configuration" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "489997b7557e9a43e192c527face4feacc78bfbe6eed67fd55c4c9e381cba290" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "filetime", + "bitflags 2.9.1", + "core-foundation 0.9.4", + "system-configuration-sys 0.6.0", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", "libc", - "redox_syscall", - "xattr", ] [[package]] -name = "tempdir" -version = "0.3.7" +name = "system-configuration-sys" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ - "rand 0.4.6", - "remove_dir_all", + "core-foundation-sys", + "libc", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "target-lexicon" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" + [[package]] name = "tempfile" -version = "3.1.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ - "cfg-if 0.1.10", - "libc", - "rand 0.7.3", - "redox_syscall", - "remove_dir_all", - "winapi 0.3.9", + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix 1.0.7", + "windows-sys 0.52.0", ] [[package]] -name = "termcolor" -version = "1.1.2" +name = "terminal_size" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ - "winapi-util", + "rustix 1.0.7", + "windows-sys 0.59.0", ] [[package]] name = "textwrap" -version = "0.11.0" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ - "unicode-width", + "unicode-linebreak", + "unicode-width 0.2.1", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", ] [[package]] name = "time" -version = "0.1.44" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ + "deranged", + "itoa", "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi 0.3.9", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", ] [[package]] -name = "tinytemplate" -version = "1.1.0" +name = "time-core" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d3dc76004a03cec1c5932bca4cdc2e39aaa798e3f82363dd94f9adf6098c12f" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ - "serde", - "serde_json", + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", ] [[package]] name = "tinyvec" -version = "1.1.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf8dbc19eb42fba10e8feaaec282fb50e2c14b2726d6301dbfeed0f73306a6f" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "0.2.24" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099837d3464c16a808060bb3f02263b412f6fafcb5d01c533d309985fbeebe48" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ + "backtrace", "bytes", - "fnv", - "futures-core", - "iovec", - "lazy_static", - "memchr", + "libc", "mio", - "num_cpus", - "pin-project-lite 0.1.11", - "slab", + "pin-project-lite", + "signal-hook-registry", + "socket2", "tokio-macros", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "0.2.6" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", @@ -1546,210 +4507,432 @@ dependencies = [ ] [[package]] -name = "tokio-tls" +name = "tokio-native-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", ] [[package]] -name = "tokio-util" +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tar" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +checksum = "9d5714c010ca3e5c27114c1cdeb9d14641ace49874aa5626d7149e47aedace75" +dependencies = [ + "filetime", + "futures-core", + "libc", + "redox_syscall 0.3.5", + "tokio", + "tokio-stream", + "xattr", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", "futures-sink", - "log", - "pin-project-lite 0.1.11", + "pin-project-lite", "tokio", ] [[package]] name = "toml" -version = "0.5.7" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] -name = "tower-service" -version = "0.3.0" +name = "toml_edit" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow 0.7.11", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.22" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f47026cdc4080c07e49b37087de021820269d996f581aac150ef9e5583eefe3" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ - "cfg-if 1.0.0", "log", - "pin-project-lite 0.2.0", + "pin-project-lite", + "tracing-attributes", "tracing-core", ] [[package]] -name = "tracing-core" -version = "0.1.17" +name = "tracing-attributes" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" dependencies = [ - "lazy_static", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "tracing-futures" -version = "0.2.4" +name = "tracing-core" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab7bb6f14721aa00656086e9335d363c5c8747bae02ebe32ea2c7dece5689b4c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ - "pin-project 0.4.27", + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] name = "try-lock" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.12.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] -name = "ucd-trie" -version = "0.1.3" +name = "uluru" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" - -[[package]] -name = "unicase" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da" dependencies = [ - "version_check", + "arrayvec", ] [[package]] -name = "unicode-bidi" -version = "0.3.4" +name = "unicode-bom" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" -dependencies = [ - "matches", -] +checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.16" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] -name = "unicode-segmentation" -version = "1.7.1" +name = "unicode-width" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" - -[[package]] -name = "unicode-xid" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.2.0" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", + "serde", ] [[package]] -name = "vcpkg" -version = "0.2.11" +name = "utf8_iter" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "vec_map" -version = "0.8.2" +name = "utf8parse" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "8.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990d9ea5967266ea0ccf413a4aa5c42a93dbcfda9cb49a97de6931726b12566" +dependencies = [ + "anyhow", + "cargo_metadata", + "cfg-if", + "regex", + "rustc_version", + "rustversion", + "time", +] [[package]] name = "version_check" -version = "0.9.2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] [[package]] name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] [[package]] name = "wasm-bindgen" -version = "0.2.69" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ - "cfg-if 1.0.0", - "serde", - "serde_json", + "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.69" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", - "lazy_static", "log", "proc-macro2", "quote", @@ -1759,21 +4942,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.19" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe9756085a84584ee9457a002b7cdfe0bfff169f45d2591d8be1345a6780e35" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.69" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1781,9 +4965,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.69" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -1794,49 +4978,60 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.69" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" - -[[package]] -name = "wasm-bindgen-test" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0355fa0c1f9b792a09b6dcb6a8be24d51e71e6d74972f9eb4a44c4c004d24a25" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ - "console_error_panic_hook", - "js-sys", - "scoped-tls", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test-macro", + "unicode-ident", ] [[package]] -name = "wasm-bindgen-test-macro" -version = "0.3.19" +name = "wasm-streams" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27e07b46b98024c2ba2f9e83a10c2ef0515f057f2da299c1762a2017de80438b" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" dependencies = [ - "proc-macro2", - "quote", + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] name = "web-sys" -version = "0.3.46" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222b1ef9334f92a21d3fb53dc3fd80f30836959a90f9274a626d7e06315ba3c3" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] -name = "winapi" -version = "0.2.8" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" [[package]] name = "winapi" @@ -1848,12 +5043,6 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -1862,11 +5051,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi 0.3.9", + "windows-sys 0.48.0", ] [[package]] @@ -1876,29 +5065,563 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "winreg" -version = "0.7.0" +name = "windows" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "winapi 0.3.9", + "windows-targets 0.48.5", ] [[package]] -name = "ws2_32-sys" +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "winapi 0.2.8", - "winapi-build", + "windows-core", + "windows-link", + "windows-threading", ] +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + [[package]] name = "xattr" -version = "0.2.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" +checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" dependencies = [ "libc", + "rustix 1.0.7", +] + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zlib-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", ] diff --git a/Cargo.toml b/Cargo.toml index 6c1f27db..b70ccfb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,29 +1,73 @@ -[package] -name = "cargo-binstall" -description = "Rust binary package installer for CI integration" -repository = "https://github.com/ryankurte/cargo-binutil" -version = "0.1.0" -authors = ["ryan "] -edition = "2018" -license = "GPL-3.0" +[workspace] +resolver = "2" +members = [ + "crates/atomic-file-install", + "crates/bin", + "crates/binstalk", + "crates/binstalk-bins", + "crates/binstalk-fetchers", + "crates/binstalk-registry", + "crates/binstalk-manifests", + "crates/binstalk-types", + "crates/binstalk-downloader", + "crates/cargo-toml-workspace", + "crates/detect-wasi", + "crates/fs-lock", + "crates/normalize-path", + "crates/detect-targets", + "crates/binstalk-git-repo-api", +] +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +panic = "abort" +strip = "symbols" -[dependencies] -crates_io_api = "0.6.1" -cargo_metadata = "0.12.1" -tinytemplate = "1.1.0" -tokio = { version = "0.2.24", features = [ "macros" ] } -log = "0.4.11" -structopt = "0.3.21" -simplelog = "0.8.0" -anyhow = "1.0.35" -reqwest = { version = "0.10.9" } -tempdir = "0.3.7" -flate2 = "1.0.19" -tar = "0.4.30" -cargo_toml = "0.8.1" -serde = { version = "1.0.118", features = [ "derive" ] } -strum_macros = "0.20.1" -strum = "0.20.0" -dirs = "3.0.1" -serde_derive = "1.0.118" +[profile.release.build-override] +inherits = "dev.build-override" + +[profile.release.package."tokio-tar"] +opt-level = "z" + +[profile.release.package."binstall-tar"] +opt-level = "z" + +[profile.dev] +opt-level = 0 +debug = "line-tables-only" +lto = false +debug-assertions = true +overflow-checks = true +codegen-units = 32 + +# Set the default for dependencies on debug. +[profile.dev.package."*"] +opt-level = 3 + +[profile.dev.package."tokio-tar"] +opt-level = "z" + +[profile.dev.package."binstall-tar"] +opt-level = "z" + +[profile.dev.build-override] +inherits = "dev" +debug = false +debug-assertions = false +overflow-checks = false +incremental = false + +[profile.check-only] +inherits = "dev" +debug = false +debug-assertions = false +overflow-checks = false +panic = "abort" + +[profile.check-only.build-override] +inherits = "check-only" + +[profile.check-only.package."*"] +inherits = "check-only" diff --git a/README.md b/README.md index a1d4c69a..d8a49b50 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,177 @@ -# Cargo B(inary) Install +# Cargo B(inary)Install -A helper for distributing / installing CI built rust binaries in a pseudo-distributed and maybe-one-day secure manner. -This is part experiment, part solving a personal problem, and part hope that we can solve / never re-visit this. Good luck! +Binstall provides a low-complexity mechanism for installing Rust binaries as an alternative to building from source (via `cargo install`) or manually downloading packages. +This is intended to work with existing CI artifacts and infrastructure, and with minimal overhead for package maintainers. -## Status +Binstall works by fetching the crate information from `crates.io` and searching the linked `repository` for matching releases and artifacts, falling back to the [quickinstall](https://github.com/alsuren/cargo-quickinstall) third-party artifact host, to alternate targets as supported, and finally to `cargo install` as a last resort. -![Rust](https://github.com/ryankurte/cargo-binstall/workflows/Rust/badge.svg) -[![GitHub tag](https://img.shields.io/github/tag/ryankurte/cargo-binstall.svg)](https://github.com/ryankurte/cargo-binstall) +[![CI build](https://github.com/cargo-bins/cargo-binstall/actions/workflows/ci.yml/badge.svg)](https://github.com/cargo-bins/cargo-binstall/actions) +[![GitHub tag](https://img.shields.io/github/tag/cargo-bins/cargo-binstall.svg)](https://github.com/cargo-bins/cargo-binstall) [![Crates.io](https://img.shields.io/crates/v/cargo-binstall.svg)](https://crates.io/crates/cargo-binstall) -[![Docs.rs](https://docs.rs/cargo-binstall/badge.svg)](https://docs.rs/cargo-binstall) - -## Getting Started - -First you'll need to install `cargo-binstall` either via `cargo install cargo-binstall` (and it'll have to compile, sorry...), or by grabbing a pre-compiled version from the [releases](https://github.com/ryankurte/cargo-binstall/releases) page and putting that somewhere on your path. It's like there's a problem we're trying to solve? - -If a project supports `binstall` you can then install binaries via `cargo binstall NAME` where `NAME` is the name of the crate. We hope the defaults will work without configuration in _some_ cases, however, different projects have wildly different configurations. You may need to add some cargo metadata to support `binstall` in your project, see [Usage](#Usage) for details. - - -## Features - -- Manifest discovery - - [x] Fetch manifest from crates.io - - [ ] Fetch manifest using git - - [x] Use local manifest (`--manifest-path`) -- Package formats - - [x] Tgz - - [x] Tar - - [x] Bin -- Security - - [ ] Package signing - - [ ] Package verification +_You may want to [see this page as it was when the latest version was published](https://crates.io/crates/cargo-binstall)._ ## Usage -Packages are located first by locating or querying for a manifest (to allow configuration of the tool), then by interpolating a templated string to download the required package. Where possible defaults are provided to avoid any need for additional configuration, these can generally be overridden via `[package.metadata]` keys at a project level, or on the command line as required (and for debugging), see `cargo binstall -- help` for details. - - -By default `binstall` will look for pre-built packages at `{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.{ format }`, where `repo`, `name`, and `version` are those specified in the crate manifest (`Cargo.toml`). -`target` defaults to your architecture, but can be overridden using the `--target` command line option _if required_, and `format` defaults to `tgz` and can be specified via the `pkg-fmt` key (you may need this if you have sneaky `tgz` files that are actually not gzipped). - -To support projects with different binary URLs you can override these via the following mechanisms: - -To replace _only_ the the package name, specify (`pkg-name`) under `[package.metadata]`. This is useful if you're using github, and your binary paths mostly match except that output package names differ from your crate name. As an example, the `ryankurte/radio-sx128x` crate produces a `sx128x-util` package, and can be configured using the following: - -``` -[package.metadata] -pkg-name = "sx128x-util" +```console +$ cargo binstall radio-sx128x@0.14.1-alpha.5 + INFO resolve: Resolving package: 'radio-sx128x@=0.14.1-alpha.5' + WARN The package radio-sx128x v0.14.1-alpha.5 (x86_64-unknown-linux-gnu) has been downloaded from github.com + INFO This will install the following binaries: + INFO - sx128x-util (sx128x-util-x86_64-unknown-linux-gnu -> /home/.cargo/bin/sx128x-util) +Do you wish to continue? [yes]/no +? yes + INFO Installing binaries... + INFO Done in 2.838798298s ``` -To replace the entire URL, with all the benefits of interpolation, specify (`pkg-url`) under `[package.metadata]`. -This lets you customise the URL for completely different paths (or different services!). Using the same example as above, this becomes: +Binstall aims to be a drop-in replacement for `cargo install` in many cases, and supports similar options. + +For unattended use (e.g. in CI), use the `--no-confirm` flag. +For additional options please see `cargo binstall --help`. + +## Installation + +### If you already have it + +To upgrade cargo-binstall, use `cargo binstall cargo-binstall`! + +### Quickly + +Here are one-liners for downloading and installing a pre-compiled `cargo-binstall` binary. + +#### Linux and macOS ``` -[package.metadata] -pkg-url = "https://github.com/ryankurte/rust-radio-sx128x/releases/download/v{ version }/sx128x-util-{ target }-v{ version }.tgz" +curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash ``` +or if you have [homebrew](https://brew.sh/) installed: + +``` +brew install cargo-binstall +``` + +#### Windows + +``` +Set-ExecutionPolicy Unrestricted -Scope Process; iex (iwr "https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.ps1").Content +``` + +### Manually + +Download the relevant package for your system below, unpack it, and move the `cargo-binstall` executable into `$HOME/.cargo/bin`: + +| OS | Arch | URL | +| ------- | ------- | ------------------------------------------------------------ | +| Linux | x86\_64 | https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz | +| Linux | armv7 | https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-armv7-unknown-linux-musleabihf.tgz | +| Linux | arm64 | https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-aarch64-unknown-linux-musl.tgz | +| Mac | Intel | https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-apple-darwin.zip | +| Mac | Apple Silicon | https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-aarch64-apple-darwin.zip | +| Mac | Universal
(both archs) | https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-universal-apple-darwin.zip | +| Windows | Intel/AMD | https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-pc-windows-msvc.zip | +| Windows | ARM 64 | https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-aarch64-pc-windows-msvc.zip | + +### From source + +With a recent [Rust](https://rustup.rs) installed: + +``` +cargo install cargo-binstall +``` + +### In GitHub Actions + +We provide a first-party, minimal action that installs the latest version of Binstall: + +```yml + - uses: cargo-bins/cargo-binstall@main +``` + +For more features, we recommend the excellent [taiki-e/install-action](https://github.com/marketplace/actions/install-development-tools), which has dedicated support for selected tools and uses Binstall for everything else. + +## Companion tools + +These are useful *third-party* tools which work well with Binstall. + +### [`cargo-update`](https://github.com/nabijaczleweli/cargo-update) + +While you can upgrade crates explicitly by running `cargo binstall` again, `cargo-update` takes care of updating all tools as needed. +It automatically uses Binstall to install the updates if it is present. + +### [`cargo-run-bin`](https://github.com/dustinblackman/cargo-run-bin) + +Binstall and `cargo install` both install tools globally by default, which is fine for system-wide tools. +When installing tooling for a project, however, you may prefer to both scope the tools to that project and control their versions in code. +That's where `cargo-run-bin` comes in, with a dedicated section in your Cargo.toml and a short cargo subcommand. +When Binstall is available, it installs from binary whenever possible... and you can even manage Binstall itself with `cargo-run-bin`! + +## Unsupported crates + +Binstall is generally smart enough to auto-detect artifacts in most situations. +However, if a package fails to install, you can manually specify the `pkg-url`, `bin-dir`, and `pkg-fmt` as needed at the command line, with values as documented in [SUPPORT.md](https://github.com/cargo-bins/cargo-binstall/blob/main/SUPPORT.md). + +```console +$ cargo-binstall \ + --pkg-url="{ repo }/releases/download/{ version }/{ name }-{ version }-{ target }.{ archive-format }" \ + --pkg-fmt="txz" \ + crate_name +``` + +Maintainers wanting to make their users' life easier can add [explicit Binstall metadata](https://github.com/cargo-bins/cargo-binstall/blob/main/SUPPORT.md) to `Cargo.toml` to locate the appropriate binary package for a given version and target. + +## Signatures + +We have initial, limited [support](https://github.com/cargo-bins/cargo-binstall/blob/main/SIGNING.md) for maintainers to specify a signing public key and where to find package signatures. +With this enabled, Binstall will download and verify signatures for that package. + +You can use `--only-signed` to refuse to install packages if they're not signed. + +If you like to live dangerously (please don't use this outside testing), you can use `--skip-signatures` to disable checking or even downloading signatures at all. + +## FAQ + +### Why use this? +Because `wget`-ing releases is frustrating, `cargo install` takes a not inconsequential portion of forever on constrained devices, and often putting together actual _packages_ is overkill. + +### Why use the cargo manifest? +Crates already have these, and they already contain a significant portion of the required information. +Also, there's this great and woefully underused (IMO) `[package.metadata]` field. + +### Is this secure? +Yes and also no? + +We have [initial support](https://github.com/cargo-bins/cargo-binstall/blob/main/SIGNING.md) for verifying signatures, but not a lot of the ecosystem produces signatures at the moment. +See [#1](https://github.com/cargo-bins/cargo-binstall/issues/1) to discuss more on this. + +We always pull the metadata from crates.io over HTTPS, and verify the checksum of the crate tar. +We also enforce using HTTPS with TLS >= 1.2 for the actual download of the package files. + +Compared to something like a `curl ... | sh` script, we're not running arbitrary code, but of course the crate you're downloading a package for might itself be malicious! + +### What do the error codes mean? +You can find a full description of errors including exit codes here: + +### Are debug symbols available? +Yes! +Extra pre-built packages with a `.full` suffix are available and contain split debuginfo, documentation files, and extra binaries like the `detect-wasi` utility. + +## Telemetry collection + +Some crate installation strategies may collect anonymized usage statistics by default. +Currently, only the name of the crate to be installed, its version, the target platform triple, and the collecting user agent are sent to endpoints under the `https://warehouse-clerk-tmp.vercel.app/api/crate` URL when the `quickinstall` artifact host is used. +The maintainers of the `quickinstall` project use this data to determine which crate versions are most worthwhile to build and host. +The aggregated collected telemetry is publicly accessible at . +Should you be interested on it, the backend code for these endpoints can be found at . + +If you prefer not to participate in this data collection, you can opt out by any of the following methods: + +- Setting the `--disable-telemetry` flag in the command line interface. +- Setting the `BINSTALL_DISABLE_TELEMETRY` environment variable to `true`. +- Disabling the `quickinstall` strategy with `--disable-strategy quick-install`, or if specifying a list of strategies to use with `--strategy`, avoiding including `quickinstall` in that list. +- Adding `quick-install` to the `disabled-strategies` configuration key in the crate metadata (refer to [the related support documentation](SUPPORT.md#support-for-cargo-binstall) for more details). + --- -If anything is not working the way you expect, add a `--log-level debug` to see debug information, and feel free to open an issue or PR. +If you have ideas/contributions or anything is not working the way you expect (in which case, please include an output with `--log-level debug`) and feel free to open an issue or PR. diff --git a/SIGNING.md b/SIGNING.md new file mode 100644 index 00000000..29803ce4 --- /dev/null +++ b/SIGNING.md @@ -0,0 +1,112 @@ +# Signature support + +Binstall supports verifying signatures of downloaded files. +At the moment, only one algorithm is supported, but this is expected to improve as time goes. + +This feature requires adding to the Cargo.toml metadata: no autodiscovery here! + +## Minimal example + +Generate a [minisign](https://jedisct1.github.io/minisign/) keypair: + +```console +minisign -G -W -p signing.pub -s signing.key + +# or with rsign2: +rsign generate -W -p signing.pub -s signing.key +``` + +In your Cargo.toml, put: + +```toml +[package.metadata.binstall.signing] +algorithm = "minisign" +pubkey = "RWRnmBcLmQbXVcEPWo2OOKMI36kki4GiI7gcBgIaPLwvxe14Wtxm9acX" +``` + +Replace the value of `pubkey` with the public key in your `signing.pub`. + +Save the `signing.key` as a secret in your CI, then use it when building packages: + +```console +tar cvf package-name.tar.zst your-files # or however + +minisign -S -W -s signing.key -x package-name.tar.zst.sig -m package-name.tar.zst + +# or with rsign2: +rsign sign -W -s signing.key -x package-name.tar.zst.sig package-name.tar.zst +``` + +Upload both your package and the matching `.sig`. + +Now when binstall downloads your packages, it will also download the `.sig` file and use the `pubkey` in the Cargo.toml to verify the signature. +If the signature has a trusted comment, it will print it at install time. + +By default, `minisign` and `rsign2` prompt for a password; above we disable this with `-W`. +While you _can_ set a password, we recommend instead using [age](https://github.com/FiloSottile/age) (or the Rust version [rage](https://github.com/str4d/rage)) to separately encrypt the key, which we find is much better for automation. + +```console +rage-keygen -o age.key +Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p + +rage -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p -o signing.key.age signing.key +rage -d -i age.key -o signing.key signing.key.age +``` + +For just-in-time or "keyless" schemes, securely generating and passing the ephemeral key to other jobs or workflows presents subtle issues. +`cargo-binstall` has an implementation in [its own release process][`release.yml`] that you can use as example. + +[`expect`]: https://linux.die.net/man/1/expect +[`release.yml`]: https://github.com/cargo-bins/cargo-binstall/blob/main/.github/workflows/release.yml + +## Reference + +- `algorithm`: required, see below. +- `pubkey`: required, must be the public key. +- `file`: optional, a template to specify the URL of the signature file. Defaults to `{ url }.sig` where `{ url }` is the download URL of the package. + +### Minisign + +`algorithm` must be `"minisign"`. + +The legacy signature format is not supported. + +The `pubkey` must be in the same format as minisign generates. +It may or may not include the untrusted comment; it's ignored by Binstall so we recommend not. + +## Just-in-time signing + +To reduce the risk of a key being stolen, this scheme supports just-in-time or "keyless" signing. +The idea is to generate a keypair when releasing, use it for signing the packages, save the key in the Cargo.toml before publishing to a registry, and then discard the private key when it's done. +That way, there's no key to steal nor to store securely, and every release is signed by a different key. +And because crates.io is immutable, it's impossible to overwrite the key. + +There is one caveat to keep in mind: with the scheme as described above, Binstalling with `--git` may not work: + +- If the Cargo.toml in the source contains a partially-filled `[...signing]` section, Binstall will fail. +- If the section contains a different key than the ephemeral one used to sign the packages, Binstall will refuse to install what it sees as corrupt packages. +- If the section is missing entirely, Binstall will work, but of course signatures won't be checked. + +The solution here is either: + +- Commit the Cargo.toml with the ephemeral public key to the repo when publishing. +- Omit the `[...signing]` section in the source, and write the entire section on publish instead of just filling in the `pubkey`; signatures won't be checked for `--git` installs. Binstall uses this approach. +- Instruct your users to use `--skip-signatures` if they want to install with `--git`. + +## Why not X? (Sigstore, GPG, signify, with SSH keys, ...) + +We're open to pull requests adding algorithms! +We're especially interested in Sigstore for a better implementation of "just-in-time" signing (which it calls "keyless"). +We chose minisign as the first supported algorithm as it's lightweight, fairly popular, and has zero options to choose from. + +## There's a competing project that does package signature verification differently! + +[Tell us about it](https://github.com/cargo-bins/cargo-binstall/issues/1)! +We're not looking to fracture the ecosystem here, and will gladly implement support if something exists already. + +We'll also work with others in the space to eventually formalise this beyond Binstall, for example around the [`dist-manifest.json`](https://crates.io/crates/cargo-dist-schema) metadata format. + +## What's the relationship to crate/registry signing? + +There isn't one. +Crate signing is something we're also interested in, and if/when it materialises we'll add support in Binstall for the bits that concern us, but by nature package signing is not related to (source) crate signing. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 00000000..f18fd17f --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,181 @@ +# Support for `cargo binstall` + +`binstall` works with existing CI-built binary outputs, with configuration via `[package.metadata.binstall]` keys in the relevant crate manifest. +When configuring `binstall` you can test against a local manifest with `--manifest-path=PATH` argument to use the crate and manifest at the provided `PATH`, skipping crate discovery and download. + +To get started, check the [default](#Defaults) first, only add a `[package.metadata.binstall]` section +to your `Cargo.toml` if the default does not work for you. + +As an example, the configuration would be like this: + +```toml +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }{ archive-suffix }" +bin-dir = "{ name }-{ target }-v{ version }/{ bin }{ binary-ext }" +pkg-fmt = "tgz" +disabled-strategies = ["quick-install", "compile"] +``` + +With the following configuration keys: + +- `pkg-url` specifies the package download URL for a given target/version, templated +- `bin-dir` specifies the binary path within the package, templated (with an `.exe` suffix on windows) +- `pkg-fmt` overrides the package format for download/extraction (defaults to: `tgz`), check [the documentation](https://docs.rs/binstalk-types/latest/binstalk_types/cargo_toml_binstall/enum.PkgFmt.html) for all supported formats. +- `disabled-strategies` to disable specific strategies (e.g. `crate-meta-data` for trying to find pre-built on your repository, + `quick-install` for pre-built from third-party cargo-bins/cargo-quickinstall, `compile` for falling back to `cargo-install`) + for your crate (defaults to empty array). + If `--strategies` is passed on the command line, then the `disabled-strategies` in `package.metadata` will be ignored. + Otherwise, the `disabled-strategies` in `package.metadata` and `--disable-strategies` will be merged. + + +`pkg-url` and `bin-dir` are templated to support different names for different versions / architectures / etc. +Template variables use the format `{ VAR }` where `VAR` is the name of the variable, +`\{` for literal `{`, `\}` for literal `}` and `\\` for literal `\`, +with the following variables available: +- `name` is the name of the crate/package +- `version` is the crate version (per `--version` and the crate manifest) +- `repo` is the repository linked in `Cargo.toml` +- `bin` is the name of a specific binary, inferred from the crate configuration +- `target` is the rust target name (defaults to your architecture, but can be overridden using the `--target` command line option if required() +- `archive-suffix` is the filename extension of the package archive format that includes the prefix `.`, e.g. `.tgz` for tgz or `.exe`/`""` for bin. +- `archive-format` is the soft-deprecated filename extension of the package archive format that does not include the prefix `.`, e.g. `tgz` for tgz or `exe`/`""` for bin. +- `binary-ext` is the string `.exe` if the `target` is for Windows, or the empty string otherwise +- `format` is a soft-deprecated alias for `archive-format` in `pkg-url`, and alias for `binary-ext` in `bin-dir`; in the future, this may warn at install time. +- `target-family`: Operating system of the target from [`target_lexicon::OperatingSystem`] +- `target-arch`: Architecture of the target, `universal` on `{universal, universal2}-apple-darwin`, + otherwise from [`target_lexicon::Architecture`] +- `target-libc`: ABI environment of the target from [`target_lexicon::Environment`] +- `target-vendor`: Vendor of the target from [`target_lexicon::Vendor`] + +[`target_lexicon::OperatingSystem`]: https://docs.rs/target-lexicon/latest/target_lexicon/enum.OperatingSystem.html +[`target_lexicon::Architecture`]: https://docs.rs/target-lexicon/latest/target_lexicon/enum.Architecture.html +[`target_lexicon::Environment`]: https://docs.rs/target-lexicon/latest/target_lexicon/enum.Environment.html +[`target_lexicon::Vendor`]: https://docs.rs/target-lexicon/latest/target_lexicon/enum.Vendor.html + +`pkg-url`, `pkg-fmt` and `bin-dir` can be overridden on a per-target basis if required, for example, if your `x86_64-pc-windows-msvc` builds use `zip` archives this could be set via: + +``` +[package.metadata.binstall.overrides.x86_64-pc-windows-msvc] +pkg-fmt = "zip" +``` + +### Defaults + +By default, `binstall` will try all supported package formats and would do the same for `bin-dir`. + +It will first extract the archives, then iterate over the following list, finding the first dir +that exists: + + - `{ name }-{ target }-v{ version }` + - `{ name }-{ target }-{ version }` + - `{ name }-{ version }-{ target }` + - `{ name }-v{ version }-{ target }` + - `{ name }-{ target }` + - `{ name }-{ version }` + - `{ name }-v{ version }` + - `{ name }` + +Then it will concat the dir with `"{ bin }{ binary-ext }"` and use that as the final `bin-dir`. + +`name` here is name of the crate, `bin` is the cargo binary name and `binary-ext` is `.exe` +on windows and empty on other platforms). + +The default value for `pkg-url` will depend on the repository of the package. + +It is set up to work with GitHub releases, GitLab releases, bitbucket downloads +and source forge downloads. + +If your package already uses any of these URLs, you shouldn't need to set anything. + +The URLs are derived from a set of filenames and a set of paths, which are +"multiplied together": every filename appended to every path. The filenames +are: + +- `{ name }-{ target }-{ version }{ archive-suffix }` +- `{ name }-{ target }-v{ version }{ archive-suffix }` +- `{ name }-{ version }-{ target }{ archive-suffix }` +- `{ name }-v{ version }-{ target }{ archive-suffix }` +- `{ name }_{ target }_{ version }{ archive-suffix }` +- `{ name }_{ target }_v{ version }{ archive-suffix }` +- `{ name }_{ version }_{ target }{ archive-suffix }` +- `{ name }_v{ version }_{ target }{ archive-suffix }` +- `{ name }-{ target }{ archive-suffix }` ("versionless") +- `{ name }_{ target }{ archive-suffix }` ("versionless") + +The paths are: + +#### for GitHub + +- `{ repo }/releases/download/{ version }/` +- `{ repo }/releases/download/v{ version }/` + +#### for GitLab + +- `{ repo }/-/releases/{ version }/downloads/binaries/` +- `{ repo }/-/releases/v{ version }/downloads/binaries/` + +Note that this uses the [Permanent links to release assets][gitlab-permalinks] +feature of GitLab EE: it requires you to create an asset as a link with a +`filepath`, which, as of writing, can only be set using GitLab's API. + +[gitlab-permalinks]: https://docs.gitlab.com/ee/user/project/releases/index.html#permanent-links-to-latest-release-assets + +#### for BitBucket + +- `{ repo }/downloads/` + +Binaries must be uploaded to the project's "Downloads" page on BitBucket. + +Also note that as there are no per-release downloads, the "versionless" +filename is not considered here. + +#### for SourceForge + +- `{ repo }/files/binaries/{ version }` +- `{ repo }/files/binaries/v{ version }` + +The URLs also have `/download` appended as per SourceForge's schema. + +Binary must be uploaded to the "File" page of your project, under the directory +`binaries/v{ version }`. + +#### Others + +For all other situations, `binstall` does not provide a default `pkg-url` and +you need to manually specify it. + +### QuickInstall + +[QuickInstall](https://github.com/alsuren/cargo-quickinstall) is an unofficial repository of prebuilt binaries for Crates, and `binstall` has built-in support for it! If your crate is built by QuickInstall, it will already work with `binstall`. However, binaries as configured above take precedence when they exist. + +### Examples + +For example, the default configuration (as shown above) for a crate called `radio-sx128x` (version: `v0.14.1-alpha.5` on x86\_64 linux) would be interpolated to: + +- A download URL of `https://github.com/rust-iot/rust-radio-sx128x/releases/download/v0.14.1-alpha.5/rust-radio-sx128x-x86_64-unknown-linux-gnu-v0.14.1-alpha.5.tgz` +- Containing a single binary file `rust-radio-sx128x-x86_64-unknown-linux-gnu-v0.14.1-alpha.5/rust-radio-x86_64-unknown-linux-gnu` +- Installed to`$HOME/.cargo/bin/rust-radio-sx128x-v0.14.1-alpha.5` +- With a symlink from `$HOME/.cargo/bin/rust-radio-sx128x` + +#### If the package name does not match the crate name + +As is common with libraries/utilities (and the `radio-sx128x` example), this can be overridden by specifying the `pkg-url`: + +```toml +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/sx128x-util-{ target }-v{ version }{ archive-suffix }" +``` + +Which provides a download URL of `https://github.com/rust-iot/rust-radio-sx128x/releases/download/v0.14.1-alpha.5/sx128x-util-x86_64-unknown-linux-gnu-v0.14.1-alpha.5.tgz` + + +#### If the package structure differs from the default + +Were the package to contain binaries in the form `name-target[.exe]`, this could be overridden using the `bin-dir` key: + +```toml +[package.metadata.binstall] +bin-dir = "{ bin }-{ target }{ binary-ext }" +``` + +Which provides a binary path of: `sx128x-util-x86_64-unknown-linux-gnu[.exe]`. It is worth noting that binary names are inferred from the crate, so long as cargo builds them this _should_ just work. diff --git a/action.yml b/action.yml new file mode 100644 index 00000000..8fd7804a --- /dev/null +++ b/action.yml @@ -0,0 +1,16 @@ +name: 'Install cargo-binstall' +description: 'Install the latest version of cargo-binstall tool' + +runs: + using: composite + steps: + - name: Install cargo-binstall + if: runner.os != 'Windows' + shell: sh + run: | + set -eu + (curl --retry 10 -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh || echo 'exit 1') | bash + - name: Install cargo-binstall + if: runner.os == 'Windows' + run: Set-ExecutionPolicy Unrestricted -Scope Process; iex (iwr "https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.ps1").Content + shell: powershell diff --git a/cleanup-cache.sh b/cleanup-cache.sh new file mode 100755 index 00000000..4fcbf6d3 --- /dev/null +++ b/cleanup-cache.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -uxo pipefail + +REPO="${REPO?}" +BRANCH="${BRANCH?}" + +while true; do + echo "Fetching list of cache key for $BRANCH" + cacheKeysForPR="$(gh actions-cache list -R "$REPO" -B "$BRANCH" -L 100 | cut -f 1)" + + if [ -z "$cacheKeysForPR" ]; then + break + fi + + echo "Deleting caches..." + for cacheKey in $cacheKeysForPR + do + echo Removing "$cacheKey" + gh actions-cache delete "$cacheKey" -R "$REPO" -B "$BRANCH" --confirm + done +done +echo "Done cleaning up $BRANCH" diff --git a/crates/atomic-file-install/CHANGELOG.md b/crates/atomic-file-install/CHANGELOG.md new file mode 100644 index 00000000..f49d292d --- /dev/null +++ b/crates/atomic-file-install/CHANGELOG.md @@ -0,0 +1,44 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.0.11](https://github.com/cargo-bins/cargo-binstall/compare/atomic-file-install-v1.0.10...atomic-file-install-v1.0.11) - 2025-03-19 + +### Other + +- *(deps)* bump windows from 0.60.0 to 0.61.1 in the deps group across 1 directory ([#2097](https://github.com/cargo-bins/cargo-binstall/pull/2097)) + +## [1.0.10](https://github.com/cargo-bins/cargo-binstall/compare/atomic-file-install-v1.0.9...atomic-file-install-v1.0.10) - 2025-02-22 + +### Other + +- *(deps)* bump windows from 0.59.0 to 0.60.0 in the deps group across 1 directory (#2063) + +## [1.0.9](https://github.com/cargo-bins/cargo-binstall/compare/atomic-file-install-v1.0.8...atomic-file-install-v1.0.9) - 2025-01-19 + +### Other + +- update Cargo.lock dependencies + +## [1.0.8](https://github.com/cargo-bins/cargo-binstall/compare/atomic-file-install-v1.0.7...atomic-file-install-v1.0.8) - 2025-01-13 + +### Other + +- update Cargo.lock dependencies + +## [1.0.7](https://github.com/cargo-bins/cargo-binstall/compare/atomic-file-install-v1.0.6...atomic-file-install-v1.0.7) - 2025-01-11 + +### Other + +- *(deps)* bump the deps group with 3 updates (#2015) + +## [1.0.6](https://github.com/cargo-bins/cargo-binstall/compare/atomic-file-install-v1.0.5...atomic-file-install-v1.0.6) - 2024-11-18 + +### Other + +- Upgrade transitive dependencies ([#1969](https://github.com/cargo-bins/cargo-binstall/pull/1969)) diff --git a/crates/atomic-file-install/Cargo.toml b/crates/atomic-file-install/Cargo.toml new file mode 100644 index 00000000..2394f0eb --- /dev/null +++ b/crates/atomic-file-install/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "atomic-file-install" +version = "1.0.11" +edition = "2021" +description = "For atomically installing a file or a symlink." +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/atomic-install" +authors = ["Jiahao XU "] +license = "Apache-2.0 OR MIT" +rust-version = "1.65.0" + +[dependencies] +reflink-copy = "0.1.15" +tempfile = "3.5.0" +tracing = "0.1.39" + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.61.1", features = ["Win32_Storage_FileSystem", "Win32_Foundation"] } diff --git a/crates/atomic-file-install/LICENSE-APACHE b/crates/atomic-file-install/LICENSE-APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/crates/atomic-file-install/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/atomic-file-install/LICENSE-MIT b/crates/atomic-file-install/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/crates/atomic-file-install/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/crates/atomic-file-install/src/lib.rs b/crates/atomic-file-install/src/lib.rs new file mode 100644 index 00000000..4f444262 --- /dev/null +++ b/crates/atomic-file-install/src/lib.rs @@ -0,0 +1,219 @@ +//! Atomically install a regular file or a symlink to destination, +//! can be either noclobber (fail if destination already exists) or +//! replacing it atomically if it exists. + +use std::{fs, io, path::Path}; + +use reflink_copy::reflink_or_copy; +use tempfile::{NamedTempFile, TempPath}; +use tracing::{debug, warn}; + +#[cfg(unix)] +use std::os::unix::fs::symlink as symlink_file_inner; + +#[cfg(windows)] +use std::os::windows::fs::symlink_file as symlink_file_inner; + +fn parent(p: &Path) -> io::Result<&Path> { + p.parent().ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("`{}` does not have a parent", p.display()), + ) + }) +} + +fn copy_to_tempfile(src: &Path, dst: &Path) -> io::Result { + let parent = parent(dst)?; + debug!("Creating named tempfile at '{}'", parent.display()); + let tempfile = NamedTempFile::new_in(parent)?; + + debug!( + "Copying from '{}' to '{}'", + src.display(), + tempfile.path().display() + ); + fs::remove_file(tempfile.path())?; + // src and dst is likely to be on the same filesystem. + // Uses reflink if the fs support it, or fallback to + // `fs::copy` if it doesn't support it or it is not on the + // same filesystem. + reflink_or_copy(src, tempfile.path())?; + + debug!("Retrieving permissions of '{}'", src.display()); + let permissions = src.metadata()?.permissions(); + + debug!( + "Setting permissions of '{}' to '{permissions:#?}'", + tempfile.path().display() + ); + tempfile.as_file().set_permissions(permissions)?; + + Ok(tempfile) +} + +/// Install a file, this fails if the `dst` already exists. +/// +/// This is a blocking function, must be called in `block_in_place` mode. +pub fn atomic_install_noclobber(src: &Path, dst: &Path) -> io::Result<()> { + debug!( + "Attempting to rename from '{}' to '{}'.", + src.display(), + dst.display() + ); + + let tempfile = copy_to_tempfile(src, dst)?; + + debug!( + "Persisting '{}' to '{}', fail if dst already exists", + tempfile.path().display(), + dst.display() + ); + tempfile.persist_noclobber(dst)?; + + Ok(()) +} + +/// Atomically install a file, this atomically replace `dst` if it exists. +/// +/// This is a blocking function, must be called in `block_in_place` mode. +pub fn atomic_install(src: &Path, dst: &Path) -> io::Result<()> { + debug!( + "Attempting to atomically rename from '{}' to '{}'", + src.display(), + dst.display() + ); + + if let Err(err) = fs::rename(src, dst) { + warn!("Attempting at atomic rename failed: {err}, fallback to other methods."); + + #[cfg(windows)] + { + match win::replace_file(src, dst) { + Ok(()) => { + debug!("ReplaceFileW succeeded."); + return Ok(()); + } + Err(err) => { + warn!("ReplaceFileW failed: {err}, fallback to using tempfile plus rename") + } + } + } + + // src and dst is not on the same filesystem/mountpoint. + // Fallback to creating NamedTempFile on the parent dir of + // dst. + + persist(copy_to_tempfile(src, dst)?.into_temp_path(), dst)?; + } else { + debug!("Attempting at atomically succeeded."); + } + + Ok(()) +} + +/// Create a symlink at `link` to `dest`, this fails if the `link` +/// already exists. +/// +/// This is a blocking function, must be called in `block_in_place` mode. +pub fn atomic_symlink_file_noclobber(dest: &Path, link: &Path) -> io::Result<()> { + match symlink_file_inner(dest, link) { + Ok(_) => Ok(()), + + #[cfg(windows)] + // Symlinks on Windows are disabled in some editions, so creating one is unreliable. + // Fallback to copy if it fails. + Err(_) => atomic_install_noclobber(dest, link), + + #[cfg(not(windows))] + Err(err) => Err(err), + } +} + +/// Atomically create a symlink at `link` to `dest`, this atomically replace +/// `link` if it already exists. +/// +/// This is a blocking function, must be called in `block_in_place` mode. +pub fn atomic_symlink_file(dest: &Path, link: &Path) -> io::Result<()> { + let parent = parent(link)?; + + debug!("Creating tempPath at '{}'", parent.display()); + let temp_path = NamedTempFile::new_in(parent)?.into_temp_path(); + // Remove this file so that we can create a symlink + // with the name. + fs::remove_file(&temp_path)?; + + debug!( + "Creating symlink '{}' to file '{}'", + temp_path.display(), + dest.display() + ); + + match symlink_file_inner(dest, &temp_path) { + Ok(_) => persist(temp_path, link), + + #[cfg(windows)] + // Symlinks on Windows are disabled in some editions, so creating one is unreliable. + // Fallback to copy if it fails. + Err(_) => atomic_install(dest, link), + + #[cfg(not(windows))] + Err(err) => Err(err), + } +} + +fn persist(temp_path: TempPath, to: &Path) -> io::Result<()> { + debug!("Persisting '{}' to '{}'", temp_path.display(), to.display()); + match temp_path.persist(to) { + Ok(()) => Ok(()), + #[cfg(windows)] + Err(tempfile::PathPersistError { + error, + path: temp_path, + }) => { + warn!( + "Failed to persist symlink '{}' to '{}': {error}, fallback to ReplaceFileW", + temp_path.display(), + to.display(), + ); + win::replace_file(&temp_path, to).map_err(io::Error::from) + } + #[cfg(not(windows))] + Err(err) => Err(err.into()), + } +} + +#[cfg(windows)] +mod win { + use std::{os::windows::ffi::OsStrExt, path::Path}; + + use windows::{ + core::{Error, PCWSTR}, + Win32::Storage::FileSystem::{ReplaceFileW, REPLACE_FILE_FLAGS}, + }; + + pub(super) fn replace_file(src: &Path, dst: &Path) -> Result<(), Error> { + let mut src: Vec<_> = src.as_os_str().encode_wide().collect(); + let mut dst: Vec<_> = dst.as_os_str().encode_wide().collect(); + + // Ensure it is terminated with 0 + src.push(0); + dst.push(0); + + // SAFETY: We use it according its doc + // https://learn.microsoft.com/en-nz/windows/win32/api/winbase/nf-winbase-replacefilew + // + // NOTE that this function is available since windows XP, so we don't need to + // lazily load this function. + unsafe { + ReplaceFileW( + PCWSTR::from_raw(dst.as_ptr()), // lpreplacedfilename + PCWSTR::from_raw(src.as_ptr()), // lpreplacementfilename + PCWSTR::null(), // lpbackupfilename, null for no backup file + REPLACE_FILE_FLAGS(0), // dwreplaceflags + None, // lpexclude, unused + None, // lpreserved, unused + ) + } + } +} diff --git a/crates/bin/Cargo.toml b/crates/bin/Cargo.toml new file mode 100644 index 00000000..2e900655 --- /dev/null +++ b/crates/bin/Cargo.toml @@ -0,0 +1,90 @@ +[package] +name = "cargo-binstall" +description = "Binary installation for rust projects" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/cargo-binstall" +version = "1.13.0" +rust-version = "1.79.0" +authors = ["ryan "] +edition = "2021" +license = "GPL-3.0-only" +readme = "../../README.md" + +# These MUST remain even if they're not needed in recent versions because +# OLD versions use them to upgrade +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ target }.{ archive-format }" +bin-dir = "{ bin }{ binary-ext }" + +[package.metadata.binstall.overrides.x86_64-pc-windows-msvc] +pkg-fmt = "zip" +[package.metadata.binstall.overrides.x86_64-apple-darwin] +pkg-fmt = "zip" + +[dependencies] +atomic-file-install = { version = "1.0.11", path = "../atomic-file-install" } +binstalk = { path = "../binstalk", version = "0.28.36", default-features = false } +binstalk-manifests = { path = "../binstalk-manifests", version = "0.16.1" } +clap = { version = "4.5.3", features = ["derive", "env", "wrap_help"] } +clap-cargo = "0.15.2" +compact_str = "0.9.0" +dirs = "6.0.0" +file-format = { version = "0.27.0", default-features = false } +home = "0.5.9" +log = { version = "0.4.22", features = ["std"] } +miette = "7.0.0" +mimalloc = { version = "0.1.39", default-features = false, optional = true } +once_cell = "1.18.0" +semver = "1.0.17" +strum = "0.27.0" +strum_macros = "0.27.0" +supports-color = "3.0.0" +tempfile = "3.5.0" +tokio = { version = "1.44.0", features = ["rt-multi-thread", "signal"], default-features = false } +tracing = { version = "0.1.39", default-features = false } +tracing-core = "0.1.32" +tracing-log = { version = "0.2.0", default-features = false } +tracing-subscriber = { version = "0.3.17", features = ["fmt", "json", "ansi"], default-features = false } +zeroize = "1.8.1" + +[build-dependencies] +embed-resource = "3.0.1" +vergen = { version = "8.2.7", features = ["build", "cargo", "git", "gitcl", "rustc"] } + +[features] +default = ["static", "rustls", "trust-dns", "fancy-no-backtrace", "zstd-thin", "git"] + +git = ["binstalk/git"] +git-max-perf = ["binstalk/git-max-perf"] + +mimalloc = ["dep:mimalloc"] + +static = ["binstalk/static"] +pkg-config = ["binstalk/pkg-config"] + +zlib-ng = ["binstalk/zlib-ng"] +zlib-rs = ["binstalk/zlib-rs"] + +rustls = ["binstalk/rustls"] +native-tls = ["binstalk/native-tls"] + +trust-dns = ["binstalk/trust-dns"] + +# Experimental HTTP/3 client, this would require `--cfg reqwest_unstable` +# to be passed to `rustc`. +http3 = ["binstalk/http3"] + +zstd-thin = ["binstalk/zstd-thin"] +cross-lang-fat-lto = ["binstalk/cross-lang-fat-lto"] + +fancy-no-backtrace = ["miette/fancy-no-backtrace"] +fancy-with-backtrace = ["fancy-no-backtrace", "miette/fancy"] + +log_max_level_info = ["log/max_level_info", "tracing/max_level_info", "log_release_max_level_info"] +log_max_level_debug = ["log/max_level_debug", "tracing/max_level_debug", "log_release_max_level_debug"] + +log_release_max_level_info = ["log/release_max_level_info", "tracing/release_max_level_info"] +log_release_max_level_debug = ["log/release_max_level_debug", "tracing/release_max_level_debug"] + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] diff --git a/LICENSE.txt b/crates/bin/LICENSE similarity index 100% rename from LICENSE.txt rename to crates/bin/LICENSE diff --git a/crates/bin/build.rs b/crates/bin/build.rs new file mode 100644 index 00000000..617c8d87 --- /dev/null +++ b/crates/bin/build.rs @@ -0,0 +1,52 @@ +use std::{ + io, + path::Path, + process::{Child, Command}, + thread, +}; + +fn succeeds(res: io::Result) -> bool { + res.and_then(|mut child| child.wait()) + .map(|status| status.success()) + .unwrap_or(false) +} + +fn emit_vergen_info() { + let git = Command::new("git").arg("--version").spawn(); + + // .git is usually a dir, but it also can be a file containing + // path to another .git if it is a submodule. + // + // If build.rs is run on a git repository, then ../../.git + // should exists. + let is_git_repo = Path::new("../../.git").exists(); + + let mut builder = vergen::EmitBuilder::builder(); + builder.all_build().all_cargo().all_rustc(); + + if is_git_repo && succeeds(git) { + builder.all_git(); + } else { + builder.disable_git(); + } + + builder.emit().unwrap(); +} + +fn main() { + thread::scope(|s| { + let handle = s.spawn(|| { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=manifest.rc"); + println!("cargo:rerun-if-changed=windows.manifest"); + + embed_resource::compile("manifest.rc", embed_resource::NONE) + .manifest_required() + .unwrap(); + }); + + emit_vergen_info(); + + handle.join().unwrap(); + }); +} diff --git a/crates/bin/manifest.rc b/crates/bin/manifest.rc new file mode 100644 index 00000000..d648a5c7 --- /dev/null +++ b/crates/bin/manifest.rc @@ -0,0 +1,2 @@ +#define RT_MANIFEST 24 +1 RT_MANIFEST "windows.manifest" diff --git a/crates/bin/release.toml b/crates/bin/release.toml new file mode 100644 index 00000000..db2a5847 --- /dev/null +++ b/crates/bin/release.toml @@ -0,0 +1,15 @@ +pre-release-commit-message = "release: cargo-binstall v{{version}}" +tag-prefix = "" +tag-message = "cargo-binstall {{version}}" + +# We wait until the release CI is done before publishing, +# because publishing is irreversible, but a release can be +# reverted a lot more easily. +publish = false + +[[pre-release-replacements]] +file = "windows.manifest" +search = "^ version=\"[\\d.]+[.]0\"" +replace = " version=\"{{version}}.0\"" +prerelease = false +max = 1 diff --git a/crates/bin/src/args.rs b/crates/bin/src/args.rs new file mode 100644 index 00000000..055e1e4e --- /dev/null +++ b/crates/bin/src/args.rs @@ -0,0 +1,738 @@ +use std::{ + env, + ffi::OsString, + fmt, mem, + num::{NonZeroU16, NonZeroU64, ParseIntError}, + path::PathBuf, + str::FromStr, +}; + +use binstalk::{ + helpers::remote, + manifests::cargo_toml_binstall::PkgFmt, + ops::resolve::{CrateName, VersionReqExt}, + registry::Registry, +}; +use binstalk_manifests::cargo_toml_binstall::{PkgOverride, Strategy}; +use clap::{builder::PossibleValue, error::ErrorKind, CommandFactory, Parser, ValueEnum}; +use compact_str::CompactString; +use log::LevelFilter; +use semver::VersionReq; +use strum::EnumCount; +use zeroize::Zeroizing; + +#[derive(Debug, Parser)] +#[clap( + version, + about = "Install a Rust binary... from binaries!", + after_long_help = + "License: GPLv3. Source available at https://github.com/cargo-bins/cargo-binstall\n\n\ + Some crate installation strategies may collect anonymized usage statistics by default. \ + If you prefer not to participate on such data collection, you can opt out by using the \ + `--disable-telemetry` flag or its associated environment variable. For more details \ + about this data collection, please refer to the mentioned flag or the project's README \ + file", + arg_required_else_help(true), + // Avoid conflict with version_req + disable_version_flag(true), + styles = clap_cargo::style::CLAP_STYLING, +)] +pub struct Args { + /// Packages to install. + /// + /// Syntax: `crate[@version]` + /// + /// Each value is either a crate name alone, or a crate name followed by @ and the version to + /// install. The version syntax is as with the --version option. + /// + /// When multiple names are provided, the --version option and override option + /// `--manifest-path` and `--git` are unavailable due to ambiguity. + /// + /// If duplicate names are provided, the last one (and their version requirement) + /// is kept. + #[clap( + help_heading = "Package selection", + value_name = "crate[@version]", + required_unless_present_any = ["version", "self_install", "help"], + )] + pub(crate) crate_names: Vec, + + /// Package version to install. + /// + /// Takes either an exact semver version or a semver version requirement expression, which will + /// be resolved to the highest matching version available. + /// + /// Cannot be used when multiple packages are installed at once, use the attached version + /// syntax in that case. + #[clap( + help_heading = "Package selection", + long = "version", + value_parser(VersionReq::parse_from_cli), + value_name = "VERSION" + )] + pub(crate) version_req: Option, + + /// Override binary target set. + /// + /// Binstall is able to look for binaries for several targets, installing the first one it finds + /// in the order the targets were given. For example, on a 64-bit glibc Linux distribution, the + /// default is to look first for a `x86_64-unknown-linux-gnu` binary, then for a + /// `x86_64-unknown-linux-musl` binary. However, on a musl system, the gnu version will not be + /// considered. + /// + /// This option takes a comma-separated list of target triples, which will be tried in order. + /// They override the default list, which is detected automatically from the current platform. + /// + /// If falling back to installing from source, the first target will be used. + #[clap( + help_heading = "Package selection", + alias = "target", + long, + value_name = "TRIPLE", + env = "CARGO_BUILD_TARGET" + )] + pub(crate) targets: Option>, + + /// Install only the specified binaries. + /// + /// This mirrors the equivalent argument in `cargo install --bin`. + /// + /// If omitted, all binaries are installed. + #[clap( + help_heading = "Package selection", + long, + value_name = "BINARY", + num_args = 1.., + action = clap::ArgAction::Append + )] + pub(crate) bin: Option>, + + /// Override Cargo.toml package manifest path. + /// + /// This skips searching crates.io for a manifest and uses the specified path directly, useful + /// for debugging and when adding Binstall support. This may be either the path to the folder + /// containing a Cargo.toml file, or the Cargo.toml file itself. + /// + /// This option cannot be used with `--git`. + #[clap(help_heading = "Overrides", long, value_name = "PATH")] + pub(crate) manifest_path: Option, + + #[cfg(feature = "git")] + /// Override how to fetch Cargo.toml package manifest. + /// + /// This skip searching crates.io and instead clone the repository specified and + /// runs as if `--manifest-path $cloned_repo` is passed to binstall. + /// + /// This option cannot be used with `--manifest-path`. + #[clap( + help_heading = "Overrides", + long, + conflicts_with("manifest_path"), + value_name = "URL" + )] + pub(crate) git: Option, + + /// Path template for binary files in packages + /// + /// Overrides the Cargo.toml package manifest bin-dir. + #[clap(help_heading = "Overrides", long)] + pub(crate) bin_dir: Option, + + /// Format for package downloads + /// + /// Overrides the Cargo.toml package manifest pkg-fmt. + /// + /// The available package formats are: + /// + /// - tar: download format is TAR (uncompressed) + /// + /// - tbz2: Download format is TAR + Bzip2 + /// + /// - tgz: Download format is TGZ (TAR + GZip) + /// + /// - txz: Download format is TAR + XZ + /// + /// - tzstd: Download format is TAR + Zstd + /// + /// - zip: Download format is Zip + /// + /// - bin: Download format is raw / binary + #[clap(help_heading = "Overrides", long, value_name = "PKG_FMT")] + pub(crate) pkg_fmt: Option, + + /// URL template for package downloads + /// + /// Overrides the Cargo.toml package manifest pkg-url. + #[clap(help_heading = "Overrides", long, value_name = "TEMPLATE")] + pub(crate) pkg_url: Option, + + /// Override the rate limit duration. + /// + /// By default, cargo-binstall allows one request per 10 ms. + /// + /// Example: + /// + /// - `6`: Set the duration to 6ms, allows one request per 6 ms. + /// + /// - `6/2`: Set the duration to 6ms and request_count to 2, + /// allows 2 requests per 6ms. + /// + /// Both duration and request count must not be 0. + #[clap( + help_heading = "Overrides", + long, + default_value_t = RateLimit::default(), + env = "BINSTALL_RATE_LIMIT", + value_name = "LIMIT", + )] + pub(crate) rate_limit: RateLimit, + + /// Specify the strategies to be used, + /// binstall will run the strategies specified in order. + /// + /// If this option is specified, then cargo-binstall will ignore + /// `disabled-strategies` in `package.metadata` in the cargo manifest + /// of the installed packages. + /// + /// Default value is "crate-meta-data,quick-install,compile". + #[clap( + help_heading = "Overrides", + long, + value_delimiter(','), + env = "BINSTALL_STRATEGIES" + )] + pub(crate) strategies: Vec, + + /// Disable the strategies specified. + /// If a strategy is specified in `--strategies` and `--disable-strategies`, + /// then it will be removed. + /// + /// If `--strategies` is not specified, then the strategies specified in this + /// option will be merged with the disabled-strategies` in `package.metadata` + /// in the cargo manifest of the installed packages. + #[clap( + help_heading = "Overrides", + long, + value_delimiter(','), + env = "BINSTALL_DISABLE_STRATEGIES", + value_name = "STRATEGIES" + )] + pub(crate) disable_strategies: Vec, + + /// If `--github-token` or environment variable `GITHUB_TOKEN`/`GH_TOKEN` + /// is not specified, then cargo-binstall will try to extract github token from + /// `$HOME/.git-credentials` or `$HOME/.config/gh/hosts.yml` by default. + /// + /// This option can be used to disable that behavior. + #[clap( + help_heading = "Overrides", + long, + env = "BINSTALL_NO_DISCOVER_GITHUB_TOKEN" + )] + pub(crate) no_discover_github_token: bool, + + /// Maximum time each resolution (one for each possible target and each strategy), in seconds. + #[clap( + help_heading = "Overrides", + long, + env = "BINSTALL_MAXIMUM_RESOLUTION_TIMEOUT", + default_value_t = NonZeroU16::new(15).unwrap(), + value_name = "TIMEOUT" + )] + pub(crate) maximum_resolution_timeout: NonZeroU16, + + /// This flag is now enabled by default thus a no-op. + /// + /// By default, Binstall will install a binary as-is in the install path. + #[clap(help_heading = "Options", long, default_value_t = true)] + pub(crate) no_symlinks: bool, + + /// Dry run, fetch and show changes without installing binaries. + #[clap(help_heading = "Options", long)] + pub(crate) dry_run: bool, + + /// Disable interactive mode / confirmation prompts. + #[clap( + help_heading = "Options", + short = 'y', + long, + env = "BINSTALL_NO_CONFIRM" + )] + pub(crate) no_confirm: bool, + + /// Do not cleanup temporary files. + #[clap(help_heading = "Options", long)] + pub(crate) no_cleanup: bool, + + /// Continue installing other crates even if one of the crate failed to install. + #[clap(help_heading = "Options", long)] + pub(crate) continue_on_failure: bool, + + /// By default, binstall keeps track of the installed packages with metadata files + /// stored in the installation root directory. + /// + /// This flag tells binstall not to use or create that file. + /// + /// With this flag, binstall will refuse to overwrite any existing files unless the + /// `--force` flag is used. + /// + /// This also disables binstall’s ability to protect against multiple concurrent + /// invocations of binstall installing at the same time. + /// + /// This flag will also be passed to `cargo-install` if it is invoked. + #[clap(help_heading = "Options", long)] + pub(crate) no_track: bool, + + /// Disable statistics collection on popular crates. + /// + /// Strategy quick-install (can be disabled via --disable-strategies) collects + /// statistics of popular crates by default, by sending the crate, version, target + /// and status to https://cargo-quickinstall-stats-server.fly.dev/record-install + #[clap(help_heading = "Options", long, env = "BINSTALL_DISABLE_TELEMETRY")] + pub(crate) disable_telemetry: bool, + + /// Install binaries in a custom location. + /// + /// By default, binaries are installed to the global location `$CARGO_HOME/bin`, and global + /// metadata files are updated with the package information. Specifying another path here + /// switches over to a "local" install, where binaries are installed at the path given, and the + /// global metadata files are not updated. + #[clap(help_heading = "Options", long, value_name = "PATH")] + pub(crate) install_path: Option, + + /// Install binaries with a custom cargo root. + /// + /// By default, we use `$CARGO_INSTALL_ROOT` or `$CARGO_HOME` as the + /// cargo root and global metadata files are updated with the + /// package information. + /// + /// Specifying another path here would install the binaries and update + /// the metadata files inside the path you specified. + /// + /// NOTE that `--install-path` takes precedence over this option. + #[clap(help_heading = "Options", long, alias = "roots")] + pub(crate) root: Option, + + /// The URL of the registry index to use. + /// + /// Cannot be used with `--registry`. + #[clap(help_heading = "Options", long)] + pub(crate) index: Option, + + /// Name of the registry to use. Registry names are defined in Cargo config + /// files . + /// + /// If not specified in cmdline or via environment variable, the default + /// registry is used, which is defined by the + /// `registry.default` config key in `.cargo/config.toml` which defaults + /// to crates-io. + /// + /// If it is set, then it will try to read environment variable + /// `CARGO_REGISTRIES_{registry_name}_INDEX` for index url and fallback to + /// reading from `registries..index`. + /// + /// Cannot be used with `--index`. + #[clap( + help_heading = "Options", + long, + env = "CARGO_REGISTRY_DEFAULT", + conflicts_with("index") + )] + pub(crate) registry: Option, + + /// This option will be passed through to all `cargo-install` invocations. + /// + /// It will require `Cargo.lock` to be up to date. + #[clap(help_heading = "Options", long)] + pub(crate) locked: bool, + + /// Deprecated, here for back-compat only. Secure is now on by default. + #[clap(hide(true), long)] + pub(crate) secure: bool, + + /// Force a crate to be installed even if it is already installed. + #[clap(help_heading = "Options", long)] + pub(crate) force: bool, + + /// Require a minimum TLS version from remote endpoints. + /// + /// The default is not to require any minimum TLS version, and use the negotiated highest + /// version available to both this client and the remote server. + #[clap(help_heading = "Options", long, value_enum, value_name = "VERSION")] + pub(crate) min_tls_version: Option, + + /// Specify the root certificates to use for https connnections, + /// in addition to default system-wide ones. + #[clap( + help_heading = "Options", + long, + env = "BINSTALL_HTTPS_ROOT_CERTS", + value_name = "PATH" + )] + pub(crate) root_certificates: Vec, + + /// Print logs in json format to be parsable. + #[clap(help_heading = "Options", long)] + pub json_output: bool, + + /// Provide the github token for accessing the restful API of api.github.com + /// + /// Fallback to environment variable `GITHUB_TOKEN` if this option is not + /// specified (which is also shown by clap's auto generated doc below), or + /// try environment variable `GH_TOKEN`, which is also used by `gh` cli. + /// + /// If none of them is present, then binstall will try to extract github + /// token from `$HOME/.git-credentials` or `$HOME/.config/gh/hosts.yml` + /// unless `--no-discover-github-token` is specified. + #[clap( + help_heading = "Options", + long, + env = "GITHUB_TOKEN", + value_name = "TOKEN" + )] + pub(crate) github_token: Option, + + /// Only install packages that are signed + /// + /// The default is to verify signatures if they are available, but to allow + /// unsigned packages as well. + #[clap(help_heading = "Options", long)] + pub(crate) only_signed: bool, + + /// Don't check any signatures + /// + /// The default is to verify signatures if they are available. This option + /// disables that behaviour entirely, which will also stop downloading + /// signature files in the first place. + /// + /// Note that this is insecure and not recommended outside of testing. + #[clap(help_heading = "Options", long, conflicts_with = "only_signed")] + pub(crate) skip_signatures: bool, + + /// Print version information + #[clap(help_heading = "Meta", short = 'V')] + pub version: bool, + + /// Utility log level + /// + /// Set to `trace` to print very low priority, often extremely + /// verbose information. + /// + /// Set to `debug` when submitting a bug report. + /// + /// Set to `info` to only print useful information. + /// + /// Set to `warn` to only print on hazardous situations. + /// + /// Set to `error` to only print serious errors. + /// + /// Set to `off` to disable logging completely, this will also + /// disable output from `cargo-install`. + /// + /// If `--log-level` is not specified on cmdline, then cargo-binstall + /// will try to read environment variable `BINSTALL_LOG_LEVEL` and + /// interpret it as a log-level. + #[clap(help_heading = "Meta", long, value_name = "LEVEL")] + pub log_level: Option, + + /// Implies `--log-level debug` and it can also be used with `--version` + /// to print out verbose information, + #[clap(help_heading = "Meta", short, long)] + pub verbose: bool, + + /// Equivalent to setting `log_level` to `off`. + /// + /// This would override the `log_level`. + #[clap(help_heading = "Meta", short, long, conflicts_with("verbose"))] + pub(crate) quiet: bool, + + #[clap(long, hide(true))] + pub(crate) self_install: bool, +} + +#[derive(Debug, Clone)] +pub(crate) struct GithubToken(pub(crate) Zeroizing>); + +impl From<&str> for GithubToken { + fn from(s: &str) -> Self { + Self(Zeroizing::new(s.into())) + } +} + +#[derive(Debug, Copy, Clone, ValueEnum)] +pub(crate) enum TLSVersion { + #[clap(name = "1.2")] + Tls1_2, + #[clap(name = "1.3")] + Tls1_3, +} + +impl From for remote::TLSVersion { + fn from(ver: TLSVersion) -> Self { + match ver { + TLSVersion::Tls1_2 => remote::TLSVersion::TLS_1_2, + TLSVersion::Tls1_3 => remote::TLSVersion::TLS_1_3, + } + } +} + +#[derive(Copy, Clone, Debug)] +pub(crate) struct RateLimit { + pub(crate) duration: NonZeroU16, + pub(crate) request_count: NonZeroU64, +} + +impl fmt::Display for RateLimit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}/{}", self.duration, self.request_count) + } +} + +impl FromStr for RateLimit { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + Ok(if let Some((first, second)) = s.split_once('/') { + Self { + duration: first.parse()?, + request_count: second.parse()?, + } + } else { + Self { + duration: s.parse()?, + ..Default::default() + } + }) + } +} + +impl Default for RateLimit { + fn default() -> Self { + Self { + duration: NonZeroU16::new(10).unwrap(), + request_count: NonZeroU64::new(1).unwrap(), + } + } +} + +/// Strategy for installing the package +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub(crate) struct StrategyWrapped(pub(crate) Strategy); + +impl StrategyWrapped { + const VARIANTS: &'static [Self; 3] = &[ + Self(Strategy::CrateMetaData), + Self(Strategy::QuickInstall), + Self(Strategy::Compile), + ]; +} + +impl ValueEnum for StrategyWrapped { + fn value_variants<'a>() -> &'a [Self] { + Self::VARIANTS + } + fn to_possible_value(&self) -> Option { + Some(PossibleValue::new(self.0.to_str())) + } +} + +pub fn parse() -> (Args, PkgOverride) { + // Filter extraneous arg when invoked by cargo + // `cargo run -- --help` gives ["target/debug/cargo-binstall", "--help"] + // `cargo binstall --help` gives ["/home/ryan/.cargo/bin/cargo-binstall", "binstall", "--help"] + let mut args: Vec = env::args_os().collect(); + let args = if args.get(1).map(|arg| arg == "binstall").unwrap_or_default() { + // Equivalent to + // + // args.remove(1); + // + // But is O(1) + args.swap(0, 1); + let mut args = args.into_iter(); + drop(args.next().unwrap()); + + args + } else { + args.into_iter() + }; + + // Load options + let mut opts = Args::parse_from(args); + + if opts.self_install { + return (opts, Default::default()); + } + + if opts.log_level.is_none() { + if let Some(log) = env::var("BINSTALL_LOG_LEVEL") + .ok() + .and_then(|s| s.parse().ok()) + { + opts.log_level = Some(log); + } else if opts.quiet { + opts.log_level = Some(LevelFilter::Off); + } else if opts.verbose { + opts.log_level = Some(LevelFilter::Debug); + } + } + + // Ensure no conflict + let mut command = Args::command(); + + if opts.crate_names.len() > 1 { + let option = if opts.version_req.is_some() { + "version" + } else if opts.manifest_path.is_some() { + "manifest-path" + } else { + #[cfg(not(feature = "git"))] + { + "" + } + + #[cfg(feature = "git")] + if opts.git.is_some() { + "git" + } else { + "" + } + }; + + if !option.is_empty() { + command + .error( + ErrorKind::ArgumentConflict, + format_args!( + r#"override option used with multi package syntax. +You cannot use --{option} and specify multiple packages at the same time. Do one or the other."# + ), + ) + .exit(); + } + } + + // Check strategies for duplicates + let mut new_dup_strategy_err = || { + command.error( + ErrorKind::TooManyValues, + "--strategies should not contain duplicate strategy", + ) + }; + + if opts.strategies.len() > Strategy::COUNT { + // If len of strategies is larger than number of variants of Strategy, + // then there must be duplicates by pigeon hole principle. + new_dup_strategy_err().exit() + } + + // Whether specific variant of Strategy is present + let mut is_variant_present = [false; Strategy::COUNT]; + + for strategy in &opts.strategies { + let index = strategy.0 as u8 as usize; + if is_variant_present[index] { + new_dup_strategy_err().exit() + } else { + is_variant_present[index] = true; + } + } + + let ignore_disabled_strategies = !opts.strategies.is_empty(); + + // Default strategies if empty + if opts.strategies.is_empty() { + opts.strategies = vec![ + StrategyWrapped(Strategy::CrateMetaData), + StrategyWrapped(Strategy::QuickInstall), + StrategyWrapped(Strategy::Compile), + ]; + } + + // Filter out all disabled strategies + if !opts.disable_strategies.is_empty() { + // Since order doesn't matter, we can sort it and remove all duplicates + // to speedup checking. + opts.disable_strategies.sort_unstable(); + opts.disable_strategies.dedup(); + + // disable_strategies.len() <= Strategy::COUNT, of which is faster + // to just use [Strategy]::contains rather than + // [Strategy]::binary_search + opts.strategies + .retain(|strategy| !opts.disable_strategies.contains(strategy)); + + if opts.strategies.is_empty() { + command + .error(ErrorKind::TooFewValues, "You have disabled all strategies") + .exit() + } + } + + // Ensure that Strategy::Compile is specified as the last strategy + if opts.strategies[..(opts.strategies.len() - 1)].contains(&StrategyWrapped(Strategy::Compile)) + { + command + .error( + ErrorKind::InvalidValue, + "Compile strategy must be the last one", + ) + .exit() + } + + if opts.github_token.is_none() { + if let Ok(github_token) = env::var("GH_TOKEN") { + opts.github_token = Some(GithubToken(Zeroizing::new(github_token.into()))); + } + } + match opts.github_token.as_ref() { + Some(token) if token.0.len() < 10 => opts.github_token = None, + _ => (), + } + + let cli_overrides = PkgOverride { + pkg_url: opts.pkg_url.take(), + pkg_fmt: opts.pkg_fmt.take(), + bin_dir: opts.bin_dir.take(), + disabled_strategies: Some( + mem::take(&mut opts.disable_strategies) + .into_iter() + .map(|strategy| strategy.0) + .collect::>() + .into_boxed_slice(), + ), + ignore_disabled_strategies, + signing: None, + }; + + (opts, cli_overrides) +} + +#[cfg(test)] +mod test { + use strum::VariantArray; + + use super::*; + + #[test] + fn verify_cli() { + Args::command().debug_assert() + } + + #[test] + fn quickinstall_url_matches() { + let long_help = Args::command() + .get_opts() + .find(|opt| opt.get_long() == Some("disable-telemetry")) + .unwrap() + .get_long_help() + .unwrap() + .to_string(); + assert!( + long_help.ends_with(binstalk::QUICKINSTALL_STATS_URL), + "{}", + long_help + ); + } + + const _: () = assert!(Strategy::VARIANTS.len() == StrategyWrapped::VARIANTS.len()); +} diff --git a/crates/bin/src/bin_util.rs b/crates/bin/src/bin_util.rs new file mode 100644 index 00000000..9761526f --- /dev/null +++ b/crates/bin/src/bin_util.rs @@ -0,0 +1,64 @@ +use std::{ + process::{ExitCode, Termination}, + time::Duration, +}; + +use binstalk::errors::BinstallError; +use binstalk::helpers::tasks::AutoAbortJoinHandle; +use miette::Result; +use tokio::runtime::Runtime; +use tracing::{error, info}; + +use crate::signal::cancel_on_user_sig_term; + +pub enum MainExit { + Success(Option), + Error(BinstallError), + Report(miette::Report), +} + +impl Termination for MainExit { + fn report(self) -> ExitCode { + match self { + Self::Success(spent) => { + if let Some(spent) = spent { + info!("Done in {spent:?}"); + } + ExitCode::SUCCESS + } + Self::Error(err) => err.report(), + Self::Report(err) => { + error!("Fatal error:\n{err:?}"); + ExitCode::from(16) + } + } + } +} + +impl MainExit { + pub fn new(res: Result<()>, done: Option) -> Self { + res.map(|()| MainExit::Success(done)).unwrap_or_else(|err| { + err.downcast::() + .map(MainExit::Error) + .unwrap_or_else(MainExit::Report) + }) + } +} + +/// This function would start a tokio multithreading runtime, +/// then `block_on` the task it returns. +/// +/// It will cancel the future if user requested cancellation +/// via signal. +pub fn run_tokio_main( + f: impl FnOnce() -> Result>>>, +) -> Result<()> { + let rt = Runtime::new().map_err(BinstallError::from)?; + let _guard = rt.enter(); + + if let Some(handle) = f()? { + rt.block_on(cancel_on_user_sig_term(handle))? + } else { + Ok(()) + } +} diff --git a/crates/bin/src/entry.rs b/crates/bin/src/entry.rs new file mode 100644 index 00000000..87893c3b --- /dev/null +++ b/crates/bin/src/entry.rs @@ -0,0 +1,631 @@ +use std::{ + env, fs, + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; + +use atomic_file_install::atomic_install; +use binstalk::{ + errors::{BinstallError, CrateContextError}, + fetchers::{Fetcher, GhCrateMeta, QuickInstall, SignaturePolicy}, + get_desired_targets, + helpers::{ + jobserver_client::LazyJobserverClient, + lazy_gh_api_client::LazyGhApiClient, + remote::{Certificate, Client}, + tasks::AutoAbortJoinHandle, + }, + ops::{ + self, + resolve::{CrateName, Resolution, ResolutionFetch, VersionReqExt}, + CargoTomlFetchOverride, Options, Resolver, + }, + TARGET, +}; +use binstalk_manifests::{ + cargo_config::Config, + cargo_toml_binstall::{PkgOverride, Strategy}, + crate_info::{CrateInfo, CrateSource}, + crates_manifests::Manifests, +}; +use compact_str::CompactString; +use file_format::FileFormat; +use home::cargo_home; +use log::LevelFilter; +use miette::{miette, Report, Result, WrapErr}; +use semver::Version; +use tokio::task::block_in_place; +use tracing::{debug, error, info, warn}; + +use crate::{args::Args, gh_token, git_credentials, install_path, ui::confirm}; + +pub fn install_crates( + args: Args, + cli_overrides: PkgOverride, + jobserver_client: LazyJobserverClient, +) -> Result>>> { + // Compute Resolvers + let mut cargo_install_fallback = false; + + let resolvers: Vec<_> = args + .strategies + .into_iter() + .filter_map(|strategy| match strategy.0 { + Strategy::CrateMetaData => Some(GhCrateMeta::new as Resolver), + Strategy::QuickInstall => Some(QuickInstall::new as Resolver), + Strategy::Compile => { + cargo_install_fallback = true; + None + } + }) + .collect(); + + // Load .cargo/config.toml + let cargo_home = cargo_home().map_err(BinstallError::from)?; + let mut config = Config::load_from_path(cargo_home.join("config.toml"))?; + + // Compute paths + let cargo_root = args.root; + let (install_path, manifests, temp_dir) = compute_paths_and_load_manifests( + cargo_root.clone(), + args.install_path, + args.no_track, + cargo_home, + &mut config, + )?; + + // Remove installed crates + let mut crate_names = + filter_out_installed_crates(args.crate_names, args.force, manifests.as_ref()).peekable(); + + if crate_names.peek().is_none() { + debug!("Nothing to do"); + return Ok(None); + } + + // Launch target detection + let desired_targets = get_desired_targets(args.targets); + + // Initialize reqwest client + let rate_limit = args.rate_limit; + + let mut http = config.http.take(); + + let client = Client::new( + concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")), + args.min_tls_version.map(|v| v.into()), + rate_limit.duration, + rate_limit.request_count, + read_root_certs( + args.root_certificates, + http.as_mut().and_then(|http| http.cainfo.take()), + ), + ) + .map_err(BinstallError::from)?; + + let gh_api_client = args + .github_token + .map(|token| token.0) + .or_else(|| { + if args.no_discover_github_token { + None + } else { + git_credentials::try_from_home() + } + }) + .map(|token| LazyGhApiClient::new(client.clone(), Some(token))) + .unwrap_or_else(|| { + if args.no_discover_github_token { + LazyGhApiClient::new(client.clone(), None) + } else { + LazyGhApiClient::with_get_gh_token_future(client.clone(), async { + match gh_token::get().await { + Ok(token) => Some(token), + Err(err) => { + debug!(?err, "Failed to retrieve token from `gh auth token`"); + debug!("Failed to read git credential file"); + None + } + } + }) + } + }); + + // Create binstall_opts + let binstall_opts = Arc::new(Options { + no_symlinks: args.no_symlinks, + dry_run: args.dry_run, + force: args.force, + quiet: args.log_level == Some(LevelFilter::Off), + locked: args.locked, + no_track: args.no_track, + + version_req: args.version_req, + #[cfg(feature = "git")] + cargo_toml_fetch_override: match (args.manifest_path, args.git) { + (Some(manifest_path), None) => Some(CargoTomlFetchOverride::Path(manifest_path)), + (None, Some(git_url)) => Some(CargoTomlFetchOverride::Git(git_url)), + (None, None) => None, + _ => unreachable!("manifest_path and git cannot be specified at the same time"), + }, + + #[cfg(not(feature = "git"))] + cargo_toml_fetch_override: args.manifest_path.map(CargoTomlFetchOverride::Path), + cli_overrides, + + desired_targets, + resolvers, + cargo_install_fallback, + bins: args.bin.map(|mut bins| { + bins.sort_unstable(); + bins + }), + + temp_dir: temp_dir.path().to_owned(), + install_path, + cargo_root, + + client, + gh_api_client, + jobserver_client, + registry: if let Some(index) = args.index { + index + } else if let Some(registry_name) = args + .registry + .or_else(|| config.registry.and_then(|registry| registry.default)) + { + let registry_name_lowercase = registry_name.to_lowercase(); + + let v = env::vars().find_map(|(k, v)| { + let name_lowercase = k + .strip_prefix("CARGO_REGISTRIES_")? + .strip_suffix("_INDEX")? + .to_lowercase(); + + (name_lowercase == registry_name_lowercase).then_some(v) + }); + + if let Some(v) = &v { + v + } else { + config + .registries + .as_ref() + .and_then(|registries| registries.get(®istry_name)) + .and_then(|registry| registry.index.as_deref()) + .ok_or_else(|| BinstallError::UnknownRegistryName(registry_name))? + } + .parse() + .map_err(BinstallError::from)? + } else { + Default::default() + }, + + signature_policy: if args.only_signed { + SignaturePolicy::Require + } else if args.skip_signatures { + SignaturePolicy::Ignore + } else { + SignaturePolicy::IfPresent + }, + disable_telemetry: args.disable_telemetry, + + maximum_resolution_timeout: Duration::from_secs( + args.maximum_resolution_timeout.get().into(), + ), + }); + + // Destruct args before any async function to reduce size of the future + let dry_run = args.dry_run; + let no_confirm = args.no_confirm; + let no_cleanup = args.no_cleanup; + + // Resolve crates + let tasks: Vec<_> = crate_names + .map(|(crate_name, current_version)| { + AutoAbortJoinHandle::spawn(ops::resolve::resolve( + binstall_opts.clone(), + crate_name, + current_version, + )) + }) + .collect(); + + Ok(Some(if args.continue_on_failure { + AutoAbortJoinHandle::spawn(async move { + // Collect results + let mut resolution_fetchs = Vec::new(); + let mut resolution_sources = Vec::new(); + let mut errors = Vec::new(); + + for task in tasks { + match task.flattened_join().await { + Ok(Resolution::AlreadyUpToDate) => {} + Ok(Resolution::Fetch(fetch)) => { + fetch.print(&binstall_opts); + resolution_fetchs.push(fetch) + } + Ok(Resolution::InstallFromSource(source)) => { + source.print(); + resolution_sources.push(source) + } + Err(BinstallError::CrateContext(err)) => errors.push(err), + Err(e) => panic!("Expected BinstallError::CrateContext(_), got {}", e), + } + } + + if resolution_fetchs.is_empty() && resolution_sources.is_empty() { + return if let Some(err) = BinstallError::crate_errors(errors) { + Err(err.into()) + } else { + debug!("Nothing to do"); + Ok(()) + }; + } + + // Confirm + if !dry_run && !no_confirm { + if let Err(abort_err) = confirm().await { + return if let Some(err) = BinstallError::crate_errors(errors) { + Err(Report::new(abort_err).wrap_err(err)) + } else { + Err(abort_err.into()) + }; + } + } + + let manifest_update_res = do_install_fetches_continue_on_failure( + resolution_fetchs, + manifests, + &binstall_opts, + dry_run, + temp_dir, + no_cleanup, + &mut errors, + ); + + let tasks: Vec<_> = resolution_sources + .into_iter() + .map(|source| AutoAbortJoinHandle::spawn(source.install(binstall_opts.clone()))) + .collect(); + + for task in tasks { + match task.flattened_join().await { + Ok(_) => (), + Err(BinstallError::CrateContext(err)) => errors.push(err), + Err(e) => panic!("Expected BinstallError::CrateContext(_), got {}", e), + } + } + + match (BinstallError::crate_errors(errors), manifest_update_res) { + (None, Ok(())) => Ok(()), + (None, Err(err)) => Err(err), + (Some(err), Ok(())) => Err(err.into()), + (Some(err), Err(manifest_update_err)) => { + Err(Report::new(err).wrap_err(manifest_update_err)) + } + } + }) + } else { + AutoAbortJoinHandle::spawn(async move { + // Collect results + let mut resolution_fetchs = Vec::new(); + let mut resolution_sources = Vec::new(); + + for task in tasks { + match task.await?? { + Resolution::AlreadyUpToDate => {} + Resolution::Fetch(fetch) => { + fetch.print(&binstall_opts); + resolution_fetchs.push(fetch) + } + Resolution::InstallFromSource(source) => { + source.print(); + resolution_sources.push(source) + } + } + } + + if resolution_fetchs.is_empty() && resolution_sources.is_empty() { + debug!("Nothing to do"); + return Ok(()); + } + + // Confirm + if !dry_run && !no_confirm { + confirm().await?; + } + + do_install_fetches( + resolution_fetchs, + manifests, + &binstall_opts, + dry_run, + temp_dir, + no_cleanup, + )?; + + let tasks: Vec<_> = resolution_sources + .into_iter() + .map(|source| AutoAbortJoinHandle::spawn(source.install(binstall_opts.clone()))) + .collect(); + + for task in tasks { + task.await??; + } + + Ok(()) + }) + })) +} + +fn do_read_root_cert(path: &Path) -> Result, BinstallError> { + use std::io::{Read, Seek}; + + let mut file = fs::File::open(path)?; + let file_format = FileFormat::from_reader(&mut file)?; + + let open_cert = match file_format { + FileFormat::PemCertificate => Certificate::from_pem, + FileFormat::DerCertificate => Certificate::from_der, + _ => { + warn!( + "Unable to load {}: Expected pem or der ceritificate but found {file_format}", + path.display() + ); + + return Ok(None); + } + }; + + // Move file back to its head + file.rewind()?; + + let mut buffer = Vec::with_capacity(200); + file.read_to_end(&mut buffer)?; + + open_cert(&buffer).map_err(From::from).map(Some) +} + +fn read_root_certs( + root_certificate_paths: Vec, + config_cainfo: Option, +) -> impl Iterator { + root_certificate_paths + .into_iter() + .chain(config_cainfo) + .filter_map(|path| match do_read_root_cert(&path) { + Ok(optional_cert) => optional_cert, + Err(err) => { + warn!( + "Failed to load root certificate at {}: {err}", + path.display() + ); + None + } + }) +} + +/// Return (install_path, manifests, temp_dir) +fn compute_paths_and_load_manifests( + roots: Option, + install_path: Option, + no_track: bool, + cargo_home: PathBuf, + config: &mut Config, +) -> Result<(PathBuf, Option, tempfile::TempDir)> { + // Compute cargo_roots + let cargo_roots = + install_path::get_cargo_roots_path(roots, cargo_home, config).ok_or_else(|| { + error!("No viable cargo roots path found of specified, try `--roots`"); + miette!("No cargo roots path found or specified") + })?; + + // Compute install directory + let (install_path, custom_install_path) = + install_path::get_install_path(install_path, Some(&cargo_roots)); + let install_path = install_path.ok_or_else(|| { + error!("No viable install path found of specified, try `--install-path`"); + miette!("No install path found or specified") + })?; + fs::create_dir_all(&install_path).map_err(BinstallError::Io)?; + debug!("Using install path: {}", install_path.display()); + + let no_manifests = no_track || custom_install_path; + + // Load manifests + let manifests = if !no_manifests { + Some(Manifests::open_exclusive(&cargo_roots)?) + } else { + None + }; + + // Create a temporary directory for downloads etc. + // + // Put all binaries to a temporary directory under `dst` first, catching + // some failure modes (e.g., out of space) before touching the existing + // binaries. This directory will get cleaned up via RAII. + let temp_dir = tempfile::Builder::new() + .prefix("cargo-binstall") + .tempdir_in(&install_path) + .map_err(BinstallError::from) + .wrap_err("Creating a temporary directory failed.")?; + + Ok((install_path, manifests, temp_dir)) +} + +/// Return vec of (crate_name, current_version) +fn filter_out_installed_crates<'a>( + crate_names: Vec, + force: bool, + manifests: Option<&'a Manifests>, +) -> impl Iterator)> + 'a { + let installed_crates = manifests.map(|m| m.installed_crates()); + + CrateName::dedup(crate_names) + .filter_map(move |crate_name| { + let name = &crate_name.name; + + let curr_version = installed_crates + // Since crate_name is deduped, every entry of installed_crates + // can be visited at most once. + // + // So here we take ownership of the version stored to avoid cloning. + .and_then(|crates| crates.get(name)); + + match ( + force, + curr_version, + &crate_name.version_req, + ) { + (false, Some(curr_version), Some(version_req)) + if version_req.is_latest_compatible(curr_version) => + { + debug!("Bailing out early because we can assume wanted is already installed from metafile"); + info!("{name} v{curr_version} is already installed, use --force to override"); + None + } + + // The version req is "*" thus a remote upgraded version could exist + (false, Some(curr_version), None) => { + Some((crate_name, Some(curr_version.clone()))) + } + + _ => Some((crate_name, None)), + } + }) +} + +#[allow(clippy::vec_box)] +fn do_install_fetches( + resolution_fetchs: Vec>, + // Take manifests by value to drop the `FileLock`. + manifests: Option, + binstall_opts: &Options, + dry_run: bool, + temp_dir: tempfile::TempDir, + no_cleanup: bool, +) -> Result<()> { + if resolution_fetchs.is_empty() { + return Ok(()); + } + + if dry_run { + info!("Dry-run: Not proceeding to install fetched binaries"); + return Ok(()); + } + + block_in_place(|| { + let metadata_vec = resolution_fetchs + .into_iter() + .map(|fetch| fetch.install(binstall_opts)) + .collect::, BinstallError>>()?; + + if let Some(manifests) = manifests { + manifests.update(metadata_vec)?; + } + + if no_cleanup { + // Consume temp_dir without removing it from fs. + let _ = temp_dir.keep(); + } else { + temp_dir.close().unwrap_or_else(|err| { + warn!("Failed to clean up some resources: {err}"); + }); + } + + Ok(()) + }) +} + +#[allow(clippy::vec_box)] +fn do_install_fetches_continue_on_failure( + resolution_fetchs: Vec>, + // Take manifests by value to drop the `FileLock`. + manifests: Option, + binstall_opts: &Options, + dry_run: bool, + temp_dir: tempfile::TempDir, + no_cleanup: bool, + errors: &mut Vec>, +) -> Result<()> { + if resolution_fetchs.is_empty() { + return Ok(()); + } + + if dry_run { + info!("Dry-run: Not proceeding to install fetched binaries"); + return Ok(()); + } + + block_in_place(|| { + let metadata_vec = resolution_fetchs + .into_iter() + .filter_map(|fetch| match fetch.install(binstall_opts) { + Ok(crate_info) => Some(crate_info), + Err(BinstallError::CrateContext(err)) => { + errors.push(err); + None + } + Err(e) => panic!("Expected BinstallError::CrateContext(_), got {}", e), + }) + .collect::>(); + + if let Some(manifests) = manifests { + manifests.update(metadata_vec)?; + } + + if no_cleanup { + // Consume temp_dir without removing it from fs. + let _ = temp_dir.keep(); + } else { + temp_dir.close().unwrap_or_else(|err| { + warn!("Failed to clean up some resources: {err}"); + }); + } + + Ok(()) + }) +} + +pub fn self_install(args: Args) -> Result<()> { + // Load .cargo/config.toml + let cargo_home = cargo_home().map_err(BinstallError::from)?; + let mut config = Config::load_from_path(cargo_home.join("config.toml"))?; + + // Compute paths + let cargo_root = args.root; + let (install_path, manifests, _) = compute_paths_and_load_manifests( + cargo_root.clone(), + args.install_path, + args.no_track, + cargo_home, + &mut config, + )?; + + let mut dest = install_path.join("cargo-binstall"); + if cfg!(windows) { + assert!(dest.set_extension("exe")); + } + + atomic_install(&env::current_exe().map_err(BinstallError::from)?, &dest) + .map_err(BinstallError::from)?; + + if let Some(manifests) = manifests { + manifests.update(vec![CrateInfo { + name: CompactString::const_new("cargo-binstall"), + version_req: CompactString::const_new("*"), + current_version: Version::new( + env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(), + env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(), + env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(), + ), + source: CrateSource::cratesio_registry(), + target: CompactString::const_new(TARGET), + bins: vec![CompactString::const_new("cargo-binstall")], + }])?; + } + + Ok(()) +} diff --git a/crates/bin/src/gh_token.rs b/crates/bin/src/gh_token.rs new file mode 100644 index 00000000..28659f76 --- /dev/null +++ b/crates/bin/src/gh_token.rs @@ -0,0 +1,94 @@ +use std::{ + io, + process::{Output, Stdio}, + str, +}; + +use tokio::{io::AsyncWriteExt, process::Command}; +use zeroize::{Zeroize, Zeroizing}; + +pub(super) async fn get() -> io::Result>> { + let output = Command::new("gh") + .args(["auth", "token"]) + .stdout_with_optional_input(None) + .await?; + + if !output.is_empty() { + return Ok(output); + } + + Command::new("git") + .args(["credential", "fill"]) + .stdout_with_optional_input(Some("host=github.com\nprotocol=https".as_bytes())) + .await? + .lines() + .find_map(|line| { + line.trim() + .strip_prefix("password=") + .map(|token| Zeroizing::new(token.into())) + }) + .ok_or_else(|| io::Error::other("Password not found in `git credential fill` output")) +} + +trait CommandExt { + // Helper function to execute a command, optionally with input + async fn stdout_with_optional_input( + &mut self, + input: Option<&[u8]>, + ) -> io::Result>>; +} + +impl CommandExt for Command { + async fn stdout_with_optional_input( + &mut self, + input: Option<&[u8]>, + ) -> io::Result>> { + self.stdout(Stdio::piped()) + .stderr(Stdio::null()) + .stdin(if input.is_some() { + Stdio::piped() + } else { + Stdio::null() + }); + + let mut child = self.spawn()?; + + if let Some(input) = input { + child.stdin.take().unwrap().write_all(input).await?; + } + + let Output { status, stdout, .. } = child.wait_with_output().await?; + + if status.success() { + let s = String::from_utf8(stdout).map_err(|err| { + let msg = format!( + "Invalid output for `{:?}`, expected utf8: {err}", + self.as_std() + ); + + zeroize_and_drop(err.into_bytes()); + + io::Error::new(io::ErrorKind::InvalidData, msg) + })?; + + let trimmed = s.trim(); + + Ok(if trimmed.len() == s.len() { + Zeroizing::new(s.into_boxed_str()) + } else { + Zeroizing::new(trimmed.into()) + }) + } else { + zeroize_and_drop(stdout); + + Err(io::Error::other(format!( + "`{:?}` process exited with `{status}`", + self.as_std() + ))) + } + } +} + +fn zeroize_and_drop(mut bytes: Vec) { + bytes.zeroize(); +} diff --git a/crates/bin/src/git_credentials.rs b/crates/bin/src/git_credentials.rs new file mode 100644 index 00000000..9c9a35ba --- /dev/null +++ b/crates/bin/src/git_credentials.rs @@ -0,0 +1,66 @@ +use std::{env, fs, path::PathBuf}; + +use dirs::home_dir; +use zeroize::Zeroizing; + +pub fn try_from_home() -> Option>> { + if let Some(mut home) = home_dir() { + home.push(".git-credentials"); + if let Some(cred) = from_file(home) { + return Some(cred); + } + } + + if let Some(home) = env::var_os("XDG_CONFIG_HOME") { + let mut home = PathBuf::from(home); + home.push("git/credentials"); + + if let Some(cred) = from_file(home) { + return Some(cred); + } + } + + None +} + +fn from_file(path: PathBuf) -> Option>> { + Zeroizing::new(fs::read_to_string(path).ok()?) + .lines() + .find_map(from_line) + .map(Box::::from) + .map(Zeroizing::new) +} + +fn from_line(line: &str) -> Option<&str> { + let cred = line + .trim() + .strip_prefix("https://")? + .strip_suffix("@github.com")?; + + Some(cred.split_once(':')?.1) +} + +#[cfg(test)] +mod test { + use super::*; + + const GIT_CREDENTIALS_TEST_CASES: &[(&str, Option<&str>)] = &[ + // Success + ("https://NobodyXu:gho_asdc@github.com", Some("gho_asdc")), + ( + "https://NobodyXu:gho_asdc12dz@github.com", + Some("gho_asdc12dz"), + ), + // Failure + ("http://NobodyXu:gho_asdc@github.com", None), + ("https://NobodyXu:gho_asdc@gitlab.com", None), + ("https://NobodyXugho_asdc@github.com", None), + ]; + + #[test] + fn test_extract_from_line() { + GIT_CREDENTIALS_TEST_CASES.iter().for_each(|(line, res)| { + assert_eq!(from_line(line), *res); + }) + } +} diff --git a/crates/bin/src/install_path.rs b/crates/bin/src/install_path.rs new file mode 100644 index 00000000..b5dea041 --- /dev/null +++ b/crates/bin/src/install_path.rs @@ -0,0 +1,56 @@ +use std::{ + env::var_os, + path::{Path, PathBuf}, +}; + +use binstalk_manifests::cargo_config::Config; +use tracing::debug; + +pub fn get_cargo_roots_path( + cargo_roots: Option, + cargo_home: PathBuf, + config: &mut Config, +) -> Option { + if let Some(p) = cargo_roots { + Some(p) + } else if let Some(p) = var_os("CARGO_INSTALL_ROOT") { + // Environmental variables + let p = PathBuf::from(p); + debug!("using CARGO_INSTALL_ROOT ({})", p.display()); + Some(p) + } else if let Some(root) = config.install.take().and_then(|install| install.root) { + debug!("using `install.root` {} from cargo config", root.display()); + Some(root) + } else { + debug!("using ({}) as cargo home", cargo_home.display()); + Some(cargo_home) + } +} + +/// Fetch install path from environment +/// roughly follows +/// +/// Return (install_path, is_custom_install_path) +pub fn get_install_path( + install_path: Option, + cargo_roots: Option>, +) -> (Option, bool) { + // Command line override first first + if let Some(p) = install_path { + return (Some(p), true); + } + + // Then cargo_roots + if let Some(p) = cargo_roots { + return (Some(p.as_ref().join("bin")), false); + } + + // Local executable dir if no cargo is found + let dir = dirs::executable_dir(); + + if let Some(d) = &dir { + debug!("Fallback to {}", d.display()); + } + + (dir, true) +} diff --git a/crates/bin/src/lib.rs b/crates/bin/src/lib.rs new file mode 100644 index 00000000..1e25d408 --- /dev/null +++ b/crates/bin/src/lib.rs @@ -0,0 +1,14 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +mod args; +mod bin_util; +mod entry; +mod gh_token; +mod git_credentials; +mod install_path; +mod logging; +mod main_impl; +mod signal; +mod ui; + +pub use main_impl::do_main; diff --git a/crates/bin/src/logging.rs b/crates/bin/src/logging.rs new file mode 100644 index 00000000..320f266a --- /dev/null +++ b/crates/bin/src/logging.rs @@ -0,0 +1,248 @@ +use std::{ + cmp::min, + io::{self, Write}, + iter::repeat, +}; + +use log::{LevelFilter, Log, STATIC_MAX_LEVEL}; +use once_cell::sync::Lazy; +use supports_color::{on as supports_color_on_stream, Stream::Stdout}; +use tracing::{ + callsite::Callsite, + dispatcher, field, + subscriber::{self, set_global_default}, + Event, Level, Metadata, +}; +use tracing_core::{identify_callsite, metadata::Kind, subscriber::Subscriber}; +use tracing_log::AsTrace; +use tracing_subscriber::{ + filter::targets::Targets, + fmt::{fmt, MakeWriter}, + layer::SubscriberExt, +}; + +// Shamelessly taken from tracing-log + +struct Fields { + message: field::Field, +} + +static FIELD_NAMES: &[&str] = &["message"]; + +impl Fields { + fn new(cs: &'static dyn Callsite) -> Self { + let fieldset = cs.metadata().fields(); + let message = fieldset.field("message").unwrap(); + Fields { message } + } +} + +macro_rules! log_cs { + ($level:expr, $cs:ident, $meta:ident, $fields:ident, $ty:ident) => { + struct $ty; + static $cs: $ty = $ty; + static $meta: Metadata<'static> = Metadata::new( + "log event", + "log", + $level, + None, + None, + None, + field::FieldSet::new(FIELD_NAMES, identify_callsite!(&$cs)), + Kind::EVENT, + ); + static $fields: Lazy = Lazy::new(|| Fields::new(&$cs)); + + impl Callsite for $ty { + fn set_interest(&self, _: subscriber::Interest) {} + fn metadata(&self) -> &'static Metadata<'static> { + &$meta + } + } + }; +} + +log_cs!( + Level::TRACE, + TRACE_CS, + TRACE_META, + TRACE_FIELDS, + TraceCallsite +); +log_cs!( + Level::DEBUG, + DEBUG_CS, + DEBUG_META, + DEBUG_FIELDS, + DebugCallsite +); +log_cs!(Level::INFO, INFO_CS, INFO_META, INFO_FIELDS, InfoCallsite); +log_cs!(Level::WARN, WARN_CS, WARN_META, WARN_FIELDS, WarnCallsite); +log_cs!( + Level::ERROR, + ERROR_CS, + ERROR_META, + ERROR_FIELDS, + ErrorCallsite +); + +fn loglevel_to_cs(level: log::Level) -> (&'static Fields, &'static Metadata<'static>) { + match level { + log::Level::Trace => (&*TRACE_FIELDS, &TRACE_META), + log::Level::Debug => (&*DEBUG_FIELDS, &DEBUG_META), + log::Level::Info => (&*INFO_FIELDS, &INFO_META), + log::Level::Warn => (&*WARN_FIELDS, &WARN_META), + log::Level::Error => (&*ERROR_FIELDS, &ERROR_META), + } +} + +struct Logger; + +impl Logger { + fn init(log_level: LevelFilter) { + log::set_max_level(log_level); + log::set_logger(&Self).unwrap(); + } +} + +impl Log for Logger { + fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { + if metadata.level() > log::max_level() { + // First, check the log record against the current max level enabled. + false + } else { + // Check if the current `tracing` dispatcher cares about this. + dispatcher::get_default(|dispatch| dispatch.enabled(&metadata.as_trace())) + } + } + + fn log(&self, record: &log::Record<'_>) { + // Dispatch manually instead of using methods provided by tracing-log + // to avoid having fields "log.target = ..." in the log message, + // which makes the log really hard to read. + if self.enabled(record.metadata()) { + dispatcher::get_default(|dispatch| { + let (keys, meta) = loglevel_to_cs(record.level()); + + dispatch.event(&Event::new( + meta, + &meta + .fields() + .value_set(&[(&keys.message, Some(record.args() as &dyn field::Value))]), + )); + }); + } + } + + fn flush(&self) {} +} + +struct ErrorFreeWriter; + +fn report_err(err: io::Error) { + writeln!(io::stderr(), "Failed to write to stdout: {err}").ok(); +} + +impl io::Write for &ErrorFreeWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + io::stdout().write(buf).or_else(|err| { + report_err(err); + // Behave as if writing to /dev/null so that logging system + // would keep working. + Ok(buf.len()) + }) + } + + fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { + io::stdout().write_all(buf).or_else(|err| { + report_err(err); + // Behave as if writing to /dev/null so that logging system + // would keep working. + Ok(()) + }) + } + + fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result { + io::stdout().write_vectored(bufs).or_else(|err| { + report_err(err); + // Behave as if writing to /dev/null so that logging system + // would keep working. + Ok(bufs.iter().map(|io_slice| io_slice.len()).sum()) + }) + } + + fn flush(&mut self) -> io::Result<()> { + io::stdout().flush().or_else(|err| { + report_err(err); + // Behave as if writing to /dev/null so that logging system + // would keep working. + Ok(()) + }) + } +} + +impl<'a> MakeWriter<'a> for ErrorFreeWriter { + type Writer = &'a Self; + + fn make_writer(&'a self) -> Self::Writer { + self + } +} + +pub fn logging(log_level: LevelFilter, json_output: bool) { + // Calculate log_level + let log_level = min(log_level, STATIC_MAX_LEVEL); + + let allowed_targets = (log_level != LevelFilter::Trace).then_some([ + "atomic_file_install", + "binstalk", + "binstalk_bins", + "binstalk_downloader", + "binstalk_fetchers", + "binstalk_registry", + "cargo_binstall", + "cargo_toml_workspace", + "detect_targets", + "simple_git", + ]); + + // Forward log to tracing + Logger::init(log_level); + + // Build fmt subscriber + let log_level = log_level.as_trace(); + let subscriber_builder = fmt().with_max_level(log_level).with_writer(ErrorFreeWriter); + + let subscriber: Box = if json_output { + Box::new(subscriber_builder.json().finish()) + } else { + // Disable time, target, file, line_num, thread name/ids to make the + // output more readable + let subscriber_builder = subscriber_builder + .without_time() + .with_target(false) + .with_file(false) + .with_line_number(false) + .with_thread_names(false) + .with_thread_ids(false); + + // subscriber_builder defaults to write to io::stdout(), + // so tests whether it supports color. + let stdout_supports_color = supports_color_on_stream(Stdout) + .map(|color_level| color_level.has_basic) + .unwrap_or_default(); + + Box::new(subscriber_builder.with_ansi(stdout_supports_color).finish()) + }; + + // Builder layer for filtering + let filter_layer = allowed_targets.map(|allowed_targets| { + Targets::new().with_targets(allowed_targets.into_iter().zip(repeat(log_level))) + }); + + // Builder final subscriber with filtering + let subscriber = subscriber.with(filter_layer); + + // Setup global subscriber + set_global_default(subscriber).unwrap(); +} diff --git a/crates/bin/src/main.rs b/crates/bin/src/main.rs new file mode 100644 index 00000000..7733f5df --- /dev/null +++ b/crates/bin/src/main.rs @@ -0,0 +1,11 @@ +use std::process::Termination; + +use cargo_binstall::do_main; + +#[cfg(feature = "mimalloc")] +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + +fn main() -> impl Termination { + do_main() +} diff --git a/crates/bin/src/main_impl.rs b/crates/bin/src/main_impl.rs new file mode 100644 index 00000000..51d29ccc --- /dev/null +++ b/crates/bin/src/main_impl.rs @@ -0,0 +1,66 @@ +use std::{process::Termination, time::Instant}; + +use binstalk::{helpers::jobserver_client::LazyJobserverClient, TARGET}; +use log::LevelFilter; +use tracing::debug; + +use crate::{ + args, + bin_util::{run_tokio_main, MainExit}, + entry, + logging::logging, +}; + +pub fn do_main() -> impl Termination { + let (args, cli_overrides) = args::parse(); + + if args.version { + let cargo_binstall_version = env!("CARGO_PKG_VERSION"); + if args.verbose { + let build_date = env!("VERGEN_BUILD_DATE"); + + let features = env!("VERGEN_CARGO_FEATURES"); + + let git_sha = option_env!("VERGEN_GIT_SHA").unwrap_or("UNKNOWN"); + let git_commit_date = option_env!("VERGEN_GIT_COMMIT_DATE").unwrap_or("UNKNOWN"); + + let rustc_semver = env!("VERGEN_RUSTC_SEMVER"); + let rustc_commit_hash = env!("VERGEN_RUSTC_COMMIT_HASH"); + let rustc_llvm_version = env!("VERGEN_RUSTC_LLVM_VERSION"); + + println!( + r#"cargo-binstall: {cargo_binstall_version} +build-date: {build_date} +build-target: {TARGET} +build-features: {features} +build-commit-hash: {git_sha} +build-commit-date: {git_commit_date} +rustc-version: {rustc_semver} +rustc-commit-hash: {rustc_commit_hash} +rustc-llvm-version: {rustc_llvm_version}"# + ); + } else { + println!("{cargo_binstall_version}"); + } + MainExit::Success(None) + } else if args.self_install { + MainExit::new(entry::self_install(args), None) + } else { + logging( + args.log_level.unwrap_or(LevelFilter::Info), + args.json_output, + ); + + let start = Instant::now(); + + let jobserver_client = LazyJobserverClient::new(); + + let result = + run_tokio_main(|| entry::install_crates(args, cli_overrides, jobserver_client)); + + let done = start.elapsed(); + debug!("run time: {done:?}"); + + MainExit::new(result, Some(done)) + } +} diff --git a/crates/bin/src/signal.rs b/crates/bin/src/signal.rs new file mode 100644 index 00000000..907fdfe8 --- /dev/null +++ b/crates/bin/src/signal.rs @@ -0,0 +1,84 @@ +use std::io; + +use binstalk::{errors::BinstallError, helpers::tasks::AutoAbortJoinHandle}; +use tokio::signal; + +/// This function will poll the handle while listening for ctrl_c, +/// `SIGINT`, `SIGHUP`, `SIGTERM` and `SIGQUIT`. +/// +/// When signal is received, [`BinstallError::UserAbort`] will be returned. +/// +/// It would also ignore `SIGUSER1` and `SIGUSER2` on unix. +/// +/// This function uses [`tokio::signal`] and once exit, does not reset the default +/// signal handler, so be careful when using it. +pub async fn cancel_on_user_sig_term( + handle: AutoAbortJoinHandle, +) -> Result { + ignore_signals()?; + + tokio::select! { + biased; + + res = wait_on_cancellation_signal() => { + res.map_err(BinstallError::Io) + .and(Err(BinstallError::UserAbort)) + } + res = handle => res, + } +} + +fn ignore_signals() -> io::Result<()> { + #[cfg(unix)] + unix::ignore_signals_on_unix()?; + + Ok(()) +} + +/// If call to it returns `Ok(())`, then all calls to this function after +/// that also returns `Ok(())`. +async fn wait_on_cancellation_signal() -> Result<(), io::Error> { + #[cfg(unix)] + unix::wait_on_cancellation_signal_unix().await?; + + #[cfg(not(unix))] + signal::ctrl_c().await?; + + Ok(()) +} + +#[cfg(unix)] +mod unix { + use super::*; + use signal::unix::{signal, SignalKind}; + + /// Same as [`wait_on_cancellation_signal`] but is only available on unix. + pub async fn wait_on_cancellation_signal_unix() -> Result<(), io::Error> { + tokio::select! { + biased; + + res = wait_for_signal_unix(SignalKind::interrupt()) => res, + res = wait_for_signal_unix(SignalKind::hangup()) => res, + res = wait_for_signal_unix(SignalKind::terminate()) => res, + res = wait_for_signal_unix(SignalKind::quit()) => res, + } + } + + /// Wait for first arrival of signal. + pub async fn wait_for_signal_unix(kind: signal::unix::SignalKind) -> Result<(), io::Error> { + let mut sig_listener = signal::unix::signal(kind)?; + if sig_listener.recv().await.is_some() { + Ok(()) + } else { + // Use pending() here for the same reason as above. + std::future::pending().await + } + } + + pub fn ignore_signals_on_unix() -> Result<(), io::Error> { + drop(signal(SignalKind::user_defined1())?); + drop(signal(SignalKind::user_defined2())?); + + Ok(()) + } +} diff --git a/crates/bin/src/ui.rs b/crates/bin/src/ui.rs new file mode 100644 index 00000000..bcbb67e1 --- /dev/null +++ b/crates/bin/src/ui.rs @@ -0,0 +1,56 @@ +use std::{ + io::{self, BufRead, StdinLock, Write}, + thread, +}; + +use binstalk::errors::BinstallError; +use tokio::sync::oneshot; + +fn ask_for_confirm(stdin: &mut StdinLock, input: &mut String) -> io::Result<()> { + { + let mut stdout = io::stdout().lock(); + + write!(&mut stdout, "Do you wish to continue? [yes]/no\n? ")?; + stdout.flush()?; + } + + stdin.read_line(input)?; + + Ok(()) +} + +pub async fn confirm() -> Result<(), BinstallError> { + let (tx, rx) = oneshot::channel(); + + thread::spawn(move || { + // This task should be the only one able to + // access stdin + let mut stdin = io::stdin().lock(); + let mut input = String::with_capacity(16); + + let res = loop { + if ask_for_confirm(&mut stdin, &mut input).is_err() { + break false; + } + + match input.as_str().trim() { + "yes" | "y" | "YES" | "Y" | "" => break true, + "no" | "n" | "NO" | "N" => break false, + _ => { + input.clear(); + continue; + } + } + }; + + // The main thread might be terminated by signal and thus cancelled + // the confirmation. + tx.send(res).ok(); + }); + + if rx.await.unwrap() { + Ok(()) + } else { + Err(BinstallError::UserAbort) + } +} diff --git a/crates/bin/windows.manifest b/crates/bin/windows.manifest new file mode 100644 index 00000000..7ad52742 --- /dev/null +++ b/crates/bin/windows.manifest @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + true + UTF-8 + SegmentHeap + + + diff --git a/crates/binstalk-bins/CHANGELOG.md b/crates/binstalk-bins/CHANGELOG.md new file mode 100644 index 00000000..909f0ae8 --- /dev/null +++ b/crates/binstalk-bins/CHANGELOG.md @@ -0,0 +1,96 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.6.14](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.13...binstalk-bins-v0.6.14) - 2025-06-06 + +### Other + +- updated the following local packages: binstalk-types + +## [0.6.13](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.12...binstalk-bins-v0.6.13) - 2025-03-19 + +### Other + +- updated the following local packages: atomic-file-install + +## [0.6.12](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.11...binstalk-bins-v0.6.12) - 2025-03-07 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#2072](https://github.com/cargo-bins/cargo-binstall/pull/2072)) + +## [0.6.11](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.10...binstalk-bins-v0.6.11) - 2025-02-22 + +### Other + +- updated the following local packages: atomic-file-install + +## [0.6.10](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.9...binstalk-bins-v0.6.10) - 2025-02-11 + +### Other + +- updated the following local packages: binstalk-types + +## [0.6.9](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.8...binstalk-bins-v0.6.9) - 2025-01-19 + +### Other + +- update Cargo.lock dependencies + +## [0.6.8](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.7...binstalk-bins-v0.6.8) - 2025-01-13 + +### Other + +- update Cargo.lock dependencies + +## [0.6.7](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.6...binstalk-bins-v0.6.7) - 2025-01-11 + +### Other + +- *(deps)* bump the deps group with 3 updates (#2015) + +## [0.6.6](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.5...binstalk-bins-v0.6.6) - 2024-12-14 + +### Other + +- *(deps)* bump the deps group with 2 updates (#1997) + +## [0.6.5](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.4...binstalk-bins-v0.6.5) - 2024-11-23 + +### Other + +- updated the following local packages: binstalk-types + +## [0.6.4](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.3...binstalk-bins-v0.6.4) - 2024-11-18 + +### Other + +- updated the following local packages: atomic-file-install + +## [0.6.3](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.2...binstalk-bins-v0.6.3) - 2024-11-09 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1966](https://github.com/cargo-bins/cargo-binstall/pull/1966)) + +## [0.6.2](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.1...binstalk-bins-v0.6.2) - 2024-11-05 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1954](https://github.com/cargo-bins/cargo-binstall/pull/1954)) + +## [0.6.1](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.0...binstalk-bins-v0.6.1) - 2024-11-02 + +### Other + +- Improve UI orompt for installation ([#1950](https://github.com/cargo-bins/cargo-binstall/pull/1950)) + +## [0.6.0](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.5.0...binstalk-bins-v0.6.0) - 2024-08-10 + +### Other +- updated the following local packages: binstalk-types diff --git a/crates/binstalk-bins/Cargo.toml b/crates/binstalk-bins/Cargo.toml new file mode 100644 index 00000000..f2dbc956 --- /dev/null +++ b/crates/binstalk-bins/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "binstalk-bins" +version = "0.6.14" +edition = "2021" + +description = "The binstall binaries discovery and installation crate." +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/binstalk-bins" +rust-version = "1.65.0" +authors = ["Jiahao XU "] +license = "GPL-3.0-only" + +[dependencies] +atomic-file-install = { version = "1.0.11", path = "../atomic-file-install" } +binstalk-types = { version = "0.10.0", path = "../binstalk-types" } +compact_str = { version = "0.9.0", features = ["serde"] } +leon = "3.0.0" +miette = "7.0.0" +normalize-path = { version = "0.2.1", path = "../normalize-path" } +thiserror = "2.0.11" +tracing = "0.1.39" diff --git a/crates/binstalk-bins/LICENSE b/crates/binstalk-bins/LICENSE new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/crates/binstalk-bins/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/crates/binstalk-bins/src/lib.rs b/crates/binstalk-bins/src/lib.rs new file mode 100644 index 00000000..15f92a25 --- /dev/null +++ b/crates/binstalk-bins/src/lib.rs @@ -0,0 +1,369 @@ +use std::{ + borrow::Cow, + fmt, io, + path::{self, Component, Path, PathBuf}, +}; + +use atomic_file_install::{ + atomic_install, atomic_install_noclobber, atomic_symlink_file, atomic_symlink_file_noclobber, +}; +use binstalk_types::cargo_toml_binstall::{PkgFmt, PkgMeta}; +use compact_str::{format_compact, CompactString}; +use leon::Template; +use miette::Diagnostic; +use normalize_path::NormalizePath; +use thiserror::Error as ThisError; +use tracing::debug; + +#[derive(Debug, ThisError, Diagnostic)] +pub enum Error { + /// bin-dir configuration provided generates source path outside + /// of the temporary dir. + #[error( + "bin-dir configuration provided generates source path outside of the temporary dir: {}", .0.display() + )] + InvalidSourceFilePath(Box), + + /// bin-dir configuration provided generates empty source path. + #[error("bin-dir configuration provided generates empty source path")] + EmptySourceFilePath, + + /// Bin file is not found. + #[error("bin file {} not found", .0.display())] + BinFileNotFound(Box), + + #[error(transparent)] + Io(#[from] io::Error), + + #[error("Failed to render template: {0}")] + #[diagnostic(transparent)] + TemplateRender(#[from] leon::RenderError), +} + +/// Return true if the path does not look outside of current dir +/// +/// * `path` - must be normalized before passing to this function +fn is_valid_path(path: &Path) -> bool { + !matches!( + path.components().next(), + // normalized path cannot have curdir or parentdir, + // so checking prefix/rootdir is enough. + Some(Component::Prefix(..) | Component::RootDir) + ) +} + +/// Must be called after the archive is downloaded and extracted. +/// This function might uses blocking I/O. +pub fn infer_bin_dir_template( + data: &Data, + has_dir: &mut dyn FnMut(&Path) -> bool, +) -> Cow<'static, str> { + let name = data.name; + let target = data.target; + let version = data.version; + + // Make sure to update + // fetchers::gh_crate_meta::hosting::{FULL_FILENAMES, + // NOVERSION_FILENAMES} if you update this array. + let gen_possible_dirs: [for<'r> fn(&'r str, &'r str, &'r str) -> String; 8] = [ + |name, target, version| format!("{name}-{target}-v{version}"), + |name, target, version| format!("{name}-{target}-{version}"), + |name, target, version| format!("{name}-{version}-{target}"), + |name, target, version| format!("{name}-v{version}-{target}"), + |name, target, _version| format!("{name}-{target}"), + // Ignore the following when updating hosting::{FULL_FILENAMES, NOVERSION_FILENAMES} + |name, _target, version| format!("{name}-{version}"), + |name, _target, version| format!("{name}-v{version}"), + |name, _target, _version| name.to_string(), + ]; + + let default_bin_dir_template = Cow::Borrowed("{ bin }{ binary-ext }"); + + gen_possible_dirs + .into_iter() + .map(|gen_possible_dir| gen_possible_dir(name, target, version)) + .find(|dirname| has_dir(Path::new(&dirname))) + .map(|mut dir| { + dir.reserve_exact(1 + default_bin_dir_template.len()); + dir += "/"; + dir += &default_bin_dir_template; + Cow::Owned(dir) + }) + // Fallback to no dir + .unwrap_or(default_bin_dir_template) +} + +pub struct BinFile { + pub base_name: CompactString, + pub source: PathBuf, + pub archive_source_path: PathBuf, + pub dest: PathBuf, + pub link: Option, +} + +impl BinFile { + /// * `tt` - must have a template with name "bin_dir" + pub fn new( + data: &Data<'_>, + base_name: &str, + tt: &Template<'_>, + no_symlinks: bool, + ) -> Result { + let binary_ext = if data.target.contains("windows") { + ".exe" + } else { + "" + }; + + let ctx = Context { + name: data.name, + repo: data.repo, + target: data.target, + version: data.version, + bin: base_name, + binary_ext, + + target_related_info: data.target_related_info, + }; + + let (source, archive_source_path) = if data.meta.pkg_fmt == Some(PkgFmt::Bin) { + ( + data.bin_path.to_path_buf(), + data.bin_path.file_name().unwrap().into(), + ) + } else { + // Generate install paths + // Source path is the download dir + the generated binary path + let path = tt.render(&ctx)?; + + let path_normalized = Path::new(&path).normalize(); + + if path_normalized.components().next().is_none() { + return Err(Error::EmptySourceFilePath); + } + + if !is_valid_path(&path_normalized) { + return Err(Error::InvalidSourceFilePath(path_normalized.into())); + } + + (data.bin_path.join(&path_normalized), path_normalized) + }; + + // Destination at install dir + base-name{.extension} + let mut dest = data.install_path.join(ctx.bin); + if !binary_ext.is_empty() { + let binary_ext = binary_ext.strip_prefix('.').unwrap(); + + // PathBuf::set_extension returns false if Path::file_name + // is None, but we know that the file name must be Some, + // thus we assert! the return value here. + assert!(dest.set_extension(binary_ext)); + } + + let (dest, link) = if no_symlinks { + (dest, None) + } else { + // Destination path is the install dir + base-name-version{.extension} + let dest_file_path_with_ver = format!("{}-v{}{}", ctx.bin, ctx.version, ctx.binary_ext); + let dest_with_ver = data.install_path.join(dest_file_path_with_ver); + + (dest_with_ver, Some(dest)) + }; + + Ok(Self { + base_name: format_compact!("{base_name}{binary_ext}"), + source, + archive_source_path, + dest, + link, + }) + } + + pub fn preview_bin(&self) -> impl fmt::Display + '_ { + struct PreviewBin<'a> { + base_name: &'a str, + dest: path::Display<'a>, + } + + impl fmt::Display for PreviewBin<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} => {}", self.base_name, self.dest) + } + } + + PreviewBin { + base_name: &self.base_name, + dest: self.dest.display(), + } + } + + pub fn preview_link(&self) -> impl fmt::Display + '_ { + OptionalLazyFormat(self.link.as_ref().map(|link| LazyFormat { + base_name: &self.base_name, + source: link.display(), + dest: self.link_dest().display(), + })) + } + + /// Return `Ok` if the source exists, otherwise `Err`. + pub fn check_source_exists( + &self, + has_file: &mut dyn FnMut(&Path) -> bool, + ) -> Result<(), Error> { + if has_file(&self.archive_source_path) { + Ok(()) + } else { + Err(Error::BinFileNotFound((&*self.source).into())) + } + } + + fn pre_install_bin(&self) -> Result<(), Error> { + if !self.source.try_exists()? { + return Err(Error::BinFileNotFound((&*self.source).into())); + } + + #[cfg(unix)] + std::fs::set_permissions( + &self.source, + std::os::unix::fs::PermissionsExt::from_mode(0o755), + )?; + + Ok(()) + } + + pub fn install_bin(&self) -> Result<(), Error> { + self.pre_install_bin()?; + + debug!( + "Atomically install file from '{}' to '{}'", + self.source.display(), + self.dest.display() + ); + + atomic_install(&self.source, &self.dest)?; + + Ok(()) + } + + pub fn install_bin_noclobber(&self) -> Result<(), Error> { + self.pre_install_bin()?; + + debug!( + "Installing file from '{}' to '{}' only if dst not exists", + self.source.display(), + self.dest.display() + ); + + atomic_install_noclobber(&self.source, &self.dest)?; + + Ok(()) + } + + pub fn install_link(&self) -> Result<(), Error> { + if let Some(link) = &self.link { + let dest = self.link_dest(); + debug!( + "Create link '{}' pointing to '{}'", + link.display(), + dest.display() + ); + atomic_symlink_file(dest, link)?; + } + + Ok(()) + } + + pub fn install_link_noclobber(&self) -> Result<(), Error> { + if let Some(link) = &self.link { + let dest = self.link_dest(); + debug!( + "Create link '{}' pointing to '{}' only if dst not exists", + link.display(), + dest.display() + ); + atomic_symlink_file_noclobber(dest, link)?; + } + + Ok(()) + } + + fn link_dest(&self) -> &Path { + if cfg!(target_family = "unix") { + Path::new(self.dest.file_name().unwrap()) + } else { + &self.dest + } + } +} + +/// Data required to get bin paths +pub struct Data<'a> { + pub name: &'a str, + pub target: &'a str, + pub version: &'a str, + pub repo: Option<&'a str>, + pub meta: PkgMeta, + pub bin_path: &'a Path, + pub install_path: &'a Path, + /// More target related info, it's recommend to provide the following keys: + /// - target_family, + /// - target_arch + /// - target_libc + /// - target_vendor + pub target_related_info: &'a dyn leon::Values, +} + +#[derive(Clone)] +struct Context<'c> { + name: &'c str, + repo: Option<&'c str>, + target: &'c str, + version: &'c str, + bin: &'c str, + + /// Filename extension on the binary, i.e. .exe on Windows, nothing otherwise + binary_ext: &'c str, + + target_related_info: &'c dyn leon::Values, +} + +impl leon::Values for Context<'_> { + fn get_value<'s>(&'s self, key: &str) -> Option> { + match key { + "name" => Some(Cow::Borrowed(self.name)), + "repo" => self.repo.map(Cow::Borrowed), + "target" => Some(Cow::Borrowed(self.target)), + "version" => Some(Cow::Borrowed(self.version)), + "bin" => Some(Cow::Borrowed(self.bin)), + "binary-ext" => Some(Cow::Borrowed(self.binary_ext)), + // Soft-deprecated alias for binary-ext + "format" => Some(Cow::Borrowed(self.binary_ext)), + + key => self.target_related_info.get_value(key), + } + } +} + +struct LazyFormat<'a> { + base_name: &'a str, + source: path::Display<'a>, + dest: path::Display<'a>, +} + +impl fmt::Display for LazyFormat<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} ({} -> {})", self.base_name, self.source, self.dest) + } +} + +struct OptionalLazyFormat<'a>(Option>); + +impl fmt::Display for OptionalLazyFormat<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(lazy_format) = self.0.as_ref() { + fmt::Display::fmt(lazy_format, f) + } else { + Ok(()) + } + } +} diff --git a/crates/binstalk-downloader/CHANGELOG.md b/crates/binstalk-downloader/CHANGELOG.md new file mode 100644 index 00000000..bf6b6042 --- /dev/null +++ b/crates/binstalk-downloader/CHANGELOG.md @@ -0,0 +1,134 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.13.20](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.19...binstalk-downloader-v0.13.20) - 2025-06-06 + +### Other + +- updated the following local packages: binstalk-types + +## [0.13.19](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.18...binstalk-downloader-v0.13.19) - 2025-05-30 + +### Other + +- Upgrade reqwest to 0.12.17 ([#2168](https://github.com/cargo-bins/cargo-binstall/pull/2168)) + +## [0.13.18](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.17...binstalk-downloader-v0.13.18) - 2025-05-16 + +### Other + +- Upgrade transitive dependencies ([#2154](https://github.com/cargo-bins/cargo-binstall/pull/2154)) + +## [0.13.17](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.16...binstalk-downloader-v0.13.17) - 2025-04-05 + +### Other + +- Fix clippy lints ([#2111](https://github.com/cargo-bins/cargo-binstall/pull/2111)) + +## [0.13.16](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.15...binstalk-downloader-v0.13.16) - 2025-03-19 + +### Other + +- Fix clippy warnings for detect-targets and binstalk-downloader ([#2098](https://github.com/cargo-bins/cargo-binstall/pull/2098)) +- Bump hickory-resolver to 0.25.1 ([#2096](https://github.com/cargo-bins/cargo-binstall/pull/2096)) + +## [0.13.15](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.14...binstalk-downloader-v0.13.15) - 2025-03-15 + +### Other + +- *(deps)* bump the deps group with 2 updates ([#2084](https://github.com/cargo-bins/cargo-binstall/pull/2084)) +- *(deps)* bump tokio from 1.43.0 to 1.44.0 in the deps group ([#2079](https://github.com/cargo-bins/cargo-binstall/pull/2079)) + +## [0.13.14](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.13...binstalk-downloader-v0.13.14) - 2025-03-07 + +### Other + +- Use bzip2/libbz2-rs-sys ([#2071](https://github.com/cargo-bins/cargo-binstall/pull/2071)) +- *(deps)* bump the deps group with 3 updates ([#2072](https://github.com/cargo-bins/cargo-binstall/pull/2072)) + +## [0.13.13](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.12...binstalk-downloader-v0.13.13) - 2025-02-28 + +### Other + +- Use flate2/zlib-rs for dev/release build ([#2068](https://github.com/cargo-bins/cargo-binstall/pull/2068)) + +## [0.13.12](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.11...binstalk-downloader-v0.13.12) - 2025-02-11 + +### Other + +- Upgrade hickory-resolver to 0.25.0-alpha.5 ([#2038](https://github.com/cargo-bins/cargo-binstall/pull/2038)) + +## [0.13.11](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.10...binstalk-downloader-v0.13.11) - 2025-02-04 + +### Added + +- *(downloader)* allow remote::Client to be customised (#2035) + +## [0.13.10](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.9...binstalk-downloader-v0.13.10) - 2025-01-19 + +### Other + +- update Cargo.lock dependencies + +## [0.13.9](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.8...binstalk-downloader-v0.13.9) - 2025-01-13 + +### Other + +- update Cargo.lock dependencies + +## [0.13.8](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.7...binstalk-downloader-v0.13.8) - 2025-01-11 + +### Other + +- *(deps)* bump the deps group with 3 updates (#2015) + +## [0.13.7](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.6...binstalk-downloader-v0.13.7) - 2025-01-04 + +### Other + +- *(deps)* bump the deps group with 2 updates (#2010) + +## [0.13.6](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.5...binstalk-downloader-v0.13.6) - 2024-12-14 + +### Other + +- *(deps)* bump the deps group with 2 updates (#1997) + +## [0.13.5](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.4...binstalk-downloader-v0.13.5) - 2024-11-23 + +### Other + +- *(deps)* bump the deps group with 2 updates ([#1981](https://github.com/cargo-bins/cargo-binstall/pull/1981)) + +## [0.13.4](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.3...binstalk-downloader-v0.13.4) - 2024-11-09 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1966](https://github.com/cargo-bins/cargo-binstall/pull/1966)) + +## [0.13.3](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.2...binstalk-downloader-v0.13.3) - 2024-11-05 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1954](https://github.com/cargo-bins/cargo-binstall/pull/1954)) + +## [0.13.2](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.1...binstalk-downloader-v0.13.2) - 2024-11-02 + +### Other + +- Use rc-zip-sync for zip extraction ([#1942](https://github.com/cargo-bins/cargo-binstall/pull/1942)) + +## [0.13.1](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.13.0...binstalk-downloader-v0.13.1) - 2024-08-12 + +### Other +- Enable happy eyeballs when using hickory-dns ([#1877](https://github.com/cargo-bins/cargo-binstall/pull/1877)) + +## [0.13.0](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-downloader-v0.12.0...binstalk-downloader-v0.13.0) - 2024-08-10 + +### Other +- Bump hickory-resolver to 0.25.0-alpha.2 ([#1869](https://github.com/cargo-bins/cargo-binstall/pull/1869)) diff --git a/crates/binstalk-downloader/Cargo.toml b/crates/binstalk-downloader/Cargo.toml new file mode 100644 index 00000000..7e0cbf93 --- /dev/null +++ b/crates/binstalk-downloader/Cargo.toml @@ -0,0 +1,141 @@ +[package] +name = "binstalk-downloader" +description = "The binstall toolkit for downloading and extracting file" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/binstalk-downloader" +version = "0.13.20" +rust-version = "1.79.0" +authors = ["ryan "] +edition = "2021" +license = "Apache-2.0 OR MIT" + +[dependencies] +async-trait = "0.1.88" +async-compression = { version = "0.4.4", features = [ + "gzip", + "zstd", + "xz", + "bzip2", + "tokio", +] } +binstalk-types = { version = "0.10.0", path = "../binstalk-types" } +bytes = "1.4.0" +bzip2 = { version = "0.5.2", default-features = false, features = [ + "libbz2-rs-sys", +] } +cfg-if = "1" +compact_str = "0.9.0" +flate2 = { version = "1.0.28", default-features = false } +futures-util = "0.3.30" +futures-io = "0.3.30" +httpdate = "1.0.2" +rc-zip-sync = { version = "4.2.6", features = [ + "deflate", + "bzip2", + "deflate64", + "lzma", + "zstd", +] } +reqwest = { version = "0.12.17", features = [ + "http2", + "stream", + "zstd", + "gzip", + "brotli", + "deflate", +], default-features = false } +serde = { version = "1.0.163", features = ["derive"], optional = true } +serde_json = { version = "1.0.107", optional = true } +# Use a fork here since we need PAX support, but the upstream +# does not hav the PR merged yet. +# +#tar = "0.4.38" +tar = { package = "binstall-tar", version = "0.4.39" } +tempfile = "3.5.0" +thiserror = "2.0.11" +tokio = { version = "1.44.0", features = [ + "macros", + "rt-multi-thread", + "sync", + "time", + "fs", +], default-features = false } +tokio-tar = "0.3.0" +tokio-util = { version = "0.7.8", features = ["io"] } +tracing = "0.1.39" +hickory-resolver = { version = "0.25.1", optional = true, features = [ + "dnssec-ring", +] } +once_cell = { version = "1.18.0", optional = true } +url = "2.5.4" + +xz2 = "0.1.7" + +# zstd is also depended by zip. +# Since zip 0.6.3 depends on zstd 0.11, we can use 0.12.0 here +# because it uses the same zstd-sys version. +# Otherwise there will be a link conflict. +zstd = { version = "0.13.2", default-features = false } + +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.native-tls-crate] +optional = true +package = "native-tls" +# The version must be kept in sync of reqwest +version = "0.2.10" + +[features] +default = ["static", "rustls"] + +static = ["bzip2/static", "xz2/static", "native-tls-crate?/vendored"] +pkg-config = ["zstd/pkg-config"] + +zlib-ng = ["flate2/zlib-ng"] +zlib-rs = ["flate2/zlib-rs"] + +# Dummy feature, enabled if rustls or native-tls is enabled. +# Used to avoid compilation error when no feature is enabled. +__tls = [] + +rustls = [ + "__tls", + + "reqwest/rustls-tls", + "reqwest/rustls-tls-webpki-roots", + "reqwest/rustls-tls-native-roots", + + # Enable the following features only if hickory-resolver is enabled. + "hickory-resolver?/tls-ring", + # hickory-resolver currently supports https with rustls + "hickory-resolver?/https-ring", + "hickory-resolver?/quic-ring", + "hickory-resolver?/h3-ring", +] +native-tls = ["__tls", "native-tls-crate", "reqwest/native-tls"] + +# Enable hickory-resolver so that features on it will also be enabled. +hickory-dns = ["hickory-resolver", "default-net", "ipconfig", "once_cell"] + +# Deprecated alias for hickory-dns, since trust-dns is renamed to hickory-dns +trust-dns = ["hickory-dns"] + +# HTTP3 is temporarily disabled by reqwest. +# +# Experimental HTTP/3 client, this would require `--cfg reqwest_unstable` +# to be passed to `rustc`. +http3 = ["reqwest/http3"] + +zstd-thin = ["zstd/thin"] + +cross-lang-fat-lto = ["zstd/fat-lto"] + +json = ["serde", "serde_json"] + +[target."cfg(windows)".dependencies] +default-net = { version = "0.22.0", optional = true } +ipconfig = { version = "0.3.2", optional = true, default-features = false } + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(reqwest_unstable)'] } diff --git a/crates/binstalk-downloader/LICENSE-APACHE b/crates/binstalk-downloader/LICENSE-APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/crates/binstalk-downloader/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/binstalk-downloader/LICENSE-MIT b/crates/binstalk-downloader/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/crates/binstalk-downloader/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/crates/binstalk-downloader/src/download.rs b/crates/binstalk-downloader/src/download.rs new file mode 100644 index 00000000..53dd9f1e --- /dev/null +++ b/crates/binstalk-downloader/src/download.rs @@ -0,0 +1,408 @@ +use std::{fmt, io, path::Path}; + +use binstalk_types::cargo_toml_binstall::PkgFmtDecomposed; +use bytes::Bytes; +use futures_util::{stream::FusedStream, Stream, StreamExt}; +use thiserror::Error as ThisError; +use tracing::{debug, error, instrument}; + +pub use binstalk_types::cargo_toml_binstall::{PkgFmt, TarBasedFmt}; +pub use rc_zip_sync::rc_zip::error::Error as ZipError; + +use crate::remote::{Client, Error as RemoteError, Response, Url}; + +mod async_extracter; +use async_extracter::*; + +mod async_tar_visitor; +use async_tar_visitor::extract_tar_based_stream_and_visit; +pub use async_tar_visitor::{TarEntriesVisitor, TarEntry, TarEntryType}; + +mod extracter; + +mod extracted_files; +pub use extracted_files::{ExtractedFiles, ExtractedFilesEntry}; + +mod zip_extraction; + +#[derive(Debug, ThisError)] +#[non_exhaustive] +pub enum DownloadError { + #[error("Failed to extract zipfile: {0}")] + Unzip(#[from] ZipError), + + #[error("Failed to download from remote: {0}")] + Remote(#[from] RemoteError), + + /// A generic I/O error. + /// + /// - Code: `binstall::io` + /// - Exit: 74 + #[error("I/O Error: {0}")] + Io(io::Error), +} + +impl From for DownloadError { + fn from(err: io::Error) -> Self { + err.downcast::() + .unwrap_or_else(DownloadError::Io) + } +} + +impl From for io::Error { + fn from(e: DownloadError) -> io::Error { + match e { + DownloadError::Io(io_error) => io_error, + e => io::Error::other(e), + } + } +} + +pub trait DataVerifier: Send + Sync { + /// Digest input data. + /// + /// This method can be called repeatedly for use with streaming messages, + /// it will be called in the order of the message received. + fn update(&mut self, data: &Bytes); + + /// Finalise the data verification. + /// + /// Return false if the data is invalid. + fn validate(&mut self) -> bool; +} + +impl DataVerifier for () { + fn update(&mut self, _: &Bytes) {} + fn validate(&mut self) -> bool { + true + } +} + +#[derive(Debug)] +enum DownloadContent { + ToIssue { client: Client, url: Url }, + Response(Response), +} + +impl DownloadContent { + async fn into_response(self) -> Result { + Ok(match self { + DownloadContent::ToIssue { client, url } => client.get(url).send(true).await?, + DownloadContent::Response(response) => response, + }) + } +} + +pub struct Download<'a> { + content: DownloadContent, + data_verifier: Option<&'a mut dyn DataVerifier>, +} + +impl fmt::Debug for Download<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.content, f) + } +} + +impl Download<'static> { + pub fn new(client: Client, url: Url) -> Self { + Self { + content: DownloadContent::ToIssue { client, url }, + data_verifier: None, + } + } + + pub fn from_response(response: Response) -> Self { + Self { + content: DownloadContent::Response(response), + data_verifier: None, + } + } +} + +impl<'a> Download<'a> { + pub fn new_with_data_verifier( + client: Client, + url: Url, + data_verifier: &'a mut dyn DataVerifier, + ) -> Self { + Self { + content: DownloadContent::ToIssue { client, url }, + data_verifier: Some(data_verifier), + } + } + + pub fn from_response_with_data_verifier( + response: Response, + data_verifier: &'a mut dyn DataVerifier, + ) -> Self { + Self { + content: DownloadContent::Response(response), + data_verifier: Some(data_verifier), + } + } + + pub fn with_data_verifier(self, data_verifier: &mut dyn DataVerifier) -> Download<'_> { + Download { + content: self.content, + data_verifier: Some(data_verifier), + } + } + + async fn get_stream( + self, + ) -> Result< + impl FusedStream> + Send + Sync + Unpin + 'a, + DownloadError, + > { + let mut data_verifier = self.data_verifier; + Ok(self + .content + .into_response() + .await? + .bytes_stream() + .map(move |res| { + let bytes = res?; + + if let Some(data_verifier) = &mut data_verifier { + data_verifier.update(&bytes); + } + + Ok(bytes) + }) + // Call `fuse` at the end to make sure `data_verifier` is only + // called when the stream still has elements left. + .fuse()) + } +} + +/// Make sure `stream` is an alias instead of taking the value to avoid +/// exploding size of the future generated. +/// +/// Accept `FusedStream` only since the `stream` could be already consumed. +async fn consume_stream(stream: &mut S) +where + S: Stream> + FusedStream + Unpin, +{ + while let Some(res) = stream.next().await { + if let Err(err) = res { + error!(?err, "failed to consume stream"); + break; + } + } +} + +impl Download<'_> { + /// Download a file from the provided URL and process it in memory. + /// + /// This does not support verifying a checksum due to the partial extraction + /// and will ignore one if specified. + /// + /// NOTE that this API does not support gnu extension sparse file unlike + /// [`Download::and_extract`]. + #[instrument(skip(self, visitor))] + pub async fn and_visit_tar( + self, + fmt: TarBasedFmt, + visitor: &mut dyn TarEntriesVisitor, + ) -> Result<(), DownloadError> { + let has_data_verifier = self.data_verifier.is_some(); + let mut stream = self.get_stream().await?; + + debug!("Downloading and extracting then in-memory processing"); + + let res = extract_tar_based_stream_and_visit(&mut stream, fmt, visitor).await; + + if has_data_verifier { + consume_stream(&mut stream).await; + } + + if res.is_ok() { + debug!("Download, extraction and in-memory procession OK"); + } + + res + } + + /// Download a file from the provided URL and extract it to the provided path. + /// + /// NOTE that this will only extract directory and regular files. + #[instrument( + skip(self, path), + fields(path = format_args!("{}", path.as_ref().display())) + )] + pub async fn and_extract( + self, + fmt: PkgFmt, + path: impl AsRef, + ) -> Result { + async fn inner( + this: Download<'_>, + fmt: PkgFmt, + path: &Path, + ) -> Result { + let has_data_verifier = this.data_verifier.is_some(); + let mut stream = this.get_stream().await?; + + debug!("Downloading and extracting to: '{}'", path.display()); + + let res = match fmt.decompose() { + PkgFmtDecomposed::Tar(fmt) => { + extract_tar_based_stream(&mut stream, path, fmt).await + } + PkgFmtDecomposed::Bin => extract_bin(&mut stream, path).await, + PkgFmtDecomposed::Zip => extract_zip(&mut stream, path).await, + }; + + if has_data_verifier { + consume_stream(&mut stream).await; + } + + if res.is_ok() { + debug!("Download OK, extracted to: '{}'", path.display()); + } + + res + } + + inner(self, fmt, path.as_ref()).await + } + + #[instrument(skip(self))] + pub async fn into_bytes(self) -> Result { + let bytes = self.content.into_response().await?.bytes().await?; + if let Some(verifier) = self.data_verifier { + verifier.update(&bytes); + } + Ok(bytes) + } +} + +#[cfg(test)] +mod test { + use super::*; + + use std::{ + collections::{HashMap, HashSet}, + ffi::OsStr, + num::NonZeroU16, + }; + use tempfile::tempdir; + + #[tokio::test] + async fn test_and_extract() { + let client = crate::remote::Client::new( + concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")), + None, + NonZeroU16::new(10).unwrap(), + 1.try_into().unwrap(), + [], + ) + .unwrap(); + + // cargo-binstall + let cargo_binstall_url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v0.20.1/cargo-binstall-aarch64-unknown-linux-musl.tgz"; + + let extracted_files = + Download::new(client.clone(), Url::parse(cargo_binstall_url).unwrap()) + .and_extract(PkgFmt::Tgz, tempdir().unwrap()) + .await + .unwrap(); + + assert!(extracted_files.has_file(Path::new("cargo-binstall"))); + assert!(!extracted_files.has_file(Path::new("1234"))); + + let files = HashSet::from([OsStr::new("cargo-binstall").into()]); + assert_eq!(extracted_files.get_dir(Path::new(".")).unwrap(), &files); + + assert_eq!( + extracted_files.0, + HashMap::from([ + ( + Path::new("cargo-binstall").into(), + ExtractedFilesEntry::File + ), + ( + Path::new(".").into(), + ExtractedFilesEntry::Dir(Box::new(files)) + ) + ]) + ); + + // cargo-watch + let cargo_watch_url = "https://github.com/watchexec/cargo-watch/releases/download/v8.4.0/cargo-watch-v8.4.0-aarch64-unknown-linux-gnu.tar.xz"; + + let extracted_files = Download::new(client.clone(), Url::parse(cargo_watch_url).unwrap()) + .and_extract(PkgFmt::Txz, tempdir().unwrap()) + .await + .unwrap(); + + let dir = Path::new("cargo-watch-v8.4.0-aarch64-unknown-linux-gnu"); + + assert_eq!( + extracted_files.get_dir(Path::new(".")).unwrap(), + &HashSet::from([dir.as_os_str().into()]) + ); + + assert_eq!( + extracted_files.get_dir(dir).unwrap(), + &HashSet::from_iter( + [ + "README.md", + "LICENSE", + "completions", + "cargo-watch", + "cargo-watch.1" + ] + .iter() + .map(OsStr::new) + .map(Box::::from) + ), + ); + + assert_eq!( + extracted_files.get_dir(&dir.join("completions")).unwrap(), + &HashSet::from([OsStr::new("zsh").into()]), + ); + + assert!(extracted_files.has_file(&dir.join("cargo-watch"))); + assert!(extracted_files.has_file(&dir.join("cargo-watch.1"))); + assert!(extracted_files.has_file(&dir.join("LICENSE"))); + assert!(extracted_files.has_file(&dir.join("README.md"))); + + assert!(!extracted_files.has_file(&dir.join("completions"))); + assert!(!extracted_files.has_file(&dir.join("asdfcqwe"))); + + assert!(extracted_files.has_file(&dir.join("completions/zsh"))); + + // sccache, tgz and zip + let sccache_config = [ + ("https://github.com/mozilla/sccache/releases/download/v0.3.3/sccache-v0.3.3-x86_64-pc-windows-msvc.tar.gz", PkgFmt::Tgz), + ("https://github.com/mozilla/sccache/releases/download/v0.3.3/sccache-v0.3.3-x86_64-pc-windows-msvc.zip", PkgFmt::Zip), + ]; + + for (sccache_url, fmt) in sccache_config { + let extracted_files = Download::new(client.clone(), Url::parse(sccache_url).unwrap()) + .and_extract(fmt, tempdir().unwrap()) + .await + .unwrap(); + + let dir = Path::new("sccache-v0.3.3-x86_64-pc-windows-msvc"); + + assert_eq!( + extracted_files.get_dir(Path::new(".")).unwrap(), + &HashSet::from([dir.as_os_str().into()]) + ); + + assert_eq!( + extracted_files.get_dir(dir).unwrap(), + &HashSet::from_iter( + ["README.md", "LICENSE", "sccache.exe"] + .iter() + .map(OsStr::new) + .map(Box::::from) + ), + ); + } + } +} diff --git a/crates/binstalk-downloader/src/download/async_extracter.rs b/crates/binstalk-downloader/src/download/async_extracter.rs new file mode 100644 index 00000000..ca2f0710 --- /dev/null +++ b/crates/binstalk-downloader/src/download/async_extracter.rs @@ -0,0 +1,167 @@ +use std::{ + borrow::Cow, + fs, + future::Future, + io::{self, Write}, + path::{Component, Path, PathBuf}, +}; + +use bytes::Bytes; +use futures_util::Stream; +use tempfile::tempfile as create_tmpfile; +use tokio::sync::mpsc; +use tracing::debug; + +use super::{extracter::*, DownloadError, ExtractedFiles, TarBasedFmt}; +use crate::{ + download::zip_extraction::do_extract_zip, + utils::{extract_with_blocking_task, StreamReadable}, +}; + +pub async fn extract_bin(stream: S, path: &Path) -> Result +where + S: Stream> + Send + Sync + Unpin, +{ + debug!("Writing to `{}`", path.display()); + + extract_with_blocking_decoder(stream, path, |rx, path| { + let mut extracted_files = ExtractedFiles::new(); + + extracted_files.add_file(Path::new(path.file_name().unwrap())); + + write_stream_to_file(rx, fs::File::create(path)?)?; + + Ok(extracted_files) + }) + .await +} + +pub async fn extract_zip(stream: S, path: &Path) -> Result +where + S: Stream> + Unpin + Send + Sync, +{ + debug!("Downloading from zip archive to tempfile"); + + extract_with_blocking_decoder(stream, path, |rx, path| { + debug!("Decompressing from zip archive to `{}`", path.display()); + + do_extract_zip(write_stream_to_file(rx, create_tmpfile()?)?, path).map_err(io::Error::from) + }) + .await +} + +pub async fn extract_tar_based_stream( + stream: S, + dst: &Path, + fmt: TarBasedFmt, +) -> Result +where + S: Stream> + Send + Sync + Unpin, +{ + debug!("Extracting from {fmt} archive to {}", dst.display()); + + extract_with_blocking_decoder(stream, dst, move |rx, dst| { + // Adapted from https://docs.rs/tar/latest/src/tar/archive.rs.html#189-219 + + if dst.symlink_metadata().is_err() { + fs::create_dir_all(dst)?; + } + + // 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 = &dst + .canonicalize() + .map(Cow::Owned) + .unwrap_or(Cow::Borrowed(dst)); + + let mut tar = create_tar_decoder(StreamReadable::new(rx), fmt)?; + let mut entries = tar.entries()?; + + let mut extracted_files = ExtractedFiles::new(); + + // Delay any directory entries until the end (they will be created if needed by + // descendants), to ensure that directory permissions do not interfer with descendant + // extraction. + let mut directories = Vec::new(); + + while let Some(mut entry) = entries.next().transpose()? { + match entry.header().entry_type() { + tar::EntryType::Regular => { + // unpack_in returns false if the path contains ".." + // and is skipped. + if entry.unpack_in(dst)? { + let path = entry.path()?; + + // create normalized_path in the same way + // tar::Entry::unpack_in would normalize the path. + let mut normalized_path = PathBuf::new(); + + for part in path.components() { + match part { + Component::Prefix(..) | Component::RootDir | Component::CurDir => { + continue + } + + // unpack_in would return false if this happens. + Component::ParentDir => unreachable!(), + + Component::Normal(part) => normalized_path.push(part), + } + } + + extracted_files.add_file(&normalized_path); + } + } + tar::EntryType::Directory => { + directories.push(entry); + } + _ => (), + } + } + + for mut dir in directories { + if dir.unpack_in(dst)? { + extracted_files.add_dir(&dir.path()?); + } + } + + Ok(extracted_files) + }) + .await +} + +fn extract_with_blocking_decoder( + stream: S, + path: &Path, + f: F, +) -> impl Future> +where + S: Stream> + Send + Sync + Unpin, + F: FnOnce(mpsc::Receiver, &Path) -> io::Result + Send + Sync + 'static, + T: Send + 'static, +{ + let path = path.to_owned(); + + extract_with_blocking_task(stream, move |rx| { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + f(rx, &path) + }) +} + +fn write_stream_to_file(mut rx: mpsc::Receiver, f: fs::File) -> io::Result { + let mut f = io::BufWriter::new(f); + + while let Some(bytes) = rx.blocking_recv() { + f.write_all(&bytes)?; + } + + f.flush()?; + + f.into_inner().map_err(io::IntoInnerError::into_error) +} diff --git a/crates/binstalk-downloader/src/download/async_tar_visitor.rs b/crates/binstalk-downloader/src/download/async_tar_visitor.rs new file mode 100644 index 00000000..b6cdfa7d --- /dev/null +++ b/crates/binstalk-downloader/src/download/async_tar_visitor.rs @@ -0,0 +1,125 @@ +use std::{borrow::Cow, fmt::Debug, io, path::Path, pin::Pin}; + +use async_compression::tokio::bufread; +use bytes::Bytes; +use futures_util::{Stream, StreamExt}; +use tokio::io::{copy, sink, AsyncRead}; +use tokio_tar::{Archive, Entry, EntryType}; +use tokio_util::io::StreamReader; +use tracing::debug; + +use super::{ + DownloadError, + TarBasedFmt::{self, *}, +}; + +pub trait TarEntry: AsyncRead + Send + Sync + Unpin + Debug { + /// Returns the path name for this entry. + /// + /// This method may fail if the pathname is not valid Unicode and + /// this is called on a Windows platform. + /// + /// Note that this function will convert any `\` characters to + /// directory separators. + fn path(&self) -> io::Result>; + + fn size(&self) -> io::Result; + + fn entry_type(&self) -> TarEntryType; +} + +impl TarEntry for &mut T { + fn path(&self) -> io::Result> { + T::path(self) + } + + fn size(&self) -> io::Result { + T::size(self) + } + + fn entry_type(&self) -> TarEntryType { + T::entry_type(self) + } +} + +impl TarEntry for Entry { + fn path(&self) -> io::Result> { + Entry::path(self) + } + + fn size(&self) -> io::Result { + self.header().size() + } + + fn entry_type(&self) -> TarEntryType { + match self.header().entry_type() { + EntryType::Regular => TarEntryType::Regular, + EntryType::Link => TarEntryType::Link, + EntryType::Symlink => TarEntryType::Symlink, + EntryType::Char => TarEntryType::Char, + EntryType::Block => TarEntryType::Block, + EntryType::Directory => TarEntryType::Directory, + EntryType::Fifo => TarEntryType::Fifo, + // Implementation-defined ‘high-performance’ type, treated as regular file + EntryType::Continuous => TarEntryType::Regular, + _ => TarEntryType::Unknown, + } + } +} + +#[derive(Copy, Clone, Debug)] +#[non_exhaustive] +pub enum TarEntryType { + Regular, + Link, + Symlink, + Char, + Block, + Directory, + Fifo, + Unknown, +} + +/// Visitor must iterate over all entries. +/// Entires can be in arbitary order. +#[async_trait::async_trait] +pub trait TarEntriesVisitor: Send + Sync { + /// Will be called once per entry + async fn visit(&mut self, entry: &mut dyn TarEntry) -> Result<(), DownloadError>; +} + +pub(crate) async fn extract_tar_based_stream_and_visit( + stream: S, + fmt: TarBasedFmt, + visitor: &mut dyn TarEntriesVisitor, +) -> Result<(), DownloadError> +where + S: Stream> + Send + Sync, +{ + debug!("Extracting from {fmt} archive to process it in memory"); + + let reader = StreamReader::new(stream); + let decoder: Pin> = match fmt { + Tar => Box::pin(reader), + Tbz2 => Box::pin(bufread::BzDecoder::new(reader)), + Tgz => Box::pin(bufread::GzipDecoder::new(reader)), + Txz => Box::pin(bufread::XzDecoder::new(reader)), + Tzstd => Box::pin(bufread::ZstdDecoder::new(reader)), + }; + + let mut tar = Archive::new(decoder); + let mut entries = tar.entries()?; + + let mut sink = sink(); + + while let Some(res) = entries.next().await { + let mut entry = res?; + visitor.visit(&mut entry).await?; + + // Consume all remaining data so that next iteration would work fine + // instead of reading the data of prevoius entry. + copy(&mut entry, &mut sink).await?; + } + + Ok(()) +} diff --git a/crates/binstalk-downloader/src/download/extracted_files.rs b/crates/binstalk-downloader/src/download/extracted_files.rs new file mode 100644 index 00000000..ac45d4fb --- /dev/null +++ b/crates/binstalk-downloader/src/download/extracted_files.rs @@ -0,0 +1,108 @@ +use std::{ + collections::{hash_map::Entry as HashMapEntry, HashMap, HashSet}, + ffi::OsStr, + path::Path, +}; + +#[derive(Debug)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub enum ExtractedFilesEntry { + Dir(Box>>), + File, +} + +impl ExtractedFilesEntry { + fn new_dir(file_name: Option<&OsStr>) -> Self { + ExtractedFilesEntry::Dir(Box::new( + file_name + .map(|file_name| HashSet::from([file_name.into()])) + .unwrap_or_default(), + )) + } +} + +#[derive(Debug)] +pub struct ExtractedFiles(pub(super) HashMap, ExtractedFilesEntry>); + +impl ExtractedFiles { + pub(super) fn new() -> Self { + Self(Default::default()) + } + + /// * `path` - must be canonical and must not be empty + /// + /// NOTE that if the entry for the `path` is previously set to a dir, + /// it would be replaced with a file. + pub(super) fn add_file(&mut self, path: &Path) { + self.0.insert(path.into(), ExtractedFilesEntry::File); + self.add_dir_if_has_parent(path); + } + + fn add_dir_if_has_parent(&mut self, path: &Path) { + if let Some(parent) = path.parent() { + if !parent.as_os_str().is_empty() { + self.add_dir_inner(parent, path.file_name()); + self.add_dir_if_has_parent(parent); + } else { + self.add_dir_inner(Path::new("."), path.file_name()) + } + } + } + + /// * `path` - must be canonical and must not be empty + /// + /// NOTE that if the entry for the `path` is previously set to a dir, + /// it would be replaced with an empty Dir entry. + pub(super) fn add_dir(&mut self, path: &Path) { + self.add_dir_inner(path, None); + self.add_dir_if_has_parent(path); + } + + /// * `path` - must be canonical and must not be empty + /// + /// NOTE that if the entry for the `path` is previously set to a dir, + /// it would be replaced with a Dir entry containing `file_name` if it + /// is `Some(..)`, or an empty Dir entry. + fn add_dir_inner(&mut self, path: &Path, file_name: Option<&OsStr>) { + match self.0.entry(path.into()) { + HashMapEntry::Vacant(entry) => { + entry.insert(ExtractedFilesEntry::new_dir(file_name)); + } + HashMapEntry::Occupied(entry) => match entry.into_mut() { + ExtractedFilesEntry::Dir(hash_set) => { + if let Some(file_name) = file_name { + hash_set.insert(file_name.into()); + } + } + entry => *entry = ExtractedFilesEntry::new_dir(file_name), + }, + } + } + + /// * `path` - must be a relative path without `.`, `..`, `/`, `prefix:/` + /// and must not be empty, for these values it is guaranteed to + /// return `None`. + /// But could be set to "." for top-level. + pub fn get_entry(&self, path: &Path) -> Option<&ExtractedFilesEntry> { + self.0.get(path) + } + + /// * `path` - must be a relative path without `.`, `..`, `/`, `prefix:/` + /// and must not be empty, for these values it is guaranteed to + /// return `None`. + /// But could be set to "." for top-level. + pub fn get_dir(&self, path: &Path) -> Option<&HashSet>> { + match self.get_entry(path)? { + ExtractedFilesEntry::Dir(file_names) => Some(file_names), + ExtractedFilesEntry::File => None, + } + } + + /// * `path` - must be a relative path without `.`, `..`, `/`, `prefix:/` + /// and must not be empty, for these values it is guaranteed to + /// return `false`. + /// But could be set to "." for top-level. + pub fn has_file(&self, path: &Path) -> bool { + matches!(self.get_entry(path), Some(ExtractedFilesEntry::File)) + } +} diff --git a/crates/binstalk-downloader/src/download/extracter.rs b/crates/binstalk-downloader/src/download/extracter.rs new file mode 100644 index 00000000..32e131e6 --- /dev/null +++ b/crates/binstalk-downloader/src/download/extracter.rs @@ -0,0 +1,31 @@ +use std::io::{self, BufRead, Read}; + +use bzip2::bufread::BzDecoder; +use flate2::bufread::GzDecoder; +use tar::Archive; +use xz2::bufread::XzDecoder; +use zstd::stream::Decoder as ZstdDecoder; + +use super::TarBasedFmt; + +pub fn create_tar_decoder( + dat: impl BufRead + 'static, + fmt: TarBasedFmt, +) -> io::Result>> { + use TarBasedFmt::*; + + let r: Box = match fmt { + Tar => Box::new(dat), + Tbz2 => Box::new(BzDecoder::new(dat)), + Tgz => Box::new(GzDecoder::new(dat)), + Txz => Box::new(XzDecoder::new(dat)), + Tzstd => { + // The error can only come from raw::Decoder::with_dictionary as of zstd 0.10.2 and + // 0.11.2, which is specified as `&[]` by `ZstdDecoder::new`, thus `ZstdDecoder::new` + // should not return any error. + Box::new(ZstdDecoder::with_buffer(dat)?) + } + }; + + Ok(Archive::new(r)) +} diff --git a/crates/binstalk-downloader/src/download/zip_extraction.rs b/crates/binstalk-downloader/src/download/zip_extraction.rs new file mode 100644 index 00000000..cb5af6e7 --- /dev/null +++ b/crates/binstalk-downloader/src/download/zip_extraction.rs @@ -0,0 +1,68 @@ +use std::{ + fs::{create_dir_all, File}, + io, + path::Path, +}; + +use cfg_if::cfg_if; +use rc_zip_sync::{rc_zip::parse::EntryKind, ReadZip}; + +use super::{DownloadError, ExtractedFiles}; + +pub(super) fn do_extract_zip(f: File, dir: &Path) -> Result { + let mut extracted_files = ExtractedFiles::new(); + + for entry in f.read_zip()?.entries() { + let Some(name) = entry.sanitized_name().map(Path::new) else { + continue; + }; + let path = dir.join(name); + + let do_extract_file = || { + let mut entry_writer = File::create(&path)?; + let mut entry_reader = entry.reader(); + io::copy(&mut entry_reader, &mut entry_writer)?; + + Ok::<_, io::Error>(()) + }; + + let parent = path + .parent() + .expect("all full entry paths should have parent paths"); + create_dir_all(parent)?; + + match entry.kind() { + EntryKind::Symlink => { + extracted_files.add_file(name); + cfg_if! { + if #[cfg(windows)] { + do_extract_file()?; + } else { + use std::{fs, io::Read}; + + match fs::symlink_metadata(&path) { + Ok(metadata) if metadata.is_file() => fs::remove_file(&path)?, + _ => (), + } + + let mut src = String::new(); + entry.reader().read_to_string(&mut src)?; + + // validate pointing path before creating a symbolic link + if src.contains("..") { + continue; + } + std::os::unix::fs::symlink(src, &path)?; + } + } + } + EntryKind::Directory => (), + EntryKind::File => { + extracted_files.add_file(name); + do_extract_file()?; + } + } + } + + Ok(extracted_files) +} diff --git a/crates/binstalk-downloader/src/lib.rs b/crates/binstalk-downloader/src/lib.rs new file mode 100644 index 00000000..1e4cfad7 --- /dev/null +++ b/crates/binstalk-downloader/src/lib.rs @@ -0,0 +1,6 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +pub use bytes; +pub mod download; +pub mod remote; +mod utils; diff --git a/crates/binstalk-downloader/src/remote.rs b/crates/binstalk-downloader/src/remote.rs new file mode 100644 index 00000000..7ef3d56e --- /dev/null +++ b/crates/binstalk-downloader/src/remote.rs @@ -0,0 +1,419 @@ +use std::{ + num::{NonZeroU16, NonZeroU64, NonZeroU8}, + ops::ControlFlow, + sync::Arc, + time::{Duration, SystemTime}, +}; + +use bytes::Bytes; +use futures_util::Stream; +use httpdate::parse_http_date; +use reqwest::{ + header::{HeaderMap, HeaderValue, RETRY_AFTER}, + Request, +}; +use thiserror::Error as ThisError; +use tracing::{debug, info, instrument}; + +pub use reqwest::{header, Error as ReqwestError, Method, StatusCode}; +pub use url::Url; + +mod delay_request; +use delay_request::DelayRequest; + +mod certificate; +pub use certificate::Certificate; + +mod request_builder; +pub use request_builder::{Body, RequestBuilder, Response}; + +mod tls_version; +pub use tls_version::TLSVersion; + +#[cfg(feature = "hickory-dns")] +mod resolver; +#[cfg(feature = "hickory-dns")] +use resolver::TrustDnsResolver; + +#[cfg(feature = "json")] +pub use request_builder::JsonError; + +const MAX_RETRY_DURATION: Duration = Duration::from_secs(120); +const MAX_RETRY_COUNT: u8 = 3; +const DEFAULT_RETRY_DURATION_FOR_RATE_LIMIT: Duration = Duration::from_millis(200); +const RETRY_DURATION_FOR_TIMEOUT: Duration = Duration::from_millis(200); +#[allow(dead_code)] +const DEFAULT_MIN_TLS: TLSVersion = TLSVersion::TLS_1_2; + +#[derive(Debug, ThisError)] +#[non_exhaustive] +pub enum Error { + #[error("Reqwest error: {0}")] + Reqwest(#[from] reqwest::Error), + + #[error(transparent)] + Http(Box), + + #[cfg(feature = "json")] + #[error("Failed to parse http response body as Json: {0}")] + Json(#[from] JsonError), +} + +#[derive(Debug, ThisError)] +#[error("could not {method} {url}: {err}")] +pub struct HttpError { + method: reqwest::Method, + url: url::Url, + #[source] + err: reqwest::Error, +} + +impl HttpError { + /// Returns true if the error is from [`Response::error_for_status`]. + pub fn is_status(&self) -> bool { + self.err.is_status() + } +} + +#[derive(Debug)] +struct Inner { + client: reqwest::Client, + service: DelayRequest, +} + +#[derive(Clone, Debug)] +pub struct Client(Arc); + +#[cfg_attr(not(feature = "__tls"), allow(unused_variables, unused_mut))] +impl Client { + /// Construct a new downloader client + /// + /// * `per_millis` - The duration (in millisecond) for which at most + /// `num_request` can be sent. Increase it if rate-limit errors + /// happen. + /// * `num_request` - maximum number of requests to be processed for + /// each `per_millis` duration. + /// + /// The [`reqwest::Client`] constructed has secure defaults, such as allowing + /// only TLS v1.2 and above, and disallowing plaintext HTTP altogether. If you + /// need more control, use the `from_builder` variant. + pub fn new( + user_agent: impl AsRef, + min_tls: Option, + per_millis: NonZeroU16, + num_request: NonZeroU64, + certificates: impl IntoIterator, + ) -> Result { + Self::from_builder( + Self::default_builder(user_agent.as_ref(), min_tls, &mut certificates.into_iter()), + per_millis, + num_request, + ) + } + + /// Constructs a default [`reqwest::ClientBuilder`]. + /// + /// This may be used alongside [`Client::from_builder`] to start from reasonable + /// defaults, but still be able to customise the reqwest instance. Arguments are + /// as [`Client::new`], but without generic parameters. + pub fn default_builder( + user_agent: &str, + min_tls: Option, + certificates: &mut dyn Iterator, + ) -> reqwest::ClientBuilder { + let mut builder = reqwest::ClientBuilder::new() + .user_agent(user_agent) + .https_only(true) + .tcp_nodelay(false); + + #[cfg(feature = "hickory-dns")] + { + builder = builder.dns_resolver(Arc::new(TrustDnsResolver::default())); + } + + #[cfg(feature = "__tls")] + { + let tls_ver = min_tls + .map(|tls| tls.max(DEFAULT_MIN_TLS)) + .unwrap_or(DEFAULT_MIN_TLS); + + builder = builder.min_tls_version(tls_ver.into()); + + for certificate in certificates { + builder = builder.add_root_certificate(certificate.0); + } + } + + #[cfg(all(reqwest_unstable, feature = "http3"))] + { + builder = builder.http3_congestion_bbr().tls_early_data(true); + } + + builder + } + + /// Construct a custom client from a [`reqwest::ClientBuilder`]. + /// + /// You may want to also use [`Client::default_builder`]. + pub fn from_builder( + builder: reqwest::ClientBuilder, + per_millis: NonZeroU16, + num_request: NonZeroU64, + ) -> Result { + let client = builder.build()?; + + Ok(Client(Arc::new(Inner { + client: client.clone(), + service: DelayRequest::new( + num_request, + Duration::from_millis(per_millis.get() as u64), + client, + ), + }))) + } + + /// Return inner reqwest client. + pub fn get_inner(&self) -> &reqwest::Client { + &self.0.client + } + + /// Return `Err(_)` for fatal error tht cannot be retried. + /// + /// Return `Ok(ControlFlow::Continue(res))` for retryable error, `res` + /// will contain the previous `Result`. + /// A retryable error could be a `ReqwestError` or `Response` with + /// unsuccessful status code. + /// + /// Return `Ok(ControlFlow::Break(response))` when succeeds and no need + /// to retry. + #[instrument( + skip(self, url), + fields( + url = format_args!("{url}"), + ), + )] + async fn do_send_request( + &self, + request: Request, + url: &Url, + ) -> Result>, ReqwestError> + { + static HEADER_VALUE_0: HeaderValue = HeaderValue::from_static("0"); + + let response = match self.0.service.call(request).await { + Err(err) if err.is_timeout() || err.is_connect() => { + let duration = RETRY_DURATION_FOR_TIMEOUT; + + info!("Received timeout error from reqwest. Delay future request by {duration:#?}"); + + self.0.service.add_urls_to_delay(&[url], duration); + + return Ok(ControlFlow::Continue(Err(err))); + } + res => res?, + }; + + let status = response.status(); + + let add_delay_and_continue = |response: reqwest::Response, duration| { + info!("Received status code {status}, will wait for {duration:#?} and retry"); + + self.0 + .service + .add_urls_to_delay(&[url, response.url()], duration); + + Ok(ControlFlow::Continue(Ok(response))) + }; + + let headers = response.headers(); + + // Some server (looking at you, github GraphQL API) may returns a rate limit + // even when OK is returned or on other status code (e.g. 453 forbidden). + if let Some(duration) = parse_header_retry_after(headers) { + add_delay_and_continue(response, duration.min(MAX_RETRY_DURATION)) + } else if headers.get("x-ratelimit-remaining") == Some(&HEADER_VALUE_0) { + let duration = headers + .get("x-ratelimit-reset") + .and_then(|value| { + let secs = value.to_str().ok()?.parse().ok()?; + Some(Duration::from_secs(secs)) + }) + .unwrap_or(DEFAULT_RETRY_DURATION_FOR_RATE_LIMIT) + .min(MAX_RETRY_DURATION); + + add_delay_and_continue(response, duration) + } else { + match status { + // Delay further request on rate limit + StatusCode::SERVICE_UNAVAILABLE | StatusCode::TOO_MANY_REQUESTS => { + add_delay_and_continue(response, DEFAULT_RETRY_DURATION_FOR_RATE_LIMIT) + } + + // Delay further request on timeout + StatusCode::REQUEST_TIMEOUT | StatusCode::GATEWAY_TIMEOUT => { + add_delay_and_continue(response, RETRY_DURATION_FOR_TIMEOUT) + } + + _ => Ok(ControlFlow::Break(response)), + } + } + } + + /// * `request` - `Request::try_clone` must always return `Some`. + async fn send_request_inner( + &self, + request: &Request, + ) -> Result { + let mut count = 0; + let max_retry_count = NonZeroU8::new(MAX_RETRY_COUNT).unwrap(); + + // Since max_retry_count is non-zero, there is at least one iteration. + loop { + // Increment the counter before checking for terminal condition. + count += 1; + + match self + .do_send_request(request.try_clone().unwrap(), request.url()) + .await? + { + ControlFlow::Break(response) => break Ok(response), + ControlFlow::Continue(res) if count >= max_retry_count.get() => { + break res; + } + _ => (), + } + } + } + + /// * `request` - `Request::try_clone` must always return `Some`. + async fn send_request( + &self, + request: Request, + error_for_status: bool, + ) -> Result { + debug!("Downloading from: '{}'", request.url()); + + self.send_request_inner(&request) + .await + .and_then(|response| { + if error_for_status { + response.error_for_status() + } else { + Ok(response) + } + }) + .map_err(|err| { + Error::Http(Box::new(HttpError { + method: request.method().clone(), + url: request.url().clone(), + err, + })) + }) + } + + async fn head_or_fallback_to_get( + &self, + url: Url, + error_for_status: bool, + ) -> Result { + let res = self + .send_request(Request::new(Method::HEAD, url.clone()), error_for_status) + .await; + + let retry_with_get = move || async move { + // Retry using GET + info!("HEAD on {url} is not allowed, fallback to GET"); + self.send_request(Request::new(Method::GET, url), error_for_status) + .await + }; + + let is_retryable = |status| { + matches!( + status, + StatusCode::BAD_REQUEST // 400 + | StatusCode::UNAUTHORIZED // 401 + | StatusCode::FORBIDDEN // 403 + | StatusCode::NOT_FOUND // 404 + | StatusCode::METHOD_NOT_ALLOWED // 405 + | StatusCode::GONE // 410 + ) + }; + + match res { + Err(Error::Http(http_error)) + if http_error.err.status().map(is_retryable).unwrap_or(false) => + { + retry_with_get().await + } + Ok(response) if is_retryable(response.status()) => retry_with_get().await, + res => res, + } + } + + /// Check if remote exists using `Method::GET`. + pub async fn remote_gettable(&self, url: Url) -> Result { + Ok(self.get(url).send(false).await?.status().is_success()) + } + + /// Attempt to get final redirected url using `Method::HEAD` or fallback + /// to `Method::GET`. + pub async fn get_redirected_final_url(&self, url: Url) -> Result { + self.head_or_fallback_to_get(url, true) + .await + .map(|response| response.url().clone()) + } + + /// Create `GET` request to `url` and return a stream of the response data. + /// On status code other than 200, it will return an error. + pub async fn get_stream( + &self, + url: Url, + ) -> Result>, Error> { + Ok(self.get(url).send(true).await?.bytes_stream()) + } + + /// Create a new request. + pub fn request(&self, method: Method, url: Url) -> RequestBuilder { + RequestBuilder { + client: self.clone(), + inner: self.0.client.request(method, url), + } + } + + /// Create a new GET request. + pub fn get(&self, url: Url) -> RequestBuilder { + self.request(Method::GET, url) + } + + /// Create a new POST request. + pub fn post(&self, url: Url, body: impl Into) -> RequestBuilder { + self.request(Method::POST, url).body(body.into()) + } +} + +fn parse_header_retry_after(headers: &HeaderMap) -> Option { + let header = headers + .get_all(RETRY_AFTER) + .into_iter() + .next_back()? + .to_str() + .ok()?; + + match header.parse::() { + Ok(dur) => Some(Duration::from_secs(dur)), + Err(_) => { + let system_time = parse_http_date(header).ok()?; + + let retry_after_unix_timestamp = + system_time.duration_since(SystemTime::UNIX_EPOCH).ok()?; + + let curr_time_unix_timestamp = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("SystemTime before UNIX EPOCH!"); + + // retry_after_unix_timestamp - curr_time_unix_timestamp + // If underflows, returns Duration::ZERO. + Some(retry_after_unix_timestamp.saturating_sub(curr_time_unix_timestamp)) + } + } +} diff --git a/crates/binstalk-downloader/src/remote/certificate.rs b/crates/binstalk-downloader/src/remote/certificate.rs new file mode 100644 index 00000000..29e9b93f --- /dev/null +++ b/crates/binstalk-downloader/src/remote/certificate.rs @@ -0,0 +1,32 @@ +#[cfg(feature = "__tls")] +use reqwest::tls; + +use super::Error; + +#[derive(Clone, Debug)] +pub struct Certificate(#[cfg(feature = "__tls")] pub(super) tls::Certificate); + +#[cfg_attr(not(feature = "__tls"), allow(unused_variables))] +impl Certificate { + /// Create a Certificate from a binary DER encoded certificate + pub fn from_der(der: impl AsRef<[u8]>) -> Result { + #[cfg(not(feature = "__tls"))] + return Ok(Self()); + + #[cfg(feature = "__tls")] + tls::Certificate::from_der(der.as_ref()) + .map(Self) + .map_err(Error::from) + } + + /// Create a Certificate from a PEM encoded certificate + pub fn from_pem(pem: impl AsRef<[u8]>) -> Result { + #[cfg(not(feature = "__tls"))] + return Ok(Self()); + + #[cfg(feature = "__tls")] + tls::Certificate::from_pem(pem.as_ref()) + .map(Self) + .map_err(Error::from) + } +} diff --git a/crates/binstalk-downloader/src/remote/delay_request.rs b/crates/binstalk-downloader/src/remote/delay_request.rs new file mode 100644 index 00000000..3feb804d --- /dev/null +++ b/crates/binstalk-downloader/src/remote/delay_request.rs @@ -0,0 +1,245 @@ +use std::{ + collections::HashMap, future::Future, iter::Peekable, num::NonZeroU64, ops::ControlFlow, + sync::Mutex, +}; + +use compact_str::{CompactString, ToCompactString}; +use reqwest::{Request, Url}; +use tokio::time::{sleep_until, Duration, Instant}; +use tracing::debug; + +pub(super) type RequestResult = Result; + +trait IterExt: Iterator { + fn dedup(self) -> Dedup + where + Self: Sized, + Self::Item: PartialEq, + { + Dedup(self.peekable()) + } +} + +impl IterExt for It {} + +struct Dedup(Peekable); + +impl Iterator for Dedup +where + It: Iterator, + It::Item: PartialEq, +{ + type Item = It::Item; + + fn next(&mut self) -> Option { + let curr = self.0.next()?; + + // Drop all consecutive dup values + while self.0.next_if_eq(&curr).is_some() {} + + Some(curr) + } +} + +#[derive(Debug)] +struct Inner { + client: reqwest::Client, + num_request: NonZeroU64, + per: Duration, + until: Instant, + state: State, +} + +#[derive(Debug)] +enum State { + Limited, + Ready { rem: NonZeroU64 }, +} + +impl Inner { + fn new(num_request: NonZeroU64, per: Duration, client: reqwest::Client) -> Self { + Inner { + client, + per, + num_request, + until: Instant::now() + per, + state: State::Ready { rem: num_request }, + } + } + + fn inc_rate_limit(&mut self) { + if let Some(num_request) = NonZeroU64::new(self.num_request.get() / 2) { + // If self.num_request.get() > 1, then cut it by half + self.num_request = num_request; + if let State::Ready { rem, .. } = &mut self.state { + *rem = num_request.min(*rem) + } + } + + let per = self.per; + if per < Duration::from_millis(700) { + self.per = per.mul_f32(1.2); + self.until += self.per - per; + } + } + + fn ready(&mut self) -> Readiness { + match self.state { + State::Ready { .. } => Readiness::Ready, + State::Limited => { + if self.until.elapsed().is_zero() { + Readiness::Limited(self.until) + } else { + // rate limit can be reset now and is ready + self.until = Instant::now() + self.per; + self.state = State::Ready { + rem: self.num_request, + }; + + Readiness::Ready + } + } + } + } + + fn call(&mut self, req: Request) -> impl Future { + match &mut self.state { + State::Ready { rem } => { + let now = Instant::now(); + + // If the period has elapsed, reset it. + if now >= self.until { + self.until = now + self.per; + *rem = self.num_request; + } + + if let Some(new_rem) = NonZeroU64::new(rem.get() - 1) { + *rem = new_rem; + } else { + // The service is disabled until further notice + self.state = State::Limited; + } + + // Call the inner future + self.client.execute(req) + } + State::Limited => panic!("service not ready; poll_ready must be called first"), + } + } +} + +enum Readiness { + Limited(Instant), + Ready, +} + +#[derive(Debug)] +pub(super) struct DelayRequest { + inner: Mutex, + hosts_to_delay: Mutex>, +} + +impl DelayRequest { + pub(super) fn new(num_request: NonZeroU64, per: Duration, client: reqwest::Client) -> Self { + Self { + inner: Mutex::new(Inner::new(num_request, per, client)), + hosts_to_delay: Default::default(), + } + } + + pub(super) fn add_urls_to_delay(&self, urls: &[&Url], delay_duration: Duration) { + let deadline = Instant::now() + delay_duration; + + let mut hosts_to_delay = self.hosts_to_delay.lock().unwrap(); + + urls.iter() + .filter_map(|url| url.host_str()) + .dedup() + .for_each(|host| { + hosts_to_delay + .entry(host.to_compact_string()) + .and_modify(|old_dl| { + *old_dl = deadline.max(*old_dl); + }) + .or_insert(deadline); + }); + } + + fn get_delay_until(&self, host: &str) -> Option { + let mut hosts_to_delay = self.hosts_to_delay.lock().unwrap(); + + hosts_to_delay.get(host).copied().and_then(|until| { + if until.elapsed().is_zero() { + Some(until) + } else { + // We have already gone past the deadline, + // so we should remove it instead. + hosts_to_delay.remove(host); + None + } + }) + } + + // Define a new function so that the guard will be dropped ASAP and not + // included in the future. + fn call_inner( + &self, + counter: &mut u32, + req: &mut Option, + ) -> ControlFlow, Instant> { + // Wait until we are ready to send next requests + // (client-side rate-limit throttler). + let mut guard = self.inner.lock().unwrap(); + + if let Readiness::Limited(until) = guard.ready() { + ControlFlow::Continue(until) + } else if let Some(until) = req + .as_ref() + .unwrap() + .url() + .host_str() + .and_then(|host| self.get_delay_until(host)) + { + // If the host rate-limit us, then wait until then + // and try again (server-side rate-limit throttler). + + // Try increasing client-side rate-limit throttler to prevent + // rate-limit in the future. + guard.inc_rate_limit(); + + let additional_delay = + Duration::from_millis(200) + Duration::from_millis(100) * 20.min(*counter); + + *counter += 1; + + debug!("server-side rate limit exceeded; sleeping."); + ControlFlow::Continue(until + additional_delay) + } else { + ControlFlow::Break(guard.call(req.take().unwrap())) + } + } + + pub(super) async fn call(&self, req: Request) -> RequestResult { + // Put all variables in a block so that will be dropped before polling + // the future returned by reqwest. + { + let mut counter = 0; + // Use Option here so that we don't have to move entire `Request` + // twice when calling `self.call_inner` while retain the ability to + // take its value without boxing. + // + // This will be taken when `ControlFlow::Break` is then it will + // break the loop, so it will never call `self.call_inner` with + // a `None`. + let mut req = Some(req); + + loop { + match self.call_inner(&mut counter, &mut req) { + ControlFlow::Continue(until) => sleep_until(until).await, + ControlFlow::Break(future) => break future, + } + } + } + .await + } +} diff --git a/crates/binstalk-downloader/src/remote/request_builder.rs b/crates/binstalk-downloader/src/remote/request_builder.rs new file mode 100644 index 00000000..4dfacd30 --- /dev/null +++ b/crates/binstalk-downloader/src/remote/request_builder.rs @@ -0,0 +1,120 @@ +use std::fmt; + +use bytes::Bytes; +use futures_util::{Stream, StreamExt}; +use reqwest::Method; + +use super::{header, Client, Error, HttpError, StatusCode, Url}; + +pub use reqwest::Body; + +#[cfg(feature = "json")] +pub use serde_json::Error as JsonError; + +#[derive(Debug)] +pub struct RequestBuilder { + pub(super) client: Client, + pub(super) inner: reqwest::RequestBuilder, +} + +impl RequestBuilder { + pub fn bearer_auth(self, token: &dyn fmt::Display) -> Self { + Self { + client: self.client, + inner: self.inner.bearer_auth(token), + } + } + + pub fn header(self, key: &str, value: &str) -> Self { + Self { + client: self.client, + inner: self.inner.header(key, value), + } + } + + pub fn body(self, body: impl Into) -> Self { + Self { + client: self.client, + inner: self.inner.body(body.into()), + } + } + + pub async fn send(self, error_for_status: bool) -> Result { + let request = self.inner.build()?; + let method = request.method().clone(); + Ok(Response { + inner: self.client.send_request(request, error_for_status).await?, + method, + }) + } +} + +#[derive(Debug)] +pub struct Response { + inner: reqwest::Response, + method: Method, +} + +impl Response { + pub async fn bytes(self) -> Result { + self.inner.bytes().await.map_err(Error::from) + } + + pub fn bytes_stream(self) -> impl Stream> { + let url = Box::new(self.inner.url().clone()); + let method = self.method; + + self.inner.bytes_stream().map(move |res| { + res.map_err(|err| { + Error::Http(Box::new(HttpError { + method: method.clone(), + url: Url::clone(&*url), + err, + })) + }) + }) + } + + pub fn status(&self) -> StatusCode { + self.inner.status() + } + + pub fn url(&self) -> &Url { + self.inner.url() + } + + pub fn method(&self) -> &Method { + &self.method + } + + pub fn error_for_status_ref(&self) -> Result<&Self, Error> { + match self.inner.error_for_status_ref() { + Ok(_) => Ok(self), + Err(err) => Err(Error::Http(Box::new(HttpError { + method: self.method().clone(), + url: self.url().clone(), + err, + }))), + } + } + + pub fn error_for_status(self) -> Result { + match self.error_for_status_ref() { + Ok(_) => Ok(self), + Err(err) => Err(err), + } + } + + pub fn headers(&self) -> &header::HeaderMap { + self.inner.headers() + } + + #[cfg(feature = "json")] + pub async fn json(self) -> Result + where + T: serde::de::DeserializeOwned, + { + let bytes = self.error_for_status()?.bytes().await?; + Ok(serde_json::from_slice(&bytes)?) + } +} diff --git a/crates/binstalk-downloader/src/remote/resolver.rs b/crates/binstalk-downloader/src/remote/resolver.rs new file mode 100644 index 00000000..1df98aab --- /dev/null +++ b/crates/binstalk-downloader/src/remote/resolver.rs @@ -0,0 +1,94 @@ +use std::{net::SocketAddr, sync::Arc}; + +use hickory_resolver::{ + config::{LookupIpStrategy, ResolverConfig, ResolverOpts}, + system_conf, TokioResolver as TokioAsyncResolver, +}; +use once_cell::sync::OnceCell; +use reqwest::dns::{Addrs, Name, Resolve, Resolving}; +use tracing::{debug, instrument, warn}; + +#[cfg(windows)] +use hickory_resolver::{config::NameServerConfig, proto::xfer::Protocol}; + +type BoxError = Box; + +#[derive(Debug, Default, Clone)] +pub struct TrustDnsResolver(Arc>); + +impl Resolve for TrustDnsResolver { + fn resolve(&self, name: Name) -> Resolving { + let resolver = self.clone(); + Box::pin(async move { + let resolver = resolver.0.get_or_try_init(new_resolver)?; + + let lookup = resolver.lookup_ip(name.as_str()).await?; + let addrs: Addrs = Box::new(lookup.into_iter().map(|ip| SocketAddr::new(ip, 0))); + Ok(addrs) + }) + } +} + +#[cfg(unix)] +fn get_configs() -> Result<(ResolverConfig, ResolverOpts), BoxError> { + debug!("Using system DNS resolver configuration"); + system_conf::read_system_conf().map_err(Into::into) +} + +#[cfg(windows)] +fn get_configs() -> Result<(ResolverConfig, ResolverOpts), BoxError> { + debug!("Using custom DNS resolver configuration"); + let mut config = ResolverConfig::new(); + let opts = ResolverOpts::default(); + + get_adapter()?.dns_servers().iter().for_each(|addr| { + tracing::trace!("Adding DNS server: {}", addr); + let socket_addr = SocketAddr::new(*addr, 53); + for protocol in [Protocol::Udp, Protocol::Tcp] { + config.add_name_server(NameServerConfig { + socket_addr, + protocol, + tls_dns_name: None, + trust_negative_responses: false, + bind_addr: None, + http_endpoint: None, + }) + } + }); + + Ok((config, opts)) +} + +#[instrument] +fn new_resolver() -> Result { + let (config, mut opts) = get_configs()?; + + debug!("Resolver configuration complete"); + opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6; + let mut builder = TokioAsyncResolver::builder_with_config(config, Default::default()); + *builder.options_mut() = opts; + Ok(builder.build()) +} + +#[cfg(windows)] +#[instrument] +fn get_adapter() -> Result { + debug!("Retrieving local IP address"); + let local_ip = + default_net::interface::get_local_ipaddr().ok_or("Local IP address not found")?; + debug!("Local IP address: {local_ip}"); + debug!("Retrieving network adapters"); + let adapters = ipconfig::get_adapters()?; + debug!("Found {} network adapters", adapters.len()); + debug!("Searching for adapter with IP address {local_ip}"); + let adapter = adapters + .into_iter() + .find(|adapter| adapter.ip_addresses().contains(&local_ip)) + .ok_or("Adapter not found")?; + debug!( + "Using adapter {} with {} DNS servers", + adapter.friendly_name(), + adapter.dns_servers().len() + ); + Ok(adapter) +} diff --git a/crates/binstalk-downloader/src/remote/tls_version.rs b/crates/binstalk-downloader/src/remote/tls_version.rs new file mode 100644 index 00000000..06bbedff --- /dev/null +++ b/crates/binstalk-downloader/src/remote/tls_version.rs @@ -0,0 +1,37 @@ +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +enum Inner { + Tls1_2 = 0, + Tls1_3 = 1, +} + +/// TLS version for [`crate::remote::Client`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct TLSVersion(Inner); + +impl TLSVersion { + pub const TLS_1_2: TLSVersion = TLSVersion(Inner::Tls1_2); + pub const TLS_1_3: TLSVersion = TLSVersion(Inner::Tls1_3); +} + +#[cfg(feature = "__tls")] +impl From for reqwest::tls::Version { + fn from(ver: TLSVersion) -> reqwest::tls::Version { + use reqwest::tls::Version; + use Inner::*; + + match ver.0 { + Tls1_2 => Version::TLS_1_2, + Tls1_3 => Version::TLS_1_3, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_tls_version_order() { + assert!(TLSVersion::TLS_1_2 < TLSVersion::TLS_1_3); + } +} diff --git a/crates/binstalk-downloader/src/utils.rs b/crates/binstalk-downloader/src/utils.rs new file mode 100644 index 00000000..0fcb4f57 --- /dev/null +++ b/crates/binstalk-downloader/src/utils.rs @@ -0,0 +1,169 @@ +use std::{ + future::Future, + io::{self, BufRead, Read}, +}; + +use bytes::{Buf, Bytes}; +use futures_util::{FutureExt, Stream, StreamExt}; +use tokio::{sync::mpsc, task}; + +pub(super) fn extract_with_blocking_task( + stream: S, + f: F, +) -> impl Future> +where + T: Send + 'static, + E: From, + E: From, + S: Stream> + Send + Sync + Unpin, + F: FnOnce(mpsc::Receiver) -> io::Result + Send + Sync + 'static, +{ + async fn inner( + mut stream: S, + task: Fut, + tx: mpsc::Sender, + ) -> Result + where + E: From, + E: From, + // We do not use trait object for S since there will only be one + // S used with this function. + S: Stream> + Send + Sync + Unpin, + // asyncify would always return the same future, so no need to + // use trait object here. + Fut: Future> + Send + Sync, + { + let read_fut = async move { + while let Some(bytes) = stream.next().await.transpose()? { + if bytes.is_empty() { + continue; + } + + if tx.send(bytes).await.is_err() { + // The extract tar returns, which could be that: + // - Extraction fails with an error + // - Extraction success without the rest of the data + // + // + // It's hard to tell the difference here, so we assume + // the first scienario occurs. + // + // Even if the second scienario occurs, it won't affect the + // extraction process anyway, so we can jsut ignore it. + return Ok(()); + } + } + + Ok::<_, E>(()) + }; + tokio::pin!(read_fut); + + let task_fut = async move { task.await.map_err(E::from) }; + tokio::pin!(task_fut); + + tokio::select! { + biased; + + res = &mut read_fut => { + // The stream reaches eof, propagate error and wait for + // read task to be done. + res?; + + task_fut.await + }, + res = &mut task_fut => { + // The task finishes before the read task, return early + // after checking for errors in read_fut. + if let Some(Err(err)) = read_fut.now_or_never() { + Err(err) + } else { + res + } + } + } + } + + // Use channel size = 5 to minimize the waiting time in the extraction task + let (tx, rx) = mpsc::channel(5); + + let task = asyncify(move || f(rx)); + + inner(stream, task, tx) +} + +/// Copied from tokio https://docs.rs/tokio/latest/src/tokio/fs/mod.rs.html#132 +pub(super) fn asyncify(f: F) -> impl Future> + Send + Sync + 'static +where + F: FnOnce() -> io::Result + Send + 'static, + T: Send + 'static, +{ + async fn inner(handle: task::JoinHandle>) -> io::Result { + match handle.await { + Ok(res) => res, + Err(err) => Err(io::Error::other(format!("background task failed: {err}"))), + } + } + + inner(task::spawn_blocking(f)) +} + +/// This wraps an AsyncIterator as a `Read`able. +/// It must be used in non-async context only, +/// meaning you have to use it with +/// `tokio::task::{block_in_place, spawn_blocking}` or +/// `std::thread::spawn`. +pub(super) struct StreamReadable { + rx: mpsc::Receiver, + bytes: Bytes, +} + +impl StreamReadable { + pub(super) fn new(rx: mpsc::Receiver) -> Self { + Self { + rx, + bytes: Bytes::new(), + } + } +} + +impl Read for StreamReadable { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if buf.is_empty() { + return Ok(0); + } + + if self.fill_buf()?.is_empty() { + return Ok(0); + } + + let bytes = &mut self.bytes; + + // copy_to_slice requires the bytes to have enough remaining bytes + // to fill buf. + let n = buf.len().min(bytes.remaining()); + + // ::copy_to_slice copies and consumes the bytes + bytes.copy_to_slice(&mut buf[..n]); + + Ok(n) + } +} + +impl BufRead for StreamReadable { + fn fill_buf(&mut self) -> io::Result<&[u8]> { + let bytes = &mut self.bytes; + + if !bytes.has_remaining() { + if let Some(new_bytes) = self.rx.blocking_recv() { + // new_bytes are guaranteed to be non-empty. + *bytes = new_bytes; + } + } + + Ok(&*bytes) + } + + fn consume(&mut self, amt: usize) { + self.bytes.advance(amt); + } +} diff --git a/crates/binstalk-fetchers/CHANGELOG.md b/crates/binstalk-fetchers/CHANGELOG.md new file mode 100644 index 00000000..a1160400 --- /dev/null +++ b/crates/binstalk-fetchers/CHANGELOG.md @@ -0,0 +1,151 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.10.21](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.20...binstalk-fetchers-v0.10.21) - 2025-06-06 + +### Other + +- updated the following local packages: binstalk-types, binstalk-downloader, binstalk-downloader, binstalk-git-repo-api + +## [0.10.20](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.19...binstalk-fetchers-v0.10.20) - 2025-05-30 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader, binstalk-git-repo-api + +## [0.10.19](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.18...binstalk-fetchers-v0.10.19) - 2025-05-16 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader, binstalk-git-repo-api + +## [0.10.18](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.17...binstalk-fetchers-v0.10.18) - 2025-04-05 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader, binstalk-git-repo-api + +## [0.10.17](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.16...binstalk-fetchers-v0.10.17) - 2025-03-19 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.10.16](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.15...binstalk-fetchers-v0.10.16) - 2025-03-15 + +### Other + +- *(deps)* bump the deps group with 2 updates ([#2084](https://github.com/cargo-bins/cargo-binstall/pull/2084)) +- *(deps)* bump tokio from 1.43.0 to 1.44.0 in the deps group ([#2079](https://github.com/cargo-bins/cargo-binstall/pull/2079)) + +## [0.10.15](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.14...binstalk-fetchers-v0.10.15) - 2025-03-07 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#2072](https://github.com/cargo-bins/cargo-binstall/pull/2072)) + +## [0.10.14](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.13...binstalk-fetchers-v0.10.14) - 2025-02-28 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.10.13](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.12...binstalk-fetchers-v0.10.13) - 2025-02-11 + +### Other + +- *(deps)* bump the deps group with 2 updates (#2044) + +## [0.10.12](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.11...binstalk-fetchers-v0.10.12) - 2025-02-04 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.10.11](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.10...binstalk-fetchers-v0.10.11) - 2025-01-19 + +### Other + +- update Cargo.lock dependencies + +## [0.10.10](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.9...binstalk-fetchers-v0.10.10) - 2025-01-13 + +### Other + +- update Cargo.lock dependencies + +## [0.10.9](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.8...binstalk-fetchers-v0.10.9) - 2025-01-11 + +### Other + +- *(deps)* bump the deps group with 3 updates (#2015) + +## [0.10.8](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.7...binstalk-fetchers-v0.10.8) - 2025-01-04 + +### Other + +- *(deps)* bump the deps group with 2 updates (#2010) + +## [0.10.7](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.6...binstalk-fetchers-v0.10.7) - 2024-12-14 + +### Other + +- *(deps)* bump the deps group with 2 updates (#1997) + +## [0.10.6](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.5...binstalk-fetchers-v0.10.6) - 2024-11-29 + +### Other + +- Upgrade transitive dependencies ([#1985](https://github.com/cargo-bins/cargo-binstall/pull/1985)) + +## [0.10.5](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.4...binstalk-fetchers-v0.10.5) - 2024-11-23 + +### Other + +- *(deps)* bump the deps group with 2 updates ([#1981](https://github.com/cargo-bins/cargo-binstall/pull/1981)) + +## [0.10.4](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.3...binstalk-fetchers-v0.10.4) - 2024-11-09 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1966](https://github.com/cargo-bins/cargo-binstall/pull/1966)) + +## [0.10.3](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.2...binstalk-fetchers-v0.10.3) - 2024-11-05 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1954](https://github.com/cargo-bins/cargo-binstall/pull/1954)) + +## [0.10.2](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.1...binstalk-fetchers-v0.10.2) - 2024-11-02 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.10.1](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.10.0...binstalk-fetchers-v0.10.1) - 2024-10-12 + +### Other + +- updated the following local packages: binstalk-git-repo-api + +## [0.10.0](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.9.1...binstalk-fetchers-v0.10.0) - 2024-09-11 + +### Other + +- report to new stats server (with status) ([#1912](https://github.com/cargo-bins/cargo-binstall/pull/1912)) +- Improve quickinstall telemetry failure message ([#1910](https://github.com/cargo-bins/cargo-binstall/pull/1910)) + +## [0.9.1](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.9.0...binstalk-fetchers-v0.9.1) - 2024-08-12 + +### Other +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.9.0](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-fetchers-v0.8.0...binstalk-fetchers-v0.9.0) - 2024-08-10 + +### Other +- updated the following local packages: binstalk-types, binstalk-downloader, binstalk-downloader diff --git a/crates/binstalk-fetchers/Cargo.toml b/crates/binstalk-fetchers/Cargo.toml new file mode 100644 index 00000000..8f716e1d --- /dev/null +++ b/crates/binstalk-fetchers/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "binstalk-fetchers" +version = "0.10.21" +edition = "2021" + +description = "The binstall fetchers" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/binstalk-fetchers" +rust-version = "1.70.0" +authors = ["Jiahao XU "] +license = "GPL-3.0-only" + +[dependencies] +async-trait = "0.1.88" +binstalk-downloader = { version = "0.13.20", path = "../binstalk-downloader", default-features = false } +binstalk-git-repo-api = { version = "0.5.22", path = "../binstalk-git-repo-api" } +binstalk-types = { version = "0.10.0", path = "../binstalk-types" } +bytes = "1.4.0" +compact_str = { version = "0.9.0" } +either = "1.11.0" +itertools = "0.14.0" +leon = "3.0.0" +leon-macros = "1.0.1" +miette = "7.0.0" +minisign-verify = "0.2.1" +once_cell = "1.18.0" +strum = "0.27.0" +thiserror = "2.0.11" +tokio = { version = "1.44.0", features = [ + "rt", + "sync", +], default-features = false } +tracing = "0.1.39" +url = "2.5.4" + +[dev-dependencies] +binstalk-downloader = { version = "0.13.20", path = "../binstalk-downloader" } + +[features] +quickinstall = [] + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +all-features = true diff --git a/crates/binstalk-fetchers/LICENSE b/crates/binstalk-fetchers/LICENSE new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/crates/binstalk-fetchers/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/crates/binstalk-fetchers/src/common.rs b/crates/binstalk-fetchers/src/common.rs new file mode 100644 index 00000000..ad9e1d68 --- /dev/null +++ b/crates/binstalk-fetchers/src/common.rs @@ -0,0 +1,120 @@ +#![allow(unused)] + +use std::{ + future::Future, + sync::{ + atomic::{AtomicBool, Ordering::Relaxed}, + Once, + }, +}; + +pub(super) use binstalk_downloader::{ + download::{Download, ExtractedFiles}, + remote::{Client, Url}, +}; +pub(super) use binstalk_git_repo_api::gh_api_client::GhApiClient; +use binstalk_git_repo_api::gh_api_client::{GhApiError, GhReleaseArtifact, GhReleaseArtifactUrl}; +pub(super) use binstalk_types::cargo_toml_binstall::{PkgFmt, PkgMeta}; +pub(super) use compact_str::CompactString; +pub(super) use tokio::task::JoinHandle; +pub(super) use tracing::{debug, instrument, warn}; + +use crate::FetchError; + +static WARN_RATE_LIMIT_ONCE: Once = Once::new(); +static WARN_UNAUTHORIZED_ONCE: Once = Once::new(); + +/// Return Ok(Some(api_artifact_url)) if exists, or Ok(None) if it doesn't. +/// +/// Caches info on all artifacts matching (repo, tag). +pub(super) async fn get_gh_release_artifact_url( + gh_api_client: GhApiClient, + artifact: GhReleaseArtifact, +) -> Result, GhApiError> { + debug!("Using GitHub API to check for existence of artifact, which will also cache the API response"); + + // The future returned has the same size as a pointer + match gh_api_client.has_release_artifact(artifact).await { + Ok(ret) => Ok(ret), + Err(GhApiError::NotFound) => Ok(None), + + Err(GhApiError::RateLimit { retry_after }) => { + WARN_RATE_LIMIT_ONCE.call_once(|| { + warn!("Your GitHub API token (if any) has reached its rate limit and cannot be used again until {retry_after:?}, so we will fallback to HEAD/GET on the url."); + warn!("If you did not supply a github token, consider doing so: GitHub limits unauthorized users to 60 requests per hour per origin IP address."); + }); + Err(GhApiError::RateLimit { retry_after }) + } + Err(GhApiError::Unauthorized) => { + WARN_UNAUTHORIZED_ONCE.call_once(|| { + warn!("GitHub API somehow requires a token for the API access, so we will fallback to HEAD/GET on the url."); + warn!("Please consider supplying a token to cargo-binstall to speedup resolution."); + }); + Err(GhApiError::Unauthorized) + } + + Err(err) => Err(err), + } +} + +/// Check if the URL exists by querying the GitHub API. +/// +/// Caches info on all artifacts matching (repo, tag). +/// +/// This function returns a future where its size should be at most size of +/// 2-4 pointers. +pub(super) async fn does_url_exist( + client: Client, + gh_api_client: GhApiClient, + url: &Url, +) -> Result { + static GH_API_CLIENT_FAILED: AtomicBool = AtomicBool::new(false); + + debug!("Checking for package at: '{url}'"); + + if !GH_API_CLIENT_FAILED.load(Relaxed) { + if let Some(artifact) = GhReleaseArtifact::try_extract_from_url(url) { + match get_gh_release_artifact_url(gh_api_client, artifact).await { + Ok(ret) => return Ok(ret.is_some()), + + Err(GhApiError::RateLimit { .. }) | Err(GhApiError::Unauthorized) => {} + + Err(err) => return Err(err.into()), + } + + GH_API_CLIENT_FAILED.store(true, Relaxed); + } + } + + Ok(Box::pin(client.remote_gettable(url.clone())).await?) +} + +#[derive(Debug)] +pub(super) struct AutoAbortJoinHandle(JoinHandle); + +impl AutoAbortJoinHandle +where + T: Send + 'static, +{ + pub(super) fn spawn(future: F) -> Self + where + F: Future + Send + 'static, + { + Self(tokio::spawn(future)) + } +} + +impl Drop for AutoAbortJoinHandle { + fn drop(&mut self) { + self.0.abort(); + } +} + +impl AutoAbortJoinHandle> +where + E: Into, +{ + pub(super) async fn flattened_join(mut self) -> Result { + (&mut self.0).await?.map_err(Into::into) + } +} diff --git a/crates/binstalk-fetchers/src/futures_resolver.rs b/crates/binstalk-fetchers/src/futures_resolver.rs new file mode 100644 index 00000000..461ab462 --- /dev/null +++ b/crates/binstalk-fetchers/src/futures_resolver.rs @@ -0,0 +1,86 @@ +use std::{fmt::Debug, future::Future, pin::Pin}; + +use tokio::sync::mpsc; +use tracing::warn; + +/// Given multiple futures with output = `Result, E>`, +/// returns the the first one that returns either `Err(_)` or +/// `Ok(Some(_))`. +pub struct FuturesResolver { + rx: mpsc::Receiver>, + tx: mpsc::Sender>, +} + +impl Default for FuturesResolver { + fn default() -> Self { + // We only need the first one, so the channel is of size 1. + let (tx, rx) = mpsc::channel(1); + Self { tx, rx } + } +} + +impl FuturesResolver { + /// Insert new future into this resolver, they will start running + /// right away. + pub fn push(&self, fut: Fut) + where + Fut: Future, E>> + Send + 'static, + { + let tx = self.tx.clone(); + + tokio::spawn(async move { + tokio::pin!(fut); + + Self::spawn_inner(fut, tx).await; + }); + } + + async fn spawn_inner( + fut: Pin<&mut (dyn Future, E>> + Send)>, + tx: mpsc::Sender>, + ) { + let res = tokio::select! { + biased; + + _ = tx.closed() => return, + res = fut => res, + }; + + if let Some(res) = res.transpose() { + // try_send can only fail due to being full or being closed. + // + // In both cases, this could means some other future has + // completed first. + // + // For closed, it could additionally means that the task + // is cancelled. + tx.try_send(res).ok(); + } + } + + /// Insert multiple futures into this resolver, they will start running + /// right away. + pub fn extend(&self, iter: Iter) + where + Fut: Future, E>> + Send + 'static, + Iter: IntoIterator, + { + iter.into_iter().for_each(|fut| self.push(fut)); + } + + /// Return the resolution. + pub fn resolve(self) -> impl Future> { + let mut rx = self.rx; + drop(self.tx); + + async move { + loop { + match rx.recv().await { + Some(Ok(ret)) => return Some(ret), + Some(Err(err)) => warn!(?err, "Fail to resolve the future"), + None => return None, + } + } + } + } +} diff --git a/crates/binstalk-fetchers/src/gh_crate_meta.rs b/crates/binstalk-fetchers/src/gh_crate_meta.rs new file mode 100644 index 00000000..d1df0637 --- /dev/null +++ b/crates/binstalk-fetchers/src/gh_crate_meta.rs @@ -0,0 +1,660 @@ +use std::{borrow::Cow, fmt, iter, path::Path, sync::Arc}; + +use binstalk_git_repo_api::gh_api_client::{GhApiError, GhReleaseArtifact, GhReleaseArtifactUrl}; +use binstalk_types::cargo_toml_binstall::Strategy; +use compact_str::{CompactString, ToCompactString}; +use either::Either; +use leon::Template; +use once_cell::sync::OnceCell; +use strum::IntoEnumIterator; +use tokio::time::sleep; +use tracing::{debug, info, trace, warn}; +use url::Url; + +use crate::{ + common::*, futures_resolver::FuturesResolver, Data, FetchError, InvalidPkgFmtError, RepoInfo, + SignaturePolicy, SignatureVerifier, TargetDataErased, DEFAULT_GH_API_RETRY_DURATION, +}; + +pub const FETCHER_GH_CRATE_META: &str = "GhCrateMeta"; + +pub(crate) mod hosting; + +pub struct GhCrateMeta { + client: Client, + gh_api_client: GhApiClient, + data: Arc, + target_data: Arc, + signature_policy: SignaturePolicy, + resolution: OnceCell, +} + +#[derive(Debug)] +struct Resolved { + url: Url, + pkg_fmt: PkgFmt, + archive_suffix: Option, + repo: Option, + subcrate: Option, + gh_release_artifact_url: Option, + is_repo_private: bool, +} + +impl GhCrateMeta { + fn launch_baseline_find_tasks( + &self, + futures_resolver: &FuturesResolver, + pkg_fmt: PkgFmt, + pkg_url: &Template<'_>, + repo: Option<&str>, + subcrate: Option<&str>, + is_repo_private: bool, + ) { + let render_url = |ext| { + let ctx = Context::from_data_with_repo( + &self.data, + &self.target_data.target, + &self.target_data.target_related_info, + ext, + repo, + subcrate, + ); + match ctx.render_url_with(pkg_url) { + Ok(url) => Some(url), + Err(err) => { + warn!("Failed to render url for {ctx:#?}: {err}"); + None + } + } + }; + + let is_windows = self.target_data.target.contains("windows"); + + let urls = if pkg_url.has_any_of_keys(&["format", "archive-format", "archive-suffix"]) { + // build up list of potential URLs + Either::Left( + pkg_fmt + .extensions(is_windows) + .iter() + .filter_map(|ext| render_url(Some(ext)).map(|url| (url, Some(ext)))), + ) + } else { + Either::Right(render_url(None).map(|url| (url, None)).into_iter()) + }; + + // go check all potential URLs at once + futures_resolver.extend(urls.map(move |(url, ext)| { + let client = self.client.clone(); + let gh_api_client = self.gh_api_client.clone(); + + let repo = repo.map(ToString::to_string); + let subcrate = subcrate.map(ToString::to_string); + let archive_suffix = ext.map(ToString::to_string); + let gh_release_artifact = GhReleaseArtifact::try_extract_from_url(&url); + + async move { + debug!("Checking for package at: '{url}'"); + + let mut resolved = Resolved { + url: url.clone(), + pkg_fmt, + repo, + subcrate, + archive_suffix, + is_repo_private, + gh_release_artifact_url: None, + }; + + if let Some(artifact) = gh_release_artifact { + loop { + match get_gh_release_artifact_url(gh_api_client.clone(), artifact.clone()) + .await + { + Ok(Some(artifact_url)) => { + resolved.gh_release_artifact_url = Some(artifact_url); + return Ok(Some(resolved)); + } + Ok(None) => return Ok(None), + + Err(GhApiError::RateLimit { retry_after }) => { + sleep(retry_after.unwrap_or(DEFAULT_GH_API_RETRY_DURATION)).await; + } + Err(GhApiError::Unauthorized) if !is_repo_private => break, + + Err(err) => return Err(err.into()), + } + } + } + + Ok(Box::pin(client.remote_gettable(url)) + .await? + .then_some(resolved)) + } + })); + } +} + +#[async_trait::async_trait] +impl super::Fetcher for GhCrateMeta { + fn new( + client: Client, + gh_api_client: GhApiClient, + data: Arc, + target_data: Arc, + signature_policy: SignaturePolicy, + ) -> Arc { + Arc::new(Self { + client, + gh_api_client, + data, + target_data, + signature_policy, + resolution: OnceCell::new(), + }) + } + + fn find(self: Arc) -> JoinHandle> { + tokio::spawn(async move { + let info = self.data.get_repo_info(&self.gh_api_client).await?; + + let repo = info.map(|info| &info.repo); + let subcrate = info.and_then(|info| info.subcrate.as_deref()); + let is_repo_private = info.map(|info| info.is_private).unwrap_or_default(); + + let mut pkg_fmt = self.target_data.meta.pkg_fmt; + + let pkg_urls = if let Some(pkg_url) = self.target_data.meta.pkg_url.as_deref() { + let template = Template::parse(pkg_url)?; + + if pkg_fmt.is_none() + && !template.has_any_of_keys(&["format", "archive-format", "archive-suffix"]) + { + // The crate does not specify the pkg-fmt, yet its pkg-url + // template doesn't contains format, archive-format or + // archive-suffix which is required for automatically + // deducing the pkg-fmt. + // + // We will attempt to guess the pkg-fmt there, but this is + // just a best-effort + pkg_fmt = PkgFmt::guess_pkg_format(pkg_url); + + let crate_name = &self.data.name; + let version = &self.data.version; + let target = &self.target_data.target; + + if pkg_fmt.is_none() { + return Err(InvalidPkgFmtError { + crate_name: crate_name.clone(), + version: version.clone(), + target: target.into(), + pkg_url: pkg_url.into(), + reason: + &"pkg-fmt is not specified, yet pkg-url does not contain format, \ + archive-format or archive-suffix which is required for automatically \ + deducing pkg-fmt", + } + .into()); + } + + warn!( + "Crate {crate_name}@{version} on target {target} does not specify pkg-fmt \ + but its pkg-url also does not contain key format, archive-format or \ + archive-suffix.\nbinstall was able to guess that from pkg-url, but \ + just note that it could be wrong:\npkg-fmt=\"{pkg_fmt}\", pkg-url=\"{pkg_url}\"", + pkg_fmt = pkg_fmt.unwrap(), + ); + } + + Either::Left(iter::once(template)) + } else if let Some(RepoInfo { + repo, + repository_host, + .. + }) = info + { + if let Some(pkg_urls) = repository_host.get_default_pkg_url_template() { + let has_subcrate = subcrate.is_some(); + + Either::Right( + pkg_urls + .map(Template::cast) + // If subcrate is Some, then all templates will be included. + // Otherwise, only templates without key "subcrate" will be + // included. + .filter(move |template| has_subcrate || !template.has_key("subcrate")), + ) + } else { + warn!( + concat!( + "Unknown repository {}, cargo-binstall cannot provide default pkg_url for it.\n", + "Please ask the upstream to provide it for target {}." + ), + repo, self.target_data.target + ); + + return Ok(false); + } + } else { + warn!( + concat!( + "Package does not specify repository, cargo-binstall cannot provide default pkg_url for it.\n", + "Please ask the upstream to provide it for target {}." + ), + self.target_data.target + ); + + return Ok(false); + }; + + // Convert Option to Option to reduce size of future. + let repo = repo.map(|u| u.as_str().trim_end_matches('/')); + + // Use reference to self to fix error of closure + // launch_baseline_find_tasks which moves `this` + let this = &self; + + let pkg_fmts = if let Some(pkg_fmt) = pkg_fmt { + Either::Left(iter::once(pkg_fmt)) + } else { + Either::Right(PkgFmt::iter()) + }; + + let resolver = FuturesResolver::default(); + + // Iterate over pkg_urls first to avoid String::clone. + for pkg_url in pkg_urls { + // Clone iter pkg_fmts to ensure all pkg_fmts is + // iterated over for each pkg_url, which is + // basically cartesian product. + // | + for pkg_fmt in pkg_fmts.clone() { + this.launch_baseline_find_tasks( + &resolver, + pkg_fmt, + &pkg_url, + repo, + subcrate, + is_repo_private, + ); + } + } + + if let Some(resolved) = resolver.resolve().await { + debug!(?resolved, "Winning URL found!"); + self.resolution + .set(resolved) + .expect("find() should be only called once"); + Ok(true) + } else { + Ok(false) + } + }) + } + + async fn fetch_and_extract(&self, dst: &Path) -> Result { + let resolved = self + .resolution + .get() + .expect("find() should be called once before fetch_and_extract()"); + trace!(?resolved, "preparing to fetch"); + + let verifier = match (self.signature_policy, &self.target_data.meta.signing) { + (SignaturePolicy::Ignore, _) | (SignaturePolicy::IfPresent, None) => { + SignatureVerifier::Noop + } + (SignaturePolicy::Require, None) => { + return Err(FetchError::MissingSignature); + } + (_, Some(config)) => { + let template = match config.file.as_deref() { + Some(file) => Template::parse(file)?, + None => leon_macros::template!("{ url }.sig"), + }; + trace!(?template, "parsed signature file template"); + + let sign_url = Context::from_data_with_repo( + &self.data, + &self.target_data.target, + &self.target_data.target_related_info, + resolved.archive_suffix.as_deref(), + resolved.repo.as_deref(), + resolved.subcrate.as_deref(), + ) + .with_url(&resolved.url) + .render_url_with(&template)?; + + debug!(?sign_url, "Downloading signature"); + let signature = Download::new(self.client.clone(), sign_url) + .into_bytes() + .await?; + trace!(?signature, "got signature contents"); + + SignatureVerifier::new(config, &signature)? + } + }; + + debug!( + url=%resolved.url, + dst=%dst.display(), + fmt=?resolved.pkg_fmt, + "Downloading package", + ); + let mut data_verifier = verifier.data_verifier()?; + let files = match resolved.gh_release_artifact_url.as_ref() { + Some(artifact_url) if resolved.is_repo_private => self + .gh_api_client + .download_artifact(artifact_url.clone()) + .await? + .with_data_verifier(data_verifier.as_mut()), + _ => Download::new_with_data_verifier( + self.client.clone(), + resolved.url.clone(), + data_verifier.as_mut(), + ), + } + .and_extract(resolved.pkg_fmt, dst) + .await?; + trace!("validating signature (if any)"); + if data_verifier.validate() { + if let Some(info) = verifier.info() { + info!( + "Verified signature for package '{}': {info}", + self.data.name + ); + } + Ok(files) + } else { + Err(FetchError::InvalidSignature) + } + } + + fn pkg_fmt(&self) -> PkgFmt { + self.resolution.get().unwrap().pkg_fmt + } + + fn target_meta(&self) -> PkgMeta { + let mut meta = self.target_data.meta.clone(); + meta.pkg_fmt = Some(self.pkg_fmt()); + meta + } + + fn source_name(&self) -> CompactString { + self.resolution + .get() + .map(|resolved| { + if let Some(domain) = resolved.url.domain() { + domain.to_compact_string() + } else if let Some(host) = resolved.url.host_str() { + host.to_compact_string() + } else { + resolved.url.to_compact_string() + } + }) + .unwrap_or_else(|| "invalid url".into()) + } + + fn fetcher_name(&self) -> &'static str { + FETCHER_GH_CRATE_META + } + + fn strategy(&self) -> Strategy { + Strategy::CrateMetaData + } + + fn is_third_party(&self) -> bool { + false + } + + fn target(&self) -> &str { + &self.target_data.target + } + + fn target_data(&self) -> &Arc { + &self.target_data + } +} + +/// Template for constructing download paths +#[derive(Clone)] +struct Context<'c> { + name: &'c str, + repo: Option<&'c str>, + target: &'c str, + version: &'c str, + + /// Archive format e.g. tar.gz, zip + archive_format: Option<&'c str>, + + archive_suffix: Option<&'c str>, + + /// Filename extension on the binary, i.e. .exe on Windows, nothing otherwise + binary_ext: &'c str, + + /// Workspace of the crate inside the repository. + subcrate: Option<&'c str>, + + /// Url of the file being downloaded (only for signing.file) + url: Option<&'c Url>, + + target_related_info: &'c dyn leon::Values, +} + +impl fmt::Debug for Context<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Context") + .field("name", &self.name) + .field("repo", &self.repo) + .field("target", &self.target) + .field("version", &self.version) + .field("archive_format", &self.archive_format) + .field("binary_ext", &self.binary_ext) + .field("subcrate", &self.subcrate) + .field("url", &self.url) + .finish_non_exhaustive() + } +} + +impl leon::Values for Context<'_> { + fn get_value<'s>(&'s self, key: &str) -> Option> { + match key { + "name" => Some(Cow::Borrowed(self.name)), + "repo" => self.repo.map(Cow::Borrowed), + "target" => Some(Cow::Borrowed(self.target)), + "version" => Some(Cow::Borrowed(self.version)), + + "archive-format" => self.archive_format.map(Cow::Borrowed), + + // Soft-deprecated alias for archive-format + "format" => self.archive_format.map(Cow::Borrowed), + + "archive-suffix" => self.archive_suffix.map(Cow::Borrowed), + + "binary-ext" => Some(Cow::Borrowed(self.binary_ext)), + + "subcrate" => self.subcrate.map(Cow::Borrowed), + + "url" => self.url.map(|url| Cow::Borrowed(url.as_str())), + + key => self.target_related_info.get_value(key), + } + } +} + +impl<'c> Context<'c> { + fn from_data_with_repo( + data: &'c Data, + target: &'c str, + target_related_info: &'c dyn leon::Values, + archive_suffix: Option<&'c str>, + repo: Option<&'c str>, + subcrate: Option<&'c str>, + ) -> Self { + let archive_format = archive_suffix.map(|archive_suffix| { + if archive_suffix.is_empty() { + // Empty archive_suffix means PkgFmt::Bin + "bin" + } else { + debug_assert!(archive_suffix.starts_with('.'), "{archive_suffix}"); + + &archive_suffix[1..] + } + }); + + Self { + name: &data.name, + repo, + target, + + version: &data.version, + archive_format, + archive_suffix, + binary_ext: if target.contains("windows") { + ".exe" + } else { + "" + }, + subcrate, + url: None, + + target_related_info, + } + } + + fn with_url(&mut self, url: &'c Url) -> &mut Self { + self.url = Some(url); + self + } + + fn render_url_with(&self, template: &Template<'_>) -> Result { + debug!(?template, context=?self, "render url template"); + Ok(Url::parse(&template.render(self)?)?) + } + + #[cfg(test)] + fn render_url(&self, template: &str) -> Result { + self.render_url_with(&Template::parse(template)?) + } +} + +#[cfg(test)] +mod test { + use super::{super::Data, Context}; + use compact_str::ToCompactString; + use url::Url; + + const DEFAULT_PKG_URL: &str = "{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.{ archive-format }"; + + fn assert_context_rendering( + data: &Data, + target: &str, + archive_format: &str, + template: &str, + expected_url: &str, + ) { + // The template provided doesn't need this, so just returning None + // is OK. + let target_info = leon::vals(|_| None); + + let ctx = Context::from_data_with_repo( + data, + target, + &target_info, + Some(archive_format), + data.repo.as_deref(), + None, + ); + + let expected_url = Url::parse(expected_url).unwrap(); + assert_eq!(ctx.render_url(template).unwrap(), expected_url); + } + + #[test] + fn defaults() { + assert_context_rendering( + &Data::new( + "cargo-binstall".to_compact_string(), + "1.2.3".to_compact_string(), + Some("https://github.com/ryankurte/cargo-binstall".to_string()), + ), + "x86_64-unknown-linux-gnu", + ".tgz", + DEFAULT_PKG_URL, + "https://github.com/ryankurte/cargo-binstall/releases/download/v1.2.3/cargo-binstall-x86_64-unknown-linux-gnu-v1.2.3.tgz" + ); + } + + #[test] + fn no_repo_but_full_url() { + assert_context_rendering( + &Data::new( + "cargo-binstall".to_compact_string(), + "1.2.3".to_compact_string(), + None, + ), + "x86_64-unknown-linux-gnu", + ".tgz", + &format!("https://example.com{}", &DEFAULT_PKG_URL[8..]), + "https://example.com/releases/download/v1.2.3/cargo-binstall-x86_64-unknown-linux-gnu-v1.2.3.tgz" + ); + } + + #[test] + fn different_url() { + assert_context_rendering( + &Data::new( + "radio-sx128x".to_compact_string(), + "0.14.1-alpha.5".to_compact_string(), + Some("https://github.com/rust-iot/rust-radio-sx128x".to_string()), + ), + "x86_64-unknown-linux-gnu", + ".tgz", + "{ repo }/releases/download/v{ version }/sx128x-util-{ target }-v{ version }.{ archive-format }", + "https://github.com/rust-iot/rust-radio-sx128x/releases/download/v0.14.1-alpha.5/sx128x-util-x86_64-unknown-linux-gnu-v0.14.1-alpha.5.tgz" + ); + } + + #[test] + fn deprecated_format() { + assert_context_rendering( + &Data::new( + "radio-sx128x".to_compact_string(), + "0.14.1-alpha.5".to_compact_string(), + Some("https://github.com/rust-iot/rust-radio-sx128x".to_string()), + ), + "x86_64-unknown-linux-gnu", + ".tgz", + "{ repo }/releases/download/v{ version }/sx128x-util-{ target }-v{ version }.{ format }", + "https://github.com/rust-iot/rust-radio-sx128x/releases/download/v0.14.1-alpha.5/sx128x-util-x86_64-unknown-linux-gnu-v0.14.1-alpha.5.tgz" + ); + } + + #[test] + fn different_ext() { + assert_context_rendering( + &Data::new( + "cargo-watch".to_compact_string(), + "9.0.0".to_compact_string(), + Some("https://github.com/watchexec/cargo-watch".to_string()), + ), + "aarch64-apple-darwin", + ".txz", + "{ repo }/releases/download/v{ version }/{ name }-v{ version }-{ target }.tar.xz", + "https://github.com/watchexec/cargo-watch/releases/download/v9.0.0/cargo-watch-v9.0.0-aarch64-apple-darwin.tar.xz" + ); + } + + #[test] + fn no_archive() { + assert_context_rendering( + &Data::new( + "cargo-watch".to_compact_string(), + "9.0.0".to_compact_string(), + Some("https://github.com/watchexec/cargo-watch".to_string()), + ), + "aarch64-pc-windows-msvc", + ".bin", + "{ repo }/releases/download/v{ version }/{ name }-v{ version }-{ target }{ binary-ext }", + "https://github.com/watchexec/cargo-watch/releases/download/v9.0.0/cargo-watch-v9.0.0-aarch64-pc-windows-msvc.exe" + ); + } +} diff --git a/crates/binstalk-fetchers/src/gh_crate_meta/hosting.rs b/crates/binstalk-fetchers/src/gh_crate_meta/hosting.rs new file mode 100644 index 00000000..db8652a9 --- /dev/null +++ b/crates/binstalk-fetchers/src/gh_crate_meta/hosting.rs @@ -0,0 +1,117 @@ +use itertools::Itertools; +use leon::{Item, Template}; +use leon_macros::template; +use url::Url; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum RepositoryHost { + GitHub, + GitLab, + BitBucket, + SourceForge, + Unknown, +} + +/// Make sure to update possible_dirs in `bins::infer_bin_dir_template` +/// if you modified FULL_FILENAMES or NOVERSION_FILENAMES. +pub const FULL_FILENAMES: &[Template<'_>] = &[ + template!("/{ name }-{ target }-v{ version }{ archive-suffix }"), + template!("/{ name }-{ target }-{ version }{ archive-suffix }"), + template!("/{ name }-{ version }-{ target }{ archive-suffix }"), + template!("/{ name }-v{ version }-{ target }{ archive-suffix }"), + template!("/{ name }_{ target }_v{ version }{ archive-suffix }"), + template!("/{ name }_{ target }_{ version }{ archive-suffix }"), + template!("/{ name }_{ version }_{ target }{ archive-suffix }"), + template!("/{ name }_v{ version }_{ target }{ archive-suffix }"), +]; + +pub const NOVERSION_FILENAMES: &[Template<'_>] = &[ + template!("/{ name }-{ target }{ archive-suffix }"), + template!("/{ name }_{ target }{ archive-suffix }"), +]; + +const GITHUB_RELEASE_PATHS: &[Template<'_>] = &[ + template!("{ repo }/releases/download/{ version }"), + template!("{ repo }/releases/download/v{ version }"), + // %2F is escaped form of '/' + template!("{ repo }/releases/download/{ subcrate }%2F{ version }"), + template!("{ repo }/releases/download/{ subcrate }%2Fv{ version }"), +]; + +const GITLAB_RELEASE_PATHS: &[Template<'_>] = &[ + template!("{ repo }/-/releases/{ version }/downloads/binaries"), + template!("{ repo }/-/releases/v{ version }/downloads/binaries"), + // %2F is escaped form of '/' + template!("{ repo }/-/releases/{ subcrate }%2F{ version }/downloads/binaries"), + template!("{ repo }/-/releases/{ subcrate }%2Fv{ version }/downloads/binaries"), +]; + +const BITBUCKET_RELEASE_PATHS: &[Template<'_>] = &[template!("{ repo }/downloads")]; + +const SOURCEFORGE_RELEASE_PATHS: &[Template<'_>] = &[ + template!("{ repo }/files/binaries/{ version }"), + template!("{ repo }/files/binaries/v{ version }"), + // %2F is escaped form of '/' + template!("{ repo }/files/binaries/{ subcrate }%2F{ version }"), + template!("{ repo }/files/binaries/{ subcrate }%2Fv{ version }"), +]; + +impl RepositoryHost { + pub fn guess_git_hosting_services(repo: &Url) -> Self { + use RepositoryHost::*; + + match repo.domain() { + Some(domain) if domain.starts_with("github") => GitHub, + Some(domain) if domain.starts_with("gitlab") => GitLab, + Some("bitbucket.org") => BitBucket, + Some("sourceforge.net") => SourceForge, + _ => Unknown, + } + } + + pub fn get_default_pkg_url_template( + self, + ) -> Option> + Clone + 'static> { + use RepositoryHost::*; + + match self { + GitHub => Some(apply_filenames_to_paths( + GITHUB_RELEASE_PATHS, + &[FULL_FILENAMES, NOVERSION_FILENAMES], + "", + )), + GitLab => Some(apply_filenames_to_paths( + GITLAB_RELEASE_PATHS, + &[FULL_FILENAMES, NOVERSION_FILENAMES], + "", + )), + BitBucket => Some(apply_filenames_to_paths( + BITBUCKET_RELEASE_PATHS, + &[FULL_FILENAMES], + "", + )), + SourceForge => Some(apply_filenames_to_paths( + SOURCEFORGE_RELEASE_PATHS, + &[FULL_FILENAMES, NOVERSION_FILENAMES], + "/download", + )), + Unknown => None, + } + } +} + +fn apply_filenames_to_paths( + paths: &'static [Template<'static>], + filenames: &'static [&'static [Template<'static>]], + suffix: &'static str, +) -> impl Iterator> + Clone + 'static { + filenames + .iter() + .flat_map(|fs| fs.iter()) + .cartesian_product(paths.iter()) + .map(move |(filename, path)| { + let mut template = path.clone() + filename; + template += Item::Text(suffix); + template + }) +} diff --git a/crates/binstalk-fetchers/src/lib.rs b/crates/binstalk-fetchers/src/lib.rs new file mode 100644 index 00000000..49a86894 --- /dev/null +++ b/crates/binstalk-fetchers/src/lib.rs @@ -0,0 +1,457 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +use std::{path::Path, sync::Arc, time::Duration}; + +use binstalk_downloader::{download::DownloadError, remote::Error as RemoteError}; +use binstalk_git_repo_api::gh_api_client::{GhApiError, GhRepo, RepoInfo as GhRepoInfo}; +use binstalk_types::cargo_toml_binstall::{SigningAlgorithm, Strategy}; +use thiserror::Error as ThisError; +use tokio::{sync::OnceCell, task::JoinError, time::sleep}; +pub use url::ParseError as UrlParseError; + +mod gh_crate_meta; +pub use gh_crate_meta::*; + +#[cfg(feature = "quickinstall")] +mod quickinstall; +#[cfg(feature = "quickinstall")] +pub use quickinstall::*; + +mod common; +use common::*; + +mod signing; +use signing::*; + +mod futures_resolver; + +use gh_crate_meta::hosting::RepositoryHost; + +static DEFAULT_GH_API_RETRY_DURATION: Duration = Duration::from_secs(1); + +#[derive(Debug, ThisError)] +#[error("Invalid pkg-url {pkg_url} for {crate_name}@{version} on {target}: {reason}")] +pub struct InvalidPkgFmtError { + pub crate_name: CompactString, + pub version: CompactString, + pub target: CompactString, + pub pkg_url: Box, + pub reason: &'static &'static str, +} + +#[derive(Debug, ThisError, miette::Diagnostic)] +#[non_exhaustive] +pub enum FetchError { + #[error(transparent)] + Download(#[from] DownloadError), + + #[error("Failed to parse template: {0}")] + #[diagnostic(transparent)] + TemplateParse(#[from] leon::ParseError), + + #[error("Failed to render template: {0}")] + #[diagnostic(transparent)] + TemplateRender(#[from] leon::RenderError), + + #[error("Failed to render template: {0}")] + GhApi(#[from] GhApiError), + + #[error(transparent)] + InvalidPkgFmt(Box), + + #[error("Failed to parse url: {0}")] + UrlParse(#[from] UrlParseError), + + #[error("Signing algorithm not supported: {0:?}")] + UnsupportedSigningAlgorithm(SigningAlgorithm), + + #[error("No signature present")] + MissingSignature, + + #[error("Failed to verify signature")] + InvalidSignature, + + #[error("Failed to wait for task: {0}")] + TaskJoinError(#[from] JoinError), +} + +impl From for FetchError { + fn from(e: RemoteError) -> Self { + DownloadError::from(e).into() + } +} + +impl From for FetchError { + fn from(e: InvalidPkgFmtError) -> Self { + Self::InvalidPkgFmt(Box::new(e)) + } +} + +#[async_trait::async_trait] +pub trait Fetcher: Send + Sync { + /// Create a new fetcher from some data + #[allow(clippy::new_ret_no_self)] + fn new( + client: Client, + gh_api_client: GhApiClient, + data: Arc, + target_data: Arc, + signature_policy: SignaturePolicy, + ) -> Arc + where + Self: Sized; + + /// Fetch a package and extract + async fn fetch_and_extract(&self, dst: &Path) -> Result; + + /// Find the package, if it is available for download + /// + /// This may look for multiple remote targets, but must write (using some form of interior + /// mutability) the best one to the implementing struct in some way so `fetch_and_extract` can + /// proceed without additional work. + /// + /// Must return `true` if a package is available, `false` if none is, and reserve errors to + /// fatal conditions only. + fn find(self: Arc) -> JoinHandle>; + + /// Report to upstream that cargo-binstall tries to use this fetcher. + /// Currently it is only overriden by [`quickinstall::QuickInstall`]. + fn report_to_upstream(self: Arc) {} + + /// Return the package format + fn pkg_fmt(&self) -> PkgFmt; + + /// Return finalized target meta. + fn target_meta(&self) -> PkgMeta; + + /// A short human-readable name or descriptor for the package source + fn source_name(&self) -> CompactString; + + /// A short human-readable name, must contains only characters + /// and numbers and it also must be unique. + /// + /// It is used to create a temporary dir where it is used for + /// [`Fetcher::fetch_and_extract`]. + fn fetcher_name(&self) -> &'static str; + + /// The strategy used by this fetcher + fn strategy(&self) -> Strategy; + + /// Should return true if the remote is from a third-party source + fn is_third_party(&self) -> bool; + + /// Return the target for this fetcher + fn target(&self) -> &str; + + fn target_data(&self) -> &Arc; +} + +#[derive(Clone, Debug)] +struct RepoInfo { + repo: Url, + repository_host: RepositoryHost, + subcrate: Option, + is_private: bool, +} + +/// What to do about package signatures +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum SignaturePolicy { + /// Don't process any signing information at all + Ignore, + + /// Verify and fail if a signature is found, but pass a signature-less package + IfPresent, + + /// Require signatures to be present (and valid) + Require, +} + +/// Data required to fetch a package +#[derive(Clone, Debug)] +pub struct Data { + name: CompactString, + version: CompactString, + repo: Option, + repo_info: OnceCell>, +} + +impl Data { + pub fn new(name: CompactString, version: CompactString, repo: Option) -> Self { + Self { + name, + version, + repo, + repo_info: OnceCell::new(), + } + } + + #[instrument(skip(client))] + async fn get_repo_info(&self, client: &GhApiClient) -> Result, FetchError> { + async fn gh_get_repo_info( + client: &GhApiClient, + gh_repo: &GhRepo, + ) -> Result { + loop { + match client.get_repo_info(gh_repo).await { + Ok(Some(gh_repo_info)) => break Ok(gh_repo_info), + Ok(None) => break Err(GhApiError::NotFound), + Err(GhApiError::RateLimit { retry_after }) => { + sleep(retry_after.unwrap_or(DEFAULT_GH_API_RETRY_DURATION)).await + } + Err(err) => break Err(err), + } + } + } + + async fn get_repo_info_inner( + repo: &str, + client: &GhApiClient, + ) -> Result { + let repo = Url::parse(repo)?; + let mut repo = client + .remote_client() + .get_redirected_final_url(repo.clone()) + .await + .unwrap_or(repo); + let repository_host = RepositoryHost::guess_git_hosting_services(&repo); + + let subcrate = RepoInfo::detect_subcrate(&mut repo, repository_host); + + if let Some(repo) = repo + .as_str() + .strip_suffix(".git") + .and_then(|s| Url::parse(s).ok()) + { + let repository_host = RepositoryHost::guess_git_hosting_services(&repo); + match GhRepo::try_extract_from_url(&repo) { + Some(gh_repo) if client.has_gh_token() => { + if let Ok(gh_repo_info) = gh_get_repo_info(client, &gh_repo).await { + return Ok(RepoInfo { + subcrate, + repository_host, + repo, + is_private: gh_repo_info.is_private(), + }); + } + } + _ => { + if let Ok(repo) = + client.remote_client().get_redirected_final_url(repo).await + { + return Ok(RepoInfo { + subcrate, + repository_host: RepositoryHost::guess_git_hosting_services(&repo), + repo, + is_private: false, + }); + } + } + } + } + + Ok(RepoInfo { + is_private: match GhRepo::try_extract_from_url(&repo) { + Some(gh_repo) if client.has_gh_token() => { + gh_get_repo_info(client, &gh_repo).await?.is_private() + } + _ => false, + }, + subcrate, + repo, + repository_host, + }) + } + + self.repo_info + .get_or_try_init(move || { + Box::pin(async move { + let Some(repo) = self.repo.as_deref() else { + return Ok(None); + }; + + let repo_info = get_repo_info_inner(repo, client).await?; + + debug!("Resolved repo_info = {repo_info:#?}"); + + Ok(Some(repo_info)) + }) + }) + .await + .map(Option::as_ref) + } +} + +impl RepoInfo { + /// If `repo` contains a subcrate, then extracts and returns it. + /// It will also remove that subcrate path from `repo` to match + /// `scheme:/{repo_owner}/{repo_name}` + fn detect_subcrate(repo: &mut Url, repository_host: RepositoryHost) -> Option { + match repository_host { + RepositoryHost::GitHub => Self::detect_subcrate_common(repo, &["tree"]), + RepositoryHost::GitLab => Self::detect_subcrate_common(repo, &["-", "blob"]), + _ => None, + } + } + + fn detect_subcrate_common(repo: &mut Url, seps: &[&str]) -> Option { + let mut path_segments = repo.path_segments()?; + + let _repo_owner = path_segments.next()?; + let _repo_name = path_segments.next()?; + + // Skip separators + for sep in seps.iter().copied() { + if path_segments.next()? != sep { + return None; + } + } + + // Skip branch name + let _branch_name = path_segments.next()?; + + let (subcrate, is_crate_present) = match path_segments.next()? { + // subcrate url is of path /crates/$subcrate_name, e.g. wasm-bindgen-cli + "crates" => (path_segments.next()?, true), + // subcrate url is of path $subcrate_name, e.g. cargo-audit + subcrate => (subcrate, false), + }; + + if path_segments.next().is_some() { + // A subcrate url should not contain anything more. + None + } else { + let subcrate = subcrate.into(); + + // Pop subcrate path to match regular repo style: + // + // scheme:/{addr}/{repo_owner}/{repo_name} + // + // path_segments() succeeds, so path_segments_mut() + // must also succeeds. + let mut paths = repo.path_segments_mut().unwrap(); + + paths.pop(); // pop subcrate + if is_crate_present { + paths.pop(); // pop crate + } + paths.pop(); // pop branch name + seps.iter().for_each(|_| { + paths.pop(); + }); // pop separators + + Some(subcrate) + } + } +} + +/// Target specific data required to fetch a package +#[derive(Clone, Debug)] +pub struct TargetData { + pub target: String, + pub meta: PkgMeta, + /// More target related info, it's recommend to provide the following keys: + /// - target_family, + /// - target_arch + /// - target_libc + /// - target_vendor + pub target_related_info: T, +} + +pub type TargetDataErased = TargetData; + +#[cfg(test)] +mod test { + use std::num::{NonZeroU16, NonZeroU64}; + + use super::*; + + #[test] + fn test_detect_subcrate_github() { + // cargo-audit + let urls = [ + "https://github.com/RustSec/rustsec/tree/main/cargo-audit", + "https://github.com/RustSec/rustsec/tree/master/cargo-audit", + ]; + for url in urls { + let mut repo = Url::parse(url).unwrap(); + + let repository_host = RepositoryHost::guess_git_hosting_services(&repo); + assert_eq!(repository_host, RepositoryHost::GitHub); + + let subcrate_prefix = RepoInfo::detect_subcrate(&mut repo, repository_host).unwrap(); + assert_eq!(subcrate_prefix, "cargo-audit"); + + assert_eq!( + repo, + Url::parse("https://github.com/RustSec/rustsec").unwrap() + ); + } + + // wasm-bindgen-cli + let urls = [ + "https://github.com/rustwasm/wasm-bindgen/tree/main/crates/cli", + "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/cli", + ]; + for url in urls { + let mut repo = Url::parse(url).unwrap(); + + let repository_host = RepositoryHost::guess_git_hosting_services(&repo); + assert_eq!(repository_host, RepositoryHost::GitHub); + + let subcrate_prefix = RepoInfo::detect_subcrate(&mut repo, repository_host).unwrap(); + assert_eq!(subcrate_prefix, "cli"); + + assert_eq!( + repo, + Url::parse("https://github.com/rustwasm/wasm-bindgen").unwrap() + ); + } + } + + #[test] + fn test_detect_subcrate_gitlab() { + let urls = [ + "https://gitlab.kitware.com/NobodyXu/hello/-/blob/main/cargo-binstall", + "https://gitlab.kitware.com/NobodyXu/hello/-/blob/master/cargo-binstall", + ]; + for url in urls { + let mut repo = Url::parse(url).unwrap(); + + let repository_host = RepositoryHost::guess_git_hosting_services(&repo); + assert_eq!(repository_host, RepositoryHost::GitLab); + + let subcrate_prefix = RepoInfo::detect_subcrate(&mut repo, repository_host).unwrap(); + assert_eq!(subcrate_prefix, "cargo-binstall"); + + assert_eq!( + repo, + Url::parse("https://gitlab.kitware.com/NobodyXu/hello").unwrap() + ); + } + } + + #[tokio::test] + async fn test_ignore_dot_git_for_github_repos() { + let url_without_git = "https://github.com/cargo-bins/cargo-binstall"; + let url_with_git = format!("{}.git", url_without_git); + + let data = Data::new("cargo-binstall".into(), "v1.2.3".into(), Some(url_with_git)); + + let gh_client = GhApiClient::new( + Client::new( + "user-agent", + None, + NonZeroU16::new(1000).unwrap(), + NonZeroU64::new(1000).unwrap(), + [], + ) + .unwrap(), + None, + ); + + let repo_info = data.get_repo_info(&gh_client).await.unwrap().unwrap(); + + assert_eq!(url_without_git, repo_info.repo.as_str()); + } +} diff --git a/crates/binstalk-fetchers/src/quickinstall.rs b/crates/binstalk-fetchers/src/quickinstall.rs new file mode 100644 index 00000000..ebf6976a --- /dev/null +++ b/crates/binstalk-fetchers/src/quickinstall.rs @@ -0,0 +1,383 @@ +use std::{ + borrow::Cow, + path::Path, + sync::{Arc, Mutex, OnceLock}, +}; + +use binstalk_downloader::remote::Method; +use binstalk_types::cargo_toml_binstall::{PkgFmt, PkgMeta, PkgSigning, Strategy}; +use tokio::sync::OnceCell; +use tracing::{error, info, trace}; +use url::Url; + +use crate::{ + common::*, Data, FetchError, SignaturePolicy, SignatureVerifier, SigningAlgorithm, + TargetDataErased, +}; + +const BASE_URL: &str = "https://github.com/cargo-bins/cargo-quickinstall/releases/download"; +pub const QUICKINSTALL_STATS_URL: &str = + "https://cargo-quickinstall-stats-server.fly.dev/record-install"; + +const QUICKINSTALL_SIGN_KEY: Cow<'static, str> = + Cow::Borrowed("RWTdnnab2pAka9OdwgCMYyOE66M/BlQoFWaJ/JjwcPV+f3n24IRTj97t"); +const QUICKINSTALL_SUPPORTED_TARGETS_URL: &str = + "https://raw.githubusercontent.com/cargo-bins/cargo-quickinstall/main/supported-targets"; + +fn is_universal_macos(target: &str) -> bool { + ["universal-apple-darwin", "universal2-apple-darwin"].contains(&target) +} + +async fn get_quickinstall_supported_targets( + client: &Client, +) -> Result<&'static [CompactString], FetchError> { + static SUPPORTED_TARGETS: OnceCell> = OnceCell::const_new(); + + SUPPORTED_TARGETS + .get_or_try_init(|| async { + let bytes = client + .get(Url::parse(QUICKINSTALL_SUPPORTED_TARGETS_URL)?) + .send(true) + .await? + .bytes() + .await?; + + let mut v: Vec = String::from_utf8_lossy(&bytes) + .split_whitespace() + .map(CompactString::new) + .collect(); + v.sort_unstable(); + v.dedup(); + Ok(v.into()) + }) + .await + .map(Box::as_ref) +} + +pub struct QuickInstall { + client: Client, + gh_api_client: GhApiClient, + is_supported_v: OnceCell, + + data: Arc, + package: String, + package_url: Url, + signature_url: Url, + signature_policy: SignaturePolicy, + + target_data: Arc, + + signature_verifier: OnceLock, + status: Mutex, +} + +#[derive(Debug, Clone, Copy)] +enum Status { + Start, + NotFound, + Found, + AttemptingInstall, + InvalidSignature, + InstalledFromTarball, +} + +impl Status { + fn as_str(&self) -> &'static str { + match self { + Status::Start => "start", + Status::NotFound => "not-found", + Status::Found => "found", + Status::AttemptingInstall => "attempting-install", + Status::InvalidSignature => "invalid-signature", + Status::InstalledFromTarball => "installed-from-tarball", + } + } +} + +impl QuickInstall { + async fn is_supported(&self) -> Result { + self.is_supported_v + .get_or_try_init(|| async { + Ok(get_quickinstall_supported_targets(&self.client) + .await? + .binary_search(&CompactString::new(&self.target_data.target)) + .is_ok()) + }) + .await + .copied() + } + + fn download_signature( + self: Arc, + ) -> AutoAbortJoinHandle> { + AutoAbortJoinHandle::spawn(async move { + if self.signature_policy == SignaturePolicy::Ignore { + Ok(SignatureVerifier::Noop) + } else { + debug!(url=%self.signature_url, "Downloading signature"); + match Download::new(self.client.clone(), self.signature_url.clone()) + .into_bytes() + .await + { + Ok(signature) => { + trace!(?signature, "got signature contents"); + let config = PkgSigning { + algorithm: SigningAlgorithm::Minisign, + pubkey: QUICKINSTALL_SIGN_KEY, + file: None, + }; + SignatureVerifier::new(&config, &signature) + } + Err(err) => { + if self.signature_policy == SignaturePolicy::Require { + error!("Failed to download signature: {err}"); + Err(FetchError::MissingSignature) + } else { + debug!("Failed to download signature, skipping verification: {err}"); + Ok(SignatureVerifier::Noop) + } + } + } + } + }) + } + + fn get_status(&self) -> Status { + *self.status.lock().unwrap() + } + + fn set_status(&self, status: Status) { + *self.status.lock().unwrap() = status; + } +} + +#[async_trait::async_trait] +impl super::Fetcher for QuickInstall { + fn new( + client: Client, + gh_api_client: GhApiClient, + data: Arc, + target_data: Arc, + signature_policy: SignaturePolicy, + ) -> Arc { + let crate_name = &data.name; + let version = &data.version; + let target = &target_data.target; + + let package = format!("{crate_name}-{version}-{target}"); + + let url = format!("{BASE_URL}/{crate_name}-{version}/{package}.tar.gz"); + + Arc::new(Self { + client, + data, + gh_api_client, + is_supported_v: OnceCell::new(), + + package_url: Url::parse(&url) + .expect("package_url is pre-generated and should never be invalid url"), + signature_url: Url::parse(&format!("{url}.sig")) + .expect("signature_url is pre-generated and should never be invalid url"), + package, + signature_policy, + + target_data, + + signature_verifier: OnceLock::new(), + status: Mutex::new(Status::Start), + }) + } + + fn find(self: Arc) -> JoinHandle> { + tokio::spawn(async move { + if !self.is_supported().await? { + return Ok(false); + } + + let download_signature_task = self.clone().download_signature(); + + let is_found = does_url_exist( + self.client.clone(), + self.gh_api_client.clone(), + &self.package_url, + ) + .await?; + + if !is_found { + self.set_status(Status::NotFound); + return Ok(false); + } + + if self + .signature_verifier + .set(download_signature_task.flattened_join().await?) + .is_err() + { + panic!("::find is run twice"); + } + + self.set_status(Status::Found); + Ok(true) + }) + } + + fn report_to_upstream(self: Arc) { + if cfg!(debug_assertions) { + debug!("Not sending quickinstall report in debug mode"); + } else if is_universal_macos(&self.target_data.target) { + debug!( + r#"Not sending quickinstall report for universal-apple-darwin +and universal2-apple-darwin. +Quickinstall does not support these targets, it only supports targets supported +by rust officially."#, + ); + } else if self.is_supported_v.get().copied() != Some(false) { + tokio::spawn(async move { + if let Err(err) = self.report().await { + warn!( + "Failed to send quickinstall report for package {} (NOTE that this does not affect package resolution): {err}", + self.package + ) + } + }); + } + } + + async fn fetch_and_extract(&self, dst: &Path) -> Result { + self.set_status(Status::AttemptingInstall); + let Some(verifier) = self.signature_verifier.get() else { + panic!("::find has not been called yet!") + }; + + debug!(url=%self.package_url, "Downloading package"); + let mut data_verifier = verifier.data_verifier()?; + let files = Download::new_with_data_verifier( + self.client.clone(), + self.package_url.clone(), + data_verifier.as_mut(), + ) + .and_extract(self.pkg_fmt(), dst) + .await?; + trace!("validating signature (if any)"); + if data_verifier.validate() { + if let Some(info) = verifier.info() { + info!("Verified signature for package '{}': {info}", self.package); + } + self.set_status(Status::InstalledFromTarball); + Ok(files) + } else { + self.set_status(Status::InvalidSignature); + Err(FetchError::InvalidSignature) + } + } + + fn pkg_fmt(&self) -> PkgFmt { + PkgFmt::Tgz + } + + fn target_meta(&self) -> PkgMeta { + let mut meta = self.target_data.meta.clone(); + meta.pkg_fmt = Some(self.pkg_fmt()); + meta.bin_dir = Some("{ bin }{ binary-ext }".to_string()); + meta + } + + fn source_name(&self) -> CompactString { + CompactString::from("QuickInstall") + } + + fn fetcher_name(&self) -> &'static str { + "QuickInstall" + } + + fn strategy(&self) -> Strategy { + Strategy::QuickInstall + } + + fn is_third_party(&self) -> bool { + true + } + + fn target(&self) -> &str { + &self.target_data.target + } + + fn target_data(&self) -> &Arc { + &self.target_data + } +} + +impl QuickInstall { + pub async fn report(&self) -> Result<(), FetchError> { + if !self.is_supported().await? { + debug!( + "Not sending quickinstall report for {} since Quickinstall does not support these targets.", + self.target_data.target + ); + + return Ok(()); + } + + let mut url = Url::parse(QUICKINSTALL_STATS_URL) + .expect("stats_url is pre-generated and should never be invalid url"); + url.query_pairs_mut() + .append_pair("crate", &self.data.name) + .append_pair("version", &self.data.version) + .append_pair("target", &self.target_data.target) + .append_pair( + "agent", + concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")), + ) + .append_pair("status", self.get_status().as_str()); + debug!("Sending installation report to quickinstall ({url})"); + + self.client.request(Method::POST, url).send(true).await?; + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::{get_quickinstall_supported_targets, Client, CompactString}; + use std::num::NonZeroU16; + + /// Mark this as an async fn so that you won't accidentally use it in + /// sync context. + async fn create_client() -> Client { + Client::new( + concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")), + None, + NonZeroU16::new(10).unwrap(), + 1.try_into().unwrap(), + [], + ) + .unwrap() + } + + #[tokio::test] + async fn test_get_quickinstall_supported_targets() { + let supported_targets = get_quickinstall_supported_targets(&create_client().await) + .await + .unwrap(); + + [ + "x86_64-pc-windows-msvc", + "x86_64-apple-darwin", + "aarch64-apple-darwin", + "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", + "aarch64-unknown-linux-gnu", + "aarch64-unknown-linux-musl", + "aarch64-pc-windows-msvc", + "armv7-unknown-linux-musleabihf", + "armv7-unknown-linux-gnueabihf", + ] + .into_iter() + .for_each(|known_supported_target| { + supported_targets + .binary_search(&CompactString::new(known_supported_target)) + .unwrap(); + }); + } +} diff --git a/crates/binstalk-fetchers/src/signing.rs b/crates/binstalk-fetchers/src/signing.rs new file mode 100644 index 00000000..b231b440 --- /dev/null +++ b/crates/binstalk-fetchers/src/signing.rs @@ -0,0 +1,91 @@ +use binstalk_downloader::download::DataVerifier; +use binstalk_types::cargo_toml_binstall::{PkgSigning, SigningAlgorithm}; +use bytes::Bytes; +use minisign_verify::{PublicKey, Signature, StreamVerifier}; +use tracing::{error, trace}; + +use crate::FetchError; + +pub enum SignatureVerifier { + Noop, + Minisign(Box), +} + +impl SignatureVerifier { + pub fn new(config: &PkgSigning, signature: &[u8]) -> Result { + match config.algorithm { + SigningAlgorithm::Minisign => MinisignVerifier::new(config, signature) + .map(Box::new) + .map(Self::Minisign), + algorithm => Err(FetchError::UnsupportedSigningAlgorithm(algorithm)), + } + } + + pub fn data_verifier(&self) -> Result, FetchError> { + match self { + Self::Noop => Ok(Box::new(())), + Self::Minisign(v) => v.data_verifier(), + } + } + + pub fn info(&self) -> Option { + match self { + Self::Noop => None, + Self::Minisign(v) => Some(v.signature.trusted_comment().into()), + } + } +} + +pub struct MinisignVerifier { + pubkey: PublicKey, + signature: Signature, +} + +impl MinisignVerifier { + pub fn new(config: &PkgSigning, signature: &[u8]) -> Result { + trace!(key=?config.pubkey, "parsing public key"); + let pubkey = PublicKey::from_base64(&config.pubkey).map_err(|err| { + error!("Package public key is invalid: {err}"); + FetchError::InvalidSignature + })?; + + trace!(?signature, "parsing signature"); + let signature = Signature::decode(std::str::from_utf8(signature).map_err(|err| { + error!(?signature, "Signature file is not UTF-8! {err}"); + FetchError::InvalidSignature + })?) + .map_err(|err| { + error!("Signature file is invalid: {err}"); + FetchError::InvalidSignature + })?; + + Ok(Self { pubkey, signature }) + } + + pub fn data_verifier(&self) -> Result, FetchError> { + self.pubkey + .verify_stream(&self.signature) + .map(|vs| Box::new(MinisignDataVerifier(vs)) as _) + .map_err(|err| { + error!("Failed to setup stream verifier: {err}"); + FetchError::InvalidSignature + }) + } +} + +pub struct MinisignDataVerifier<'a>(StreamVerifier<'a>); + +impl DataVerifier for MinisignDataVerifier<'_> { + fn update(&mut self, data: &Bytes) { + self.0.update(data); + } + + fn validate(&mut self) -> bool { + if let Err(err) = self.0.finalize() { + error!("Failed to finalize signature verify: {err}"); + false + } else { + true + } + } +} diff --git a/crates/binstalk-git-repo-api/CHANGELOG.md b/crates/binstalk-git-repo-api/CHANGELOG.md new file mode 100644 index 00000000..f2ceba62 --- /dev/null +++ b/crates/binstalk-git-repo-api/CHANGELOG.md @@ -0,0 +1,147 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.5.22](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.21...binstalk-git-repo-api-v0.5.22) - 2025-06-06 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.5.21](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.20...binstalk-git-repo-api-v0.5.21) - 2025-05-30 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.5.20](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.19...binstalk-git-repo-api-v0.5.20) - 2025-05-16 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.5.19](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.18...binstalk-git-repo-api-v0.5.19) - 2025-04-05 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.5.18](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.17...binstalk-git-repo-api-v0.5.18) - 2025-03-19 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.5.17](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.16...binstalk-git-repo-api-v0.5.17) - 2025-03-15 + +### Other + +- *(deps)* bump tokio from 1.43.0 to 1.44.0 in the deps group ([#2079](https://github.com/cargo-bins/cargo-binstall/pull/2079)) + +## [0.5.16](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.15...binstalk-git-repo-api-v0.5.16) - 2025-03-07 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#2072](https://github.com/cargo-bins/cargo-binstall/pull/2072)) + +## [0.5.15](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.14...binstalk-git-repo-api-v0.5.15) - 2025-02-28 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.5.14](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.13...binstalk-git-repo-api-v0.5.14) - 2025-02-11 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.5.13](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.12...binstalk-git-repo-api-v0.5.13) - 2025-02-04 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.5.12](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.11...binstalk-git-repo-api-v0.5.12) - 2025-01-19 + +### Other + +- update Cargo.lock dependencies + +## [0.5.11](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.10...binstalk-git-repo-api-v0.5.11) - 2025-01-13 + +### Other + +- update Cargo.lock dependencies + +## [0.5.10](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.9...binstalk-git-repo-api-v0.5.10) - 2025-01-11 + +### Other + +- *(deps)* bump the deps group with 3 updates (#2015) + +## [0.5.9](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.8...binstalk-git-repo-api-v0.5.9) - 2025-01-04 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.5.8](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.7...binstalk-git-repo-api-v0.5.8) - 2024-12-14 + +### Other + +- *(deps)* bump the deps group with 2 updates (#1997) + +## [0.5.7](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.6...binstalk-git-repo-api-v0.5.7) - 2024-11-23 + +### Other + +- *(deps)* bump the deps group with 2 updates ([#1981](https://github.com/cargo-bins/cargo-binstall/pull/1981)) + +## [0.5.6](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.5...binstalk-git-repo-api-v0.5.6) - 2024-11-09 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1966](https://github.com/cargo-bins/cargo-binstall/pull/1966)) + +## [0.5.5](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.4...binstalk-git-repo-api-v0.5.5) - 2024-11-05 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1954](https://github.com/cargo-bins/cargo-binstall/pull/1954)) + +## [0.5.4](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.3...binstalk-git-repo-api-v0.5.4) - 2024-11-02 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.5.3](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.2...binstalk-git-repo-api-v0.5.3) - 2024-10-12 + +### Fixed + +- *(gh_api_client)* remote client should never being shared everywhere bacause the underlying connection pool will be reused. ([#1930](https://github.com/cargo-bins/cargo-binstall/pull/1930)) + +### Other + +- Fix binstalk-git-repo-api on PR of forks ([#1932](https://github.com/cargo-bins/cargo-binstall/pull/1932)) + +## [0.5.2](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.1...binstalk-git-repo-api-v0.5.2) - 2024-09-11 + +### Other + +- report to new stats server (with status) ([#1912](https://github.com/cargo-bins/cargo-binstall/pull/1912)) + +## [0.5.1](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.5.0...binstalk-git-repo-api-v0.5.1) - 2024-08-12 + +### Other +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.5.0](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-git-repo-api-v0.4.0...binstalk-git-repo-api-v0.5.0) - 2024-08-10 + +### Other +- updated the following local packages: binstalk-downloader, binstalk-downloader diff --git a/crates/binstalk-git-repo-api/Cargo.toml b/crates/binstalk-git-repo-api/Cargo.toml new file mode 100644 index 00000000..a03f4781 --- /dev/null +++ b/crates/binstalk-git-repo-api/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "binstalk-git-repo-api" +description = "The binstall toolkit for accessing API for git repository" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/binstalk-git-repo-api" +version = "0.5.22" +rust-version = "1.70.0" +authors = ["Jiahao XU "] +edition = "2021" +license = "Apache-2.0 OR MIT" + +[dependencies] +binstalk-downloader = { version = "0.13.20", path = "../binstalk-downloader", default-features = false, features = [ + "json", +] } +compact_str = "0.9.0" +percent-encoding = "2.2.0" +serde = { version = "1.0.163", features = ["derive"] } +serde-tuple-vec-map = "1.0.1" +serde_json = { version = "1.0.107" } +thiserror = "2.0.11" +tokio = { version = "1.44.0", features = ["sync"], default-features = false } +tracing = "0.1.39" +url = "2.5.4" +zeroize = "1.8.1" + +[dev-dependencies] +binstalk-downloader = { version = "0.13.20", path = "../binstalk-downloader" } +tracing-subscriber = "0.3" +once_cell = "1" diff --git a/crates/binstalk-git-repo-api/LICENSE-APACHE b/crates/binstalk-git-repo-api/LICENSE-APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/crates/binstalk-git-repo-api/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/binstalk-git-repo-api/LICENSE-MIT b/crates/binstalk-git-repo-api/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/crates/binstalk-git-repo-api/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/crates/binstalk-git-repo-api/src/gh_api_client.rs b/crates/binstalk-git-repo-api/src/gh_api_client.rs new file mode 100644 index 00000000..6af635ab --- /dev/null +++ b/crates/binstalk-git-repo-api/src/gh_api_client.rs @@ -0,0 +1,730 @@ +use std::{ + collections::HashMap, + future::Future, + ops::Deref, + sync::{ + atomic::{AtomicBool, Ordering::Relaxed}, + Arc, Mutex, RwLock, + }, + time::{Duration, Instant}, +}; + +use binstalk_downloader::{download::Download, remote}; +use compact_str::{format_compact, CompactString, ToCompactString}; +use tokio::sync::OnceCell; +use tracing::{instrument, Level}; +use url::Url; +use zeroize::Zeroizing; + +mod common; +mod error; +mod release_artifacts; +mod repo_info; + +use common::{check_http_status_and_header, percent_decode_http_url_path}; +pub use error::{GhApiContextError, GhApiError, GhGraphQLErrors}; +pub use repo_info::RepoInfo; + +/// default retry duration if x-ratelimit-reset is not found in response header +const DEFAULT_RETRY_DURATION: Duration = Duration::from_secs(10 * 60); + +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct GhRepo { + pub owner: CompactString, + pub repo: CompactString, +} +impl GhRepo { + pub fn repo_url(&self) -> Result { + Url::parse(&format_compact!( + "https://github.com/{}/{}", + self.owner, + self.repo + )) + } + + pub fn try_extract_from_url(url: &Url) -> Option { + if url.domain() != Some("github.com") { + return None; + } + + let mut path_segments = url.path_segments()?; + + Some(Self { + owner: path_segments.next()?.to_compact_string(), + repo: path_segments.next()?.to_compact_string(), + }) + } +} + +/// The keys required to identify a github release. +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct GhRelease { + pub repo: GhRepo, + pub tag: CompactString, +} + +/// The Github Release and one of its artifact. +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct GhReleaseArtifact { + pub release: GhRelease, + pub artifact_name: CompactString, +} + +impl GhReleaseArtifact { + /// Create [`GhReleaseArtifact`] from url. + pub fn try_extract_from_url(url: &remote::Url) -> Option { + if url.domain() != Some("github.com") { + return None; + } + + let mut path_segments = url.path_segments()?; + + let owner = path_segments.next()?; + let repo = path_segments.next()?; + + if (path_segments.next()?, path_segments.next()?) != ("releases", "download") { + return None; + } + + let tag = path_segments.next()?; + let artifact_name = path_segments.next()?; + + (path_segments.next().is_none() && url.fragment().is_none() && url.query().is_none()).then( + || Self { + release: GhRelease { + repo: GhRepo { + owner: percent_decode_http_url_path(owner), + repo: percent_decode_http_url_path(repo), + }, + tag: percent_decode_http_url_path(tag), + }, + artifact_name: percent_decode_http_url_path(artifact_name), + }, + ) + } +} + +#[derive(Debug)] +struct Map(RwLock>>); + +impl Default for Map { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Map +where + K: Eq + std::hash::Hash, + V: Default, +{ + fn get(&self, k: K) -> Arc { + let optional_value = self.0.read().unwrap().deref().get(&k).cloned(); + optional_value.unwrap_or_else(|| Arc::clone(self.0.write().unwrap().entry(k).or_default())) + } +} + +#[derive(Debug)] +struct Inner { + client: remote::Client, + release_artifacts: Map>>, + retry_after: Mutex>, + + auth_token: Option>>, + is_auth_token_valid: AtomicBool, + + only_use_restful_api: AtomicBool, +} + +/// Github API client for querying whether a release artifact exitsts. +/// Can only handle github.com for now. +#[derive(Clone, Debug)] +pub struct GhApiClient(Arc); + +impl GhApiClient { + pub fn new(client: remote::Client, auth_token: Option>>) -> Self { + Self(Arc::new(Inner { + client, + release_artifacts: Default::default(), + retry_after: Default::default(), + + auth_token, + is_auth_token_valid: AtomicBool::new(true), + + only_use_restful_api: AtomicBool::new(false), + })) + } + + /// If you don't want to use GitHub GraphQL API for whatever reason, call this. + pub fn set_only_use_restful_api(&self) { + self.0.only_use_restful_api.store(true, Relaxed); + } + + pub fn remote_client(&self) -> &remote::Client { + &self.0.client + } +} + +impl GhApiClient { + fn check_retry_after(&self) -> Result<(), GhApiError> { + let mut guard = self.0.retry_after.lock().unwrap(); + + if let Some(retry_after) = *guard { + if retry_after.elapsed().is_zero() { + return Err(GhApiError::RateLimit { + retry_after: Some(retry_after - Instant::now()), + }); + } else { + // Instant retry_after is already reached. + *guard = None; + } + } + + Ok(()) + } + + fn get_auth_token(&self) -> Option<&str> { + if self.0.is_auth_token_valid.load(Relaxed) { + self.0.auth_token.as_deref().map(|s| &**s) + } else { + None + } + } + + pub fn has_gh_token(&self) -> bool { + self.get_auth_token().is_some() + } + + async fn do_fetch( + &self, + graphql_func: GraphQLFn, + restful_func: RestfulFn, + data: &T, + ) -> Result + where + GraphQLFn: Fn(&remote::Client, &T, &str) -> GraphQLFut, + RestfulFn: Fn(&remote::Client, &T, Option<&str>) -> RestfulFut, + GraphQLFut: Future> + Send + 'static, + RestfulFut: Future> + Send + 'static, + { + self.check_retry_after()?; + + if !self.0.only_use_restful_api.load(Relaxed) { + if let Some(auth_token) = self.get_auth_token() { + match graphql_func(&self.0.client, data, auth_token).await { + Err(GhApiError::Unauthorized) => { + self.0.is_auth_token_valid.store(false, Relaxed); + } + res => return res.map_err(|err| err.context("GraphQL API")), + } + } + } + + restful_func(&self.0.client, data, self.get_auth_token()) + .await + .map_err(|err| err.context("Restful API")) + } + + #[instrument(skip(self), ret(level = Level::DEBUG))] + pub async fn get_repo_info(&self, repo: &GhRepo) -> Result, GhApiError> { + match self + .do_fetch( + repo_info::fetch_repo_info_graphql_api, + repo_info::fetch_repo_info_restful_api, + repo, + ) + .await + { + Ok(repo_info) => Ok(repo_info), + Err(GhApiError::NotFound) => Ok(None), + Err(err) => Err(err), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct GhReleaseArtifactUrl(Url); + +impl GhApiClient { + /// Return `Ok(Some(api_artifact_url))` if exists. + /// + /// Caches info on all artifacts matching (repo, tag). + /// + /// The returned future is guaranteed to be pointer size. + #[instrument(skip(self), ret(level = Level::DEBUG))] + pub async fn has_release_artifact( + &self, + GhReleaseArtifact { + release, + artifact_name, + }: GhReleaseArtifact, + ) -> Result, GhApiError> { + let once_cell = self.0.release_artifacts.get(release.clone()); + let res = once_cell + .get_or_try_init(|| { + Box::pin(async { + match self + .do_fetch( + release_artifacts::fetch_release_artifacts_graphql_api, + release_artifacts::fetch_release_artifacts_restful_api, + &release, + ) + .await + { + Ok(artifacts) => Ok(Some(artifacts)), + Err(GhApiError::NotFound) => Ok(None), + Err(err) => Err(err), + } + }) + }) + .await; + + match res { + Ok(Some(artifacts)) => Ok(artifacts + .get_artifact_url(&artifact_name) + .map(GhReleaseArtifactUrl)), + Ok(None) => Ok(None), + Err(GhApiError::RateLimit { retry_after }) => { + *self.0.retry_after.lock().unwrap() = + Some(Instant::now() + retry_after.unwrap_or(DEFAULT_RETRY_DURATION)); + + Err(GhApiError::RateLimit { retry_after }) + } + Err(err) => Err(err), + } + } + + pub async fn download_artifact( + &self, + artifact_url: GhReleaseArtifactUrl, + ) -> Result, GhApiError> { + self.check_retry_after()?; + + let Some(auth_token) = self.get_auth_token() else { + return Err(GhApiError::Unauthorized); + }; + + let response = self + .0 + .client + .get(artifact_url.0) + .header("Accept", "application/octet-stream") + .bearer_auth(&auth_token) + .send(false) + .await?; + + match check_http_status_and_header(response) { + Err(GhApiError::Unauthorized) => { + self.0.is_auth_token_valid.store(false, Relaxed); + Err(GhApiError::Unauthorized) + } + res => res.map(Download::from_response), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use compact_str::{CompactString, ToCompactString}; + use std::{env, num::NonZeroU16, time::Duration}; + use tokio::time::sleep; + use tracing::subscriber::set_global_default; + use tracing_subscriber::{filter::LevelFilter, fmt::fmt}; + + static DEFAULT_RETRY_AFTER: Duration = Duration::from_secs(1); + + mod cargo_binstall_v0_20_1 { + use super::{CompactString, GhRelease, GhRepo}; + + pub(super) const RELEASE: GhRelease = GhRelease { + repo: GhRepo { + owner: CompactString::const_new("cargo-bins"), + repo: CompactString::const_new("cargo-binstall"), + }, + tag: CompactString::const_new("v0.20.1"), + }; + + pub(super) const ARTIFACTS: &[&str] = &[ + "cargo-binstall-aarch64-apple-darwin.full.zip", + "cargo-binstall-aarch64-apple-darwin.zip", + "cargo-binstall-aarch64-pc-windows-msvc.full.zip", + "cargo-binstall-aarch64-pc-windows-msvc.zip", + "cargo-binstall-aarch64-unknown-linux-gnu.full.tgz", + "cargo-binstall-aarch64-unknown-linux-gnu.tgz", + "cargo-binstall-aarch64-unknown-linux-musl.full.tgz", + "cargo-binstall-aarch64-unknown-linux-musl.tgz", + "cargo-binstall-armv7-unknown-linux-gnueabihf.full.tgz", + "cargo-binstall-armv7-unknown-linux-gnueabihf.tgz", + "cargo-binstall-armv7-unknown-linux-musleabihf.full.tgz", + "cargo-binstall-armv7-unknown-linux-musleabihf.tgz", + "cargo-binstall-universal-apple-darwin.full.zip", + "cargo-binstall-universal-apple-darwin.zip", + "cargo-binstall-x86_64-apple-darwin.full.zip", + "cargo-binstall-x86_64-apple-darwin.zip", + "cargo-binstall-x86_64-pc-windows-msvc.full.zip", + "cargo-binstall-x86_64-pc-windows-msvc.zip", + "cargo-binstall-x86_64-unknown-linux-gnu.full.tgz", + "cargo-binstall-x86_64-unknown-linux-gnu.tgz", + "cargo-binstall-x86_64-unknown-linux-musl.full.tgz", + "cargo-binstall-x86_64-unknown-linux-musl.tgz", + ]; + } + + mod cargo_audit_v_0_17_6 { + use super::*; + + pub(super) const RELEASE: GhRelease = GhRelease { + repo: GhRepo { + owner: CompactString::const_new("rustsec"), + repo: CompactString::const_new("rustsec"), + }, + tag: CompactString::const_new("cargo-audit/v0.17.6"), + }; + + #[allow(unused)] + pub(super) const ARTIFACTS: &[&str] = &[ + "cargo-audit-aarch64-unknown-linux-gnu-v0.17.6.tgz", + "cargo-audit-armv7-unknown-linux-gnueabihf-v0.17.6.tgz", + "cargo-audit-x86_64-apple-darwin-v0.17.6.tgz", + "cargo-audit-x86_64-pc-windows-msvc-v0.17.6.zip", + "cargo-audit-x86_64-unknown-linux-gnu-v0.17.6.tgz", + "cargo-audit-x86_64-unknown-linux-musl-v0.17.6.tgz", + ]; + + #[test] + fn extract_with_escaped_characters() { + let release_artifact = try_extract_artifact_from_str( +"https://github.com/rustsec/rustsec/releases/download/cargo-audit%2Fv0.17.6/cargo-audit-aarch64-unknown-linux-gnu-v0.17.6.tgz" + ).unwrap(); + + assert_eq!( + release_artifact, + GhReleaseArtifact { + release: RELEASE, + artifact_name: CompactString::from( + "cargo-audit-aarch64-unknown-linux-gnu-v0.17.6.tgz", + ) + } + ); + } + } + + #[test] + fn gh_repo_extract_from_and_to_url() { + [ + "https://github.com/cargo-bins/cargo-binstall", + "https://github.com/rustsec/rustsec", + ] + .into_iter() + .for_each(|url| { + let url = Url::parse(url).unwrap(); + assert_eq!( + GhRepo::try_extract_from_url(&url) + .unwrap() + .repo_url() + .unwrap(), + url + ); + }) + } + + fn try_extract_artifact_from_str(s: &str) -> Option { + GhReleaseArtifact::try_extract_from_url(&url::Url::parse(s).unwrap()) + } + + fn assert_extract_gh_release_artifacts_failures(urls: &[&str]) { + for url in urls { + assert_eq!(try_extract_artifact_from_str(url), None); + } + } + + #[test] + fn extract_gh_release_artifacts_failure() { + use cargo_binstall_v0_20_1::*; + + let GhRelease { + repo: GhRepo { owner, repo }, + tag, + } = RELEASE; + + assert_extract_gh_release_artifacts_failures(&[ + "https://examle.com", + "https://github.com", + &format!("https://github.com/{owner}"), + &format!("https://github.com/{owner}/{repo}"), + &format!("https://github.com/{owner}/{repo}/123e"), + &format!("https://github.com/{owner}/{repo}/releases/21343"), + &format!("https://github.com/{owner}/{repo}/releases/download"), + &format!("https://github.com/{owner}/{repo}/releases/download/{tag}"), + &format!("https://github.com/{owner}/{repo}/releases/download/{tag}/a/23"), + &format!("https://github.com/{owner}/{repo}/releases/download/{tag}/a#a=12"), + &format!("https://github.com/{owner}/{repo}/releases/download/{tag}/a?page=3"), + ]); + } + + #[test] + fn extract_gh_release_artifacts_success() { + use cargo_binstall_v0_20_1::*; + + let GhRelease { + repo: GhRepo { owner, repo }, + tag, + } = RELEASE; + + for artifact in ARTIFACTS { + let GhReleaseArtifact { + release, + artifact_name, + } = try_extract_artifact_from_str(&format!( + "https://github.com/{owner}/{repo}/releases/download/{tag}/{artifact}" + )) + .unwrap(); + + assert_eq!(release, RELEASE); + assert_eq!(artifact_name, artifact); + } + } + + fn init_logger() { + // Disable time, target, file, line_num, thread name/ids to make the + // output more readable + let subscriber = fmt() + .without_time() + .with_target(false) + .with_file(false) + .with_line_number(false) + .with_thread_names(false) + .with_thread_ids(false) + .with_test_writer() + .with_max_level(LevelFilter::DEBUG) + .finish(); + + // Setup global subscriber + let _ = set_global_default(subscriber); + } + + fn create_remote_client() -> remote::Client { + remote::Client::new( + concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")), + None, + NonZeroU16::new(300).unwrap(), + 1.try_into().unwrap(), + [], + ) + .unwrap() + } + + /// Mark this as an async fn so that you won't accidentally use it in + /// sync context. + fn create_client() -> Vec { + let client = create_remote_client(); + + let auth_token = match env::var("CI_UNIT_TEST_GITHUB_TOKEN") { + Ok(auth_token) if !auth_token.is_empty() => { + Some(zeroize::Zeroizing::new(auth_token.into_boxed_str())) + } + _ => None, + }; + + let gh_client = GhApiClient::new(client.clone(), auth_token.clone()); + gh_client.set_only_use_restful_api(); + + let mut gh_clients = vec![gh_client]; + + if auth_token.is_some() { + gh_clients.push(GhApiClient::new(client, auth_token)); + } + + gh_clients + } + + #[tokio::test] + async fn rate_limited_test_get_repo_info() { + const PUBLIC_REPOS: [GhRepo; 1] = [GhRepo { + owner: CompactString::const_new("cargo-bins"), + repo: CompactString::const_new("cargo-binstall"), + }]; + const PRIVATE_REPOS: [GhRepo; 1] = [GhRepo { + owner: CompactString::const_new("cargo-bins"), + repo: CompactString::const_new("private-repo-for-testing"), + }]; + const NON_EXISTENT_REPOS: [GhRepo; 1] = [GhRepo { + owner: CompactString::const_new("cargo-bins"), + repo: CompactString::const_new("ttt"), + }]; + + init_logger(); + + let mut tests: Vec<(_, _)> = Vec::new(); + + for client in create_client() { + let spawn_get_repo_info_task = |repo| { + let client = client.clone(); + tokio::spawn(async move { + loop { + match client.get_repo_info(&repo).await { + Err(GhApiError::RateLimit { retry_after }) => { + sleep(retry_after.unwrap_or(DEFAULT_RETRY_AFTER)).await + } + res => break res, + } + } + }) + }; + + for repo in PUBLIC_REPOS { + tests.push(( + Some(RepoInfo::new(repo.clone(), false)), + spawn_get_repo_info_task(repo), + )); + } + + for repo in NON_EXISTENT_REPOS { + tests.push((None, spawn_get_repo_info_task(repo))); + } + + if client.has_gh_token() { + for repo in PRIVATE_REPOS { + tests.push(( + Some(RepoInfo::new(repo.clone(), true)), + spawn_get_repo_info_task(repo), + )); + } + } + } + + for (expected, task) in tests { + assert_eq!(task.await.unwrap().unwrap(), expected); + } + } + + #[tokio::test] + async fn rate_limited_test_has_release_artifact_and_download_artifacts() { + const RELEASES: [(GhRelease, &[&str]); 1] = [( + cargo_binstall_v0_20_1::RELEASE, + cargo_binstall_v0_20_1::ARTIFACTS, + )]; + const NON_EXISTENT_RELEASES: [GhRelease; 1] = [GhRelease { + repo: GhRepo { + owner: CompactString::const_new("cargo-bins"), + repo: CompactString::const_new("cargo-binstall"), + }, + // We are currently at v0.20.1 and we would never release + // anything older than v0.20.1 + tag: CompactString::const_new("v0.18.2"), + }]; + + init_logger(); + + let mut tasks = Vec::new(); + + for client in create_client() { + async fn has_release_artifact( + client: &GhApiClient, + artifact: &GhReleaseArtifact, + ) -> Result, GhApiError> { + loop { + match client.has_release_artifact(artifact.clone()).await { + Err(GhApiError::RateLimit { retry_after }) => { + sleep(retry_after.unwrap_or(DEFAULT_RETRY_AFTER)).await + } + res => break res, + } + } + } + + for (release, artifacts) in RELEASES { + for artifact_name in artifacts { + let client = client.clone(); + let release = release.clone(); + tasks.push(tokio::spawn(async move { + let artifact = GhReleaseArtifact { + release, + artifact_name: artifact_name.to_compact_string(), + }; + + let browser_download_task = client.get_auth_token().map(|_| { + tokio::spawn( + Download::new( + client.remote_client().clone(), + Url::parse(&format!( + "https://github.com/{}/{}/releases/download/{}/{}", + artifact.release.repo.owner, + artifact.release.repo.repo, + artifact.release.tag, + artifact.artifact_name, + )) + .unwrap(), + ) + .into_bytes(), + ) + }); + let artifact_url = has_release_artifact(&client, &artifact) + .await + .unwrap() + .unwrap(); + + if let Some(browser_download_task) = browser_download_task { + let artifact_download_data = loop { + match client.download_artifact(artifact_url.clone()).await { + Err(GhApiError::RateLimit { retry_after }) => { + sleep(retry_after.unwrap_or(DEFAULT_RETRY_AFTER)).await + } + res => break res.unwrap(), + } + } + .into_bytes() + .await + .unwrap(); + + let browser_download_data = + browser_download_task.await.unwrap().unwrap(); + + assert_eq!(artifact_download_data, browser_download_data); + } + })); + } + + let client = client.clone(); + tasks.push(tokio::spawn(async move { + assert_eq!( + has_release_artifact( + &client, + &GhReleaseArtifact { + release, + artifact_name: "123z".to_compact_string(), + } + ) + .await + .unwrap(), + None + ); + })); + } + + for release in NON_EXISTENT_RELEASES { + let client = client.clone(); + + tasks.push(tokio::spawn(async move { + assert_eq!( + has_release_artifact( + &client, + &GhReleaseArtifact { + release, + artifact_name: "1234".to_compact_string(), + } + ) + .await + .unwrap(), + None + ); + })); + } + } + + for task in tasks { + task.await.unwrap(); + } + } +} diff --git a/crates/binstalk-git-repo-api/src/gh_api_client/common.rs b/crates/binstalk-git-repo-api/src/gh_api_client/common.rs new file mode 100644 index 00000000..723834ec --- /dev/null +++ b/crates/binstalk-git-repo-api/src/gh_api_client/common.rs @@ -0,0 +1,130 @@ +use std::{fmt::Debug, future::Future, sync::OnceLock}; + +use binstalk_downloader::remote::{self, Response, Url}; +use compact_str::CompactString; +use percent_encoding::percent_decode_str; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::to_string as to_json_string; +use tracing::debug; + +use super::{GhApiError, GhGraphQLErrors}; + +pub(super) fn percent_decode_http_url_path(input: &str) -> CompactString { + if input.contains('%') { + percent_decode_str(input).decode_utf8_lossy().into() + } else { + // No '%', no need to decode. + CompactString::new(input) + } +} + +pub(super) fn check_http_status_and_header(response: Response) -> Result { + match response.status() { + remote::StatusCode::UNAUTHORIZED => Err(GhApiError::Unauthorized), + remote::StatusCode::NOT_FOUND => Err(GhApiError::NotFound), + + _ => Ok(response.error_for_status()?), + } +} + +fn get_api_endpoint() -> &'static Url { + static API_ENDPOINT: OnceLock = OnceLock::new(); + + API_ENDPOINT.get_or_init(|| { + Url::parse("https://api.github.com/").expect("Literal provided must be a valid url") + }) +} + +pub(super) fn issue_restful_api( + client: &remote::Client, + path: &[&str], + auth_token: Option<&str>, +) -> impl Future> + Send + 'static +where + T: DeserializeOwned, +{ + let mut url = get_api_endpoint().clone(); + + url.path_segments_mut() + .expect("get_api_endpoint() should return a https url") + .extend(path); + + debug!("Getting restful API: {url}"); + + let mut request_builder = client + .get(url) + .header("Accept", "application/vnd.github+json") + .header("X-GitHub-Api-Version", "2022-11-28"); + + if let Some(auth_token) = auth_token { + request_builder = request_builder.bearer_auth(&auth_token); + } + + let future = request_builder.send(false); + + async move { + let response = check_http_status_and_header(future.await?)?; + + Ok(response.json().await?) + } +} + +#[derive(Debug, Deserialize)] +struct GraphQLResponse { + data: T, + errors: Option, +} + +#[derive(Serialize)] +struct GraphQLQuery { + query: String, +} + +fn get_graphql_endpoint() -> Url { + let mut graphql_endpoint = get_api_endpoint().clone(); + + graphql_endpoint + .path_segments_mut() + .expect("get_api_endpoint() should return a https url") + .push("graphql"); + + graphql_endpoint +} + +pub(super) fn issue_graphql_query( + client: &remote::Client, + query: String, + auth_token: &str, +) -> impl Future> + Send + 'static +where + T: DeserializeOwned + Debug, +{ + let res = to_json_string(&GraphQLQuery { query }) + .map_err(remote::Error::from) + .map(|graphql_query| { + let graphql_endpoint = get_graphql_endpoint(); + + debug!("Sending graphql query to {graphql_endpoint}: '{graphql_query}'"); + + let request_builder = client + .post(graphql_endpoint, graphql_query) + .header("Accept", "application/vnd.github+json") + .bearer_auth(&auth_token); + + request_builder.send(false) + }); + + async move { + let response = check_http_status_and_header(res?.await?)?; + + let mut response: GraphQLResponse = response.json().await?; + + debug!("response = {response:?}"); + + if let Some(error) = response.errors.take() { + Err(error.into()) + } else { + Ok(response.data) + } + } +} diff --git a/crates/binstalk-git-repo-api/src/gh_api_client/error.rs b/crates/binstalk-git-repo-api/src/gh_api_client/error.rs new file mode 100644 index 00000000..0a2918a9 --- /dev/null +++ b/crates/binstalk-git-repo-api/src/gh_api_client/error.rs @@ -0,0 +1,203 @@ +use std::{error, fmt, io, time::Duration}; + +use binstalk_downloader::remote; +use compact_str::{CompactString, ToCompactString}; +use serde::{de::Deserializer, Deserialize}; +use thiserror::Error as ThisError; + +#[derive(ThisError, Debug)] +#[error("Context: '{context}', err: '{err}'")] +pub struct GhApiContextError { + context: CompactString, + #[source] + err: GhApiError, +} + +#[derive(ThisError, Debug)] +#[non_exhaustive] +pub enum GhApiError { + #[error("IO Error: {0}")] + Io(#[from] io::Error), + + #[error("Remote Error: {0}")] + Remote(#[from] remote::Error), + + #[error("Failed to parse url: {0}")] + InvalidUrl(#[from] url::ParseError), + + /// A wrapped error providing the context the error is about. + #[error(transparent)] + Context(Box), + + #[error("Remote failed to process GraphQL query: {0}")] + GraphQLErrors(GhGraphQLErrors), + + #[error("Hit rate-limit, retry after {retry_after:?}")] + RateLimit { retry_after: Option }, + + #[error("Corresponding resource is not found")] + NotFound, + + #[error("Does not have permission to access the API")] + Unauthorized, +} + +impl GhApiError { + /// Attach context to [`GhApiError`] + pub fn context(self, context: impl fmt::Display) -> Self { + use GhApiError::*; + + if matches!(self, RateLimit { .. } | NotFound | Unauthorized) { + self + } else { + Self::Context(Box::new(GhApiContextError { + context: context.to_compact_string(), + err: self, + })) + } + } +} + +impl From for GhApiError { + fn from(e: GhGraphQLErrors) -> Self { + if e.is_rate_limited() { + Self::RateLimit { retry_after: None } + } else if e.is_not_found_error() { + Self::NotFound + } else { + Self::GraphQLErrors(e) + } + } +} + +#[derive(Debug, Deserialize)] +pub struct GhGraphQLErrors(Box<[GraphQLError]>); + +impl GhGraphQLErrors { + fn is_rate_limited(&self) -> bool { + self.0 + .iter() + .any(|error| matches!(error.error_type, GraphQLErrorType::RateLimited)) + } + + fn is_not_found_error(&self) -> bool { + self.0 + .iter() + .any(|error| matches!(&error.error_type, GraphQLErrorType::Other(error_type) if *error_type == "NOT_FOUND")) + } +} + +impl error::Error for GhGraphQLErrors {} + +impl fmt::Display for GhGraphQLErrors { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let last_error_index = self.0.len() - 1; + + for (i, error) in self.0.iter().enumerate() { + write!( + f, + "type: '{error_type}', msg: '{msg}'", + error_type = error.error_type, + msg = error.message, + )?; + + for location in error.locations.as_deref().into_iter().flatten() { + write!( + f, + ", occured on query line {line} col {col}", + line = location.line, + col = location.column + )?; + } + + for (k, v) in &error.others { + write!(f, ", {k}: {v}")?; + } + + if i < last_error_index { + f.write_str("\n")?; + } + } + + Ok(()) + } +} + +#[derive(Debug, Deserialize)] +struct GraphQLError { + message: CompactString, + locations: Option>, + + #[serde(rename = "type")] + error_type: GraphQLErrorType, + + #[serde(flatten, with = "tuple_vec_map")] + others: Vec<(CompactString, serde_json::Value)>, +} + +#[derive(Debug)] +pub(super) enum GraphQLErrorType { + RateLimited, + Other(CompactString), +} + +impl fmt::Display for GraphQLErrorType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + GraphQLErrorType::RateLimited => "RATE_LIMITED", + GraphQLErrorType::Other(s) => s, + }) + } +} + +impl<'de> Deserialize<'de> for GraphQLErrorType { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = CompactString::deserialize(deserializer)?; + Ok(match &*s { + "RATE_LIMITED" => GraphQLErrorType::RateLimited, + _ => GraphQLErrorType::Other(s), + }) + } +} + +#[derive(Debug, Deserialize)] +struct GraphQLLocation { + line: u64, + column: u64, +} + +#[cfg(test)] +mod test { + use super::*; + use serde::de::value::{BorrowedStrDeserializer, Error}; + + macro_rules! assert_matches { + ($expression:expr, $pattern:pat $(if $guard:expr)? $(,)?) => { + match $expression { + $pattern $(if $guard)? => true, + expr => { + panic!( + "assertion failed: `{expr:?}` does not match `{}`", + stringify!($pattern $(if $guard)?) + ) + } + } + } + } + + #[test] + fn test_graph_ql_error_type() { + let deserialize = |input: &str| { + GraphQLErrorType::deserialize(BorrowedStrDeserializer::<'_, Error>::new(input)).unwrap() + }; + + assert_matches!(deserialize("RATE_LIMITED"), GraphQLErrorType::RateLimited); + assert_matches!( + deserialize("rATE_LIMITED"), + GraphQLErrorType::Other(val) if val == CompactString::const_new("rATE_LIMITED") + ); + } +} diff --git a/crates/binstalk-git-repo-api/src/gh_api_client/release_artifacts.rs b/crates/binstalk-git-repo-api/src/gh_api_client/release_artifacts.rs new file mode 100644 index 00000000..5b738842 --- /dev/null +++ b/crates/binstalk-git-repo-api/src/gh_api_client/release_artifacts.rs @@ -0,0 +1,192 @@ +use std::{ + borrow::Borrow, + collections::HashSet, + fmt, + future::Future, + hash::{Hash, Hasher}, +}; + +use binstalk_downloader::remote::{self}; +use compact_str::{CompactString, ToCompactString}; +use serde::Deserialize; +use url::Url; + +use super::{ + common::{issue_graphql_query, issue_restful_api}, + GhApiError, GhRelease, GhRepo, +}; + +// Only include fields we do care about + +#[derive(Eq, Deserialize, Debug)] +struct Artifact { + name: CompactString, + url: Url, +} + +// Manually implement PartialEq and Hash to ensure it will always produce the +// same hash as a str with the same content, and that the comparison will be +// the same to coparing a string. + +impl PartialEq for Artifact { + fn eq(&self, other: &Self) -> bool { + self.name.eq(&other.name) + } +} + +impl Hash for Artifact { + fn hash(&self, state: &mut H) + where + H: Hasher, + { + let s: &str = self.name.as_str(); + s.hash(state) + } +} + +// Implement Borrow so that we can use call +// `HashSet::contains::` + +impl Borrow for Artifact { + fn borrow(&self) -> &str { + &self.name + } +} + +#[derive(Debug, Default, Deserialize)] +pub(super) struct Artifacts { + assets: HashSet, +} + +impl Artifacts { + /// get url for downloading the artifact using GitHub API (for private repository). + pub(super) fn get_artifact_url(&self, artifact_name: &str) -> Option { + self.assets + .get(artifact_name) + .map(|artifact| artifact.url.clone()) + } +} + +pub(super) fn fetch_release_artifacts_restful_api( + client: &remote::Client, + GhRelease { + repo: GhRepo { owner, repo }, + tag, + }: &GhRelease, + auth_token: Option<&str>, +) -> impl Future> + Send + 'static { + issue_restful_api( + client, + &["repos", owner, repo, "releases", "tags", tag], + auth_token, + ) +} + +#[derive(Debug, Deserialize)] +struct GraphQLData { + repository: Option, +} + +#[derive(Debug, Deserialize)] +struct GraphQLRepo { + release: Option, +} + +#[derive(Debug, Deserialize)] +struct GraphQLRelease { + #[serde(rename = "releaseAssets")] + assets: GraphQLReleaseAssets, +} + +#[derive(Debug, Deserialize)] +struct GraphQLReleaseAssets { + nodes: Vec, + #[serde(rename = "pageInfo")] + page_info: GraphQLPageInfo, +} + +#[derive(Debug, Deserialize)] +struct GraphQLPageInfo { + #[serde(rename = "endCursor")] + end_cursor: Option, + #[serde(rename = "hasNextPage")] + has_next_page: bool, +} + +enum FilterCondition { + Init, + After(CompactString), +} + +impl fmt::Display for FilterCondition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + // GitHub imposes a limit of 100 for the value passed to param "first" + FilterCondition::Init => f.write_str("first:100"), + FilterCondition::After(end_cursor) => write!(f, r#"first:100,after:"{end_cursor}""#), + } + } +} + +pub(super) fn fetch_release_artifacts_graphql_api( + client: &remote::Client, + GhRelease { + repo: GhRepo { owner, repo }, + tag, + }: &GhRelease, + auth_token: &str, +) -> impl Future> + Send + 'static { + let client = client.clone(); + let auth_token = auth_token.to_compact_string(); + + let base_query_prefix = format!( + r#" +query {{ + repository(owner:"{owner}",name:"{repo}") {{ + release(tagName:"{tag}") {{"# + ); + + let base_query_suffix = r#" + nodes { name url } + pageInfo { endCursor hasNextPage } +}}}}"# + .trim(); + + async move { + let mut artifacts = Artifacts::default(); + let mut cond = FilterCondition::Init; + let base_query_prefix = base_query_prefix.trim(); + + loop { + let query = format!( + r#" +{base_query_prefix} +releaseAssets({cond}) {{ +{base_query_suffix}"# + ); + + let data: GraphQLData = issue_graphql_query(&client, query, &auth_token).await?; + + let assets = data + .repository + .and_then(|repository| repository.release) + .map(|release| release.assets); + + if let Some(assets) = assets { + artifacts.assets.extend(assets.nodes); + + match assets.page_info { + GraphQLPageInfo { + end_cursor: Some(end_cursor), + has_next_page: true, + } => { + cond = FilterCondition::After(end_cursor); + } + _ => break Ok(artifacts), + } + } else { + break Err(GhApiError::NotFound); + } + } + } +} diff --git a/crates/binstalk-git-repo-api/src/gh_api_client/repo_info.rs b/crates/binstalk-git-repo-api/src/gh_api_client/repo_info.rs new file mode 100644 index 00000000..d95cca81 --- /dev/null +++ b/crates/binstalk-git-repo-api/src/gh_api_client/repo_info.rs @@ -0,0 +1,91 @@ +use std::{fmt, future::Future}; + +use compact_str::CompactString; +use serde::Deserialize; + +use super::{ + common::{issue_graphql_query, issue_restful_api}, + remote, GhApiError, GhRepo, +}; + +#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize)] +struct Owner { + login: CompactString, +} + +#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize)] +pub struct RepoInfo { + owner: Owner, + name: CompactString, + private: bool, +} + +impl fmt::Display for RepoInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "RepoInfo {{ owner: {}, name: {}, is_private: {} }}", + self.owner.login, self.name, self.private + ) + } +} + +impl RepoInfo { + #[cfg(test)] + pub(crate) fn new(GhRepo { owner, repo }: GhRepo, private: bool) -> Self { + Self { + owner: Owner { login: owner }, + name: repo, + private, + } + } + pub fn repo(&self) -> GhRepo { + GhRepo { + owner: self.owner.login.clone(), + repo: self.name.clone(), + } + } + + pub fn is_private(&self) -> bool { + self.private + } +} + +pub(super) fn fetch_repo_info_restful_api( + client: &remote::Client, + GhRepo { owner, repo }: &GhRepo, + auth_token: Option<&str>, +) -> impl Future, GhApiError>> + Send + 'static { + issue_restful_api(client, &["repos", owner, repo], auth_token) +} + +#[derive(Debug, Deserialize)] +struct GraphQLData { + repository: Option, +} + +pub(super) fn fetch_repo_info_graphql_api( + client: &remote::Client, + GhRepo { owner, repo }: &GhRepo, + auth_token: &str, +) -> impl Future, GhApiError>> + Send + 'static { + let query = format!( + r#" +query {{ + repository(owner:"{owner}",name:"{repo}") {{ + owner {{ + login + }} + name + private: isPrivate + }} +}}"# + ); + + let future = issue_graphql_query(client, query, auth_token); + + async move { + let data: GraphQLData = future.await?; + Ok(data.repository) + } +} diff --git a/crates/binstalk-git-repo-api/src/lib.rs b/crates/binstalk-git-repo-api/src/lib.rs new file mode 100644 index 00000000..7d7dd52c --- /dev/null +++ b/crates/binstalk-git-repo-api/src/lib.rs @@ -0,0 +1 @@ +pub mod gh_api_client; diff --git a/crates/binstalk-manifests/CHANGELOG.md b/crates/binstalk-manifests/CHANGELOG.md new file mode 100644 index 00000000..3eac50b0 --- /dev/null +++ b/crates/binstalk-manifests/CHANGELOG.md @@ -0,0 +1,217 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.16.1](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.16.0...binstalk-manifests-v0.16.1) - 2025-06-10 + +### Other + +- updated the following local packages: detect-targets + +## [0.16.0](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.31...binstalk-manifests-v0.16.0) - 2025-06-06 + +### Fixed + +- fix updating of installed crates manifest on custom sparse registry ([#2178](https://github.com/cargo-bins/cargo-binstall/pull/2178)) + +### Other + +- Optimize CratesToml ([#2186](https://github.com/cargo-bins/cargo-binstall/pull/2186)) + +## [0.15.31](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.30...binstalk-manifests-v0.15.31) - 2025-05-30 + +### Other + +- updated the following local packages: detect-targets + +## [0.15.30](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.29...binstalk-manifests-v0.15.30) - 2025-05-16 + +### Other + +- updated the following local packages: detect-targets + +## [0.15.29](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.28...binstalk-manifests-v0.15.29) - 2025-05-07 + +### Other + +- updated the following local packages: detect-targets + +## [0.15.28](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.27...binstalk-manifests-v0.15.28) - 2025-04-05 + +### Other + +- Fix clippy lints ([#2111](https://github.com/cargo-bins/cargo-binstall/pull/2111)) + +## [0.15.27](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.26...binstalk-manifests-v0.15.27) - 2025-03-19 + +### Other + +- updated the following local packages: detect-targets, fs-lock + +## [0.15.26](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.25...binstalk-manifests-v0.15.26) - 2025-03-15 + +### Other + +- updated the following local packages: detect-targets + +## [0.15.25](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.24...binstalk-manifests-v0.15.25) - 2025-03-07 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#2072](https://github.com/cargo-bins/cargo-binstall/pull/2072)) + +## [0.15.24](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.23...binstalk-manifests-v0.15.24) - 2025-02-28 + +### Other + +- updated the following local packages: detect-targets + +## [0.15.23](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.22...binstalk-manifests-v0.15.23) - 2025-02-22 + +### Other + +- Log when FileLock::drop fails to unlock file ([#2064](https://github.com/cargo-bins/cargo-binstall/pull/2064)) + +## [0.15.22](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.21...binstalk-manifests-v0.15.22) - 2025-02-15 + +### Other + +- updated the following local packages: detect-targets + +## [0.15.21](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.20...binstalk-manifests-v0.15.21) - 2025-02-11 + +### Other + +- updated the following local packages: binstalk-types, detect-targets + +## [0.15.20](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.19...binstalk-manifests-v0.15.20) - 2025-02-04 + +### Other + +- updated the following local packages: detect-targets + +## [0.15.19](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.18...binstalk-manifests-v0.15.19) - 2025-01-19 + +### Other + +- update Cargo.lock dependencies + +## [0.15.18](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.17...binstalk-manifests-v0.15.18) - 2025-01-13 + +### Other + +- update Cargo.lock dependencies + +## [0.15.17](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.16...binstalk-manifests-v0.15.17) - 2025-01-11 + +### Other + +- *(deps)* bump the deps group with 3 updates (#2015) + +## [0.15.16](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.15...binstalk-manifests-v0.15.16) - 2025-01-04 + +### Other + +- updated the following local packages: detect-targets + +## [0.15.15](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.14...binstalk-manifests-v0.15.15) - 2024-12-28 + +### Other + +- updated the following local packages: detect-targets + +## [0.15.14](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.13...binstalk-manifests-v0.15.14) - 2024-12-14 + +### Other + +- *(deps)* bump the deps group with 2 updates (#1997) + +## [0.15.13](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.12...binstalk-manifests-v0.15.13) - 2024-12-07 + +### Other + +- updated the following local packages: detect-targets, fs-lock + +## [0.15.12](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.11...binstalk-manifests-v0.15.12) - 2024-11-29 + +### Other + +- updated the following local packages: detect-targets + +## [0.15.11](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.10...binstalk-manifests-v0.15.11) - 2024-11-23 + +### Other + +- *(deps)* bump the deps group with 2 updates ([#1981](https://github.com/cargo-bins/cargo-binstall/pull/1981)) + +## [0.15.10](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.9...binstalk-manifests-v0.15.10) - 2024-11-18 + +### Other + +- updated the following local packages: detect-targets + +## [0.15.9](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.8...binstalk-manifests-v0.15.9) - 2024-11-09 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1966](https://github.com/cargo-bins/cargo-binstall/pull/1966)) + +## [0.15.8](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.7...binstalk-manifests-v0.15.8) - 2024-11-05 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1954](https://github.com/cargo-bins/cargo-binstall/pull/1954)) + +## [0.15.7](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.6...binstalk-manifests-v0.15.7) - 2024-11-02 + +### Other + +- updated the following local packages: detect-targets + +## [0.15.6](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.5...binstalk-manifests-v0.15.6) - 2024-10-25 + +### Other + +- updated the following local packages: detect-targets + +## [0.15.5](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.4...binstalk-manifests-v0.15.5) - 2024-10-12 + +### Other + +- updated the following local packages: detect-targets, fs-lock + +## [0.15.4](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.3...binstalk-manifests-v0.15.4) - 2024-10-04 + +### Other + +- updated the following local packages: detect-targets + +## [0.15.3](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.2...binstalk-manifests-v0.15.3) - 2024-09-22 + +### Other + +- updated the following local packages: detect-targets + +## [0.15.2](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.1...binstalk-manifests-v0.15.2) - 2024-09-06 + +### Other +- updated the following local packages: detect-targets + +## [0.15.1](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.15.0...binstalk-manifests-v0.15.1) - 2024-08-25 + +### Other +- updated the following local packages: detect-targets + +## [0.15.0](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.14.1...binstalk-manifests-v0.15.0) - 2024-08-10 + +### Other +- updated the following local packages: binstalk-types, detect-targets + +## [0.14.1](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-manifests-v0.14.0...binstalk-manifests-v0.14.1) - 2024-08-04 + +### Other +- updated the following local packages: detect-targets, fs-lock diff --git a/crates/binstalk-manifests/Cargo.toml b/crates/binstalk-manifests/Cargo.toml new file mode 100644 index 00000000..30581b59 --- /dev/null +++ b/crates/binstalk-manifests/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "binstalk-manifests" +description = "The binstall toolkit for manipulating with manifest" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/binstalk-manifests" +version = "0.16.1" +rust-version = "1.61.0" +authors = ["ryan "] +edition = "2021" +license = "Apache-2.0 OR MIT" + +[dependencies] +beef = { version = "0.5.2", features = ["impl_serde"] } +binstalk-types = { version = "0.10.0", path = "../binstalk-types" } +compact_str = { version = "0.9.0", features = ["serde"] } +fs-lock = { version = "0.1.10", path = "../fs-lock", features = ["tracing"] } +home = "0.5.9" +miette = "7.0.0" +semver = { version = "1.0.17", features = ["serde"] } +serde = { version = "1.0.163", features = ["derive"] } +serde-tuple-vec-map = "1.0.1" +serde_json = "1.0.107" +thiserror = "2.0.11" +toml_edit = { version = "0.22.12", features = ["serde"] } +url = { version = "2.5.4", features = ["serde"] } + +[dev-dependencies] +detect-targets = { version = "0.1.52", path = "../detect-targets" } +tempfile = "3.5.0" diff --git a/crates/binstalk-manifests/LICENSE-APACHE b/crates/binstalk-manifests/LICENSE-APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/crates/binstalk-manifests/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/binstalk-manifests/LICENSE-MIT b/crates/binstalk-manifests/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/crates/binstalk-manifests/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/crates/binstalk-manifests/src/binstall_crates_v1.rs b/crates/binstalk-manifests/src/binstall_crates_v1.rs new file mode 100644 index 00000000..827ea2c0 --- /dev/null +++ b/crates/binstalk-manifests/src/binstall_crates_v1.rs @@ -0,0 +1,332 @@ +//! Binstall's `crates-v1.json` manifest. +//! +//! This manifest is used by Binstall to record which crates were installed, and may be used by +//! other (third party) tooling to act upon these crates (e.g. upgrade them, list them, etc). +//! +//! The format is a series of JSON object concatenated together. It is _not_ NLJSON, though writing +//! NLJSON to the file will be understood fine. + +use std::{ + borrow::Borrow, + cmp, + collections::{btree_set, BTreeSet}, + fs, + io::{self, Seek, Write}, + iter::{IntoIterator, Iterator}, + path::{Path, PathBuf}, +}; + +use compact_str::CompactString; +use fs_lock::FileLock; +use home::cargo_home; +use miette::Diagnostic; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::{crate_info::CrateInfo, helpers::create_if_not_exist}; + +/// Buffer size for loading and writing binstall_crates_v1 manifest. +const BUFFER_SIZE: usize = 4096 * 5; + +#[derive(Debug, Diagnostic, Error)] +#[non_exhaustive] +pub enum Error { + #[error("I/O Error: {0}")] + Io(#[from] io::Error), + + #[error("Failed to parse json: {0}")] + SerdeJsonParse(#[from] serde_json::Error), +} + +pub fn append_to_path(path: impl AsRef, iter: Iter) -> Result<(), Error> +where + Iter: IntoIterator, + Data: From, +{ + let path = path.as_ref(); + let mut file = create_if_not_exist(path)?; + // Move the cursor to EOF + file.seek(io::SeekFrom::End(0))?; + + write_to(&mut file, &mut iter.into_iter().map(Data::from)) +} + +pub fn append(iter: Iter) -> Result<(), Error> +where + Iter: IntoIterator, + Data: From, +{ + append_to_path(default_path()?, iter) +} + +pub fn write_to(file: &mut FileLock, iter: &mut dyn Iterator) -> Result<(), Error> { + let writer = io::BufWriter::with_capacity(BUFFER_SIZE, file); + + let mut ser = serde_json::Serializer::new(writer); + + for item in iter { + item.serialize(&mut ser)?; + } + + ser.into_inner().flush()?; + + Ok(()) +} + +pub fn default_path() -> Result { + let dir = cargo_home()?.join("binstall"); + + fs::create_dir_all(&dir)?; + + Ok(dir.join("crates-v1.json")) +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Data { + #[serde(flatten)] + pub crate_info: CrateInfo, + + /// Forwards compatibility. Unknown keys from future versions + /// will be stored here and retained when the file is saved. + /// + /// We use an `Vec` here since it is never accessed in Rust. + #[serde(flatten, with = "tuple_vec_map")] + pub other: Vec<(CompactString, serde_json::Value)>, +} + +impl From for Data { + fn from(crate_info: CrateInfo) -> Self { + Self { + crate_info, + other: Vec::new(), + } + } +} + +impl From for CrateInfo { + fn from(data: Data) -> Self { + data.crate_info + } +} + +impl Borrow for Data { + fn borrow(&self) -> &str { + &self.crate_info.name + } +} + +impl PartialEq for Data { + fn eq(&self, other: &Self) -> bool { + self.crate_info.name == other.crate_info.name + } +} +impl PartialEq for Data { + fn eq(&self, other: &CrateInfo) -> bool { + self.crate_info.name == other.name + } +} +impl PartialEq for CrateInfo { + fn eq(&self, other: &Data) -> bool { + self.name == other.crate_info.name + } +} +impl Eq for Data {} + +impl PartialOrd for Data { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Data { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.crate_info.name.cmp(&other.crate_info.name) + } +} + +#[derive(Debug)] +pub struct Records { + file: FileLock, + /// Use BTreeSet to dedup the metadata + data: BTreeSet, +} + +impl Records { + fn load_impl(&mut self) -> Result<(), Error> { + let reader = io::BufReader::with_capacity(BUFFER_SIZE, &mut self.file); + let stream_deser = serde_json::Deserializer::from_reader(reader).into_iter(); + + for res in stream_deser { + let item = res?; + + self.data.replace(item); + } + + Ok(()) + } + + pub fn load_from_path(path: impl AsRef) -> Result { + let mut this = Self { + file: create_if_not_exist(path.as_ref())?, + data: BTreeSet::default(), + }; + this.load_impl()?; + Ok(this) + } + + pub fn load() -> Result { + Self::load_from_path(default_path()?) + } + + /// **Warning: This will overwrite all existing records!** + pub fn overwrite(mut self) -> Result<(), Error> { + self.file.rewind()?; + write_to(&mut self.file, &mut self.data.into_iter())?; + + let len = self.file.stream_position()?; + self.file.set_len(len)?; + + Ok(()) + } + + pub fn get(&self, value: impl AsRef) -> Option<&CrateInfo> { + self.data.get(value.as_ref()).map(|data| &data.crate_info) + } + + pub fn contains(&self, value: impl AsRef) -> bool { + self.data.contains(value.as_ref()) + } + + /// Adds a value to the set. + /// If the set did not have an equal element present, true is returned. + /// If the set did have an equal element present, false is returned, + /// and the entry is not updated. + pub fn insert(&mut self, value: CrateInfo) -> bool { + self.data.insert(Data::from(value)) + } + + /// Return the previous `CrateInfo` for the package if there is any. + pub fn replace(&mut self, value: CrateInfo) -> Option { + self.data.replace(Data::from(value)).map(CrateInfo::from) + } + + pub fn remove(&mut self, value: impl AsRef) -> bool { + self.data.remove(value.as_ref()) + } + + /// Remove crates that `f(&data.crate_info)` returns `false`. + pub fn retain(&mut self, mut f: impl FnMut(&CrateInfo) -> bool) { + self.data.retain(|data| f(&data.crate_info)) + } + + pub fn take(&mut self, value: impl AsRef) -> Option { + self.data.take(value.as_ref()).map(CrateInfo::from) + } + + pub fn len(&self) -> usize { + self.data.len() + } + + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } +} + +impl<'a> IntoIterator for &'a Records { + type Item = &'a Data; + + type IntoIter = btree_set::Iter<'a, Data>; + + fn into_iter(self) -> Self::IntoIter { + self.data.iter() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::crate_info::CrateSource; + + use compact_str::CompactString; + use detect_targets::TARGET; + use semver::Version; + use tempfile::NamedTempFile; + + macro_rules! assert_records_eq { + ($records:expr, $metadata_set:expr) => { + assert_eq!($records.len(), $metadata_set.len()); + for (record, metadata) in $records.into_iter().zip($metadata_set.iter()) { + assert_eq!(record, metadata); + } + }; + } + + #[test] + fn rw_test() { + let target = CompactString::from(TARGET); + + let named_tempfile = NamedTempFile::new().unwrap(); + let path = named_tempfile.path(); + + let metadata_vec = [ + CrateInfo { + name: "a".into(), + version_req: "*".into(), + current_version: Version::new(0, 1, 0), + source: CrateSource::cratesio_registry(), + target: target.clone(), + bins: vec!["1".into(), "2".into()], + }, + CrateInfo { + name: "b".into(), + version_req: "0.1.0".into(), + current_version: Version::new(0, 1, 0), + source: CrateSource::cratesio_registry(), + target: target.clone(), + bins: vec!["1".into(), "2".into()], + }, + CrateInfo { + name: "a".into(), + version_req: "*".into(), + current_version: Version::new(0, 2, 0), + source: CrateSource::cratesio_registry(), + target: target.clone(), + bins: vec!["1".into()], + }, + ]; + + append_to_path(path, metadata_vec.clone()).unwrap(); + + let mut iter = metadata_vec.into_iter(); + iter.next().unwrap(); + + let mut metadata_set: BTreeSet<_> = iter.collect(); + + let mut records = Records::load_from_path(path).unwrap(); + assert_records_eq!(&records, &metadata_set); + + assert!(records.remove("b")); + metadata_set.remove("b"); + assert_eq!(records.len(), metadata_set.len()); + records.overwrite().unwrap(); + + let records = Records::load_from_path(path).unwrap(); + assert_records_eq!(&records, &metadata_set); + // Drop the exclusive file lock + drop(records); + + let new_metadata = CrateInfo { + name: "b".into(), + version_req: "0.1.0".into(), + current_version: Version::new(0, 1, 1), + source: CrateSource::cratesio_registry(), + target, + bins: vec!["1".into(), "2".into()], + }; + append_to_path(path, [new_metadata.clone()]).unwrap(); + metadata_set.insert(new_metadata); + + let records = Records::load_from_path(path).unwrap(); + assert_records_eq!(&records, &metadata_set); + } +} diff --git a/crates/binstalk-manifests/src/cargo_config.rs b/crates/binstalk-manifests/src/cargo_config.rs new file mode 100644 index 00000000..22e25971 --- /dev/null +++ b/crates/binstalk-manifests/src/cargo_config.rs @@ -0,0 +1,240 @@ +//! Cargo's `.cargo/config.toml` +//! +//! This manifest is used by Cargo to load configurations stored by users. +//! +//! Binstall reads from them to be compatible with `cargo-install`'s behavior. + +use std::{ + borrow::Cow, + collections::BTreeMap, + fs::File, + io, + path::{Path, PathBuf}, +}; + +use compact_str::CompactString; +use fs_lock::FileLock; +use home::cargo_home; +use miette::Diagnostic; +use serde::Deserialize; +use thiserror::Error; + +#[derive(Debug, Deserialize)] +pub struct Install { + /// `cargo install` destination directory + pub root: Option, +} + +#[derive(Debug, Deserialize)] +pub struct Http { + /// HTTP proxy in libcurl format: "host:port" + /// + /// env: CARGO_HTTP_PROXY or HTTPS_PROXY or https_proxy or http_proxy + pub proxy: Option, + /// timeout for each HTTP request, in seconds + /// + /// env: CARGO_HTTP_TIMEOUT or HTTP_TIMEOUT + pub timeout: Option, + /// path to Certificate Authority (CA) bundle + pub cainfo: Option, +} + +#[derive(Eq, PartialEq, Debug, Deserialize)] +#[serde(untagged)] +pub enum Env { + Value(CompactString), + WithOptions { + value: CompactString, + force: Option, + relative: Option, + }, +} + +#[derive(Debug, Deserialize)] +pub struct Registry { + pub index: Option, +} + +#[derive(Debug, Deserialize)] +pub struct DefaultRegistry { + pub default: Option, +} + +#[derive(Debug, Default, Deserialize)] +pub struct Config { + pub install: Option, + pub http: Option, + pub env: Option>, + pub registries: Option>, + pub registry: Option, +} + +fn join_if_relative(path: Option<&mut PathBuf>, dir: &Path) { + match path { + Some(path) if path.is_relative() => *path = dir.join(&*path), + _ => (), + } +} + +impl Config { + pub fn default_path() -> Result { + Ok(cargo_home()?.join("config.toml")) + } + + pub fn load() -> Result { + Self::load_from_path(Self::default_path()?) + } + + /// * `dir` - path to the dir where the config.toml is located. + /// For relative path in the config, `Config::load_from_reader` + /// will join the `dir` and the relative path to form the final + /// path. + pub fn load_from_reader( + mut reader: R, + dir: &Path, + ) -> Result { + fn inner(reader: &mut dyn io::Read, dir: &Path) -> Result { + let mut vec = Vec::new(); + reader.read_to_end(&mut vec)?; + + if vec.is_empty() { + Ok(Default::default()) + } else { + let mut config: Config = toml_edit::de::from_slice(&vec)?; + join_if_relative( + config + .install + .as_mut() + .and_then(|install| install.root.as_mut()), + dir, + ); + join_if_relative( + config.http.as_mut().and_then(|http| http.cainfo.as_mut()), + dir, + ); + if let Some(envs) = config.env.as_mut() { + for env in envs.values_mut() { + if let Env::WithOptions { + value, + relative: Some(true), + .. + } = env + { + let path = Cow::Borrowed(Path::new(&value)); + if path.is_relative() { + *value = dir.join(&path).to_string_lossy().into(); + } + } + } + } + Ok(config) + } + } + + inner(&mut reader, dir) + } + + pub fn load_from_path(path: impl AsRef) -> Result { + fn inner(path: &Path) -> Result { + match File::open(path) { + Ok(file) => { + let file = FileLock::new_shared(file)?.set_file_path(path); + // Any regular file must have a parent dir + Config::load_from_reader(file, path.parent().unwrap()) + } + Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(Default::default()), + Err(err) => Err(err.into()), + } + } + + inner(path.as_ref()) + } +} + +#[derive(Debug, Diagnostic, Error)] +#[non_exhaustive] +pub enum ConfigLoadError { + #[error("I/O Error: {0}")] + Io(#[from] io::Error), + + #[error("Failed to deserialize toml: {0}")] + TomlParse(Box), +} + +impl From for ConfigLoadError { + fn from(e: toml_edit::de::Error) -> Self { + ConfigLoadError::TomlParse(Box::new(e)) + } +} + +impl From for ConfigLoadError { + fn from(e: toml_edit::TomlError) -> Self { + ConfigLoadError::TomlParse(Box::new(e.into())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::{io::Cursor, path::MAIN_SEPARATOR}; + + use compact_str::format_compact; + + const CONFIG: &str = r#" +[env] +# Set ENV_VAR_NAME=value for any process run by Cargo +ENV_VAR_NAME = "value" +# Set even if already present in environment +ENV_VAR_NAME_2 = { value = "value", force = true } +# Value is relative to .cargo directory containing `config.toml`, make absolute +ENV_VAR_NAME_3 = { value = "relative-path", relative = true } + +[http] +debug = false # HTTP debugging +proxy = "host:port" # HTTP proxy in libcurl format +timeout = 30 # timeout for each HTTP request, in seconds +cainfo = "cert.pem" # path to Certificate Authority (CA) bundle + +[install] +root = "/some/path" # `cargo install` destination directory + "#; + + #[test] + fn test_loading() { + let config = Config::load_from_reader(Cursor::new(&CONFIG), Path::new("root")).unwrap(); + + assert_eq!( + config.install.unwrap().root.as_deref().unwrap(), + Path::new("/some/path") + ); + + let http = config.http.unwrap(); + assert_eq!(http.proxy.unwrap(), CompactString::const_new("host:port")); + assert_eq!(http.timeout.unwrap(), 30); + assert_eq!(http.cainfo.unwrap(), Path::new("root").join("cert.pem")); + + let env = config.env.unwrap(); + assert_eq!(env.len(), 3); + assert_eq!( + env.get("ENV_VAR_NAME").unwrap(), + &Env::Value(CompactString::const_new("value")) + ); + assert_eq!( + env.get("ENV_VAR_NAME_2").unwrap(), + &Env::WithOptions { + value: CompactString::new("value"), + force: Some(true), + relative: None, + } + ); + assert_eq!( + env.get("ENV_VAR_NAME_3").unwrap(), + &Env::WithOptions { + value: format_compact!("root{MAIN_SEPARATOR}relative-path"), + force: None, + relative: Some(true), + } + ); + } +} diff --git a/crates/binstalk-manifests/src/cargo_crates_v1.rs b/crates/binstalk-manifests/src/cargo_crates_v1.rs new file mode 100644 index 00000000..fb254238 --- /dev/null +++ b/crates/binstalk-manifests/src/cargo_crates_v1.rs @@ -0,0 +1,319 @@ +//! Cargo's `.crates.toml` manifest. +//! +//! This manifest is used by Cargo to record which crates were installed by `cargo-install` and by +//! other Cargo (first and third party) tooling to act upon these crates (e.g. upgrade them, list +//! them, etc). +//! +//! Binstall writes to this manifest when installing a crate, for interoperability with the Cargo +//! ecosystem. + +use std::{ + collections::BTreeMap, + fs::File, + io::{self, Seek}, + iter::IntoIterator, + path::{Path, PathBuf}, +}; + +use beef::Cow; +use compact_str::CompactString; +use fs_lock::FileLock; +use home::cargo_home; +use miette::Diagnostic; +use semver::Version; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::helpers::create_if_not_exist; + +use super::crate_info::CrateInfo; + +mod crate_version_source; +use crate_version_source::*; + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct CratesToml<'a> { + #[serde(with = "tuple_vec_map")] + v1: Vec<(Box, Cow<'a, [CompactString]>)>, +} + +impl<'v1> CratesToml<'v1> { + pub fn default_path() -> Result { + Ok(cargo_home()?.join(".crates.toml")) + } + + pub fn load() -> Result { + Self::load_from_path(Self::default_path()?) + } + + pub fn load_from_reader(mut reader: R) -> Result { + fn inner(reader: &mut dyn io::Read) -> Result, CratesTomlParseError> { + let mut vec = Vec::new(); + reader.read_to_end(&mut vec)?; + + if vec.is_empty() { + Ok(CratesToml::default()) + } else { + toml_edit::de::from_slice(&vec).map_err(CratesTomlParseError::from) + } + } + + inner(&mut reader) + } + + pub fn load_from_path(path: impl AsRef) -> Result { + let path = path.as_ref(); + let file = FileLock::new_shared(File::open(path)?)?.set_file_path(path); + Self::load_from_reader(file) + } + + pub fn remove(&mut self, name: &str) { + self.remove_all(&[name]); + } + + /// * `sorted_names` - must be sorted + pub fn remove_all(&mut self, sorted_names: &[&str]) { + self.v1.retain(|(s, _bin)| { + s.split_once(' ') + .map(|(crate_name, _rest)| sorted_names.binary_search(&crate_name).is_err()) + .unwrap_or_default() + }); + } + + pub fn write(&self) -> Result<(), CratesTomlParseError> { + self.write_to_path(Self::default_path()?) + } + + pub fn write_to_writer(&self, mut writer: W) -> Result<(), CratesTomlParseError> { + fn inner( + this: &CratesToml<'_>, + writer: &mut dyn io::Write, + ) -> Result<(), CratesTomlParseError> { + let data = toml_edit::ser::to_string_pretty(&this)?; + writer.write_all(data.as_bytes())?; + Ok(()) + } + + inner(self, &mut writer) + } + + pub fn write_to_file(&self, file: &mut File) -> Result<(), CratesTomlParseError> { + self.write_to_writer(&mut *file)?; + let pos = file.stream_position()?; + file.set_len(pos)?; + + Ok(()) + } + + pub fn write_to_path(&self, path: impl AsRef) -> Result<(), CratesTomlParseError> { + let path = path.as_ref(); + let mut file = FileLock::new_exclusive(File::create(path)?)?.set_file_path(path); + self.write_to_file(&mut file) + } + + pub fn add_crate(&mut self, metadata: &'v1 CrateInfo) { + let name = &metadata.name; + let version = &metadata.current_version; + let source = Source::from(&metadata.source); + + self.v1.push(( + format!("{name} {version} ({source})").into(), + Cow::borrowed(&metadata.bins), + )); + } + + pub fn append_to_file( + file: &mut File, + crates: &[CrateInfo], + ) -> Result<(), CratesTomlParseError> { + let mut c1 = CratesToml::load_from_reader(&mut *file)?; + + c1.remove_all(&{ + let mut crate_names: Vec<_> = crates + .iter() + .map(|metadata| metadata.name.as_str()) + .collect(); + crate_names.sort_unstable(); + crate_names + }); + + c1.v1.reserve_exact(crates.len()); + + for metadata in crates { + c1.add_crate(metadata); + } + + file.rewind()?; + c1.write_to_file(file)?; + + Ok(()) + } + + pub fn append_to_path( + path: impl AsRef, + crates: &[CrateInfo], + ) -> Result<(), CratesTomlParseError> { + let mut file = create_if_not_exist(path.as_ref())?; + Self::append_to_file(&mut file, crates) + } + + pub fn append(crates: &[CrateInfo]) -> Result<(), CratesTomlParseError> { + Self::append_to_path(Self::default_path()?, crates) + } + + /// Return BTreeMap with crate name as key and its corresponding version + /// as value. + pub fn collect_into_crates_versions( + self, + ) -> Result, CratesTomlParseError> { + fn parse_name_ver(s: &str) -> Result<(CompactString, Version), CvsParseError> { + match s.splitn(3, ' ').collect::>()[..] { + [name, version, _source] => Ok((CompactString::new(name), version.parse()?)), + _ => Err(CvsParseError::BadFormat), + } + } + + self.v1 + .into_iter() + .map(|(s, _bins)| parse_name_ver(&s).map_err(CratesTomlParseError::from)) + .collect() + } +} + +#[derive(Debug, Diagnostic, Error)] +#[non_exhaustive] +pub enum CratesTomlParseError { + #[error("I/O Error: {0}")] + Io(#[from] io::Error), + + #[error("Failed to deserialize toml: {0}")] + TomlParse(Box), + + #[error("Failed to serialie toml: {0}")] + TomlWrite(Box), + + #[error(transparent)] + CvsParse(Box), +} + +impl From for CratesTomlParseError { + fn from(e: CvsParseError) -> Self { + CratesTomlParseError::CvsParse(Box::new(e)) + } +} + +impl From for CratesTomlParseError { + fn from(e: toml_edit::ser::Error) -> Self { + CratesTomlParseError::TomlWrite(Box::new(e)) + } +} + +impl From for CratesTomlParseError { + fn from(e: toml_edit::de::Error) -> Self { + CratesTomlParseError::TomlParse(Box::new(e)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::crate_info::CrateSource; + + use detect_targets::TARGET; + use semver::Version; + use tempfile::TempDir; + + #[test] + fn test_empty() { + let tempdir = TempDir::new().unwrap(); + let path = tempdir.path().join("crates-v1.toml"); + + CratesToml::append_to_path( + &path, + &[CrateInfo { + name: "cargo-binstall".into(), + version_req: "*".into(), + current_version: Version::new(0, 11, 1), + source: CrateSource::cratesio_registry(), + target: TARGET.into(), + bins: vec!["cargo-binstall".into()], + }], + ) + .unwrap(); + + let crates = CratesToml::load_from_path(&path) + .unwrap() + .collect_into_crates_versions() + .unwrap(); + + assert_eq!(crates.len(), 1); + + assert_eq!( + crates.get("cargo-binstall").unwrap(), + &Version::new(0, 11, 1) + ); + + // Update + CratesToml::append_to_path( + &path, + &[CrateInfo { + name: "cargo-binstall".into(), + version_req: "*".into(), + current_version: Version::new(0, 12, 0), + source: CrateSource::cratesio_registry(), + target: TARGET.into(), + bins: vec!["cargo-binstall".into()], + }], + ) + .unwrap(); + + let crates = CratesToml::load_from_path(&path) + .unwrap() + .collect_into_crates_versions() + .unwrap(); + + assert_eq!(crates.len(), 1); + + assert_eq!( + crates.get("cargo-binstall").unwrap(), + &Version::new(0, 12, 0) + ); + } + + #[test] + fn test_empty_file() { + let tempdir = TempDir::new().unwrap(); + let path = tempdir.path().join("crates-v1.toml"); + + File::create(&path).unwrap(); + + assert!(CratesToml::load_from_path(&path).unwrap().v1.is_empty()); + } + + #[test] + fn test_loading() { + let raw_data = br#" +[v1] +"alacritty 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = ["alacritty"] +"cargo-audit 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-audit"] +"cargo-binstall 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-binstall"] +"cargo-criterion 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-criterion"] +"cargo-edit 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-add", "cargo-rm", "cargo-set-version", "cargo-upgrade"] +"cargo-expand 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-expand"] +"cargo-geiger 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-geiger"] +"cargo-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-hack"] +"cargo-nextest 0.9.26 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-nextest"] +"cargo-supply-chain 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-supply-chain"] +"cargo-tarpaulin 0.20.1 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-tarpaulin"] +"cargo-update 8.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-install-update", "cargo-install-update-config"] +"cargo-watch 8.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-watch"] +"cargo-with 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-with"] +"cross 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = ["cross", "cross-util"] +"irust 1.63.3 (registry+https://github.com/rust-lang/crates.io-index)" = ["irust"] +"tokei 12.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = ["tokei"] +"xargo 0.3.26 (registry+https://github.com/rust-lang/crates.io-index)" = ["xargo", "xargo-check"] + "#; + + CratesToml::load_from_reader(raw_data.as_slice()).unwrap(); + } +} diff --git a/crates/binstalk-manifests/src/cargo_crates_v1/crate_version_source.rs b/crates/binstalk-manifests/src/cargo_crates_v1/crate_version_source.rs new file mode 100644 index 00000000..e58b99bf --- /dev/null +++ b/crates/binstalk-manifests/src/cargo_crates_v1/crate_version_source.rs @@ -0,0 +1,165 @@ +use std::{ + borrow::Cow, + fmt::{self, Write as _}, + str::FromStr, +}; + +use binstalk_types::maybe_owned::MaybeOwned; +use compact_str::CompactString; +use miette::Diagnostic; +use semver::Version; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use thiserror::Error; +use url::Url; + +use crate::crate_info::{CrateInfo, CrateSource, SourceType}; + +#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] +pub struct CrateVersionSource { + pub name: CompactString, + pub version: Version, + pub source: Source<'static>, +} + +impl From<&CrateInfo> for CrateVersionSource { + fn from(metadata: &CrateInfo) -> Self { + use SourceType::*; + + let url = metadata.source.url.clone(); + + super::CrateVersionSource { + name: metadata.name.clone(), + version: metadata.current_version.clone(), + source: match metadata.source.source_type { + Git => Source::Git(url), + Path => Source::Path(url), + Registry => Source::Registry(url), + Sparse => Source::Sparse(url), + }, + } + } +} + +#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] +pub enum Source<'a> { + Git(MaybeOwned<'a, Url>), + Path(MaybeOwned<'a, Url>), + Registry(MaybeOwned<'a, Url>), + Sparse(MaybeOwned<'a, Url>), +} + +impl<'a> From<&'a CrateSource> for Source<'a> { + fn from(source: &'a CrateSource) -> Self { + use SourceType::*; + + let url = MaybeOwned::Borrowed(source.url.as_ref()); + + match source.source_type { + Git => Self::Git(url), + Path => Self::Path(url), + Registry => Self::Registry(url), + Sparse => Self::Sparse(url), + } + } +} + +impl FromStr for CrateVersionSource { + type Err = CvsParseError; + fn from_str(s: &str) -> Result { + match s.splitn(3, ' ').collect::>()[..] { + [name, version, source] => { + let version = version.parse()?; + let source = match source + .trim_matches(&['(', ')'][..]) + .splitn(2, '+') + .collect::>()[..] + { + ["git", url] => Source::Git(Url::parse(url)?.into()), + ["path", url] => Source::Path(Url::parse(url)?.into()), + ["registry", url] => Source::Registry(Url::parse(url)?.into()), + [kind, arg] => { + return Err(CvsParseError::UnknownSourceType { + kind: kind.to_string().into_boxed_str(), + arg: arg.to_string().into_boxed_str(), + }) + } + _ => return Err(CvsParseError::BadSource), + }; + Ok(Self { + name: name.into(), + version, + source, + }) + } + _ => Err(CvsParseError::BadFormat), + } + } +} + +#[derive(Debug, Diagnostic, Error)] +#[non_exhaustive] +pub enum CvsParseError { + #[error("Failed to parse url in cvs: {0}")] + UrlParse(#[from] url::ParseError), + + #[error("Failed to parse version in cvs: {0}")] + VersionParse(#[from] semver::Error), + + #[error("unknown source type {kind}+{arg}")] + UnknownSourceType { kind: Box, arg: Box }, + + #[error("bad source format")] + BadSource, + + #[error("bad CVS format")] + BadFormat, +} + +impl fmt::Display for CrateVersionSource { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + name, + version, + source, + } = &self; + write!(f, "{name} {version} ({source})") + } +} + +impl fmt::Display for Source<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Source::Git(url) => write!(f, "git+{url}"), + Source::Path(url) => write!(f, "path+{url}"), + Source::Registry(url) => write!(f, "registry+{url}"), + Source::Sparse(url) => { + let url = url.as_str(); + write!(f, "sparse+{url}")?; + if url.ends_with("/") { + Ok(()) + } else { + f.write_char('/') + } + } + } + } +} + +impl Serialize for CrateVersionSource { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for CrateVersionSource { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = Cow::<'_, str>::deserialize(deserializer)?; + Self::from_str(&s).map_err(serde::de::Error::custom) + } +} diff --git a/crates/binstalk-manifests/src/crates_manifests.rs b/crates/binstalk-manifests/src/crates_manifests.rs new file mode 100644 index 00000000..38eed6a7 --- /dev/null +++ b/crates/binstalk-manifests/src/crates_manifests.rs @@ -0,0 +1,90 @@ +use std::{ + collections::BTreeMap, + fs, + io::{self, Seek}, + path::Path, +}; + +use fs_lock::FileLock; +use miette::Diagnostic; +use thiserror::Error as ThisError; + +use crate::{ + binstall_crates_v1::{Error as BinstallCratesV1Error, Records as BinstallCratesV1Records}, + cargo_crates_v1::{CratesToml, CratesTomlParseError}, + crate_info::CrateInfo, + helpers::create_if_not_exist, + CompactString, Version, +}; + +#[derive(Debug, Diagnostic, ThisError)] +#[non_exhaustive] +pub enum ManifestsError { + #[error("failed to parse binstall crates-v1 manifest: {0}")] + #[diagnostic(transparent)] + BinstallCratesV1(#[from] BinstallCratesV1Error), + + #[error("failed to parse cargo v1 manifest: {0}")] + #[diagnostic(transparent)] + CargoManifestV1(#[from] CratesTomlParseError), + + #[error("I/O error: {0}")] + Io(#[from] io::Error), +} + +pub struct Manifests { + binstall: BinstallCratesV1Records, + cargo_crates_v1: FileLock, + installed_crates: BTreeMap, +} + +impl Manifests { + pub fn open_exclusive(cargo_roots: &Path) -> Result { + // Read cargo_binstall_metadata + let metadata_path = cargo_roots.join("binstall/crates-v1.json"); + fs::create_dir_all(metadata_path.parent().unwrap())?; + + let mut binstall = BinstallCratesV1Records::load_from_path(&metadata_path)?; + + // Read cargo_install_v1_metadata + let manifest_path = cargo_roots.join(".crates.toml"); + + let mut cargo_crates_v1 = create_if_not_exist(&manifest_path)?; + + let installed_crates = CratesToml::load_from_reader(&mut cargo_crates_v1) + .and_then(CratesToml::collect_into_crates_versions)?; + + binstall.retain(|crate_info| installed_crates.contains_key(&crate_info.name)); + + Ok(Self { + binstall, + cargo_crates_v1, + installed_crates, + }) + } + + fn rewind_cargo_crates_v1(&mut self) -> Result<(), ManifestsError> { + self.cargo_crates_v1.rewind().map_err(ManifestsError::from) + } + + /// `cargo-uninstall` can be called to uninstall crates, + /// but it only updates .crates.toml. + /// + /// So here we will honour .crates.toml only. + pub fn installed_crates(&self) -> &BTreeMap { + &self.installed_crates + } + + pub fn update(mut self, metadata_vec: Vec) -> Result<(), ManifestsError> { + self.rewind_cargo_crates_v1()?; + + CratesToml::append_to_file(&mut self.cargo_crates_v1, &metadata_vec)?; + + for metadata in metadata_vec { + self.binstall.replace(metadata); + } + self.binstall.overwrite()?; + + Ok(()) + } +} diff --git a/crates/binstalk-manifests/src/helpers.rs b/crates/binstalk-manifests/src/helpers.rs new file mode 100644 index 00000000..45d55fb8 --- /dev/null +++ b/crates/binstalk-manifests/src/helpers.rs @@ -0,0 +1,15 @@ +use std::{fs, io, path::Path}; + +use fs_lock::FileLock; + +/// Return exclusively locked file that is readable and writable. +pub(crate) fn create_if_not_exist(path: &Path) -> io::Result { + fs::File::options() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(path) + .and_then(FileLock::new_exclusive) + .map(|file_lock| file_lock.set_file_path(path)) +} diff --git a/crates/binstalk-manifests/src/lib.rs b/crates/binstalk-manifests/src/lib.rs new file mode 100644 index 00000000..c61b5181 --- /dev/null +++ b/crates/binstalk-manifests/src/lib.rs @@ -0,0 +1,22 @@ +//! Manifest formats and utilities. +//! +//! There are three types of manifests Binstall may deal with: +//! - manifests that define how to fetch and install a package +//! ([Cargo.toml's `[metadata.binstall]`][cargo_toml_binstall]); +//! - manifests that record which packages _are_ installed +//! ([Cargo's `.crates.toml`][cargo_crates_v1] and +//! [Binstall's `.crates-v1.json`][binstall_crates_v1]); +//! - manifests that specify which packages _to_ install (currently none). + +mod helpers; + +pub mod binstall_crates_v1; +pub mod cargo_config; +pub mod cargo_crates_v1; +/// Contains both [`binstall_crates_v1`] and [`cargo_crates_v1`]. +pub mod crates_manifests; + +pub use binstalk_types::{cargo_toml_binstall, crate_info}; +pub use compact_str::CompactString; +pub use semver::Version; +pub use url::Url; diff --git a/crates/binstalk-registry/CHANGELOG.md b/crates/binstalk-registry/CHANGELOG.md new file mode 100644 index 00000000..1d869d8c --- /dev/null +++ b/crates/binstalk-registry/CHANGELOG.md @@ -0,0 +1,138 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.11.21](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.20...binstalk-registry-v0.11.21) - 2025-06-06 + +### Fixed + +- fix updating of installed crates manifest on custom sparse registry ([#2178](https://github.com/cargo-bins/cargo-binstall/pull/2178)) + +## [0.11.20](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.19...binstalk-registry-v0.11.20) - 2025-05-30 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.11.19](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.18...binstalk-registry-v0.11.19) - 2025-05-16 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.11.18](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.17...binstalk-registry-v0.11.18) - 2025-04-05 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.11.17](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.16...binstalk-registry-v0.11.17) - 2025-03-19 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.11.16](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.15...binstalk-registry-v0.11.16) - 2025-03-15 + +### Other + +- *(deps)* bump the deps group with 2 updates ([#2084](https://github.com/cargo-bins/cargo-binstall/pull/2084)) +- *(deps)* bump tokio from 1.43.0 to 1.44.0 in the deps group ([#2079](https://github.com/cargo-bins/cargo-binstall/pull/2079)) + +## [0.11.15](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.14...binstalk-registry-v0.11.15) - 2025-03-07 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#2072](https://github.com/cargo-bins/cargo-binstall/pull/2072)) + +## [0.11.14](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.13...binstalk-registry-v0.11.14) - 2025-02-28 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.11.13](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.12...binstalk-registry-v0.11.13) - 2025-02-11 + +### Other + +- updated the following local packages: binstalk-types, binstalk-downloader, binstalk-downloader + +## [0.11.12](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.11...binstalk-registry-v0.11.12) - 2025-02-04 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.11.11](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.10...binstalk-registry-v0.11.11) - 2025-01-19 + +### Other + +- update Cargo.lock dependencies + +## [0.11.10](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.9...binstalk-registry-v0.11.10) - 2025-01-13 + +### Other + +- update Cargo.lock dependencies + +## [0.11.9](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.8...binstalk-registry-v0.11.9) - 2025-01-11 + +### Other + +- *(deps)* bump the deps group with 3 updates (#2015) + +## [0.11.8](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.7...binstalk-registry-v0.11.8) - 2025-01-04 + +### Other + +- *(deps)* bump the deps group with 2 updates (#2010) + +## [0.11.7](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.6...binstalk-registry-v0.11.7) - 2024-12-14 + +### Other + +- *(deps)* bump the deps group with 2 updates (#1997) + +## [0.11.6](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.5...binstalk-registry-v0.11.6) - 2024-12-07 + +### Other + +- updated the following local packages: cargo-toml-workspace + +## [0.11.5](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.4...binstalk-registry-v0.11.5) - 2024-11-23 + +### Other + +- *(deps)* bump the deps group with 2 updates ([#1981](https://github.com/cargo-bins/cargo-binstall/pull/1981)) + +## [0.11.4](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.3...binstalk-registry-v0.11.4) - 2024-11-09 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1966](https://github.com/cargo-bins/cargo-binstall/pull/1966)) + +## [0.11.3](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.2...binstalk-registry-v0.11.3) - 2024-11-05 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1954](https://github.com/cargo-bins/cargo-binstall/pull/1954)) + +## [0.11.2](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.1...binstalk-registry-v0.11.2) - 2024-11-02 + +### Other + +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.11.1](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.11.0...binstalk-registry-v0.11.1) - 2024-08-12 + +### Other +- updated the following local packages: binstalk-downloader, binstalk-downloader + +## [0.11.0](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-registry-v0.10.0...binstalk-registry-v0.11.0) - 2024-08-10 + +### Other +- updated the following local packages: binstalk-types, binstalk-downloader, binstalk-downloader diff --git a/crates/binstalk-registry/Cargo.toml b/crates/binstalk-registry/Cargo.toml new file mode 100644 index 00000000..256030c8 --- /dev/null +++ b/crates/binstalk-registry/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "binstalk-registry" +version = "0.11.21" +edition = "2021" +rust-version = "1.65.0" + +description = "The binstall toolkit for fetching package from arbitrary registry" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/binstalk-registry" +authors = ["Jiahao_XU@outlook "] +license = "Apache-2.0 OR MIT" + +[dependencies] +async-trait = "0.1.88" +base16 = "0.2.1" +binstalk-downloader = { version = "0.13.20", path = "../binstalk-downloader", default-features = false, features = [ + "json", +] } +binstalk-types = { version = "0.10.0", path = "../binstalk-types" } +cargo-toml-workspace = { version = "7.0.6", path = "../cargo-toml-workspace" } +compact_str = { version = "0.9.0", features = ["serde"] } +leon = "3.0.0" +miette = "7.0.0" +normalize-path = { version = "0.2.1", path = "../normalize-path" } +once_cell = "1.18.0" +semver = { version = "1.0.17", features = ["serde"] } +serde = { version = "1.0.163", features = ["derive"] } +serde_json = "1.0.107" +sha2 = "0.10.7" +simple-git = { version = "0.2.4", optional = true } +tempfile = "3.5.0" +thiserror = "2.0.11" +tokio = { version = "1.44.0", features = [ + "rt", + "sync", +], default-features = false } +tracing = "0.1.39" +url = "2.5.4" + +[dev-dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +toml_edit = { version = "0.22.12", features = ["serde"] } +binstalk-downloader = { version = "0.13.20", path = "../binstalk-downloader", default-features = false, features = [ + "rustls", +] } + +[features] +git = ["simple-git"] + +rustls = ["simple-git?/rustls"] +native-tls = ["simple-git?/native-tls"] + +crates_io_api = [] + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +all-features = true diff --git a/crates/binstalk-registry/LICENSE-APACHE b/crates/binstalk-registry/LICENSE-APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/crates/binstalk-registry/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/binstalk-registry/LICENSE-MIT b/crates/binstalk-registry/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/crates/binstalk-registry/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/crates/binstalk-registry/src/common.rs b/crates/binstalk-registry/src/common.rs new file mode 100644 index 00000000..cd426298 --- /dev/null +++ b/crates/binstalk-registry/src/common.rs @@ -0,0 +1,216 @@ +use std::borrow::Cow; + +use base16::{decode as decode_base16, encode_lower as encode_base16}; +use binstalk_downloader::{ + bytes::Bytes, + download::{DataVerifier, Download}, + remote::{Client, Url}, +}; +use binstalk_types::cargo_toml_binstall::{Meta, TarBasedFmt}; +use cargo_toml_workspace::cargo_toml::Manifest; +use compact_str::{format_compact, CompactString, ToCompactString}; +use leon::{Template, Values}; +use semver::{Version, VersionReq}; +use serde::Deserialize; +use serde_json::Error as JsonError; +use sha2::{Digest, Sha256}; +use tracing::{debug, instrument}; + +use crate::{visitor::ManifestVisitor, RegistryError}; + +#[derive(Deserialize)] +pub(super) struct RegistryConfig { + pub(super) dl: CompactString, +} + +struct Sha256Digest { + expected: Vec, + actual: Option>, + state: Option, +} + +impl Sha256Digest { + fn new(checksum: Vec) -> Self { + Self { + expected: checksum, + actual: None, + state: Some(Sha256::new()), + } + } +} + +impl DataVerifier for Sha256Digest { + fn update(&mut self, data: &Bytes) { + if let Some(ref mut state) = &mut self.state { + state.update(data); + } + } + + fn validate(&mut self) -> bool { + if let Some(state) = self.state.take() { + self.actual = Some(state.finalize().to_vec()); + } + + self.actual.as_ref().unwrap() == &self.expected + } +} + +#[instrument( + skip(client, crate_url), + fields( + crate_url = format_args!("{crate_url}"), + ), +)] +pub(super) async fn parse_manifest( + client: Client, + crate_name: &str, + crate_url: Url, + MatchedVersion { version, cksum }: MatchedVersion, +) -> Result, RegistryError> { + debug!("Fetching crate from: {crate_url} and extracting Cargo.toml from it"); + + let mut manifest_visitor = ManifestVisitor::new(format!("{crate_name}-{version}").into()); + + let checksum = decode_base16(cksum.as_bytes()).map_err(RegistryError::from)?; + let mut digest = Sha256Digest::new(checksum); + + Download::new_with_data_verifier(client, crate_url, &mut digest) + .and_visit_tar(TarBasedFmt::Tgz, &mut manifest_visitor) + .await?; + + if !digest.validate() { + Err(RegistryError::UnmatchedChecksum { + expected: encode_base16(digest.expected.as_slice()).into(), + actual: encode_base16(digest.actual.unwrap().as_slice()).into(), + }) + } else { + manifest_visitor.load_manifest() + } +} + +/// Return components of crate prefix +pub(super) fn crate_prefix_components( + crate_name: &str, +) -> Result<(CompactString, Option), RegistryError> { + let mut chars = crate_name.chars(); + + match (chars.next(), chars.next(), chars.next(), chars.next()) { + (None, None, None, None) => Err(RegistryError::NotFound(crate_name.into())), + (Some(_), None, None, None) => Ok((CompactString::const_new("1"), None)), + (Some(_), Some(_), None, None) => Ok((CompactString::const_new("2"), None)), + (Some(ch), Some(_), Some(_), None) => Ok(( + CompactString::const_new("3"), + Some(ch.to_lowercase().to_compact_string()), + )), + (Some(a), Some(b), Some(c), Some(d)) => Ok(( + format_compact!("{}{}", a.to_lowercase(), b.to_lowercase()), + Some(format_compact!("{}{}", c.to_lowercase(), d.to_lowercase())), + )), + _ => unreachable!(), + } +} + +pub(super) fn render_dl_template( + dl_template: &str, + crate_name: &str, + (c1, c2): &(CompactString, Option), + MatchedVersion { version, cksum }: &MatchedVersion, +) -> Result { + let template = Template::parse(dl_template)?; + if template.keys().next().is_some() { + let mut crate_prefix = c1.clone(); + if let Some(c2) = c2 { + crate_prefix.push('/'); + crate_prefix.push_str(c2); + } + + struct Context<'a> { + crate_name: &'a str, + crate_prefix: CompactString, + crate_lowerprefix: CompactString, + version: &'a str, + cksum: &'a str, + } + impl Values for Context<'_> { + fn get_value(&self, key: &str) -> Option> { + match key { + "crate" => Some(Cow::Borrowed(self.crate_name)), + "version" => Some(Cow::Borrowed(self.version)), + "prefix" => Some(Cow::Borrowed(&self.crate_prefix)), + "lowerprefix" => Some(Cow::Borrowed(&self.crate_lowerprefix)), + "sha256-checksum" => Some(Cow::Borrowed(self.cksum)), + _ => None, + } + } + } + Ok(template.render(&Context { + crate_name, + crate_lowerprefix: crate_prefix.to_lowercase(), + crate_prefix, + version, + cksum, + })?) + } else { + Ok(format!("{dl_template}/{crate_name}/{version}/download")) + } +} + +#[derive(Deserialize)] +pub(super) struct RegistryIndexEntry { + vers: CompactString, + yanked: bool, + cksum: String, +} + +pub(super) struct MatchedVersion { + pub(super) version: CompactString, + /// sha256 checksum encoded in base16 + pub(super) cksum: String, +} + +impl MatchedVersion { + pub(super) fn find( + it: &mut dyn Iterator>, + version_req: &VersionReq, + ) -> Result { + let mut ret = Option::<(Self, Version)>::None; + + for res in it { + let entry = res.map_err(RegistryError::from)?; + + if entry.yanked { + continue; + } + + let num = entry.vers; + + // Parse out version + let Ok(ver) = Version::parse(&num) else { + continue; + }; + + // Filter by version match + if !version_req.matches(&ver) { + continue; + } + + let matched = Self { + version: num, + cksum: entry.cksum, + }; + + if let Some((_, max_ver)) = &ret { + if ver > *max_ver { + ret = Some((matched, ver)); + } + } else { + ret = Some((matched, ver)); + } + } + + ret.map(|(num, _)| num) + .ok_or_else(|| RegistryError::VersionMismatch { + req: version_req.clone(), + }) + } +} diff --git a/crates/binstalk-registry/src/crates_io_registry.rs b/crates/binstalk-registry/src/crates_io_registry.rs new file mode 100644 index 00000000..b07b6a1a --- /dev/null +++ b/crates/binstalk-registry/src/crates_io_registry.rs @@ -0,0 +1,166 @@ +use binstalk_downloader::remote::{Client, Error as RemoteError, Url}; +use binstalk_types::cargo_toml_binstall::Meta; +use cargo_toml_workspace::cargo_toml::Manifest; +use compact_str::{CompactString, ToCompactString}; +use semver::{Comparator, Op as ComparatorOp, Version as SemVersion, VersionReq}; +use serde::Deserialize; +use tracing::{debug, instrument}; + +use crate::{parse_manifest, MatchedVersion, RegistryError}; + +/// Return `Some(checksum)` if the version is not yanked, otherwise `None`. +async fn is_crate_yanked(client: &Client, url: Url) -> Result, RemoteError> { + #[derive(Deserialize)] + struct CrateInfo { + version: Inner, + } + + #[derive(Deserialize)] + struct Inner { + yanked: bool, + checksum: String, + } + + // Fetch / update index + debug!("Looking up crate information"); + + let info: CrateInfo = client.get(url).send(true).await?.json().await?; + let version = info.version; + + Ok((!version.yanked).then_some(version.checksum)) +} + +async fn fetch_crate_cratesio_version_matched( + client: &Client, + url: Url, + version_req: &VersionReq, +) -> Result, RemoteError> { + #[derive(Deserialize)] + struct CrateInfo { + #[serde(rename = "crate")] + inner: CrateInfoInner, + + versions: Vec, + } + + #[derive(Deserialize)] + struct CrateInfoInner { + max_stable_version: CompactString, + } + + #[derive(Deserialize)] + struct Version { + num: CompactString, + yanked: bool, + checksum: String, + } + + // Fetch / update index + debug!("Looking up crate information"); + + let crate_info: CrateInfo = client.get(url).send(true).await?.json().await?; + + let version_with_checksum = if version_req == &VersionReq::STAR { + let version = crate_info.inner.max_stable_version; + crate_info + .versions + .into_iter() + .find_map(|v| (v.num.as_str() == version.as_str()).then_some(v.checksum)) + .map(|checksum| (version, checksum)) + } else { + crate_info + .versions + .into_iter() + .filter_map(|item| { + if !item.yanked { + // Remove leading `v` for git tags + let num = if let Some(num) = item.num.strip_prefix('v') { + num.into() + } else { + item.num + }; + + // Parse out version + let ver = semver::Version::parse(&num).ok()?; + + // Filter by version match + version_req + .matches(&ver) + .then_some((num, ver, item.checksum)) + } else { + None + } + }) + // Return highest version + .max_by( + |(_ver_str_x, ver_x, _checksum_x), (_ver_str_y, ver_y, _checksum_y)| { + ver_x.cmp(ver_y) + }, + ) + .map(|(ver_str, _, checksum)| (ver_str, checksum)) + }; + + Ok(version_with_checksum) +} + +/// Find the crate by name, get its latest stable version matches `version_req`, +/// retrieve its Cargo.toml and infer all its bins. +#[instrument( + skip(client), + fields( + version_req = format_args!("{version_req}"), + ) +)] +pub async fn fetch_crate_cratesio_api( + client: Client, + name: &str, + version_req: &VersionReq, +) -> Result, RegistryError> { + let url = Url::parse(&format!("https://crates.io/api/v1/crates/{name}"))?; + + let (version, cksum) = match version_req.comparators.as_slice() { + [Comparator { + op: ComparatorOp::Exact, + major, + minor: Some(minor), + patch: Some(patch), + pre, + }] => { + let version = SemVersion { + major: *major, + minor: *minor, + patch: *patch, + pre: pre.clone(), + build: Default::default(), + } + .to_compact_string(); + + let mut url = url.clone(); + url.path_segments_mut().unwrap().push(&version); + + is_crate_yanked(&client, url) + .await + .map(|ret| ret.map(|checksum| (version, checksum))) + } + _ => fetch_crate_cratesio_version_matched(&client, url.clone(), version_req).await, + } + .map_err(|e| match e { + RemoteError::Http(e) if e.is_status() => RegistryError::NotFound(name.into()), + e => e.into(), + })? + .ok_or_else(|| RegistryError::VersionMismatch { + req: version_req.clone(), + })?; + + debug!("Found information for crate version: '{version}'"); + + // Download crate to temporary dir (crates.io or git?) + let mut crate_url = url; + crate_url + .path_segments_mut() + .unwrap() + .push(&version) + .push("download"); + + parse_manifest(client, name, crate_url, MatchedVersion { version, cksum }).await +} diff --git a/crates/binstalk-registry/src/git_registry.rs b/crates/binstalk-registry/src/git_registry.rs new file mode 100644 index 00000000..ac0a3b2e --- /dev/null +++ b/crates/binstalk-registry/src/git_registry.rs @@ -0,0 +1,154 @@ +use std::{io, path::PathBuf, sync::Arc}; + +use binstalk_downloader::remote::Client; +use binstalk_types::cargo_toml_binstall::Meta; +use cargo_toml_workspace::cargo_toml::Manifest; +use compact_str::{CompactString, ToCompactString}; +use once_cell::sync::OnceCell; +use semver::VersionReq; +use serde_json::{from_slice as json_from_slice, Deserializer as JsonDeserializer}; +use simple_git::{GitCancellationToken, GitUrl, Repository}; +use tempfile::TempDir; +use tokio::task::spawn_blocking; +use tracing::instrument; +use url::Url; + +use crate::{ + crate_prefix_components, parse_manifest, render_dl_template, MatchedVersion, RegistryConfig, + RegistryError, +}; + +#[derive(Debug)] +struct GitIndex { + _tempdir: TempDir, + repo: Repository, + dl_template: CompactString, +} + +impl GitIndex { + fn new(url: GitUrl, cancellation_token: GitCancellationToken) -> Result { + let tempdir = TempDir::new()?; + + let repo = Repository::shallow_clone_bare( + url.clone(), + tempdir.as_ref(), + Some(cancellation_token), + )?; + + let config: RegistryConfig = { + let config = repo + .get_head_commit_entry_data_by_path("config.json")? + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + format!("config.json not found in repository `{url}`"), + ) + })?; + + json_from_slice(&config).map_err(RegistryError::from)? + }; + + Ok(Self { + _tempdir: tempdir, + repo, + dl_template: config.dl, + }) + } +} + +#[derive(Debug)] +struct GitRegistryInner { + url: GitUrl, + git_index: OnceCell, +} + +#[derive(Clone, Debug)] +pub struct GitRegistry(Arc); + +impl GitRegistry { + pub fn new(url: GitUrl) -> Self { + Self(Arc::new(GitRegistryInner { + url, + git_index: Default::default(), + })) + } + + pub fn url(&self) -> &GitUrl { + &self.0.url + } + + /// WARNING: This is a blocking operation. + fn find_crate_matched_ver( + repo: &Repository, + crate_name: &str, + (c1, c2): &(CompactString, Option), + version_req: &VersionReq, + ) -> Result { + let mut path = PathBuf::with_capacity(128); + path.push(&**c1); + if let Some(c2) = c2 { + path.push(&**c2); + } + + path.push(&*crate_name.to_lowercase()); + let crate_versions = repo + .get_head_commit_entry_data_by_path(path)? + .ok_or_else(|| RegistryError::NotFound(crate_name.into()))?; + + MatchedVersion::find( + &mut JsonDeserializer::from_slice(&crate_versions).into_iter(), + version_req, + ) + } + + #[instrument( + skip(self, client, version_req), + fields( + version_req = format_args!("{version_req}"), + ), + )] + pub async fn fetch_crate_matched( + &self, + client: Client, + name: &str, + version_req: &VersionReq, + ) -> Result, RegistryError> { + let crate_prefix = crate_prefix_components(name)?; + let crate_name = name.to_compact_string(); + let version_req = version_req.clone(); + let this = self.clone(); + + let cancellation_token = GitCancellationToken::default(); + // Cancel git operation if the future is cancelled (dropped). + let cancel_on_drop = cancellation_token.clone().cancel_on_drop(); + + let (matched_version, dl_url) = spawn_blocking(move || { + let GitIndex { + _tempdir: _, + repo, + dl_template, + } = this + .0 + .git_index + .get_or_try_init(|| GitIndex::new(this.0.url.clone(), cancellation_token))?; + + let matched_version = + Self::find_crate_matched_ver(repo, &crate_name, &crate_prefix, &version_req)?; + + let url = Url::parse(&render_dl_template( + dl_template, + &crate_name, + &crate_prefix, + &matched_version, + )?)?; + + Ok::<_, RegistryError>((matched_version, url)) + }) + .await??; + + // Git operation done, disarm it + cancel_on_drop.disarm(); + + parse_manifest(client, name, dl_url, matched_version).await + } +} diff --git a/crates/binstalk-registry/src/lib.rs b/crates/binstalk-registry/src/lib.rs new file mode 100644 index 00000000..5c7dddd6 --- /dev/null +++ b/crates/binstalk-registry/src/lib.rs @@ -0,0 +1,360 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +use std::{fmt, io, str::FromStr, sync::Arc}; + +use base16::DecodeError as Base16DecodeError; +use binstalk_downloader::{ + download::DownloadError, + remote::{Client, Error as RemoteError}, +}; +use binstalk_types::{ + cargo_toml_binstall::Meta, + crate_info::{CrateSource, SourceType}, + maybe_owned::MaybeOwned, +}; +use cargo_toml_workspace::cargo_toml::{Error as CargoTomlError, Manifest}; +use compact_str::CompactString; +use leon::{ParseError, RenderError}; +use miette::Diagnostic; +use semver::VersionReq; +use serde_json::Error as JsonError; +use thiserror::Error as ThisError; +use tokio::task; +use url::{ParseError as UrlParseError, Url}; + +#[cfg(feature = "git")] +pub use simple_git::{GitError, GitUrl, GitUrlParseError}; + +mod vfs; + +mod visitor; + +mod common; +use common::*; + +#[cfg(feature = "git")] +mod git_registry; +#[cfg(feature = "git")] +pub use git_registry::GitRegistry; + +#[cfg(any(feature = "crates_io_api", test))] +mod crates_io_registry; +#[cfg(any(feature = "crates_io_api", test))] +pub use crates_io_registry::fetch_crate_cratesio_api; + +mod sparse_registry; +pub use sparse_registry::SparseRegistry; + +#[derive(Debug, ThisError, Diagnostic)] +#[diagnostic(severity(error), code(binstall::cargo_registry))] +#[non_exhaustive] +pub enum RegistryError { + #[error(transparent)] + Remote(#[from] RemoteError), + + #[error("{0} is not found")] + #[diagnostic( + help("Check that the crate name you provided is correct.\nYou can also search for a matching crate at: https://lib.rs/search?q={0}") + )] + NotFound(CompactString), + + #[error(transparent)] + Json(#[from] JsonError), + + #[error("Failed to parse dl config: {0}")] + ParseDlConfig(#[from] ParseError), + + #[error("Failed to render dl config: {0}")] + RenderDlConfig(#[from] RenderError), + + #[error("Failed to parse checksum encoded in hex: {0}")] + InvalidHex(#[from] Base16DecodeError), + + #[error("Expected checksum `{expected}`, actual checksum `{actual}`")] + UnmatchedChecksum { + expected: Box, + actual: Box, + }, + + #[error("no version matching requirement '{req}'")] + VersionMismatch { req: semver::VersionReq }, + + #[error("Failed to parse cargo manifest: {0}")] + #[diagnostic(help("If you used --manifest-path, check the Cargo.toml syntax."))] + CargoManifest(#[from] Box), + + #[error("Failed to parse url: {0}")] + UrlParse(#[from] UrlParseError), + + #[error(transparent)] + Download(#[from] DownloadError), + + #[error("I/O Error: {0}")] + Io(#[from] io::Error), + + #[error(transparent)] + TaskJoinError(#[from] task::JoinError), + + #[cfg(feature = "git")] + #[error("Failed to shallow clone git repository: {0}")] + GitError(#[from] GitError), +} + +impl From for RegistryError { + fn from(e: CargoTomlError) -> Self { + Self::from(Box::new(e)) + } +} + +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum Registry { + Sparse(Arc), + + #[cfg(feature = "git")] + Git(GitRegistry), +} + +impl Default for Registry { + fn default() -> Self { + Self::crates_io_sparse_registry() + } +} + +#[derive(Debug, ThisError)] +#[error("Invalid registry `{src}`, {inner}")] +pub struct InvalidRegistryError { + src: CompactString, + #[source] + inner: InvalidRegistryErrorInner, +} + +#[derive(Debug, ThisError)] +enum InvalidRegistryErrorInner { + #[cfg(feature = "git")] + #[error("failed to parse git url {0}")] + GitUrlParseErr(#[from] Box), + + #[error("failed to parse sparse registry url: {0}")] + UrlParseErr(#[from] UrlParseError), + + #[error("expected protocol http(s), actual url `{0}`")] + InvalidScheme(Box), + + #[cfg(not(feature = "git"))] + #[error("git registry not supported")] + GitRegistryNotSupported, +} + +impl Registry { + /// Return a crates.io sparse registry + pub fn crates_io_sparse_registry() -> Self { + Self::Sparse(Arc::new(SparseRegistry::new( + Url::parse("https://index.crates.io/").unwrap(), + ))) + } + + fn from_str_inner(s: &str) -> Result { + if let Some(s) = s.strip_prefix("sparse+") { + let url = Url::parse(s.trim_end_matches('/'))?; + + let scheme = url.scheme(); + if scheme != "http" && scheme != "https" { + Err(InvalidRegistryErrorInner::InvalidScheme(Box::new(url))) + } else { + Ok(Self::Sparse(Arc::new(SparseRegistry::new(url)))) + } + } else { + #[cfg(not(feature = "git"))] + { + Err(InvalidRegistryErrorInner::GitRegistryNotSupported) + } + #[cfg(feature = "git")] + { + let url = GitUrl::from_str(s).map_err(Box::new)?; + Ok(Self::Git(GitRegistry::new(url))) + } + } + } + + /// Fetch the latest crate with `crate_name` and with version matching + /// `version_req`. + pub async fn fetch_crate_matched( + &self, + client: Client, + crate_name: &str, + version_req: &VersionReq, + ) -> Result, RegistryError> { + match self { + Self::Sparse(sparse_registry) => { + sparse_registry + .fetch_crate_matched(client, crate_name, version_req) + .await + } + #[cfg(feature = "git")] + Self::Git(git_registry) => { + git_registry + .fetch_crate_matched(client, crate_name, version_req) + .await + } + } + } + + /// Get url of the regsitry + pub fn url(&self) -> Result, UrlParseError> { + match self { + #[cfg(feature = "git")] + Registry::Git(registry) => { + Url::parse(®istry.url().to_string()).map(MaybeOwned::Owned) + } + Registry::Sparse(registry) => Ok(MaybeOwned::Borrowed(registry.url())), + } + } + + /// Get crate source of this registry + pub fn crate_source(&self) -> Result { + let registry = self.url()?; + let source_type = match self { + #[cfg(feature = "git")] + Registry::Git(_) => SourceType::Git, + Registry::Sparse(_) => SourceType::Sparse, + }; + + Ok(match (registry.as_str(), source_type) { + ("https://index.crates.io/", SourceType::Sparse) + | ("https://github.com/rust-lang/crates.io-index", SourceType::Git) => { + CrateSource::cratesio_registry() + } + _ => CrateSource { + source_type, + url: MaybeOwned::Owned(registry.into_owned()), + }, + }) + } +} + +impl fmt::Display for Registry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + #[cfg(feature = "git")] + Registry::Git(registry) => fmt::Display::fmt(®istry.url(), f), + Registry::Sparse(registry) => fmt::Display::fmt(®istry.url(), f), + } + } +} + +impl FromStr for Registry { + type Err = InvalidRegistryError; + + fn from_str(s: &str) -> Result { + Self::from_str_inner(s).map_err(|inner| InvalidRegistryError { + src: s.into(), + inner, + }) + } +} + +#[cfg(test)] +mod test { + use std::num::NonZeroU16; + + use toml_edit::ser::to_string; + + use super::*; + + /// Mark this as an async fn so that you won't accidentally use it in + /// sync context. + fn create_client() -> Client { + Client::new( + concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")), + None, + NonZeroU16::new(10).unwrap(), + 1.try_into().unwrap(), + [], + ) + .unwrap() + } + + #[tokio::test] + async fn test_crates_io_sparse_registry() { + let client = create_client(); + + let crate_name = "cargo-binstall"; + let version_req = &VersionReq::parse("=1.0.0").unwrap(); + + let serialized_manifest_from_sparse_task = tokio::spawn({ + let client = client.clone(); + let version_req = version_req.clone(); + + async move { + let sparse_registry: Registry = Registry::crates_io_sparse_registry(); + assert!( + matches!(sparse_registry, Registry::Sparse(_)), + "{:?}", + sparse_registry + ); + + let manifest_from_sparse = sparse_registry + .fetch_crate_matched(client, crate_name, &version_req) + .await + .unwrap(); + + to_string(&manifest_from_sparse).unwrap() + } + }); + + let manifest_from_cratesio_api = fetch_crate_cratesio_api(client, crate_name, version_req) + .await + .unwrap(); + + let serialized_manifest_from_cratesio_api = to_string(&manifest_from_cratesio_api).unwrap(); + + assert_eq!( + serialized_manifest_from_sparse_task.await.unwrap(), + serialized_manifest_from_cratesio_api + ); + } + + #[cfg(feature = "git")] + #[tokio::test] + async fn test_crates_io_git_registry() { + let client = create_client(); + + let crate_name = "cargo-binstall"; + let version_req = &VersionReq::parse("=1.0.0").unwrap(); + + let serialized_manifest_from_git_task = tokio::spawn({ + let version_req = version_req.clone(); + let client = client.clone(); + + async move { + let git_registry: Registry = "https://github.com/rust-lang/crates.io-index" + .parse() + .unwrap(); + assert!( + matches!(git_registry, Registry::Git(_)), + "{:?}", + git_registry + ); + + let manifest_from_git = git_registry + .fetch_crate_matched(client, crate_name, &version_req) + .await + .unwrap(); + to_string(&manifest_from_git).unwrap() + } + }); + + let manifest_from_cratesio_api = Registry::default() + .fetch_crate_matched(client, crate_name, version_req) + .await + .unwrap(); + + let serialized_manifest_from_cratesio_api = to_string(&manifest_from_cratesio_api).unwrap(); + + assert_eq!( + serialized_manifest_from_git_task.await.unwrap(), + serialized_manifest_from_cratesio_api + ); + } +} diff --git a/crates/binstalk-registry/src/sparse_registry.rs b/crates/binstalk-registry/src/sparse_registry.rs new file mode 100644 index 00000000..7d65f180 --- /dev/null +++ b/crates/binstalk-registry/src/sparse_registry.rs @@ -0,0 +1,117 @@ +use binstalk_downloader::remote::{Client, Error as RemoteError}; +use binstalk_types::cargo_toml_binstall::Meta; +use cargo_toml_workspace::cargo_toml::Manifest; +use compact_str::CompactString; +use semver::VersionReq; +use serde_json::Deserializer as JsonDeserializer; +use tokio::sync::OnceCell; +use tracing::instrument; +use url::Url; + +use crate::{ + crate_prefix_components, parse_manifest, render_dl_template, MatchedVersion, RegistryConfig, + RegistryError, +}; + +#[derive(Debug)] +pub struct SparseRegistry { + url: Url, + dl_template: OnceCell, +} + +impl SparseRegistry { + /// * `url` - `url.cannot_be_a_base()` must be `false` + pub fn new(url: Url) -> Self { + Self { + url, + dl_template: Default::default(), + } + } + + pub fn url(&self) -> &Url { + &self.url + } + + async fn get_dl_template(&self, client: &Client) -> Result<&str, RegistryError> { + self.dl_template + .get_or_try_init(|| { + Box::pin(async { + let mut url = self.url.clone(); + url.path_segments_mut().unwrap().push("config.json"); + let config: RegistryConfig = client.get(url).send(true).await?.json().await?; + Ok(config.dl) + }) + }) + .await + .map(AsRef::as_ref) + } + + /// `url` must be a valid http(s) url. + async fn find_crate_matched_ver( + client: &Client, + mut url: Url, + crate_name: &str, + (c1, c2): &(CompactString, Option), + version_req: &VersionReq, + ) -> Result { + { + let mut path = url.path_segments_mut().unwrap(); + + path.push(c1); + if let Some(c2) = c2 { + path.push(c2); + } + + path.push(&crate_name.to_lowercase()); + } + + let body = client + .get(url) + .send(true) + .await + .map_err(|e| match e { + RemoteError::Http(e) if e.is_status() => RegistryError::NotFound(crate_name.into()), + e => e.into(), + })? + .bytes() + .await + .map_err(RegistryError::from)?; + MatchedVersion::find( + &mut JsonDeserializer::from_slice(&body).into_iter(), + version_req, + ) + } + + #[instrument( + skip(self, client, version_req), + fields( + registry_url = format_args!("{}", self.url), + version_req = format_args!("{version_req}"), + ) + )] + pub async fn fetch_crate_matched( + &self, + client: Client, + crate_name: &str, + version_req: &VersionReq, + ) -> Result, RegistryError> { + let crate_prefix = crate_prefix_components(crate_name)?; + let dl_template = self.get_dl_template(&client).await?; + let matched_version = Self::find_crate_matched_ver( + &client, + self.url.clone(), + crate_name, + &crate_prefix, + version_req, + ) + .await?; + let dl_url = Url::parse(&render_dl_template( + dl_template, + crate_name, + &crate_prefix, + &matched_version, + )?)?; + + parse_manifest(client, crate_name, dl_url, matched_version).await + } +} diff --git a/crates/binstalk-registry/src/vfs.rs b/crates/binstalk-registry/src/vfs.rs new file mode 100644 index 00000000..7779b431 --- /dev/null +++ b/crates/binstalk-registry/src/vfs.rs @@ -0,0 +1,43 @@ +use std::{ + collections::{hash_set::HashSet, BTreeMap}, + io, + path::Path, +}; + +use cargo_toml_workspace::cargo_toml::AbstractFilesystem; +use normalize_path::NormalizePath; + +/// This type stores the filesystem structure for the crate tarball +/// extracted in memory and can be passed to +/// `cargo_toml::Manifest::complete_from_abstract_filesystem`. +#[derive(Debug, Default)] +pub(super) struct Vfs(BTreeMap, HashSet>>); + +impl Vfs { + /// * `path` - must be canonical, must not be empty. + pub(super) fn add_path(&mut self, mut path: &Path) { + while let Some(parent) = path.parent() { + // Since path has parent, it must have a filename + let filename = path.file_name().unwrap(); + + // `cargo_toml`'s implementation does the same thing. + // https://docs.rs/cargo_toml/0.11.5/src/cargo_toml/afs.rs.html#24 + let filename = filename.to_string_lossy(); + + self.0 + .entry(parent.into()) + .or_insert_with(|| HashSet::with_capacity(4)) + .insert(filename.into()); + + path = parent; + } + } +} + +impl AbstractFilesystem for Vfs { + fn file_names_in(&self, rel_path: &str) -> io::Result>> { + let rel_path = Path::new(rel_path).normalize(); + + Ok(self.0.get(&*rel_path).cloned().unwrap_or_default()) + } +} diff --git a/crates/binstalk-registry/src/visitor.rs b/crates/binstalk-registry/src/visitor.rs new file mode 100644 index 00000000..e7ecaacd --- /dev/null +++ b/crates/binstalk-registry/src/visitor.rs @@ -0,0 +1,81 @@ +use std::path::{Path, PathBuf}; + +use binstalk_downloader::download::{DownloadError, TarEntriesVisitor, TarEntry}; +use binstalk_types::cargo_toml_binstall::Meta; +use cargo_toml_workspace::cargo_toml::{Manifest, Value}; +use normalize_path::NormalizePath; +use tokio::io::AsyncReadExt; +use tracing::debug; + +use crate::{vfs::Vfs, RegistryError}; + +#[derive(Debug)] +pub(super) struct ManifestVisitor { + cargo_toml_content: Vec, + /// manifest_dir_path is treated as the current dir. + manifest_dir_path: PathBuf, + + vfs: Vfs, +} + +impl ManifestVisitor { + pub(super) fn new(manifest_dir_path: PathBuf) -> Self { + Self { + // Cargo.toml is quite large usually. + cargo_toml_content: Vec::with_capacity(2000), + manifest_dir_path, + vfs: Vfs::default(), + } + } +} + +#[async_trait::async_trait] +impl TarEntriesVisitor for ManifestVisitor { + async fn visit(&mut self, entry: &mut dyn TarEntry) -> Result<(), DownloadError> { + let path = entry.path()?; + let path = path.normalize(); + + let path = if let Ok(path) = path.strip_prefix(&self.manifest_dir_path) { + path + } else { + // The path is outside of the curr dir (manifest dir), + // ignore it. + return Ok(()); + }; + + if path == Path::new("Cargo.toml") + || path == Path::new("src/main.rs") + || path.starts_with("src/bin") + { + self.vfs.add_path(path); + } + + if path == Path::new("Cargo.toml") { + // Since it is possible for the same Cargo.toml to appear + // multiple times using `tar --keep-old-files`, here we + // clear the buffer first before reading into it. + self.cargo_toml_content.clear(); + self.cargo_toml_content + .reserve_exact(entry.size()?.try_into().unwrap_or(usize::MAX)); + entry.read_to_end(&mut self.cargo_toml_content).await?; + } + + Ok(()) + } +} + +impl ManifestVisitor { + /// Load binstall metadata using the extracted information stored in memory. + pub(super) fn load_manifest(self) -> Result, RegistryError> { + debug!("Loading manifest directly from extracted file"); + + // Load and parse manifest + let mut manifest = Manifest::from_slice_with_metadata(&self.cargo_toml_content)?; + debug!("Manifest: {manifest:?}"); + // Checks vfs for binary output names + manifest.complete_from_abstract_filesystem::(&self.vfs, None)?; + + // Return metadata + Ok(manifest) + } +} diff --git a/crates/binstalk-types/CHANGELOG.md b/crates/binstalk-types/CHANGELOG.md new file mode 100644 index 00000000..9c32637c --- /dev/null +++ b/crates/binstalk-types/CHANGELOG.md @@ -0,0 +1,42 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.10.0](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-types-v0.9.4...binstalk-types-v0.10.0) - 2025-06-06 + +### Fixed + +- fix updating of installed crates manifest on custom sparse registry ([#2178](https://github.com/cargo-bins/cargo-binstall/pull/2178)) + +## [0.9.4](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-types-v0.9.3...binstalk-types-v0.9.4) - 2025-03-07 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#2072](https://github.com/cargo-bins/cargo-binstall/pull/2072)) + +## [0.9.3](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-types-v0.9.2...binstalk-types-v0.9.3) - 2025-02-11 + +### Other + +- *(deps)* bump the deps group with 2 updates (#2044) + +## [0.9.2](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-types-v0.9.1...binstalk-types-v0.9.2) - 2024-11-23 + +### Other + +- *(deps)* bump the deps group with 2 updates ([#1981](https://github.com/cargo-bins/cargo-binstall/pull/1981)) + +## [0.9.1](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-types-v0.9.0...binstalk-types-v0.9.1) - 2024-11-05 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1954](https://github.com/cargo-bins/cargo-binstall/pull/1954)) + +## [0.9.0](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-types-v0.8.0...binstalk-types-v0.9.0) - 2024-08-10 + +### Added +- Merge disable strategies ([#1868](https://github.com/cargo-bins/cargo-binstall/pull/1868)) diff --git a/crates/binstalk-types/Cargo.toml b/crates/binstalk-types/Cargo.toml new file mode 100644 index 00000000..158afde2 --- /dev/null +++ b/crates/binstalk-types/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "binstalk-types" +description = "The binstall toolkit that contains basic types for binstalk crates" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/binstalk-types" +version = "0.10.0" +rust-version = "1.61.0" +authors = ["ryan "] +edition = "2021" +license = "Apache-2.0 OR MIT" + +[dependencies] +compact_str = { version = "0.9.0", features = ["serde"] } +maybe-owned = { version = "0.3.4", features = ["serde"] } +once_cell = "1.18.0" +semver = { version = "1.0.17", features = ["serde"] } +serde = { version = "1.0.163", features = ["derive"] } +strum = "0.27.0" +strum_macros = "0.27.0" +url = { version = "2.5.4", features = ["serde"] } + +[dev-dependencies] +serde_json = "1" diff --git a/crates/binstalk-types/LICENSE-APACHE b/crates/binstalk-types/LICENSE-APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/crates/binstalk-types/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/binstalk-types/LICENSE-MIT b/crates/binstalk-types/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/crates/binstalk-types/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/crates/binstalk-types/src/cargo_toml_binstall.rs b/crates/binstalk-types/src/cargo_toml_binstall.rs new file mode 100644 index 00000000..74330e9e --- /dev/null +++ b/crates/binstalk-types/src/cargo_toml_binstall.rs @@ -0,0 +1,228 @@ +//! The format of the `[package.metadata.binstall]` manifest. +//! +//! This manifest defines how a particular binary crate may be installed by Binstall. + +use std::{borrow::Cow, collections::BTreeMap}; + +use serde::{Deserialize, Serialize}; +use strum_macros::{EnumCount, VariantArray}; + +mod package_formats; +#[doc(inline)] +pub use package_formats::*; + +/// `binstall` metadata container +/// +/// Required to nest metadata under `package.metadata.binstall` +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Meta { + pub binstall: Option, +} + +/// Strategies to use for binary discovery +#[derive( + Debug, + Copy, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + EnumCount, + VariantArray, + Deserialize, + Serialize, +)] +#[serde(rename_all = "kebab-case")] +pub enum Strategy { + /// Attempt to download official pre-built artifacts using + /// information provided in `Cargo.toml`. + CrateMetaData, + /// Query third-party QuickInstall for the crates. + QuickInstall, + /// Build the crates from source using `cargo-build`. + Compile, +} + +impl Strategy { + pub const fn to_str(self) -> &'static str { + match self { + Strategy::CrateMetaData => "crate-meta-data", + Strategy::QuickInstall => "quick-install", + Strategy::Compile => "compile", + } + } +} + +/// Metadata for binary installation use. +/// +/// Exposed via `[package.metadata]` in `Cargo.toml` +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", default)] +pub struct PkgMeta { + /// URL template for package downloads + pub pkg_url: Option, + + /// Format for package downloads + pub pkg_fmt: Option, + + /// Path template for binary files in packages + pub bin_dir: Option, + + /// Package signing configuration + pub signing: Option, + + /// Stratgies to disable + pub disabled_strategies: Option>, + + /// Target specific overrides + pub overrides: BTreeMap, +} + +impl PkgMeta { + /// Merge configuration overrides into object + pub fn merge(&mut self, pkg_override: &PkgOverride) { + if let Some(o) = &pkg_override.pkg_url { + self.pkg_url = Some(o.clone()); + } + if let Some(o) = &pkg_override.pkg_fmt { + self.pkg_fmt = Some(*o); + } + if let Some(o) = &pkg_override.bin_dir { + self.bin_dir = Some(o.clone()); + } + } + + /// Merge configuration overrides into object + /// + /// * `pkg_overrides` - ordered in preference + pub fn merge_overrides<'a, It>(&self, pkg_overrides: It) -> Self + where + It: IntoIterator + Clone, + { + let ignore_disabled_strategies = pkg_overrides + .clone() + .into_iter() + .any(|pkg_override| pkg_override.ignore_disabled_strategies); + + Self { + pkg_url: pkg_overrides + .clone() + .into_iter() + .find_map(|pkg_override| pkg_override.pkg_url.clone()) + .or_else(|| self.pkg_url.clone()), + + pkg_fmt: pkg_overrides + .clone() + .into_iter() + .find_map(|pkg_override| pkg_override.pkg_fmt) + .or(self.pkg_fmt), + + bin_dir: pkg_overrides + .clone() + .into_iter() + .find_map(|pkg_override| pkg_override.bin_dir.clone()) + .or_else(|| self.bin_dir.clone()), + + signing: pkg_overrides + .clone() + .into_iter() + .find_map(|pkg_override| pkg_override.signing.clone()) + .or_else(|| self.signing.clone()), + + disabled_strategies: if ignore_disabled_strategies { + None + } else { + let mut disabled_strategies = pkg_overrides + .into_iter() + .filter_map(|pkg_override| pkg_override.disabled_strategies.as_deref()) + .flatten() + .chain(self.disabled_strategies.as_deref().into_iter().flatten()) + .copied() + .collect::>(); + + disabled_strategies.sort_unstable(); + disabled_strategies.dedup(); + + Some(disabled_strategies.into_boxed_slice()) + }, + + overrides: Default::default(), + } + } +} + +/// Target specific overrides for binary installation +/// +/// Exposed via `[package.metadata.TARGET]` in `Cargo.toml` +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", default)] +pub struct PkgOverride { + /// URL template override for package downloads + pub pkg_url: Option, + + /// Format override for package downloads + pub pkg_fmt: Option, + + /// Path template override for binary files in packages + pub bin_dir: Option, + + /// Stratgies to disable + pub disabled_strategies: Option>, + + /// Package signing configuration + pub signing: Option, + + #[serde(skip)] + pub ignore_disabled_strategies: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct BinMeta { + /// Binary name + pub name: String, + + /// Binary template (path within package) + pub path: String, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct PkgSigning { + /// Signing algorithm supported by Binstall. + pub algorithm: SigningAlgorithm, + + /// Signing public key + pub pubkey: Cow<'static, str>, + + /// Signature file override template (url to download) + #[serde(default)] + pub file: Option, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +#[non_exhaustive] +pub enum SigningAlgorithm { + /// [minisign](https://jedisct1.github.io/minisign/) + Minisign, +} + +#[cfg(test)] +mod tests { + use strum::VariantArray; + + use super::*; + + #[test] + fn test_strategy_ser() { + Strategy::VARIANTS.iter().for_each(|strategy| { + assert_eq!( + serde_json::to_string(&strategy).unwrap(), + format!(r#""{}""#, strategy.to_str()) + ) + }); + } +} diff --git a/crates/binstalk-types/src/cargo_toml_binstall/package_formats.rs b/crates/binstalk-types/src/cargo_toml_binstall/package_formats.rs new file mode 100644 index 00000000..8619bddc --- /dev/null +++ b/crates/binstalk-types/src/cargo_toml_binstall/package_formats.rs @@ -0,0 +1,134 @@ +use serde::{Deserialize, Serialize}; +use strum_macros::{Display, EnumIter, EnumString}; + +/// Binary format enumeration +#[derive( + Debug, Display, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, EnumString, EnumIter, +)] +#[serde(rename_all = "snake_case")] +#[strum(ascii_case_insensitive)] +pub enum PkgFmt { + /// Download format is TAR (uncompressed) + Tar, + /// Download format is TAR + Bzip2 + Tbz2, + /// Download format is TGZ (TAR + GZip) + Tgz, + /// Download format is TAR + XZ + Txz, + /// Download format is TAR + Zstd + Tzstd, + /// Download format is Zip + Zip, + /// Download format is raw / binary + Bin, +} + +impl Default for PkgFmt { + fn default() -> Self { + Self::Tgz + } +} + +impl PkgFmt { + /// If self is one of the tar based formats, return Some. + pub fn decompose(self) -> PkgFmtDecomposed { + match self { + PkgFmt::Tar => PkgFmtDecomposed::Tar(TarBasedFmt::Tar), + PkgFmt::Tbz2 => PkgFmtDecomposed::Tar(TarBasedFmt::Tbz2), + PkgFmt::Tgz => PkgFmtDecomposed::Tar(TarBasedFmt::Tgz), + PkgFmt::Txz => PkgFmtDecomposed::Tar(TarBasedFmt::Txz), + PkgFmt::Tzstd => PkgFmtDecomposed::Tar(TarBasedFmt::Tzstd), + PkgFmt::Bin => PkgFmtDecomposed::Bin, + PkgFmt::Zip => PkgFmtDecomposed::Zip, + } + } + + /// List of possible file extensions for the format + /// (with prefix `.`). + /// + /// * `is_windows` - if true and `self == PkgFmt::Bin`, then it will return + /// `.exe` in additional to other bin extension names. + pub fn extensions(self, is_windows: bool) -> &'static [&'static str] { + match self { + PkgFmt::Tar => &[".tar"], + PkgFmt::Tbz2 => &[".tbz2", ".tar.bz2"], + PkgFmt::Tgz => &[".tgz", ".tar.gz"], + PkgFmt::Txz => &[".txz", ".tar.xz"], + PkgFmt::Tzstd => &[".tzstd", ".tzst", ".tar.zst"], + PkgFmt::Bin => { + if is_windows { + &[".bin", "", ".exe"] + } else { + &[".bin", ""] + } + } + PkgFmt::Zip => &[".zip"], + } + } + + /// Given the pkg-url template, guess the possible pkg-fmt. + pub fn guess_pkg_format(pkg_url: &str) -> Option { + let mut it = pkg_url.rsplitn(3, '.'); + + let guess = match it.next()? { + "tar" => Some(PkgFmt::Tar), + + "tbz2" => Some(PkgFmt::Tbz2), + "bz2" if it.next() == Some("tar") => Some(PkgFmt::Tbz2), + + "tgz" => Some(PkgFmt::Tgz), + "gz" if it.next() == Some("tar") => Some(PkgFmt::Tgz), + + "txz" => Some(PkgFmt::Txz), + "xz" if it.next() == Some("tar") => Some(PkgFmt::Txz), + + "tzstd" | "tzst" => Some(PkgFmt::Tzstd), + "zst" if it.next() == Some("tar") => Some(PkgFmt::Tzstd), + + "exe" | "bin" => Some(PkgFmt::Bin), + "zip" => Some(PkgFmt::Zip), + + _ => None, + }; + + if it.next().is_some() { + guess + } else { + None + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum PkgFmtDecomposed { + Tar(TarBasedFmt), + Bin, + Zip, +} + +#[derive(Debug, Display, Copy, Clone, Eq, PartialEq)] +pub enum TarBasedFmt { + /// Download format is TAR (uncompressed) + Tar, + /// Download format is TAR + Bzip2 + Tbz2, + /// Download format is TGZ (TAR + GZip) + Tgz, + /// Download format is TAR + XZ + Txz, + /// Download format is TAR + Zstd + Tzstd, +} + +impl From for PkgFmt { + fn from(fmt: TarBasedFmt) -> Self { + match fmt { + TarBasedFmt::Tar => PkgFmt::Tar, + TarBasedFmt::Tbz2 => PkgFmt::Tbz2, + TarBasedFmt::Tgz => PkgFmt::Tgz, + TarBasedFmt::Txz => PkgFmt::Txz, + TarBasedFmt::Tzstd => PkgFmt::Tzstd, + } + } +} diff --git a/crates/binstalk-types/src/crate_info.rs b/crates/binstalk-types/src/crate_info.rs new file mode 100644 index 00000000..7c8f2b64 --- /dev/null +++ b/crates/binstalk-types/src/crate_info.rs @@ -0,0 +1,84 @@ +//! Common structure for crate information for post-install manifests. + +use std::{borrow, cmp, hash}; + +use compact_str::CompactString; +use maybe_owned::MaybeOwned; +use once_cell::sync::Lazy; +use semver::Version; +use serde::{Deserialize, Serialize}; +use url::Url; + +pub fn cratesio_url() -> &'static Url { + static CRATESIO: Lazy Url> = + Lazy::new(|| Url::parse("https://github.com/rust-lang/crates.io-index").unwrap()); + + &CRATESIO +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CrateInfo { + pub name: CompactString, + pub version_req: CompactString, + pub current_version: Version, + pub source: CrateSource, + pub target: CompactString, + pub bins: Vec, +} + +impl borrow::Borrow for CrateInfo { + fn borrow(&self) -> &str { + &self.name + } +} + +impl PartialEq for CrateInfo { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} +impl Eq for CrateInfo {} + +impl PartialOrd for CrateInfo { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for CrateInfo { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.name.cmp(&other.name) + } +} + +impl hash::Hash for CrateInfo { + fn hash(&self, state: &mut H) + where + H: hash::Hasher, + { + self.name.hash(state) + } +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub enum SourceType { + Git, + Path, + Registry, + Sparse, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CrateSource { + pub source_type: SourceType, + pub url: MaybeOwned<'static, Url>, +} + +impl CrateSource { + pub fn cratesio_registry() -> CrateSource { + Self { + source_type: SourceType::Registry, + url: MaybeOwned::Borrowed(cratesio_url()), + } + } +} diff --git a/crates/binstalk-types/src/lib.rs b/crates/binstalk-types/src/lib.rs new file mode 100644 index 00000000..25096520 --- /dev/null +++ b/crates/binstalk-types/src/lib.rs @@ -0,0 +1,4 @@ +pub mod cargo_toml_binstall; +pub mod crate_info; + +pub use maybe_owned; diff --git a/crates/binstalk/CHANGELOG.md b/crates/binstalk/CHANGELOG.md new file mode 100644 index 00000000..480af6ca --- /dev/null +++ b/crates/binstalk/CHANGELOG.md @@ -0,0 +1,230 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.28.36](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.35...binstalk-v0.28.36) - 2025-06-10 + +### Other + +- Add a `--bin` argument to mirror `cargo install --bin`. ([#2189](https://github.com/cargo-bins/cargo-binstall/pull/2189)) + +## [0.28.35](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.34...binstalk-v0.28.35) - 2025-06-06 + +### Fixed + +- fix updating of installed crates manifest on custom sparse registry ([#2178](https://github.com/cargo-bins/cargo-binstall/pull/2178)) + +## [0.28.34](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.33...binstalk-v0.28.34) - 2025-05-30 + +### Other + +- updated the following local packages: binstalk-downloader, detect-targets, binstalk-git-repo-api, binstalk-fetchers, binstalk-registry + +## [0.28.33](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.32...binstalk-v0.28.33) - 2025-05-16 + +### Other + +- Upgrade transitive dependencies ([#2154](https://github.com/cargo-bins/cargo-binstall/pull/2154)) + +## [0.28.32](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.31...binstalk-v0.28.32) - 2025-05-07 + +### Other + +- updated the following local packages: detect-targets + +## [0.28.31](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.30...binstalk-v0.28.31) - 2025-04-05 + +### Other + +- updated the following local packages: binstalk-downloader, detect-targets, binstalk-git-repo-api, binstalk-fetchers, binstalk-registry + +## [0.28.30](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.29...binstalk-v0.28.30) - 2025-03-19 + +### Other + +- Use zlib-rs for gitoxide and avoid pulling in zlib-ng ([#2099](https://github.com/cargo-bins/cargo-binstall/pull/2099)) + +## [0.28.29](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.28...binstalk-v0.28.29) - 2025-03-15 + +### Other + +- *(deps)* bump tokio from 1.43.0 to 1.44.0 in the deps group ([#2079](https://github.com/cargo-bins/cargo-binstall/pull/2079)) + +## [0.28.28](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.27...binstalk-v0.28.28) - 2025-03-07 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#2072](https://github.com/cargo-bins/cargo-binstall/pull/2072)) + +## [0.28.27](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.26...binstalk-v0.28.27) - 2025-02-28 + +### Other + +- Use flate2/zlib-rs for dev/release build ([#2068](https://github.com/cargo-bins/cargo-binstall/pull/2068)) + +## [0.28.26](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.25...binstalk-v0.28.26) - 2025-02-22 + +### Other + +- updated the following local packages: detect-targets + +## [0.28.25](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.24...binstalk-v0.28.25) - 2025-02-15 + +### Other + +- updated the following local packages: detect-targets + +## [0.28.24](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.23...binstalk-v0.28.24) - 2025-02-11 + +### Other + +- *(deps)* bump the deps group with 2 updates (#2044) + +## [0.28.23](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.22...binstalk-v0.28.23) - 2025-02-04 + +### Other + +- update Cargo.lock dependencies + +## [0.28.22](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.21...binstalk-v0.28.22) - 2025-01-19 + +### Other + +- update Cargo.lock dependencies + +## [0.28.21](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.20...binstalk-v0.28.21) - 2025-01-13 + +### Other + +- update Cargo.lock dependencies + +## [0.28.20](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.19...binstalk-v0.28.20) - 2025-01-11 + +### Other + +- *(deps)* bump the deps group with 3 updates (#2015) + +## [0.28.19](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.18...binstalk-v0.28.19) - 2025-01-04 + +### Other + +- *(deps)* bump the deps group with 2 updates (#2010) + +## [0.28.18](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.17...binstalk-v0.28.18) - 2024-12-28 + +### Other + +- updated the following local packages: detect-targets + +## [0.28.17](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.16...binstalk-v0.28.17) - 2024-12-14 + +### Other + +- *(deps)* bump the deps group with 2 updates (#1997) + +## [0.28.16](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.15...binstalk-v0.28.16) - 2024-12-07 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1993](https://github.com/cargo-bins/cargo-binstall/pull/1993)) + +## [0.28.15](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.14...binstalk-v0.28.15) - 2024-11-29 + +### Other + +- updated the following local packages: binstalk-fetchers, detect-targets + +## [0.28.14](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.13...binstalk-v0.28.14) - 2024-11-23 + +### Other + +- *(deps)* bump the deps group with 2 updates ([#1981](https://github.com/cargo-bins/cargo-binstall/pull/1981)) + +## [0.28.13](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.12...binstalk-v0.28.13) - 2024-11-18 + +### Other + +- updated the following local packages: detect-targets + +## [0.28.12](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.11...binstalk-v0.28.12) - 2024-11-09 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1966](https://github.com/cargo-bins/cargo-binstall/pull/1966)) + +## [0.28.11](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.10...binstalk-v0.28.11) - 2024-11-05 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1954](https://github.com/cargo-bins/cargo-binstall/pull/1954)) + +## [0.28.10](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.9...binstalk-v0.28.10) - 2024-11-02 + +### Other + +- updated the following local packages: binstalk-bins, binstalk-downloader, detect-targets + +## [0.28.9](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.8...binstalk-v0.28.9) - 2024-10-25 + +### Other + +- updated the following local packages: detect-targets + +## [0.28.8](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.7...binstalk-v0.28.8) - 2024-10-12 + +### Other + +- updated the following local packages: binstalk-git-repo-api + +## [0.28.7](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.6...binstalk-v0.28.7) - 2024-10-12 + +### Other + +- updated the following local packages: detect-targets + +## [0.28.6](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.5...binstalk-v0.28.6) - 2024-10-04 + +### Other + +- updated the following local packages: detect-targets + +## [0.28.5](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.4...binstalk-v0.28.5) - 2024-09-22 + +### Other + +- updated the following local packages: detect-targets + +## [0.28.4](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.3...binstalk-v0.28.4) - 2024-09-11 + +### Other + +- report to new stats server (with status) ([#1912](https://github.com/cargo-bins/cargo-binstall/pull/1912)) + +## [0.28.3](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.2...binstalk-v0.28.3) - 2024-09-06 + +### Other +- Send telemetry report to quickinstall if no pre-built is found ([#1905](https://github.com/cargo-bins/cargo-binstall/pull/1905)) + +## [0.28.2](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.1...binstalk-v0.28.2) - 2024-08-25 + +### Other +- updated the following local packages: detect-targets + +## [0.28.1](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.28.0...binstalk-v0.28.1) - 2024-08-12 + +### Other +- updated the following local packages: binstalk-downloader + +## [0.28.0](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.27.1...binstalk-v0.27.2) - 2024-08-10 + +### Other +- updated the following local packages: binstalk-types, binstalk-downloader, detect-targets + +## [0.27.1](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-v0.27.0...binstalk-v0.27.1) - 2024-08-04 + +### Other +- Add `--maximum-resolution-timeout` ([#1862](https://github.com/cargo-bins/cargo-binstall/pull/1862)) diff --git a/crates/binstalk/Cargo.toml b/crates/binstalk/Cargo.toml new file mode 100644 index 00000000..148faf9f --- /dev/null +++ b/crates/binstalk/Cargo.toml @@ -0,0 +1,74 @@ +[package] +name = "binstalk" +description = "The binstall toolkit (library interface)" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/binstalk" +version = "0.28.36" +rust-version = "1.79.0" +authors = ["ryan "] +edition = "2021" +license = "GPL-3.0-only" + +[dependencies] +binstalk-bins = { version = "0.6.14", path = "../binstalk-bins" } +binstalk-downloader = { version = "0.13.20", path = "../binstalk-downloader", default-features = false } +binstalk-git-repo-api = { version = "0.5.22", path = "../binstalk-git-repo-api" } +binstalk-fetchers = { version = "0.10.21", path = "../binstalk-fetchers", features = [ + "quickinstall", +] } +binstalk-registry = { version = "0.11.21", path = "../binstalk-registry" } +binstalk-types = { version = "0.10.0", path = "../binstalk-types" } +cargo-toml-workspace = { version = "7.0.6", path = "../cargo-toml-workspace" } +command-group = { version = "5.0.1", features = ["with-tokio"] } +compact_str = { version = "0.9.0", features = ["serde"] } +detect-targets = { version = "0.1.52", path = "../detect-targets", features = [ + "tracing", +] } +either = "1.11.0" +itertools = "0.14.0" +jobslot = { version = "0.2.11", features = ["tokio"] } +leon = "3.0.0" +maybe-owned = "0.3.4" +miette = "7.0.0" +semver = { version = "1.0.17", features = ["serde"] } +simple-git = { version = "0.2.18", optional = true } +strum = "0.27.0" +target-lexicon = { version = "0.13.0", features = ["std"] } +tempfile = "3.5.0" +thiserror = "2.0.11" +tokio = { version = "1.44.0", features = [ + "rt", + "process", + "sync", + "time", +], default-features = false } +tracing = "0.1.39" +url = { version = "2.5.4", features = ["serde"] } +zeroize = "1.8.1" + +[features] +default = ["static", "rustls", "git"] + +git = ["binstalk-registry/git", "simple-git"] +git-max-perf = ["git", "simple-git/git-max-perf-safe", "zlib-rs"] + +static = ["binstalk-downloader/static"] +pkg-config = ["binstalk-downloader/pkg-config"] + +zlib-ng = ["binstalk-downloader/zlib-ng"] +zlib-rs = ["binstalk-downloader/zlib-rs"] + +rustls = ["binstalk-downloader/rustls", "binstalk-registry/rustls"] +native-tls = ["binstalk-downloader/native-tls", "binstalk-registry/native-tls"] + +trust-dns = ["binstalk-downloader/trust-dns"] + +# Experimental HTTP/3 client, this would require `--cfg reqwest_unstable` +# to be passed to `rustc`. +http3 = ["binstalk-downloader/http3"] + +zstd-thin = ["binstalk-downloader/zstd-thin"] +cross-lang-fat-lto = ["binstalk-downloader/cross-lang-fat-lto"] + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] diff --git a/crates/binstalk/LICENSE b/crates/binstalk/LICENSE new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/crates/binstalk/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/crates/binstalk/src/errors.rs b/crates/binstalk/src/errors.rs new file mode 100644 index 00000000..afbcfe55 --- /dev/null +++ b/crates/binstalk/src/errors.rs @@ -0,0 +1,599 @@ +use std::{ + fmt, io, ops, + path::PathBuf, + process::{ExitCode, ExitStatus, Termination}, +}; + +use binstalk_downloader::{download::DownloadError, remote::Error as RemoteError}; +use binstalk_fetchers::FetchError; +use compact_str::CompactString; +use itertools::Itertools; +use miette::{Diagnostic, Report}; +use target_lexicon::ParseError as TargetTripleParseError; +use thiserror::Error; +use tokio::task; +use tracing::{error, warn}; + +use crate::{ + bins, + helpers::{ + cargo_toml::Error as CargoTomlError, + cargo_toml_workspace::Error as LoadManifestFromWSError, gh_api_client::GhApiError, + }, + registry::{InvalidRegistryError, RegistryError}, +}; + +#[derive(Debug, Error)] +#[error("version string '{v}' is not semver: {err}")] +pub struct VersionParseError { + pub v: CompactString, + #[source] + pub err: semver::Error, +} + +#[derive(Debug, Diagnostic, Error)] +#[error("For crate {crate_name}: {err}")] +pub struct CrateContextError { + crate_name: CompactString, + #[source] + #[diagnostic(transparent)] + err: BinstallError, +} + +#[derive(Debug)] +pub struct CrateErrors(Box<[Box]>); + +impl CrateErrors { + fn iter(&self) -> impl Iterator + Clone { + self.0.iter().map(ops::Deref::deref) + } + + fn get_iter_for<'a, T: 'a>( + &'a self, + f: fn(&'a CrateContextError) -> Option, + ) -> Option + 'a> { + let iter = self.iter().filter_map(f); + + if iter.clone().next().is_none() { + None + } else { + Some(iter) + } + } +} + +impl fmt::Display for CrateErrors { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0.iter().format(", "), f) + } +} + +impl std::error::Error for CrateErrors { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.0.first().map(|e| e as _) + } +} + +impl miette::Diagnostic for CrateErrors { + fn code<'a>(&'a self) -> Option> { + Some(Box::new("binstall::many_failure")) + } + + fn severity(&self) -> Option { + self.iter().filter_map(miette::Diagnostic::severity).max() + } + + fn help<'a>(&'a self) -> Option> { + Some(Box::new( + self.get_iter_for(miette::Diagnostic::help)?.format("\n"), + )) + } + + fn url<'a>(&'a self) -> Option> { + Some(Box::new( + self.get_iter_for(miette::Diagnostic::url)?.format("\n"), + )) + } + + fn source_code(&self) -> Option<&dyn miette::SourceCode> { + self.iter().find_map(miette::Diagnostic::source_code) + } + + fn labels(&self) -> Option + '_>> { + let get_iter = || self.iter().filter_map(miette::Diagnostic::labels).flatten(); + + if get_iter().next().is_none() { + None + } else { + Some(Box::new(get_iter())) + } + } + + fn related<'a>(&'a self) -> Option + 'a>> { + Some(Box::new( + self.iter().map(|e| e as _).chain( + self.iter() + .filter_map(miette::Diagnostic::related) + .flatten(), + ), + )) + } + + fn diagnostic_source(&self) -> Option<&dyn miette::Diagnostic> { + self.0.first().map(|err| &**err as _) + } +} + +#[derive(Debug, Error)] +#[error("Invalid pkg-url {pkg_url} for {crate_name}@{version} on {target}: {reason}")] +pub struct InvalidPkgFmtError { + pub crate_name: CompactString, + pub version: CompactString, + pub target: String, + pub pkg_url: String, + pub reason: &'static str, +} + +/// Error kinds emitted by cargo-binstall. +#[derive(Error, Diagnostic, Debug)] +#[non_exhaustive] +pub enum BinstallError { + /// Internal: a task could not be joined. + /// + /// - Code: `binstall::internal::task_join` + /// - Exit: 17 + #[error(transparent)] + #[diagnostic(severity(error), code(binstall::internal::task_join))] + TaskJoinError(#[from] task::JoinError), + + /// The installation was cancelled by a user at a confirmation prompt, + /// or user send a ctrl_c on all platforms or + /// `SIGINT`, `SIGHUP`, `SIGTERM` or `SIGQUIT` on unix to the program. + /// + /// - Code: `binstall::user_abort` + /// - Exit: 32 + #[error("installation cancelled by user")] + #[diagnostic(severity(info), code(binstall::user_abort))] + UserAbort, + + /// Package is not signed and policy requires it. + /// + /// - Code: `binstall::signature::invalid` + /// - Exit: 40 + #[error("Crate {crate_name} is signed and package {package_name} failed verification")] + #[diagnostic(severity(error), code(binstall::signature::invalid))] + InvalidSignature { + crate_name: CompactString, + package_name: CompactString, + }, + + /// Package is not signed and policy requires it. + /// + /// - Code: `binstall::signature::missing` + /// - Exit: 41 + #[error("Crate {0} does not have signing information")] + #[diagnostic(severity(error), code(binstall::signature::missing))] + MissingSignature(CompactString), + + /// A URL is invalid. + /// + /// This may be the result of a template in a Cargo manifest. + /// + /// - Code: `binstall::url_parse` + /// - Exit: 65 + #[error("Failed to parse url: {0}")] + #[diagnostic(severity(error), code(binstall::url_parse))] + UrlParse(#[from] url::ParseError), + + /// Failed to parse template. + /// + /// - Code: `binstall::template` + /// - Exit: 67 + #[error(transparent)] + #[diagnostic(severity(error), code(binstall::template))] + #[source_code(transparent)] + #[label(transparent)] + TemplateParseError( + #[from] + #[diagnostic_source] + leon::ParseError, + ), + + /// Failed to fetch pre-built binaries. + /// + /// - Code: `binstall::fetch` + /// - Exit: 68 + #[error(transparent)] + #[diagnostic(severity(error), code(binstall::fetch))] + #[source_code(transparent)] + #[label(transparent)] + FetchError(Box), + + /// Failed to download or failed to decode the body. + /// + /// - Code: `binstall::download` + /// - Exit: 68 + #[error(transparent)] + #[diagnostic(severity(error), code(binstall::download))] + Download(#[from] DownloadError), + + /// A subprocess failed. + /// + /// This is often about cargo-install calls. + /// + /// - Code: `binstall::subprocess` + /// - Exit: 70 + #[error("subprocess {command} errored with {status}")] + #[diagnostic(severity(error), code(binstall::subprocess))] + SubProcess { + command: Box, + status: ExitStatus, + }, + + /// A generic I/O error. + /// + /// - Code: `binstall::io` + /// - Exit: 74 + #[error("I/O Error: {0}")] + #[diagnostic(severity(error), code(binstall::io))] + Io(io::Error), + + /// Unknown registry name + /// + /// - Code: `binstall::cargo_registry` + /// - Exit: 75 + #[error("Unknown registry name {0}, env `CARGO_REGISTRIES_{0}_INDEX` nor is it in .cargo/config.toml")] + #[diagnostic(severity(error), code(binstall::cargo_registry))] + UnknownRegistryName(CompactString), + + /// An error interacting with the crates.io API. + /// + /// This could either be a "not found" or a server/transport error. + /// + /// - Code: `binstall::cargo_registry` + /// - Exit: 76 + #[error(transparent)] + #[diagnostic(transparent)] + RegistryError(#[from] Box), + + /// The override path to the cargo manifest is invalid or cannot be resolved. + /// + /// - Code: `binstall::cargo_manifest_path` + /// - Exit: 77 + #[error("the --manifest-path is invalid or cannot be resolved")] + #[diagnostic(severity(error), code(binstall::cargo_manifest_path))] + CargoManifestPath, + + /// A parsing or validation error in a cargo manifest. + /// + /// This should be rare, as manifests are generally fetched from crates.io, which does its own + /// validation upstream. The most common failure will therefore be for direct repository access + /// and with the `--manifest-path` option. + /// + /// - Code: `binstall::cargo_manifest` + /// - Exit: 78 + #[error("Failed to parse cargo manifest: {0}")] + #[diagnostic( + severity(error), + code(binstall::cargo_manifest), + help("If you used --manifest-path, check the Cargo.toml syntax.") + )] + CargoManifest(Box), + + /// Failure to parse registry index url + /// + /// - Code: `binstall::cargo_registry` + /// - Exit: 79 + #[error(transparent)] + #[diagnostic(severity(error), code(binstall::cargo_registry))] + RegistryParseError(#[from] Box), + + /// A version is not valid semver. + /// + /// Note that we use the [`semver`] crate, which parses Cargo version syntax; this may be + /// somewhat stricter or very slightly different from other semver implementations. + /// + /// - Code: `binstall::version::parse` + /// - Exit: 80 + #[error(transparent)] + #[diagnostic(severity(error), code(binstall::version::parse))] + VersionParse(#[from] Box), + + /// The crate@version syntax was used at the same time as the --version option. + /// + /// You can't do that as it's ambiguous which should apply. + /// + /// - Code: `binstall::conflict::version` + /// - Exit: 84 + #[error("superfluous version specification")] + #[diagnostic( + severity(error), + code(binstall::conflict::version), + help("You cannot use both crate@version and the --version option. Remove one.") + )] + SuperfluousVersionOption, + + /// No binaries were found for the crate. + /// + /// When installing, either the binaries are specified in the crate's Cargo.toml, or they're + /// inferred from the crate layout (e.g. src/main.rs or src/bins/name.rs). If no binaries are + /// found through these methods, we can't know what to install! + /// + /// - Code: `binstall::resolve::binaries` + /// - Exit: 86 + #[error("no binaries specified nor inferred")] + #[diagnostic( + severity(error), + code(binstall::resolve::binaries), + help("This crate doesn't specify any binaries, so there's nothing to install.") + )] + UnspecifiedBinaries, + + /// No viable targets were found. + /// + /// When installing, we attempt to find which targets the host (your computer) supports, and + /// discover builds for these targets from the remote binary source. This error occurs when we + /// fail to discover the host's target. + /// + /// You should in this case specify --target manually. + /// + /// - Code: `binstall::targets::none_host` + /// - Exit: 87 + #[error("failed to discovered a viable target from the host")] + #[diagnostic( + severity(error), + code(binstall::targets::none_host), + help("Try to specify --target") + )] + NoViableTargets, + + /// Failed to find or install binaries. + /// + /// - Code: `binstall::bins` + /// - Exit: 88 + #[error("failed to find or install binaries: {0}")] + #[diagnostic( + severity(error), + code(binstall::targets::none_host), + help("Try to specify --target") + )] + BinFile(#[from] bins::Error), + + /// `Cargo.toml` of the crate does not have section "Package". + /// + /// - Code: `binstall::cargo_manifest` + /// - Exit: 89 + #[error("Cargo.toml of crate {0} does not have section \"Package\"")] + #[diagnostic(severity(error), code(binstall::cargo_manifest))] + CargoTomlMissingPackage(CompactString), + + /// bin-dir configuration provided generates duplicate source path. + /// + /// - Code: `binstall::cargo_manifest` + /// - Exit: 90 + #[error("bin-dir configuration provided generates duplicate source path: {path}")] + #[diagnostic(severity(error), code(binstall::SourceFilePath))] + DuplicateSourceFilePath { path: PathBuf }, + + /// Fallback to `cargo-install` is disabled. + /// + /// - Code: `binstall::no_fallback_to_cargo_install` + /// - Exit: 94 + #[error("Fallback to cargo-install is disabled")] + #[diagnostic(severity(error), code(binstall::no_fallback_to_cargo_install))] + NoFallbackToCargoInstall, + + /// Fallback to `cargo-install` is disabled. + /// + /// - Code: `binstall::invalid_pkg_fmt` + /// - Exit: 95 + #[error(transparent)] + #[diagnostic(severity(error), code(binstall::invalid_pkg_fmt))] + InvalidPkgFmt(Box), + + /// Request to GitHub API failed + /// + /// - Code: `binstall::gh_api_failure` + /// - Exit: 96 + #[error("Request to GitHub API failed: {0}")] + #[diagnostic(severity(error), code(binstall::gh_api_failure))] + GhApiErr(#[source] Box), + + /// Failed to parse target triple + /// + /// - Code: `binstall::target_triple_parse_error` + /// - Exit: 97 + #[error("Failed to parse target triple: {0}")] + #[diagnostic(severity(error), code(binstall::target_triple_parse_error))] + TargetTripleParseError(#[source] Box), + + /// Failed to shallow clone git repository + /// + /// - Code: `binstall::git` + /// - Exit: 98 + #[cfg(feature = "git")] + #[error("Failed to shallow clone git repository: {0}")] + #[diagnostic(severity(error), code(binstall::git))] + GitError(#[from] crate::helpers::git::GitError), + + /// Failed to load manifest from workspace + /// + /// - Code: `binstall::load_manifest_from_workspace` + /// - Exit: 99 + #[error(transparent)] + #[diagnostic(severity(error), code(binstall::load_manifest_from_workspace))] + LoadManifestFromWSError(#[from] Box), + + /// A wrapped error providing the context of which crate the error is about. + #[error(transparent)] + #[diagnostic(transparent)] + CrateContext(Box), + + /// A wrapped error for failures of multiple crates when `--continue-on-failure` is specified. + #[error(transparent)] + #[diagnostic(transparent)] + Errors(CrateErrors), +} + +impl BinstallError { + fn exit_number(&self) -> u8 { + use BinstallError::*; + let code: u8 = match self { + TaskJoinError(_) => 17, + UserAbort => 32, + InvalidSignature { .. } => 40, + MissingSignature(_) => 41, + UrlParse(_) => 65, + TemplateParseError(..) => 67, + FetchError(..) => 68, + Download(_) => 68, + SubProcess { .. } => 70, + Io(_) => 74, + UnknownRegistryName(_) => 75, + RegistryError { .. } => 76, + CargoManifestPath => 77, + CargoManifest { .. } => 78, + RegistryParseError(..) => 79, + VersionParse { .. } => 80, + SuperfluousVersionOption => 84, + UnspecifiedBinaries => 86, + NoViableTargets => 87, + BinFile(_) => 88, + CargoTomlMissingPackage(_) => 89, + DuplicateSourceFilePath { .. } => 90, + NoFallbackToCargoInstall => 94, + InvalidPkgFmt(..) => 95, + GhApiErr(..) => 96, + TargetTripleParseError(..) => 97, + #[cfg(feature = "git")] + GitError(_) => 98, + LoadManifestFromWSError(_) => 99, + CrateContext(context) => context.err.exit_number(), + Errors(errors) => (errors.0)[0].err.exit_number(), + }; + + // reserved codes + debug_assert!(code != 64 && code != 16 && code != 1 && code != 2 && code != 0); + + code + } + + /// The recommended exit code for this error. + /// + /// This will never output: + /// - 0 (success) + /// - 1 and 2 (catchall and shell) + /// - 16 (binstall errors not handled here) + /// - 64 (generic error) + pub fn exit_code(&self) -> ExitCode { + self.exit_number().into() + } + + /// Add crate context to the error + pub fn crate_context(self, crate_name: impl Into) -> Self { + self.crate_context_inner(crate_name.into()) + } + + fn crate_context_inner(self, crate_name: CompactString) -> Self { + match self { + Self::CrateContext(mut crate_context_error) => { + crate_context_error.crate_name = crate_name; + Self::CrateContext(crate_context_error) + } + err => Self::CrateContext(Box::new(CrateContextError { err, crate_name })), + } + } + + pub fn crate_errors(mut errors: Vec>) -> Option { + if errors.is_empty() { + None + } else if errors.len() == 1 { + Some(Self::CrateContext(errors.pop().unwrap())) + } else { + Some(Self::Errors(CrateErrors(errors.into_boxed_slice()))) + } + } +} + +impl Termination for BinstallError { + fn report(self) -> ExitCode { + let code = self.exit_code(); + if let BinstallError::UserAbort = self { + warn!("Installation cancelled"); + } else { + error!("Fatal error:\n{:?}", Report::new(self)); + } + + code + } +} + +impl From for BinstallError { + fn from(err: io::Error) -> Self { + err.downcast::() + .unwrap_or_else(BinstallError::Io) + } +} + +impl From for io::Error { + fn from(e: BinstallError) -> io::Error { + match e { + BinstallError::Io(io_error) => io_error, + e => io::Error::other(e), + } + } +} + +impl From for BinstallError { + fn from(e: RemoteError) -> Self { + DownloadError::from(e).into() + } +} + +impl From for BinstallError { + fn from(e: CargoTomlError) -> Self { + BinstallError::CargoManifest(Box::new(e)) + } +} + +impl From for BinstallError { + fn from(e: InvalidPkgFmtError) -> Self { + BinstallError::InvalidPkgFmt(Box::new(e)) + } +} + +impl From for BinstallError { + fn from(e: GhApiError) -> Self { + BinstallError::GhApiErr(Box::new(e)) + } +} + +impl From for BinstallError { + fn from(e: target_lexicon::ParseError) -> Self { + BinstallError::TargetTripleParseError(Box::new(e)) + } +} + +impl From for BinstallError { + fn from(e: RegistryError) -> Self { + BinstallError::RegistryError(Box::new(e)) + } +} + +impl From for BinstallError { + fn from(e: InvalidRegistryError) -> Self { + BinstallError::RegistryParseError(Box::new(e)) + } +} + +impl From for BinstallError { + fn from(e: LoadManifestFromWSError) -> Self { + BinstallError::LoadManifestFromWSError(Box::new(e)) + } +} + +impl From for BinstallError { + fn from(e: FetchError) -> Self { + BinstallError::FetchError(Box::new(e)) + } +} diff --git a/crates/binstalk/src/helpers.rs b/crates/binstalk/src/helpers.rs new file mode 100644 index 00000000..5222f440 --- /dev/null +++ b/crates/binstalk/src/helpers.rs @@ -0,0 +1,19 @@ +pub mod jobserver_client; +pub mod remote { + pub use binstalk_downloader::remote::*; + pub use url::ParseError as UrlParseError; +} +pub mod lazy_gh_api_client; +pub(crate) mod target_triple; +pub mod tasks; + +pub(crate) use binstalk_downloader::download; +pub use binstalk_git_repo_api::gh_api_client; + +pub(crate) use cargo_toml_workspace::{self, cargo_toml}; +#[cfg(feature = "git")] +pub(crate) use simple_git as git; + +pub(crate) fn is_universal_macos(target: &str) -> bool { + ["universal-apple-darwin", "universal2-apple-darwin"].contains(&target) +} diff --git a/crates/binstalk/src/helpers/jobserver_client.rs b/crates/binstalk/src/helpers/jobserver_client.rs new file mode 100644 index 00000000..ae9db6b2 --- /dev/null +++ b/crates/binstalk/src/helpers/jobserver_client.rs @@ -0,0 +1,33 @@ +use std::{num::NonZeroUsize, thread::available_parallelism}; + +use jobslot::Client; +use tokio::sync::OnceCell; + +use crate::errors::BinstallError; + +#[derive(Debug)] +pub struct LazyJobserverClient(OnceCell); + +impl LazyJobserverClient { + /// This must be called at the start of the program since + /// `Client::from_env` requires that. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + // Safety: + // + // Client::from_env is unsafe because from_raw_fd is unsafe. + // It doesn't do anything that is actually unsafe, like + // dereferencing pointer. + let opt = unsafe { Client::from_env() }; + Self(OnceCell::new_with(opt)) + } + + pub async fn get(&self) -> Result<&Client, BinstallError> { + self.0 + .get_or_try_init(|| async { + let ncore = available_parallelism().map(NonZeroUsize::get).unwrap_or(1); + Ok(Client::new(ncore)?) + }) + .await + } +} diff --git a/crates/binstalk/src/helpers/lazy_gh_api_client.rs b/crates/binstalk/src/helpers/lazy_gh_api_client.rs new file mode 100644 index 00000000..26869e91 --- /dev/null +++ b/crates/binstalk/src/helpers/lazy_gh_api_client.rs @@ -0,0 +1,53 @@ +use std::{future::Future, sync::Mutex}; + +use binstalk_git_repo_api::gh_api_client::GhApiClient; +use tokio::sync::OnceCell; +use zeroize::Zeroizing; + +use crate::{ + errors::BinstallError, + helpers::{remote, tasks::AutoAbortJoinHandle}, +}; + +pub type GitHubToken = Option>>; + +#[derive(Debug)] +pub struct LazyGhApiClient { + client: remote::Client, + inner: OnceCell, + task: Mutex>>, +} + +impl LazyGhApiClient { + pub fn new(client: remote::Client, auth_token: GitHubToken) -> Self { + Self { + inner: OnceCell::new_with(Some(GhApiClient::new(client.clone(), auth_token))), + client, + task: Mutex::new(None), + } + } + + pub fn with_get_gh_token_future(client: remote::Client, get_auth_token_future: Fut) -> Self + where + Fut: Future + Send + Sync + 'static, + { + Self { + inner: OnceCell::new(), + task: Mutex::new(Some(AutoAbortJoinHandle::spawn(get_auth_token_future))), + client, + } + } + + pub async fn get(&self) -> Result<&GhApiClient, BinstallError> { + self.inner + .get_or_try_init(|| async { + let task = self.task.lock().unwrap().take(); + Ok(if let Some(task) = task { + GhApiClient::new(self.client.clone(), task.await?) + } else { + GhApiClient::new(self.client.clone(), None) + }) + }) + .await + } +} diff --git a/crates/binstalk/src/helpers/target_triple.rs b/crates/binstalk/src/helpers/target_triple.rs new file mode 100644 index 00000000..260deac4 --- /dev/null +++ b/crates/binstalk/src/helpers/target_triple.rs @@ -0,0 +1,52 @@ +use std::{borrow::Cow, str::FromStr}; + +use compact_str::{CompactString, ToCompactString}; +use target_lexicon::Triple; + +use crate::{errors::BinstallError, helpers::is_universal_macos}; + +#[derive(Clone, Debug)] +pub struct TargetTriple { + pub target_family: Cow<'static, str>, + pub target_arch: Cow<'static, str>, + pub target_libc: Cow<'static, str>, + pub target_vendor: CompactString, +} + +impl FromStr for TargetTriple { + type Err = BinstallError; + + fn from_str(mut s: &str) -> Result { + let is_universal_macos = is_universal_macos(s); + + if is_universal_macos { + s = "x86_64-apple-darwin"; + } + + let triple = Triple::from_str(s)?; + + Ok(Self { + target_family: triple.operating_system.into_str(), + target_arch: if is_universal_macos { + Cow::Borrowed("universal") + } else { + triple.architecture.into_str() + }, + target_libc: triple.environment.into_str(), + target_vendor: triple.vendor.to_compact_string(), + }) + } +} + +impl leon::Values for TargetTriple { + fn get_value<'s>(&'s self, key: &str) -> Option> { + match key { + "target-family" => Some(Cow::Borrowed(&self.target_family)), + "target-arch" => Some(Cow::Borrowed(&self.target_arch)), + "target-libc" => Some(Cow::Borrowed(&self.target_libc)), + "target-vendor" => Some(Cow::Borrowed(&self.target_vendor)), + + _ => None, + } + } +} diff --git a/crates/binstalk/src/helpers/tasks.rs b/crates/binstalk/src/helpers/tasks.rs new file mode 100644 index 00000000..6530c444 --- /dev/null +++ b/crates/binstalk/src/helpers/tasks.rs @@ -0,0 +1,70 @@ +use std::{ + future::Future, + ops::{Deref, DerefMut}, + pin::Pin, + task::{Context, Poll}, +}; + +use tokio::task::JoinHandle; + +use crate::errors::BinstallError; + +#[derive(Debug)] +pub struct AutoAbortJoinHandle(JoinHandle); + +impl AutoAbortJoinHandle { + pub fn new(handle: JoinHandle) -> Self { + Self(handle) + } +} + +impl AutoAbortJoinHandle +where + T: Send + 'static, +{ + pub fn spawn(future: F) -> Self + where + F: Future + Send + 'static, + { + Self(tokio::spawn(future)) + } +} + +impl Drop for AutoAbortJoinHandle { + fn drop(&mut self) { + self.0.abort(); + } +} + +impl Deref for AutoAbortJoinHandle { + type Target = JoinHandle; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for AutoAbortJoinHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Future for AutoAbortJoinHandle { + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut Pin::into_inner(self).0) + .poll(cx) + .map_err(BinstallError::TaskJoinError) + } +} + +impl AutoAbortJoinHandle> +where + E: Into, +{ + pub async fn flattened_join(self) -> Result { + self.await?.map_err(Into::into) + } +} diff --git a/crates/binstalk/src/lib.rs b/crates/binstalk/src/lib.rs new file mode 100644 index 00000000..83df1938 --- /dev/null +++ b/crates/binstalk/src/lib.rs @@ -0,0 +1,13 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +pub mod errors; +pub mod helpers; +pub mod ops; + +use binstalk_bins as bins; +pub use binstalk_fetchers as fetchers; +pub use binstalk_registry as registry; +pub use binstalk_types as manifests; +pub use detect_targets::{get_desired_targets, DesiredTargets, TARGET}; + +pub use fetchers::QUICKINSTALL_STATS_URL; diff --git a/crates/binstalk/src/ops.rs b/crates/binstalk/src/ops.rs new file mode 100644 index 00000000..88d38fa2 --- /dev/null +++ b/crates/binstalk/src/ops.rs @@ -0,0 +1,65 @@ +//! Concrete Binstall operations. + +use std::{path::PathBuf, sync::Arc, time::Duration}; + +use compact_str::CompactString; +use semver::VersionReq; + +use crate::{ + fetchers::{Data, Fetcher, SignaturePolicy, TargetDataErased}, + helpers::{ + gh_api_client::GhApiClient, jobserver_client::LazyJobserverClient, + lazy_gh_api_client::LazyGhApiClient, remote::Client, + }, + manifests::cargo_toml_binstall::PkgOverride, + registry::Registry, + DesiredTargets, +}; + +pub mod resolve; + +pub type Resolver = + fn(Client, GhApiClient, Arc, Arc, SignaturePolicy) -> Arc; + +#[derive(Debug)] +#[non_exhaustive] +pub enum CargoTomlFetchOverride { + #[cfg(feature = "git")] + Git(crate::helpers::git::GitUrl), + Path(PathBuf), +} + +#[derive(Debug)] +pub struct Options { + pub no_symlinks: bool, + pub dry_run: bool, + pub force: bool, + pub quiet: bool, + pub locked: bool, + pub no_track: bool, + + pub version_req: Option, + pub cargo_toml_fetch_override: Option, + pub cli_overrides: PkgOverride, + + pub desired_targets: DesiredTargets, + pub resolvers: Vec, + pub cargo_install_fallback: bool, + + /// If provided, the names are sorted. + pub bins: Option>, + + pub temp_dir: PathBuf, + pub install_path: PathBuf, + pub cargo_root: Option, + + pub client: Client, + pub gh_api_client: LazyGhApiClient, + pub jobserver_client: LazyJobserverClient, + pub registry: Registry, + + pub signature_policy: SignaturePolicy, + pub disable_telemetry: bool, + + pub maximum_resolution_timeout: Duration, +} diff --git a/crates/binstalk/src/ops/resolve.rs b/crates/binstalk/src/ops/resolve.rs new file mode 100644 index 00000000..74266340 --- /dev/null +++ b/crates/binstalk/src/ops/resolve.rs @@ -0,0 +1,586 @@ +use std::{ + borrow::Cow, + collections::{BTreeMap, BTreeSet}, + iter, mem, + path::Path, + str::FromStr, + sync::Arc, +}; + +use binstalk_fetchers::FETCHER_GH_CRATE_META; +use binstalk_types::{ + cargo_toml_binstall::Strategy, + crate_info::{CrateSource, SourceType}, +}; +use compact_str::{CompactString, ToCompactString}; +use itertools::Itertools; +use leon::Template; +use maybe_owned::MaybeOwned; +use semver::{Version, VersionReq}; +use tokio::{task::spawn_blocking, time::timeout}; +use tracing::{debug, error, info, instrument, warn}; +use url::Url; + +use crate::{ + bins, + errors::{BinstallError, VersionParseError}, + fetchers::{Data, Fetcher, TargetData}, + helpers::{ + cargo_toml::Manifest, cargo_toml_workspace::load_manifest_from_workspace, + download::ExtractedFiles, remote::Client, target_triple::TargetTriple, + tasks::AutoAbortJoinHandle, + }, + manifests::cargo_toml_binstall::{Meta, PkgMeta, PkgOverride}, + ops::{CargoTomlFetchOverride, Options}, +}; + +mod crate_name; +#[doc(inline)] +pub use crate_name::CrateName; + +mod version_ext; +#[doc(inline)] +pub use version_ext::VersionReqExt; + +mod resolution; +#[doc(inline)] +pub use resolution::{Resolution, ResolutionFetch, ResolutionSource}; + +#[instrument(skip_all)] +pub async fn resolve( + opts: Arc, + crate_name: CrateName, + curr_version: Option, +) -> Result { + let crate_name_name = crate_name.name.clone(); + let resolution = resolve_inner(opts, crate_name, curr_version) + .await + .map_err(|err| err.crate_context(crate_name_name))?; + + Ok(resolution) +} + +async fn resolve_inner( + opts: Arc, + crate_name: CrateName, + curr_version: Option, +) -> Result { + info!("Resolving package: '{}'", crate_name); + + let version_req = match (&crate_name.version_req, &opts.version_req) { + (Some(version), None) => MaybeOwned::Borrowed(version), + (None, Some(version)) => MaybeOwned::Borrowed(version), + (Some(_), Some(_)) => Err(BinstallError::SuperfluousVersionOption)?, + (None, None) => MaybeOwned::Owned(VersionReq::STAR), + }; + + let version_req_str = version_req.to_compact_string(); + + let Some(package_info) = PackageInfo::resolve( + &opts, + crate_name.name, + curr_version, + &version_req, + opts.client.clone(), + ) + .await? + else { + return Ok(Resolution::AlreadyUpToDate); + }; + + let desired_targets = opts + .desired_targets + .get() + .await + .iter() + .map(|target| { + debug!("Building metadata for target: {target}"); + + let meta = package_info.meta.merge_overrides( + iter::once(&opts.cli_overrides).chain(package_info.overrides.get(target)), + ); + + debug!("Found metadata: {meta:?}"); + + Ok(Arc::new(TargetData { + target: target.clone(), + meta, + target_related_info: TargetTriple::from_str(target)?, + })) + }) + .collect::, BinstallError>>()?; + let resolvers = &opts.resolvers; + + let binary_name = match package_info.binaries.as_slice() { + [bin] if bin.name != package_info.name => Some(CompactString::from(bin.name.as_str())), + _ => None, + }; + + let mut handles: Vec> = Vec::with_capacity( + desired_targets.len() * resolvers.len() + + if binary_name.is_some() { + desired_targets.len() + } else { + 0 + }, + ); + + let gh_api_client = opts.gh_api_client.get().await?; + + let mut handles_fn = + |data: Arc, filter_fetcher_by_name_predicate: fn(&'static str) -> bool| { + handles.extend( + resolvers + .iter() + .cartesian_product(&desired_targets) + .filter_map(|(f, target_data)| { + let fetcher = f( + opts.client.clone(), + gh_api_client.clone(), + data.clone(), + target_data.clone(), + opts.signature_policy, + ); + + if let Some(disabled_strategies) = + target_data.meta.disabled_strategies.as_deref() + { + if disabled_strategies.contains(&fetcher.strategy()) { + return None; + } + } + + filter_fetcher_by_name_predicate(fetcher.fetcher_name()).then_some(fetcher) + }), + ) + }; + + handles_fn( + Arc::new(Data::new( + package_info.name.clone(), + package_info.version_str.clone(), + package_info.repo.clone(), + )), + |_| true, + ); + + if let Some(binary_name) = binary_name { + handles_fn( + Arc::new(Data::new( + binary_name, + package_info.version_str.clone(), + package_info.repo.clone(), + )), + |name| name == FETCHER_GH_CRATE_META, + ); + } + + for fetcher in &handles { + match timeout( + opts.maximum_resolution_timeout, + AutoAbortJoinHandle::new(fetcher.clone().find()).flattened_join(), + ) + .await + { + Ok(ret) => match ret { + Ok(true) => { + // Generate temporary binary path + let bin_path = opts.temp_dir.join(format!( + "bin-{}-{}-{}", + package_info.name, + fetcher.target(), + fetcher.fetcher_name() + )); + + match download_extract_and_verify( + fetcher.as_ref(), + &bin_path, + &package_info, + &opts.install_path, + opts.no_symlinks, + ) + .await + { + Ok(bin_files) => { + if !bin_files.is_empty() { + fetcher.clone().report_to_upstream(); + return Ok(Resolution::Fetch(Box::new(ResolutionFetch { + fetcher: fetcher.clone(), + new_version: package_info.version, + name: package_info.name, + version_req: version_req_str, + source: package_info.source, + bin_files, + }))); + } else { + warn!( + "Error when checking binaries provided by fetcher {}: \ + The fetcher does not provide any optional binary", + fetcher.source_name(), + ); + } + } + Err(err) => { + if let BinstallError::UserAbort = err { + return Err(err); + } + warn!( + "Error while downloading and extracting from fetcher {}: {}", + fetcher.source_name(), + err + ); + } + } + } + Ok(false) => (), + Err(err) => { + warn!( + "Error while checking fetcher {}: {}", + fetcher.source_name(), + err + ); + } + }, + Err(err) => { + warn!( + "Timeout reached while checking fetcher {}: {}", + fetcher.source_name(), + err + ); + } + } + } + + // At this point, we don't know whether fallback to cargo install is allowed, or whether it will + // succeed, but things start to get convoluted when try to include that data, so this will do. + if !opts.disable_telemetry { + for fetcher in handles { + fetcher.report_to_upstream(); + } + } + + if !opts.cargo_install_fallback { + return Err(BinstallError::NoFallbackToCargoInstall); + } + + let meta = package_info + .meta + .merge_overrides(iter::once(&opts.cli_overrides)); + + let target_meta = desired_targets + .first() + .map(|target_data| &target_data.meta) + .unwrap_or(&meta); + + if let Some(disabled_strategies) = target_meta.disabled_strategies.as_deref() { + if disabled_strategies.contains(&Strategy::Compile) { + return Err(BinstallError::NoFallbackToCargoInstall); + } + } + + Ok(Resolution::InstallFromSource(ResolutionSource { + name: package_info.name, + version: package_info.version_str, + })) +} + +/// * `fetcher` - `fetcher.find()` must have returned `Ok(true)`. +/// +/// Can return empty Vec if all `BinFile` is optional and does not exist +/// in the archive downloaded. +async fn download_extract_and_verify( + fetcher: &dyn Fetcher, + bin_path: &Path, + package_info: &PackageInfo, + install_path: &Path, + no_symlinks: bool, +) -> Result, BinstallError> { + // Download and extract it. + // If that fails, then ignore this fetcher. + let extracted_files = fetcher.fetch_and_extract(bin_path).await?; + debug!("extracted_files = {extracted_files:#?}"); + + // Build final metadata + let meta = fetcher.target_meta(); + + // Verify that all non-optional bin_files exist + let bin_files = collect_bin_files( + fetcher, + package_info, + meta, + bin_path, + install_path, + no_symlinks, + &extracted_files, + )?; + + let name = &package_info.name; + + package_info + .binaries + .iter() + .zip(bin_files) + .filter_map(|(bin, bin_file)| { + match bin_file.check_source_exists(&mut |p| extracted_files.has_file(p)) { + Ok(()) => Some(Ok(bin_file)), + + // This binary is optional + Err(err) => { + let required_features = &bin.required_features; + let bin_name = bin.name.as_str(); + + if required_features.is_empty() { + error!( + "When resolving {name} bin {bin_name} is not found. \ +This binary is not optional so it must be included in the archive, please contact with \ +upstream to fix this issue." + ); + // This bin is not optional, error + Some(Err(err)) + } else { + // Optional, print a warning and continue. + let features = required_features.iter().format(","); + warn!( + "When resolving {name} bin {bin_name} is not found. \ +But since it requires features {features}, this bin is ignored." + ); + None + } + } + } + }) + .collect::, bins::Error>>() + .map_err(BinstallError::from) +} + +fn collect_bin_files( + fetcher: &dyn Fetcher, + package_info: &PackageInfo, + meta: PkgMeta, + bin_path: &Path, + install_path: &Path, + no_symlinks: bool, + extracted_files: &ExtractedFiles, +) -> Result, BinstallError> { + // List files to be installed + // based on those found via Cargo.toml + let bin_data = bins::Data { + name: &package_info.name, + target: fetcher.target(), + version: &package_info.version_str, + repo: package_info.repo.as_deref(), + meta, + bin_path, + install_path, + target_related_info: &fetcher.target_data().target_related_info, + }; + + let bin_dir = bin_data + .meta + .bin_dir + .as_deref() + .map(Cow::Borrowed) + .unwrap_or_else(|| { + bins::infer_bin_dir_template(&bin_data, &mut |p| extracted_files.get_dir(p).is_some()) + }); + + let template = Template::parse(&bin_dir)?; + + // Create bin_files + let bin_files = package_info + .binaries + .iter() + .map(|bin| bins::BinFile::new(&bin_data, bin.name.as_str(), &template, no_symlinks)) + .collect::, bins::Error>>()?; + + let mut source_set = BTreeSet::new(); + + for bin in &bin_files { + if !source_set.insert(&bin.source) { + return Err(BinstallError::DuplicateSourceFilePath { + path: bin.source.clone(), + }); + } + } + + Ok(bin_files) +} + +struct PackageInfo { + meta: PkgMeta, + binaries: Vec, + name: CompactString, + version_str: CompactString, + source: CrateSource, + version: Version, + repo: Option, + overrides: BTreeMap, +} + +struct Bin { + name: String, + required_features: Vec, +} + +impl PackageInfo { + /// Return `None` if already up-to-date. + async fn resolve( + opts: &Options, + name: CompactString, + curr_version: Option, + version_req: &VersionReq, + client: Client, + ) -> Result, BinstallError> { + use CargoTomlFetchOverride::*; + + // Fetch crate via crates.io, git, or use a local manifest path + let (manifest, source) = match opts.cargo_toml_fetch_override.as_ref() { + Some(Path(manifest_path)) => ( + spawn_blocking({ + let manifest_path = manifest_path.clone(); + let name = name.clone(); + + move || load_manifest_path(manifest_path, &name) + }) + .await??, + CrateSource { + source_type: SourceType::Path, + url: MaybeOwned::Owned(Url::parse(&format!( + "file://{}", + manifest_path.display() + ))?), + }, + ), + #[cfg(feature = "git")] + Some(Git(git_url)) => { + use crate::helpers::git::{GitCancellationToken, Repository as GitRepository}; + + let cancellation_token = GitCancellationToken::default(); + // Cancel git operation if the future is cancelled (dropped). + let cancel_on_drop = cancellation_token.clone().cancel_on_drop(); + + let (ret, commit_hash) = spawn_blocking({ + let git_url = git_url.clone(); + let name = name.clone(); + move || { + let dir = tempfile::TempDir::new()?; + let repo = GitRepository::shallow_clone( + git_url, + dir.as_ref(), + Some(cancellation_token), + )?; + + Ok::<_, BinstallError>(( + load_manifest_from_workspace(dir.as_ref(), &name) + .map_err(BinstallError::from)?, + repo.get_head_commit_hash()?, + )) + } + }) + .await??; + + // Git operation done, disarm it + cancel_on_drop.disarm(); + + ( + ret, + CrateSource { + source_type: SourceType::Git, + url: MaybeOwned::Owned(Url::parse(&format!("{git_url}#{commit_hash}"))?), + }, + ) + } + None => ( + Box::pin( + opts.registry + .fetch_crate_matched(client, &name, version_req), + ) + .await?, + opts.registry.crate_source()?, + ), + }; + + let Some(mut package) = manifest.package else { + return Err(BinstallError::CargoTomlMissingPackage(name)); + }; + + let new_version_str = package.version().to_compact_string(); + let new_version = match Version::parse(&new_version_str) { + Ok(new_version) => new_version, + Err(err) => { + return Err(Box::new(VersionParseError { + v: new_version_str, + err, + }) + .into()) + } + }; + + if let Some(curr_version) = curr_version { + if new_version == curr_version { + info!( + "{} v{curr_version} is already installed, use --force to override", + name + ); + return Ok(None); + } + } + + let (mut meta, binaries): (_, Vec) = ( + package + .metadata + .take() + .and_then(|m| m.binstall) + .unwrap_or_default(), + manifest + .bin + .into_iter() + .filter_map(|p| { + p.name.map(|name| Bin { + name, + required_features: p.required_features, + }) + }) + .collect(), + ); + + // Check binaries + if binaries.is_empty() { + Err(BinstallError::UnspecifiedBinaries) + } else { + Ok(Some(Self { + overrides: mem::take(&mut meta.overrides), + meta, + binaries, + name, + source, + version_str: new_version_str, + version: new_version, + repo: package.repository().map(ToString::to_string), + })) + } + } +} + +/// Load binstall metadata from the crate `Cargo.toml` at the provided path +/// +/// This is a blocking function. +pub fn load_manifest_path, N: AsRef>( + manifest_path: P, + name: N, +) -> Result, BinstallError> { + fn inner(manifest_path: &Path, crate_name: &str) -> Result, BinstallError> { + debug!( + "Reading crate {crate_name} manifest at local path: {}", + manifest_path.display() + ); + + // Load and parse manifest (this checks file system for binary output names) + let manifest = load_manifest_from_workspace(manifest_path, crate_name)?; + + // Return metadata + Ok(manifest) + } + + inner(manifest_path.as_ref(), name.as_ref()) +} diff --git a/crates/binstalk/src/ops/resolve/crate_name.rs b/crates/binstalk/src/ops/resolve/crate_name.rs new file mode 100644 index 00000000..775c1f33 --- /dev/null +++ b/crates/binstalk/src/ops/resolve/crate_name.rs @@ -0,0 +1,111 @@ +use std::{fmt, str::FromStr}; + +use compact_str::CompactString; +use itertools::Itertools; +use semver::{Error, VersionReq}; + +use super::version_ext::VersionReqExt; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct CrateName { + pub name: CompactString, + pub version_req: Option, +} + +impl fmt::Display for CrateName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name)?; + + if let Some(version) = &self.version_req { + write!(f, "@{version}")?; + } + + Ok(()) + } +} + +impl FromStr for CrateName { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(if let Some((name, version)) = s.split_once('@') { + CrateName { + name: name.into(), + version_req: Some(VersionReq::parse_from_cli(version)?), + } + } else { + CrateName { + name: s.into(), + version_req: None, + } + }) + } +} + +impl CrateName { + pub fn dedup(mut crate_names: Vec) -> impl Iterator { + crate_names.sort_by(|x, y| x.name.cmp(&y.name)); + crate_names.into_iter().coalesce(|previous, current| { + if previous.name == current.name { + Ok(current) + } else { + Err((previous, current)) + } + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! assert_dedup { + ([ $( ( $input_name:expr, $input_version:expr ) ),* ], [ $( ( $output_name:expr, $output_version:expr ) ),* ]) => { + let input_crate_names = vec![$( CrateName { + name: $input_name.into(), + version_req: Some($input_version.parse().unwrap()) + }, )*]; + + let mut output_crate_names: Vec = vec![$( CrateName { + name: $output_name.into(), version_req: Some($output_version.parse().unwrap()) + }, )*]; + output_crate_names.sort_by(|x, y| x.name.cmp(&y.name)); + + let crate_names: Vec<_> = CrateName::dedup(input_crate_names).collect(); + assert_eq!(crate_names, output_crate_names); + }; + } + + #[test] + fn test_dedup() { + // Base case 0: Empty input + assert_dedup!([], []); + + // Base case 1: With only one input + assert_dedup!([("a", "1")], [("a", "1")]); + + // Base Case 2: Only has duplicate names + assert_dedup!([("a", "1"), ("a", "2")], [("a", "2")]); + + // Complex Case 0: Having two crates + assert_dedup!( + [("a", "10"), ("b", "3"), ("a", "0"), ("b", "0"), ("a", "1")], + [("a", "1"), ("b", "0")] + ); + + // Complex Case 1: Having three crates + assert_dedup!( + [ + ("d", "1.1"), + ("a", "10"), + ("b", "3"), + ("d", "230"), + ("a", "0"), + ("b", "0"), + ("a", "1"), + ("d", "23") + ], + [("a", "1"), ("b", "0"), ("d", "23")] + ); + } +} diff --git a/crates/binstalk/src/ops/resolve/resolution.rs b/crates/binstalk/src/ops/resolve/resolution.rs new file mode 100644 index 00000000..0e53dbc9 --- /dev/null +++ b/crates/binstalk/src/ops/resolve/resolution.rs @@ -0,0 +1,255 @@ +use std::{borrow::Cow, env, ffi::OsStr, fmt, iter, path::Path, sync::Arc}; + +use binstalk_bins::BinFile; +use command_group::AsyncCommandGroup; +use compact_str::{CompactString, ToCompactString}; +use either::Either; +use itertools::Itertools; +use semver::Version; +use tokio::process::Command; +use tracing::{debug, error, info, warn}; + +use crate::{ + bins, + errors::BinstallError, + fetchers::Fetcher, + manifests::crate_info::{CrateInfo, CrateSource}, + ops::Options, +}; + +pub struct ResolutionFetch { + pub fetcher: Arc, + pub new_version: Version, + pub name: CompactString, + pub version_req: CompactString, + pub bin_files: Vec, + pub source: CrateSource, +} + +pub struct ResolutionSource { + pub name: CompactString, + pub version: CompactString, +} + +pub enum Resolution { + Fetch(Box), + InstallFromSource(ResolutionSource), + AlreadyUpToDate, +} + +impl Resolution { + pub fn print(&self, opts: &Options) { + match self { + Resolution::Fetch(fetch) => { + fetch.print(opts); + } + Resolution::InstallFromSource(source) => { + source.print(); + } + Resolution::AlreadyUpToDate => (), + } + } +} + +impl ResolutionFetch { + pub fn install(self, opts: &Options) -> Result { + let crate_name = self.name.clone(); + self.install_inner(opts) + .map_err(|err| err.crate_context(crate_name)) + } + + fn install_inner(self, opts: &Options) -> Result { + type InstallFp = fn(&bins::BinFile) -> Result<(), bins::Error>; + + let (install_bin, install_link): (InstallFp, InstallFp) = match (opts.no_track, opts.force) + { + (true, true) | (false, _) => (bins::BinFile::install_bin, bins::BinFile::install_link), + (true, false) => ( + bins::BinFile::install_bin_noclobber, + bins::BinFile::install_link_noclobber, + ), + }; + + info!("Installing binaries..."); + for file in &self.bin_files { + install_bin(file)?; + } + + // Generate symlinks + if !opts.no_symlinks { + for file in &self.bin_files { + install_link(file)?; + } + } + + Ok(CrateInfo { + name: self.name, + version_req: self.version_req, + current_version: self.new_version, + source: self.source, + target: self.fetcher.target().to_compact_string(), + bins: Self::resolve_bins(&opts.bins, self.bin_files), + }) + } + + fn resolve_bins( + user_specified_bins: &Option>, + crate_bin_files: Vec, + ) -> Vec { + // We need to filter crate_bin_files by user_specified_bins in case the prebuilt doesn't + // have featured-gated (optional) binary (gated behind feature). + crate_bin_files + .into_iter() + .map(|bin| bin.base_name) + .filter(|bin_name| { + user_specified_bins + .as_ref() + .map_or(true, |bins| bins.binary_search(bin_name).is_ok()) + }) + .collect() + } + + pub fn print(&self, opts: &Options) { + let fetcher = &self.fetcher; + let bin_files = &self.bin_files; + let name = &self.name; + let new_version = &self.new_version; + let target = fetcher.target(); + + debug!( + "Found a binary install source: {} ({target})", + fetcher.source_name(), + ); + + warn!( + "The package {name} v{new_version} ({target}) has been downloaded from {}{}", + if fetcher.is_third_party() { + "third-party source " + } else { + "" + }, + fetcher.source_name() + ); + + info!("This will install the following binaries:"); + for file in bin_files { + info!(" - {}", file.preview_bin()); + } + + if !opts.no_symlinks { + info!("And create (or update) the following symlinks:"); + for file in bin_files { + info!(" - {}", file.preview_link()); + } + } + } +} + +impl ResolutionSource { + pub async fn install(self, opts: Arc) -> Result<(), BinstallError> { + let crate_name = self.name.clone(); + self.install_inner(opts) + .await + .map_err(|err| err.crate_context(crate_name)) + } + + async fn install_inner(self, opts: Arc) -> Result<(), BinstallError> { + let target = if let Some(targets) = opts.desired_targets.get_initialized() { + Some(targets.first().ok_or(BinstallError::NoViableTargets)?) + } else { + None + }; + + let name = &self.name; + let version = &self.version; + + let cargo = env::var_os("CARGO") + .map(Cow::Owned) + .unwrap_or_else(|| Cow::Borrowed(OsStr::new("cargo"))); + + let mut cmd = Command::new(cargo); + + cmd.arg("install") + .arg(name) + .arg("--version") + .arg(version) + .kill_on_drop(true); + + if let Some(target) = target { + cmd.arg("--target").arg(target); + } + + if opts.quiet { + cmd.arg("--quiet"); + } + + if opts.force { + cmd.arg("--force"); + } + + if opts.locked { + cmd.arg("--locked"); + } + + if let Some(cargo_root) = &opts.cargo_root { + cmd.arg("--root").arg(cargo_root); + } + + if opts.no_track { + cmd.arg("--no-track"); + } + + if let Some(bins) = &opts.bins { + for bin in bins { + cmd.arg("--bin").arg(bin); + } + } + + debug!("Running `{}`", format_cmd(&cmd)); + + if !opts.dry_run { + let mut child = opts + .jobserver_client + .get() + .await? + .configure_and_run(&mut cmd, |cmd| cmd.group_spawn())?; + + debug!("Spawned command pid={:?}", child.id()); + + let status = child.wait().await?; + if status.success() { + info!("Cargo finished successfully"); + Ok(()) + } else { + error!("Cargo errored! {status:?}"); + Err(BinstallError::SubProcess { + command: format_cmd(&cmd).to_string().into_boxed_str(), + status, + }) + } + } else { + info!("Dry-run: running `{}`", format_cmd(&cmd)); + Ok(()) + } + } + + pub fn print(&self) { + warn!( + "The package {} v{} will be installed from source (with cargo)", + self.name, self.version + ) + } +} + +fn format_cmd(cmd: &Command) -> impl fmt::Display + '_ { + let cmd = cmd.as_std(); + + let program = Either::Left(Path::new(cmd.get_program()).display()); + + let program_args = cmd + .get_args() + .map(OsStr::to_string_lossy) + .map(Either::Right); + + iter::once(program).chain(program_args).format(" ") +} diff --git a/crates/binstalk/src/ops/resolve/version_ext.rs b/crates/binstalk/src/ops/resolve/version_ext.rs new file mode 100644 index 00000000..7c83e045 --- /dev/null +++ b/crates/binstalk/src/ops/resolve/version_ext.rs @@ -0,0 +1,99 @@ +use compact_str::format_compact; +use semver::{Prerelease, Version, VersionReq}; + +/// Extension trait for [`VersionReq`]. +pub trait VersionReqExt { + /// Return `true` if `self.matches(version)` returns `true` + /// and the `version` is the latest one acceptable by `self`. + fn is_latest_compatible(&self, version: &Version) -> bool; + + /// Parse from CLI option. + /// + /// Notably, a bare version is treated as if preceded by `=`, not by `^` as in Cargo.toml + /// dependencies. + fn parse_from_cli(str: &str) -> Result + where + Self: Sized; +} + +impl VersionReqExt for VersionReq { + fn is_latest_compatible(&self, version: &Version) -> bool { + if !self.matches(version) { + return false; + } + + // Test if bumping patch will be accepted + let bumped_version = Version::new(version.major, version.minor, version.patch + 1); + + if self.matches(&bumped_version) { + return false; + } + + // Test if bumping prerelease will be accepted if version has one. + let pre = &version.pre; + if !pre.is_empty() { + // Bump pre by appending random number to the end. + let bumped_pre = format_compact!("{}.1", pre.as_str()); + + let bumped_version = Version { + major: version.major, + minor: version.minor, + patch: version.patch, + pre: Prerelease::new(&bumped_pre).unwrap(), + build: Default::default(), + }; + + if self.matches(&bumped_version) { + return false; + } + } + + true + } + + fn parse_from_cli(version: &str) -> Result { + if version + .chars() + .next() + .map(|ch| ch.is_ascii_digit()) + .unwrap_or(false) + { + format_compact!("={version}").parse() + } else { + version.parse() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test() { + // Test star + assert!(!VersionReq::STAR.is_latest_compatible(&Version::parse("0.0.1").unwrap())); + assert!(!VersionReq::STAR.is_latest_compatible(&Version::parse("0.1.1").unwrap())); + assert!(!VersionReq::STAR.is_latest_compatible(&Version::parse("0.1.1-alpha").unwrap())); + + // Test ^x.y.z + assert!(!VersionReq::parse("^0.1") + .unwrap() + .is_latest_compatible(&Version::parse("0.1.99").unwrap())); + + // Test =x.y.z + assert!(VersionReq::parse("=0.1.0") + .unwrap() + .is_latest_compatible(&Version::parse("0.1.0").unwrap())); + + // Test =x.y.z-alpha + assert!(VersionReq::parse("=0.1.0-alpha") + .unwrap() + .is_latest_compatible(&Version::parse("0.1.0-alpha").unwrap())); + + // Test >=x.y.z-alpha + assert!(!VersionReq::parse(">=0.1.0-alpha") + .unwrap() + .is_latest_compatible(&Version::parse("0.1.0-alpha").unwrap())); + } +} diff --git a/crates/binstalk/tests/parse-meta.Cargo.toml b/crates/binstalk/tests/parse-meta.Cargo.toml new file mode 100644 index 00000000..26d6e052 --- /dev/null +++ b/crates/binstalk/tests/parse-meta.Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "cargo-binstall-test" +repository = "https://github.com/cargo-bins/cargo-binstall" +version = "1.2.3" + +[[bin]] +name = "cargo-binstall" +path = "src/main.rs" +edition = "2021" + +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ target }.{ archive-format }" +bin-dir = "{ bin }{ binary-ext }" + +[package.metadata.binstall.overrides.x86_64-pc-windows-msvc] +pkg-fmt = "zip" +[package.metadata.binstall.overrides.x86_64-apple-darwin] +pkg-fmt = "zip" diff --git a/crates/binstalk/tests/parse-meta.rs b/crates/binstalk/tests/parse-meta.rs new file mode 100644 index 00000000..72da08f7 --- /dev/null +++ b/crates/binstalk/tests/parse-meta.rs @@ -0,0 +1,31 @@ +use binstalk::ops::resolve::load_manifest_path; +use cargo_toml_workspace::cargo_toml::{Edition, Product}; +use std::path::PathBuf; + +#[test] +fn parse_meta() { + let mut manifest_dir = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()); + manifest_dir.push("tests/parse-meta.Cargo.toml"); + + let manifest = + load_manifest_path(&manifest_dir, "cargo-binstall-test").expect("Error parsing metadata"); + let package = manifest.package.unwrap(); + let meta = package.metadata.and_then(|m| m.binstall).unwrap(); + + assert_eq!(&package.name, "cargo-binstall-test"); + + assert_eq!( + meta.pkg_url.as_deref().unwrap(), + "{ repo }/releases/download/v{ version }/{ name }-{ target }.{ archive-format }" + ); + + assert_eq!( + manifest.bin.as_slice(), + &[Product { + name: Some("cargo-binstall".to_string()), + path: Some("src/main.rs".to_string()), + edition: Some(Edition::E2021), + ..Default::default() + },], + ); +} diff --git a/crates/cargo-toml-workspace/CHANGELOG.md b/crates/cargo-toml-workspace/CHANGELOG.md new file mode 100644 index 00000000..a0846372 --- /dev/null +++ b/crates/cargo-toml-workspace/CHANGELOG.md @@ -0,0 +1,62 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [7.0.6](https://github.com/cargo-bins/cargo-binstall/compare/cargo-toml-workspace-v7.0.5...cargo-toml-workspace-v7.0.6) - 2025-03-15 + +### Other + +- *(deps)* bump the deps group with 2 updates ([#2084](https://github.com/cargo-bins/cargo-binstall/pull/2084)) + +## [7.0.5](https://github.com/cargo-bins/cargo-binstall/compare/cargo-toml-workspace-v7.0.4...cargo-toml-workspace-v7.0.5) - 2025-03-07 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#2072](https://github.com/cargo-bins/cargo-binstall/pull/2072)) + +## [7.0.4](https://github.com/cargo-bins/cargo-binstall/compare/cargo-toml-workspace-v7.0.3...cargo-toml-workspace-v7.0.4) - 2025-01-19 + +### Other + +- update Cargo.lock dependencies + +## [7.0.3](https://github.com/cargo-bins/cargo-binstall/compare/cargo-toml-workspace-v7.0.2...cargo-toml-workspace-v7.0.3) - 2025-01-13 + +### Other + +- update Cargo.lock dependencies + +## [7.0.2](https://github.com/cargo-bins/cargo-binstall/compare/cargo-toml-workspace-v7.0.1...cargo-toml-workspace-v7.0.2) - 2025-01-11 + +### Other + +- *(deps)* bump the deps group with 3 updates (#2015) + +## [7.0.1](https://github.com/cargo-bins/cargo-binstall/compare/cargo-toml-workspace-v7.0.0...cargo-toml-workspace-v7.0.1) - 2024-12-14 + +### Other + +- *(deps)* bump the deps group with 2 updates (#1997) + +## [7.0.0](https://github.com/cargo-bins/cargo-binstall/compare/cargo-toml-workspace-v6.0.3...cargo-toml-workspace-v7.0.0) - 2024-12-07 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1993](https://github.com/cargo-bins/cargo-binstall/pull/1993)) + +## [6.0.3](https://github.com/cargo-bins/cargo-binstall/compare/cargo-toml-workspace-v6.0.2...cargo-toml-workspace-v6.0.3) - 2024-11-09 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1966](https://github.com/cargo-bins/cargo-binstall/pull/1966)) + +## [6.0.2](https://github.com/cargo-bins/cargo-binstall/compare/cargo-toml-workspace-v6.0.1...cargo-toml-workspace-v6.0.2) - 2024-11-05 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1954](https://github.com/cargo-bins/cargo-binstall/pull/1954)) diff --git a/crates/cargo-toml-workspace/Cargo.toml b/crates/cargo-toml-workspace/Cargo.toml new file mode 100644 index 00000000..0a7c62ac --- /dev/null +++ b/crates/cargo-toml-workspace/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "cargo-toml-workspace" +version = "7.0.6" +edition = "2021" +description = "Parse cargo workspace and load specific crate" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/cargo-toml-workspace" +rust-version = "1.65.0" +authors = ["Jiahao XU "] +license = "Apache-2.0 OR MIT" + +[dependencies] +cargo_toml = "0.22.1" +compact_str = { version = "0.9.0", features = ["serde"] } +glob = "0.3.1" +normalize-path = { version = "0.2.1", path = "../normalize-path" } +serde = "1.0.163" +thiserror = "2.0.11" +tracing = "0.1.39" + +[dev-dependencies] +tempfile = "3.5.0" diff --git a/crates/cargo-toml-workspace/LICENSE-APACHE b/crates/cargo-toml-workspace/LICENSE-APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/crates/cargo-toml-workspace/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/cargo-toml-workspace/LICENSE-MIT b/crates/cargo-toml-workspace/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/crates/cargo-toml-workspace/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/crates/cargo-toml-workspace/src/lib.rs b/crates/cargo-toml-workspace/src/lib.rs new file mode 100644 index 00000000..16c8050d --- /dev/null +++ b/crates/cargo-toml-workspace/src/lib.rs @@ -0,0 +1,272 @@ +use std::{ + io, mem, + path::{Path, PathBuf}, +}; + +use cargo_toml::{Error as CargoTomlError, Manifest}; +use compact_str::CompactString; +use glob::PatternError; +use normalize_path::NormalizePath; +use serde::de::DeserializeOwned; +use thiserror::Error as ThisError; +use tracing::{debug, instrument, warn}; + +pub use cargo_toml; + +/// Load binstall metadata `Cargo.toml` from workspace at the provided path +/// +/// WARNING: This is a blocking operation. +/// +/// * `workspace_path` - can be a directory (path to workspace) or +/// a file (path to `Cargo.toml`). +pub fn load_manifest_from_workspace( + workspace_path: impl AsRef, + crate_name: impl AsRef, +) -> Result, Error> { + fn inner( + workspace_path: &Path, + crate_name: &str, + ) -> Result, Error> { + load_manifest_from_workspace_inner(workspace_path, crate_name).map_err(|inner| Error { + workspace_path: workspace_path.into(), + crate_name: crate_name.into(), + inner, + }) + } + + inner(workspace_path.as_ref(), crate_name.as_ref()) +} + +#[derive(Debug, ThisError)] +#[error("Failed to load {crate_name} from {}: {inner}", workspace_path.display())] +pub struct Error { + workspace_path: Box, + crate_name: CompactString, + #[source] + inner: ErrorInner, +} + +#[derive(Debug, ThisError)] +enum ErrorInner { + #[error("Invalid pattern in workspace.members or workspace.exclude: {0}")] + PatternError(#[from] PatternError), + + #[error("Invalid pattern `{0}`: It must be relative and point within current dir")] + InvalidPatternError(CompactString), + + #[error("Failed to parse cargo manifest: {0}")] + CargoManifest(#[from] CargoTomlError), + + #[error("I/O error: {0}")] + Io(#[from] io::Error), + + #[error("Not found")] + NotFound, +} + +#[instrument] +fn load_manifest_from_workspace_inner( + workspace_path: &Path, + crate_name: &str, +) -> Result, ErrorInner> { + debug!( + "Loading manifest of crate {crate_name} from workspace: {}", + workspace_path.display() + ); + + let manifest_path = if workspace_path.is_file() { + workspace_path.to_owned() + } else { + workspace_path.join("Cargo.toml") + }; + + let mut manifest_paths = vec![manifest_path]; + + while let Some(manifest_path) = manifest_paths.pop() { + let manifest = Manifest::::from_path_with_metadata(&manifest_path)?; + + let name = manifest.package.as_ref().map(|p| &*p.name); + debug!( + "Loading from {}, manifest.package.name = {:#?}", + manifest_path.display(), + name + ); + + if name == Some(crate_name) { + return Ok(manifest); + } + + if let Some(ws) = manifest.workspace { + let excludes = ws.exclude; + let members = ws.members; + + if members.is_empty() { + continue; + } + + let exclude_patterns = excludes + .into_iter() + .map(|pat| Pattern::new(&pat)) + .collect::, _>>()?; + + let workspace_path = manifest_path.parent().unwrap(); + + for member in members { + for path in Pattern::new(&member)?.glob_dirs(workspace_path)? { + if !exclude_patterns + .iter() + .any(|exclude| exclude.matches_with_trailing(&path)) + { + manifest_paths.push(workspace_path.join(path).join("Cargo.toml")); + } + } + } + } + } + + Err(ErrorInner::NotFound) +} + +struct Pattern(Vec); + +impl Pattern { + fn new(pat: &str) -> Result { + Path::new(pat) + .try_normalize() + .ok_or_else(|| ErrorInner::InvalidPatternError(pat.into()))? + .iter() + .map(|c| glob::Pattern::new(c.to_str().unwrap())) + .collect::, _>>() + .map_err(Into::into) + .map(Self) + } + + /// * `glob_path` - path to dir to glob for + /// + /// return paths relative to `glob_path`. + fn glob_dirs(&self, glob_path: &Path) -> Result, ErrorInner> { + let mut paths = vec![PathBuf::new()]; + + for pattern in &self.0 { + if paths.is_empty() { + break; + } + + for path in mem::take(&mut paths) { + let p = glob_path.join(&path); + let res = p.read_dir(); + if res.is_err() && !p.is_dir() { + continue; + } + drop(p); + + for res in res? { + let entry = res?; + + let is_dir = entry + .file_type() + .map(|file_type| file_type.is_dir() || file_type.is_symlink()) + .unwrap_or(false); + if !is_dir { + continue; + } + + let filename = entry.file_name(); + if filename != "." // Ignore current dir + && filename != ".." // Ignore parent dir + && pattern.matches(&filename.to_string_lossy()) + { + paths.push(path.join(filename)); + } + } + } + } + + Ok(paths) + } + + /// Return `true` if `path` matches the pattern. + /// It will still return `true` even if there are some trailing components. + fn matches_with_trailing(&self, path: &Path) -> bool { + let mut iter = path.iter().map(|os_str| os_str.to_string_lossy()); + for pattern in &self.0 { + match iter.next() { + Some(s) if pattern.matches(&s) => (), + _ => return false, + } + } + true + } +} + +#[cfg(test)] +mod test { + use std::fs::create_dir_all as mkdir; + + use tempfile::TempDir; + + use super::*; + + #[test] + fn test_glob_dirs() { + let pattern = Pattern::new("*/*/q/*").unwrap(); + let tempdir = TempDir::new().unwrap(); + + mkdir(tempdir.as_ref().join("a/b/c/efe")).unwrap(); + mkdir(tempdir.as_ref().join("a/b/q/ww")).unwrap(); + mkdir(tempdir.as_ref().join("d/233/q/d")).unwrap(); + + let mut paths = pattern.glob_dirs(tempdir.as_ref()).unwrap(); + paths.sort_unstable(); + assert_eq!( + paths, + vec![PathBuf::from("a/b/q/ww"), PathBuf::from("d/233/q/d")] + ); + } + + #[test] + fn test_matches_with_trailing() { + let pattern = Pattern::new("*/*/q/*").unwrap(); + + assert!(pattern.matches_with_trailing(Path::new("a/b/q/d/"))); + assert!(pattern.matches_with_trailing(Path::new("a/b/q/d"))); + assert!(pattern.matches_with_trailing(Path::new("a/b/q/d/234"))); + assert!(pattern.matches_with_trailing(Path::new("a/234/q/d/234"))); + + assert!(!pattern.matches_with_trailing(Path::new(""))); + assert!(!pattern.matches_with_trailing(Path::new("a/"))); + assert!(!pattern.matches_with_trailing(Path::new("a/234"))); + assert!(!pattern.matches_with_trailing(Path::new("a/234/q"))); + } + + #[test] + fn test_load() { + let p = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .parent() + .unwrap() + .join("e2e-tests/manifests/workspace"); + + let manifest = + load_manifest_from_workspace::(&p, "cargo-binstall").unwrap(); + let package = manifest.package.unwrap(); + assert_eq!(package.name, "cargo-binstall"); + assert_eq!(package.version.as_ref().unwrap(), "0.12.0"); + assert_eq!(manifest.bin.len(), 1); + assert_eq!(manifest.bin[0].name.as_deref().unwrap(), "cargo-binstall"); + assert_eq!(manifest.bin[0].path.as_deref().unwrap(), "src/main.rs"); + + let err = load_manifest_from_workspace_inner::(&p, "cargo-binstall2") + .unwrap_err(); + assert!(matches!(err, ErrorInner::NotFound), "{:#?}", err); + + let manifest = + load_manifest_from_workspace::(&p, "cargo-watch").unwrap(); + let package = manifest.package.unwrap(); + assert_eq!(package.name, "cargo-watch"); + assert_eq!(package.version.as_ref().unwrap(), "8.4.0"); + assert_eq!(manifest.bin.len(), 1); + assert_eq!(manifest.bin[0].name.as_deref().unwrap(), "cargo-watch"); + } +} diff --git a/crates/detect-targets/CHANGELOG.md b/crates/detect-targets/CHANGELOG.md new file mode 100644 index 00000000..c8768ca6 --- /dev/null +++ b/crates/detect-targets/CHANGELOG.md @@ -0,0 +1,213 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.52](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.51...detect-targets-v0.1.52) - 2025-06-10 + +### Other + +- update Cargo.lock dependencies + +## [0.1.51](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.50...detect-targets-v0.1.51) - 2025-06-06 + +### Other + +- update Cargo.lock dependencies + +## [0.1.50](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.49...detect-targets-v0.1.50) - 2025-05-30 + +### Other + +- update Cargo.lock dependencies + +## [0.1.49](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.48...detect-targets-v0.1.49) - 2025-05-16 + +### Other + +- update Cargo.lock dependencies + +## [0.1.48](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.47...detect-targets-v0.1.48) - 2025-05-07 + +### Other + +- Fix glibc detection on ubuntu 24.02 ([#2143](https://github.com/cargo-bins/cargo-binstall/pull/2143)) + +## [0.1.47](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.46...detect-targets-v0.1.47) - 2025-04-05 + +### Other + +- update Cargo.lock dependencies + +## [0.1.46](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.45...detect-targets-v0.1.46) - 2025-03-19 + +### Other + +- Fix clippy warnings for detect-targets and binstalk-downloader ([#2098](https://github.com/cargo-bins/cargo-binstall/pull/2098)) + +## [0.1.45](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.44...detect-targets-v0.1.45) - 2025-03-15 + +### Other + +- *(deps)* bump tokio from 1.43.0 to 1.44.0 in the deps group ([#2079](https://github.com/cargo-bins/cargo-binstall/pull/2079)) + +## [0.1.44](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.43...detect-targets-v0.1.44) - 2025-03-07 + +### Other + +- Fix detect-targets musl fallback for android and alpine ([#2076](https://github.com/cargo-bins/cargo-binstall/pull/2076)) + +## [0.1.43](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.42...detect-targets-v0.1.43) - 2025-02-28 + +### Other + +- update Cargo.lock dependencies + +## [0.1.42](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.41...detect-targets-v0.1.42) - 2025-02-22 + +### Other + +- update Cargo.lock dependencies + +## [0.1.41](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.40...detect-targets-v0.1.41) - 2025-02-15 + +### Other + +- update Cargo.lock dependencies + +## [0.1.40](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.39...detect-targets-v0.1.40) - 2025-02-11 + +### Other + +- update Cargo.lock dependencies + +## [0.1.39](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.38...detect-targets-v0.1.39) - 2025-02-04 + +### Other + +- update Cargo.lock dependencies + +## [0.1.38](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.37...detect-targets-v0.1.38) - 2025-01-19 + +### Other + +- update Cargo.lock dependencies + +## [0.1.37](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.36...detect-targets-v0.1.37) - 2025-01-13 + +### Other + +- update Cargo.lock dependencies + +## [0.1.36](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.35...detect-targets-v0.1.36) - 2025-01-11 + +### Other + +- update Cargo.lock dependencies + +## [0.1.35](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.34...detect-targets-v0.1.35) - 2025-01-04 + +### Other + +- update Cargo.lock dependencies + +## [0.1.34](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.33...detect-targets-v0.1.34) - 2024-12-28 + +### Other + +- update Cargo.lock dependencies + +## [0.1.33](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.32...detect-targets-v0.1.33) - 2024-12-14 + +### Other + +- update Cargo.lock dependencies + +## [0.1.32](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.31...detect-targets-v0.1.32) - 2024-12-07 + +### Other + +- update Cargo.lock dependencies + +## [0.1.31](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.30...detect-targets-v0.1.31) - 2024-11-29 + +### Other + +- update Cargo.lock dependencies + +## [0.1.30](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.29...detect-targets-v0.1.30) - 2024-11-23 + +### Other + +- update Cargo.lock dependencies + +## [0.1.29](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.28...detect-targets-v0.1.29) - 2024-11-18 + +### Other + +- update Cargo.lock dependencies + +## [0.1.28](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.27...detect-targets-v0.1.28) - 2024-11-09 + +### Other + +- update Cargo.lock dependencies + +## [0.1.27](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.26...detect-targets-v0.1.27) - 2024-11-05 + +### Other + +- update Cargo.lock dependencies + +## [0.1.26](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.25...detect-targets-v0.1.26) - 2024-11-02 + +### Other + +- update Cargo.lock dependencies + +## [0.1.25](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.24...detect-targets-v0.1.25) - 2024-10-25 + +### Other + +- update Cargo.lock dependencies + +## [0.1.24](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.23...detect-targets-v0.1.24) - 2024-10-12 + +### Other + +- update Cargo.lock dependencies + +## [0.1.23](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.22...detect-targets-v0.1.23) - 2024-10-04 + +### Other + +- update Cargo.lock dependencies + +## [0.1.22](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.21...detect-targets-v0.1.22) - 2024-09-22 + +### Other + +- update Cargo.lock dependencies + +## [0.1.21](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.20...detect-targets-v0.1.21) - 2024-09-06 + +### Other +- update Cargo.lock dependencies + +## [0.1.20](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.19...detect-targets-v0.1.20) - 2024-08-25 + +### Other +- update Cargo.lock dependencies + +## [0.1.19](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.18...detect-targets-v0.1.19) - 2024-08-10 + +### Other +- update Cargo.lock dependencies + +## [0.1.18](https://github.com/cargo-bins/cargo-binstall/compare/detect-targets-v0.1.17...detect-targets-v0.1.18) - 2024-08-04 + +### Other +- *(deps)* bump the deps group across 1 directory with 2 updates ([#1859](https://github.com/cargo-bins/cargo-binstall/pull/1859)) diff --git a/crates/detect-targets/Cargo.toml b/crates/detect-targets/Cargo.toml new file mode 100644 index 00000000..e9380991 --- /dev/null +++ b/crates/detect-targets/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "detect-targets" +description = "Detect the target of the env at runtime" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/detect-targets" +version = "0.1.52" +rust-version = "1.62.0" +authors = ["Jiahao XU "] +edition = "2021" +license = "Apache-2.0 OR MIT" + +[dependencies] +tokio = { version = "1.44.0", features = [ + "rt", + "process", + "sync", +], default-features = false } +tracing = { version = "0.1.39", optional = true } +tracing-subscriber = { version = "0.3.17", features = [ + "fmt", +], default-features = false, optional = true } +cfg-if = "1.0.0" +guess_host_triple = "0.1.3" + +[features] +tracing = ["dep:tracing"] +cli-logging = ["tracing", "dep:tracing-subscriber"] + +[target.'cfg(target_os = "windows")'.dependencies] +windows-sys = { version = "0.60.2", features = [ + "Win32_System_Threading", + "Win32_System_SystemInformation", + "Win32_Foundation", + "Win32_System_LibraryLoader", +] } + +[dev-dependencies] +tokio = { version = "1.44.0", features = ["macros"], default-features = false } + +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/cargo-binstall-{ target }.full.{ archive-format }" diff --git a/crates/detect-targets/LICENSE-APACHE b/crates/detect-targets/LICENSE-APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/crates/detect-targets/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/detect-targets/LICENSE-MIT b/crates/detect-targets/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/crates/detect-targets/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/build.rs b/crates/detect-targets/build.rs similarity index 51% rename from build.rs rename to crates/detect-targets/build.rs index a77a878b..15152d41 100644 --- a/build.rs +++ b/crates/detect-targets/build.rs @@ -1,6 +1,7 @@ - -// Fetch build target and define this for the compiler fn main() { + println!("cargo:rerun-if-changed=build.rs"); + + // Fetch build target and define this for the compiler println!( "cargo:rustc-env=TARGET={}", std::env::var("TARGET").unwrap() diff --git a/crates/detect-targets/src/desired_targets.rs b/crates/detect-targets/src/desired_targets.rs new file mode 100644 index 00000000..36726495 --- /dev/null +++ b/crates/detect-targets/src/desired_targets.rs @@ -0,0 +1,69 @@ +use crate::detect_targets; + +use std::sync::Arc; + +use tokio::sync::OnceCell; + +#[derive(Debug)] +enum DesiredTargetsInner { + AutoDetect(Arc>>), + Initialized(Vec), +} + +#[derive(Debug)] +pub struct DesiredTargets(DesiredTargetsInner); + +impl DesiredTargets { + fn initialized(targets: Vec) -> Self { + Self(DesiredTargetsInner::Initialized(targets)) + } + + fn auto_detect() -> Self { + let arc = Arc::new(OnceCell::new()); + + let once_cell = arc.clone(); + tokio::spawn(async move { + once_cell.get_or_init(detect_targets).await; + }); + + Self(DesiredTargetsInner::AutoDetect(arc)) + } + + pub async fn get(&self) -> &[String] { + use DesiredTargetsInner::*; + + match &self.0 { + Initialized(targets) => targets, + + // This will mostly just wait for the spawned task, + // on rare occausion though, it will poll the future + // returned by `detect_targets`. + AutoDetect(once_cell) => once_cell.get_or_init(detect_targets).await, + } + } + + /// If `DesiredTargets` is provided with a list of desired targets instead + /// of detecting the targets, then this function would return `Some`. + pub fn get_initialized(&self) -> Option<&[String]> { + use DesiredTargetsInner::*; + + match &self.0 { + Initialized(targets) => Some(targets), + AutoDetect(..) => None, + } + } +} + +/// If opts_targets is `Some`, then it will be used. +/// Otherwise, call `detect_targets` using `tokio::spawn` to detect targets. +/// +/// Since `detect_targets` internally spawns a process and wait for it, +/// it's pretty costy, it is recommended to run this fn ASAP and +/// reuse the result. +pub fn get_desired_targets(opts_targets: Option>) -> DesiredTargets { + if let Some(targets) = opts_targets { + DesiredTargets::initialized(targets) + } else { + DesiredTargets::auto_detect() + } +} diff --git a/crates/detect-targets/src/detect.rs b/crates/detect-targets/src/detect.rs new file mode 100644 index 00000000..a3f4a67c --- /dev/null +++ b/crates/detect-targets/src/detect.rs @@ -0,0 +1,115 @@ +use std::{ + borrow::Cow, + env, + ffi::OsStr, + process::{Output, Stdio}, +}; + +use cfg_if::cfg_if; +use tokio::process::Command; +#[cfg(feature = "tracing")] +use tracing::debug; + +cfg_if! { + if #[cfg(any(target_os = "linux", target_os = "android"))] { + mod linux; + } else if #[cfg(target_os = "macos")] { + mod macos; + } else if #[cfg(target_os = "windows")] { + mod windows; + } +} + +/// Detect the targets supported at runtime, +/// which might be different from `TARGET` which is detected +/// at compile-time. +/// +/// Return targets supported in the order of preference. +/// If target_os is linux and it support gnu, then it is preferred +/// to musl. +/// +/// If target_os is mac and it is aarch64, then aarch64 is preferred +/// to x86_64. +/// +/// Check [this issue](https://github.com/ryankurte/cargo-binstall/issues/155) +/// for more information. +pub async fn detect_targets() -> Vec { + let target = get_target_from_rustc().await; + #[cfg(feature = "tracing")] + debug!("get_target_from_rustc()={target:?}"); + let target = target.unwrap_or_else(|| { + let target = guess_host_triple::guess_host_triple(); + #[cfg(feature = "tracing")] + debug!("guess_host_triple::guess_host_triple()={target:?}"); + target.unwrap_or(crate::TARGET).to_string() + }); + + cfg_if! { + if #[cfg(target_os = "macos")] { + let mut targets = vec![target]; + targets.extend(macos::detect_alternative_targets(&targets[0]).await); + targets + } else if #[cfg(target_os = "windows")] { + let mut targets = vec![target]; + targets.extend(windows::detect_alternative_targets(&targets[0])); + targets + } else if #[cfg(any(target_os = "linux", target_os = "android"))] { + // Linux is a bit special, since the result from `guess_host_triple` + // might be wrong about whether glibc or musl is used. + linux::detect_targets(target).await + } else { + vec![target] + } + } +} + +/// Figure out what the host target is using `rustc`. +/// If `rustc` is absent, then it would return `None`. +/// +/// If environment variable `CARGO` is present, then +/// `$CARGO -vV` will be run instead. +/// +/// Otherwise, it will run `rustc -vV` to detect target. +async fn get_target_from_rustc() -> Option { + let cmd = env::var_os("CARGO") + .map(Cow::Owned) + .unwrap_or_else(|| Cow::Borrowed(OsStr::new("rustc"))); + + let Output { status, stdout, .. } = Command::new(cmd) + .arg("-vV") + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .ok()? + .wait_with_output() + .await + .ok()?; + + if !status.success() { + return None; + } + + let stdout = String::from_utf8_lossy(&stdout); + let target = stdout + .lines() + .find_map(|line| line.strip_prefix("host: "))?; + + // The target triplets have the form of 'arch-vendor-system'. + // + // When building for Linux (e.g. the 'system' part is + // 'linux-something'), replace the vendor with 'unknown' + // so that mapping to rust standard targets happens correctly. + // + // For example, alpine set `rustc` host triple to + // `x86_64-alpine-linux-musl`. + // + // Here we use splitn with n=4 since we just need to check + // the third part to see if it equals to "linux" and verify + // that we have at least three parts. + let mut parts: Vec<&str> = target.splitn(4, '-').collect(); + if *parts.get(2)? == "linux" { + parts[1] = "unknown"; + } + Some(parts.join("-")) +} diff --git a/crates/detect-targets/src/detect/linux.rs b/crates/detect-targets/src/detect/linux.rs new file mode 100644 index 00000000..7068db8e --- /dev/null +++ b/crates/detect-targets/src/detect/linux.rs @@ -0,0 +1,172 @@ +use std::{ + process::{Output, Stdio}, + str, +}; + +use tokio::{process::Command, task}; +#[cfg(feature = "tracing")] +use tracing::debug; + +pub(super) async fn detect_targets(target: String) -> Vec { + let (_, postfix) = target + .rsplit_once('-') + .expect("unwrap: target always has a -"); + + let (abi, libc) = if let Some(abi) = postfix.strip_prefix("musl") { + (abi, Libc::Musl) + } else if let Some(abi) = postfix.strip_prefix("gnu") { + (abi, Libc::Gnu) + } else if let Some(abi) = postfix.strip_prefix("android") { + (abi, Libc::Android) + } else { + (postfix, Libc::Unknown) + }; + + let cpu_arch = target + .split_once('-') + .expect("unwrap: target always has a - for cpu_arch") + .0; + + // For android the `-unknown-` is omitted, for alpine it has `-alpine-` + // instead of `-unknown-`. + let musl_fallback_target = || format!("{cpu_arch}-unknown-linux-musl{abi}"); + + match libc { + // guess_host_triple cannot detect whether the system is using glibc, + // musl libc or other libc. + // + // On Alpine, you can use `apk add gcompat` to install glibc + // and run glibc programs. + // + // As such, we need to launch the test ourselves. + Libc::Gnu | Libc::Musl => { + let handles: Vec<_> = { + let cpu_arch_suffix = cpu_arch.replace('_', "-"); + let filename = format!("ld-linux-{cpu_arch_suffix}.so.2"); + let dirname = format!("{cpu_arch}-linux-gnu"); + + [ + format!("/lib/{filename}"), + format!("/lib64/{filename}"), + format!("/lib/{dirname}/{filename}"), + format!("/lib64/{dirname}/{filename}"), + format!("/usr/lib/{dirname}/{filename}"), + format!("/usr/lib64/{dirname}/{filename}"), + format!("/usr/lib/{dirname}/libc.so.6"), + format!("/usr/lib64/{dirname}/libc.so.6"), + format!("/usr/lib/{dirname}/libc.so"), + format!("/usr/lib64/{dirname}/libc.so"), + ] + .into_iter() + .map(|p| AutoAbortHandle(tokio::spawn(is_gnu_ld(p)))) + .collect() + }; + + let has_glibc = async move { + for mut handle in handles { + if let Ok(true) = (&mut handle.0).await { + return true; + } + } + + false + } + .await; + + [ + has_glibc.then(|| format!("{cpu_arch}-unknown-linux-gnu{abi}")), + Some(musl_fallback_target()), + ] + } + Libc::Android | Libc::Unknown => [Some(target.clone()), Some(musl_fallback_target())], + } + .into_iter() + .flatten() + .collect() +} + +async fn is_gnu_ld(cmd: String) -> bool { + get_ld_flavor(&cmd).await == Some(Libc::Gnu) +} + +async fn get_ld_flavor(cmd: &str) -> Option { + let Output { + status, + stdout, + stderr, + } = match Command::new(cmd) + .arg("--version") + .stdin(Stdio::null()) + .output() + .await + { + Ok(output) => output, + Err(_err) => { + #[cfg(feature = "tracing")] + debug!("Running `{cmd} --version`: err={_err:?}"); + return None; + } + }; + + let stdout = String::from_utf8_lossy(&stdout); + let stderr = String::from_utf8_lossy(&stderr); + + #[cfg(feature = "tracing")] + debug!("`{cmd} --version`: status={status}, stdout='{stdout}', stderr='{stderr}'"); + + const ALPINE_GCOMPAT: &str = r#"This is the gcompat ELF interpreter stub. +You are not meant to run this directly. +"#; + + if status.success() { + // Executing glibc ldd or /lib/ld-linux-{cpu_arch}.so.1 will always + // succeeds. + (stdout.contains("GLIBC") || stdout.contains("GNU libc")).then_some(Libc::Gnu) + } else if status.code() == Some(1) { + // On Alpine, executing both the gcompat glibc and the ldd and + // /lib/ld-musl-{cpu_arch}.so.1 will fail with exit status 1. + if stdout == ALPINE_GCOMPAT { + // Alpine's gcompat package will output ALPINE_GCOMPAT to stdout + Some(Libc::Gnu) + } else if stderr.contains("musl libc") { + // Alpine/s ldd and musl dynlib will output to stderr + Some(Libc::Musl) + } else { + None + } + } else if status.code() == Some(127) { + // On Ubuntu 20.04 (glibc 2.31), the `--version` flag is not supported + // and it will exit with status 127. + let status = Command::new(cmd) + .arg("/bin/true") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .await + .ok()?; + + #[cfg(feature = "tracing")] + debug!("`{cmd} --version`: status={status}"); + + status.success().then_some(Libc::Gnu) + } else { + None + } +} + +#[derive(Eq, PartialEq)] +enum Libc { + Gnu, + Musl, + Android, + Unknown, +} + +struct AutoAbortHandle(task::JoinHandle); + +impl Drop for AutoAbortHandle { + fn drop(&mut self) { + self.0.abort(); + } +} diff --git a/crates/detect-targets/src/detect/macos.rs b/crates/detect-targets/src/detect/macos.rs new file mode 100644 index 00000000..ceafb6a7 --- /dev/null +++ b/crates/detect-targets/src/detect/macos.rs @@ -0,0 +1,67 @@ +use std::process::Stdio; + +use tokio::process::Command; + +const AARCH64: &str = "aarch64-apple-darwin"; +const X86: &str = "x86_64-apple-darwin"; +/// https://doc.rust-lang.org/nightly/rustc/platform-support/x86_64h-apple-darwin.html +/// +/// This target is an x86_64 target that only supports Apple's late-gen +/// (Haswell-compatible) Intel chips. +/// +/// It enables a set of target features available on these chips (AVX2 and similar), +/// and MachO binaries built with this target may be used as the x86_64h entry in +/// universal binaries ("fat" MachO binaries), and will fail to load on machines +/// that do not support this. +/// +/// It is similar to x86_64-apple-darwin in nearly all respects, although +/// the minimum supported OS version is slightly higher (it requires 10.8 +/// rather than x86_64-apple-darwin's 10.7). +const X86H: &str = "x86_64h-apple-darwin"; +const UNIVERSAL: &str = "universal-apple-darwin"; +const UNIVERSAL2: &str = "universal2-apple-darwin"; + +async fn is_arch_supported(arch_name: &str) -> bool { + Command::new("arch") + .args(["-arch", arch_name, "/usr/bin/true"]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .await + .map(|exit_status| exit_status.success()) + .unwrap_or(false) +} + +pub(super) async fn detect_alternative_targets(target: &str) -> impl Iterator { + match target { + AARCH64 => { + // Spawn `arch` in parallel (probably from different threads if + // mutlti-thread runtime is used). + // + // These two tasks are never cancelled, so it can only fail due to + // panic, in which cause we would propagate by also panic here. + let x86_64h_task = tokio::spawn(is_arch_supported("x86_64h")); + let x86_64_task = tokio::spawn(is_arch_supported("x86_64")); + [ + // Prefer universal as it provides native arm executable + Some(UNIVERSAL), + Some(UNIVERSAL2), + // Prefer x86h since it is more optimized + x86_64h_task.await.unwrap().then_some(X86H), + x86_64_task.await.unwrap().then_some(X86), + ] + } + X86 => [ + is_arch_supported("x86_64h").await.then_some(X86H), + Some(UNIVERSAL), + Some(UNIVERSAL2), + None, + ], + X86H => [Some(X86), Some(UNIVERSAL), Some(UNIVERSAL2), None], + _ => [None, None, None, None], + } + .into_iter() + .flatten() + .map(ToString::to_string) +} diff --git a/crates/detect-targets/src/detect/windows.rs b/crates/detect-targets/src/detect/windows.rs new file mode 100644 index 00000000..5bb397cb --- /dev/null +++ b/crates/detect-targets/src/detect/windows.rs @@ -0,0 +1,133 @@ +use std::mem; +use windows_sys::Win32::{ + Foundation::{HMODULE, S_OK}, + System::{ + LibraryLoader::{GetProcAddress, LoadLibraryA}, + SystemInformation::{ + IMAGE_FILE_MACHINE, IMAGE_FILE_MACHINE_AMD64, IMAGE_FILE_MACHINE_ARM, + IMAGE_FILE_MACHINE_ARM64, IMAGE_FILE_MACHINE_I386, + }, + Threading::{GetMachineTypeAttributes, UserEnabled, Wow64Container, MACHINE_ATTRIBUTES}, + }, +}; + +struct LibraryHandle(HMODULE); + +impl LibraryHandle { + fn new(name: &[u8]) -> Option { + let handle = unsafe { LoadLibraryA(name.as_ptr() as _) }; + (!handle.is_null()).then_some(Self(handle)) + } + + /// Get a function pointer to a function in the library. + /// # SAFETY + /// + /// The caller must ensure that the function signature matches the actual function. + /// The easiest way to do this is to add an entry to windows_sys_no_link.list and use the + /// generated function for `func_signature`. + /// + /// The function returned cannot be used after the handle is dropped. + unsafe fn get_proc_address(&self, name: &[u8]) -> Option { + let symbol = unsafe { GetProcAddress(self.0, name.as_ptr() as _) }; + symbol.map(|symbol| unsafe { mem::transmute_copy(&symbol) }) + } +} + +type GetMachineTypeAttributesFuncType = + unsafe extern "system" fn(u16, *mut MACHINE_ATTRIBUTES) -> i32; +const _: () = { + // Ensure that our hand-written signature matches the actual function signature. + // We can't use `GetMachineTypeAttributes` outside of a const scope otherwise we'll end up statically linking to + // it, which will fail to load on older versions of Windows. + let _: GetMachineTypeAttributesFuncType = GetMachineTypeAttributes; +}; + +fn is_arch_supported_inner(arch: IMAGE_FILE_MACHINE) -> Option { + // GetMachineTypeAttributes is only available on Win11 22000+, so dynamically load it. + let kernel32 = LibraryHandle::new(b"kernel32.dll\0")?; + // SAFETY: GetMachineTypeAttributesFuncType is checked to match the real function signature. + let get_machine_type_attributes = unsafe { + kernel32.get_proc_address::(b"GetMachineTypeAttributes\0") + }?; + + let mut machine_attributes = mem::MaybeUninit::uninit(); + if unsafe { get_machine_type_attributes(arch, machine_attributes.as_mut_ptr()) } == S_OK { + let machine_attributes = unsafe { machine_attributes.assume_init() }; + Some((machine_attributes & (Wow64Container | UserEnabled)) != 0) + } else { + Some(false) + } +} + +fn is_arch_supported(arch: IMAGE_FILE_MACHINE) -> bool { + is_arch_supported_inner(arch).unwrap_or(false) +} + +pub(super) fn detect_alternative_targets(target: &str) -> impl Iterator { + let (prefix, abi) = target + .rsplit_once('-') + .expect("unwrap: target always has a -"); + + let arch = prefix + .split_once('-') + .expect("unwrap: target always has at least two -") + .0; + + let msvc_fallback_target = (abi != "msvc").then(|| format!("{prefix}-msvc")); + + let gnu_fallback_targets = (abi == "msvc") + .then(|| [format!("{prefix}-gnu"), format!("{prefix}-gnullvm")]) + .into_iter() + .flatten(); + + let x64_fallback_targets = (arch != "x86_64" && is_arch_supported(IMAGE_FILE_MACHINE_AMD64)) + .then_some([ + "x86_64-pc-windows-msvc", + "x86_64-pc-windows-gnu", + "x86_64-pc-windows-gnullvm", + ]) + .into_iter() + .flatten() + .map(ToString::to_string); + + let x86_fallback_targets = (arch != "x86" && is_arch_supported(IMAGE_FILE_MACHINE_I386)) + .then_some([ + "i586-pc-windows-msvc", + "i586-pc-windows-gnu", + "i586-pc-windows-gnullvm", + "i686-pc-windows-msvc", + "i686-pc-windows-gnu", + "i686-pc-windows-gnullvm", + ]) + .into_iter() + .flatten() + .map(ToString::to_string); + + let arm32_fallback_targets = (arch != "thumbv7a" && is_arch_supported(IMAGE_FILE_MACHINE_ARM)) + .then_some([ + "thumbv7a-pc-windows-msvc", + "thumbv7a-pc-windows-gnu", + "thumbv7a-pc-windows-gnullvm", + ]) + .into_iter() + .flatten() + .map(ToString::to_string); + + let arm64_fallback_targets = (arch != "aarch64" && is_arch_supported(IMAGE_FILE_MACHINE_ARM64)) + .then_some([ + "aarch64-pc-windows-msvc", + "aarch64-pc-windows-gnu", + "aarch64-pc-windows-gnullvm", + ]) + .into_iter() + .flatten() + .map(ToString::to_string); + + msvc_fallback_target + .into_iter() + .chain(gnu_fallback_targets) + .chain(x64_fallback_targets) + .chain(x86_fallback_targets) + .chain(arm32_fallback_targets) + .chain(arm64_fallback_targets) +} diff --git a/crates/detect-targets/src/lib.rs b/crates/detect-targets/src/lib.rs new file mode 100644 index 00000000..a9a5c5c4 --- /dev/null +++ b/crates/detect-targets/src/lib.rs @@ -0,0 +1,74 @@ +//! Detect the target at the runtime. +//! +//! It runs `$CARGO -vV` if environment variable `CARGO` is present +//! for cargo subcommands, otherwise it would try running `rustc -vV`. +//! +//! If both `rustc` isn't present on the system, it will fallback +//! to using syscalls plus `ldd` on Linux to detect targets. +//! +//! Example use cases: +//! - The binary is built with musl libc to run on anywhere, but +//! the runtime supports glibc. +//! - The binary is built for x86_64-apple-darwin, but run on +//! aarch64-apple-darwin. +//! +//! This crate provides two API: +//! - [`detect_targets`] provides the API to get the target +//! at runtime, but the code is run on the current thread. +//! - [`get_desired_targets`] provides the API to either +//! use override provided by the users, or run [`detect_targets`] +//! in the background using [`tokio::spawn`]. +//! +//! # Example +//! +//! `detect_targets`: +//! +//! ```rust +//! use detect_targets::detect_targets; +//! # #[tokio::main(flavor = "current_thread")] +//! # async fn main() { +//! +//! let targets = detect_targets().await; +//! eprintln!("Your platform supports targets: {targets:#?}"); +//! # } +//! ``` +//! +//! `get_desired_targets` with user override: +//! +//! ```rust +//! use detect_targets::get_desired_targets; +//! # #[tokio::main(flavor = "current_thread")] +//! # async fn main() { +//! +//! assert_eq!( +//! get_desired_targets(Some(vec![ +//! "x86_64-apple-darwin".to_string(), +//! "aarch64-apple-darwin".to_string(), +//! ])).get().await, +//! &["x86_64-apple-darwin", "aarch64-apple-darwin"], +//! ); +//! # } +//! ``` +//! +//! `get_desired_targets` without user override: +//! +//! ```rust +//! use detect_targets::get_desired_targets; +//! # #[tokio::main(flavor = "current_thread")] +//! # async fn main() { +//! +//! eprintln!( +//! "Your platform supports targets: {:#?}", +//! get_desired_targets(None).get().await +//! ); +//! # } +//! ``` + +mod detect; +pub use detect::detect_targets; + +mod desired_targets; +pub use desired_targets::{get_desired_targets, DesiredTargets}; + +/// Compiled target triple, used as default for binary fetching +pub const TARGET: &str = env!("TARGET"); diff --git a/crates/detect-targets/src/main.rs b/crates/detect-targets/src/main.rs new file mode 100644 index 00000000..2623c077 --- /dev/null +++ b/crates/detect-targets/src/main.rs @@ -0,0 +1,23 @@ +use std::io; + +use detect_targets::detect_targets; +use tokio::runtime; + +fn main() -> io::Result<()> { + #[cfg(feature = "cli-logging")] + tracing_subscriber::fmt::fmt() + .with_max_level(tracing::Level::TRACE) + .with_writer(std::io::stderr) + .init(); + + let targets = runtime::Builder::new_current_thread() + .enable_all() + .build()? + .block_on(detect_targets()); + + for target in targets { + println!("{target}"); + } + + Ok(()) +} diff --git a/crates/detect-wasi/CHANGELOG.md b/crates/detect-wasi/CHANGELOG.md new file mode 100644 index 00000000..a558a87e --- /dev/null +++ b/crates/detect-wasi/CHANGELOG.md @@ -0,0 +1,191 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.0.31](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.30...detect-wasi-v1.0.31) - 2025-06-10 + +### Other + +- update Cargo.lock dependencies + +## [1.0.30](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.29...detect-wasi-v1.0.30) - 2025-05-16 + +### Other + +- update Cargo.lock dependencies + +## [1.0.29](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.28...detect-wasi-v1.0.29) - 2025-05-07 + +### Other + +- update Cargo.lock dependencies + +## [1.0.28](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.27...detect-wasi-v1.0.28) - 2025-04-05 + +### Other + +- update Cargo.lock dependencies + +## [1.0.27](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.26...detect-wasi-v1.0.27) - 2025-03-19 + +### Other + +- update Cargo.lock dependencies + +## [1.0.26](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.25...detect-wasi-v1.0.26) - 2025-03-15 + +### Other + +- update Cargo.lock dependencies + +## [1.0.25](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.24...detect-wasi-v1.0.25) - 2025-03-07 + +### Other + +- update Cargo.lock dependencies + +## [1.0.24](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.23...detect-wasi-v1.0.24) - 2025-02-28 + +### Other + +- update Cargo.lock dependencies + +## [1.0.23](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.22...detect-wasi-v1.0.23) - 2025-02-22 + +### Other + +- update Cargo.lock dependencies + +## [1.0.22](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.21...detect-wasi-v1.0.22) - 2025-02-11 + +### Other + +- update Cargo.lock dependencies + +## [1.0.21](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.20...detect-wasi-v1.0.21) - 2025-02-04 + +### Other + +- update Cargo.lock dependencies + +## [1.0.20](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.19...detect-wasi-v1.0.20) - 2025-01-19 + +### Other + +- update Cargo.lock dependencies + +## [1.0.19](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.18...detect-wasi-v1.0.19) - 2025-01-11 + +### Other + +- update Cargo.lock dependencies + +## [1.0.18](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.17...detect-wasi-v1.0.18) - 2025-01-04 + +### Other + +- update Cargo.lock dependencies + +## [1.0.17](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.16...detect-wasi-v1.0.17) - 2024-12-28 + +### Other + +- update Cargo.lock dependencies + +## [1.0.16](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.15...detect-wasi-v1.0.16) - 2024-12-14 + +### Other + +- update Cargo.lock dependencies + +## [1.0.15](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.14...detect-wasi-v1.0.15) - 2024-12-07 + +### Other + +- update Cargo.lock dependencies + +## [1.0.14](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.13...detect-wasi-v1.0.14) - 2024-11-29 + +### Other + +- update Cargo.lock dependencies + +## [1.0.13](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.12...detect-wasi-v1.0.13) - 2024-11-23 + +### Other + +- update Cargo.lock dependencies + +## [1.0.12](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.11...detect-wasi-v1.0.12) - 2024-11-18 + +### Other + +- update Cargo.lock dependencies + +## [1.0.11](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.10...detect-wasi-v1.0.11) - 2024-11-09 + +### Other + +- update Cargo.lock dependencies + +## [1.0.10](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.9...detect-wasi-v1.0.10) - 2024-11-05 + +### Other + +- update Cargo.lock dependencies + +## [1.0.9](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.8...detect-wasi-v1.0.9) - 2024-11-02 + +### Other + +- update Cargo.lock dependencies + +## [1.0.8](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.7...detect-wasi-v1.0.8) - 2024-10-25 + +### Other + +- update Cargo.lock dependencies + +## [1.0.7](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.6...detect-wasi-v1.0.7) - 2024-10-12 + +### Other + +- update Cargo.lock dependencies + +## [1.0.6](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.5...detect-wasi-v1.0.6) - 2024-10-04 + +### Other + +- update Cargo.lock dependencies + +## [1.0.5](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.4...detect-wasi-v1.0.5) - 2024-09-22 + +### Other + +- update Cargo.lock dependencies + +## [1.0.4](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.3...detect-wasi-v1.0.4) - 2024-09-06 + +### Other +- update Cargo.lock dependencies + +## [1.0.3](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.2...detect-wasi-v1.0.3) - 2024-08-25 + +### Other +- update Cargo.lock dependencies + +## [1.0.2](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.1...detect-wasi-v1.0.2) - 2024-08-10 + +### Other +- update Cargo.lock dependencies + +## [1.0.1](https://github.com/cargo-bins/cargo-binstall/compare/detect-wasi-v1.0.0...detect-wasi-v1.0.1) - 2024-08-04 + +### Other +- Bump tempfile from 3.4.0 to 3.5.0 ([#967](https://github.com/cargo-bins/cargo-binstall/pull/967)) +- Bump tempfile from 3.3.0 to 3.4.0 ([#834](https://github.com/cargo-bins/cargo-binstall/pull/834)) +- Migrate CI and builds to Just, add "full" builds ([#660](https://github.com/cargo-bins/cargo-binstall/pull/660)) diff --git a/crates/detect-wasi/Cargo.toml b/crates/detect-wasi/Cargo.toml new file mode 100644 index 00000000..8af34b2d --- /dev/null +++ b/crates/detect-wasi/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "detect-wasi" +description = "Detect if WASI can be run" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/detect-wasi" +version = "1.0.31" +rust-version = "1.61.0" +authors = ["Félix Saparelli "] +edition = "2021" +license = "Apache-2.0 OR MIT" + +[dependencies] +tempfile = "3.5.0" + +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/cargo-binstall-{ target }.full.{ archive-format }" diff --git a/crates/detect-wasi/LICENSE-APACHE b/crates/detect-wasi/LICENSE-APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/crates/detect-wasi/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/detect-wasi/LICENSE-MIT b/crates/detect-wasi/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/crates/detect-wasi/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/crates/detect-wasi/src/bin/detect-wasi.rs b/crates/detect-wasi/src/bin/detect-wasi.rs new file mode 100644 index 00000000..c5cc8f99 --- /dev/null +++ b/crates/detect-wasi/src/bin/detect-wasi.rs @@ -0,0 +1,13 @@ +use std::process::exit; + +use detect_wasi::detect_wasi_runability; + +fn main() { + if detect_wasi_runability().unwrap() { + println!("WASI is runnable!"); + exit(0); + } else { + println!("WASI is not runnable"); + exit(1); + } +} diff --git a/crates/detect-wasi/src/lib.rs b/crates/detect-wasi/src/lib.rs new file mode 100644 index 00000000..e780276f --- /dev/null +++ b/crates/detect-wasi/src/lib.rs @@ -0,0 +1,42 @@ +use std::{ + fs::File, + io::{Result, Write}, + process::Command, +}; +#[cfg(unix)] +use std::{fs::Permissions, os::unix::fs::PermissionsExt}; + +use tempfile::tempdir; + +const WASI_PROGRAM: &[u8] = include_bytes!("miniwasi.wasm"); + +/// Detect the ability to run WASI +/// +/// This attempts to run a small embedded WASI program, and returns true if no errors happened. +/// Errors returned by the `Result` are I/O errors from the establishment of the context, not +/// errors from the run attempt. +/// +/// On Linux, you can configure your system to run WASI programs using a binfmt directive. Under +/// systemd, write the below to `/etc/binfmt.d/wasi.conf`, with `/usr/bin/wasmtime` optionally +/// replaced with the path to your WASI runtime of choice: +/// +/// ```plain +/// :wasi:M::\x00asm::/usr/bin/wasmtime: +/// ``` +pub fn detect_wasi_runability() -> Result { + let progdir = tempdir()?; + let prog = progdir.path().join("miniwasi.wasm"); + + { + let mut progfile = File::create(&prog)?; + progfile.write_all(WASI_PROGRAM)?; + + #[cfg(unix)] + progfile.set_permissions(Permissions::from_mode(0o777))?; + } + + match Command::new(prog).output() { + Ok(out) => Ok(out.status.success() && out.stdout.is_empty() && out.stderr.is_empty()), + Err(_) => Ok(false), + } +} diff --git a/crates/detect-wasi/src/miniwasi.wasm b/crates/detect-wasi/src/miniwasi.wasm new file mode 100755 index 00000000..56dcdc6a Binary files /dev/null and b/crates/detect-wasi/src/miniwasi.wasm differ diff --git a/crates/detect-wasi/src/miniwasi.wast b/crates/detect-wasi/src/miniwasi.wast new file mode 100644 index 00000000..0a2b05fa --- /dev/null +++ b/crates/detect-wasi/src/miniwasi.wast @@ -0,0 +1,10 @@ +(module + (import "wasi_snapshot_preview1" "proc_exit" (func $exit (param i32))) + (memory $0 0) + (export "memory" (memory $0)) + (export "_start" (func $0)) + (func $0 + (call $exit (i32.const 0)) + (unreachable) + ) +) diff --git a/crates/fs-lock/CHANGELOG.md b/crates/fs-lock/CHANGELOG.md new file mode 100644 index 00000000..b9213f27 --- /dev/null +++ b/crates/fs-lock/CHANGELOG.md @@ -0,0 +1,49 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.10](https://github.com/cargo-bins/cargo-binstall/compare/fs-lock-v0.1.9...fs-lock-v0.1.10) - 2025-03-19 + +### Fixed + +- actually check if lock was acquired ([#2091](https://github.com/cargo-bins/cargo-binstall/pull/2091)) + +## [0.1.9](https://github.com/cargo-bins/cargo-binstall/compare/fs-lock-v0.1.8...fs-lock-v0.1.9) - 2025-03-07 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#2072](https://github.com/cargo-bins/cargo-binstall/pull/2072)) + +## [0.1.8](https://github.com/cargo-bins/cargo-binstall/compare/fs-lock-v0.1.7...fs-lock-v0.1.8) - 2025-02-22 + +### Other + +- Log when FileLock::drop fails to unlock file ([#2064](https://github.com/cargo-bins/cargo-binstall/pull/2064)) +- Fix fs-lock error on nightly ([#2059](https://github.com/cargo-bins/cargo-binstall/pull/2059)) + +## [0.1.7](https://github.com/cargo-bins/cargo-binstall/compare/fs-lock-v0.1.6...fs-lock-v0.1.7) - 2024-12-07 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1993](https://github.com/cargo-bins/cargo-binstall/pull/1993)) + +## [0.1.6](https://github.com/cargo-bins/cargo-binstall/compare/fs-lock-v0.1.5...fs-lock-v0.1.6) - 2024-11-05 + +### Other + +- *(deps)* bump the deps group with 3 updates ([#1954](https://github.com/cargo-bins/cargo-binstall/pull/1954)) + +## [0.1.5](https://github.com/cargo-bins/cargo-binstall/compare/fs-lock-v0.1.4...fs-lock-v0.1.5) - 2024-10-12 + +### Other + +- *(deps)* bump fs4 from 0.9.1 to 0.10.0 in the deps group ([#1929](https://github.com/cargo-bins/cargo-binstall/pull/1929)) + +## [0.1.4](https://github.com/cargo-bins/cargo-binstall/compare/fs-lock-v0.1.3...fs-lock-v0.1.4) - 2024-08-04 + +### Other +- *(deps)* bump the deps group across 1 directory with 2 updates ([#1859](https://github.com/cargo-bins/cargo-binstall/pull/1859)) diff --git a/crates/fs-lock/Cargo.toml b/crates/fs-lock/Cargo.toml new file mode 100644 index 00000000..2c0e8711 --- /dev/null +++ b/crates/fs-lock/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "fs-lock" +description = "Locked files that can be used like normal File" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/fs-lock" +version = "0.1.10" +rust-version = "1.61.0" +authors = ["Jiahao XU "] +edition = "2021" +license = "Apache-2.0 OR MIT" + +[dependencies] +fs4 = "0.13.0" +tracing = { version = "0.1", optional = true } diff --git a/crates/fs-lock/LICENSE-APACHE b/crates/fs-lock/LICENSE-APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/crates/fs-lock/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/fs-lock/LICENSE-MIT b/crates/fs-lock/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/crates/fs-lock/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/crates/fs-lock/src/lib.rs b/crates/fs-lock/src/lib.rs new file mode 100644 index 00000000..311c72d7 --- /dev/null +++ b/crates/fs-lock/src/lib.rs @@ -0,0 +1,166 @@ +//! Locked files with the same API as normal [`File`]s. +//! +//! These use the same mechanisms as, and are interoperable with, Cargo. + +use std::{ + fs::File, + io::{self, IoSlice, IoSliceMut, SeekFrom}, + ops, + path::Path, +}; + +use fs4::fs_std::FileExt; + +/// A locked file. +#[derive(Debug)] +pub struct FileLock(File, #[cfg(feature = "tracing")] Option>); + +impl FileLock { + #[cfg(not(feature = "tracing"))] + fn new(file: File) -> Self { + Self(file) + } + + #[cfg(feature = "tracing")] + fn new(file: File) -> Self { + Self(file, None) + } + + /// Take an exclusive lock on a [`File`]. + /// + /// Note that this operation is blocking, and should not be called in async contexts. + pub fn new_exclusive(file: File) -> io::Result { + FileExt::lock_exclusive(&file)?; + + Ok(Self::new(file)) + } + + /// Try to take an exclusive lock on a [`File`]. + /// + /// On success returns [`Self`]. On error the original [`File`] and optionally + /// an [`io::Error`] if the the failure was caused by anything other than + /// the lock being taken already. + /// + /// Note that this operation is blocking, and should not be called in async contexts. + pub fn new_try_exclusive(file: File) -> Result)> { + match FileExt::try_lock_exclusive(&file) { + Ok(true) => Ok(Self::new(file)), + Ok(false) => Err((file, None)), + Err(e) if e.raw_os_error() == fs4::lock_contended_error().raw_os_error() => { + Err((file, None)) + } + Err(e) => Err((file, Some(e))), + } + } + + /// Take a shared lock on a [`File`]. + /// + /// Note that this operation is blocking, and should not be called in async contexts. + pub fn new_shared(file: File) -> io::Result { + FileExt::lock_shared(&file)?; + + Ok(Self::new(file)) + } + + /// Try to take a shared lock on a [`File`]. + /// + /// On success returns [`Self`]. On error the original [`File`] and optionally + /// an [`io::Error`] if the the failure was caused by anything other than + /// the lock being taken already. + /// + /// Note that this operation is blocking, and should not be called in async contexts. + pub fn new_try_shared(file: File) -> Result)> { + match FileExt::try_lock_shared(&file) { + Ok(true) => Ok(Self::new(file)), + Ok(false) => Err((file, None)), + Err(e) if e.raw_os_error() == fs4::lock_contended_error().raw_os_error() => { + Err((file, None)) + } + Err(e) => Err((file, Some(e))), + } + } + + /// Set path to the file for logging on unlock error, if feature tracing is enabled + pub fn set_file_path(mut self, path: impl Into>) -> Self { + #[cfg(feature = "tracing")] + { + self.1 = Some(path.into()); + } + self + } +} + +impl Drop for FileLock { + fn drop(&mut self) { + let _res = FileExt::unlock(&self.0); + #[cfg(feature = "tracing")] + if let Err(err) = _res { + use std::fmt; + + struct OptionalPath<'a>(Option<&'a Path>); + impl fmt::Display for OptionalPath<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(path) = self.0 { + fmt::Display::fmt(&path.display(), f) + } else { + Ok(()) + } + } + } + + tracing::warn!( + "Failed to unlock file{}: {err}", + OptionalPath(self.1.as_deref()), + ); + } + } +} + +impl ops::Deref for FileLock { + type Target = File; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl ops::DerefMut for FileLock { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl io::Write for FileLock { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.write(buf) + } + fn flush(&mut self) -> io::Result<()> { + self.0.flush() + } + + fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result { + self.0.write_vectored(bufs) + } +} + +impl io::Read for FileLock { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.0.read(buf) + } + + fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> io::Result { + self.0.read_vectored(bufs) + } +} + +impl io::Seek for FileLock { + fn seek(&mut self, pos: SeekFrom) -> io::Result { + self.0.seek(pos) + } + + fn rewind(&mut self) -> io::Result<()> { + self.0.rewind() + } + fn stream_position(&mut self) -> io::Result { + self.0.stream_position() + } +} diff --git a/crates/normalize-path/Cargo.toml b/crates/normalize-path/Cargo.toml new file mode 100644 index 00000000..7d781312 --- /dev/null +++ b/crates/normalize-path/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "normalize-path" +description = "Like canonicalize, but without performing I/O" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/normalize-path" +version = "0.2.1" +rust-version = "1.61.0" +authors = ["Jiahao XU "] +edition = "2021" +license = "Apache-2.0 OR MIT" diff --git a/crates/normalize-path/LICENSE-APACHE b/crates/normalize-path/LICENSE-APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/crates/normalize-path/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/normalize-path/LICENSE-MIT b/crates/normalize-path/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/crates/normalize-path/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/crates/normalize-path/src/lib.rs b/crates/normalize-path/src/lib.rs new file mode 100644 index 00000000..cf65d025 --- /dev/null +++ b/crates/normalize-path/src/lib.rs @@ -0,0 +1,109 @@ +//! Normalizes paths similarly to canonicalize, but without performing I/O. +//! +//! This is like Python's `os.path.normpath`. +//! +//! Initially adapted from [Cargo's implementation][cargo-paths]. +//! +//! [cargo-paths]: https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61 +//! +//! # Example +//! +//! ``` +//! use normalize_path::NormalizePath; +//! use std::path::Path; +//! +//! assert_eq!( +//! Path::new("/A/foo/../B/./").normalize(), +//! Path::new("/A/B") +//! ); +//! ``` + +use std::path::{Component, Path, PathBuf}; + +/// Extension trait to add `normalize_path` to std's [`Path`]. +pub trait NormalizePath { + /// Normalize a path without performing I/O. + /// + /// All redundant separator and up-level references are collapsed. + /// + /// However, this does not resolve links. + fn normalize(&self) -> PathBuf; + + /// Same as [`NormalizePath::normalize`] except that if + /// `Component::Prefix`/`Component::RootDir` is encountered, + /// or if the path points outside of current dir, returns `None`. + fn try_normalize(&self) -> Option; + + /// Return `true` if the path is normalized. + /// + /// # Quirk + /// + /// If the path does not start with `./` but contains `./` in the middle, + /// then this function might returns `true`. + fn is_normalized(&self) -> bool; +} + +impl NormalizePath for Path { + fn normalize(&self) -> PathBuf { + let mut components = self.components().peekable(); + let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek() { + let buf = PathBuf::from(c.as_os_str()); + components.next(); + buf + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(..) => unreachable!(), + Component::RootDir => { + ret.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + ret.push(c); + } + } + } + + ret + } + + fn try_normalize(&self) -> Option { + let mut ret = PathBuf::new(); + + for component in self.components() { + match component { + Component::Prefix(..) | Component::RootDir => return None, + Component::CurDir => {} + Component::ParentDir => { + if !ret.pop() { + return None; + } + } + Component::Normal(c) => { + ret.push(c); + } + } + } + + Some(ret) + } + + fn is_normalized(&self) -> bool { + for component in self.components() { + match component { + Component::CurDir | Component::ParentDir => { + return false; + } + _ => continue, + } + } + + true + } +} diff --git a/e2e-tests/continue-on-failure.sh b/e2e-tests/continue-on-failure.sh new file mode 100755 index 00000000..44080313 --- /dev/null +++ b/e2e-tests/continue-on-failure.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +set -euxo pipefail + +unset CARGO_INSTALL_ROOT + +CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') +export CARGO_HOME +othertmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-test') +export PATH="$CARGO_HOME/bin:$othertmpdir/bin:$PATH" + +mkdir -p "$othertmpdir/bin" +# Copy it to bin to test use of env var `CARGO` +cp "./$1" "$othertmpdir/bin/" + + +## Test --continue-on-failure +set +e +cargo binstall --no-confirm --continue-on-failure cargo-watch@8.4.0 non-existent-clippy +exit_code="$?" + +set -e + +if [ "$exit_code" != 76 ]; then + echo "Expected exit code 76, but actual exit code $exit_code" + exit 1 +fi + + +cargo_watch_version="$(cargo watch -V)" +echo "$cargo_watch_version" + +[ "$cargo_watch_version" = "cargo-watch 8.4.0" ] + + +## Test that it is no-op when only one crate is passed +set +e +cargo binstall --no-confirm --continue-on-failure non-existent-clippy +exit_code="$?" + +set -e + +if [ "$exit_code" != 76 ]; then + echo "Expected exit code 76, but actual exit code $exit_code" + exit 1 +fi + +# Test if both crates are invalid +set +e +cargo binstall --no-confirm --continue-on-failure non-existent-clippy non-existent-clippy2 +exit_code="$?" + +set -e + +if [ "$exit_code" != 76 ]; then + echo "Expected exit code 76, but actual exit code $exit_code" + exit 1 +fi diff --git a/e2e-tests/fake-cargo/cargo b/e2e-tests/fake-cargo/cargo new file mode 100755 index 00000000..da30ddf7 --- /dev/null +++ b/e2e-tests/fake-cargo/cargo @@ -0,0 +1,4 @@ +#!/bin/bash + +echo Always returns 1 to prevent use of "cargo-build" +exit 1 diff --git a/e2e-tests/git.sh b/e2e-tests/git.sh new file mode 100644 index 00000000..0811f88b --- /dev/null +++ b/e2e-tests/git.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +set -euxo pipefail + +test_cargo_binstall_install() { + # Test that the installed binaries can be run + cargo binstall --help >/dev/null + + cargo_binstall_version="$(cargo binstall -V)" + echo "$cargo_binstall_version" + + [ "$cargo_binstall_version" = "cargo-binstall 0.12.0" ] +} + +unset CARGO_INSTALL_ROOT + +CARGO_HOME="$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home')" +export CARGO_HOME +export PATH="$CARGO_HOME/bin:$PATH" + +GIT="$(mktemp -d 2>/dev/null || mktemp -d -t 'git')" +if [ "$OSTYPE" = "cygwin" ] || [ "$OSTYPE" = "msys" ]; then + # Convert it to windows path so `--git "file://$GIT"` would work + # on windows. + GIT="$(cygpath -w "$GIT")" +fi + +git init "$GIT" +cp manifests/github-test-Cargo.toml "$GIT/Cargo.toml" +( + cd "$GIT" + git config user.email 'test@example.com' + git config user.name 'test' + git add Cargo.toml + git commit -m "Add Cargo.toml" +) + +# Install binaries using `--git` +"./$1" binstall --force --git "file://$GIT" --no-confirm cargo-binstall + +test_cargo_binstall_install + +cp -r manifests/workspace/* "$GIT" +( + cd "$GIT" + git add . + git commit -m 'Update to workspace' +) +COMMIT_HASH="$(cd "$GIT" && git rev-parse HEAD)" + +if [ "$OSTYPE" = "cygwin" ] || [ "$OSTYPE" = "msys" ]; then + source="(git+file:///$(cygpath -m "$GIT")#$COMMIT_HASH)" +else + source="(git+file://$GIT#$COMMIT_HASH)" +fi + +# Install cargo-binstall using `--git` +"./$1" binstall --force --git "file://$GIT" --no-confirm cargo-binstall + +test_cargo_binstall_install + +cat "$CARGO_HOME/.crates.toml" +grep -F "cargo-binstall 0.12.0 $source" <"$CARGO_HOME/.crates.toml" + +# Install cargo-watch using `--git` +"./$1" binstall --force --git "file://$GIT" --no-confirm cargo-watch + +cargo_watch_version="$(cargo watch -V)" +echo "$cargo_watch_version" + +[ "$cargo_watch_version" = "cargo-watch 8.4.0" ] + +cat "$CARGO_HOME/.crates.toml" +grep -F "cargo-watch 8.4.0 $source" <"$CARGO_HOME/.crates.toml" diff --git a/e2e-tests/live.sh b/e2e-tests/live.sh new file mode 100755 index 00000000..49eb8d1e --- /dev/null +++ b/e2e-tests/live.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +set -euxo pipefail + +unset CARGO_INSTALL_ROOT + +# - `b3sum@<=1.3.3` would test `fetch_crate_cratesio_version_matched` ability +# to find versions matching <= 1.3.3 +# - `cargo-quickinstall` would test `fetch_crate_cratesio_version_matched` ability +# to find latest stable version. +# - `git-mob-tool tests the using of using a binary name (`git-mob`) different +# from the package name. +crates="b3sum@<=1.3.3 cargo-release@0.24.9 cargo-binstall@0.20.1 cargo-watch@8.4.0 miniserve@0.23.0 sccache@0.3.3 cargo-quickinstall jj-cli@0.18.0 git-mob-tool@1.6.1" + +CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') +export CARGO_HOME +othertmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-test') +export PATH="$CARGO_HOME/bin:$othertmpdir/bin:$PATH" + +mkdir -p "$othertmpdir/bin" +# Copy it to bin to test use of env var `CARGO` +cp "./$1" "$othertmpdir/bin/" + +# Install binaries using cargo-binstall +# shellcheck disable=SC2086 +cargo binstall --no-confirm $crates + +rm -r "$othertmpdir" + +# Test that the installed binaries can be run +b3sum_version="$(b3sum --version)" +echo "$b3sum_version" + +[ "$b3sum_version" = "b3sum 1.3.3" ] + +cargo_release_version="$(cargo-release release --version)" +echo "$cargo_release_version" + +[ "$cargo_release_version" = "cargo-release 0.24.9" ] + +cargo binstall --help >/dev/null + +cargo_binstall_version="$(cargo-binstall -V)" +echo "cargo-binstall version $cargo_binstall_version" + +[ "$cargo_binstall_version" = "0.20.1" ] + +cargo_watch_version="$(cargo watch -V)" +echo "$cargo_watch_version" + +[ "$cargo_watch_version" = "cargo-watch 8.4.0" ] + +miniserve_version="$(miniserve -V)" +echo "$miniserve_version" + +[ "$miniserve_version" = "miniserve 0.23.0" ] + +cargo-quickinstall -V + +jj_version="$(jj --version)" +echo "$jj_version" + +[ "$jj_version" = "jj 0.18.0-9fb5307b7886e390c02817af7c31b403f0279144" ] + +git_mob_version="$(git-mob --version)" +echo "$git_mob_version" + +[ "$git_mob_version" = "git-mob-tool 1.6.1" ] + +cargo uninstall b3sum cargo-binstall + +"./$1" binstall -y cargo-binstall@0.20.1 +jq <"$CARGO_HOME/binstall/crates-v1.json" | grep -v b3sum diff --git a/e2e-tests/manifest-path.sh b/e2e-tests/manifest-path.sh new file mode 100755 index 00000000..35f2a1b7 --- /dev/null +++ b/e2e-tests/manifest-path.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +set -euxo pipefail + +unset CARGO_INSTALL_ROOT + +CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') +export CARGO_HOME +export PATH="$CARGO_HOME/bin:$PATH" + +# Install binaries using `--manifest-path` +# Also test default github template +"./$1" binstall --force --manifest-path "manifests/github-test-Cargo.toml" --no-confirm cargo-binstall + +# Test that the installed binaries can be run +cargo binstall --help >/dev/null + +cargo_binstall_version="$(cargo binstall -V)" +echo "$cargo_binstall_version" + +[ "$cargo_binstall_version" = "cargo-binstall 0.12.0" ] + +cat "$CARGO_HOME/.crates.toml" +grep -F "cargo-binstall 0.12.0 (path+file://manifests/github-test-Cargo.toml)" <"$CARGO_HOME/.crates.toml" diff --git a/e2e-tests/manifests/bitbucket-test-Cargo.toml b/e2e-tests/manifests/bitbucket-test-Cargo.toml new file mode 100644 index 00000000..4b91c06f --- /dev/null +++ b/e2e-tests/manifests/bitbucket-test-Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cargo-binstall" +description = "Rust binary package installer for CI integration" +repository = "https://bitbucket.org/nobodyxusdcdc/hello-world" +version = "0.12.0" +rust-version = "1.61.0" +authors = ["ryan "] +edition = "2021" +license = "GPL-3.0" + +[[bin]] +name = "cargo-binstall" +path = "src/main.rs" diff --git a/e2e-tests/manifests/github-test-Cargo.toml b/e2e-tests/manifests/github-test-Cargo.toml new file mode 100644 index 00000000..aa3a0898 --- /dev/null +++ b/e2e-tests/manifests/github-test-Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cargo-binstall" +description = "Rust binary package installer for CI integration" +repository = "https://github.com/cargo-bins/cargo-binstall" +version = "0.12.0" +rust-version = "1.61.0" +authors = ["ryan "] +edition = "2021" +license = "GPL-3.0" + +[[bin]] +name = "cargo-binstall" +path = "src/main.rs" diff --git a/e2e-tests/manifests/github-test-Cargo2.toml b/e2e-tests/manifests/github-test-Cargo2.toml new file mode 100644 index 00000000..8732c248 --- /dev/null +++ b/e2e-tests/manifests/github-test-Cargo2.toml @@ -0,0 +1,16 @@ +[package] +name = "cargo-binstall" +description = "Rust binary package installer for CI integration" +repository = "https://github.com/cargo-bins/cargo-binstall.git" +version = "0.12.0" +rust-version = "1.61.0" +authors = ["ryan "] +edition = "2021" +license = "GPL-3.0" + +[package.metadata.binstall] +bin-dir = "{ bin }{ binary-ext }" + +[[bin]] +name = "cargo-binstall" +path = "src/main.rs" diff --git a/e2e-tests/manifests/gitlab-test-Cargo.toml b/e2e-tests/manifests/gitlab-test-Cargo.toml new file mode 100644 index 00000000..5d8ae7f9 --- /dev/null +++ b/e2e-tests/manifests/gitlab-test-Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cargo-binstall" +description = "Rust binary package installer for CI integration" +repository = "https://gitlab.kitware.com/NobodyXu/hello-world.git" +version = "0.2.0" +rust-version = "1.61.0" +authors = ["ryan "] +edition = "2021" +license = "GPL-3.0" + +[[bin]] +name = "cargo-binstall" +path = "src/main.rs" diff --git a/e2e-tests/manifests/private-github-repo-test-Cargo.toml b/e2e-tests/manifests/private-github-repo-test-Cargo.toml new file mode 100644 index 00000000..38e3ea76 --- /dev/null +++ b/e2e-tests/manifests/private-github-repo-test-Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cargo-binstall" +description = "Rust binary package installer for CI integration" +repository = "https://github.com/cargo-bins/private-repo-for-testing.git" +version = "0.12.0" +rust-version = "1.61.0" +authors = ["ryan "] +edition = "2021" +license = "GPL-3.0" + +[[bin]] +name = "cargo-binstall" +path = "src/main.rs" diff --git a/e2e-tests/manifests/signing-Cargo.toml b/e2e-tests/manifests/signing-Cargo.toml new file mode 100644 index 00000000..962416ad --- /dev/null +++ b/e2e-tests/manifests/signing-Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "signing-test" +description = "Rust binary package installer for CI integration" +version = "0.1.0" +authors = ["ryan "] +edition = "2021" +license = "GPL-3.0" + +[[bin]] +name = "signing-test" +path = "src/main.rs" + +[package.metadata.binstall] +pkg-url = "https://localhost:4443/signing-test.tar" +pkg-fmt = "tar" + +[package.metadata.binstall.signing] +algorithm = "minisign" +pubkey = "RWRnmBcLmQbXVcEPWo2OOKMI36kki4GiI7gcBgIaPLwvxe14Wtxm9acX" diff --git a/e2e-tests/manifests/strategies-test-Cargo.toml b/e2e-tests/manifests/strategies-test-Cargo.toml new file mode 100644 index 00000000..f4235a84 --- /dev/null +++ b/e2e-tests/manifests/strategies-test-Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "cargo-update" +repository = "https://github.com/nabijaczleweli/cargo-update" +version = "11.1.2" + +[[bin]] +name = "cargo-install-update" +path = "src/main.rs" +test = false +doc = false + +[[bin]] +name = "cargo-install-update-config" +path = "src/main-config.rs" +test = false +doc = false + +[package.metadata.binstall] +disabled-strategies = ["quick-install", "compile"] diff --git a/e2e-tests/manifests/strategies-test-Cargo2.toml b/e2e-tests/manifests/strategies-test-Cargo2.toml new file mode 100644 index 00000000..aef2811e --- /dev/null +++ b/e2e-tests/manifests/strategies-test-Cargo2.toml @@ -0,0 +1,19 @@ +[package] +name = "cargo-update" +repository = "https://github.com/nabijaczleweli/cargo-update" +version = "11.1.2" + +[[bin]] +name = "cargo-install-update" +path = "src/main.rs" +test = false +doc = false + +[[bin]] +name = "cargo-install-update-config" +path = "src/main-config.rs" +test = false +doc = false + +[package.metadata.binstall] +disabled-strategies = ["quick-install"] diff --git a/e2e-tests/manifests/strategies-test-override-Cargo.toml b/e2e-tests/manifests/strategies-test-override-Cargo.toml new file mode 100644 index 00000000..97127671 --- /dev/null +++ b/e2e-tests/manifests/strategies-test-override-Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cargo-quickinstall" +repository = "https://github.com/cargo-bins/cargo-quickinstall" +version = "0.2.10" + +[[bin]] +name = "cargo-quickinstall" +path = "src/main.rs" +test = false +doc = false + +[package.metadata.binstall] +disabled-strategies = ["crate-meta-data", "quick-install", "compile"] diff --git a/e2e-tests/manifests/workspace/Cargo.toml b/e2e-tests/manifests/workspace/Cargo.toml new file mode 100644 index 00000000..6e98755f --- /dev/null +++ b/e2e-tests/manifests/workspace/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["crates/*/*/*/*/*/*", "b/*/*/*/*"] +exclude = ["b/c/d/e/*"] diff --git a/e2e-tests/manifests/workspace/b/c/d/e/f/Cargo.toml b/e2e-tests/manifests/workspace/b/c/d/e/f/Cargo.toml new file mode 100644 index 00000000..176f7044 --- /dev/null +++ b/e2e-tests/manifests/workspace/b/c/d/e/f/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cargo-binstall2" +description = "Rust binary package installer for CI integration" +repository = "https://bitbucket.org/nobodyxusdcdc/hello-world" +version = "0.0.0" +rust-version = "1.61.0" +authors = ["ryan "] +edition = "2021" +license = "GPL-3.0" + +[[bin]] +name = "cargo-binstall" +path = "src/main.rs" diff --git a/e2e-tests/manifests/workspace/crates/a/b/c/d/e/cargo-binstall/Cargo.toml b/e2e-tests/manifests/workspace/crates/a/b/c/d/e/cargo-binstall/Cargo.toml new file mode 100644 index 00000000..d5435856 --- /dev/null +++ b/e2e-tests/manifests/workspace/crates/a/b/c/d/e/cargo-binstall/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "cargo-binstall" +version = "0.12.0" +repository = "https://github.com/cargo-bins/cargo-binstall" + +[[bin]] +name = "cargo-binstall" +path = "src/main.rs" diff --git a/e2e-tests/manifests/workspace/crates/a/b/c/d/e/cargo-watch/Cargo.toml b/e2e-tests/manifests/workspace/crates/a/b/c/d/e/cargo-watch/Cargo.toml new file mode 100644 index 00000000..71d35971 --- /dev/null +++ b/e2e-tests/manifests/workspace/crates/a/b/c/d/e/cargo-watch/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "cargo-watch" +version = "8.4.0" +repository = "https://github.com/watchexec/cargo-watch" + +[[bin]] +name = "cargo-watch" diff --git a/e2e-tests/manifests/workspace/crates/a/b/c/d/e/cargo-watch/src/main.rs b/e2e-tests/manifests/workspace/crates/a/b/c/d/e/cargo-watch/src/main.rs new file mode 100644 index 00000000..e69de29b diff --git a/e2e-tests/no-track.sh b/e2e-tests/no-track.sh new file mode 100644 index 00000000..28f1bd74 --- /dev/null +++ b/e2e-tests/no-track.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -euxo pipefail + +unset CARGO_INSTALL_ROOT + +CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') +export CARGO_HOME +export PATH="$CARGO_HOME/bin:$PATH" + +"./$1" binstall -y cargo-binstall@0.20.1 +cargo-binstall --help >/dev/null + +set +e + +"./$1" binstall -y --no-track cargo-binstall@0.20.1 +exit_code="$?" + +set -e + +if [ "$exit_code" != 88 ]; then + echo "Expected exit code 88 BinFile Error, but actual exit code $exit_code" + exit 1 +fi + + +"./$1" binstall -y --no-track --force cargo-binstall@0.20.1 +cargo-binstall --help >/dev/null diff --git a/e2e-tests/other-repos.sh b/e2e-tests/other-repos.sh new file mode 100755 index 00000000..d492d126 --- /dev/null +++ b/e2e-tests/other-repos.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +set -euxo pipefail + +unset CARGO_INSTALL_ROOT + +CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') +export CARGO_HOME +export PATH="$CARGO_HOME/bin:$PATH" + +# Test default GitLab pkg-url templates +#"./$1" binstall \ +# --force \ +# --manifest-path "manifests/gitlab-test-Cargo.toml" \ +# --no-confirm \ +# --disable-strategies compile \ +# cargo-binstall + +# temporarily disable bitbucket testing as bitbucket is down +## Test default BitBucket pkg-url templates +#"./$1" binstall \ +# --force \ +# --manifest-path "manifests/bitbucket-test-Cargo.toml" \ +# --no-confirm \ +# --disable-strategies compile \ +# cargo-binstall +# +## Test that the installed binaries can be run +#cargo binstall --help >/dev/null +# +#cargo_binstall_version="$(cargo binstall -V)" +#echo "$cargo_binstall_version" +# +#[ "$cargo_binstall_version" = "cargo-binstall 0.12.0" ] + +# Test default Github pkg-url templates, +# with bin-dir provided +"./$1" binstall \ + --force \ + --manifest-path "manifests/github-test-Cargo2.toml" \ + --no-confirm \ + --disable-strategies compile \ + cargo-binstall + +# Test that the installed binaries can be run +cargo binstall --help >/dev/null + +cargo_binstall_version="$(cargo binstall -V)" +echo "$cargo_binstall_version" + +[ "$cargo_binstall_version" = "cargo-binstall 0.12.0" ] diff --git a/e2e-tests/private-github-repo.sh b/e2e-tests/private-github-repo.sh new file mode 100755 index 00000000..e0050101 --- /dev/null +++ b/e2e-tests/private-github-repo.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -euxo pipefail + +unset CARGO_INSTALL_ROOT + +CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') +export CARGO_HOME +export PATH="$CARGO_HOME/bin:$PATH" + +# Install binaries using `--manifest-path` +# Also test default github template +"./$1" binstall --force --manifest-path "manifests/private-github-repo-test-Cargo.toml" --no-confirm cargo-binstall --strategies crate-meta-data + +# Test that the installed binaries can be run +cargo binstall --help >/dev/null + +cargo_binstall_version="$(cargo binstall -V)" +echo "$cargo_binstall_version" + +[ "$cargo_binstall_version" = "cargo-binstall 0.12.0" ] diff --git a/e2e-tests/registries.sh b/e2e-tests/registries.sh new file mode 100644 index 00000000..7be7ffaf --- /dev/null +++ b/e2e-tests/registries.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +set -euxo pipefail + +test_cargo_binstall_install() { + # Test that the installed binaries can be run + cargo binstall --help >/dev/null + + cargo_binstall_version="$(cargo binstall -V)" + echo "$cargo_binstall_version" + + [ "$cargo_binstall_version" = "cargo-binstall 0.12.0" ] +} + +unset CARGO_INSTALL_ROOT + +CARGO_HOME="$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home')" +export CARGO_HOME +export PATH="$CARGO_HOME/bin:$PATH" + +# Testing conflicts of `--index` and `--registry` +set +e + +"./$1" binstall --index 'sparse+https://index.crates.io/' --registry t1 cargo-binstall +exit_code="$?" + +set -e + +if [ "$exit_code" != 2 ]; then + echo "Expected exit code 2, but actual exit code $exit_code" + exit 1 +fi + +cat >"$CARGO_HOME/config.toml" << EOF +[registries] +t1 = { index = "https://github.com/rust-lang/crates.io-index" } +t2 = { index = "sparse+https://index.crates.io/" } + +[registry] +default = "t1" +EOF + +# Install binaries using default registry in config +"./$1" binstall --force -y cargo-binstall@0.12.0 + +grep -F "cargo-binstall 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" <"$CARGO_HOME/.crates.toml" + +test_cargo_binstall_install + +# Install binaries using registry t2 in config +"./$1" binstall --force --registry t2 -y cargo-binstall@0.12.0 + +grep -F "cargo-binstall 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" <"$CARGO_HOME/.crates.toml" + +test_cargo_binstall_install + +# Install binaries using registry t3 in env +CARGO_REGISTRIES_t3_INDEX='sparse+https://index.crates.io/' "./$1" binstall --force --registry t3 -y cargo-binstall@0.12.0 + +grep -F "cargo-binstall 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" <"$CARGO_HOME/.crates.toml" + +test_cargo_binstall_install + +# Install binaries using index directly +"./$1" binstall --force --index 'sparse+https://index.crates.io/' -y cargo-binstall@0.12.0 + +grep -F "cargo-binstall 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" <"$CARGO_HOME/.crates.toml" + +test_cargo_binstall_install diff --git a/e2e-tests/self-install.sh b/e2e-tests/self-install.sh new file mode 100644 index 00000000..e00f3538 --- /dev/null +++ b/e2e-tests/self-install.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -euxo pipefail + +unset CARGO_INSTALL_ROOT + +CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') +export CARGO_HOME +export PATH="$CARGO_HOME/bin:$PATH" + +"./$1" --self-install + +cargo binstall --help +cargo install --list diff --git a/e2e-tests/self-upgrade-no-symlink.sh b/e2e-tests/self-upgrade-no-symlink.sh new file mode 100644 index 00000000..d00cca88 --- /dev/null +++ b/e2e-tests/self-upgrade-no-symlink.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +set -euxo pipefail + +unset CARGO_INSTALL_ROOT + +CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') +export CARGO_HOME +export PATH="$CARGO_HOME/bin:$PATH" + +# first boostrap-install into the CARGO_HOME +mkdir -p "$CARGO_HOME/bin" +cp "./$1" "$CARGO_HOME/bin" + +# now we're running the CARGO_HOME/bin/cargo-binstall (via cargo): + +# self update replacing no-symlinks with no-symlinks +cargo binstall --no-confirm --no-symlinks --force cargo-binstall@0.20.1 + +# self update replacing no-symlinks with symlinks +cp "./$1" "$CARGO_HOME/bin" + +cargo binstall --no-confirm --force cargo-binstall@0.20.1 + +# self update replacing symlinks with symlinks +ln -snf "$(pwd)/cargo-binstall" "$CARGO_HOME/bin/cargo-binstall" + +cargo binstall --no-confirm --force cargo-binstall@0.20.1 + +# self update replacing symlinks with no-symlinks +ln -snf "$(pwd)/cargo-binstall" "$CARGO_HOME/bin/cargo-binstall" + +cargo binstall --no-confirm --force --no-symlinks cargo-binstall@0.20.1 diff --git a/e2e-tests/signing.sh b/e2e-tests/signing.sh new file mode 100755 index 00000000..08915c82 --- /dev/null +++ b/e2e-tests/signing.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -euxo pipefail + +unset CARGO_INSTALL_ROOT + +CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') +export CARGO_HOME +export PATH="$CARGO_HOME/bin:$PATH" + +echo Generate tls cert + +CERT_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'cert-dir') +export CERT_DIR + +openssl req -newkey rsa:4096 -x509 -sha256 -days 1 -nodes -out "$CERT_DIR/"ca.pem -keyout "$CERT_DIR/"ca.key -subj '//C=UT/CN=ca.localhost' +openssl req -new -newkey rsa:4096 -sha256 -nodes -out "$CERT_DIR/"server.csr -keyout "$CERT_DIR/"server.key -subj '//C=UT/CN=localhost' +openssl x509 -req -in "$CERT_DIR/"server.csr -CA "$CERT_DIR/"ca.pem -CAkey "$CERT_DIR/"ca.key -CAcreateserial -out "$CERT_DIR/"server.pem -days 1 -sha256 -extfile signing/server.ext + +python3 signing/server.py & +server_pid=$! +trap 'kill $server_pid' ERR INT TERM + +export BINSTALL_HTTPS_ROOT_CERTS="$CERT_DIR/ca.pem" + +signing/wait-for-server.sh + +"./$1" binstall --force --manifest-path manifests/signing-Cargo.toml --no-confirm signing-test +"./$1" binstall --force --manifest-path manifests/signing-Cargo.toml --no-confirm --only-signed signing-test +"./$1" binstall --force --manifest-path manifests/signing-Cargo.toml --no-confirm --skip-signatures signing-test + +# from quick-install +"./$1" binstall --force --strategies quick-install --no-confirm --only-signed --target x86_64-unknown-linux-musl zellij@0.38.2 + +kill $server_pid || true diff --git a/e2e-tests/signing/minisign.key b/e2e-tests/signing/minisign.key new file mode 100644 index 00000000..5716be67 --- /dev/null +++ b/e2e-tests/signing/minisign.key @@ -0,0 +1,2 @@ +untrusted comment: minisign encrypted secret key +RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZ5gXC5kG11Wu99VVpToebb+yc0MOw4cbWzxSHyOxoSTu6kBrK09z/MEPWo2OOKMI36kki4GiI7gcBgIaPLwvxe14Wtxm9acXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= diff --git a/e2e-tests/signing/minisign.pub b/e2e-tests/signing/minisign.pub new file mode 100644 index 00000000..0baa41c7 --- /dev/null +++ b/e2e-tests/signing/minisign.pub @@ -0,0 +1,2 @@ +untrusted comment: minisign public key 55D706990B179867 +RWRnmBcLmQbXVcEPWo2OOKMI36kki4GiI7gcBgIaPLwvxe14Wtxm9acX diff --git a/e2e-tests/signing/server.ext b/e2e-tests/signing/server.ext new file mode 100644 index 00000000..0bba95d3 --- /dev/null +++ b/e2e-tests/signing/server.ext @@ -0,0 +1,6 @@ +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names +[alt_names] +DNS.1 = localhost diff --git a/e2e-tests/signing/server.py b/e2e-tests/signing/server.py new file mode 100644 index 00000000..79d7749a --- /dev/null +++ b/e2e-tests/signing/server.py @@ -0,0 +1,15 @@ +import http.server +import os +import ssl +from pathlib import Path + +cert_dir = Path(os.environ["CERT_DIR"]) + +os.chdir(os.path.dirname(__file__)) + +server_address = ('', 4443) +httpd = http.server.HTTPServer(server_address, http.server.SimpleHTTPRequestHandler) +ctx = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_SERVER) +ctx.load_cert_chain(certfile=cert_dir / "server.pem", keyfile=cert_dir / "server.key") +httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True) +httpd.serve_forever() diff --git a/e2e-tests/signing/signing-test.exe.nasm b/e2e-tests/signing/signing-test.exe.nasm new file mode 100644 index 00000000..35b23301 --- /dev/null +++ b/e2e-tests/signing/signing-test.exe.nasm @@ -0,0 +1,74 @@ +; tiny97.asm, copyright Alexander Sotirov + +BITS 32 +; +; MZ header +; The only two fields that matter are e_magic and e_lfanew + +mzhdr: + dw "MZ" ; e_magic + dw 0 ; e_cblp UNUSED + +; PE signature +pesig: + dd "PE" ; e_cp, e_crlc UNUSED ; PE signature + +; PE header +pehdr: + dw 0x014C ; e_cparhdr UNUSED ; Machine (Intel 386) + dw 1 ; e_minalloc UNUSED ; NumberOfSections + +; dd 0xC3582A6A ; e_maxalloc, e_ss UNUSED ; TimeDateStamp UNUSED + +; Entry point +start: + push byte 42 + pop eax + ret + +codesize equ $ - start + + dd 0 ; e_sp, e_csum UNUSED ; PointerToSymbolTable UNUSED + dd 0 ; e_ip, e_cs UNUSED ; NumberOfSymbols UNUSED + dw sections-opthdr ; e_lsarlc UNUSED ; SizeOfOptionalHeader + dw 0x103 ; e_ovno UNUSED ; Characteristics + +; PE optional header +; The debug directory size at offset 0x94 from here must be 0 + +filealign equ 4 +sect_align equ 4 ; must be 4 because of e_lfanew + +%define round(n, r) (((n+(r-1))/r)*r) + +opthdr: + dw 0x10B ; e_res UNUSED ; Magic (PE32) + db 8 ; MajorLinkerVersion UNUSED + db 0 ; MinorLinkerVersion UNUSED + +; PE code section +sections: + dd round(codesize, filealign) ; SizeOfCode UNUSED ; Name UNUSED + dd 0 ; e_oemid, e_oeminfo UNUSED ; SizeOfInitializedData UNUSED + dd codesize ; e_res2 UNUSED ; SizeOfUninitializedData UNUSED ; VirtualSize + dd start ; AddressOfEntryPoint ; VirtualAddress + dd codesize ; BaseOfCode UNUSED ; SizeOfRawData + dd start ; BaseOfData UNUSED ; PointerToRawData + dd 0x400000 ; ImageBase ; PointerToRelocations UNUSED + dd sect_align ; e_lfanew ; SectionAlignment ; PointerToLinenumbers UNUSED + dd filealign ; FileAlignment ; NumberOfRelocations, NumberOfLinenumbers UNUSED + dw 4 ; MajorOperatingSystemVersion UNUSED ; Characteristics UNUSED + dw 0 ; MinorOperatingSystemVersion UNUSED + dw 0 ; MajorImageVersion UNUSED + dw 0 ; MinorImageVersion UNUSED + dw 4 ; MajorSubsystemVersion + dw 0 ; MinorSubsystemVersion UNUSED + dd 0 ; Win32VersionValue UNUSED + dd round(hdrsize, sect_align)+round(codesize,sect_align) ; SizeOfImage + dd round(hdrsize, filealign) ; SizeOfHeaders + dd 0 ; CheckSum UNUSED + db 2 ; Subsystem (Win32 GUI) + +hdrsize equ $ - $$ +filesize equ $ - $$ + diff --git a/e2e-tests/signing/signing-test.tar b/e2e-tests/signing/signing-test.tar new file mode 100644 index 00000000..de55cfef Binary files /dev/null and b/e2e-tests/signing/signing-test.tar differ diff --git a/e2e-tests/signing/signing-test.tar.sig b/e2e-tests/signing/signing-test.tar.sig new file mode 100644 index 00000000..0f059432 --- /dev/null +++ b/e2e-tests/signing/signing-test.tar.sig @@ -0,0 +1,4 @@ +untrusted comment: signature from minisign secret key +RURnmBcLmQbXVVINqskhik18fjpzn1TTn7UZWPC6TuVNSZc+0CqLiNxJhBvT3aXiFHxiEwiBeQaFipsxXux06C12+rwT9Pozgwo= +trusted comment: timestamp:1693846563 file:signing-test.tar hashed +fQqqvTO6KgHSHf6/n18FQVJgO8azb1dB90jwj2YukbRfwK3QD0rNSDFBmhN73H7Pwxsz9of42OG60dfXA+ldCQ== diff --git a/e2e-tests/signing/wait-for-server.sh b/e2e-tests/signing/wait-for-server.sh new file mode 100755 index 00000000..00d0d25c --- /dev/null +++ b/e2e-tests/signing/wait-for-server.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -euxo pipefail + +CERT="${BINSTALL_HTTPS_ROOT_CERTS?}" + +counter=0 + +while ! curl --cacert "$CERT" --ssl-revoke-best-effort -L https://localhost:4443/signing-test.tar | file -; do + counter=$(( counter + 1 )) + if [ "$counter" = "20" ]; then + echo Failed to connect to https server + exit 1; + fi + sleep 10 +done diff --git a/e2e-tests/specific-binaries.sh b/e2e-tests/specific-binaries.sh new file mode 100755 index 00000000..bc3d92c8 --- /dev/null +++ b/e2e-tests/specific-binaries.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +set -euxo pipefail + +unset CARGO_INSTALL_ROOT + +CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') +export CARGO_HOME +export PATH="$CARGO_HOME/bin:$PATH" + +# Install a specific binary, ensuring we don't fallback to source. +"./$1" binstall --no-confirm taplo-cli --bin taplo + +# Verify that the binary was installed and is executable +if ! command -v taplo >/dev/null 2>&1; then + echo "taplo was not installed" + exit 1 +fi + +# Run the binary to check it works +taplo --version + +# Install a specific binary, but always compile from source. +"./$1" binstall --no-confirm ripgrep --bin rg --strategies compile + +# Verify that the binary was installed and is executable +if ! command -v rg >/dev/null 2>&1; then + echo "rg was not installed" + exit 1 +fi + +# Run the binary to check it works +rg --version diff --git a/e2e-tests/strategies.sh b/e2e-tests/strategies.sh new file mode 100755 index 00000000..981ce188 --- /dev/null +++ b/e2e-tests/strategies.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +set -euxo pipefail + +unset CARGO_INSTALL_ROOT + +CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') +export CARGO_HOME +export PATH="$CARGO_HOME/bin:$PATH" + +## Test --disable-strategies +set +e + +"./$1" binstall --no-confirm --disable-strategies quick-install,compile cargo-update@11.1.2 +exit_code="$?" + +set -e + +if [ "$exit_code" != 94 ]; then + echo "Expected exit code 94, but actual exit code $exit_code" + exit 1 +fi + +## Test --strategies +set +e + +"./$1" binstall --no-confirm --strategies crate-meta-data cargo-update@11.1.2 +exit_code="$?" + +set -e + +if [ "$exit_code" != 94 ]; then + echo "Expected exit code 94, but actual exit code $exit_code" + exit 1 +fi + +## Test compile-only strategy +"./$1" binstall --no-confirm --strategies compile cargo-quickinstall@0.2.8 + +## Test Cargo.toml disable-strategies +set +e + +"./$1" binstall --no-confirm --manifest-path "manifests/strategies-test-Cargo.toml" cargo-update@11.1.2 +exit_code="$?" + +set -e + +if [ "$exit_code" != 94 ]; then + echo "Expected exit code 94, but actual exit code $exit_code" + exit 1 +fi + +set +e + +"./$1" binstall --disable-strategies compile --no-confirm --manifest-path "manifests/strategies-test-Cargo2.toml" cargo-update@11.1.2 +exit_code="$?" + +set -e + +if [ "$exit_code" != 94 ]; then + echo "Expected exit code 94, but actual exit code $exit_code" + exit 1 +fi + +## Test --strategies overriding `disabled-strategies=["compile"]` in Cargo.toml + "./$1" binstall --no-confirm --manifest-path "manifests/strategies-test-override-Cargo.toml" --strategies compile cargo-quickinstall@0.2.10 diff --git a/e2e-tests/subcrate.sh b/e2e-tests/subcrate.sh new file mode 100755 index 00000000..40a15ecc --- /dev/null +++ b/e2e-tests/subcrate.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -euxo pipefail + +unset CARGO_INSTALL_ROOT + +CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') +export CARGO_HOME +othertmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-test') +export PATH="$CARGO_HOME/bin:$othertmpdir/bin:$PATH" + +mkdir -p "$othertmpdir/bin" +# Copy it to bin to test use of env var `CARGO` +cp "./$1" "$othertmpdir/bin/" + +# cargo-audit +cargo binstall --no-confirm cargo-audit@0.18.3 --strategies crate-meta-data + +cargo_audit_version="$(cargo audit --version)" +echo "$cargo_audit_version" + +[ "$cargo_audit_version" = "cargo-audit 0.18.3" ] diff --git a/e2e-tests/tls.sh b/e2e-tests/tls.sh new file mode 100755 index 00000000..b54ab9c7 --- /dev/null +++ b/e2e-tests/tls.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -euxo pipefail + +unset CARGO_INSTALL_ROOT + +CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') +export CARGO_HOME +export PATH="$CARGO_HOME/bin:$PATH" + +"./$1" binstall \ + --force \ + --min-tls-version "${2:-1.3}" \ + --no-confirm \ + cargo-binstall@0.20.1 +# Test that the installed binaries can be run +cargo binstall --help >/dev/null diff --git a/e2e-tests/uninstall.sh b/e2e-tests/uninstall.sh new file mode 100644 index 00000000..3cfd8cbc --- /dev/null +++ b/e2e-tests/uninstall.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -euxo pipefail + +unset CARGO_INSTALL_ROOT + +CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') +export CARGO_HOME +othertmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-test') +export PATH="$CARGO_HOME/bin:$othertmpdir/bin:$PATH" + +mkdir -p "$othertmpdir/bin" +# Copy it to bin to test use of env var `CARGO` +cp "./$1" "$othertmpdir/bin/" + + +cargo binstall --no-confirm cargo-watch@8.4.0 +cargo uninstall cargo-watch diff --git a/e2e-tests/upgrade.sh b/e2e-tests/upgrade.sh new file mode 100755 index 00000000..affdab73 --- /dev/null +++ b/e2e-tests/upgrade.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -euxo pipefail + +unset CARGO_INSTALL_ROOT + +CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') +export CARGO_HOME +export PATH="$CARGO_HOME/bin:$PATH" + +# Test skip when installed +"./$1" binstall --no-confirm --force cargo-binstall@0.11.1 +"./$1" binstall --log-level=info --no-confirm cargo-binstall@0.11.1 | grep -q 'cargo-binstall v0.11.1 is already installed' + +## Test When 0.11.0 is installed but can be upgraded. +"./$1" binstall --no-confirm cargo-binstall@0.12.0 +"./$1" binstall --log-level=info --no-confirm cargo-binstall@0.12.0 | grep -q 'cargo-binstall v0.12.0 is already installed' +"./$1" binstall --log-level=info --no-confirm cargo-binstall@^0.12.0 | grep -q -v 'cargo-binstall v0.12.0 is already installed' diff --git a/e2e-tests/version-syntax.sh b/e2e-tests/version-syntax.sh new file mode 100755 index 00000000..c9551c51 --- /dev/null +++ b/e2e-tests/version-syntax.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -euxo pipefail + +unset CARGO_INSTALL_ROOT + +CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') +export CARGO_HOME +export PATH="$CARGO_HOME/bin:$PATH" + +# Test --version +"./$1" binstall --force --no-confirm --version 0.11.1 cargo-binstall +# Test that the installed binaries can be run +cargo binstall --help >/dev/null + +# Test "$crate_name@$version" +"./$1" binstall --force --no-confirm cargo-binstall@0.11.1 +# Test that the installed binaries can be run +cargo binstall --help >/dev/null diff --git a/install-from-binstall-release.ps1 b/install-from-binstall-release.ps1 new file mode 100644 index 00000000..abbc8050 --- /dev/null +++ b/install-from-binstall-release.ps1 @@ -0,0 +1,48 @@ +$ErrorActionPreference = "Stop" +Set-PSDebug -Trace 1 +$tmpdir = $Env:TEMP +$BINSTALL_VERSION = $Env:BINSTALL_VERSION +if ($BINSTALL_VERSION -and $BINSTALL_VERSION -notlike 'v*') { + # prefix version with v + $BINSTALL_VERSION = "v$BINSTALL_VERSION" +} +# Fetch binaries from `[..]/releases/latest/download/[..]` if _no_ version is +# given, otherwise from `[..]/releases/download/VERSION/[..]`. Note the shifted +# location of '/download'. +$base_url = if (-not $BINSTALL_VERSION) { + "https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-" +} else { + "https://github.com/cargo-bins/cargo-binstall/releases/download/$BINSTALL_VERSION/cargo-binstall-" +} + +$proc_arch = [Environment]::GetEnvironmentVariable("PROCESSOR_ARCHITECTURE", [EnvironmentVariableTarget]::Machine) +if ($proc_arch -eq "AMD64") { + $arch = "x86_64" +} elseif ($proc_arch -eq "ARM64") { + $arch = "aarch64" +} else { + Write-Host "Unsupported Architecture: $type" -ForegroundColor Red + [Environment]::Exit(1) +} +$url = "$base_url$arch-pc-windows-msvc.zip" +Invoke-WebRequest $url -OutFile $tmpdir\cargo-binstall.zip +Expand-Archive -Force $tmpdir\cargo-binstall.zip $tmpdir\cargo-binstall +Write-Host "" + +$ps = Start-Process -PassThru -Wait "$tmpdir\cargo-binstall\cargo-binstall.exe" "--self-install" +if ($ps.ExitCode -ne 0) { + Invoke-Expression "$tmpdir\cargo-binstall\cargo-binstall.exe -y --force cargo-binstall" +} + +Remove-Item -Force $tmpdir\cargo-binstall.zip +Remove-Item -Recurse -Force $tmpdir\cargo-binstall +$cargo_home = if ($Env:CARGO_HOME -ne $null) { $Env:CARGO_HOME } else { "$HOME\.cargo" } +if ($Env:Path -split ";" -notcontains "$cargo_home\bin") { + if (($Env:CI -ne $null) -and ($Env:GITHUB_PATH -ne $null)) { + Add-Content -Path "$Env:GITHUB_PATH" -Value "$cargo_home\bin" + } else { + Write-Host "" + Write-Host "Your path is missing $cargo_home\bin, you might want to add it." -ForegroundColor Red + Write-Host "" + } +} diff --git a/install-from-binstall-release.sh b/install-from-binstall-release.sh new file mode 100755 index 00000000..b228b9d3 --- /dev/null +++ b/install-from-binstall-release.sh @@ -0,0 +1,75 @@ +#!/bin/sh + +set -eux + +do_curl() { + curl --retry 10 -A "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0" -L --proto '=https' --tlsv1.2 -sSf "$@" +} + +# Set pipefail if it works in a subshell, disregard if unsupported +# shellcheck disable=SC3040 +(set -o pipefail 2> /dev/null) && set -o pipefail + +case "${BINSTALL_VERSION:-}" in + "") ;; # unset + v*) ;; # already includes the `v` + *) BINSTALL_VERSION="v$BINSTALL_VERSION" ;; # Add a leading `v` +esac + +cd "$(mktemp -d)" + +# Fetch binaries from `[..]/releases/latest/download/[..]` if _no_ version is +# given, otherwise from `[..]/releases/download/VERSION/[..]`. Note the shifted +# location of '/download'. +if [ -z "${BINSTALL_VERSION:-}" ]; then + base_url="https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-" +else + base_url="https://github.com/cargo-bins/cargo-binstall/releases/download/${BINSTALL_VERSION}/cargo-binstall-" +fi + +os="$(uname -s)" +if [ "$os" = "Darwin" ]; then + url="${base_url}universal-apple-darwin.zip" + do_curl -O "$url" + unzip cargo-binstall-universal-apple-darwin.zip +elif [ "$os" = "Linux" ]; then + machine="$(uname -m)" + if [ "$machine" = "armv7l" ]; then + machine="armv7" + fi + target="${machine}-unknown-linux-musl" + if [ "$machine" = "armv7" ]; then + target="${target}eabihf" + fi + + url="${base_url}${target}.tgz" + do_curl "$url" | tar -xvzf - +elif [ "${OS-}" = "Windows_NT" ]; then + machine="$(uname -m)" + target="${machine}-pc-windows-msvc" + url="${base_url}${target}.zip" + do_curl -O "$url" + unzip "cargo-binstall-${target}.zip" +else + echo "Unsupported OS ${os}" + exit 1 +fi + +./cargo-binstall --self-install || ./cargo-binstall -y --force cargo-binstall + +CARGO_HOME="${CARGO_HOME:-$HOME/.cargo}" + +case ":$PATH:" in + *":$CARGO_HOME/bin:"*) ;; # Cargo home is already in path + *) needs_cargo_home=1 ;; +esac + +if [ -n "${needs_cargo_home:-}" ]; then + if [ -n "${CI:-}" ] && [ -n "${GITHUB_PATH:-}" ]; then + echo "$CARGO_HOME/bin" >> "$GITHUB_PATH" + else + echo + printf "\033[0;31mYour path is missing %s, you might want to add it.\033[0m\n" "$CARGO_HOME/bin" + echo + fi +fi diff --git a/justfile b/justfile new file mode 100644 index 00000000..b63c682c --- /dev/null +++ b/justfile @@ -0,0 +1,375 @@ +# input variables +ci := env_var_or_default("CI", "") +for-release := env_var_or_default("JUST_FOR_RELEASE", "") +use-cross := env_var_or_default("JUST_USE_CROSS", "") +use-cargo-zigbuild := env_var_or_default("JUST_USE_CARGO_ZIGBUILD", "") +extra-build-args := env_var_or_default("JUST_EXTRA_BUILD_ARGS", "") +extra-features := env_var_or_default("JUST_EXTRA_FEATURES", "") +default-features := env_var_or_default("JUST_DEFAULT_FEATURES", "") +override-features := env_var_or_default("JUST_OVERRIDE_FEATURES", "") +glibc-version := env_var_or_default("GLIBC_VERSION", "") +use-auditable := env_var_or_default("JUST_USE_AUDITABLE", "") +timings := env_var_or_default("JUST_TIMINGS", "") +build-std := env_var_or_default("JUST_BUILD_STD", "") +enable-h3 := env_var_or_default("JUST_ENABLE_H3", "") +cargo-nextest-additional-args := env_var_or_default("CARGO_NEXTEST_ADDITIONAL_ARGS", "") + +export BINSTALL_LOG_LEVEL := if env_var_or_default("RUNNER_DEBUG", "0") == "1" { "debug" } else { "info" } +export BINSTALL_RATE_LIMIT := "30/1" + +cargo := if use-cargo-zigbuild != "" { "cargo-zigbuild" } else if use-cross != "" { "cross" } else { "cargo" } +export CARGO := cargo + +# target information +target-host := `rustc -vV | grep host: | cut -d ' ' -f 2` +target := env_var_or_default("CARGO_BUILD_TARGET", target-host) +target-os := if target =~ "-windows-" { "windows" + } else if target =~ "darwin" { "macos" + } else if target =~ "linux" { "linux" + } else { "unknown" } +target-arch := if target =~ "x86_64" { "x64" + } else if target =~ "i[56]86" { "x86" + } else if target =~ "aarch64" { "arm64" + } else if target =~ "armv7" { "arm32" + } else { "unknown" } +target-libc := if target =~ "gnu" { "gnu" + } else if target =~ "musl" { "musl" + } else { "unknown" } + +# build output location +output-ext := if target-os == "windows" { ".exe" } else { "" } +output-filename := "cargo-binstall" + output-ext +output-profile-folder := if for-release != "" { "release" } else { "debug" } +output-folder := "target" / target / output-profile-folder +output-path := output-folder / output-filename + +# which tool to use for compiling +cargo-bin := if use-auditable != "" { + "cargo-auditable auditable" +} else { + cargo +} + +# cargo compile options +cargo-profile := if for-release != "" { "release" } else { "dev" } + + +ci-or-no := if ci != "" { "ci" } else { "noci" } + +# In release builds in CI, build the std library ourselves so it uses our +# compile profile, and optimise panic messages out with immediate abort. +cargo-buildstd := if build-std != "" { + " -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort" +} else if target == "x86_64h-apple-darwin" { + " -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort" +} else { "" } + +# In musl release builds in CI, statically link gcclibs. +rustc-gcclibs := if (cargo-profile / ci-or-no / target-libc) == "release/ci/musl" { + if use-cargo-zigbuild != "" { "-C link-arg=-static-libgcc" } else { " -C link-arg=-lgcc -C link-arg=-static-libgcc" } +} else { "" } + +# disable default features in CI for debug builds, for speed +cargo-no-default-features := if default-features == "false" { " --no-default-features" + } else if default-features == "true" { "" + } else if (cargo-profile / ci-or-no) == "dev/ci" { " --no-default-features" + } else { "" } + +support-pkg-config := if target == target-host { + if target-os == "linux" { "true" } else { "" } +} else { "" } + +h3-features := if enable-h3 != "" { ",http3" } else { "" } +cargo-features := trim_end_match(if override-features != "" { override-features + h3-features + } else if (cargo-profile / ci-or-no) == "dev/ci" { "git,rustls,fancy-with-backtrace,zstd-thin,log_max_level_debug,zlib-rs" + (if support-pkg-config != "" { ",pkg-config" } else { "" }) + h3-features + extra-features + } else if (cargo-profile / ci-or-no) == "release/ci" { "git,static,rustls,trust-dns,fancy-no-backtrace,zstd-thin,log_release_max_level_debug,cross-lang-fat-lto,zlib-rs" + h3-features + extra-features + } else if extra-features != "" { extra-features + h3-features + } else if enable-h3 != "" { "http3" + } else { "" +}, ",") + +# it seems we can't split debuginfo for non-buildstd builds +# errors with: "Found a record with an unknown abbreviation code" +cargo-split-debuginfo := if cargo-buildstd != "" { " --config='profile.release.split-debuginfo=\"packed\"' --config=profile.release.debug=2" } else { "" } + +# MIR optimisation level (defaults to 2, bring it up to 4 for release builds) +# **DISABLED because it's buggy** +rustc-miropt := "" # if for-release != "" { " -Z mir-opt-level=4" } else { "" } + +# Use rust-lld that is bundled with rustup to speedup linking +# and support for icf=safe. +# +# -Zgcc-ld=lld uses the rust-lld that is bundled with rustup. +# +# TODO: There is ongoing effort to stabilise this and we will need to update +# this once it is merged. +# https://github.com/rust-lang/compiler-team/issues/510 +# +# If cargo-zigbuild is used, then it will provide the lld linker. +# This option is disabled on windows since it not supported. +rust-lld := "" #if use-cargo-zigbuild != "" { +#"" +#} else { +#" -C link-arg=-fuse-ld=lld" +#} + +# ICF: link-time identical code folding +# +# On windows it works out of the box. +rustc-icf := if for-release != "" { + if target-os == "windows" { + " -C link-arg=-Wl,--icf=safe" + } else if target-os == "linux" { + "" + } else { + "" + } +} else { + "" +} + +# Only enable linker-plugin-lto for release +# Also disable this on windows since it uses msvc. +# +# Temporarily disable this on linux due to mismatch llvm version +# } else if target-os == "linux" { +# "-C linker-plugin-lto " +linker-plugin-lto := if for-release == "" { + "" +} else { + "" +} + +target-glibc-ver-postfix := if glibc-version != "" { + if use-cargo-zigbuild != "" { + "." + glibc-version + } else { + "" + } +} else { + "" +} + +cargo-check-args := (" --target ") + (target) + (target-glibc-ver-postfix) + (cargo-buildstd) + (if extra-build-args != "" { " " + extra-build-args } else { "" }) + (cargo-split-debuginfo) +cargo-build-args := (if for-release != "" { " --release" } else { "" }) + (cargo-check-args) + (cargo-no-default-features) + (if cargo-features != "" { " --features " + cargo-features } else { "" }) + (if timings != "" { " --timings" } else { "" }) +export RUSTFLAGS := (linker-plugin-lto) + (rustc-gcclibs) + (rustc-miropt) + (rust-lld) + (rustc-icf) + (if enable-h3 != "" { " --cfg reqwest_unstable" } else { "" }) + + +# libblocksruntime-dev provides compiler-rt +ci-apt-deps := if target == "x86_64-unknown-linux-gnu" { "liblzma-dev libzip-dev libzstd-dev" + } else { "" } + +[linux] +ci-install-deps: + if [ -n "{{ci-apt-deps}}" ]; then sudo apt update && sudo apt install -y --no-install-recommends {{ci-apt-deps}}; fi + if [ -n "{{use-cargo-zigbuild}}" ]; then pip3 install -r zigbuild-requirements.txt; fi + +[macos] +[windows] +ci-install-deps: + +toolchain-name := if cargo-buildstd != "" { "nightly" } else { "stable" } +# x86_64h-apple-darwin does not contain pre-built libstd, instead we will +# install rust-src and use build-std to build it. +target-name := if target == "x86_64h-apple-darwin" { "" } else { target } +default-components := if cargo-buildstd != "" { "rust-src" } else { "" } + +toolchain components=default-components: + rustup toolchain install {{toolchain-name}} {{ if components != "" { "--component " + components } else { "" } }} --no-self-update --profile minimal {{ if target-name != "" { "--target " + target-name } else { "" } }} + rustup override set {{toolchain-name}} + +print-env: + @echo "env RUSTFLAGS='$RUSTFLAGS', CARGO='$CARGO'" + +print-rustflags: + @echo "$RUSTFLAGS" + +build: print-env + {{cargo-bin}} build {{cargo-build-args}} + +check: print-env + {{cargo-bin}} check {{cargo-build-args}} --profile check-only + {{cargo-bin}} check -p binstalk-downloader --no-default-features --profile check-only + {{cargo-bin}} check -p cargo-binstall --no-default-features --features rustls {{cargo-check-args}} --profile check-only + cargo-hack hack check -p binstalk-downloader \ + --feature-powerset \ + --include-features default,json \ + --profile check-only \ + {{cargo-check-args}} + +get-output file outdir=".": + test -d "{{outdir}}" || mkdir -p {{outdir}} + cp -r {{ output-folder / file }} {{outdir}}/{{ file_name(file) }} + -ls -l {{outdir}}/{{ file_name(file) }} + +get-binary outdir=".": (get-output output-filename outdir) + -chmod +x {{ outdir / output-filename }} + +e2e-test file *arguments: (get-binary "e2e-tests") + cd e2e-tests && env -u RUSTFLAGS -u CARGO_BUILD_TARGET bash {{file}}.sh {{output-filename}} {{arguments}} + +e2e-test-live: (e2e-test "live") +e2e-test-subcrate: (e2e-test "subcrate") +e2e-test-manifest-path: (e2e-test "manifest-path") +e2e-test-other-repos: (e2e-test "other-repos") +e2e-test-strategies: (e2e-test "strategies") +e2e-test-version-syntax: (e2e-test "version-syntax") +e2e-test-upgrade: (e2e-test "upgrade") +e2e-test-self-upgrade-no-symlink: (e2e-test "self-upgrade-no-symlink") +e2e-test-uninstall: (e2e-test "uninstall") +e2e-test-no-track: (e2e-test "no-track") +e2e-test-git: (e2e-test "git") +e2e-test-registries: (e2e-test "registries") +e2e-test-signing: (e2e-test "signing") +e2e-test-continue-on-failure: (e2e-test "continue-on-failure") +e2e-test-private-github-repo: (e2e-test "private-github-repo") +e2e-test-self-install: (e2e-test "self-install") +e2e-test-specific-binaries: (e2e-test "specific-binaries") + +# WinTLS (Windows in CI) does not have TLS 1.3 support +[windows] +e2e-test-tls: (e2e-test "tls" "1.2") +[linux] +[macos] +e2e-test-tls: (e2e-test "tls" "1.2") (e2e-test "tls" "1.3") + +# e2e-test-self-install needs to be the last task to run, as it would consume the cargo-binstall binary +e2e-tests: e2e-test-live e2e-test-manifest-path e2e-test-git e2e-test-other-repos e2e-test-strategies e2e-test-version-syntax e2e-test-upgrade e2e-test-tls e2e-test-self-upgrade-no-symlink e2e-test-uninstall e2e-test-subcrate e2e-test-no-track e2e-test-registries e2e-test-signing e2e-test-continue-on-failure e2e-test-private-github-repo e2e-test-specific-binaries e2e-test-self-install + +unit-tests: print-env + cargo test --no-run --target {{target}} + cargo nextest run --target {{target}} --no-tests=pass {{cargo-nextest-additional-args}} + cargo test --doc --target {{target}} + +test: unit-tests build e2e-tests + +clippy: print-env + {{cargo-bin}} clippy --no-deps -- -D clippy::all + +doc: print-env + cargo doc --no-deps --workspace + +fmt: print-env + cargo fmt --all -- --check + +fmt-check: fmt + +lint: clippy fmt-check doc + +# Rm dev-dependencies for `cargo-check` and clippy to speedup compilation. +# This is a workaround for the cargo nightly option `-Z avoid-dev-deps` +avoid-dev-deps: + for crate in ./crates/*; do \ + sed 's/\[dev-dependencies\]/[workaround-avoid-dev-deps]/g' "$crate/Cargo.toml" >"$crate/Cargo.toml.tmp"; \ + mv "$crate/Cargo.toml.tmp" "$crate/Cargo.toml" \ + ; done + +package-dir: + rm -rf packages/prep + mkdir -p packages/prep + cp crates/bin/LICENSE packages/prep + cp README.md packages/prep + -cp minisign.pub packages/prep + +[macos] +package-prepare: build package-dir + just get-binary packages/prep + -just get-output cargo-binstall.dSYM packages/prep + + just get-output detect-wasi{{output-ext}} packages/prep + -just get-output detect-wasi.dSYM packages/prep + + just get-output detect-targets{{output-ext}} packages/prep + -just get-output detect-targets.dSYM packages/prep + +# when https://github.com/rust-lang/cargo/pull/11384 lands, we can use +# -just get-output cargo_binstall.dwp packages/prep +# underscored dwp name needs to remain for debuggers to find the file properly +[linux] +package-prepare: build package-dir + just get-binary packages/prep + -cp {{output-folder}}/deps/cargo_binstall-*.dwp packages/prep/cargo_binstall.dwp + + just get-output detect-wasi packages/prep + -cp {{output-folder}}/deps/detect_wasi-*.dwp packages/prep/detect_wasi.dwp + + just get-output detect-targets packages/prep + -cp {{output-folder}}/deps/detect_target-*.dwp packages/prep/detect_target.dwp + +# underscored pdb name needs to remain for debuggers to find the file properly +# read from deps because sometimes cargo doesn't copy the pdb to the output folder +[windows] +package-prepare: build package-dir + just get-binary packages/prep + -just get-output deps/cargo_binstall.pdb packages/prep + + just get-output detect-wasi.exe packages/prep + -just get-output deps/detect_wasi.pdb packages/prep + + just get-output detect-targets.exe packages/prep + -just get-output deps/detect_target.pdb packages/prep + +# we don't get dSYM bundles for universal binaries; unsure if it's even a thing +[macos] +lipo-prepare: package-dir + just target=aarch64-apple-darwin build get-binary packages/prep/arm64 + just target=x86_64-apple-darwin build get-binary packages/prep/x64 + just target=x86_64h-apple-darwin build get-binary packages/prep/x64h + + just target=aarch64-apple-darwin get-binary packages/prep/arm64 + just target=x86_64-apple-darwin get-binary packages/prep/x64 + just target=x86_64h-apple-darwin get-binary packages/prep/x64h + lipo -create -output packages/prep/{{output-filename}} packages/prep/{arm64,x64,x64h}/{{output-filename}} + + just target=aarch64-apple-darwin get-output detect-wasi{{output-ext}} packages/prep/arm64 + just target=x86_64-apple-darwin get-output detect-wasi{{output-ext}} packages/prep/x64 + just target=x86_64h-apple-darwin get-output detect-wasi{{output-ext}} packages/prep/x64h + lipo -create -output packages/prep/detect-wasi{{output-ext}} packages/prep/{arm64,x64,x64h}/detect-wasi{{output-ext}} + + just target=aarch64-apple-darwin get-output detect-targets{{output-ext}} packages/prep/arm64 + just target=x86_64-apple-darwin get-output detect-targets{{output-ext}} packages/prep/x64 + just target=x86_64h-apple-darwin get-output detect-targets{{output-ext}} packages/prep/x64h + lipo -create -output packages/prep/detect-targets{{output-ext}} packages/prep/{arm64,x64,x64h}/detect-targets{{output-ext}} + + rm -rf packages/prep/{arm64,x64,x64h} + + +[linux] +package: package-prepare + cd packages/prep && tar cv {{output-filename}} | gzip -9 > "../cargo-binstall-{{target}}.tgz" + cd packages/prep && tar cv * | gzip -9 > "../cargo-binstall-{{target}}.full.tgz" + +[macos] +package: package-prepare + cd packages/prep && zip -r -9 "../cargo-binstall-{{target}}.zip" {{output-filename}} + cd packages/prep && zip -r -9 "../cargo-binstall-{{target}}.full.zip" * + +[windows] +package: package-prepare + cd packages/prep && 7z a -mx9 "../cargo-binstall-{{target}}.zip" {{output-filename}} + cd packages/prep && 7z a -mx9 "../cargo-binstall-{{target}}.full.zip" * + +[macos] +package-lipo: lipo-prepare + cd packages/prep && zip -r -9 "../cargo-binstall-universal-apple-darwin.zip" {{output-filename}} + cd packages/prep && zip -r -9 "../cargo-binstall-universal-apple-darwin.full.zip" * + +# assuming x64 and arm64 packages are already built, extract and lipo them +[macos] +repackage-lipo: package-dir + set -euxo pipefail + + mkdir -p packages/prep/{arm64,x64,x64h} + cd packages/prep/x64 && unzip -o "../../cargo-binstall-x86_64-apple-darwin.full.zip" + cd packages/prep/x64h && unzip -o "../../cargo-binstall-x86_64h-apple-darwin.full.zip" + cd packages/prep/arm64 && unzip -o "../../cargo-binstall-aarch64-apple-darwin.full.zip" + + lipo -create -output packages/prep/{{output-filename}} packages/prep/{arm64,x64,x64h}/{{output-filename}} + lipo -create -output packages/prep/detect-wasi packages/prep/{arm64,x64,x64h}/detect-wasi + lipo -create -output packages/prep/detect-targets packages/prep/{arm64,x64,x64h}/detect-targets + + ./packages/prep/{{output-filename}} -vV + + rm -rf packages/prep/{arm64,x64,x64h} + cd packages/prep && zip -9 "../cargo-binstall-universal-apple-darwin.zip" {{output-filename}} + cd packages/prep && zip -9 "../cargo-binstall-universal-apple-darwin.full.zip" * diff --git a/release-plz.toml b/release-plz.toml new file mode 100644 index 00000000..37bb12f8 --- /dev/null +++ b/release-plz.toml @@ -0,0 +1,7 @@ +[workspace] +git_release_latest = false # don't set release as latest release + +[[package]] +name = "cargo-binstall" +release = false # don't process this package + diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..05e6ca1b --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "stable" +profile = "minimal" +components = ["rustfmt", "clippy"] diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index b5ec71af..00000000 --- a/src/main.rs +++ /dev/null @@ -1,379 +0,0 @@ -use std::time::Duration; -use std::path::{PathBuf, Path}; - -use log::{debug, info, error, LevelFilter}; -use simplelog::{TermLogger, ConfigBuilder, TerminalMode}; - -use structopt::StructOpt; -use serde::{Serialize, Deserialize}; - -use crates_io_api::AsyncClient; -use cargo_toml::Manifest; - -use tempdir::TempDir; -use flate2::read::GzDecoder; -use tar::Archive; - -use tinytemplate::TinyTemplate; - -/// Compiled target triple, used as default for binary fetching -const TARGET: &'static str = env!("TARGET"); - -/// Default binary path for use if no path is specified -const DEFAULT_BIN_PATH: &'static str = "{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.{ format }"; - -/// Binary format enumeration -#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] -#[derive(strum_macros::Display, strum_macros::EnumString, strum_macros::EnumVariantNames)] -#[strum(serialize_all = "snake_case")] -#[serde(rename_all = "snake_case")] -pub enum PkgFmt { - /// Download format is TAR (uncompressed) - Tar, - /// Download format is TGZ (TAR + GZip) - Tgz, - /// Download format is raw / binary - Bin, -} - -#[derive(Debug, StructOpt)] -struct Options { - /// Crate name to install - #[structopt()] - name: String, - - /// Crate version to install - #[structopt(long)] - version: Option, - - /// Override the package path template. - /// If no `metadata.pkg_url` key is set or `--pkg-url` argument provided, this - /// defaults to `{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.tgz` - #[structopt(long)] - pkg_url: Option, - - /// Override format for binary file download. - /// Defaults to `tgz` - #[structopt(long)] - pkg_fmt: Option, - - /// Override the package name. - /// This is only useful for diagnostics when using the default `pkg_url` - /// as you can otherwise customise this in the path. - /// Defaults to the crate name. - #[structopt(long)] - pkg_name: Option, - - /// Override install path for downloaded binary. - /// Defaults to `$HOME/.cargo/bin` - #[structopt(long)] - install_path: Option, - - /// Override binary target, ignoring compiled version - #[structopt(long, default_value = TARGET)] - target: String, - - /// Override manifest source. - /// This skips searching crates.io for a manifest and uses - /// the specified path directly, useful for debugging - #[structopt(long)] - manifest_path: Option, - - /// Utility log level - #[structopt(long, default_value = "info")] - log_level: LevelFilter, - - /// Do not cleanup temporary files on success - #[structopt(long)] - no_cleanup: bool, -} - - -/// Metadata for cargo-binstall exposed via cargo.toml -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct Meta { - /// Path template override for binary downloads - pub pkg_url: Option, - /// Package name override for binary downloads - pub pkg_name: Option, - /// Format override for binary downloads - pub pkg_fmt: Option, -} - -/// Template for constructing download paths -#[derive(Clone, Debug, Serialize)] -pub struct Context { - name: String, - repo: Option, - target: String, - version: String, - format: PkgFmt, -} - - -#[tokio::main] -async fn main() -> Result<(), anyhow::Error> { - - // Filter extraneous arg when invoked by cargo - // `cargo run -- --help` gives ["target/debug/cargo-binstall", "--help"] - // `cargo binstall --help` gives ["/home/ryan/.cargo/bin/cargo-binstall", "binstall", "--help"] - let mut args: Vec = std::env::args().collect(); - if args.len() > 1 && args[1] == "binstall" { - args.remove(1); - } - - // Load options - let opts = Options::from_iter(args.iter()); - - // Setup logging - let mut log_config = ConfigBuilder::new(); - log_config.add_filter_ignore("hyper".to_string()); - log_config.add_filter_ignore("reqwest".to_string()); - log_config.set_location_level(LevelFilter::Off); - TermLogger::init(opts.log_level, log_config.build(), TerminalMode::Mixed).unwrap(); - - // Create a temporary directory for downloads etc. - let temp_dir = TempDir::new("cargo-binstall")?; - - // Fetch crate via crates.io, git, or use a local manifest path - // TODO: work out which of these to do based on `opts.name` - let crate_path = match opts.manifest_path { - Some(p) => p, - None => fetch_crate_cratesio(&opts.name, opts.version.as_deref(), temp_dir.path()).await?, - }; - - // Read cargo manifest - let manifest_path = crate_path.join("Cargo.toml"); - - debug!("Reading manifest: {}", manifest_path.to_str().unwrap()); - let package = match Manifest::::from_path_with_metadata(&manifest_path) { - Ok(m) => m.package.unwrap(), - Err(e) => { - error!("Error reading manifest '{}': {:?}", manifest_path.to_str().unwrap(), e); - return Err(e.into()); - }, - }; - - let meta = package.metadata; - debug!("Retrieved metadata: {:?}", meta); - - // Select which binary path to use - let pkg_url = match (opts.pkg_url, meta.as_ref().map(|m| m.pkg_url.clone() ).flatten()) { - (Some(p), _) => { - info!("Using package url override: '{}'", p); - p - }, - (_, Some(m)) => { - info!("Using package url: '{}'", &m); - m - }, - _ => { - info!("No `pkg-url` key found in Cargo.toml or `--pkg-url` argument provided"); - info!("Using default url: {}", DEFAULT_BIN_PATH); - DEFAULT_BIN_PATH.to_string() - }, - }; - - // Select bin format to use - let pkg_fmt = match (opts.pkg_fmt, meta.as_ref().map(|m| m.pkg_fmt.clone() ).flatten()) { - (Some(o), _) => o, - (_, Some(m)) => m.clone(), - _ => PkgFmt::Tgz, - }; - - // Override package name if required - let pkg_name = match (&opts.pkg_name, meta.as_ref().map(|m| m.pkg_name.clone() ).flatten()) { - (Some(o), _) => o.clone(), - (_, Some(m)) => m, - _ => opts.name.clone(), - }; - - // Generate context for interpolation - let ctx = Context { - name: pkg_name.to_string(), - repo: package.repository, - target: opts.target.clone(), - version: package.version.clone(), - format: pkg_fmt.clone(), - }; - - debug!("Using context: {:?}", ctx); - - // Interpolate version / target / etc. - let mut tt = TinyTemplate::new(); - tt.add_template("path", &pkg_url)?; - let rendered = tt.render("path", &ctx)?; - - info!("Downloading package from: '{}'", rendered); - - // Download package - let pkg_path = temp_dir.path().join(format!("pkg-{}.{}", pkg_name, pkg_fmt)); - download(&rendered, pkg_path.to_str().unwrap()).await?; - - - if opts.no_cleanup { - // Do not delete temporary directory - let _ = temp_dir.into_path(); - } - - // TODO: check signature - - // Compute install directory - let install_path = match get_install_path(opts.install_path) { - Some(p) => p, - None => { - error!("No viable install path found of specified, try `--install-path`"); - return Err(anyhow::anyhow!("No install path found or specified")); - } - }; - - // Install package - info!("Installing to: '{}'", install_path); - extract(&pkg_path, pkg_fmt, &install_path)?; - - - info!("Installation done!"); - - Ok(()) -} - -/// Download a file from the provided URL to the provided path -async fn download>(url: &str, path: P) -> Result<(), anyhow::Error> { - - debug!("Downloading from: '{}'", url); - - let resp = reqwest::get(url).await?; - - if !resp.status().is_success() { - error!("Download error: {}", resp.status()); - return Err(anyhow::anyhow!(resp.status())); - } - - let bytes = resp.bytes().await?; - - debug!("Download OK, writing to file: '{:?}'", path.as_ref()); - - std::fs::write(&path, bytes)?; - - Ok(()) -} - -fn extract, P: AsRef>(source: S, fmt: PkgFmt, path: P) -> Result<(), anyhow::Error> { - match fmt { - PkgFmt::Tar => { - // Extract to install dir - debug!("Extracting from archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref()); - - let dat = std::fs::File::open(source)?; - let mut tar = Archive::new(dat); - - tar.unpack(path)?; - }, - PkgFmt::Tgz => { - // Extract to install dir - debug!("Decompressing from archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref()); - - let dat = std::fs::File::open(source)?; - let tar = GzDecoder::new(dat); - let mut tgz = Archive::new(tar); - - tgz.unpack(path)?; - }, - PkgFmt::Bin => { - debug!("Copying data from archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref()); - // Copy to install dir - std::fs::copy(source, path)?; - }, - }; - - Ok(()) -} - -/// Fetch a crate by name and version from crates.io -async fn fetch_crate_cratesio(name: &str, version: Option<&str>, temp_dir: &Path) -> Result { - // Build crates.io api client and fetch info - // TODO: support git-based fetches (whole repo name rather than just crate name) - let api_client = AsyncClient::new("cargo-binstall (https://github.com/ryankurte/cargo-binstall)", Duration::from_millis(100))?; - - info!("Fetching information for crate: '{}'", name); - - // Fetch overall crate info - let info = match api_client.get_crate(name.as_ref()).await { - Ok(i) => i, - Err(e) => { - error!("Error fetching information for crate {}: {}", name, e); - return Err(e.into()) - } - }; - - // Use specified or latest version - let version_num = match version { - Some(v) => v.to_string(), - None => info.crate_data.max_version, - }; - - // Fetch crates.io information for the specified version - // TODO: could do a semver match and sort here? - let version = match info.versions.iter().find(|v| v.num == version_num) { - Some(v) => v, - None => { - error!("No crates.io information found for crate: '{}' version: '{}'", - name, version_num); - return Err(anyhow::anyhow!("No crate information found")); - } - }; - - info!("Found information for crate version: '{}'", version.num); - - // Download crate to temporary dir (crates.io or git?) - let crate_url = format!("https://crates.io/{}", version.dl_path); - let tgz_path = temp_dir.join(format!("{}.tgz", name)); - - debug!("Fetching crate from: {}", crate_url); - - // Download crate - download(&crate_url, &tgz_path).await?; - - // Decompress downloaded tgz - debug!("Decompressing crate archive"); - extract(&tgz_path, PkgFmt::Tgz, &temp_dir)?; - let crate_path = temp_dir.join(format!("{}-{}", name, version_num)); - - // Return crate directory - Ok(crate_path) -} - -/// Fetch install path -/// roughly follows https://doc.rust-lang.org/cargo/commands/cargo-install.html#description -fn get_install_path(opt: Option) -> Option { - // Command line override first first - if let Some(p) = opt { - return Some(p) - } - - // Environmental variables - if let Ok(p) = std::env::var("CARGO_INSTALL_ROOT") { - return Some(format!("{}/bin", p)) - } - if let Ok(p) = std::env::var("CARGO_HOME") { - return Some(format!("{}/bin", p)) - } - - // Standard $HOME/.cargo/bin - if let Some(mut d) = dirs::home_dir() { - d.push(".cargo/bin"); - let p = d.as_path(); - - if p.exists() { - return Some(p.to_str().unwrap().to_owned()); - } - } - - // Local executable dir if no cargo is found - if let Some(d) = dirs::executable_dir() { - return Some(d.to_str().unwrap().to_owned()); - } - - None -} \ No newline at end of file diff --git a/zigbuild-requirements.txt b/zigbuild-requirements.txt new file mode 100644 index 00000000..3f2a1dfc --- /dev/null +++ b/zigbuild-requirements.txt @@ -0,0 +1,5 @@ +###### Requirements without Version Specifiers ###### +cargo-zigbuild + +###### Requirements with Version Specifiers ###### +ziglang