├── .config └── nextest.toml ├── .editorconfig ├── .github ├── FUNDING.yml ├── actions │ └── just-setup │ │ └── action.yml ├── dependabot.yml ├── scripts │ ├── ephemeral-crate.sh │ ├── ephemeral-gen.sh │ ├── ephemeral-sign.sh │ ├── release-pr-template.ejs │ └── test-detect-targets-musl.sh └── workflows │ ├── cache-cleanup.yml │ ├── ci.yml │ ├── gh-action.yml │ ├── install-script.yml │ ├── release-cli.yml │ ├── release-packages.yml │ ├── release-plz.yml │ ├── release-pr.yml │ ├── release.yml │ ├── shellcheck.yml │ └── upgrade-transitive-deps.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── SIGNING.md ├── SUPPORT.md ├── action.yml ├── cleanup-cache.sh ├── crates ├── atomic-file-install │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ └── src │ │ └── lib.rs ├── bin │ ├── Cargo.toml │ ├── LICENSE │ ├── build.rs │ ├── manifest.rc │ ├── release.toml │ ├── src │ │ ├── args.rs │ │ ├── bin_util.rs │ │ ├── entry.rs │ │ ├── gh_token.rs │ │ ├── git_credentials.rs │ │ ├── install_path.rs │ │ ├── lib.rs │ │ ├── logging.rs │ │ ├── main.rs │ │ ├── main_impl.rs │ │ ├── signal.rs │ │ └── ui.rs │ └── windows.manifest ├── binstalk-bins │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE │ └── src │ │ └── lib.rs ├── binstalk-downloader │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ └── src │ │ ├── download.rs │ │ ├── download │ │ ├── async_extracter.rs │ │ ├── async_tar_visitor.rs │ │ ├── extracted_files.rs │ │ ├── extracter.rs │ │ └── zip_extraction.rs │ │ ├── lib.rs │ │ ├── remote.rs │ │ ├── remote │ │ ├── certificate.rs │ │ ├── delay_request.rs │ │ ├── request_builder.rs │ │ ├── resolver.rs │ │ └── tls_version.rs │ │ └── utils.rs ├── binstalk-fetchers │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE │ └── src │ │ ├── common.rs │ │ ├── futures_resolver.rs │ │ ├── gh_crate_meta.rs │ │ ├── gh_crate_meta │ │ └── hosting.rs │ │ ├── lib.rs │ │ ├── quickinstall.rs │ │ └── signing.rs ├── binstalk-git-repo-api │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ └── src │ │ ├── gh_api_client.rs │ │ ├── gh_api_client │ │ ├── common.rs │ │ ├── error.rs │ │ ├── release_artifacts.rs │ │ └── repo_info.rs │ │ └── lib.rs ├── binstalk-manifests │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ └── src │ │ ├── binstall_crates_v1.rs │ │ ├── cargo_config.rs │ │ ├── cargo_crates_v1.rs │ │ ├── cargo_crates_v1 │ │ └── crate_version_source.rs │ │ ├── crates_manifests.rs │ │ ├── helpers.rs │ │ └── lib.rs ├── binstalk-registry │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ └── src │ │ ├── common.rs │ │ ├── crates_io_registry.rs │ │ ├── git_registry.rs │ │ ├── lib.rs │ │ ├── sparse_registry.rs │ │ ├── vfs.rs │ │ └── visitor.rs ├── binstalk-types │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ └── src │ │ ├── cargo_toml_binstall.rs │ │ ├── cargo_toml_binstall │ │ └── package_formats.rs │ │ ├── crate_info.rs │ │ └── lib.rs ├── binstalk │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE │ ├── src │ │ ├── errors.rs │ │ ├── helpers.rs │ │ ├── helpers │ │ │ ├── jobserver_client.rs │ │ │ ├── lazy_gh_api_client.rs │ │ │ ├── target_triple.rs │ │ │ └── tasks.rs │ │ ├── lib.rs │ │ ├── ops.rs │ │ └── ops │ │ │ ├── resolve.rs │ │ │ └── resolve │ │ │ ├── crate_name.rs │ │ │ ├── resolution.rs │ │ │ └── version_ext.rs │ └── tests │ │ ├── parse-meta.Cargo.toml │ │ └── parse-meta.rs ├── cargo-toml-workspace │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ └── src │ │ └── lib.rs ├── detect-targets │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ ├── build.rs │ └── src │ │ ├── desired_targets.rs │ │ ├── detect.rs │ │ ├── detect │ │ ├── linux.rs │ │ ├── macos.rs │ │ └── windows.rs │ │ ├── lib.rs │ │ └── main.rs ├── detect-wasi │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ └── src │ │ ├── bin │ │ └── detect-wasi.rs │ │ ├── lib.rs │ │ ├── miniwasi.wasm │ │ └── miniwasi.wast ├── fs-lock │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ └── src │ │ └── lib.rs └── normalize-path │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ └── src │ └── lib.rs ├── e2e-tests ├── continue-on-failure.sh ├── fake-cargo │ └── cargo ├── git.sh ├── live.sh ├── manifest-path.sh ├── manifests │ ├── bitbucket-test-Cargo.toml │ ├── github-test-Cargo.toml │ ├── github-test-Cargo2.toml │ ├── gitlab-test-Cargo.toml │ ├── private-github-repo-test-Cargo.toml │ ├── signing-Cargo.toml │ ├── strategies-test-Cargo.toml │ ├── strategies-test-Cargo2.toml │ ├── strategies-test-override-Cargo.toml │ └── workspace │ │ ├── Cargo.toml │ │ ├── b │ │ └── c │ │ │ └── d │ │ │ └── e │ │ │ └── f │ │ │ └── Cargo.toml │ │ └── crates │ │ └── a │ │ └── b │ │ └── c │ │ └── d │ │ └── e │ │ ├── cargo-binstall │ │ └── Cargo.toml │ │ └── cargo-watch │ │ ├── Cargo.toml │ │ └── src │ │ └── main.rs ├── no-track.sh ├── other-repos.sh ├── private-github-repo.sh ├── registries.sh ├── self-install.sh ├── self-upgrade-no-symlink.sh ├── signing.sh ├── signing │ ├── minisign.key │ ├── minisign.pub │ ├── server.ext │ ├── server.py │ ├── signing-test.exe.nasm │ ├── signing-test.tar │ ├── signing-test.tar.sig │ └── wait-for-server.sh ├── strategies.sh ├── subcrate.sh ├── tls.sh ├── uninstall.sh ├── upgrade.sh └── version-syntax.sh ├── install-from-binstall-release.ps1 ├── install-from-binstall-release.sh ├── justfile ├── release-plz.toml ├── rust-toolchain.toml └── zigbuild-requirements.txt /.config/nextest.toml: -------------------------------------------------------------------------------- 1 | [test-groups] 2 | rate-limited = { max-threads = 1 } 3 | 4 | [[profile.default.overrides]] 5 | filter = 'test(rate_limited::)' 6 | test-group = 'rate-limited' 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [tests/snapshots/*] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{cff,yml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [NobodyXu] 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot dependency version checks / updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | # Workflow files stored in the 7 | # default location of `.github/workflows` 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | - package-ecosystem: "cargo" 12 | directory: "/" 13 | schedule: 14 | # Only run dependabot after all compatible upgrades and transitive deps 15 | # are done to reduce amount of PRs opened. 16 | interval: "weekly" 17 | day: "saturday" 18 | groups: 19 | deps: 20 | patterns: 21 | - "*" 22 | -------------------------------------------------------------------------------- /.github/scripts/ephemeral-crate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euxo pipefail 4 | 5 | cat >> crates/bin/Cargo.toml <> age.key <<< "$AGE_KEY_SECRET" 7 | 8 | set -x 9 | 10 | rage --decrypt --identity age.key --output minisign.key minisign.key.age 11 | 12 | ts=$(node -e 'console.log((new Date).toISOString())') 13 | git=$(git rev-parse HEAD) 14 | comment="gh=$GITHUB_REPOSITORY git=$git ts=$ts run=$GITHUB_RUN_ID" 15 | 16 | for file in "$@"; do 17 | rsign sign -W -s minisign.key -x "$file.sig" -t "$comment" "$file" 18 | done 19 | 20 | rm age.key minisign.key 21 | -------------------------------------------------------------------------------- /.github/scripts/release-pr-template.ejs: -------------------------------------------------------------------------------- 1 | <% if (pr.metaComment) { %> 2 | 3 | <% } %> 4 | 5 | This is a release PR for **<%= crate.name %>** version **<%= version.actual %>**<% 6 | if (version.actual != version.desired) { 7 | %> (performing a <%= version.desired %> bump).<% 8 | } else { 9 | %>.<% 10 | } 11 | %> 12 | 13 | **Use squash merge.** 14 | 15 | <% if (crate.name == "cargo-binstall") { %> 16 | Upon merging, this will automatically create the tag `v<%= version.actual %>`, build the CLI, 17 | create a GitHub release with the release notes below 18 | <% } else { %> 19 | Upon merging, this will create the tag `<%= crate.name %>-v<%= version.actual %>` 20 | <% } %>, and CI will publish to crates.io on merge of this PR. 21 | 22 | **To trigger builds initially, close and then immediately re-open this PR once.** 23 | 24 | <% if (pr.releaseNotes) { %> 25 | --- 26 | 27 | _Edit release notes into the section below:_ 28 | 29 | 30 | ### Release notes 31 | 32 | _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`._ 33 | 34 | #### In this release: 35 | 36 | - 37 | 38 | #### Other changes: 39 | 40 | - 41 | <% } %> 42 | -------------------------------------------------------------------------------- /.github/scripts/test-detect-targets-musl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -exuo pipefail 4 | 5 | TARGET=${1?} 6 | 7 | [ "$(detect-targets)" = "$TARGET" ] 8 | 9 | apk update 10 | apk add gcompat 11 | 12 | ls -lsha /lib 13 | 14 | GNU_TARGET=${TARGET//musl/gnu} 15 | 16 | [ "$(detect-targets)" = "$(printf '%s\n%s' "$GNU_TARGET" "$TARGET")" ] 17 | 18 | echo 19 | -------------------------------------------------------------------------------- /.github/workflows/cache-cleanup.yml: -------------------------------------------------------------------------------- 1 | name: Cleanup caches for closed PRs 2 | 3 | on: 4 | # Run twice every day to remove the cache so that the caches from the closed prs 5 | # are removed. 6 | schedule: 7 | - cron: "0 17 * * *" 8 | - cron: "30 18 * * *" 9 | workflow_dispatch: 10 | 11 | jobs: 12 | cleanup: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Cleanup 17 | run: | 18 | set -euxo pipefail 19 | 20 | gh extension install actions/gh-actions-cache 21 | 22 | export REPO="${{ github.repository }}" 23 | 24 | # Setting this to not fail the workflow while deleting cache keys. 25 | set +e 26 | 27 | # Remove pull requests cache, since they cannot be reused 28 | gh pr list --state closed -L 20 --json number --jq '.[]|.number' | ( 29 | while IFS='$\n' read -r closed_pr; do 30 | BRANCH="refs/pull/${closed_pr}/merge" ./cleanup-cache.sh 31 | done 32 | ) 33 | # Remove merge queue cache, since they cannot be reused 34 | gh actions-cache list -L 100 | cut -f 3 | (grep 'gh-readonly-queue' || true) | sort -u | ( 35 | while IFS='$\n' read -r branch; do 36 | BRANCH="$branch" ./cleanup-cache.sh 37 | done 38 | ) 39 | env: 40 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | -------------------------------------------------------------------------------- /.github/workflows/gh-action.yml: -------------------------------------------------------------------------------- 1 | name: Test GitHub Action installer 2 | on: 3 | merge_group: 4 | pull_request: 5 | paths: 6 | - install-from-binstall-release.ps1 7 | - install-from-binstall-release.sh 8 | - action.yml 9 | push: 10 | branches: 11 | - main 12 | paths: 13 | - install-from-binstall-release.ps1 14 | - install-from-binstall-release.sh 15 | - action.yml 16 | 17 | jobs: 18 | test-gha-installer: 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | matrix: 22 | os: [macos-latest, ubuntu-latest, windows-latest] 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Install cargo-binstall 27 | uses: ./ # uses action.yml from root of the repo 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.CI_RELEASE_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} 30 | 31 | - name: Verify successful installation - display cargo-binstall's help 32 | run: cargo binstall --help 33 | 34 | - name: Verify successful installation - install example binary using cargo-binstall 35 | run: cargo binstall -y ripgrep 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.CI_RELEASE_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} 38 | 39 | - name: Verify successful installation - display help of installed binary 40 | run: rg --help 41 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | release-plz: 14 | name: Release-plz 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Install Rust toolchain 22 | run: rustup toolchain install stable --no-self-update --profile minimal 23 | - name: Run release-plz 24 | uses: MarcoIeni/release-plz-action@v0.5 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/release-pr.yml: -------------------------------------------------------------------------------- 1 | name: Open cargo-binstall release PR 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: Version to release 7 | required: true 8 | type: string 9 | default: patch 10 | 11 | permissions: 12 | pull-requests: write 13 | 14 | jobs: 15 | make-release-pr: 16 | permissions: 17 | id-token: write # Enable OIDC 18 | pull-requests: write 19 | contents: write 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Configure toolchain 24 | run: | 25 | rustup toolchain install --profile minimal --no-self-update nightly 26 | rustup default nightly 27 | - uses: chainguard-dev/actions/setup-gitsign@main 28 | - name: Install cargo-release 29 | uses: taiki-e/install-action@v2 30 | with: 31 | tool: cargo-release,cargo-semver-checks 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.CI_RELEASE_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} 34 | 35 | - run: rustup toolchain install stable --no-self-update --profile minimal 36 | - uses: cargo-bins/release-pr@v2.1.3 37 | with: 38 | github-token: ${{ secrets.GITHUB_TOKEN }} 39 | version: ${{ inputs.version }} 40 | crate-path: crates/bin 41 | pr-label: release 42 | pr-release-notes: true 43 | pr-template-file: .github/scripts/release-pr-template.ejs 44 | check-semver: false 45 | check-package: true 46 | env: 47 | RUSTFLAGS: --cfg reqwest_unstable 48 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: On release 2 | on: 3 | pull_request: 4 | types: closed 5 | branches: [main] # target branch of release PRs 6 | 7 | jobs: 8 | info: 9 | if: github.event.pull_request.merged 10 | 11 | outputs: 12 | is-release: ${{ steps.meta.outputs.is-release }} 13 | crate: ${{ steps.meta.outputs.crates-names }} 14 | version: ${{ steps.meta.outputs.version-actual }} 15 | notes: ${{ steps.meta.outputs.notes }} 16 | 17 | runs-on: ubuntu-latest 18 | steps: 19 | - id: meta 20 | uses: cargo-bins/release-meta@v1 21 | with: 22 | event-data: ${{ toJSON(github.event) }} 23 | extract-notes-under: '### Release notes' 24 | 25 | release-lib: 26 | if: needs.info.outputs.is-release == 'true' && needs.info.outputs.crate != 'cargo-binstall' 27 | needs: info 28 | permissions: 29 | contents: write 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - run: rustup toolchain install stable --no-self-update --profile minimal 34 | - name: Push lib release tag 35 | if: needs.info.outputs.crate != 'cargo-binstall' 36 | uses: mathieudutour/github-tag-action@v6.2 37 | with: 38 | github_token: ${{ secrets.GITHUB_TOKEN }} 39 | custom_tag: ${{ needs.info.outputs.version }} 40 | tag_prefix: ${{ needs.info.outputs.crate }}-v 41 | - name: Publish to crates.io 42 | run: | 43 | cargo publish -p '${{ needs.info.outputs.crate }}' 44 | env: 45 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 46 | 47 | release-cli: 48 | if: needs.info.outputs.crate == 'cargo-binstall' 49 | needs: info 50 | uses: ./.github/workflows/release-cli.yml 51 | secrets: inherit 52 | with: 53 | info: ${{ toJSON(needs.info.outputs) }} 54 | 55 | -------------------------------------------------------------------------------- /.github/workflows/shellcheck.yml: -------------------------------------------------------------------------------- 1 | name: Shellcheck 2 | 3 | on: 4 | merge_group: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | - synchronize 10 | paths: 11 | - '**.sh' 12 | push: 13 | branches: 14 | - main 15 | paths: 16 | - '**.sh' 17 | 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.ref || github.event.pull_request.number || github.sha }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | shellcheck: 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: taiki-e/install-action@v2 29 | with: 30 | tool: fd-find 31 | - name: shellcheck 32 | run: fd -e sh -t f -X shellcheck 33 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-transitive-deps.yml: -------------------------------------------------------------------------------- 1 | name: Upgrade transitive dependencies 2 | 3 | on: 4 | workflow_dispatch: # Allow running on-demand 5 | schedule: 6 | - cron: "0 3 * * 5" 7 | 8 | jobs: 9 | upgrade: 10 | name: Upgrade & Open Pull Request 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | persist-credentials: true 16 | 17 | - name: Generate branch name 18 | run: | 19 | git checkout -b deps/transitive/${{ github.run_id }} 20 | 21 | - name: Install rust 22 | run: | 23 | rustup toolchain install stable --no-self-update --profile minimal 24 | 25 | - name: Upgrade transitive dependencies 26 | run: cargo update --aggressive 27 | 28 | - name: Detect changes 29 | id: changes 30 | run: 31 | # This output boolean tells us if the dependencies have actually changed 32 | echo "count=$(git status --porcelain=v1 | wc -l)" >> $GITHUB_OUTPUT 33 | 34 | - name: Commit and push changes 35 | # Only push if changes exist 36 | if: steps.changes.outputs.count > 0 37 | run: | 38 | git config user.name github-actions 39 | git config user.email github-actions@github.com 40 | git commit -am "dep: Upgrade transitive dependencies" 41 | git push origin HEAD 42 | 43 | - name: Open pull request if needed 44 | if: steps.changes.outputs.count > 0 45 | env: 46 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | run: | 48 | gh pr create --base main --label 'PR: dependencies' --title 'dep: Upgrade transitive dependencies' --body 'Update dependencies' --head $(git branch --show-current) 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | /packages 4 | /e2e-tests/cargo-binstall* 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "crates/atomic-file-install", 5 | "crates/bin", 6 | "crates/binstalk", 7 | "crates/binstalk-bins", 8 | "crates/binstalk-fetchers", 9 | "crates/binstalk-registry", 10 | "crates/binstalk-manifests", 11 | "crates/binstalk-types", 12 | "crates/binstalk-downloader", 13 | "crates/cargo-toml-workspace", 14 | "crates/detect-wasi", 15 | "crates/fs-lock", 16 | "crates/normalize-path", 17 | "crates/detect-targets", 18 | "crates/binstalk-git-repo-api", 19 | ] 20 | 21 | [profile.release] 22 | opt-level = 3 23 | lto = true 24 | codegen-units = 1 25 | panic = "abort" 26 | strip = "symbols" 27 | 28 | [profile.release.build-override] 29 | inherits = "dev.build-override" 30 | 31 | [profile.release.package."tokio-tar"] 32 | opt-level = "z" 33 | 34 | [profile.release.package."binstall-tar"] 35 | opt-level = "z" 36 | 37 | [profile.dev] 38 | opt-level = 0 39 | debug = true 40 | lto = false 41 | debug-assertions = true 42 | overflow-checks = true 43 | codegen-units = 32 44 | 45 | # Set the default for dependencies on debug. 46 | [profile.dev.package."*"] 47 | opt-level = 3 48 | 49 | [profile.dev.package."tokio-tar"] 50 | opt-level = "z" 51 | 52 | [profile.dev.package."binstall-tar"] 53 | opt-level = "z" 54 | 55 | [profile.dev.build-override] 56 | inherits = "dev" 57 | debug = false 58 | debug-assertions = false 59 | overflow-checks = false 60 | incremental = false 61 | 62 | [profile.check-only] 63 | inherits = "dev" 64 | debug = false 65 | debug-assertions = false 66 | overflow-checks = false 67 | panic = "abort" 68 | 69 | [profile.check-only.build-override] 70 | inherits = "check-only" 71 | 72 | [profile.check-only.package."*"] 73 | inherits = "check-only" 74 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Install cargo-binstall' 2 | description: 'Install the latest version of cargo-binstall tool' 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Install cargo-binstall 8 | if: runner.os != 'Windows' 9 | shell: sh 10 | run: | 11 | set -eu 12 | (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 13 | - name: Install cargo-binstall 14 | if: runner.os == 'Windows' 15 | run: Set-ExecutionPolicy Unrestricted -Scope Process; iex (iwr "https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.ps1").Content 16 | shell: powershell 17 | -------------------------------------------------------------------------------- /cleanup-cache.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -uxo pipefail 4 | 5 | REPO="${REPO?}" 6 | BRANCH="${BRANCH?}" 7 | 8 | while true; do 9 | echo "Fetching list of cache key for $BRANCH" 10 | cacheKeysForPR="$(gh actions-cache list -R "$REPO" -B "$BRANCH" -L 100 | cut -f 1)" 11 | 12 | if [ -z "$cacheKeysForPR" ]; then 13 | break 14 | fi 15 | 16 | echo "Deleting caches..." 17 | for cacheKey in $cacheKeysForPR 18 | do 19 | echo Removing "$cacheKey" 20 | gh actions-cache delete "$cacheKey" -R "$REPO" -B "$BRANCH" --confirm 21 | done 22 | done 23 | echo "Done cleaning up $BRANCH" 24 | -------------------------------------------------------------------------------- /crates/atomic-file-install/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [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 11 | 12 | ### Other 13 | 14 | - *(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)) 15 | 16 | ## [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 17 | 18 | ### Other 19 | 20 | - *(deps)* bump windows from 0.59.0 to 0.60.0 in the deps group across 1 directory (#2063) 21 | 22 | ## [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 23 | 24 | ### Other 25 | 26 | - update Cargo.lock dependencies 27 | 28 | ## [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 29 | 30 | ### Other 31 | 32 | - update Cargo.lock dependencies 33 | 34 | ## [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 35 | 36 | ### Other 37 | 38 | - *(deps)* bump the deps group with 3 updates (#2015) 39 | 40 | ## [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 41 | 42 | ### Other 43 | 44 | - Upgrade transitive dependencies ([#1969](https://github.com/cargo-bins/cargo-binstall/pull/1969)) 45 | -------------------------------------------------------------------------------- /crates/atomic-file-install/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "atomic-file-install" 3 | version = "1.0.11" 4 | edition = "2021" 5 | description = "For atomically installing a file or a symlink." 6 | repository = "https://github.com/cargo-bins/cargo-binstall" 7 | documentation = "https://docs.rs/atomic-install" 8 | authors = ["Jiahao XU "] 9 | license = "Apache-2.0 OR MIT" 10 | rust-version = "1.65.0" 11 | 12 | [dependencies] 13 | reflink-copy = "0.1.15" 14 | tempfile = "3.5.0" 15 | tracing = "0.1.39" 16 | 17 | [target.'cfg(windows)'.dependencies] 18 | windows = { version = "0.61.1", features = ["Win32_Storage_FileSystem", "Win32_Foundation"] } 19 | -------------------------------------------------------------------------------- /crates/atomic-file-install/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /crates/bin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-binstall" 3 | description = "Binary installation for rust projects" 4 | repository = "https://github.com/cargo-bins/cargo-binstall" 5 | documentation = "https://docs.rs/cargo-binstall" 6 | version = "1.12.6" 7 | rust-version = "1.79.0" 8 | authors = ["ryan "] 9 | edition = "2021" 10 | license = "GPL-3.0-only" 11 | readme = "../../README.md" 12 | 13 | # These MUST remain even if they're not needed in recent versions because 14 | # OLD versions use them to upgrade 15 | [package.metadata.binstall] 16 | pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ target }.{ archive-format }" 17 | bin-dir = "{ bin }{ binary-ext }" 18 | 19 | [package.metadata.binstall.overrides.x86_64-pc-windows-msvc] 20 | pkg-fmt = "zip" 21 | [package.metadata.binstall.overrides.x86_64-apple-darwin] 22 | pkg-fmt = "zip" 23 | 24 | [dependencies] 25 | atomic-file-install = { version = "1.0.11", path = "../atomic-file-install" } 26 | binstalk = { path = "../binstalk", version = "0.28.34", default-features = false } 27 | binstalk-manifests = { path = "../binstalk-manifests", version = "0.15.31" } 28 | clap = { version = "4.5.3", features = ["derive", "env", "wrap_help"] } 29 | clap-cargo = "0.15.2" 30 | compact_str = "0.9.0" 31 | dirs = "6.0.0" 32 | file-format = { version = "0.27.0", default-features = false } 33 | home = "0.5.9" 34 | log = { version = "0.4.22", features = ["std"] } 35 | miette = "7.0.0" 36 | mimalloc = { version = "0.1.39", default-features = false, optional = true } 37 | once_cell = "1.18.0" 38 | semver = "1.0.17" 39 | strum = "0.27.0" 40 | strum_macros = "0.27.0" 41 | supports-color = "3.0.0" 42 | tempfile = "3.5.0" 43 | tokio = { version = "1.44.0", features = ["rt-multi-thread", "signal"], default-features = false } 44 | tracing = { version = "0.1.39", default-features = false } 45 | tracing-core = "0.1.32" 46 | tracing-log = { version = "0.2.0", default-features = false } 47 | tracing-subscriber = { version = "0.3.17", features = ["fmt", "json", "ansi"], default-features = false } 48 | zeroize = "1.8.1" 49 | 50 | [build-dependencies] 51 | embed-resource = "3.0.1" 52 | vergen = { version = "8.2.7", features = ["build", "cargo", "git", "gitcl", "rustc"] } 53 | 54 | [features] 55 | default = ["static", "rustls", "trust-dns", "fancy-no-backtrace", "zstd-thin", "git"] 56 | 57 | git = ["binstalk/git"] 58 | git-max-perf = ["binstalk/git-max-perf"] 59 | 60 | mimalloc = ["dep:mimalloc"] 61 | 62 | static = ["binstalk/static"] 63 | pkg-config = ["binstalk/pkg-config"] 64 | 65 | zlib-ng = ["binstalk/zlib-ng"] 66 | zlib-rs = ["binstalk/zlib-rs"] 67 | 68 | rustls = ["binstalk/rustls"] 69 | native-tls = ["binstalk/native-tls"] 70 | 71 | trust-dns = ["binstalk/trust-dns"] 72 | 73 | # Experimental HTTP/3 client, this would require `--cfg reqwest_unstable` 74 | # to be passed to `rustc`. 75 | http3 = ["binstalk/http3"] 76 | 77 | zstd-thin = ["binstalk/zstd-thin"] 78 | cross-lang-fat-lto = ["binstalk/cross-lang-fat-lto"] 79 | 80 | fancy-no-backtrace = ["miette/fancy-no-backtrace"] 81 | fancy-with-backtrace = ["fancy-no-backtrace", "miette/fancy"] 82 | 83 | log_max_level_info = ["log/max_level_info", "tracing/max_level_info", "log_release_max_level_info"] 84 | log_max_level_debug = ["log/max_level_debug", "tracing/max_level_debug", "log_release_max_level_debug"] 85 | 86 | log_release_max_level_info = ["log/release_max_level_info", "tracing/release_max_level_info"] 87 | log_release_max_level_debug = ["log/release_max_level_debug", "tracing/release_max_level_debug"] 88 | 89 | [package.metadata.docs.rs] 90 | rustdoc-args = ["--cfg", "docsrs"] 91 | -------------------------------------------------------------------------------- /crates/bin/build.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | path::Path, 4 | process::{Child, Command}, 5 | thread, 6 | }; 7 | 8 | fn succeeds(res: io::Result) -> bool { 9 | res.and_then(|mut child| child.wait()) 10 | .map(|status| status.success()) 11 | .unwrap_or(false) 12 | } 13 | 14 | fn main() { 15 | let handle = thread::spawn(|| { 16 | println!("cargo:rerun-if-changed=build.rs"); 17 | println!("cargo:rerun-if-changed=manifest.rc"); 18 | println!("cargo:rerun-if-changed=windows.manifest"); 19 | 20 | embed_resource::compile("manifest.rc", embed_resource::NONE) 21 | .manifest_required() 22 | .unwrap(); 23 | }); 24 | 25 | let git = Command::new("git").arg("--version").spawn(); 26 | 27 | // .git is usually a dir, but it also can be a file containing 28 | // path to another .git if it is a submodule. 29 | // 30 | // If build.rs is run on a git repository, then ../../.git 31 | // should exists. 32 | let is_git_repo = Path::new("../../.git").exists(); 33 | 34 | let mut builder = vergen::EmitBuilder::builder(); 35 | builder.all_build().all_cargo().all_rustc(); 36 | 37 | if is_git_repo && succeeds(git) { 38 | builder.all_git(); 39 | } else { 40 | builder.disable_git(); 41 | } 42 | 43 | builder.emit().unwrap(); 44 | 45 | handle.join().unwrap(); 46 | } 47 | -------------------------------------------------------------------------------- /crates/bin/manifest.rc: -------------------------------------------------------------------------------- 1 | #define RT_MANIFEST 24 2 | 1 RT_MANIFEST "windows.manifest" 3 | -------------------------------------------------------------------------------- /crates/bin/release.toml: -------------------------------------------------------------------------------- 1 | pre-release-commit-message = "release: cargo-binstall v{{version}}" 2 | tag-prefix = "" 3 | tag-message = "cargo-binstall {{version}}" 4 | 5 | # We wait until the release CI is done before publishing, 6 | # because publishing is irreversible, but a release can be 7 | # reverted a lot more easily. 8 | publish = false 9 | 10 | [[pre-release-replacements]] 11 | file = "windows.manifest" 12 | search = "^ version=\"[\\d.]+[.]0\"" 13 | replace = " version=\"{{version}}.0\"" 14 | prerelease = false 15 | max = 1 16 | -------------------------------------------------------------------------------- /crates/bin/src/bin_util.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | process::{ExitCode, Termination}, 3 | time::Duration, 4 | }; 5 | 6 | use binstalk::errors::BinstallError; 7 | use binstalk::helpers::tasks::AutoAbortJoinHandle; 8 | use miette::Result; 9 | use tokio::runtime::Runtime; 10 | use tracing::{error, info}; 11 | 12 | use crate::signal::cancel_on_user_sig_term; 13 | 14 | pub enum MainExit { 15 | Success(Option), 16 | Error(BinstallError), 17 | Report(miette::Report), 18 | } 19 | 20 | impl Termination for MainExit { 21 | fn report(self) -> ExitCode { 22 | match self { 23 | Self::Success(spent) => { 24 | if let Some(spent) = spent { 25 | info!("Done in {spent:?}"); 26 | } 27 | ExitCode::SUCCESS 28 | } 29 | Self::Error(err) => err.report(), 30 | Self::Report(err) => { 31 | error!("Fatal error:\n{err:?}"); 32 | ExitCode::from(16) 33 | } 34 | } 35 | } 36 | } 37 | 38 | impl MainExit { 39 | pub fn new(res: Result<()>, done: Option) -> Self { 40 | res.map(|()| MainExit::Success(done)).unwrap_or_else(|err| { 41 | err.downcast::() 42 | .map(MainExit::Error) 43 | .unwrap_or_else(MainExit::Report) 44 | }) 45 | } 46 | } 47 | 48 | /// This function would start a tokio multithreading runtime, 49 | /// then `block_on` the task it returns. 50 | /// 51 | /// It will cancel the future if user requested cancellation 52 | /// via signal. 53 | pub fn run_tokio_main( 54 | f: impl FnOnce() -> Result>>>, 55 | ) -> Result<()> { 56 | let rt = Runtime::new().map_err(BinstallError::from)?; 57 | let _guard = rt.enter(); 58 | 59 | if let Some(handle) = f()? { 60 | rt.block_on(cancel_on_user_sig_term(handle))? 61 | } else { 62 | Ok(()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /crates/bin/src/gh_token.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | process::{Output, Stdio}, 4 | str, 5 | }; 6 | 7 | use tokio::{io::AsyncWriteExt, process::Command}; 8 | use zeroize::{Zeroize, Zeroizing}; 9 | 10 | pub(super) async fn get() -> io::Result>> { 11 | let output = Command::new("gh") 12 | .args(["auth", "token"]) 13 | .stdout_with_optional_input(None) 14 | .await?; 15 | 16 | if !output.is_empty() { 17 | return Ok(output); 18 | } 19 | 20 | Command::new("git") 21 | .args(["credential", "fill"]) 22 | .stdout_with_optional_input(Some("host=github.com\nprotocol=https".as_bytes())) 23 | .await? 24 | .lines() 25 | .find_map(|line| { 26 | line.trim() 27 | .strip_prefix("password=") 28 | .map(|token| Zeroizing::new(token.into())) 29 | }) 30 | .ok_or_else(|| io::Error::other("Password not found in `git credential fill` output")) 31 | } 32 | 33 | trait CommandExt { 34 | // Helper function to execute a command, optionally with input 35 | async fn stdout_with_optional_input( 36 | &mut self, 37 | input: Option<&[u8]>, 38 | ) -> io::Result>>; 39 | } 40 | 41 | impl CommandExt for Command { 42 | async fn stdout_with_optional_input( 43 | &mut self, 44 | input: Option<&[u8]>, 45 | ) -> io::Result>> { 46 | self.stdout(Stdio::piped()) 47 | .stderr(Stdio::null()) 48 | .stdin(if input.is_some() { 49 | Stdio::piped() 50 | } else { 51 | Stdio::null() 52 | }); 53 | 54 | let mut child = self.spawn()?; 55 | 56 | if let Some(input) = input { 57 | child.stdin.take().unwrap().write_all(input).await?; 58 | } 59 | 60 | let Output { status, stdout, .. } = child.wait_with_output().await?; 61 | 62 | if status.success() { 63 | let s = String::from_utf8(stdout).map_err(|err| { 64 | let msg = format!( 65 | "Invalid output for `{:?}`, expected utf8: {err}", 66 | self.as_std() 67 | ); 68 | 69 | zeroize_and_drop(err.into_bytes()); 70 | 71 | io::Error::new(io::ErrorKind::InvalidData, msg) 72 | })?; 73 | 74 | let trimmed = s.trim(); 75 | 76 | Ok(if trimmed.len() == s.len() { 77 | Zeroizing::new(s.into_boxed_str()) 78 | } else { 79 | Zeroizing::new(trimmed.into()) 80 | }) 81 | } else { 82 | zeroize_and_drop(stdout); 83 | 84 | Err(io::Error::other(format!( 85 | "`{:?}` process exited with `{status}`", 86 | self.as_std() 87 | ))) 88 | } 89 | } 90 | } 91 | 92 | fn zeroize_and_drop(mut bytes: Vec) { 93 | bytes.zeroize(); 94 | } 95 | -------------------------------------------------------------------------------- /crates/bin/src/git_credentials.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs, path::PathBuf}; 2 | 3 | use dirs::home_dir; 4 | use zeroize::Zeroizing; 5 | 6 | pub fn try_from_home() -> Option>> { 7 | if let Some(mut home) = home_dir() { 8 | home.push(".git-credentials"); 9 | if let Some(cred) = from_file(home) { 10 | return Some(cred); 11 | } 12 | } 13 | 14 | if let Some(home) = env::var_os("XDG_CONFIG_HOME") { 15 | let mut home = PathBuf::from(home); 16 | home.push("git/credentials"); 17 | 18 | if let Some(cred) = from_file(home) { 19 | return Some(cred); 20 | } 21 | } 22 | 23 | None 24 | } 25 | 26 | fn from_file(path: PathBuf) -> Option>> { 27 | Zeroizing::new(fs::read_to_string(path).ok()?) 28 | .lines() 29 | .find_map(from_line) 30 | .map(Box::::from) 31 | .map(Zeroizing::new) 32 | } 33 | 34 | fn from_line(line: &str) -> Option<&str> { 35 | let cred = line 36 | .trim() 37 | .strip_prefix("https://")? 38 | .strip_suffix("@github.com")?; 39 | 40 | Some(cred.split_once(':')?.1) 41 | } 42 | 43 | #[cfg(test)] 44 | mod test { 45 | use super::*; 46 | 47 | const GIT_CREDENTIALS_TEST_CASES: &[(&str, Option<&str>)] = &[ 48 | // Success 49 | ("https://NobodyXu:gho_asdc@github.com", Some("gho_asdc")), 50 | ( 51 | "https://NobodyXu:gho_asdc12dz@github.com", 52 | Some("gho_asdc12dz"), 53 | ), 54 | // Failure 55 | ("http://NobodyXu:gho_asdc@github.com", None), 56 | ("https://NobodyXu:gho_asdc@gitlab.com", None), 57 | ("https://NobodyXugho_asdc@github.com", None), 58 | ]; 59 | 60 | #[test] 61 | fn test_extract_from_line() { 62 | GIT_CREDENTIALS_TEST_CASES.iter().for_each(|(line, res)| { 63 | assert_eq!(from_line(line), *res); 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/bin/src/install_path.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env::var_os, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use binstalk_manifests::cargo_config::Config; 7 | use tracing::debug; 8 | 9 | pub fn get_cargo_roots_path( 10 | cargo_roots: Option, 11 | cargo_home: PathBuf, 12 | config: &mut Config, 13 | ) -> Option { 14 | if let Some(p) = cargo_roots { 15 | Some(p) 16 | } else if let Some(p) = var_os("CARGO_INSTALL_ROOT") { 17 | // Environmental variables 18 | let p = PathBuf::from(p); 19 | debug!("using CARGO_INSTALL_ROOT ({})", p.display()); 20 | Some(p) 21 | } else if let Some(root) = config.install.take().and_then(|install| install.root) { 22 | debug!("using `install.root` {} from cargo config", root.display()); 23 | Some(root) 24 | } else { 25 | debug!("using ({}) as cargo home", cargo_home.display()); 26 | Some(cargo_home) 27 | } 28 | } 29 | 30 | /// Fetch install path from environment 31 | /// roughly follows 32 | /// 33 | /// Return (install_path, is_custom_install_path) 34 | pub fn get_install_path( 35 | install_path: Option, 36 | cargo_roots: Option>, 37 | ) -> (Option, bool) { 38 | // Command line override first first 39 | if let Some(p) = install_path { 40 | return (Some(p), true); 41 | } 42 | 43 | // Then cargo_roots 44 | if let Some(p) = cargo_roots { 45 | return (Some(p.as_ref().join("bin")), false); 46 | } 47 | 48 | // Local executable dir if no cargo is found 49 | let dir = dirs::executable_dir(); 50 | 51 | if let Some(d) = &dir { 52 | debug!("Fallback to {}", d.display()); 53 | } 54 | 55 | (dir, true) 56 | } 57 | -------------------------------------------------------------------------------- /crates/bin/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 2 | 3 | mod args; 4 | mod bin_util; 5 | mod entry; 6 | mod gh_token; 7 | mod git_credentials; 8 | mod install_path; 9 | mod logging; 10 | mod main_impl; 11 | mod signal; 12 | mod ui; 13 | 14 | pub use main_impl::do_main; 15 | -------------------------------------------------------------------------------- /crates/bin/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::process::Termination; 2 | 3 | use cargo_binstall::do_main; 4 | 5 | #[cfg(feature = "mimalloc")] 6 | #[global_allocator] 7 | static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; 8 | 9 | fn main() -> impl Termination { 10 | do_main() 11 | } 12 | -------------------------------------------------------------------------------- /crates/bin/src/main_impl.rs: -------------------------------------------------------------------------------- 1 | use std::{process::Termination, time::Instant}; 2 | 3 | use binstalk::{helpers::jobserver_client::LazyJobserverClient, TARGET}; 4 | use log::LevelFilter; 5 | use tracing::debug; 6 | 7 | use crate::{ 8 | args, 9 | bin_util::{run_tokio_main, MainExit}, 10 | entry, 11 | logging::logging, 12 | }; 13 | 14 | pub fn do_main() -> impl Termination { 15 | let (args, cli_overrides) = args::parse(); 16 | 17 | if args.version { 18 | let cargo_binstall_version = env!("CARGO_PKG_VERSION"); 19 | if args.verbose { 20 | let build_date = env!("VERGEN_BUILD_DATE"); 21 | 22 | let features = env!("VERGEN_CARGO_FEATURES"); 23 | 24 | let git_sha = option_env!("VERGEN_GIT_SHA").unwrap_or("UNKNOWN"); 25 | let git_commit_date = option_env!("VERGEN_GIT_COMMIT_DATE").unwrap_or("UNKNOWN"); 26 | 27 | let rustc_semver = env!("VERGEN_RUSTC_SEMVER"); 28 | let rustc_commit_hash = env!("VERGEN_RUSTC_COMMIT_HASH"); 29 | let rustc_llvm_version = env!("VERGEN_RUSTC_LLVM_VERSION"); 30 | 31 | println!( 32 | r#"cargo-binstall: {cargo_binstall_version} 33 | build-date: {build_date} 34 | build-target: {TARGET} 35 | build-features: {features} 36 | build-commit-hash: {git_sha} 37 | build-commit-date: {git_commit_date} 38 | rustc-version: {rustc_semver} 39 | rustc-commit-hash: {rustc_commit_hash} 40 | rustc-llvm-version: {rustc_llvm_version}"# 41 | ); 42 | } else { 43 | println!("{cargo_binstall_version}"); 44 | } 45 | MainExit::Success(None) 46 | } else if args.self_install { 47 | MainExit::new(entry::self_install(args), None) 48 | } else { 49 | logging( 50 | args.log_level.unwrap_or(LevelFilter::Info), 51 | args.json_output, 52 | ); 53 | 54 | let start = Instant::now(); 55 | 56 | let jobserver_client = LazyJobserverClient::new(); 57 | 58 | let result = 59 | run_tokio_main(|| entry::install_crates(args, cli_overrides, jobserver_client)); 60 | 61 | let done = start.elapsed(); 62 | debug!("run time: {done:?}"); 63 | 64 | MainExit::new(result, Some(done)) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/bin/src/signal.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use binstalk::{errors::BinstallError, helpers::tasks::AutoAbortJoinHandle}; 4 | use tokio::signal; 5 | 6 | /// This function will poll the handle while listening for ctrl_c, 7 | /// `SIGINT`, `SIGHUP`, `SIGTERM` and `SIGQUIT`. 8 | /// 9 | /// When signal is received, [`BinstallError::UserAbort`] will be returned. 10 | /// 11 | /// It would also ignore `SIGUSER1` and `SIGUSER2` on unix. 12 | /// 13 | /// This function uses [`tokio::signal`] and once exit, does not reset the default 14 | /// signal handler, so be careful when using it. 15 | pub async fn cancel_on_user_sig_term( 16 | handle: AutoAbortJoinHandle, 17 | ) -> Result { 18 | ignore_signals()?; 19 | 20 | tokio::select! { 21 | biased; 22 | 23 | res = wait_on_cancellation_signal() => { 24 | res.map_err(BinstallError::Io) 25 | .and(Err(BinstallError::UserAbort)) 26 | } 27 | res = handle => res, 28 | } 29 | } 30 | 31 | fn ignore_signals() -> io::Result<()> { 32 | #[cfg(unix)] 33 | unix::ignore_signals_on_unix()?; 34 | 35 | Ok(()) 36 | } 37 | 38 | /// If call to it returns `Ok(())`, then all calls to this function after 39 | /// that also returns `Ok(())`. 40 | async fn wait_on_cancellation_signal() -> Result<(), io::Error> { 41 | #[cfg(unix)] 42 | unix::wait_on_cancellation_signal_unix().await?; 43 | 44 | #[cfg(not(unix))] 45 | signal::ctrl_c().await?; 46 | 47 | Ok(()) 48 | } 49 | 50 | #[cfg(unix)] 51 | mod unix { 52 | use super::*; 53 | use signal::unix::{signal, SignalKind}; 54 | 55 | /// Same as [`wait_on_cancellation_signal`] but is only available on unix. 56 | pub async fn wait_on_cancellation_signal_unix() -> Result<(), io::Error> { 57 | tokio::select! { 58 | biased; 59 | 60 | res = wait_for_signal_unix(SignalKind::interrupt()) => res, 61 | res = wait_for_signal_unix(SignalKind::hangup()) => res, 62 | res = wait_for_signal_unix(SignalKind::terminate()) => res, 63 | res = wait_for_signal_unix(SignalKind::quit()) => res, 64 | } 65 | } 66 | 67 | /// Wait for first arrival of signal. 68 | pub async fn wait_for_signal_unix(kind: signal::unix::SignalKind) -> Result<(), io::Error> { 69 | let mut sig_listener = signal::unix::signal(kind)?; 70 | if sig_listener.recv().await.is_some() { 71 | Ok(()) 72 | } else { 73 | // Use pending() here for the same reason as above. 74 | std::future::pending().await 75 | } 76 | } 77 | 78 | pub fn ignore_signals_on_unix() -> Result<(), io::Error> { 79 | drop(signal(SignalKind::user_defined1())?); 80 | drop(signal(SignalKind::user_defined2())?); 81 | 82 | Ok(()) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /crates/bin/src/ui.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{self, BufRead, StdinLock, Write}, 3 | thread, 4 | }; 5 | 6 | use binstalk::errors::BinstallError; 7 | use tokio::sync::oneshot; 8 | 9 | fn ask_for_confirm(stdin: &mut StdinLock, input: &mut String) -> io::Result<()> { 10 | { 11 | let mut stdout = io::stdout().lock(); 12 | 13 | write!(&mut stdout, "Do you wish to continue? [yes]/no\n? ")?; 14 | stdout.flush()?; 15 | } 16 | 17 | stdin.read_line(input)?; 18 | 19 | Ok(()) 20 | } 21 | 22 | pub async fn confirm() -> Result<(), BinstallError> { 23 | let (tx, rx) = oneshot::channel(); 24 | 25 | thread::spawn(move || { 26 | // This task should be the only one able to 27 | // access stdin 28 | let mut stdin = io::stdin().lock(); 29 | let mut input = String::with_capacity(16); 30 | 31 | let res = loop { 32 | if ask_for_confirm(&mut stdin, &mut input).is_err() { 33 | break false; 34 | } 35 | 36 | match input.as_str().trim() { 37 | "yes" | "y" | "YES" | "Y" | "" => break true, 38 | "no" | "n" | "NO" | "N" => break false, 39 | _ => { 40 | input.clear(); 41 | continue; 42 | } 43 | } 44 | }; 45 | 46 | // The main thread might be terminated by signal and thus cancelled 47 | // the confirmation. 48 | tx.send(res).ok(); 49 | }); 50 | 51 | if rx.await.unwrap() { 52 | Ok(()) 53 | } else { 54 | Err(BinstallError::UserAbort) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/bin/windows.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | true 37 | UTF-8 38 | SegmentHeap 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /crates/binstalk-bins/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.6.13](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.12...binstalk-bins-v0.6.13) - 2025-03-19 10 | 11 | ### Other 12 | 13 | - updated the following local packages: atomic-file-install 14 | 15 | ## [0.6.12](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.11...binstalk-bins-v0.6.12) - 2025-03-07 16 | 17 | ### Other 18 | 19 | - *(deps)* bump the deps group with 3 updates ([#2072](https://github.com/cargo-bins/cargo-binstall/pull/2072)) 20 | 21 | ## [0.6.11](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.10...binstalk-bins-v0.6.11) - 2025-02-22 22 | 23 | ### Other 24 | 25 | - updated the following local packages: atomic-file-install 26 | 27 | ## [0.6.10](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.9...binstalk-bins-v0.6.10) - 2025-02-11 28 | 29 | ### Other 30 | 31 | - updated the following local packages: binstalk-types 32 | 33 | ## [0.6.9](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.8...binstalk-bins-v0.6.9) - 2025-01-19 34 | 35 | ### Other 36 | 37 | - update Cargo.lock dependencies 38 | 39 | ## [0.6.8](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.7...binstalk-bins-v0.6.8) - 2025-01-13 40 | 41 | ### Other 42 | 43 | - update Cargo.lock dependencies 44 | 45 | ## [0.6.7](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.6...binstalk-bins-v0.6.7) - 2025-01-11 46 | 47 | ### Other 48 | 49 | - *(deps)* bump the deps group with 3 updates (#2015) 50 | 51 | ## [0.6.6](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.5...binstalk-bins-v0.6.6) - 2024-12-14 52 | 53 | ### Other 54 | 55 | - *(deps)* bump the deps group with 2 updates (#1997) 56 | 57 | ## [0.6.5](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.4...binstalk-bins-v0.6.5) - 2024-11-23 58 | 59 | ### Other 60 | 61 | - updated the following local packages: binstalk-types 62 | 63 | ## [0.6.4](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.3...binstalk-bins-v0.6.4) - 2024-11-18 64 | 65 | ### Other 66 | 67 | - updated the following local packages: atomic-file-install 68 | 69 | ## [0.6.3](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.2...binstalk-bins-v0.6.3) - 2024-11-09 70 | 71 | ### Other 72 | 73 | - *(deps)* bump the deps group with 3 updates ([#1966](https://github.com/cargo-bins/cargo-binstall/pull/1966)) 74 | 75 | ## [0.6.2](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.1...binstalk-bins-v0.6.2) - 2024-11-05 76 | 77 | ### Other 78 | 79 | - *(deps)* bump the deps group with 3 updates ([#1954](https://github.com/cargo-bins/cargo-binstall/pull/1954)) 80 | 81 | ## [0.6.1](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.6.0...binstalk-bins-v0.6.1) - 2024-11-02 82 | 83 | ### Other 84 | 85 | - Improve UI orompt for installation ([#1950](https://github.com/cargo-bins/cargo-binstall/pull/1950)) 86 | 87 | ## [0.6.0](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-bins-v0.5.0...binstalk-bins-v0.6.0) - 2024-08-10 88 | 89 | ### Other 90 | - updated the following local packages: binstalk-types 91 | -------------------------------------------------------------------------------- /crates/binstalk-bins/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "binstalk-bins" 3 | version = "0.6.13" 4 | edition = "2021" 5 | 6 | description = "The binstall binaries discovery and installation crate." 7 | repository = "https://github.com/cargo-bins/cargo-binstall" 8 | documentation = "https://docs.rs/binstalk-bins" 9 | rust-version = "1.65.0" 10 | authors = ["Jiahao XU "] 11 | license = "GPL-3.0-only" 12 | 13 | [dependencies] 14 | atomic-file-install = { version = "1.0.11", path = "../atomic-file-install" } 15 | binstalk-types = { version = "0.9.4", path = "../binstalk-types" } 16 | compact_str = { version = "0.9.0", features = ["serde"] } 17 | leon = "3.0.0" 18 | miette = "7.0.0" 19 | normalize-path = { version = "0.2.1", path = "../normalize-path" } 20 | thiserror = "2.0.11" 21 | tracing = "0.1.39" 22 | -------------------------------------------------------------------------------- /crates/binstalk-downloader/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /crates/binstalk-downloader/src/download/async_tar_visitor.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, fmt::Debug, io, path::Path, pin::Pin}; 2 | 3 | use async_compression::tokio::bufread; 4 | use bytes::Bytes; 5 | use futures_util::{Stream, StreamExt}; 6 | use tokio::io::{copy, sink, AsyncRead}; 7 | use tokio_tar::{Archive, Entry, EntryType}; 8 | use tokio_util::io::StreamReader; 9 | use tracing::debug; 10 | 11 | use super::{ 12 | DownloadError, 13 | TarBasedFmt::{self, *}, 14 | }; 15 | 16 | pub trait TarEntry: AsyncRead + Send + Sync + Unpin + Debug { 17 | /// Returns the path name for this entry. 18 | /// 19 | /// This method may fail if the pathname is not valid Unicode and 20 | /// this is called on a Windows platform. 21 | /// 22 | /// Note that this function will convert any `\` characters to 23 | /// directory separators. 24 | fn path(&self) -> io::Result>; 25 | 26 | fn size(&self) -> io::Result; 27 | 28 | fn entry_type(&self) -> TarEntryType; 29 | } 30 | 31 | impl TarEntry for &mut T { 32 | fn path(&self) -> io::Result> { 33 | T::path(self) 34 | } 35 | 36 | fn size(&self) -> io::Result { 37 | T::size(self) 38 | } 39 | 40 | fn entry_type(&self) -> TarEntryType { 41 | T::entry_type(self) 42 | } 43 | } 44 | 45 | impl TarEntry for Entry { 46 | fn path(&self) -> io::Result> { 47 | Entry::path(self) 48 | } 49 | 50 | fn size(&self) -> io::Result { 51 | self.header().size() 52 | } 53 | 54 | fn entry_type(&self) -> TarEntryType { 55 | match self.header().entry_type() { 56 | EntryType::Regular => TarEntryType::Regular, 57 | EntryType::Link => TarEntryType::Link, 58 | EntryType::Symlink => TarEntryType::Symlink, 59 | EntryType::Char => TarEntryType::Char, 60 | EntryType::Block => TarEntryType::Block, 61 | EntryType::Directory => TarEntryType::Directory, 62 | EntryType::Fifo => TarEntryType::Fifo, 63 | // Implementation-defined ‘high-performance’ type, treated as regular file 64 | EntryType::Continuous => TarEntryType::Regular, 65 | _ => TarEntryType::Unknown, 66 | } 67 | } 68 | } 69 | 70 | #[derive(Copy, Clone, Debug)] 71 | #[non_exhaustive] 72 | pub enum TarEntryType { 73 | Regular, 74 | Link, 75 | Symlink, 76 | Char, 77 | Block, 78 | Directory, 79 | Fifo, 80 | Unknown, 81 | } 82 | 83 | /// Visitor must iterate over all entries. 84 | /// Entires can be in arbitary order. 85 | #[async_trait::async_trait] 86 | pub trait TarEntriesVisitor: Send + Sync { 87 | /// Will be called once per entry 88 | async fn visit(&mut self, entry: &mut dyn TarEntry) -> Result<(), DownloadError>; 89 | } 90 | 91 | pub(crate) async fn extract_tar_based_stream_and_visit( 92 | stream: S, 93 | fmt: TarBasedFmt, 94 | visitor: &mut dyn TarEntriesVisitor, 95 | ) -> Result<(), DownloadError> 96 | where 97 | S: Stream> + Send + Sync, 98 | { 99 | debug!("Extracting from {fmt} archive to process it in memory"); 100 | 101 | let reader = StreamReader::new(stream); 102 | let decoder: Pin> = match fmt { 103 | Tar => Box::pin(reader), 104 | Tbz2 => Box::pin(bufread::BzDecoder::new(reader)), 105 | Tgz => Box::pin(bufread::GzipDecoder::new(reader)), 106 | Txz => Box::pin(bufread::XzDecoder::new(reader)), 107 | Tzstd => Box::pin(bufread::ZstdDecoder::new(reader)), 108 | }; 109 | 110 | let mut tar = Archive::new(decoder); 111 | let mut entries = tar.entries()?; 112 | 113 | let mut sink = sink(); 114 | 115 | while let Some(res) = entries.next().await { 116 | let mut entry = res?; 117 | visitor.visit(&mut entry).await?; 118 | 119 | // Consume all remaining data so that next iteration would work fine 120 | // instead of reading the data of prevoius entry. 121 | copy(&mut entry, &mut sink).await?; 122 | } 123 | 124 | Ok(()) 125 | } 126 | -------------------------------------------------------------------------------- /crates/binstalk-downloader/src/download/extracted_files.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{hash_map::Entry as HashMapEntry, HashMap, HashSet}, 3 | ffi::OsStr, 4 | path::Path, 5 | }; 6 | 7 | #[derive(Debug)] 8 | #[cfg_attr(test, derive(Eq, PartialEq))] 9 | pub enum ExtractedFilesEntry { 10 | Dir(Box>>), 11 | File, 12 | } 13 | 14 | impl ExtractedFilesEntry { 15 | fn new_dir(file_name: Option<&OsStr>) -> Self { 16 | ExtractedFilesEntry::Dir(Box::new( 17 | file_name 18 | .map(|file_name| HashSet::from([file_name.into()])) 19 | .unwrap_or_default(), 20 | )) 21 | } 22 | } 23 | 24 | #[derive(Debug)] 25 | pub struct ExtractedFiles(pub(super) HashMap, ExtractedFilesEntry>); 26 | 27 | impl ExtractedFiles { 28 | pub(super) fn new() -> Self { 29 | Self(Default::default()) 30 | } 31 | 32 | /// * `path` - must be canonical and must not be empty 33 | /// 34 | /// NOTE that if the entry for the `path` is previously set to a dir, 35 | /// it would be replaced with a file. 36 | pub(super) fn add_file(&mut self, path: &Path) { 37 | self.0.insert(path.into(), ExtractedFilesEntry::File); 38 | self.add_dir_if_has_parent(path); 39 | } 40 | 41 | fn add_dir_if_has_parent(&mut self, path: &Path) { 42 | if let Some(parent) = path.parent() { 43 | if !parent.as_os_str().is_empty() { 44 | self.add_dir_inner(parent, path.file_name()); 45 | self.add_dir_if_has_parent(parent); 46 | } else { 47 | self.add_dir_inner(Path::new("."), path.file_name()) 48 | } 49 | } 50 | } 51 | 52 | /// * `path` - must be canonical and must not be empty 53 | /// 54 | /// NOTE that if the entry for the `path` is previously set to a dir, 55 | /// it would be replaced with an empty Dir entry. 56 | pub(super) fn add_dir(&mut self, path: &Path) { 57 | self.add_dir_inner(path, None); 58 | self.add_dir_if_has_parent(path); 59 | } 60 | 61 | /// * `path` - must be canonical and must not be empty 62 | /// 63 | /// NOTE that if the entry for the `path` is previously set to a dir, 64 | /// it would be replaced with a Dir entry containing `file_name` if it 65 | /// is `Some(..)`, or an empty Dir entry. 66 | fn add_dir_inner(&mut self, path: &Path, file_name: Option<&OsStr>) { 67 | match self.0.entry(path.into()) { 68 | HashMapEntry::Vacant(entry) => { 69 | entry.insert(ExtractedFilesEntry::new_dir(file_name)); 70 | } 71 | HashMapEntry::Occupied(entry) => match entry.into_mut() { 72 | ExtractedFilesEntry::Dir(hash_set) => { 73 | if let Some(file_name) = file_name { 74 | hash_set.insert(file_name.into()); 75 | } 76 | } 77 | entry => *entry = ExtractedFilesEntry::new_dir(file_name), 78 | }, 79 | } 80 | } 81 | 82 | /// * `path` - must be a relative path without `.`, `..`, `/`, `prefix:/` 83 | /// and must not be empty, for these values it is guaranteed to 84 | /// return `None`. 85 | /// But could be set to "." for top-level. 86 | pub fn get_entry(&self, path: &Path) -> Option<&ExtractedFilesEntry> { 87 | self.0.get(path) 88 | } 89 | 90 | /// * `path` - must be a relative path without `.`, `..`, `/`, `prefix:/` 91 | /// and must not be empty, for these values it is guaranteed to 92 | /// return `None`. 93 | /// But could be set to "." for top-level. 94 | pub fn get_dir(&self, path: &Path) -> Option<&HashSet>> { 95 | match self.get_entry(path)? { 96 | ExtractedFilesEntry::Dir(file_names) => Some(file_names), 97 | ExtractedFilesEntry::File => None, 98 | } 99 | } 100 | 101 | /// * `path` - must be a relative path without `.`, `..`, `/`, `prefix:/` 102 | /// and must not be empty, for these values it is guaranteed to 103 | /// return `false`. 104 | /// But could be set to "." for top-level. 105 | pub fn has_file(&self, path: &Path) -> bool { 106 | matches!(self.get_entry(path), Some(ExtractedFilesEntry::File)) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /crates/binstalk-downloader/src/download/extracter.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, BufRead, Read}; 2 | 3 | use bzip2::bufread::BzDecoder; 4 | use flate2::bufread::GzDecoder; 5 | use tar::Archive; 6 | use xz2::bufread::XzDecoder; 7 | use zstd::stream::Decoder as ZstdDecoder; 8 | 9 | use super::TarBasedFmt; 10 | 11 | pub fn create_tar_decoder( 12 | dat: impl BufRead + 'static, 13 | fmt: TarBasedFmt, 14 | ) -> io::Result>> { 15 | use TarBasedFmt::*; 16 | 17 | let r: Box = match fmt { 18 | Tar => Box::new(dat), 19 | Tbz2 => Box::new(BzDecoder::new(dat)), 20 | Tgz => Box::new(GzDecoder::new(dat)), 21 | Txz => Box::new(XzDecoder::new(dat)), 22 | Tzstd => { 23 | // The error can only come from raw::Decoder::with_dictionary as of zstd 0.10.2 and 24 | // 0.11.2, which is specified as `&[]` by `ZstdDecoder::new`, thus `ZstdDecoder::new` 25 | // should not return any error. 26 | Box::new(ZstdDecoder::with_buffer(dat)?) 27 | } 28 | }; 29 | 30 | Ok(Archive::new(r)) 31 | } 32 | -------------------------------------------------------------------------------- /crates/binstalk-downloader/src/download/zip_extraction.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{create_dir_all, File}, 3 | io, 4 | path::Path, 5 | }; 6 | 7 | use cfg_if::cfg_if; 8 | use rc_zip_sync::{rc_zip::parse::EntryKind, ReadZip}; 9 | 10 | use super::{DownloadError, ExtractedFiles}; 11 | 12 | pub(super) fn do_extract_zip(f: File, dir: &Path) -> Result { 13 | let mut extracted_files = ExtractedFiles::new(); 14 | 15 | for entry in f.read_zip()?.entries() { 16 | let Some(name) = entry.sanitized_name().map(Path::new) else { 17 | continue; 18 | }; 19 | let path = dir.join(name); 20 | 21 | let do_extract_file = || { 22 | let mut entry_writer = File::create(&path)?; 23 | let mut entry_reader = entry.reader(); 24 | io::copy(&mut entry_reader, &mut entry_writer)?; 25 | 26 | Ok::<_, io::Error>(()) 27 | }; 28 | 29 | let parent = path 30 | .parent() 31 | .expect("all full entry paths should have parent paths"); 32 | create_dir_all(parent)?; 33 | 34 | match entry.kind() { 35 | EntryKind::Symlink => { 36 | extracted_files.add_file(name); 37 | cfg_if! { 38 | if #[cfg(windows)] { 39 | do_extract_file()?; 40 | } else { 41 | use std::{fs, io::Read}; 42 | 43 | match fs::symlink_metadata(&path) { 44 | Ok(metadata) if metadata.is_file() => fs::remove_file(&path)?, 45 | _ => (), 46 | } 47 | 48 | let mut src = String::new(); 49 | entry.reader().read_to_string(&mut src)?; 50 | 51 | // validate pointing path before creating a symbolic link 52 | if src.contains("..") { 53 | continue; 54 | } 55 | std::os::unix::fs::symlink(src, &path)?; 56 | } 57 | } 58 | } 59 | EntryKind::Directory => (), 60 | EntryKind::File => { 61 | extracted_files.add_file(name); 62 | do_extract_file()?; 63 | } 64 | } 65 | } 66 | 67 | Ok(extracted_files) 68 | } 69 | -------------------------------------------------------------------------------- /crates/binstalk-downloader/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 2 | 3 | pub use bytes; 4 | pub mod download; 5 | pub mod remote; 6 | mod utils; 7 | -------------------------------------------------------------------------------- /crates/binstalk-downloader/src/remote/certificate.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "__tls")] 2 | use reqwest::tls; 3 | 4 | use super::Error; 5 | 6 | #[derive(Clone, Debug)] 7 | pub struct Certificate(#[cfg(feature = "__tls")] pub(super) tls::Certificate); 8 | 9 | #[cfg_attr(not(feature = "__tls"), allow(unused_variables))] 10 | impl Certificate { 11 | /// Create a Certificate from a binary DER encoded certificate 12 | pub fn from_der(der: impl AsRef<[u8]>) -> Result { 13 | #[cfg(not(feature = "__tls"))] 14 | return Ok(Self()); 15 | 16 | #[cfg(feature = "__tls")] 17 | tls::Certificate::from_der(der.as_ref()) 18 | .map(Self) 19 | .map_err(Error::from) 20 | } 21 | 22 | /// Create a Certificate from a PEM encoded certificate 23 | pub fn from_pem(pem: impl AsRef<[u8]>) -> Result { 24 | #[cfg(not(feature = "__tls"))] 25 | return Ok(Self()); 26 | 27 | #[cfg(feature = "__tls")] 28 | tls::Certificate::from_pem(pem.as_ref()) 29 | .map(Self) 30 | .map_err(Error::from) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/binstalk-downloader/src/remote/request_builder.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use bytes::Bytes; 4 | use futures_util::{Stream, StreamExt}; 5 | use reqwest::Method; 6 | 7 | use super::{header, Client, Error, HttpError, StatusCode, Url}; 8 | 9 | pub use reqwest::Body; 10 | 11 | #[cfg(feature = "json")] 12 | pub use serde_json::Error as JsonError; 13 | 14 | #[derive(Debug)] 15 | pub struct RequestBuilder { 16 | pub(super) client: Client, 17 | pub(super) inner: reqwest::RequestBuilder, 18 | } 19 | 20 | impl RequestBuilder { 21 | pub fn bearer_auth(self, token: &dyn fmt::Display) -> Self { 22 | Self { 23 | client: self.client, 24 | inner: self.inner.bearer_auth(token), 25 | } 26 | } 27 | 28 | pub fn header(self, key: &str, value: &str) -> Self { 29 | Self { 30 | client: self.client, 31 | inner: self.inner.header(key, value), 32 | } 33 | } 34 | 35 | pub fn body(self, body: impl Into) -> Self { 36 | Self { 37 | client: self.client, 38 | inner: self.inner.body(body.into()), 39 | } 40 | } 41 | 42 | pub async fn send(self, error_for_status: bool) -> Result { 43 | let request = self.inner.build()?; 44 | let method = request.method().clone(); 45 | Ok(Response { 46 | inner: self.client.send_request(request, error_for_status).await?, 47 | method, 48 | }) 49 | } 50 | } 51 | 52 | #[derive(Debug)] 53 | pub struct Response { 54 | inner: reqwest::Response, 55 | method: Method, 56 | } 57 | 58 | impl Response { 59 | pub async fn bytes(self) -> Result { 60 | self.inner.bytes().await.map_err(Error::from) 61 | } 62 | 63 | pub fn bytes_stream(self) -> impl Stream> { 64 | let url = Box::new(self.inner.url().clone()); 65 | let method = self.method; 66 | 67 | self.inner.bytes_stream().map(move |res| { 68 | res.map_err(|err| { 69 | Error::Http(Box::new(HttpError { 70 | method: method.clone(), 71 | url: Url::clone(&*url), 72 | err, 73 | })) 74 | }) 75 | }) 76 | } 77 | 78 | pub fn status(&self) -> StatusCode { 79 | self.inner.status() 80 | } 81 | 82 | pub fn url(&self) -> &Url { 83 | self.inner.url() 84 | } 85 | 86 | pub fn method(&self) -> &Method { 87 | &self.method 88 | } 89 | 90 | pub fn error_for_status_ref(&self) -> Result<&Self, Error> { 91 | match self.inner.error_for_status_ref() { 92 | Ok(_) => Ok(self), 93 | Err(err) => Err(Error::Http(Box::new(HttpError { 94 | method: self.method().clone(), 95 | url: self.url().clone(), 96 | err, 97 | }))), 98 | } 99 | } 100 | 101 | pub fn error_for_status(self) -> Result { 102 | match self.error_for_status_ref() { 103 | Ok(_) => Ok(self), 104 | Err(err) => Err(err), 105 | } 106 | } 107 | 108 | pub fn headers(&self) -> &header::HeaderMap { 109 | self.inner.headers() 110 | } 111 | 112 | #[cfg(feature = "json")] 113 | pub async fn json(self) -> Result 114 | where 115 | T: serde::de::DeserializeOwned, 116 | { 117 | let bytes = self.error_for_status()?.bytes().await?; 118 | Ok(serde_json::from_slice(&bytes)?) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /crates/binstalk-downloader/src/remote/resolver.rs: -------------------------------------------------------------------------------- 1 | use std::{net::SocketAddr, sync::Arc}; 2 | 3 | use hickory_resolver::{ 4 | config::{LookupIpStrategy, ResolverConfig, ResolverOpts}, 5 | system_conf, TokioResolver as TokioAsyncResolver, 6 | }; 7 | use once_cell::sync::OnceCell; 8 | use reqwest::dns::{Addrs, Name, Resolve, Resolving}; 9 | use tracing::{debug, instrument, warn}; 10 | 11 | #[cfg(windows)] 12 | use hickory_resolver::{config::NameServerConfig, proto::xfer::Protocol}; 13 | 14 | type BoxError = Box; 15 | 16 | #[derive(Debug, Default, Clone)] 17 | pub struct TrustDnsResolver(Arc>); 18 | 19 | impl Resolve for TrustDnsResolver { 20 | fn resolve(&self, name: Name) -> Resolving { 21 | let resolver = self.clone(); 22 | Box::pin(async move { 23 | let resolver = resolver.0.get_or_try_init(new_resolver)?; 24 | 25 | let lookup = resolver.lookup_ip(name.as_str()).await?; 26 | let addrs: Addrs = Box::new(lookup.into_iter().map(|ip| SocketAddr::new(ip, 0))); 27 | Ok(addrs) 28 | }) 29 | } 30 | } 31 | 32 | #[cfg(unix)] 33 | fn get_configs() -> Result<(ResolverConfig, ResolverOpts), BoxError> { 34 | debug!("Using system DNS resolver configuration"); 35 | system_conf::read_system_conf().map_err(Into::into) 36 | } 37 | 38 | #[cfg(windows)] 39 | fn get_configs() -> Result<(ResolverConfig, ResolverOpts), BoxError> { 40 | debug!("Using custom DNS resolver configuration"); 41 | let mut config = ResolverConfig::new(); 42 | let opts = ResolverOpts::default(); 43 | 44 | get_adapter()?.dns_servers().iter().for_each(|addr| { 45 | tracing::trace!("Adding DNS server: {}", addr); 46 | let socket_addr = SocketAddr::new(*addr, 53); 47 | for protocol in [Protocol::Udp, Protocol::Tcp] { 48 | config.add_name_server(NameServerConfig { 49 | socket_addr, 50 | protocol, 51 | tls_dns_name: None, 52 | trust_negative_responses: false, 53 | bind_addr: None, 54 | http_endpoint: None, 55 | }) 56 | } 57 | }); 58 | 59 | Ok((config, opts)) 60 | } 61 | 62 | #[instrument] 63 | fn new_resolver() -> Result { 64 | let (config, mut opts) = get_configs()?; 65 | 66 | debug!("Resolver configuration complete"); 67 | opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6; 68 | let mut builder = TokioAsyncResolver::builder_with_config(config, Default::default()); 69 | *builder.options_mut() = opts; 70 | Ok(builder.build()) 71 | } 72 | 73 | #[cfg(windows)] 74 | #[instrument] 75 | fn get_adapter() -> Result { 76 | debug!("Retrieving local IP address"); 77 | let local_ip = 78 | default_net::interface::get_local_ipaddr().ok_or("Local IP address not found")?; 79 | debug!("Local IP address: {local_ip}"); 80 | debug!("Retrieving network adapters"); 81 | let adapters = ipconfig::get_adapters()?; 82 | debug!("Found {} network adapters", adapters.len()); 83 | debug!("Searching for adapter with IP address {local_ip}"); 84 | let adapter = adapters 85 | .into_iter() 86 | .find(|adapter| adapter.ip_addresses().contains(&local_ip)) 87 | .ok_or("Adapter not found")?; 88 | debug!( 89 | "Using adapter {} with {} DNS servers", 90 | adapter.friendly_name(), 91 | adapter.dns_servers().len() 92 | ); 93 | Ok(adapter) 94 | } 95 | -------------------------------------------------------------------------------- /crates/binstalk-downloader/src/remote/tls_version.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 2 | enum Inner { 3 | Tls1_2 = 0, 4 | Tls1_3 = 1, 5 | } 6 | 7 | /// TLS version for [`crate::remote::Client`]. 8 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 9 | pub struct TLSVersion(Inner); 10 | 11 | impl TLSVersion { 12 | pub const TLS_1_2: TLSVersion = TLSVersion(Inner::Tls1_2); 13 | pub const TLS_1_3: TLSVersion = TLSVersion(Inner::Tls1_3); 14 | } 15 | 16 | #[cfg(feature = "__tls")] 17 | impl From for reqwest::tls::Version { 18 | fn from(ver: TLSVersion) -> reqwest::tls::Version { 19 | use reqwest::tls::Version; 20 | use Inner::*; 21 | 22 | match ver.0 { 23 | Tls1_2 => Version::TLS_1_2, 24 | Tls1_3 => Version::TLS_1_3, 25 | } 26 | } 27 | } 28 | 29 | #[cfg(test)] 30 | mod test { 31 | use super::*; 32 | 33 | #[test] 34 | fn test_tls_version_order() { 35 | assert!(TLSVersion::TLS_1_2 < TLSVersion::TLS_1_3); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/binstalk-fetchers/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "binstalk-fetchers" 3 | version = "0.10.20" 4 | edition = "2021" 5 | 6 | description = "The binstall fetchers" 7 | repository = "https://github.com/cargo-bins/cargo-binstall" 8 | documentation = "https://docs.rs/binstalk-fetchers" 9 | rust-version = "1.70.0" 10 | authors = ["Jiahao XU "] 11 | license = "GPL-3.0-only" 12 | 13 | [dependencies] 14 | async-trait = "0.1.88" 15 | binstalk-downloader = { version = "0.13.19", path = "../binstalk-downloader", default-features = false } 16 | binstalk-git-repo-api = { version = "0.5.21", path = "../binstalk-git-repo-api" } 17 | binstalk-types = { version = "0.9.4", path = "../binstalk-types" } 18 | bytes = "1.4.0" 19 | compact_str = { version = "0.9.0" } 20 | either = "1.11.0" 21 | itertools = "0.14.0" 22 | leon = "3.0.0" 23 | leon-macros = "1.0.1" 24 | miette = "7.0.0" 25 | minisign-verify = "0.2.1" 26 | once_cell = "1.18.0" 27 | strum = "0.27.0" 28 | thiserror = "2.0.11" 29 | tokio = { version = "1.44.0", features = [ 30 | "rt", 31 | "sync", 32 | ], default-features = false } 33 | tracing = "0.1.39" 34 | url = "2.5.4" 35 | 36 | [dev-dependencies] 37 | binstalk-downloader = { version = "0.13.19", path = "../binstalk-downloader" } 38 | 39 | [features] 40 | quickinstall = [] 41 | 42 | [package.metadata.docs.rs] 43 | rustdoc-args = ["--cfg", "docsrs"] 44 | all-features = true 45 | -------------------------------------------------------------------------------- /crates/binstalk-fetchers/src/common.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use std::{ 4 | future::Future, 5 | sync::{ 6 | atomic::{AtomicBool, Ordering::Relaxed}, 7 | Once, 8 | }, 9 | }; 10 | 11 | pub(super) use binstalk_downloader::{ 12 | download::{Download, ExtractedFiles}, 13 | remote::{Client, Url}, 14 | }; 15 | pub(super) use binstalk_git_repo_api::gh_api_client::GhApiClient; 16 | use binstalk_git_repo_api::gh_api_client::{GhApiError, GhReleaseArtifact, GhReleaseArtifactUrl}; 17 | pub(super) use binstalk_types::cargo_toml_binstall::{PkgFmt, PkgMeta}; 18 | pub(super) use compact_str::CompactString; 19 | pub(super) use tokio::task::JoinHandle; 20 | pub(super) use tracing::{debug, instrument, warn}; 21 | 22 | use crate::FetchError; 23 | 24 | static WARN_RATE_LIMIT_ONCE: Once = Once::new(); 25 | static WARN_UNAUTHORIZED_ONCE: Once = Once::new(); 26 | 27 | /// Return Ok(Some(api_artifact_url)) if exists, or Ok(None) if it doesn't. 28 | /// 29 | /// Caches info on all artifacts matching (repo, tag). 30 | pub(super) async fn get_gh_release_artifact_url( 31 | gh_api_client: GhApiClient, 32 | artifact: GhReleaseArtifact, 33 | ) -> Result, GhApiError> { 34 | debug!("Using GitHub API to check for existence of artifact, which will also cache the API response"); 35 | 36 | // The future returned has the same size as a pointer 37 | match gh_api_client.has_release_artifact(artifact).await { 38 | Ok(ret) => Ok(ret), 39 | Err(GhApiError::NotFound) => Ok(None), 40 | 41 | Err(GhApiError::RateLimit { retry_after }) => { 42 | WARN_RATE_LIMIT_ONCE.call_once(|| { 43 | 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."); 44 | 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."); 45 | }); 46 | Err(GhApiError::RateLimit { retry_after }) 47 | } 48 | Err(GhApiError::Unauthorized) => { 49 | WARN_UNAUTHORIZED_ONCE.call_once(|| { 50 | warn!("GitHub API somehow requires a token for the API access, so we will fallback to HEAD/GET on the url."); 51 | warn!("Please consider supplying a token to cargo-binstall to speedup resolution."); 52 | }); 53 | Err(GhApiError::Unauthorized) 54 | } 55 | 56 | Err(err) => Err(err), 57 | } 58 | } 59 | 60 | /// Check if the URL exists by querying the GitHub API. 61 | /// 62 | /// Caches info on all artifacts matching (repo, tag). 63 | /// 64 | /// This function returns a future where its size should be at most size of 65 | /// 2-4 pointers. 66 | pub(super) async fn does_url_exist( 67 | client: Client, 68 | gh_api_client: GhApiClient, 69 | url: &Url, 70 | ) -> Result { 71 | static GH_API_CLIENT_FAILED: AtomicBool = AtomicBool::new(false); 72 | 73 | debug!("Checking for package at: '{url}'"); 74 | 75 | if !GH_API_CLIENT_FAILED.load(Relaxed) { 76 | if let Some(artifact) = GhReleaseArtifact::try_extract_from_url(url) { 77 | match get_gh_release_artifact_url(gh_api_client, artifact).await { 78 | Ok(ret) => return Ok(ret.is_some()), 79 | 80 | Err(GhApiError::RateLimit { .. }) | Err(GhApiError::Unauthorized) => {} 81 | 82 | Err(err) => return Err(err.into()), 83 | } 84 | 85 | GH_API_CLIENT_FAILED.store(true, Relaxed); 86 | } 87 | } 88 | 89 | Ok(Box::pin(client.remote_gettable(url.clone())).await?) 90 | } 91 | 92 | #[derive(Debug)] 93 | pub(super) struct AutoAbortJoinHandle(JoinHandle); 94 | 95 | impl AutoAbortJoinHandle 96 | where 97 | T: Send + 'static, 98 | { 99 | pub(super) fn spawn(future: F) -> Self 100 | where 101 | F: Future + Send + 'static, 102 | { 103 | Self(tokio::spawn(future)) 104 | } 105 | } 106 | 107 | impl Drop for AutoAbortJoinHandle { 108 | fn drop(&mut self) { 109 | self.0.abort(); 110 | } 111 | } 112 | 113 | impl AutoAbortJoinHandle> 114 | where 115 | E: Into, 116 | { 117 | pub(super) async fn flattened_join(mut self) -> Result { 118 | (&mut self.0).await?.map_err(Into::into) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /crates/binstalk-fetchers/src/futures_resolver.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, future::Future, pin::Pin}; 2 | 3 | use tokio::sync::mpsc; 4 | use tracing::warn; 5 | 6 | /// Given multiple futures with output = `Result, E>`, 7 | /// returns the the first one that returns either `Err(_)` or 8 | /// `Ok(Some(_))`. 9 | pub struct FuturesResolver { 10 | rx: mpsc::Receiver>, 11 | tx: mpsc::Sender>, 12 | } 13 | 14 | impl Default for FuturesResolver { 15 | fn default() -> Self { 16 | // We only need the first one, so the channel is of size 1. 17 | let (tx, rx) = mpsc::channel(1); 18 | Self { tx, rx } 19 | } 20 | } 21 | 22 | impl FuturesResolver { 23 | /// Insert new future into this resolver, they will start running 24 | /// right away. 25 | pub fn push(&self, fut: Fut) 26 | where 27 | Fut: Future, E>> + Send + 'static, 28 | { 29 | let tx = self.tx.clone(); 30 | 31 | tokio::spawn(async move { 32 | tokio::pin!(fut); 33 | 34 | Self::spawn_inner(fut, tx).await; 35 | }); 36 | } 37 | 38 | async fn spawn_inner( 39 | fut: Pin<&mut (dyn Future, E>> + Send)>, 40 | tx: mpsc::Sender>, 41 | ) { 42 | let res = tokio::select! { 43 | biased; 44 | 45 | _ = tx.closed() => return, 46 | res = fut => res, 47 | }; 48 | 49 | if let Some(res) = res.transpose() { 50 | // try_send can only fail due to being full or being closed. 51 | // 52 | // In both cases, this could means some other future has 53 | // completed first. 54 | // 55 | // For closed, it could additionally means that the task 56 | // is cancelled. 57 | tx.try_send(res).ok(); 58 | } 59 | } 60 | 61 | /// Insert multiple futures into this resolver, they will start running 62 | /// right away. 63 | pub fn extend(&self, iter: Iter) 64 | where 65 | Fut: Future, E>> + Send + 'static, 66 | Iter: IntoIterator, 67 | { 68 | iter.into_iter().for_each(|fut| self.push(fut)); 69 | } 70 | 71 | /// Return the resolution. 72 | pub fn resolve(self) -> impl Future> { 73 | let mut rx = self.rx; 74 | drop(self.tx); 75 | 76 | async move { 77 | loop { 78 | match rx.recv().await { 79 | Some(Ok(ret)) => return Some(ret), 80 | Some(Err(err)) => warn!(?err, "Fail to resolve the future"), 81 | None => return None, 82 | } 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /crates/binstalk-fetchers/src/signing.rs: -------------------------------------------------------------------------------- 1 | use binstalk_downloader::download::DataVerifier; 2 | use binstalk_types::cargo_toml_binstall::{PkgSigning, SigningAlgorithm}; 3 | use bytes::Bytes; 4 | use minisign_verify::{PublicKey, Signature, StreamVerifier}; 5 | use tracing::{error, trace}; 6 | 7 | use crate::FetchError; 8 | 9 | pub enum SignatureVerifier { 10 | Noop, 11 | Minisign(Box), 12 | } 13 | 14 | impl SignatureVerifier { 15 | pub fn new(config: &PkgSigning, signature: &[u8]) -> Result { 16 | match config.algorithm { 17 | SigningAlgorithm::Minisign => MinisignVerifier::new(config, signature) 18 | .map(Box::new) 19 | .map(Self::Minisign), 20 | algorithm => Err(FetchError::UnsupportedSigningAlgorithm(algorithm)), 21 | } 22 | } 23 | 24 | pub fn data_verifier(&self) -> Result, FetchError> { 25 | match self { 26 | Self::Noop => Ok(Box::new(())), 27 | Self::Minisign(v) => v.data_verifier(), 28 | } 29 | } 30 | 31 | pub fn info(&self) -> Option { 32 | match self { 33 | Self::Noop => None, 34 | Self::Minisign(v) => Some(v.signature.trusted_comment().into()), 35 | } 36 | } 37 | } 38 | 39 | pub struct MinisignVerifier { 40 | pubkey: PublicKey, 41 | signature: Signature, 42 | } 43 | 44 | impl MinisignVerifier { 45 | pub fn new(config: &PkgSigning, signature: &[u8]) -> Result { 46 | trace!(key=?config.pubkey, "parsing public key"); 47 | let pubkey = PublicKey::from_base64(&config.pubkey).map_err(|err| { 48 | error!("Package public key is invalid: {err}"); 49 | FetchError::InvalidSignature 50 | })?; 51 | 52 | trace!(?signature, "parsing signature"); 53 | let signature = Signature::decode(std::str::from_utf8(signature).map_err(|err| { 54 | error!(?signature, "Signature file is not UTF-8! {err}"); 55 | FetchError::InvalidSignature 56 | })?) 57 | .map_err(|err| { 58 | error!("Signature file is invalid: {err}"); 59 | FetchError::InvalidSignature 60 | })?; 61 | 62 | Ok(Self { pubkey, signature }) 63 | } 64 | 65 | pub fn data_verifier(&self) -> Result, FetchError> { 66 | self.pubkey 67 | .verify_stream(&self.signature) 68 | .map(|vs| Box::new(MinisignDataVerifier(vs)) as _) 69 | .map_err(|err| { 70 | error!("Failed to setup stream verifier: {err}"); 71 | FetchError::InvalidSignature 72 | }) 73 | } 74 | } 75 | 76 | pub struct MinisignDataVerifier<'a>(StreamVerifier<'a>); 77 | 78 | impl DataVerifier for MinisignDataVerifier<'_> { 79 | fn update(&mut self, data: &Bytes) { 80 | self.0.update(data); 81 | } 82 | 83 | fn validate(&mut self) -> bool { 84 | if let Err(err) = self.0.finalize() { 85 | error!("Failed to finalize signature verify: {err}"); 86 | false 87 | } else { 88 | true 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /crates/binstalk-git-repo-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "binstalk-git-repo-api" 3 | description = "The binstall toolkit for accessing API for git repository" 4 | repository = "https://github.com/cargo-bins/cargo-binstall" 5 | documentation = "https://docs.rs/binstalk-git-repo-api" 6 | version = "0.5.21" 7 | rust-version = "1.70.0" 8 | authors = ["Jiahao XU "] 9 | edition = "2021" 10 | license = "Apache-2.0 OR MIT" 11 | 12 | [dependencies] 13 | binstalk-downloader = { version = "0.13.19", path = "../binstalk-downloader", default-features = false, features = [ 14 | "json", 15 | ] } 16 | compact_str = "0.9.0" 17 | percent-encoding = "2.2.0" 18 | serde = { version = "1.0.163", features = ["derive"] } 19 | serde-tuple-vec-map = "1.0.1" 20 | serde_json = { version = "1.0.107" } 21 | thiserror = "2.0.11" 22 | tokio = { version = "1.44.0", features = ["sync"], default-features = false } 23 | tracing = "0.1.39" 24 | url = "2.5.4" 25 | zeroize = "1.8.1" 26 | 27 | [dev-dependencies] 28 | binstalk-downloader = { version = "0.13.19", path = "../binstalk-downloader" } 29 | tracing-subscriber = "0.3" 30 | once_cell = "1" 31 | -------------------------------------------------------------------------------- /crates/binstalk-git-repo-api/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /crates/binstalk-git-repo-api/src/gh_api_client/common.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, future::Future, sync::OnceLock}; 2 | 3 | use binstalk_downloader::remote::{self, Response, Url}; 4 | use compact_str::CompactString; 5 | use percent_encoding::percent_decode_str; 6 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 7 | use serde_json::to_string as to_json_string; 8 | use tracing::debug; 9 | 10 | use super::{GhApiError, GhGraphQLErrors}; 11 | 12 | pub(super) fn percent_decode_http_url_path(input: &str) -> CompactString { 13 | if input.contains('%') { 14 | percent_decode_str(input).decode_utf8_lossy().into() 15 | } else { 16 | // No '%', no need to decode. 17 | CompactString::new(input) 18 | } 19 | } 20 | 21 | pub(super) fn check_http_status_and_header(response: Response) -> Result { 22 | match response.status() { 23 | remote::StatusCode::UNAUTHORIZED => Err(GhApiError::Unauthorized), 24 | remote::StatusCode::NOT_FOUND => Err(GhApiError::NotFound), 25 | 26 | _ => Ok(response.error_for_status()?), 27 | } 28 | } 29 | 30 | fn get_api_endpoint() -> &'static Url { 31 | static API_ENDPOINT: OnceLock = OnceLock::new(); 32 | 33 | API_ENDPOINT.get_or_init(|| { 34 | Url::parse("https://api.github.com/").expect("Literal provided must be a valid url") 35 | }) 36 | } 37 | 38 | pub(super) fn issue_restful_api( 39 | client: &remote::Client, 40 | path: &[&str], 41 | auth_token: Option<&str>, 42 | ) -> impl Future> + Send + 'static 43 | where 44 | T: DeserializeOwned, 45 | { 46 | let mut url = get_api_endpoint().clone(); 47 | 48 | url.path_segments_mut() 49 | .expect("get_api_endpoint() should return a https url") 50 | .extend(path); 51 | 52 | debug!("Getting restful API: {url}"); 53 | 54 | let mut request_builder = client 55 | .get(url) 56 | .header("Accept", "application/vnd.github+json") 57 | .header("X-GitHub-Api-Version", "2022-11-28"); 58 | 59 | if let Some(auth_token) = auth_token { 60 | request_builder = request_builder.bearer_auth(&auth_token); 61 | } 62 | 63 | let future = request_builder.send(false); 64 | 65 | async move { 66 | let response = check_http_status_and_header(future.await?)?; 67 | 68 | Ok(response.json().await?) 69 | } 70 | } 71 | 72 | #[derive(Debug, Deserialize)] 73 | struct GraphQLResponse { 74 | data: T, 75 | errors: Option, 76 | } 77 | 78 | #[derive(Serialize)] 79 | struct GraphQLQuery { 80 | query: String, 81 | } 82 | 83 | fn get_graphql_endpoint() -> Url { 84 | let mut graphql_endpoint = get_api_endpoint().clone(); 85 | 86 | graphql_endpoint 87 | .path_segments_mut() 88 | .expect("get_api_endpoint() should return a https url") 89 | .push("graphql"); 90 | 91 | graphql_endpoint 92 | } 93 | 94 | pub(super) fn issue_graphql_query( 95 | client: &remote::Client, 96 | query: String, 97 | auth_token: &str, 98 | ) -> impl Future> + Send + 'static 99 | where 100 | T: DeserializeOwned + Debug, 101 | { 102 | let res = to_json_string(&GraphQLQuery { query }) 103 | .map_err(remote::Error::from) 104 | .map(|graphql_query| { 105 | let graphql_endpoint = get_graphql_endpoint(); 106 | 107 | debug!("Sending graphql query to {graphql_endpoint}: '{graphql_query}'"); 108 | 109 | let request_builder = client 110 | .post(graphql_endpoint, graphql_query) 111 | .header("Accept", "application/vnd.github+json") 112 | .bearer_auth(&auth_token); 113 | 114 | request_builder.send(false) 115 | }); 116 | 117 | async move { 118 | let response = check_http_status_and_header(res?.await?)?; 119 | 120 | let mut response: GraphQLResponse = response.json().await?; 121 | 122 | debug!("response = {response:?}"); 123 | 124 | if let Some(error) = response.errors.take() { 125 | Err(error.into()) 126 | } else { 127 | Ok(response.data) 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /crates/binstalk-git-repo-api/src/gh_api_client/repo_info.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, future::Future}; 2 | 3 | use compact_str::CompactString; 4 | use serde::Deserialize; 5 | 6 | use super::{ 7 | common::{issue_graphql_query, issue_restful_api}, 8 | remote, GhApiError, GhRepo, 9 | }; 10 | 11 | #[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize)] 12 | struct Owner { 13 | login: CompactString, 14 | } 15 | 16 | #[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize)] 17 | pub struct RepoInfo { 18 | owner: Owner, 19 | name: CompactString, 20 | private: bool, 21 | } 22 | 23 | impl fmt::Display for RepoInfo { 24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 | write!( 26 | f, 27 | "RepoInfo {{ owner: {}, name: {}, is_private: {} }}", 28 | self.owner.login, self.name, self.private 29 | ) 30 | } 31 | } 32 | 33 | impl RepoInfo { 34 | #[cfg(test)] 35 | pub(crate) fn new(GhRepo { owner, repo }: GhRepo, private: bool) -> Self { 36 | Self { 37 | owner: Owner { login: owner }, 38 | name: repo, 39 | private, 40 | } 41 | } 42 | pub fn repo(&self) -> GhRepo { 43 | GhRepo { 44 | owner: self.owner.login.clone(), 45 | repo: self.name.clone(), 46 | } 47 | } 48 | 49 | pub fn is_private(&self) -> bool { 50 | self.private 51 | } 52 | } 53 | 54 | pub(super) fn fetch_repo_info_restful_api( 55 | client: &remote::Client, 56 | GhRepo { owner, repo }: &GhRepo, 57 | auth_token: Option<&str>, 58 | ) -> impl Future, GhApiError>> + Send + 'static { 59 | issue_restful_api(client, &["repos", owner, repo], auth_token) 60 | } 61 | 62 | #[derive(Debug, Deserialize)] 63 | struct GraphQLData { 64 | repository: Option, 65 | } 66 | 67 | pub(super) fn fetch_repo_info_graphql_api( 68 | client: &remote::Client, 69 | GhRepo { owner, repo }: &GhRepo, 70 | auth_token: &str, 71 | ) -> impl Future, GhApiError>> + Send + 'static { 72 | let query = format!( 73 | r#" 74 | query {{ 75 | repository(owner:"{owner}",name:"{repo}") {{ 76 | owner {{ 77 | login 78 | }} 79 | name 80 | private: isPrivate 81 | }} 82 | }}"# 83 | ); 84 | 85 | let future = issue_graphql_query(client, query, auth_token); 86 | 87 | async move { 88 | let data: GraphQLData = future.await?; 89 | Ok(data.repository) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /crates/binstalk-git-repo-api/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod gh_api_client; 2 | -------------------------------------------------------------------------------- /crates/binstalk-manifests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "binstalk-manifests" 3 | description = "The binstall toolkit for manipulating with manifest" 4 | repository = "https://github.com/cargo-bins/cargo-binstall" 5 | documentation = "https://docs.rs/binstalk-manifests" 6 | version = "0.15.31" 7 | rust-version = "1.61.0" 8 | authors = ["ryan "] 9 | edition = "2021" 10 | license = "Apache-2.0 OR MIT" 11 | 12 | [dependencies] 13 | beef = { version = "0.5.2", features = ["impl_serde"] } 14 | binstalk-types = { version = "0.9.4", path = "../binstalk-types" } 15 | compact_str = { version = "0.9.0", features = ["serde"] } 16 | fs-lock = { version = "0.1.10", path = "../fs-lock", features = ["tracing"] } 17 | home = "0.5.9" 18 | miette = "7.0.0" 19 | semver = { version = "1.0.17", features = ["serde"] } 20 | serde = { version = "1.0.163", features = ["derive"] } 21 | serde-tuple-vec-map = "1.0.1" 22 | serde_json = "1.0.107" 23 | thiserror = "2.0.11" 24 | toml_edit = { version = "0.22.12", features = ["serde"] } 25 | url = { version = "2.5.4", features = ["serde"] } 26 | 27 | [dev-dependencies] 28 | detect-targets = { version = "0.1.50", path = "../detect-targets" } 29 | tempfile = "3.5.0" 30 | -------------------------------------------------------------------------------- /crates/binstalk-manifests/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /crates/binstalk-manifests/src/crates_manifests.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::BTreeMap, 3 | fs, 4 | io::{self, Seek}, 5 | path::Path, 6 | }; 7 | 8 | use fs_lock::FileLock; 9 | use miette::Diagnostic; 10 | use thiserror::Error as ThisError; 11 | 12 | use crate::{ 13 | binstall_crates_v1::{Error as BinstallCratesV1Error, Records as BinstallCratesV1Records}, 14 | cargo_crates_v1::{CratesToml, CratesTomlParseError}, 15 | crate_info::CrateInfo, 16 | helpers::create_if_not_exist, 17 | CompactString, Version, 18 | }; 19 | 20 | #[derive(Debug, Diagnostic, ThisError)] 21 | #[non_exhaustive] 22 | pub enum ManifestsError { 23 | #[error("failed to parse binstall crates-v1 manifest: {0}")] 24 | #[diagnostic(transparent)] 25 | BinstallCratesV1(#[from] BinstallCratesV1Error), 26 | 27 | #[error("failed to parse cargo v1 manifest: {0}")] 28 | #[diagnostic(transparent)] 29 | CargoManifestV1(#[from] CratesTomlParseError), 30 | 31 | #[error("I/O error: {0}")] 32 | Io(#[from] io::Error), 33 | } 34 | 35 | pub struct Manifests { 36 | binstall: BinstallCratesV1Records, 37 | cargo_crates_v1: FileLock, 38 | } 39 | 40 | impl Manifests { 41 | pub fn open_exclusive(cargo_roots: &Path) -> Result { 42 | // Read cargo_binstall_metadata 43 | let metadata_path = cargo_roots.join("binstall/crates-v1.json"); 44 | fs::create_dir_all(metadata_path.parent().unwrap())?; 45 | 46 | let binstall = BinstallCratesV1Records::load_from_path(&metadata_path)?; 47 | 48 | // Read cargo_install_v1_metadata 49 | let manifest_path = cargo_roots.join(".crates.toml"); 50 | 51 | let cargo_crates_v1 = create_if_not_exist(&manifest_path)?; 52 | 53 | Ok(Self { 54 | binstall, 55 | cargo_crates_v1, 56 | }) 57 | } 58 | 59 | fn rewind_cargo_crates_v1(&mut self) -> Result<(), ManifestsError> { 60 | self.cargo_crates_v1.rewind().map_err(ManifestsError::from) 61 | } 62 | 63 | /// `cargo-uninstall` can be called to uninstall crates, 64 | /// but it only updates .crates.toml. 65 | /// 66 | /// So here we will honour .crates.toml only. 67 | pub fn load_installed_crates( 68 | &mut self, 69 | ) -> Result, ManifestsError> { 70 | self.rewind_cargo_crates_v1()?; 71 | 72 | CratesToml::load_from_reader(&mut self.cargo_crates_v1) 73 | .and_then(CratesToml::collect_into_crates_versions) 74 | .map_err(ManifestsError::from) 75 | } 76 | 77 | pub fn update(mut self, metadata_vec: Vec) -> Result<(), ManifestsError> { 78 | self.rewind_cargo_crates_v1()?; 79 | 80 | CratesToml::append_to_file(&mut self.cargo_crates_v1, &metadata_vec)?; 81 | 82 | for metadata in metadata_vec { 83 | self.binstall.replace(metadata); 84 | } 85 | self.binstall.overwrite()?; 86 | 87 | Ok(()) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /crates/binstalk-manifests/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, io, path::Path}; 2 | 3 | use fs_lock::FileLock; 4 | 5 | /// Return exclusively locked file that is readable and writable. 6 | pub(crate) fn create_if_not_exist(path: &Path) -> io::Result { 7 | fs::File::options() 8 | .read(true) 9 | .write(true) 10 | .create(true) 11 | .truncate(false) 12 | .open(path) 13 | .and_then(FileLock::new_exclusive) 14 | .map(|file_lock| file_lock.set_file_path(path)) 15 | } 16 | -------------------------------------------------------------------------------- /crates/binstalk-manifests/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Manifest formats and utilities. 2 | //! 3 | //! There are three types of manifests Binstall may deal with: 4 | //! - manifests that define how to fetch and install a package 5 | //! ([Cargo.toml's `[metadata.binstall]`][cargo_toml_binstall]); 6 | //! - manifests that record which packages _are_ installed 7 | //! ([Cargo's `.crates.toml`][cargo_crates_v1] and 8 | //! [Binstall's `.crates-v1.json`][binstall_crates_v1]); 9 | //! - manifests that specify which packages _to_ install (currently none). 10 | 11 | mod helpers; 12 | 13 | pub mod binstall_crates_v1; 14 | pub mod cargo_config; 15 | pub mod cargo_crates_v1; 16 | /// Contains both [`binstall_crates_v1`] and [`cargo_crates_v1`]. 17 | pub mod crates_manifests; 18 | 19 | pub use binstalk_types::{cargo_toml_binstall, crate_info}; 20 | pub use compact_str::CompactString; 21 | pub use semver::Version; 22 | pub use url::Url; 23 | -------------------------------------------------------------------------------- /crates/binstalk-registry/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "binstalk-registry" 3 | version = "0.11.20" 4 | edition = "2021" 5 | rust-version = "1.65.0" 6 | 7 | description = "The binstall toolkit for fetching package from arbitrary registry" 8 | repository = "https://github.com/cargo-bins/cargo-binstall" 9 | documentation = "https://docs.rs/binstalk-registry" 10 | authors = ["Jiahao_XU@outlook "] 11 | license = "Apache-2.0 OR MIT" 12 | 13 | [dependencies] 14 | async-trait = "0.1.88" 15 | base16 = "0.2.1" 16 | binstalk-downloader = { version = "0.13.19", path = "../binstalk-downloader", default-features = false, features = [ 17 | "json", 18 | ] } 19 | binstalk-types = { version = "0.9.4", path = "../binstalk-types" } 20 | cargo-toml-workspace = { version = "7.0.6", path = "../cargo-toml-workspace" } 21 | compact_str = { version = "0.9.0", features = ["serde"] } 22 | leon = "3.0.0" 23 | miette = "7.0.0" 24 | normalize-path = { version = "0.2.1", path = "../normalize-path" } 25 | once_cell = "1.18.0" 26 | semver = { version = "1.0.17", features = ["serde"] } 27 | serde = { version = "1.0.163", features = ["derive"] } 28 | serde_json = "1.0.107" 29 | sha2 = "0.10.7" 30 | simple-git = { version = "0.2.4", optional = true } 31 | tempfile = "3.5.0" 32 | thiserror = "2.0.11" 33 | tokio = { version = "1.44.0", features = [ 34 | "rt", 35 | "sync", 36 | ], default-features = false } 37 | tracing = "0.1.39" 38 | url = "2.5.4" 39 | 40 | [dev-dependencies] 41 | tokio = { version = "1", features = ["rt-multi-thread", "macros"] } 42 | toml_edit = { version = "0.22.12", features = ["serde"] } 43 | binstalk-downloader = { version = "0.13.19", path = "../binstalk-downloader", default-features = false, features = [ 44 | "rustls", 45 | ] } 46 | 47 | [features] 48 | git = ["simple-git"] 49 | 50 | rustls = ["simple-git?/rustls"] 51 | native-tls = ["simple-git?/native-tls"] 52 | 53 | crates_io_api = [] 54 | 55 | [package.metadata.docs.rs] 56 | rustdoc-args = ["--cfg", "docsrs"] 57 | all-features = true 58 | -------------------------------------------------------------------------------- /crates/binstalk-registry/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /crates/binstalk-registry/src/sparse_registry.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use binstalk_downloader::remote::{Client, Error as RemoteError}; 4 | use binstalk_types::cargo_toml_binstall::Meta; 5 | use cargo_toml_workspace::cargo_toml::Manifest; 6 | use compact_str::CompactString; 7 | use semver::VersionReq; 8 | use serde_json::Deserializer as JsonDeserializer; 9 | use tokio::sync::OnceCell; 10 | use tracing::instrument; 11 | use url::Url; 12 | 13 | use crate::{ 14 | crate_prefix_components, parse_manifest, render_dl_template, MatchedVersion, RegistryConfig, 15 | RegistryError, 16 | }; 17 | 18 | #[derive(Debug)] 19 | pub struct SparseRegistry { 20 | url: Url, 21 | dl_template: OnceCell, 22 | } 23 | 24 | impl SparseRegistry { 25 | /// * `url` - `url.cannot_be_a_base()` must be `false` 26 | pub fn new(url: Url) -> Self { 27 | Self { 28 | url, 29 | dl_template: Default::default(), 30 | } 31 | } 32 | 33 | pub fn url(&self) -> impl Display + '_ { 34 | &self.url 35 | } 36 | 37 | async fn get_dl_template(&self, client: &Client) -> Result<&str, RegistryError> { 38 | self.dl_template 39 | .get_or_try_init(|| { 40 | Box::pin(async { 41 | let mut url = self.url.clone(); 42 | url.path_segments_mut().unwrap().push("config.json"); 43 | let config: RegistryConfig = client.get(url).send(true).await?.json().await?; 44 | Ok(config.dl) 45 | }) 46 | }) 47 | .await 48 | .map(AsRef::as_ref) 49 | } 50 | 51 | /// `url` must be a valid http(s) url. 52 | async fn find_crate_matched_ver( 53 | client: &Client, 54 | mut url: Url, 55 | crate_name: &str, 56 | (c1, c2): &(CompactString, Option), 57 | version_req: &VersionReq, 58 | ) -> Result { 59 | { 60 | let mut path = url.path_segments_mut().unwrap(); 61 | 62 | path.push(c1); 63 | if let Some(c2) = c2 { 64 | path.push(c2); 65 | } 66 | 67 | path.push(&crate_name.to_lowercase()); 68 | } 69 | 70 | let body = client 71 | .get(url) 72 | .send(true) 73 | .await 74 | .map_err(|e| match e { 75 | RemoteError::Http(e) if e.is_status() => RegistryError::NotFound(crate_name.into()), 76 | e => e.into(), 77 | })? 78 | .bytes() 79 | .await 80 | .map_err(RegistryError::from)?; 81 | MatchedVersion::find( 82 | &mut JsonDeserializer::from_slice(&body).into_iter(), 83 | version_req, 84 | ) 85 | } 86 | 87 | #[instrument( 88 | skip(self, client, version_req), 89 | fields( 90 | registry_url = format_args!("{}", self.url), 91 | version_req = format_args!("{version_req}"), 92 | ) 93 | )] 94 | pub async fn fetch_crate_matched( 95 | &self, 96 | client: Client, 97 | crate_name: &str, 98 | version_req: &VersionReq, 99 | ) -> Result, RegistryError> { 100 | let crate_prefix = crate_prefix_components(crate_name)?; 101 | let dl_template = self.get_dl_template(&client).await?; 102 | let matched_version = Self::find_crate_matched_ver( 103 | &client, 104 | self.url.clone(), 105 | crate_name, 106 | &crate_prefix, 107 | version_req, 108 | ) 109 | .await?; 110 | let dl_url = Url::parse(&render_dl_template( 111 | dl_template, 112 | crate_name, 113 | &crate_prefix, 114 | &matched_version, 115 | )?)?; 116 | 117 | parse_manifest(client, crate_name, dl_url, matched_version).await 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /crates/binstalk-registry/src/vfs.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{hash_set::HashSet, BTreeMap}, 3 | io, 4 | path::Path, 5 | }; 6 | 7 | use cargo_toml_workspace::cargo_toml::AbstractFilesystem; 8 | use normalize_path::NormalizePath; 9 | 10 | /// This type stores the filesystem structure for the crate tarball 11 | /// extracted in memory and can be passed to 12 | /// `cargo_toml::Manifest::complete_from_abstract_filesystem`. 13 | #[derive(Debug, Default)] 14 | pub(super) struct Vfs(BTreeMap, HashSet>>); 15 | 16 | impl Vfs { 17 | /// * `path` - must be canonical, must not be empty. 18 | pub(super) fn add_path(&mut self, mut path: &Path) { 19 | while let Some(parent) = path.parent() { 20 | // Since path has parent, it must have a filename 21 | let filename = path.file_name().unwrap(); 22 | 23 | // `cargo_toml`'s implementation does the same thing. 24 | // https://docs.rs/cargo_toml/0.11.5/src/cargo_toml/afs.rs.html#24 25 | let filename = filename.to_string_lossy(); 26 | 27 | self.0 28 | .entry(parent.into()) 29 | .or_insert_with(|| HashSet::with_capacity(4)) 30 | .insert(filename.into()); 31 | 32 | path = parent; 33 | } 34 | } 35 | } 36 | 37 | impl AbstractFilesystem for Vfs { 38 | fn file_names_in(&self, rel_path: &str) -> io::Result>> { 39 | let rel_path = Path::new(rel_path).normalize(); 40 | 41 | Ok(self.0.get(&*rel_path).cloned().unwrap_or_default()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /crates/binstalk-registry/src/visitor.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use binstalk_downloader::download::{DownloadError, TarEntriesVisitor, TarEntry}; 4 | use binstalk_types::cargo_toml_binstall::Meta; 5 | use cargo_toml_workspace::cargo_toml::{Manifest, Value}; 6 | use normalize_path::NormalizePath; 7 | use tokio::io::AsyncReadExt; 8 | use tracing::debug; 9 | 10 | use crate::{vfs::Vfs, RegistryError}; 11 | 12 | #[derive(Debug)] 13 | pub(super) struct ManifestVisitor { 14 | cargo_toml_content: Vec, 15 | /// manifest_dir_path is treated as the current dir. 16 | manifest_dir_path: PathBuf, 17 | 18 | vfs: Vfs, 19 | } 20 | 21 | impl ManifestVisitor { 22 | pub(super) fn new(manifest_dir_path: PathBuf) -> Self { 23 | Self { 24 | // Cargo.toml is quite large usually. 25 | cargo_toml_content: Vec::with_capacity(2000), 26 | manifest_dir_path, 27 | vfs: Vfs::default(), 28 | } 29 | } 30 | } 31 | 32 | #[async_trait::async_trait] 33 | impl TarEntriesVisitor for ManifestVisitor { 34 | async fn visit(&mut self, entry: &mut dyn TarEntry) -> Result<(), DownloadError> { 35 | let path = entry.path()?; 36 | let path = path.normalize(); 37 | 38 | let path = if let Ok(path) = path.strip_prefix(&self.manifest_dir_path) { 39 | path 40 | } else { 41 | // The path is outside of the curr dir (manifest dir), 42 | // ignore it. 43 | return Ok(()); 44 | }; 45 | 46 | if path == Path::new("Cargo.toml") 47 | || path == Path::new("src/main.rs") 48 | || path.starts_with("src/bin") 49 | { 50 | self.vfs.add_path(path); 51 | } 52 | 53 | if path == Path::new("Cargo.toml") { 54 | // Since it is possible for the same Cargo.toml to appear 55 | // multiple times using `tar --keep-old-files`, here we 56 | // clear the buffer first before reading into it. 57 | self.cargo_toml_content.clear(); 58 | self.cargo_toml_content 59 | .reserve_exact(entry.size()?.try_into().unwrap_or(usize::MAX)); 60 | entry.read_to_end(&mut self.cargo_toml_content).await?; 61 | } 62 | 63 | Ok(()) 64 | } 65 | } 66 | 67 | impl ManifestVisitor { 68 | /// Load binstall metadata using the extracted information stored in memory. 69 | pub(super) fn load_manifest(self) -> Result, RegistryError> { 70 | debug!("Loading manifest directly from extracted file"); 71 | 72 | // Load and parse manifest 73 | let mut manifest = Manifest::from_slice_with_metadata(&self.cargo_toml_content)?; 74 | debug!("Manifest: {manifest:?}"); 75 | // Checks vfs for binary output names 76 | manifest.complete_from_abstract_filesystem::(&self.vfs, None)?; 77 | 78 | // Return metadata 79 | Ok(manifest) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /crates/binstalk-types/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.9.4](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-types-v0.9.3...binstalk-types-v0.9.4) - 2025-03-07 10 | 11 | ### Other 12 | 13 | - *(deps)* bump the deps group with 3 updates ([#2072](https://github.com/cargo-bins/cargo-binstall/pull/2072)) 14 | 15 | ## [0.9.3](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-types-v0.9.2...binstalk-types-v0.9.3) - 2025-02-11 16 | 17 | ### Other 18 | 19 | - *(deps)* bump the deps group with 2 updates (#2044) 20 | 21 | ## [0.9.2](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-types-v0.9.1...binstalk-types-v0.9.2) - 2024-11-23 22 | 23 | ### Other 24 | 25 | - *(deps)* bump the deps group with 2 updates ([#1981](https://github.com/cargo-bins/cargo-binstall/pull/1981)) 26 | 27 | ## [0.9.1](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-types-v0.9.0...binstalk-types-v0.9.1) - 2024-11-05 28 | 29 | ### Other 30 | 31 | - *(deps)* bump the deps group with 3 updates ([#1954](https://github.com/cargo-bins/cargo-binstall/pull/1954)) 32 | 33 | ## [0.9.0](https://github.com/cargo-bins/cargo-binstall/compare/binstalk-types-v0.8.0...binstalk-types-v0.9.0) - 2024-08-10 34 | 35 | ### Added 36 | - Merge disable strategies ([#1868](https://github.com/cargo-bins/cargo-binstall/pull/1868)) 37 | -------------------------------------------------------------------------------- /crates/binstalk-types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "binstalk-types" 3 | description = "The binstall toolkit that contains basic types for binstalk crates" 4 | repository = "https://github.com/cargo-bins/cargo-binstall" 5 | documentation = "https://docs.rs/binstalk-types" 6 | version = "0.9.4" 7 | rust-version = "1.61.0" 8 | authors = ["ryan "] 9 | edition = "2021" 10 | license = "Apache-2.0 OR MIT" 11 | 12 | [dependencies] 13 | compact_str = { version = "0.9.0", features = ["serde"] } 14 | maybe-owned = { version = "0.3.4", features = ["serde"] } 15 | once_cell = "1.18.0" 16 | semver = { version = "1.0.17", features = ["serde"] } 17 | serde = { version = "1.0.163", features = ["derive"] } 18 | strum = "0.27.0" 19 | strum_macros = "0.27.0" 20 | url = { version = "2.5.4", features = ["serde"] } 21 | 22 | [dev-dependencies] 23 | serde_json = "1" 24 | -------------------------------------------------------------------------------- /crates/binstalk-types/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /crates/binstalk-types/src/cargo_toml_binstall/package_formats.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use strum_macros::{Display, EnumIter, EnumString}; 3 | 4 | /// Binary format enumeration 5 | #[derive( 6 | Debug, Display, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, EnumString, EnumIter, 7 | )] 8 | #[serde(rename_all = "snake_case")] 9 | #[strum(ascii_case_insensitive)] 10 | pub enum PkgFmt { 11 | /// Download format is TAR (uncompressed) 12 | Tar, 13 | /// Download format is TAR + Bzip2 14 | Tbz2, 15 | /// Download format is TGZ (TAR + GZip) 16 | Tgz, 17 | /// Download format is TAR + XZ 18 | Txz, 19 | /// Download format is TAR + Zstd 20 | Tzstd, 21 | /// Download format is Zip 22 | Zip, 23 | /// Download format is raw / binary 24 | Bin, 25 | } 26 | 27 | impl Default for PkgFmt { 28 | fn default() -> Self { 29 | Self::Tgz 30 | } 31 | } 32 | 33 | impl PkgFmt { 34 | /// If self is one of the tar based formats, return Some. 35 | pub fn decompose(self) -> PkgFmtDecomposed { 36 | match self { 37 | PkgFmt::Tar => PkgFmtDecomposed::Tar(TarBasedFmt::Tar), 38 | PkgFmt::Tbz2 => PkgFmtDecomposed::Tar(TarBasedFmt::Tbz2), 39 | PkgFmt::Tgz => PkgFmtDecomposed::Tar(TarBasedFmt::Tgz), 40 | PkgFmt::Txz => PkgFmtDecomposed::Tar(TarBasedFmt::Txz), 41 | PkgFmt::Tzstd => PkgFmtDecomposed::Tar(TarBasedFmt::Tzstd), 42 | PkgFmt::Bin => PkgFmtDecomposed::Bin, 43 | PkgFmt::Zip => PkgFmtDecomposed::Zip, 44 | } 45 | } 46 | 47 | /// List of possible file extensions for the format 48 | /// (with prefix `.`). 49 | /// 50 | /// * `is_windows` - if true and `self == PkgFmt::Bin`, then it will return 51 | /// `.exe` in additional to other bin extension names. 52 | pub fn extensions(self, is_windows: bool) -> &'static [&'static str] { 53 | match self { 54 | PkgFmt::Tar => &[".tar"], 55 | PkgFmt::Tbz2 => &[".tbz2", ".tar.bz2"], 56 | PkgFmt::Tgz => &[".tgz", ".tar.gz"], 57 | PkgFmt::Txz => &[".txz", ".tar.xz"], 58 | PkgFmt::Tzstd => &[".tzstd", ".tzst", ".tar.zst"], 59 | PkgFmt::Bin => { 60 | if is_windows { 61 | &[".bin", "", ".exe"] 62 | } else { 63 | &[".bin", ""] 64 | } 65 | } 66 | PkgFmt::Zip => &[".zip"], 67 | } 68 | } 69 | 70 | /// Given the pkg-url template, guess the possible pkg-fmt. 71 | pub fn guess_pkg_format(pkg_url: &str) -> Option { 72 | let mut it = pkg_url.rsplitn(3, '.'); 73 | 74 | let guess = match it.next()? { 75 | "tar" => Some(PkgFmt::Tar), 76 | 77 | "tbz2" => Some(PkgFmt::Tbz2), 78 | "bz2" if it.next() == Some("tar") => Some(PkgFmt::Tbz2), 79 | 80 | "tgz" => Some(PkgFmt::Tgz), 81 | "gz" if it.next() == Some("tar") => Some(PkgFmt::Tgz), 82 | 83 | "txz" => Some(PkgFmt::Txz), 84 | "xz" if it.next() == Some("tar") => Some(PkgFmt::Txz), 85 | 86 | "tzstd" | "tzst" => Some(PkgFmt::Tzstd), 87 | "zst" if it.next() == Some("tar") => Some(PkgFmt::Tzstd), 88 | 89 | "exe" | "bin" => Some(PkgFmt::Bin), 90 | "zip" => Some(PkgFmt::Zip), 91 | 92 | _ => None, 93 | }; 94 | 95 | if it.next().is_some() { 96 | guess 97 | } else { 98 | None 99 | } 100 | } 101 | } 102 | 103 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 104 | pub enum PkgFmtDecomposed { 105 | Tar(TarBasedFmt), 106 | Bin, 107 | Zip, 108 | } 109 | 110 | #[derive(Debug, Display, Copy, Clone, Eq, PartialEq)] 111 | pub enum TarBasedFmt { 112 | /// Download format is TAR (uncompressed) 113 | Tar, 114 | /// Download format is TAR + Bzip2 115 | Tbz2, 116 | /// Download format is TGZ (TAR + GZip) 117 | Tgz, 118 | /// Download format is TAR + XZ 119 | Txz, 120 | /// Download format is TAR + Zstd 121 | Tzstd, 122 | } 123 | 124 | impl From for PkgFmt { 125 | fn from(fmt: TarBasedFmt) -> Self { 126 | match fmt { 127 | TarBasedFmt::Tar => PkgFmt::Tar, 128 | TarBasedFmt::Tbz2 => PkgFmt::Tbz2, 129 | TarBasedFmt::Tgz => PkgFmt::Tgz, 130 | TarBasedFmt::Txz => PkgFmt::Txz, 131 | TarBasedFmt::Tzstd => PkgFmt::Tzstd, 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /crates/binstalk-types/src/crate_info.rs: -------------------------------------------------------------------------------- 1 | //! Common structure for crate information for post-install manifests. 2 | 3 | use std::{borrow, cmp, hash}; 4 | 5 | use compact_str::CompactString; 6 | use maybe_owned::MaybeOwned; 7 | use once_cell::sync::Lazy; 8 | use semver::Version; 9 | use serde::{Deserialize, Serialize}; 10 | use url::Url; 11 | 12 | pub fn cratesio_url() -> &'static Url { 13 | static CRATESIO: Lazy Url> = 14 | Lazy::new(|| Url::parse("https://github.com/rust-lang/crates.io-index").unwrap()); 15 | 16 | &CRATESIO 17 | } 18 | 19 | #[derive(Clone, Debug, Serialize, Deserialize)] 20 | pub struct CrateInfo { 21 | pub name: CompactString, 22 | pub version_req: CompactString, 23 | pub current_version: Version, 24 | pub source: CrateSource, 25 | pub target: CompactString, 26 | pub bins: Vec, 27 | } 28 | 29 | impl borrow::Borrow for CrateInfo { 30 | fn borrow(&self) -> &str { 31 | &self.name 32 | } 33 | } 34 | 35 | impl PartialEq for CrateInfo { 36 | fn eq(&self, other: &Self) -> bool { 37 | self.name == other.name 38 | } 39 | } 40 | impl Eq for CrateInfo {} 41 | 42 | impl PartialOrd for CrateInfo { 43 | fn partial_cmp(&self, other: &Self) -> Option { 44 | Some(self.cmp(other)) 45 | } 46 | } 47 | 48 | impl Ord for CrateInfo { 49 | fn cmp(&self, other: &Self) -> cmp::Ordering { 50 | self.name.cmp(&other.name) 51 | } 52 | } 53 | 54 | impl hash::Hash for CrateInfo { 55 | fn hash(&self, state: &mut H) 56 | where 57 | H: hash::Hasher, 58 | { 59 | self.name.hash(state) 60 | } 61 | } 62 | 63 | #[derive(Debug, Copy, Clone, Serialize, Deserialize)] 64 | pub enum SourceType { 65 | Git, 66 | Path, 67 | Registry, 68 | } 69 | 70 | #[derive(Clone, Debug, Serialize, Deserialize)] 71 | pub struct CrateSource { 72 | pub source_type: SourceType, 73 | pub url: MaybeOwned<'static, Url>, 74 | } 75 | 76 | impl CrateSource { 77 | pub fn cratesio_registry() -> CrateSource { 78 | Self { 79 | source_type: SourceType::Registry, 80 | url: MaybeOwned::Borrowed(cratesio_url()), 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /crates/binstalk-types/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cargo_toml_binstall; 2 | pub mod crate_info; 3 | 4 | pub use maybe_owned; 5 | -------------------------------------------------------------------------------- /crates/binstalk/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "binstalk" 3 | description = "The binstall toolkit (library interface)" 4 | repository = "https://github.com/cargo-bins/cargo-binstall" 5 | documentation = "https://docs.rs/binstalk" 6 | version = "0.28.34" 7 | rust-version = "1.79.0" 8 | authors = ["ryan "] 9 | edition = "2021" 10 | license = "GPL-3.0-only" 11 | 12 | [dependencies] 13 | binstalk-bins = { version = "0.6.13", path = "../binstalk-bins" } 14 | binstalk-downloader = { version = "0.13.19", path = "../binstalk-downloader", default-features = false } 15 | binstalk-git-repo-api = { version = "0.5.21", path = "../binstalk-git-repo-api" } 16 | binstalk-fetchers = { version = "0.10.20", path = "../binstalk-fetchers", features = [ 17 | "quickinstall", 18 | ] } 19 | binstalk-registry = { version = "0.11.20", path = "../binstalk-registry" } 20 | binstalk-types = { version = "0.9.4", path = "../binstalk-types" } 21 | cargo-toml-workspace = { version = "7.0.6", path = "../cargo-toml-workspace" } 22 | command-group = { version = "5.0.1", features = ["with-tokio"] } 23 | compact_str = { version = "0.9.0", features = ["serde"] } 24 | detect-targets = { version = "0.1.50", path = "../detect-targets", features = [ 25 | "tracing", 26 | ] } 27 | either = "1.11.0" 28 | itertools = "0.14.0" 29 | jobslot = { version = "0.2.11", features = ["tokio"] } 30 | leon = "3.0.0" 31 | maybe-owned = "0.3.4" 32 | miette = "7.0.0" 33 | semver = { version = "1.0.17", features = ["serde"] } 34 | simple-git = { version = "0.2.18", optional = true } 35 | strum = "0.27.0" 36 | target-lexicon = { version = "0.13.0", features = ["std"] } 37 | tempfile = "3.5.0" 38 | thiserror = "2.0.11" 39 | tokio = { version = "1.44.0", features = [ 40 | "rt", 41 | "process", 42 | "sync", 43 | "time", 44 | ], default-features = false } 45 | tracing = "0.1.39" 46 | url = { version = "2.5.4", features = ["serde"] } 47 | zeroize = "1.8.1" 48 | 49 | [features] 50 | default = ["static", "rustls", "git"] 51 | 52 | git = ["binstalk-registry/git", "simple-git"] 53 | git-max-perf = ["git", "simple-git/git-max-perf-safe", "zlib-rs"] 54 | 55 | static = ["binstalk-downloader/static"] 56 | pkg-config = ["binstalk-downloader/pkg-config"] 57 | 58 | zlib-ng = ["binstalk-downloader/zlib-ng"] 59 | zlib-rs = ["binstalk-downloader/zlib-rs"] 60 | 61 | rustls = ["binstalk-downloader/rustls", "binstalk-registry/rustls"] 62 | native-tls = ["binstalk-downloader/native-tls", "binstalk-registry/native-tls"] 63 | 64 | trust-dns = ["binstalk-downloader/trust-dns"] 65 | 66 | # Experimental HTTP/3 client, this would require `--cfg reqwest_unstable` 67 | # to be passed to `rustc`. 68 | http3 = ["binstalk-downloader/http3"] 69 | 70 | zstd-thin = ["binstalk-downloader/zstd-thin"] 71 | cross-lang-fat-lto = ["binstalk-downloader/cross-lang-fat-lto"] 72 | 73 | [package.metadata.docs.rs] 74 | rustdoc-args = ["--cfg", "docsrs"] 75 | -------------------------------------------------------------------------------- /crates/binstalk/src/helpers.rs: -------------------------------------------------------------------------------- 1 | pub mod jobserver_client; 2 | pub mod remote { 3 | pub use binstalk_downloader::remote::*; 4 | pub use url::ParseError as UrlParseError; 5 | } 6 | pub mod lazy_gh_api_client; 7 | pub(crate) mod target_triple; 8 | pub mod tasks; 9 | 10 | pub(crate) use binstalk_downloader::download; 11 | pub use binstalk_git_repo_api::gh_api_client; 12 | 13 | pub(crate) use cargo_toml_workspace::{self, cargo_toml}; 14 | #[cfg(feature = "git")] 15 | pub(crate) use simple_git as git; 16 | 17 | pub(crate) fn is_universal_macos(target: &str) -> bool { 18 | ["universal-apple-darwin", "universal2-apple-darwin"].contains(&target) 19 | } 20 | -------------------------------------------------------------------------------- /crates/binstalk/src/helpers/jobserver_client.rs: -------------------------------------------------------------------------------- 1 | use std::{num::NonZeroUsize, thread::available_parallelism}; 2 | 3 | use jobslot::Client; 4 | use tokio::sync::OnceCell; 5 | 6 | use crate::errors::BinstallError; 7 | 8 | #[derive(Debug)] 9 | pub struct LazyJobserverClient(OnceCell); 10 | 11 | impl LazyJobserverClient { 12 | /// This must be called at the start of the program since 13 | /// `Client::from_env` requires that. 14 | #[allow(clippy::new_without_default)] 15 | pub fn new() -> Self { 16 | // Safety: 17 | // 18 | // Client::from_env is unsafe because from_raw_fd is unsafe. 19 | // It doesn't do anything that is actually unsafe, like 20 | // dereferencing pointer. 21 | let opt = unsafe { Client::from_env() }; 22 | Self(OnceCell::new_with(opt)) 23 | } 24 | 25 | pub async fn get(&self) -> Result<&Client, BinstallError> { 26 | self.0 27 | .get_or_try_init(|| async { 28 | let ncore = available_parallelism().map(NonZeroUsize::get).unwrap_or(1); 29 | Ok(Client::new(ncore)?) 30 | }) 31 | .await 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /crates/binstalk/src/helpers/lazy_gh_api_client.rs: -------------------------------------------------------------------------------- 1 | use std::{future::Future, sync::Mutex}; 2 | 3 | use binstalk_git_repo_api::gh_api_client::GhApiClient; 4 | use tokio::sync::OnceCell; 5 | use zeroize::Zeroizing; 6 | 7 | use crate::{ 8 | errors::BinstallError, 9 | helpers::{remote, tasks::AutoAbortJoinHandle}, 10 | }; 11 | 12 | pub type GitHubToken = Option>>; 13 | 14 | #[derive(Debug)] 15 | pub struct LazyGhApiClient { 16 | client: remote::Client, 17 | inner: OnceCell, 18 | task: Mutex>>, 19 | } 20 | 21 | impl LazyGhApiClient { 22 | pub fn new(client: remote::Client, auth_token: GitHubToken) -> Self { 23 | Self { 24 | inner: OnceCell::new_with(Some(GhApiClient::new(client.clone(), auth_token))), 25 | client, 26 | task: Mutex::new(None), 27 | } 28 | } 29 | 30 | pub fn with_get_gh_token_future(client: remote::Client, get_auth_token_future: Fut) -> Self 31 | where 32 | Fut: Future + Send + Sync + 'static, 33 | { 34 | Self { 35 | inner: OnceCell::new(), 36 | task: Mutex::new(Some(AutoAbortJoinHandle::spawn(get_auth_token_future))), 37 | client, 38 | } 39 | } 40 | 41 | pub async fn get(&self) -> Result<&GhApiClient, BinstallError> { 42 | self.inner 43 | .get_or_try_init(|| async { 44 | let task = self.task.lock().unwrap().take(); 45 | Ok(if let Some(task) = task { 46 | GhApiClient::new(self.client.clone(), task.await?) 47 | } else { 48 | GhApiClient::new(self.client.clone(), None) 49 | }) 50 | }) 51 | .await 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/binstalk/src/helpers/target_triple.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, str::FromStr}; 2 | 3 | use compact_str::{CompactString, ToCompactString}; 4 | use target_lexicon::Triple; 5 | 6 | use crate::{errors::BinstallError, helpers::is_universal_macos}; 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct TargetTriple { 10 | pub target_family: Cow<'static, str>, 11 | pub target_arch: Cow<'static, str>, 12 | pub target_libc: Cow<'static, str>, 13 | pub target_vendor: CompactString, 14 | } 15 | 16 | impl FromStr for TargetTriple { 17 | type Err = BinstallError; 18 | 19 | fn from_str(mut s: &str) -> Result { 20 | let is_universal_macos = is_universal_macos(s); 21 | 22 | if is_universal_macos { 23 | s = "x86_64-apple-darwin"; 24 | } 25 | 26 | let triple = Triple::from_str(s)?; 27 | 28 | Ok(Self { 29 | target_family: triple.operating_system.into_str(), 30 | target_arch: if is_universal_macos { 31 | Cow::Borrowed("universal") 32 | } else { 33 | triple.architecture.into_str() 34 | }, 35 | target_libc: triple.environment.into_str(), 36 | target_vendor: triple.vendor.to_compact_string(), 37 | }) 38 | } 39 | } 40 | 41 | impl leon::Values for TargetTriple { 42 | fn get_value<'s>(&'s self, key: &str) -> Option> { 43 | match key { 44 | "target-family" => Some(Cow::Borrowed(&self.target_family)), 45 | "target-arch" => Some(Cow::Borrowed(&self.target_arch)), 46 | "target-libc" => Some(Cow::Borrowed(&self.target_libc)), 47 | "target-vendor" => Some(Cow::Borrowed(&self.target_vendor)), 48 | 49 | _ => None, 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /crates/binstalk/src/helpers/tasks.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | future::Future, 3 | ops::{Deref, DerefMut}, 4 | pin::Pin, 5 | task::{Context, Poll}, 6 | }; 7 | 8 | use tokio::task::JoinHandle; 9 | 10 | use crate::errors::BinstallError; 11 | 12 | #[derive(Debug)] 13 | pub struct AutoAbortJoinHandle(JoinHandle); 14 | 15 | impl AutoAbortJoinHandle { 16 | pub fn new(handle: JoinHandle) -> Self { 17 | Self(handle) 18 | } 19 | } 20 | 21 | impl AutoAbortJoinHandle 22 | where 23 | T: Send + 'static, 24 | { 25 | pub fn spawn(future: F) -> Self 26 | where 27 | F: Future + Send + 'static, 28 | { 29 | Self(tokio::spawn(future)) 30 | } 31 | } 32 | 33 | impl Drop for AutoAbortJoinHandle { 34 | fn drop(&mut self) { 35 | self.0.abort(); 36 | } 37 | } 38 | 39 | impl Deref for AutoAbortJoinHandle { 40 | type Target = JoinHandle; 41 | 42 | fn deref(&self) -> &Self::Target { 43 | &self.0 44 | } 45 | } 46 | 47 | impl DerefMut for AutoAbortJoinHandle { 48 | fn deref_mut(&mut self) -> &mut Self::Target { 49 | &mut self.0 50 | } 51 | } 52 | 53 | impl Future for AutoAbortJoinHandle { 54 | type Output = Result; 55 | 56 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 57 | Pin::new(&mut Pin::into_inner(self).0) 58 | .poll(cx) 59 | .map_err(BinstallError::TaskJoinError) 60 | } 61 | } 62 | 63 | impl AutoAbortJoinHandle> 64 | where 65 | E: Into, 66 | { 67 | pub async fn flattened_join(self) -> Result { 68 | self.await?.map_err(Into::into) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /crates/binstalk/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 2 | 3 | pub mod errors; 4 | pub mod helpers; 5 | pub mod ops; 6 | 7 | use binstalk_bins as bins; 8 | pub use binstalk_fetchers as fetchers; 9 | pub use binstalk_registry as registry; 10 | pub use binstalk_types as manifests; 11 | pub use detect_targets::{get_desired_targets, DesiredTargets, TARGET}; 12 | 13 | pub use fetchers::QUICKINSTALL_STATS_URL; 14 | -------------------------------------------------------------------------------- /crates/binstalk/src/ops.rs: -------------------------------------------------------------------------------- 1 | //! Concrete Binstall operations. 2 | 3 | use std::{path::PathBuf, sync::Arc, time::Duration}; 4 | 5 | use semver::VersionReq; 6 | 7 | use crate::{ 8 | fetchers::{Data, Fetcher, SignaturePolicy, TargetDataErased}, 9 | helpers::{ 10 | gh_api_client::GhApiClient, jobserver_client::LazyJobserverClient, 11 | lazy_gh_api_client::LazyGhApiClient, remote::Client, 12 | }, 13 | manifests::cargo_toml_binstall::PkgOverride, 14 | registry::Registry, 15 | DesiredTargets, 16 | }; 17 | 18 | pub mod resolve; 19 | 20 | pub type Resolver = 21 | fn(Client, GhApiClient, Arc, Arc, SignaturePolicy) -> Arc; 22 | 23 | #[derive(Debug)] 24 | #[non_exhaustive] 25 | pub enum CargoTomlFetchOverride { 26 | #[cfg(feature = "git")] 27 | Git(crate::helpers::git::GitUrl), 28 | Path(PathBuf), 29 | } 30 | 31 | #[derive(Debug)] 32 | pub struct Options { 33 | pub no_symlinks: bool, 34 | pub dry_run: bool, 35 | pub force: bool, 36 | pub quiet: bool, 37 | pub locked: bool, 38 | pub no_track: bool, 39 | 40 | pub version_req: Option, 41 | pub cargo_toml_fetch_override: Option, 42 | pub cli_overrides: PkgOverride, 43 | 44 | pub desired_targets: DesiredTargets, 45 | pub resolvers: Vec, 46 | pub cargo_install_fallback: bool, 47 | 48 | pub temp_dir: PathBuf, 49 | pub install_path: PathBuf, 50 | pub cargo_root: Option, 51 | 52 | pub client: Client, 53 | pub gh_api_client: LazyGhApiClient, 54 | pub jobserver_client: LazyJobserverClient, 55 | pub registry: Registry, 56 | 57 | pub signature_policy: SignaturePolicy, 58 | pub disable_telemetry: bool, 59 | 60 | pub maximum_resolution_timeout: Duration, 61 | } 62 | -------------------------------------------------------------------------------- /crates/binstalk/src/ops/resolve/crate_name.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, str::FromStr}; 2 | 3 | use compact_str::CompactString; 4 | use itertools::Itertools; 5 | use semver::{Error, VersionReq}; 6 | 7 | use super::version_ext::VersionReqExt; 8 | 9 | #[derive(Debug, Clone, Eq, PartialEq)] 10 | pub struct CrateName { 11 | pub name: CompactString, 12 | pub version_req: Option, 13 | } 14 | 15 | impl fmt::Display for CrateName { 16 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 17 | write!(f, "{}", self.name)?; 18 | 19 | if let Some(version) = &self.version_req { 20 | write!(f, "@{version}")?; 21 | } 22 | 23 | Ok(()) 24 | } 25 | } 26 | 27 | impl FromStr for CrateName { 28 | type Err = Error; 29 | 30 | fn from_str(s: &str) -> Result { 31 | Ok(if let Some((name, version)) = s.split_once('@') { 32 | CrateName { 33 | name: name.into(), 34 | version_req: Some(VersionReq::parse_from_cli(version)?), 35 | } 36 | } else { 37 | CrateName { 38 | name: s.into(), 39 | version_req: None, 40 | } 41 | }) 42 | } 43 | } 44 | 45 | impl CrateName { 46 | pub fn dedup(mut crate_names: Vec) -> impl Iterator { 47 | crate_names.sort_by(|x, y| x.name.cmp(&y.name)); 48 | crate_names.into_iter().coalesce(|previous, current| { 49 | if previous.name == current.name { 50 | Ok(current) 51 | } else { 52 | Err((previous, current)) 53 | } 54 | }) 55 | } 56 | } 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use super::*; 61 | 62 | macro_rules! assert_dedup { 63 | ([ $( ( $input_name:expr, $input_version:expr ) ),* ], [ $( ( $output_name:expr, $output_version:expr ) ),* ]) => { 64 | let input_crate_names = vec![$( CrateName { 65 | name: $input_name.into(), 66 | version_req: Some($input_version.parse().unwrap()) 67 | }, )*]; 68 | 69 | let mut output_crate_names: Vec = vec![$( CrateName { 70 | name: $output_name.into(), version_req: Some($output_version.parse().unwrap()) 71 | }, )*]; 72 | output_crate_names.sort_by(|x, y| x.name.cmp(&y.name)); 73 | 74 | let crate_names: Vec<_> = CrateName::dedup(input_crate_names).collect(); 75 | assert_eq!(crate_names, output_crate_names); 76 | }; 77 | } 78 | 79 | #[test] 80 | fn test_dedup() { 81 | // Base case 0: Empty input 82 | assert_dedup!([], []); 83 | 84 | // Base case 1: With only one input 85 | assert_dedup!([("a", "1")], [("a", "1")]); 86 | 87 | // Base Case 2: Only has duplicate names 88 | assert_dedup!([("a", "1"), ("a", "2")], [("a", "2")]); 89 | 90 | // Complex Case 0: Having two crates 91 | assert_dedup!( 92 | [("a", "10"), ("b", "3"), ("a", "0"), ("b", "0"), ("a", "1")], 93 | [("a", "1"), ("b", "0")] 94 | ); 95 | 96 | // Complex Case 1: Having three crates 97 | assert_dedup!( 98 | [ 99 | ("d", "1.1"), 100 | ("a", "10"), 101 | ("b", "3"), 102 | ("d", "230"), 103 | ("a", "0"), 104 | ("b", "0"), 105 | ("a", "1"), 106 | ("d", "23") 107 | ], 108 | [("a", "1"), ("b", "0"), ("d", "23")] 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /crates/binstalk/src/ops/resolve/version_ext.rs: -------------------------------------------------------------------------------- 1 | use compact_str::format_compact; 2 | use semver::{Prerelease, Version, VersionReq}; 3 | 4 | /// Extension trait for [`VersionReq`]. 5 | pub trait VersionReqExt { 6 | /// Return `true` if `self.matches(version)` returns `true` 7 | /// and the `version` is the latest one acceptable by `self`. 8 | fn is_latest_compatible(&self, version: &Version) -> bool; 9 | 10 | /// Parse from CLI option. 11 | /// 12 | /// Notably, a bare version is treated as if preceded by `=`, not by `^` as in Cargo.toml 13 | /// dependencies. 14 | fn parse_from_cli(str: &str) -> Result 15 | where 16 | Self: Sized; 17 | } 18 | 19 | impl VersionReqExt for VersionReq { 20 | fn is_latest_compatible(&self, version: &Version) -> bool { 21 | if !self.matches(version) { 22 | return false; 23 | } 24 | 25 | // Test if bumping patch will be accepted 26 | let bumped_version = Version::new(version.major, version.minor, version.patch + 1); 27 | 28 | if self.matches(&bumped_version) { 29 | return false; 30 | } 31 | 32 | // Test if bumping prerelease will be accepted if version has one. 33 | let pre = &version.pre; 34 | if !pre.is_empty() { 35 | // Bump pre by appending random number to the end. 36 | let bumped_pre = format_compact!("{}.1", pre.as_str()); 37 | 38 | let bumped_version = Version { 39 | major: version.major, 40 | minor: version.minor, 41 | patch: version.patch, 42 | pre: Prerelease::new(&bumped_pre).unwrap(), 43 | build: Default::default(), 44 | }; 45 | 46 | if self.matches(&bumped_version) { 47 | return false; 48 | } 49 | } 50 | 51 | true 52 | } 53 | 54 | fn parse_from_cli(version: &str) -> Result { 55 | if version 56 | .chars() 57 | .next() 58 | .map(|ch| ch.is_ascii_digit()) 59 | .unwrap_or(false) 60 | { 61 | format_compact!("={version}").parse() 62 | } else { 63 | version.parse() 64 | } 65 | } 66 | } 67 | 68 | #[cfg(test)] 69 | mod tests { 70 | use super::*; 71 | 72 | #[test] 73 | fn test() { 74 | // Test star 75 | assert!(!VersionReq::STAR.is_latest_compatible(&Version::parse("0.0.1").unwrap())); 76 | assert!(!VersionReq::STAR.is_latest_compatible(&Version::parse("0.1.1").unwrap())); 77 | assert!(!VersionReq::STAR.is_latest_compatible(&Version::parse("0.1.1-alpha").unwrap())); 78 | 79 | // Test ^x.y.z 80 | assert!(!VersionReq::parse("^0.1") 81 | .unwrap() 82 | .is_latest_compatible(&Version::parse("0.1.99").unwrap())); 83 | 84 | // Test =x.y.z 85 | assert!(VersionReq::parse("=0.1.0") 86 | .unwrap() 87 | .is_latest_compatible(&Version::parse("0.1.0").unwrap())); 88 | 89 | // Test =x.y.z-alpha 90 | assert!(VersionReq::parse("=0.1.0-alpha") 91 | .unwrap() 92 | .is_latest_compatible(&Version::parse("0.1.0-alpha").unwrap())); 93 | 94 | // Test >=x.y.z-alpha 95 | assert!(!VersionReq::parse(">=0.1.0-alpha") 96 | .unwrap() 97 | .is_latest_compatible(&Version::parse("0.1.0-alpha").unwrap())); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /crates/binstalk/tests/parse-meta.Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-binstall-test" 3 | repository = "https://github.com/cargo-bins/cargo-binstall" 4 | version = "1.2.3" 5 | 6 | [[bin]] 7 | name = "cargo-binstall" 8 | path = "src/main.rs" 9 | edition = "2021" 10 | 11 | [package.metadata.binstall] 12 | pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ target }.{ archive-format }" 13 | bin-dir = "{ bin }{ binary-ext }" 14 | 15 | [package.metadata.binstall.overrides.x86_64-pc-windows-msvc] 16 | pkg-fmt = "zip" 17 | [package.metadata.binstall.overrides.x86_64-apple-darwin] 18 | pkg-fmt = "zip" 19 | -------------------------------------------------------------------------------- /crates/binstalk/tests/parse-meta.rs: -------------------------------------------------------------------------------- 1 | use binstalk::ops::resolve::load_manifest_path; 2 | use cargo_toml_workspace::cargo_toml::{Edition, Product}; 3 | use std::path::PathBuf; 4 | 5 | #[test] 6 | fn parse_meta() { 7 | let mut manifest_dir = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()); 8 | manifest_dir.push("tests/parse-meta.Cargo.toml"); 9 | 10 | let manifest = 11 | load_manifest_path(&manifest_dir, "cargo-binstall-test").expect("Error parsing metadata"); 12 | let package = manifest.package.unwrap(); 13 | let meta = package.metadata.and_then(|m| m.binstall).unwrap(); 14 | 15 | assert_eq!(&package.name, "cargo-binstall-test"); 16 | 17 | assert_eq!( 18 | meta.pkg_url.as_deref().unwrap(), 19 | "{ repo }/releases/download/v{ version }/{ name }-{ target }.{ archive-format }" 20 | ); 21 | 22 | assert_eq!( 23 | manifest.bin.as_slice(), 24 | &[Product { 25 | name: Some("cargo-binstall".to_string()), 26 | path: Some("src/main.rs".to_string()), 27 | edition: Some(Edition::E2021), 28 | ..Default::default() 29 | },], 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /crates/cargo-toml-workspace/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [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 11 | 12 | ### Other 13 | 14 | - *(deps)* bump the deps group with 2 updates ([#2084](https://github.com/cargo-bins/cargo-binstall/pull/2084)) 15 | 16 | ## [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 17 | 18 | ### Other 19 | 20 | - *(deps)* bump the deps group with 3 updates ([#2072](https://github.com/cargo-bins/cargo-binstall/pull/2072)) 21 | 22 | ## [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 23 | 24 | ### Other 25 | 26 | - update Cargo.lock dependencies 27 | 28 | ## [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 29 | 30 | ### Other 31 | 32 | - update Cargo.lock dependencies 33 | 34 | ## [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 35 | 36 | ### Other 37 | 38 | - *(deps)* bump the deps group with 3 updates (#2015) 39 | 40 | ## [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 41 | 42 | ### Other 43 | 44 | - *(deps)* bump the deps group with 2 updates (#1997) 45 | 46 | ## [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 47 | 48 | ### Other 49 | 50 | - *(deps)* bump the deps group with 3 updates ([#1993](https://github.com/cargo-bins/cargo-binstall/pull/1993)) 51 | 52 | ## [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 53 | 54 | ### Other 55 | 56 | - *(deps)* bump the deps group with 3 updates ([#1966](https://github.com/cargo-bins/cargo-binstall/pull/1966)) 57 | 58 | ## [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 59 | 60 | ### Other 61 | 62 | - *(deps)* bump the deps group with 3 updates ([#1954](https://github.com/cargo-bins/cargo-binstall/pull/1954)) 63 | -------------------------------------------------------------------------------- /crates/cargo-toml-workspace/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-toml-workspace" 3 | version = "7.0.6" 4 | edition = "2021" 5 | description = "Parse cargo workspace and load specific crate" 6 | repository = "https://github.com/cargo-bins/cargo-binstall" 7 | documentation = "https://docs.rs/cargo-toml-workspace" 8 | rust-version = "1.65.0" 9 | authors = ["Jiahao XU "] 10 | license = "Apache-2.0 OR MIT" 11 | 12 | [dependencies] 13 | cargo_toml = "0.22.1" 14 | compact_str = { version = "0.9.0", features = ["serde"] } 15 | glob = "0.3.1" 16 | normalize-path = { version = "0.2.1", path = "../normalize-path" } 17 | serde = "1.0.163" 18 | thiserror = "2.0.11" 19 | tracing = "0.1.39" 20 | 21 | [dev-dependencies] 22 | tempfile = "3.5.0" 23 | -------------------------------------------------------------------------------- /crates/cargo-toml-workspace/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /crates/detect-targets/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "detect-targets" 3 | description = "Detect the target of the env at runtime" 4 | repository = "https://github.com/cargo-bins/cargo-binstall" 5 | documentation = "https://docs.rs/detect-targets" 6 | version = "0.1.50" 7 | rust-version = "1.62.0" 8 | authors = ["Jiahao XU "] 9 | edition = "2021" 10 | license = "Apache-2.0 OR MIT" 11 | 12 | [dependencies] 13 | tokio = { version = "1.44.0", features = [ 14 | "rt", 15 | "process", 16 | "sync", 17 | ], default-features = false } 18 | tracing = { version = "0.1.39", optional = true } 19 | tracing-subscriber = { version = "0.3.17", features = [ 20 | "fmt", 21 | ], default-features = false, optional = true } 22 | cfg-if = "1.0.0" 23 | guess_host_triple = "0.1.3" 24 | 25 | [features] 26 | tracing = ["dep:tracing"] 27 | cli-logging = ["tracing", "dep:tracing-subscriber"] 28 | 29 | [target.'cfg(target_os = "windows")'.dependencies] 30 | windows-sys = { version = "0.59.0", features = [ 31 | "Win32_System_Threading", 32 | "Win32_System_SystemInformation", 33 | "Win32_Foundation", 34 | "Win32_System_LibraryLoader", 35 | ] } 36 | 37 | [dev-dependencies] 38 | tokio = { version = "1.44.0", features = ["macros"], default-features = false } 39 | 40 | [package.metadata.binstall] 41 | pkg-url = "{ repo }/releases/download/v{ version }/cargo-binstall-{ target }.full.{ archive-format }" 42 | -------------------------------------------------------------------------------- /crates/detect-targets/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /crates/detect-targets/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rerun-if-changed=build.rs"); 3 | 4 | // Fetch build target and define this for the compiler 5 | println!( 6 | "cargo:rustc-env=TARGET={}", 7 | std::env::var("TARGET").unwrap() 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /crates/detect-targets/src/desired_targets.rs: -------------------------------------------------------------------------------- 1 | use crate::detect_targets; 2 | 3 | use std::sync::Arc; 4 | 5 | use tokio::sync::OnceCell; 6 | 7 | #[derive(Debug)] 8 | enum DesiredTargetsInner { 9 | AutoDetect(Arc>>), 10 | Initialized(Vec), 11 | } 12 | 13 | #[derive(Debug)] 14 | pub struct DesiredTargets(DesiredTargetsInner); 15 | 16 | impl DesiredTargets { 17 | fn initialized(targets: Vec) -> Self { 18 | Self(DesiredTargetsInner::Initialized(targets)) 19 | } 20 | 21 | fn auto_detect() -> Self { 22 | let arc = Arc::new(OnceCell::new()); 23 | 24 | let once_cell = arc.clone(); 25 | tokio::spawn(async move { 26 | once_cell.get_or_init(detect_targets).await; 27 | }); 28 | 29 | Self(DesiredTargetsInner::AutoDetect(arc)) 30 | } 31 | 32 | pub async fn get(&self) -> &[String] { 33 | use DesiredTargetsInner::*; 34 | 35 | match &self.0 { 36 | Initialized(targets) => targets, 37 | 38 | // This will mostly just wait for the spawned task, 39 | // on rare occausion though, it will poll the future 40 | // returned by `detect_targets`. 41 | AutoDetect(once_cell) => once_cell.get_or_init(detect_targets).await, 42 | } 43 | } 44 | 45 | /// If `DesiredTargets` is provided with a list of desired targets instead 46 | /// of detecting the targets, then this function would return `Some`. 47 | pub fn get_initialized(&self) -> Option<&[String]> { 48 | use DesiredTargetsInner::*; 49 | 50 | match &self.0 { 51 | Initialized(targets) => Some(targets), 52 | AutoDetect(..) => None, 53 | } 54 | } 55 | } 56 | 57 | /// If opts_targets is `Some`, then it will be used. 58 | /// Otherwise, call `detect_targets` using `tokio::spawn` to detect targets. 59 | /// 60 | /// Since `detect_targets` internally spawns a process and wait for it, 61 | /// it's pretty costy, it is recommended to run this fn ASAP and 62 | /// reuse the result. 63 | pub fn get_desired_targets(opts_targets: Option>) -> DesiredTargets { 64 | if let Some(targets) = opts_targets { 65 | DesiredTargets::initialized(targets) 66 | } else { 67 | DesiredTargets::auto_detect() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /crates/detect-targets/src/detect.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | env, 4 | ffi::OsStr, 5 | process::{Output, Stdio}, 6 | }; 7 | 8 | use cfg_if::cfg_if; 9 | use tokio::process::Command; 10 | #[cfg(feature = "tracing")] 11 | use tracing::debug; 12 | 13 | cfg_if! { 14 | if #[cfg(any(target_os = "linux", target_os = "android"))] { 15 | mod linux; 16 | } else if #[cfg(target_os = "macos")] { 17 | mod macos; 18 | } else if #[cfg(target_os = "windows")] { 19 | mod windows; 20 | } 21 | } 22 | 23 | /// Detect the targets supported at runtime, 24 | /// which might be different from `TARGET` which is detected 25 | /// at compile-time. 26 | /// 27 | /// Return targets supported in the order of preference. 28 | /// If target_os is linux and it support gnu, then it is preferred 29 | /// to musl. 30 | /// 31 | /// If target_os is mac and it is aarch64, then aarch64 is preferred 32 | /// to x86_64. 33 | /// 34 | /// Check [this issue](https://github.com/ryankurte/cargo-binstall/issues/155) 35 | /// for more information. 36 | pub async fn detect_targets() -> Vec { 37 | let target = get_target_from_rustc().await; 38 | #[cfg(feature = "tracing")] 39 | debug!("get_target_from_rustc()={target:?}"); 40 | let target = target.unwrap_or_else(|| { 41 | let target = guess_host_triple::guess_host_triple(); 42 | #[cfg(feature = "tracing")] 43 | debug!("guess_host_triple::guess_host_triple()={target:?}"); 44 | target.unwrap_or(crate::TARGET).to_string() 45 | }); 46 | 47 | cfg_if! { 48 | if #[cfg(target_os = "macos")] { 49 | let mut targets = vec![target]; 50 | targets.extend(macos::detect_alternative_targets(&targets[0]).await); 51 | targets 52 | } else if #[cfg(target_os = "windows")] { 53 | let mut targets = vec![target]; 54 | targets.extend(windows::detect_alternative_targets(&targets[0])); 55 | targets 56 | } else if #[cfg(any(target_os = "linux", target_os = "android"))] { 57 | // Linux is a bit special, since the result from `guess_host_triple` 58 | // might be wrong about whether glibc or musl is used. 59 | linux::detect_targets(target).await 60 | } else { 61 | vec![target] 62 | } 63 | } 64 | } 65 | 66 | /// Figure out what the host target is using `rustc`. 67 | /// If `rustc` is absent, then it would return `None`. 68 | /// 69 | /// If environment variable `CARGO` is present, then 70 | /// `$CARGO -vV` will be run instead. 71 | /// 72 | /// Otherwise, it will run `rustc -vV` to detect target. 73 | async fn get_target_from_rustc() -> Option { 74 | let cmd = env::var_os("CARGO") 75 | .map(Cow::Owned) 76 | .unwrap_or_else(|| Cow::Borrowed(OsStr::new("rustc"))); 77 | 78 | let Output { status, stdout, .. } = Command::new(cmd) 79 | .arg("-vV") 80 | .stdin(Stdio::null()) 81 | .stdout(Stdio::piped()) 82 | .stderr(Stdio::null()) 83 | .spawn() 84 | .ok()? 85 | .wait_with_output() 86 | .await 87 | .ok()?; 88 | 89 | if !status.success() { 90 | return None; 91 | } 92 | 93 | let stdout = String::from_utf8_lossy(&stdout); 94 | let target = stdout 95 | .lines() 96 | .find_map(|line| line.strip_prefix("host: "))?; 97 | 98 | // The target triplets have the form of 'arch-vendor-system'. 99 | // 100 | // When building for Linux (e.g. the 'system' part is 101 | // 'linux-something'), replace the vendor with 'unknown' 102 | // so that mapping to rust standard targets happens correctly. 103 | // 104 | // For example, alpine set `rustc` host triple to 105 | // `x86_64-alpine-linux-musl`. 106 | // 107 | // Here we use splitn with n=4 since we just need to check 108 | // the third part to see if it equals to "linux" and verify 109 | // that we have at least three parts. 110 | let mut parts: Vec<&str> = target.splitn(4, '-').collect(); 111 | if *parts.get(2)? == "linux" { 112 | parts[1] = "unknown"; 113 | } 114 | Some(parts.join("-")) 115 | } 116 | -------------------------------------------------------------------------------- /crates/detect-targets/src/detect/macos.rs: -------------------------------------------------------------------------------- 1 | use std::process::Stdio; 2 | 3 | use tokio::process::Command; 4 | 5 | const AARCH64: &str = "aarch64-apple-darwin"; 6 | const X86: &str = "x86_64-apple-darwin"; 7 | /// https://doc.rust-lang.org/nightly/rustc/platform-support/x86_64h-apple-darwin.html 8 | /// 9 | /// This target is an x86_64 target that only supports Apple's late-gen 10 | /// (Haswell-compatible) Intel chips. 11 | /// 12 | /// It enables a set of target features available on these chips (AVX2 and similar), 13 | /// and MachO binaries built with this target may be used as the x86_64h entry in 14 | /// universal binaries ("fat" MachO binaries), and will fail to load on machines 15 | /// that do not support this. 16 | /// 17 | /// It is similar to x86_64-apple-darwin in nearly all respects, although 18 | /// the minimum supported OS version is slightly higher (it requires 10.8 19 | /// rather than x86_64-apple-darwin's 10.7). 20 | const X86H: &str = "x86_64h-apple-darwin"; 21 | const UNIVERSAL: &str = "universal-apple-darwin"; 22 | const UNIVERSAL2: &str = "universal2-apple-darwin"; 23 | 24 | async fn is_arch_supported(arch_name: &str) -> bool { 25 | Command::new("arch") 26 | .args(["-arch", arch_name, "/usr/bin/true"]) 27 | .stdin(Stdio::null()) 28 | .stdout(Stdio::null()) 29 | .stderr(Stdio::null()) 30 | .status() 31 | .await 32 | .map(|exit_status| exit_status.success()) 33 | .unwrap_or(false) 34 | } 35 | 36 | pub(super) async fn detect_alternative_targets(target: &str) -> impl Iterator { 37 | match target { 38 | AARCH64 => { 39 | // Spawn `arch` in parallel (probably from different threads if 40 | // mutlti-thread runtime is used). 41 | // 42 | // These two tasks are never cancelled, so it can only fail due to 43 | // panic, in which cause we would propagate by also panic here. 44 | let x86_64h_task = tokio::spawn(is_arch_supported("x86_64h")); 45 | let x86_64_task = tokio::spawn(is_arch_supported("x86_64")); 46 | [ 47 | // Prefer universal as it provides native arm executable 48 | Some(UNIVERSAL), 49 | Some(UNIVERSAL2), 50 | // Prefer x86h since it is more optimized 51 | x86_64h_task.await.unwrap().then_some(X86H), 52 | x86_64_task.await.unwrap().then_some(X86), 53 | ] 54 | } 55 | X86 => [ 56 | is_arch_supported("x86_64h").await.then_some(X86H), 57 | Some(UNIVERSAL), 58 | Some(UNIVERSAL2), 59 | None, 60 | ], 61 | X86H => [Some(X86), Some(UNIVERSAL), Some(UNIVERSAL2), None], 62 | _ => [None, None, None, None], 63 | } 64 | .into_iter() 65 | .flatten() 66 | .map(ToString::to_string) 67 | } 68 | -------------------------------------------------------------------------------- /crates/detect-targets/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Detect the target at the runtime. 2 | //! 3 | //! It runs `$CARGO -vV` if environment variable `CARGO` is present 4 | //! for cargo subcommands, otherwise it would try running `rustc -vV`. 5 | //! 6 | //! If both `rustc` isn't present on the system, it will fallback 7 | //! to using syscalls plus `ldd` on Linux to detect targets. 8 | //! 9 | //! Example use cases: 10 | //! - The binary is built with musl libc to run on anywhere, but 11 | //! the runtime supports glibc. 12 | //! - The binary is built for x86_64-apple-darwin, but run on 13 | //! aarch64-apple-darwin. 14 | //! 15 | //! This crate provides two API: 16 | //! - [`detect_targets`] provides the API to get the target 17 | //! at runtime, but the code is run on the current thread. 18 | //! - [`get_desired_targets`] provides the API to either 19 | //! use override provided by the users, or run [`detect_targets`] 20 | //! in the background using [`tokio::spawn`]. 21 | //! 22 | //! # Example 23 | //! 24 | //! `detect_targets`: 25 | //! 26 | //! ```rust 27 | //! use detect_targets::detect_targets; 28 | //! # #[tokio::main(flavor = "current_thread")] 29 | //! # async fn main() { 30 | //! 31 | //! let targets = detect_targets().await; 32 | //! eprintln!("Your platform supports targets: {targets:#?}"); 33 | //! # } 34 | //! ``` 35 | //! 36 | //! `get_desired_targets` with user override: 37 | //! 38 | //! ```rust 39 | //! use detect_targets::get_desired_targets; 40 | //! # #[tokio::main(flavor = "current_thread")] 41 | //! # async fn main() { 42 | //! 43 | //! assert_eq!( 44 | //! get_desired_targets(Some(vec![ 45 | //! "x86_64-apple-darwin".to_string(), 46 | //! "aarch64-apple-darwin".to_string(), 47 | //! ])).get().await, 48 | //! &["x86_64-apple-darwin", "aarch64-apple-darwin"], 49 | //! ); 50 | //! # } 51 | //! ``` 52 | //! 53 | //! `get_desired_targets` without user override: 54 | //! 55 | //! ```rust 56 | //! use detect_targets::get_desired_targets; 57 | //! # #[tokio::main(flavor = "current_thread")] 58 | //! # async fn main() { 59 | //! 60 | //! eprintln!( 61 | //! "Your platform supports targets: {:#?}", 62 | //! get_desired_targets(None).get().await 63 | //! ); 64 | //! # } 65 | //! ``` 66 | 67 | mod detect; 68 | pub use detect::detect_targets; 69 | 70 | mod desired_targets; 71 | pub use desired_targets::{get_desired_targets, DesiredTargets}; 72 | 73 | /// Compiled target triple, used as default for binary fetching 74 | pub const TARGET: &str = env!("TARGET"); 75 | -------------------------------------------------------------------------------- /crates/detect-targets/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use detect_targets::detect_targets; 4 | use tokio::runtime; 5 | 6 | fn main() -> io::Result<()> { 7 | #[cfg(feature = "cli-logging")] 8 | tracing_subscriber::fmt::fmt() 9 | .with_max_level(tracing::Level::TRACE) 10 | .with_writer(std::io::stderr) 11 | .init(); 12 | 13 | let targets = runtime::Builder::new_current_thread() 14 | .enable_all() 15 | .build()? 16 | .block_on(detect_targets()); 17 | 18 | for target in targets { 19 | println!("{target}"); 20 | } 21 | 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /crates/detect-wasi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "detect-wasi" 3 | description = "Detect if WASI can be run" 4 | repository = "https://github.com/cargo-bins/cargo-binstall" 5 | documentation = "https://docs.rs/detect-wasi" 6 | version = "1.0.30" 7 | rust-version = "1.61.0" 8 | authors = ["Félix Saparelli "] 9 | edition = "2021" 10 | license = "Apache-2.0 OR MIT" 11 | 12 | [dependencies] 13 | tempfile = "3.5.0" 14 | 15 | [package.metadata.binstall] 16 | pkg-url = "{ repo }/releases/download/v{ version }/cargo-binstall-{ target }.full.{ archive-format }" 17 | -------------------------------------------------------------------------------- /crates/detect-wasi/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /crates/detect-wasi/src/bin/detect-wasi.rs: -------------------------------------------------------------------------------- 1 | use std::process::exit; 2 | 3 | use detect_wasi::detect_wasi_runability; 4 | 5 | fn main() { 6 | if detect_wasi_runability().unwrap() { 7 | println!("WASI is runnable!"); 8 | exit(0); 9 | } else { 10 | println!("WASI is not runnable"); 11 | exit(1); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /crates/detect-wasi/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::{Result, Write}, 4 | process::Command, 5 | }; 6 | #[cfg(unix)] 7 | use std::{fs::Permissions, os::unix::fs::PermissionsExt}; 8 | 9 | use tempfile::tempdir; 10 | 11 | const WASI_PROGRAM: &[u8] = include_bytes!("miniwasi.wasm"); 12 | 13 | /// Detect the ability to run WASI 14 | /// 15 | /// This attempts to run a small embedded WASI program, and returns true if no errors happened. 16 | /// Errors returned by the `Result` are I/O errors from the establishment of the context, not 17 | /// errors from the run attempt. 18 | /// 19 | /// On Linux, you can configure your system to run WASI programs using a binfmt directive. Under 20 | /// systemd, write the below to `/etc/binfmt.d/wasi.conf`, with `/usr/bin/wasmtime` optionally 21 | /// replaced with the path to your WASI runtime of choice: 22 | /// 23 | /// ```plain 24 | /// :wasi:M::\x00asm::/usr/bin/wasmtime: 25 | /// ``` 26 | pub fn detect_wasi_runability() -> Result { 27 | let progdir = tempdir()?; 28 | let prog = progdir.path().join("miniwasi.wasm"); 29 | 30 | { 31 | let mut progfile = File::create(&prog)?; 32 | progfile.write_all(WASI_PROGRAM)?; 33 | 34 | #[cfg(unix)] 35 | progfile.set_permissions(Permissions::from_mode(0o777))?; 36 | } 37 | 38 | match Command::new(prog).output() { 39 | Ok(out) => Ok(out.status.success() && out.stdout.is_empty() && out.stderr.is_empty()), 40 | Err(_) => Ok(false), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/detect-wasi/src/miniwasi.wasm: -------------------------------------------------------------------------------- 1 | asm``$wasi_snapshot_preview1 proc_exitmemory_start 2 | A -------------------------------------------------------------------------------- /crates/detect-wasi/src/miniwasi.wast: -------------------------------------------------------------------------------- 1 | (module 2 | (import "wasi_snapshot_preview1" "proc_exit" (func $exit (param i32))) 3 | (memory $0 0) 4 | (export "memory" (memory $0)) 5 | (export "_start" (func $0)) 6 | (func $0 7 | (call $exit (i32.const 0)) 8 | (unreachable) 9 | ) 10 | ) 11 | -------------------------------------------------------------------------------- /crates/fs-lock/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.1.10](https://github.com/cargo-bins/cargo-binstall/compare/fs-lock-v0.1.9...fs-lock-v0.1.10) - 2025-03-19 10 | 11 | ### Fixed 12 | 13 | - actually check if lock was acquired ([#2091](https://github.com/cargo-bins/cargo-binstall/pull/2091)) 14 | 15 | ## [0.1.9](https://github.com/cargo-bins/cargo-binstall/compare/fs-lock-v0.1.8...fs-lock-v0.1.9) - 2025-03-07 16 | 17 | ### Other 18 | 19 | - *(deps)* bump the deps group with 3 updates ([#2072](https://github.com/cargo-bins/cargo-binstall/pull/2072)) 20 | 21 | ## [0.1.8](https://github.com/cargo-bins/cargo-binstall/compare/fs-lock-v0.1.7...fs-lock-v0.1.8) - 2025-02-22 22 | 23 | ### Other 24 | 25 | - Log when FileLock::drop fails to unlock file ([#2064](https://github.com/cargo-bins/cargo-binstall/pull/2064)) 26 | - Fix fs-lock error on nightly ([#2059](https://github.com/cargo-bins/cargo-binstall/pull/2059)) 27 | 28 | ## [0.1.7](https://github.com/cargo-bins/cargo-binstall/compare/fs-lock-v0.1.6...fs-lock-v0.1.7) - 2024-12-07 29 | 30 | ### Other 31 | 32 | - *(deps)* bump the deps group with 3 updates ([#1993](https://github.com/cargo-bins/cargo-binstall/pull/1993)) 33 | 34 | ## [0.1.6](https://github.com/cargo-bins/cargo-binstall/compare/fs-lock-v0.1.5...fs-lock-v0.1.6) - 2024-11-05 35 | 36 | ### Other 37 | 38 | - *(deps)* bump the deps group with 3 updates ([#1954](https://github.com/cargo-bins/cargo-binstall/pull/1954)) 39 | 40 | ## [0.1.5](https://github.com/cargo-bins/cargo-binstall/compare/fs-lock-v0.1.4...fs-lock-v0.1.5) - 2024-10-12 41 | 42 | ### Other 43 | 44 | - *(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)) 45 | 46 | ## [0.1.4](https://github.com/cargo-bins/cargo-binstall/compare/fs-lock-v0.1.3...fs-lock-v0.1.4) - 2024-08-04 47 | 48 | ### Other 49 | - *(deps)* bump the deps group across 1 directory with 2 updates ([#1859](https://github.com/cargo-bins/cargo-binstall/pull/1859)) 50 | -------------------------------------------------------------------------------- /crates/fs-lock/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fs-lock" 3 | description = "Locked files that can be used like normal File" 4 | repository = "https://github.com/cargo-bins/cargo-binstall" 5 | documentation = "https://docs.rs/fs-lock" 6 | version = "0.1.10" 7 | rust-version = "1.61.0" 8 | authors = ["Jiahao XU "] 9 | edition = "2021" 10 | license = "Apache-2.0 OR MIT" 11 | 12 | [dependencies] 13 | fs4 = "0.13.0" 14 | tracing = { version = "0.1", optional = true } 15 | -------------------------------------------------------------------------------- /crates/fs-lock/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /crates/normalize-path/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "normalize-path" 3 | description = "Like canonicalize, but without performing I/O" 4 | repository = "https://github.com/cargo-bins/cargo-binstall" 5 | documentation = "https://docs.rs/normalize-path" 6 | version = "0.2.1" 7 | rust-version = "1.61.0" 8 | authors = ["Jiahao XU "] 9 | edition = "2021" 10 | license = "Apache-2.0 OR MIT" 11 | -------------------------------------------------------------------------------- /crates/normalize-path/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /crates/normalize-path/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Normalizes paths similarly to canonicalize, but without performing I/O. 2 | //! 3 | //! This is like Python's `os.path.normpath`. 4 | //! 5 | //! Initially adapted from [Cargo's implementation][cargo-paths]. 6 | //! 7 | //! [cargo-paths]: https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61 8 | //! 9 | //! # Example 10 | //! 11 | //! ``` 12 | //! use normalize_path::NormalizePath; 13 | //! use std::path::Path; 14 | //! 15 | //! assert_eq!( 16 | //! Path::new("/A/foo/../B/./").normalize(), 17 | //! Path::new("/A/B") 18 | //! ); 19 | //! ``` 20 | 21 | use std::path::{Component, Path, PathBuf}; 22 | 23 | /// Extension trait to add `normalize_path` to std's [`Path`]. 24 | pub trait NormalizePath { 25 | /// Normalize a path without performing I/O. 26 | /// 27 | /// All redundant separator and up-level references are collapsed. 28 | /// 29 | /// However, this does not resolve links. 30 | fn normalize(&self) -> PathBuf; 31 | 32 | /// Same as [`NormalizePath::normalize`] except that if 33 | /// `Component::Prefix`/`Component::RootDir` is encountered, 34 | /// or if the path points outside of current dir, returns `None`. 35 | fn try_normalize(&self) -> Option; 36 | 37 | /// Return `true` if the path is normalized. 38 | /// 39 | /// # Quirk 40 | /// 41 | /// If the path does not start with `./` but contains `./` in the middle, 42 | /// then this function might returns `true`. 43 | fn is_normalized(&self) -> bool; 44 | } 45 | 46 | impl NormalizePath for Path { 47 | fn normalize(&self) -> PathBuf { 48 | let mut components = self.components().peekable(); 49 | let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek() { 50 | let buf = PathBuf::from(c.as_os_str()); 51 | components.next(); 52 | buf 53 | } else { 54 | PathBuf::new() 55 | }; 56 | 57 | for component in components { 58 | match component { 59 | Component::Prefix(..) => unreachable!(), 60 | Component::RootDir => { 61 | ret.push(component.as_os_str()); 62 | } 63 | Component::CurDir => {} 64 | Component::ParentDir => { 65 | ret.pop(); 66 | } 67 | Component::Normal(c) => { 68 | ret.push(c); 69 | } 70 | } 71 | } 72 | 73 | ret 74 | } 75 | 76 | fn try_normalize(&self) -> Option { 77 | let mut ret = PathBuf::new(); 78 | 79 | for component in self.components() { 80 | match component { 81 | Component::Prefix(..) | Component::RootDir => return None, 82 | Component::CurDir => {} 83 | Component::ParentDir => { 84 | if !ret.pop() { 85 | return None; 86 | } 87 | } 88 | Component::Normal(c) => { 89 | ret.push(c); 90 | } 91 | } 92 | } 93 | 94 | Some(ret) 95 | } 96 | 97 | fn is_normalized(&self) -> bool { 98 | for component in self.components() { 99 | match component { 100 | Component::CurDir | Component::ParentDir => { 101 | return false; 102 | } 103 | _ => continue, 104 | } 105 | } 106 | 107 | true 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /e2e-tests/continue-on-failure.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | unset CARGO_INSTALL_ROOT 6 | 7 | CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') 8 | export CARGO_HOME 9 | othertmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-test') 10 | export PATH="$CARGO_HOME/bin:$othertmpdir/bin:$PATH" 11 | 12 | mkdir -p "$othertmpdir/bin" 13 | # Copy it to bin to test use of env var `CARGO` 14 | cp "./$1" "$othertmpdir/bin/" 15 | 16 | 17 | ## Test --continue-on-failure 18 | set +e 19 | cargo binstall --no-confirm --continue-on-failure cargo-watch@8.4.0 non-existent-clippy 20 | exit_code="$?" 21 | 22 | set -e 23 | 24 | if [ "$exit_code" != 76 ]; then 25 | echo "Expected exit code 76, but actual exit code $exit_code" 26 | exit 1 27 | fi 28 | 29 | 30 | cargo_watch_version="$(cargo watch -V)" 31 | echo "$cargo_watch_version" 32 | 33 | [ "$cargo_watch_version" = "cargo-watch 8.4.0" ] 34 | 35 | 36 | ## Test that it is no-op when only one crate is passed 37 | set +e 38 | cargo binstall --no-confirm --continue-on-failure non-existent-clippy 39 | exit_code="$?" 40 | 41 | set -e 42 | 43 | if [ "$exit_code" != 76 ]; then 44 | echo "Expected exit code 76, but actual exit code $exit_code" 45 | exit 1 46 | fi 47 | 48 | # Test if both crates are invalid 49 | set +e 50 | cargo binstall --no-confirm --continue-on-failure non-existent-clippy non-existent-clippy2 51 | exit_code="$?" 52 | 53 | set -e 54 | 55 | if [ "$exit_code" != 76 ]; then 56 | echo "Expected exit code 76, but actual exit code $exit_code" 57 | exit 1 58 | fi 59 | -------------------------------------------------------------------------------- /e2e-tests/fake-cargo/cargo: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo Always returns 1 to prevent use of "cargo-build" 4 | exit 1 5 | -------------------------------------------------------------------------------- /e2e-tests/git.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | test_cargo_binstall_install() { 6 | # Test that the installed binaries can be run 7 | cargo binstall --help >/dev/null 8 | 9 | cargo_binstall_version="$(cargo binstall -V)" 10 | echo "$cargo_binstall_version" 11 | 12 | [ "$cargo_binstall_version" = "cargo-binstall 0.12.0" ] 13 | } 14 | 15 | unset CARGO_INSTALL_ROOT 16 | 17 | CARGO_HOME="$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home')" 18 | export CARGO_HOME 19 | export PATH="$CARGO_HOME/bin:$PATH" 20 | 21 | GIT="$(mktemp -d 2>/dev/null || mktemp -d -t 'git')" 22 | if [ "$OSTYPE" = "cygwin" ] || [ "$OSTYPE" = "msys" ]; then 23 | # Convert it to windows path so `--git "file://$GIT"` would work 24 | # on windows. 25 | GIT="$(cygpath -w "$GIT")" 26 | fi 27 | 28 | git init "$GIT" 29 | cp manifests/github-test-Cargo.toml "$GIT/Cargo.toml" 30 | ( 31 | cd "$GIT" 32 | git config user.email 'test@example.com' 33 | git config user.name 'test' 34 | git add Cargo.toml 35 | git commit -m "Add Cargo.toml" 36 | ) 37 | 38 | # Install binaries using `--git` 39 | "./$1" binstall --force --git "file://$GIT" --no-confirm cargo-binstall 40 | 41 | test_cargo_binstall_install 42 | 43 | cp -r manifests/workspace/* "$GIT" 44 | ( 45 | cd "$GIT" 46 | git add . 47 | git commit -m 'Update to workspace' 48 | ) 49 | COMMIT_HASH="$(cd "$GIT" && git rev-parse HEAD)" 50 | 51 | if [ "$OSTYPE" = "cygwin" ] || [ "$OSTYPE" = "msys" ]; then 52 | source="(git+file:///$(cygpath -m "$GIT")#$COMMIT_HASH)" 53 | else 54 | source="(git+file://$GIT#$COMMIT_HASH)" 55 | fi 56 | 57 | # Install cargo-binstall using `--git` 58 | "./$1" binstall --force --git "file://$GIT" --no-confirm cargo-binstall 59 | 60 | test_cargo_binstall_install 61 | 62 | cat "$CARGO_HOME/.crates.toml" 63 | grep -F "cargo-binstall 0.12.0 $source" <"$CARGO_HOME/.crates.toml" 64 | 65 | # Install cargo-watch using `--git` 66 | "./$1" binstall --force --git "file://$GIT" --no-confirm cargo-watch 67 | 68 | cargo_watch_version="$(cargo watch -V)" 69 | echo "$cargo_watch_version" 70 | 71 | [ "$cargo_watch_version" = "cargo-watch 8.4.0" ] 72 | 73 | cat "$CARGO_HOME/.crates.toml" 74 | grep -F "cargo-watch 8.4.0 $source" <"$CARGO_HOME/.crates.toml" 75 | -------------------------------------------------------------------------------- /e2e-tests/live.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | unset CARGO_INSTALL_ROOT 6 | 7 | # - `b3sum@<=1.3.3` would test `fetch_crate_cratesio_version_matched` ability 8 | # to find versions matching <= 1.3.3 9 | # - `cargo-quickinstall` would test `fetch_crate_cratesio_version_matched` ability 10 | # to find latest stable version. 11 | # - `git-mob-tool tests the using of using a binary name (`git-mob`) different 12 | # from the package name. 13 | 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" 14 | 15 | CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') 16 | export CARGO_HOME 17 | othertmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-test') 18 | export PATH="$CARGO_HOME/bin:$othertmpdir/bin:$PATH" 19 | 20 | mkdir -p "$othertmpdir/bin" 21 | # Copy it to bin to test use of env var `CARGO` 22 | cp "./$1" "$othertmpdir/bin/" 23 | 24 | # Install binaries using cargo-binstall 25 | # shellcheck disable=SC2086 26 | cargo binstall --no-confirm $crates 27 | 28 | rm -r "$othertmpdir" 29 | 30 | # Test that the installed binaries can be run 31 | b3sum_version="$(b3sum --version)" 32 | echo "$b3sum_version" 33 | 34 | [ "$b3sum_version" = "b3sum 1.3.3" ] 35 | 36 | cargo_release_version="$(cargo-release release --version)" 37 | echo "$cargo_release_version" 38 | 39 | [ "$cargo_release_version" = "cargo-release 0.24.9" ] 40 | 41 | cargo binstall --help >/dev/null 42 | 43 | cargo_binstall_version="$(cargo-binstall -V)" 44 | echo "cargo-binstall version $cargo_binstall_version" 45 | 46 | [ "$cargo_binstall_version" = "0.20.1" ] 47 | 48 | cargo_watch_version="$(cargo watch -V)" 49 | echo "$cargo_watch_version" 50 | 51 | [ "$cargo_watch_version" = "cargo-watch 8.4.0" ] 52 | 53 | miniserve_version="$(miniserve -V)" 54 | echo "$miniserve_version" 55 | 56 | [ "$miniserve_version" = "miniserve 0.23.0" ] 57 | 58 | cargo-quickinstall -V 59 | 60 | jj_version="$(jj --version)" 61 | echo "$jj_version" 62 | 63 | [ "$jj_version" = "jj 0.18.0-9fb5307b7886e390c02817af7c31b403f0279144" ] 64 | 65 | git_mob_version="$(git-mob --version)" 66 | echo "$git_mob_version" 67 | 68 | [ "$git_mob_version" = "git-mob-tool 1.6.1" ] 69 | -------------------------------------------------------------------------------- /e2e-tests/manifest-path.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | unset CARGO_INSTALL_ROOT 6 | 7 | CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') 8 | export CARGO_HOME 9 | export PATH="$CARGO_HOME/bin:$PATH" 10 | 11 | # Install binaries using `--manifest-path` 12 | # Also test default github template 13 | "./$1" binstall --force --manifest-path "manifests/github-test-Cargo.toml" --no-confirm cargo-binstall 14 | 15 | # Test that the installed binaries can be run 16 | cargo binstall --help >/dev/null 17 | 18 | cargo_binstall_version="$(cargo binstall -V)" 19 | echo "$cargo_binstall_version" 20 | 21 | [ "$cargo_binstall_version" = "cargo-binstall 0.12.0" ] 22 | 23 | cat "$CARGO_HOME/.crates.toml" 24 | grep -F "cargo-binstall 0.12.0 (path+file://manifests/github-test-Cargo.toml)" <"$CARGO_HOME/.crates.toml" 25 | -------------------------------------------------------------------------------- /e2e-tests/manifests/bitbucket-test-Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-binstall" 3 | description = "Rust binary package installer for CI integration" 4 | repository = "https://bitbucket.org/nobodyxusdcdc/hello-world" 5 | version = "0.12.0" 6 | rust-version = "1.61.0" 7 | authors = ["ryan "] 8 | edition = "2021" 9 | license = "GPL-3.0" 10 | 11 | [[bin]] 12 | name = "cargo-binstall" 13 | path = "src/main.rs" 14 | -------------------------------------------------------------------------------- /e2e-tests/manifests/github-test-Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-binstall" 3 | description = "Rust binary package installer for CI integration" 4 | repository = "https://github.com/cargo-bins/cargo-binstall" 5 | version = "0.12.0" 6 | rust-version = "1.61.0" 7 | authors = ["ryan "] 8 | edition = "2021" 9 | license = "GPL-3.0" 10 | 11 | [[bin]] 12 | name = "cargo-binstall" 13 | path = "src/main.rs" 14 | -------------------------------------------------------------------------------- /e2e-tests/manifests/github-test-Cargo2.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-binstall" 3 | description = "Rust binary package installer for CI integration" 4 | repository = "https://github.com/cargo-bins/cargo-binstall.git" 5 | version = "0.12.0" 6 | rust-version = "1.61.0" 7 | authors = ["ryan "] 8 | edition = "2021" 9 | license = "GPL-3.0" 10 | 11 | [package.metadata.binstall] 12 | bin-dir = "{ bin }{ binary-ext }" 13 | 14 | [[bin]] 15 | name = "cargo-binstall" 16 | path = "src/main.rs" 17 | -------------------------------------------------------------------------------- /e2e-tests/manifests/gitlab-test-Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-binstall" 3 | description = "Rust binary package installer for CI integration" 4 | repository = "https://gitlab.kitware.com/NobodyXu/hello-world.git" 5 | version = "0.2.0" 6 | rust-version = "1.61.0" 7 | authors = ["ryan "] 8 | edition = "2021" 9 | license = "GPL-3.0" 10 | 11 | [[bin]] 12 | name = "cargo-binstall" 13 | path = "src/main.rs" 14 | -------------------------------------------------------------------------------- /e2e-tests/manifests/private-github-repo-test-Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-binstall" 3 | description = "Rust binary package installer for CI integration" 4 | repository = "https://github.com/cargo-bins/private-repo-for-testing.git" 5 | version = "0.12.0" 6 | rust-version = "1.61.0" 7 | authors = ["ryan "] 8 | edition = "2021" 9 | license = "GPL-3.0" 10 | 11 | [[bin]] 12 | name = "cargo-binstall" 13 | path = "src/main.rs" 14 | -------------------------------------------------------------------------------- /e2e-tests/manifests/signing-Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "signing-test" 3 | description = "Rust binary package installer for CI integration" 4 | version = "0.1.0" 5 | authors = ["ryan "] 6 | edition = "2021" 7 | license = "GPL-3.0" 8 | 9 | [[bin]] 10 | name = "signing-test" 11 | path = "src/main.rs" 12 | 13 | [package.metadata.binstall] 14 | pkg-url = "https://localhost:4443/signing-test.tar" 15 | pkg-fmt = "tar" 16 | 17 | [package.metadata.binstall.signing] 18 | algorithm = "minisign" 19 | pubkey = "RWRnmBcLmQbXVcEPWo2OOKMI36kki4GiI7gcBgIaPLwvxe14Wtxm9acX" 20 | -------------------------------------------------------------------------------- /e2e-tests/manifests/strategies-test-Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-update" 3 | repository = "https://github.com/nabijaczleweli/cargo-update" 4 | version = "11.1.2" 5 | 6 | [[bin]] 7 | name = "cargo-install-update" 8 | path = "src/main.rs" 9 | test = false 10 | doc = false 11 | 12 | [[bin]] 13 | name = "cargo-install-update-config" 14 | path = "src/main-config.rs" 15 | test = false 16 | doc = false 17 | 18 | [package.metadata.binstall] 19 | disabled-strategies = ["quick-install", "compile"] 20 | -------------------------------------------------------------------------------- /e2e-tests/manifests/strategies-test-Cargo2.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-update" 3 | repository = "https://github.com/nabijaczleweli/cargo-update" 4 | version = "11.1.2" 5 | 6 | [[bin]] 7 | name = "cargo-install-update" 8 | path = "src/main.rs" 9 | test = false 10 | doc = false 11 | 12 | [[bin]] 13 | name = "cargo-install-update-config" 14 | path = "src/main-config.rs" 15 | test = false 16 | doc = false 17 | 18 | [package.metadata.binstall] 19 | disabled-strategies = ["quick-install"] 20 | -------------------------------------------------------------------------------- /e2e-tests/manifests/strategies-test-override-Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-quickinstall" 3 | repository = "https://github.com/cargo-bins/cargo-quickinstall" 4 | version = "0.2.10" 5 | 6 | [[bin]] 7 | name = "cargo-quickinstall" 8 | path = "src/main.rs" 9 | test = false 10 | doc = false 11 | 12 | [package.metadata.binstall] 13 | disabled-strategies = ["crate-meta-data", "quick-install", "compile"] 14 | -------------------------------------------------------------------------------- /e2e-tests/manifests/workspace/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/*/*/*/*/*/*", "b/*/*/*/*"] 3 | exclude = ["b/c/d/e/*"] 4 | -------------------------------------------------------------------------------- /e2e-tests/manifests/workspace/b/c/d/e/f/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-binstall2" 3 | description = "Rust binary package installer for CI integration" 4 | repository = "https://bitbucket.org/nobodyxusdcdc/hello-world" 5 | version = "0.0.0" 6 | rust-version = "1.61.0" 7 | authors = ["ryan "] 8 | edition = "2021" 9 | license = "GPL-3.0" 10 | 11 | [[bin]] 12 | name = "cargo-binstall" 13 | path = "src/main.rs" 14 | -------------------------------------------------------------------------------- /e2e-tests/manifests/workspace/crates/a/b/c/d/e/cargo-binstall/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-binstall" 3 | version = "0.12.0" 4 | repository = "https://github.com/cargo-bins/cargo-binstall" 5 | 6 | [[bin]] 7 | name = "cargo-binstall" 8 | path = "src/main.rs" 9 | -------------------------------------------------------------------------------- /e2e-tests/manifests/workspace/crates/a/b/c/d/e/cargo-watch/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-watch" 3 | version = "8.4.0" 4 | repository = "https://github.com/watchexec/cargo-watch" 5 | 6 | [[bin]] 7 | name = "cargo-watch" 8 | -------------------------------------------------------------------------------- /e2e-tests/manifests/workspace/crates/a/b/c/d/e/cargo-watch/src/main.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cargo-bins/cargo-binstall/e8c9cc3599f6c4063d143083205f98ca25d91677/e2e-tests/manifests/workspace/crates/a/b/c/d/e/cargo-watch/src/main.rs -------------------------------------------------------------------------------- /e2e-tests/no-track.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | unset CARGO_INSTALL_ROOT 6 | 7 | CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') 8 | export CARGO_HOME 9 | export PATH="$CARGO_HOME/bin:$PATH" 10 | 11 | "./$1" binstall -y cargo-binstall@0.20.1 12 | cargo-binstall --help >/dev/null 13 | 14 | set +e 15 | 16 | "./$1" binstall -y --no-track cargo-binstall@0.20.1 17 | exit_code="$?" 18 | 19 | set -e 20 | 21 | if [ "$exit_code" != 88 ]; then 22 | echo "Expected exit code 88 BinFile Error, but actual exit code $exit_code" 23 | exit 1 24 | fi 25 | 26 | 27 | "./$1" binstall -y --no-track --force cargo-binstall@0.20.1 28 | cargo-binstall --help >/dev/null 29 | -------------------------------------------------------------------------------- /e2e-tests/other-repos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | unset CARGO_INSTALL_ROOT 6 | 7 | CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') 8 | export CARGO_HOME 9 | export PATH="$CARGO_HOME/bin:$PATH" 10 | 11 | # Test default GitLab pkg-url templates 12 | #"./$1" binstall \ 13 | # --force \ 14 | # --manifest-path "manifests/gitlab-test-Cargo.toml" \ 15 | # --no-confirm \ 16 | # --disable-strategies compile \ 17 | # cargo-binstall 18 | 19 | # temporarily disable bitbucket testing as bitbucket is down 20 | ## Test default BitBucket pkg-url templates 21 | #"./$1" binstall \ 22 | # --force \ 23 | # --manifest-path "manifests/bitbucket-test-Cargo.toml" \ 24 | # --no-confirm \ 25 | # --disable-strategies compile \ 26 | # cargo-binstall 27 | # 28 | ## Test that the installed binaries can be run 29 | #cargo binstall --help >/dev/null 30 | # 31 | #cargo_binstall_version="$(cargo binstall -V)" 32 | #echo "$cargo_binstall_version" 33 | # 34 | #[ "$cargo_binstall_version" = "cargo-binstall 0.12.0" ] 35 | 36 | # Test default Github pkg-url templates, 37 | # with bin-dir provided 38 | "./$1" binstall \ 39 | --force \ 40 | --manifest-path "manifests/github-test-Cargo2.toml" \ 41 | --no-confirm \ 42 | --disable-strategies compile \ 43 | cargo-binstall 44 | 45 | # Test that the installed binaries can be run 46 | cargo binstall --help >/dev/null 47 | 48 | cargo_binstall_version="$(cargo binstall -V)" 49 | echo "$cargo_binstall_version" 50 | 51 | [ "$cargo_binstall_version" = "cargo-binstall 0.12.0" ] 52 | -------------------------------------------------------------------------------- /e2e-tests/private-github-repo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | unset CARGO_INSTALL_ROOT 6 | 7 | CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') 8 | export CARGO_HOME 9 | export PATH="$CARGO_HOME/bin:$PATH" 10 | 11 | # Install binaries using `--manifest-path` 12 | # Also test default github template 13 | "./$1" binstall --force --manifest-path "manifests/private-github-repo-test-Cargo.toml" --no-confirm cargo-binstall --strategies crate-meta-data 14 | 15 | # Test that the installed binaries can be run 16 | cargo binstall --help >/dev/null 17 | 18 | cargo_binstall_version="$(cargo binstall -V)" 19 | echo "$cargo_binstall_version" 20 | 21 | [ "$cargo_binstall_version" = "cargo-binstall 0.12.0" ] 22 | -------------------------------------------------------------------------------- /e2e-tests/registries.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | test_cargo_binstall_install() { 6 | # Test that the installed binaries can be run 7 | cargo binstall --help >/dev/null 8 | 9 | cargo_binstall_version="$(cargo binstall -V)" 10 | echo "$cargo_binstall_version" 11 | 12 | [ "$cargo_binstall_version" = "cargo-binstall 0.12.0" ] 13 | } 14 | 15 | unset CARGO_INSTALL_ROOT 16 | 17 | CARGO_HOME="$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home')" 18 | export CARGO_HOME 19 | export PATH="$CARGO_HOME/bin:$PATH" 20 | 21 | # Testing conflicts of `--index` and `--registry` 22 | set +e 23 | 24 | "./$1" binstall --index 'sparse+https://index.crates.io/' --registry t1 cargo-binstall 25 | exit_code="$?" 26 | 27 | set -e 28 | 29 | if [ "$exit_code" != 2 ]; then 30 | echo "Expected exit code 2, but actual exit code $exit_code" 31 | exit 1 32 | fi 33 | 34 | cat >"$CARGO_HOME/config.toml" << EOF 35 | [registries] 36 | t1 = { index = "https://github.com/rust-lang/crates.io-index" } 37 | t2 = { index = "sparse+https://index.crates.io/" } 38 | 39 | [registry] 40 | default = "t1" 41 | EOF 42 | 43 | # Install binaries using default registry in config 44 | "./$1" binstall --force -y cargo-binstall@0.12.0 45 | 46 | grep -F "cargo-binstall 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" <"$CARGO_HOME/.crates.toml" 47 | 48 | test_cargo_binstall_install 49 | 50 | # Install binaries using registry t2 in config 51 | "./$1" binstall --force --registry t2 -y cargo-binstall@0.12.0 52 | 53 | grep -F "cargo-binstall 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" <"$CARGO_HOME/.crates.toml" 54 | 55 | test_cargo_binstall_install 56 | 57 | # Install binaries using registry t3 in env 58 | CARGO_REGISTRIES_t3_INDEX='sparse+https://index.crates.io/' "./$1" binstall --force --registry t3 -y cargo-binstall@0.12.0 59 | 60 | grep -F "cargo-binstall 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" <"$CARGO_HOME/.crates.toml" 61 | 62 | test_cargo_binstall_install 63 | 64 | # Install binaries using index directly 65 | "./$1" binstall --force --index 'sparse+https://index.crates.io/' -y cargo-binstall@0.12.0 66 | 67 | grep -F "cargo-binstall 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" <"$CARGO_HOME/.crates.toml" 68 | 69 | test_cargo_binstall_install 70 | -------------------------------------------------------------------------------- /e2e-tests/self-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | unset CARGO_INSTALL_ROOT 6 | 7 | CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') 8 | export CARGO_HOME 9 | export PATH="$CARGO_HOME/bin:$PATH" 10 | 11 | "./$1" --self-install 12 | 13 | cargo binstall --help 14 | cargo install --list 15 | -------------------------------------------------------------------------------- /e2e-tests/self-upgrade-no-symlink.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | unset CARGO_INSTALL_ROOT 6 | 7 | CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') 8 | export CARGO_HOME 9 | export PATH="$CARGO_HOME/bin:$PATH" 10 | 11 | # first boostrap-install into the CARGO_HOME 12 | mkdir -p "$CARGO_HOME/bin" 13 | cp "./$1" "$CARGO_HOME/bin" 14 | 15 | # now we're running the CARGO_HOME/bin/cargo-binstall (via cargo): 16 | 17 | # self update replacing no-symlinks with no-symlinks 18 | cargo binstall --no-confirm --no-symlinks --force cargo-binstall@0.20.1 19 | 20 | # self update replacing no-symlinks with symlinks 21 | cp "./$1" "$CARGO_HOME/bin" 22 | 23 | cargo binstall --no-confirm --force cargo-binstall@0.20.1 24 | 25 | # self update replacing symlinks with symlinks 26 | ln -snf "$(pwd)/cargo-binstall" "$CARGO_HOME/bin/cargo-binstall" 27 | 28 | cargo binstall --no-confirm --force cargo-binstall@0.20.1 29 | 30 | # self update replacing symlinks with no-symlinks 31 | ln -snf "$(pwd)/cargo-binstall" "$CARGO_HOME/bin/cargo-binstall" 32 | 33 | cargo binstall --no-confirm --force --no-symlinks cargo-binstall@0.20.1 34 | -------------------------------------------------------------------------------- /e2e-tests/signing.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | unset CARGO_INSTALL_ROOT 6 | 7 | CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') 8 | export CARGO_HOME 9 | export PATH="$CARGO_HOME/bin:$PATH" 10 | 11 | echo Generate tls cert 12 | 13 | CERT_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'cert-dir') 14 | export CERT_DIR 15 | 16 | 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' 17 | openssl req -new -newkey rsa:4096 -sha256 -nodes -out "$CERT_DIR/"server.csr -keyout "$CERT_DIR/"server.key -subj '//C=UT/CN=localhost' 18 | 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 19 | 20 | python3 signing/server.py & 21 | server_pid=$! 22 | trap 'kill $server_pid' ERR INT TERM 23 | 24 | export BINSTALL_HTTPS_ROOT_CERTS="$CERT_DIR/ca.pem" 25 | 26 | signing/wait-for-server.sh 27 | 28 | "./$1" binstall --force --manifest-path manifests/signing-Cargo.toml --no-confirm signing-test 29 | "./$1" binstall --force --manifest-path manifests/signing-Cargo.toml --no-confirm --only-signed signing-test 30 | "./$1" binstall --force --manifest-path manifests/signing-Cargo.toml --no-confirm --skip-signatures signing-test 31 | 32 | # from quick-install 33 | "./$1" binstall --force --strategies quick-install --no-confirm --only-signed --target x86_64-unknown-linux-musl zellij@0.38.2 34 | 35 | kill $server_pid || true 36 | -------------------------------------------------------------------------------- /e2e-tests/signing/minisign.key: -------------------------------------------------------------------------------- 1 | untrusted comment: minisign encrypted secret key 2 | RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZ5gXC5kG11Wu99VVpToebb+yc0MOw4cbWzxSHyOxoSTu6kBrK09z/MEPWo2OOKMI36kki4GiI7gcBgIaPLwvxe14Wtxm9acXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= 3 | -------------------------------------------------------------------------------- /e2e-tests/signing/minisign.pub: -------------------------------------------------------------------------------- 1 | untrusted comment: minisign public key 55D706990B179867 2 | RWRnmBcLmQbXVcEPWo2OOKMI36kki4GiI7gcBgIaPLwvxe14Wtxm9acX 3 | -------------------------------------------------------------------------------- /e2e-tests/signing/server.ext: -------------------------------------------------------------------------------- 1 | authorityKeyIdentifier=keyid,issuer 2 | basicConstraints=CA:FALSE 3 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 4 | subjectAltName = @alt_names 5 | [alt_names] 6 | DNS.1 = localhost 7 | -------------------------------------------------------------------------------- /e2e-tests/signing/server.py: -------------------------------------------------------------------------------- 1 | import http.server 2 | import os 3 | import ssl 4 | from pathlib import Path 5 | 6 | cert_dir = Path(os.environ["CERT_DIR"]) 7 | 8 | os.chdir(os.path.dirname(__file__)) 9 | 10 | server_address = ('', 4443) 11 | httpd = http.server.HTTPServer(server_address, http.server.SimpleHTTPRequestHandler) 12 | ctx = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_SERVER) 13 | ctx.load_cert_chain(certfile=cert_dir / "server.pem", keyfile=cert_dir / "server.key") 14 | httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True) 15 | httpd.serve_forever() 16 | -------------------------------------------------------------------------------- /e2e-tests/signing/signing-test.exe.nasm: -------------------------------------------------------------------------------- 1 | ; tiny97.asm, copyright Alexander Sotirov 2 | 3 | BITS 32 4 | ; 5 | ; MZ header 6 | ; The only two fields that matter are e_magic and e_lfanew 7 | 8 | mzhdr: 9 | dw "MZ" ; e_magic 10 | dw 0 ; e_cblp UNUSED 11 | 12 | ; PE signature 13 | pesig: 14 | dd "PE" ; e_cp, e_crlc UNUSED ; PE signature 15 | 16 | ; PE header 17 | pehdr: 18 | dw 0x014C ; e_cparhdr UNUSED ; Machine (Intel 386) 19 | dw 1 ; e_minalloc UNUSED ; NumberOfSections 20 | 21 | ; dd 0xC3582A6A ; e_maxalloc, e_ss UNUSED ; TimeDateStamp UNUSED 22 | 23 | ; Entry point 24 | start: 25 | push byte 42 26 | pop eax 27 | ret 28 | 29 | codesize equ $ - start 30 | 31 | dd 0 ; e_sp, e_csum UNUSED ; PointerToSymbolTable UNUSED 32 | dd 0 ; e_ip, e_cs UNUSED ; NumberOfSymbols UNUSED 33 | dw sections-opthdr ; e_lsarlc UNUSED ; SizeOfOptionalHeader 34 | dw 0x103 ; e_ovno UNUSED ; Characteristics 35 | 36 | ; PE optional header 37 | ; The debug directory size at offset 0x94 from here must be 0 38 | 39 | filealign equ 4 40 | sect_align equ 4 ; must be 4 because of e_lfanew 41 | 42 | %define round(n, r) (((n+(r-1))/r)*r) 43 | 44 | opthdr: 45 | dw 0x10B ; e_res UNUSED ; Magic (PE32) 46 | db 8 ; MajorLinkerVersion UNUSED 47 | db 0 ; MinorLinkerVersion UNUSED 48 | 49 | ; PE code section 50 | sections: 51 | dd round(codesize, filealign) ; SizeOfCode UNUSED ; Name UNUSED 52 | dd 0 ; e_oemid, e_oeminfo UNUSED ; SizeOfInitializedData UNUSED 53 | dd codesize ; e_res2 UNUSED ; SizeOfUninitializedData UNUSED ; VirtualSize 54 | dd start ; AddressOfEntryPoint ; VirtualAddress 55 | dd codesize ; BaseOfCode UNUSED ; SizeOfRawData 56 | dd start ; BaseOfData UNUSED ; PointerToRawData 57 | dd 0x400000 ; ImageBase ; PointerToRelocations UNUSED 58 | dd sect_align ; e_lfanew ; SectionAlignment ; PointerToLinenumbers UNUSED 59 | dd filealign ; FileAlignment ; NumberOfRelocations, NumberOfLinenumbers UNUSED 60 | dw 4 ; MajorOperatingSystemVersion UNUSED ; Characteristics UNUSED 61 | dw 0 ; MinorOperatingSystemVersion UNUSED 62 | dw 0 ; MajorImageVersion UNUSED 63 | dw 0 ; MinorImageVersion UNUSED 64 | dw 4 ; MajorSubsystemVersion 65 | dw 0 ; MinorSubsystemVersion UNUSED 66 | dd 0 ; Win32VersionValue UNUSED 67 | dd round(hdrsize, sect_align)+round(codesize,sect_align) ; SizeOfImage 68 | dd round(hdrsize, filealign) ; SizeOfHeaders 69 | dd 0 ; CheckSum UNUSED 70 | db 2 ; Subsystem (Win32 GUI) 71 | 72 | hdrsize equ $ - $$ 73 | filesize equ $ - $$ 74 | 75 | -------------------------------------------------------------------------------- /e2e-tests/signing/signing-test.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cargo-bins/cargo-binstall/e8c9cc3599f6c4063d143083205f98ca25d91677/e2e-tests/signing/signing-test.tar -------------------------------------------------------------------------------- /e2e-tests/signing/signing-test.tar.sig: -------------------------------------------------------------------------------- 1 | untrusted comment: signature from minisign secret key 2 | RURnmBcLmQbXVVINqskhik18fjpzn1TTn7UZWPC6TuVNSZc+0CqLiNxJhBvT3aXiFHxiEwiBeQaFipsxXux06C12+rwT9Pozgwo= 3 | trusted comment: timestamp:1693846563 file:signing-test.tar hashed 4 | fQqqvTO6KgHSHf6/n18FQVJgO8azb1dB90jwj2YukbRfwK3QD0rNSDFBmhN73H7Pwxsz9of42OG60dfXA+ldCQ== 5 | -------------------------------------------------------------------------------- /e2e-tests/signing/wait-for-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | CERT="${BINSTALL_HTTPS_ROOT_CERTS?}" 6 | 7 | counter=0 8 | 9 | while ! curl --cacert "$CERT" --ssl-revoke-best-effort -L https://localhost:4443/signing-test.tar | file -; do 10 | counter=$(( counter + 1 )) 11 | if [ "$counter" = "20" ]; then 12 | echo Failed to connect to https server 13 | exit 1; 14 | fi 15 | sleep 10 16 | done 17 | -------------------------------------------------------------------------------- /e2e-tests/strategies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | unset CARGO_INSTALL_ROOT 6 | 7 | CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') 8 | export CARGO_HOME 9 | export PATH="$CARGO_HOME/bin:$PATH" 10 | 11 | ## Test --disable-strategies 12 | set +e 13 | 14 | "./$1" binstall --no-confirm --disable-strategies quick-install,compile cargo-update@11.1.2 15 | exit_code="$?" 16 | 17 | set -e 18 | 19 | if [ "$exit_code" != 94 ]; then 20 | echo "Expected exit code 94, but actual exit code $exit_code" 21 | exit 1 22 | fi 23 | 24 | ## Test --strategies 25 | set +e 26 | 27 | "./$1" binstall --no-confirm --strategies crate-meta-data cargo-update@11.1.2 28 | exit_code="$?" 29 | 30 | set -e 31 | 32 | if [ "$exit_code" != 94 ]; then 33 | echo "Expected exit code 94, but actual exit code $exit_code" 34 | exit 1 35 | fi 36 | 37 | ## Test compile-only strategy 38 | "./$1" binstall --no-confirm --strategies compile cargo-quickinstall@0.2.8 39 | 40 | ## Test Cargo.toml disable-strategies 41 | set +e 42 | 43 | "./$1" binstall --no-confirm --manifest-path "manifests/strategies-test-Cargo.toml" cargo-update@11.1.2 44 | exit_code="$?" 45 | 46 | set -e 47 | 48 | if [ "$exit_code" != 94 ]; then 49 | echo "Expected exit code 94, but actual exit code $exit_code" 50 | exit 1 51 | fi 52 | 53 | set +e 54 | 55 | "./$1" binstall --disable-strategies compile --no-confirm --manifest-path "manifests/strategies-test-Cargo2.toml" cargo-update@11.1.2 56 | exit_code="$?" 57 | 58 | set -e 59 | 60 | if [ "$exit_code" != 94 ]; then 61 | echo "Expected exit code 94, but actual exit code $exit_code" 62 | exit 1 63 | fi 64 | 65 | ## Test --strategies overriding `disabled-strategies=["compile"]` in Cargo.toml 66 | "./$1" binstall --no-confirm --manifest-path "manifests/strategies-test-override-Cargo.toml" --strategies compile cargo-quickinstall@0.2.10 67 | -------------------------------------------------------------------------------- /e2e-tests/subcrate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | unset CARGO_INSTALL_ROOT 6 | 7 | CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') 8 | export CARGO_HOME 9 | othertmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-test') 10 | export PATH="$CARGO_HOME/bin:$othertmpdir/bin:$PATH" 11 | 12 | mkdir -p "$othertmpdir/bin" 13 | # Copy it to bin to test use of env var `CARGO` 14 | cp "./$1" "$othertmpdir/bin/" 15 | 16 | # cargo-audit 17 | cargo binstall --no-confirm cargo-audit@0.18.3 --strategies crate-meta-data 18 | 19 | cargo_audit_version="$(cargo audit --version)" 20 | echo "$cargo_audit_version" 21 | 22 | [ "$cargo_audit_version" = "cargo-audit 0.18.3" ] 23 | -------------------------------------------------------------------------------- /e2e-tests/tls.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | unset CARGO_INSTALL_ROOT 6 | 7 | CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') 8 | export CARGO_HOME 9 | export PATH="$CARGO_HOME/bin:$PATH" 10 | 11 | "./$1" binstall \ 12 | --force \ 13 | --min-tls-version "${2:-1.3}" \ 14 | --no-confirm \ 15 | cargo-binstall@0.20.1 16 | # Test that the installed binaries can be run 17 | cargo binstall --help >/dev/null 18 | -------------------------------------------------------------------------------- /e2e-tests/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | unset CARGO_INSTALL_ROOT 6 | 7 | CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') 8 | export CARGO_HOME 9 | othertmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-test') 10 | export PATH="$CARGO_HOME/bin:$othertmpdir/bin:$PATH" 11 | 12 | mkdir -p "$othertmpdir/bin" 13 | # Copy it to bin to test use of env var `CARGO` 14 | cp "./$1" "$othertmpdir/bin/" 15 | 16 | 17 | cargo binstall --no-confirm cargo-watch@8.4.0 18 | cargo uninstall cargo-watch 19 | -------------------------------------------------------------------------------- /e2e-tests/upgrade.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | unset CARGO_INSTALL_ROOT 6 | 7 | CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') 8 | export CARGO_HOME 9 | export PATH="$CARGO_HOME/bin:$PATH" 10 | 11 | # Test skip when installed 12 | "./$1" binstall --no-confirm --force cargo-binstall@0.11.1 13 | "./$1" binstall --log-level=info --no-confirm cargo-binstall@0.11.1 | grep -q 'cargo-binstall v0.11.1 is already installed' 14 | 15 | ## Test When 0.11.0 is installed but can be upgraded. 16 | "./$1" binstall --no-confirm cargo-binstall@0.12.0 17 | "./$1" binstall --log-level=info --no-confirm cargo-binstall@0.12.0 | grep -q 'cargo-binstall v0.12.0 is already installed' 18 | "./$1" binstall --log-level=info --no-confirm cargo-binstall@^0.12.0 | grep -q -v 'cargo-binstall v0.12.0 is already installed' 19 | -------------------------------------------------------------------------------- /e2e-tests/version-syntax.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | unset CARGO_INSTALL_ROOT 6 | 7 | CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') 8 | export CARGO_HOME 9 | export PATH="$CARGO_HOME/bin:$PATH" 10 | 11 | # Test --version 12 | "./$1" binstall --force --no-confirm --version 0.11.1 cargo-binstall 13 | # Test that the installed binaries can be run 14 | cargo binstall --help >/dev/null 15 | 16 | # Test "$crate_name@$version" 17 | "./$1" binstall --force --no-confirm cargo-binstall@0.11.1 18 | # Test that the installed binaries can be run 19 | cargo binstall --help >/dev/null 20 | -------------------------------------------------------------------------------- /install-from-binstall-release.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | Set-PSDebug -Trace 1 3 | $tmpdir = $Env:TEMP 4 | $BINSTALL_VERSION = $Env:BINSTALL_VERSION 5 | if ($BINSTALL_VERSION -and $BINSTALL_VERSION -notlike 'v*') { 6 | # prefix version with v 7 | $BINSTALL_VERSION = "v$BINSTALL_VERSION" 8 | } 9 | # Fetch binaries from `[..]/releases/latest/download/[..]` if _no_ version is 10 | # given, otherwise from `[..]/releases/download/VERSION/[..]`. Note the shifted 11 | # location of '/download'. 12 | $base_url = if (-not $BINSTALL_VERSION) { 13 | "https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-" 14 | } else { 15 | "https://github.com/cargo-bins/cargo-binstall/releases/download/$BINSTALL_VERSION/cargo-binstall-" 16 | } 17 | 18 | $proc_arch = [Environment]::GetEnvironmentVariable("PROCESSOR_ARCHITECTURE", [EnvironmentVariableTarget]::Machine) 19 | if ($proc_arch -eq "AMD64") { 20 | $arch = "x86_64" 21 | } elseif ($proc_arch -eq "ARM64") { 22 | $arch = "aarch64" 23 | } else { 24 | Write-Host "Unsupported Architecture: $type" -ForegroundColor Red 25 | [Environment]::Exit(1) 26 | } 27 | $url = "$base_url$arch-pc-windows-msvc.zip" 28 | Invoke-WebRequest $url -OutFile $tmpdir\cargo-binstall.zip 29 | Expand-Archive -Force $tmpdir\cargo-binstall.zip $tmpdir\cargo-binstall 30 | Write-Host "" 31 | 32 | $ps = Start-Process -PassThru -Wait "$tmpdir\cargo-binstall\cargo-binstall.exe" "--self-install" 33 | if ($ps.ExitCode -ne 0) { 34 | Invoke-Expression "$tmpdir\cargo-binstall\cargo-binstall.exe -y --force cargo-binstall" 35 | } 36 | 37 | Remove-Item -Force $tmpdir\cargo-binstall.zip 38 | Remove-Item -Recurse -Force $tmpdir\cargo-binstall 39 | $cargo_home = if ($Env:CARGO_HOME -ne $null) { $Env:CARGO_HOME } else { "$HOME\.cargo" } 40 | if ($Env:Path -split ";" -notcontains "$cargo_home\bin") { 41 | if (($Env:CI -ne $null) -and ($Env:GITHUB_PATH -ne $null)) { 42 | Add-Content -Path "$Env:GITHUB_PATH" -Value "$cargo_home\bin" 43 | } else { 44 | Write-Host "" 45 | Write-Host "Your path is missing $cargo_home\bin, you might want to add it." -ForegroundColor Red 46 | Write-Host "" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /install-from-binstall-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eux 4 | 5 | do_curl() { 6 | 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 "$@" 7 | } 8 | 9 | # Set pipefail if it works in a subshell, disregard if unsupported 10 | # shellcheck disable=SC3040 11 | (set -o pipefail 2> /dev/null) && set -o pipefail 12 | 13 | case "${BINSTALL_VERSION:-}" in 14 | "") ;; # unset 15 | v*) ;; # already includes the `v` 16 | *) BINSTALL_VERSION="v$BINSTALL_VERSION" ;; # Add a leading `v` 17 | esac 18 | 19 | cd "$(mktemp -d)" 20 | 21 | # Fetch binaries from `[..]/releases/latest/download/[..]` if _no_ version is 22 | # given, otherwise from `[..]/releases/download/VERSION/[..]`. Note the shifted 23 | # location of '/download'. 24 | if [ -z "${BINSTALL_VERSION:-}" ]; then 25 | base_url="https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-" 26 | else 27 | base_url="https://github.com/cargo-bins/cargo-binstall/releases/download/${BINSTALL_VERSION}/cargo-binstall-" 28 | fi 29 | 30 | os="$(uname -s)" 31 | if [ "$os" = "Darwin" ]; then 32 | url="${base_url}universal-apple-darwin.zip" 33 | do_curl -O "$url" 34 | unzip cargo-binstall-universal-apple-darwin.zip 35 | elif [ "$os" = "Linux" ]; then 36 | machine="$(uname -m)" 37 | if [ "$machine" = "armv7l" ]; then 38 | machine="armv7" 39 | fi 40 | target="${machine}-unknown-linux-musl" 41 | if [ "$machine" = "armv7" ]; then 42 | target="${target}eabihf" 43 | fi 44 | 45 | url="${base_url}${target}.tgz" 46 | do_curl "$url" | tar -xvzf - 47 | elif [ "${OS-}" = "Windows_NT" ]; then 48 | machine="$(uname -m)" 49 | target="${machine}-pc-windows-msvc" 50 | url="${base_url}${target}.zip" 51 | do_curl -O "$url" 52 | unzip "cargo-binstall-${target}.zip" 53 | else 54 | echo "Unsupported OS ${os}" 55 | exit 1 56 | fi 57 | 58 | ./cargo-binstall --self-install || ./cargo-binstall -y --force cargo-binstall 59 | 60 | CARGO_HOME="${CARGO_HOME:-$HOME/.cargo}" 61 | 62 | case ":$PATH:" in 63 | *":$CARGO_HOME/bin:"*) ;; # Cargo home is already in path 64 | *) needs_cargo_home=1 ;; 65 | esac 66 | 67 | if [ -n "${needs_cargo_home:-}" ]; then 68 | if [ -n "${CI:-}" ] && [ -n "${GITHUB_PATH:-}" ]; then 69 | echo "$CARGO_HOME/bin" >> "$GITHUB_PATH" 70 | else 71 | echo 72 | printf "\033[0;31mYour path is missing %s, you might want to add it.\033[0m\n" "$CARGO_HOME/bin" 73 | echo 74 | fi 75 | fi 76 | -------------------------------------------------------------------------------- /release-plz.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | git_release_latest = false # don't set release as latest release 3 | 4 | [[package]] 5 | name = "cargo-binstall" 6 | release = false # don't process this package 7 | 8 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | profile = "minimal" 4 | components = ["rustfmt", "clippy"] 5 | -------------------------------------------------------------------------------- /zigbuild-requirements.txt: -------------------------------------------------------------------------------- 1 | ###### Requirements without Version Specifiers ###### 2 | cargo-zigbuild 3 | 4 | ###### Requirements with Version Specifiers ###### 5 | ziglang 6 | --------------------------------------------------------------------------------