├── .editorconfig ├── .github ├── actions │ └── cronjob-setup │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── build-package.yml │ ├── cronjob.yml │ ├── deploy-stats-server.yml │ ├── release-plz.yml │ ├── selfbuild.yml │ └── test.yml ├── .gitignore ├── .python-version ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── Makefile ├── README.md ├── TODO.md ├── Vagrantfile ├── add-exclude.sh ├── build-version.sh ├── cargo-quickinstall ├── CHANGELOG.md ├── Cargo.toml ├── build.rs ├── manifest.rc ├── src │ ├── args.rs │ ├── command_ext.rs │ ├── install_error.rs │ ├── json_value_ext.rs │ ├── lib.rs │ ├── main.rs │ └── utils.rs ├── tests │ └── integration_tests.rs └── windows.manifest ├── create_and_upload_tag.sh ├── cronjob_scripts ├── .python-version ├── README.md ├── __init__.py ├── architectures.py ├── checkout_worktree.py ├── crates_io_popular_crates.py ├── find-common-failures.py ├── get_latest_version.py ├── stats.py ├── tests │ ├── __init__.py │ └── test_architectures.py ├── trigger_package_build.py └── types.py ├── get-popular-crates.sh ├── get-repo.sh ├── minisign.pub ├── pkg-config-cross.sh ├── pyproject.toml ├── rebuild-popular-crates.sh ├── requirements.txt ├── stats-server ├── .dockerignore ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── README.md ├── fly.toml └── src │ └── main.rs ├── supported-targets └── zigbuild-requirements.txt /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.sh] 2 | indent_style = space 3 | indent_size = 4 4 | -------------------------------------------------------------------------------- /.github/actions/cronjob-setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup env for cronjob and cache 2 | 3 | runs: 4 | using: composite 5 | steps: 6 | - uses: actions/setup-python@v5 7 | with: 8 | python-version-file: "cronjob_scripts/.python-version" 9 | cache: "pip" 10 | cache-dependency-path: "**/requirements.txt" 11 | 12 | - name: Set current date as env variable 13 | run: echo "NOW=$(date +'%Y-%m-%d')" >> $GITHUB_ENV 14 | shell: bash 15 | 16 | - name: Cache crates-io popular crates 17 | uses: actions/cache@v4 18 | with: 19 | path: cached_crates_io_popular_crates.parquet 20 | key: ${{ env.NOW }}-cached_crates_io_popular_crates.parquet 21 | -------------------------------------------------------------------------------- /.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 | # This will update the Cargo.lock 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | groups: 17 | deps: 18 | patterns: 19 | - "*" 20 | - package-ecosystem: "cargo" 21 | # This will update the cargo-quickinstall/Cargo.toml 22 | directory: "/cargo-quickinstall" 23 | schedule: 24 | interval: "daily" 25 | groups: 26 | deps: 27 | patterns: 28 | - "*" 29 | - package-ecosystem: "cargo" 30 | # This will update the stats-server/Cargo.toml 31 | directory: "/stats-server" 32 | schedule: 33 | interval: "daily" 34 | groups: 35 | deps: 36 | patterns: 37 | - "*" 38 | - package-ecosystem: "pip" 39 | # This will update cronjob_scripts/requirements.txt 40 | directory: "/cronjob_scripts" 41 | schedule: 42 | interval: "daily" 43 | groups: 44 | deps: 45 | patterns: 46 | - "*" 47 | -------------------------------------------------------------------------------- /.github/workflows/build-package.yml: -------------------------------------------------------------------------------- 1 | name: Build package 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | crate: 7 | description: Crate to release 8 | required: true 9 | type: string 10 | version: 11 | description: Version to release 12 | required: true 13 | type: string 14 | target_arch: 15 | description: Target to build against 16 | required: true 17 | type: string 18 | build_os: 19 | description: The OS to use for building 20 | required: true 21 | type: string 22 | branch: 23 | description: The branch which is checked out 24 | required: true 25 | type: string 26 | features: 27 | description: Features to enable 28 | default: "" 29 | type: string 30 | no_default_features: 31 | description: Disable default features 32 | default: "" 33 | type: string 34 | always_build: 35 | description: Always build even if it's already present, set to 1 to enable this 36 | default: "" 37 | type: string 38 | skip_upload: 39 | description: Skip upload 40 | default: "" 41 | type: string 42 | workflow_dispatch: 43 | inputs: 44 | crate: 45 | description: Crate to release 46 | required: true 47 | type: string 48 | version: 49 | description: Version to release 50 | required: true 51 | type: string 52 | target_arch: 53 | description: Target to build against 54 | required: true 55 | type: string 56 | build_os: 57 | description: The OS to use for building 58 | required: true 59 | type: string 60 | branch: 61 | description: The branch which is checked out 62 | required: true 63 | type: string 64 | features: 65 | description: Features to enable 66 | default: "" 67 | type: string 68 | no_default_features: 69 | description: Disable default features 70 | default: "" 71 | type: string 72 | always_build: 73 | description: Always build even if it's already present, set to 1 to enable this 74 | default: "" 75 | type: string 76 | skip_upload: 77 | description: Skip upload 78 | default: "" 79 | type: string 80 | 81 | concurrency: 82 | group: build-package-${{ github.event.inputs.crate }}-${{ github.event.inputs.version }}-${{ github.event.inputs.target_arch }} 83 | 84 | env: 85 | CRATE: ${{ inputs.crate }} 86 | TARGET_ARCH: ${{ inputs.target_arch }} 87 | VERSION: ${{ inputs.version }} 88 | FEATURES: ${{ inputs.features }} 89 | NO_DEFAULT_FEATURES: ${{ inputs.NO_DEFAULT_FEATURES }} 90 | BUILD_DIR: ${{ github.workspace }}/built.d 91 | 92 | jobs: 93 | build-popular-package: 94 | name: build-popular-package-${{ inputs.crate }}-${{ inputs.version }}-${{ inputs.target_arch }}-${{ inputs.features }} 95 | runs-on: ${{ inputs.build_os }} 96 | permissions: {} 97 | defaults: 98 | run: 99 | shell: bash 100 | steps: 101 | - name: Checkout trigger commit 102 | uses: actions/checkout@v4 103 | with: 104 | persist-credentials: false 105 | path: trigger 106 | - name: Checkout main repo 107 | uses: actions/checkout@v4 108 | with: 109 | persist-credentials: false 110 | ref: ${{ inputs.branch }} 111 | path: cargo-quickinstall 112 | 113 | - name: Print inputs 114 | run: | 115 | echo $CRATE 116 | echo $VERSION 117 | echo $TARGET_ARCH 118 | echo $FEATURES 119 | 120 | - name: Install latest rust 121 | run: rustup toolchain install stable --no-self-update --profile minimal 122 | 123 | - name: build package 124 | env: 125 | ALWAYS_BUILD: ${{ inputs.always_build }} 126 | TEMPDIR: ${{ env.BUILD_DIR }} 127 | run: | 128 | set -euxo pipefail 129 | # `tar` does not understand mixed forward and backslashes, but mkdir does. 130 | # Try coercing it into a single style? 131 | mkdir -p "$TEMPDIR" 132 | pushd "$TEMPDIR" 133 | TEMPDIR="$PWD" 134 | popd 135 | 136 | cargo-quickinstall/build-version.sh "$CRATE" 137 | touch "$TEMPDIR/dummy-file" 138 | ls "$TEMPDIR" 139 | # At this point, I don't think that you can really trust anything on the system anymore. 140 | # I'm not sure whether the js actions runtime is also affected by this. 141 | # TODO: try breaking things so that uploads don't work. 142 | 143 | - name: Upload run-local binary artifact 144 | uses: actions/upload-artifact@v4 145 | with: 146 | name: built-${{ inputs.build_os }} 147 | path: ${{ env.BUILD_DIR }} 148 | if-no-files-found: error 149 | 150 | upload-popular-package: 151 | name: Upload 152 | needs: build-popular-package 153 | runs-on: ubuntu-22.04 154 | defaults: 155 | run: 156 | shell: bash 157 | env: 158 | BUILD_ARCHIVE: ${{ github.workspace }}/built.d/${{ inputs.crate }}-${{ inputs.version }}-${{ inputs.target_arch }}.tar.gz 159 | SIG_FILENAME: ${{ github.workspace }}/built.d/${{ inputs.crate }}-${{ inputs.version }}-${{ inputs.target_arch }}.tar.gz.sig 160 | steps: 161 | - name: Checkout trigger commit 162 | uses: actions/checkout@v4 163 | with: 164 | ssh-key: ${{ secrets.CRONJOB_DEPLOY_KEY }} 165 | persist-credentials: true 166 | path: trigger 167 | - name: Checkout main repo 168 | uses: actions/checkout@v4 169 | with: 170 | persist-credentials: false 171 | # TODO: maybe this should be main or configurable or something? 172 | ref: ${{ inputs.branch }} 173 | path: cargo-quickinstall 174 | 175 | - name: Download binary artifact 176 | uses: actions/download-artifact@v4 177 | with: 178 | name: built-${{ inputs.build_os }} 179 | # TODO: check that we it can't write anywhere other than built.d 180 | path: ${{ env.BUILD_DIR }} 181 | 182 | - name: Check if tarball exists 183 | id: check_files 184 | uses: andstor/file-existence-action@v3 185 | with: 186 | files: ${{ env.BUILD_ARCHIVE }} 187 | 188 | - name: Cancel if no tarball 189 | if: steps.check_files.outputs.files_exists == 'false' 190 | uses: andymckay/cancel-action@0.5 191 | 192 | - name: Wait for cancellation signal if no tarball 193 | if: steps.check_files.outputs.files_exists == 'false' 194 | run: | 195 | sleep 1m 196 | exit 1 197 | 198 | - name: Install minisign 199 | run: | 200 | set -euxo pipefail 201 | cd /tmp 202 | curl -L "$URL" | tar -xz 203 | sudo mv minisign-linux/x86_64/minisign /usr/local/bin 204 | env: 205 | URL: https://github.com/jedisct1/minisign/releases/download/0.11/minisign-0.11-linux.tar.gz 206 | 207 | - name: Sign the tarball 208 | run: | 209 | echo "$MINISIGN_PRIVATE_KEY" > /tmp/minisign-key 210 | minisign -S -s /tmp/minisign-key -x $SIG_FILENAME -m $BUILD_ARCHIVE 211 | env: 212 | MINISIGN_PRIVATE_KEY: ${{ secrets.MINISIGN_PRIVATE_KEY }} 213 | 214 | - name: Tag release 215 | if: "! inputs.skip_upload" 216 | run: | 217 | ( 218 | set -euxo pipefail 219 | cd trigger 220 | if ! ../cargo-quickinstall/create_and_upload_tag.sh $CRATE-$VERSION; then 221 | echo "$CRATE-$VERSION tag already exists" 222 | fi 223 | ) 224 | 225 | - name: Releasing assets in new release format 226 | if: "! inputs.skip_upload" 227 | uses: svenstaro/upload-release-action@v2 228 | with: 229 | repo_token: ${{ secrets.GITHUB_TOKEN }} 230 | tag: ${{ inputs.crate }}-${{ inputs.version }} 231 | release_name: ${{ inputs.crate }}-${{ inputs.version }} 232 | body: build ${{ inputs.crate }}@${{ inputs.version }} 233 | file: ${{ env.BUILD_ARCHIVE }} 234 | 235 | - name: Releasing assets signature in new release format 236 | if: "! inputs.skip_upload" 237 | uses: svenstaro/upload-release-action@v2 238 | with: 239 | repo_token: ${{ secrets.GITHUB_TOKEN }} 240 | tag: ${{ inputs.crate }}-${{ inputs.version }} 241 | release_name: ${{ inputs.crate }}-${{ inputs.version }} 242 | body: build ${{ inputs.crate }}@${{ inputs.version }} 243 | file: ${{ env.SIG_FILENAME }} 244 | 245 | - name: Upload signature 246 | uses: actions/upload-artifact@v4 247 | with: 248 | name: signature 249 | path: ${{ env.SIG_FILENAME }} 250 | if-no-files-found: error 251 | 252 | build-popular-package-failure: 253 | needs: 254 | - build-popular-package 255 | if: ${{ failure() }} 256 | runs-on: ubuntu-latest 257 | defaults: 258 | run: 259 | shell: bash 260 | steps: 261 | - name: Checkout trigger branch 262 | uses: actions/checkout@v4 263 | with: 264 | ssh-key: ${{ secrets.CRONJOB_DEPLOY_KEY }} 265 | persist-credentials: true 266 | ref: trigger/${{ inputs.target_arch }} 267 | path: trigger 268 | - name: Checkout trigger commit 269 | uses: actions/checkout@v4 270 | with: 271 | persist-credentials: false 272 | path: cargo-quickinstall 273 | - name: Record failure 274 | run: ${{ github.workspace }}/cargo-quickinstall/add-exclude.sh ${{ github.workspace }}/trigger 275 | -------------------------------------------------------------------------------- /.github/workflows/cronjob.yml: -------------------------------------------------------------------------------- 1 | name: Cronjob 2 | 3 | concurrency: 4 | group: trigger-package-build 5 | 6 | on: 7 | # Run every hour on the hour. 8 | schedule: 9 | - cron: "0 * * * *" 10 | # If you are an upstream maintainer and you want to test your branch before merging, run 11 | # 12 | # git push 13 | # gh workflow run cronjob.yml --ref `git rev-parse --abbrev-ref HEAD` 14 | # gh run list --workflow=cronjob.yml -L1 --json url 15 | # 16 | # This may also work on forks, but you might need to set up some repo secrets first, 17 | # and I've not tested it. 18 | workflow_dispatch: 19 | 20 | jobs: 21 | build-popular-package: 22 | name: Build 23 | # This is because I want to use a vaguely modern python without having to install it. 24 | # Other actions will may use an older version of ubuntu in order to link an older glibc, 25 | # but this action won't be doing any building so it's fine. 26 | runs-on: ubuntu-24.04 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | ssh-key: ${{ secrets.CRONJOB_DEPLOY_KEY }} 31 | persist-credentials: true 32 | 33 | - uses: ./.github/actions/cronjob-setup 34 | 35 | - name: Trigger Package Build 36 | id: find_crate 37 | run: make trigger-all 38 | env: 39 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | INFLUXDB_TOKEN: ${{ secrets.INFLUXDB_TOKEN }} 41 | CRATE_CHECK_LIMIT: 10 42 | PYTHONUNBUFFERED: 1 43 | -------------------------------------------------------------------------------- /.github/workflows/deploy-stats-server.yml: -------------------------------------------------------------------------------- 1 | name: Fly Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | deploy: 8 | name: Deploy app 9 | environment: stats server production 10 | runs-on: ubuntu-latest 11 | concurrency: deploy-group 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: superfly/flyctl-actions/setup-flyctl@master 15 | - run: flyctl deploy stats-server --wait-timeout 1m 16 | env: 17 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | actions: write 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | release-plz: 15 | name: Release-plz 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - name: Install Rust toolchain 23 | uses: dtolnay/rust-toolchain@stable 24 | - name: Run release-plz 25 | uses: MarcoIeni/release-plz-action@v0.5 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 29 | 30 | - uses: ./.github/actions/cronjob-setup 31 | - name: Trigger tarball builds of cargo-quickinstall 32 | run: make recheck-self-only 33 | env: 34 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/selfbuild.yml: -------------------------------------------------------------------------------- 1 | name: Self-Build 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | self-build-with-build-version: 8 | name: Self-Build-With-build-version 9 | runs-on: ${{ matrix.os }} 10 | defaults: 11 | run: 12 | shell: bash 13 | strategy: 14 | matrix: 15 | include: 16 | - target_arch: x86_64-pc-windows-msvc 17 | os: windows-latest 18 | - target_arch: aarch64-pc-windows-msvc 19 | os: windows-latest 20 | - target_arch: x86_64-apple-darwin 21 | os: macos-latest 22 | - target_arch: aarch64-apple-darwin 23 | os: macos-latest 24 | - target_arch: x86_64-unknown-linux-gnu 25 | os: ubuntu-20.04 26 | - target_arch: x86_64-unknown-linux-musl 27 | os: ubuntu-20.04 28 | - target_arch: aarch64-unknown-linux-gnu 29 | os: ubuntu-20.04 30 | - target_arch: aarch64-unknown-linux-musl 31 | os: ubuntu-20.04 32 | - target_arch: armv7-unknown-linux-gnueabihf 33 | os: ubuntu-20.04 34 | - target_arch: armv7-unknown-linux-musleabihf 35 | os: ubuntu-20.04 36 | steps: 37 | - uses: actions/checkout@v4 38 | with: 39 | persist-credentials: false 40 | 41 | - name: Install latest rust 42 | run: rustup toolchain install stable --no-self-update --profile minimal 43 | 44 | - name: Build Thyself 45 | env: 46 | TARGET_ARCH: ${{ matrix.target_arch }} 47 | run: | 48 | set -euxo pipefail 49 | 50 | VERSION="$( 51 | curl \ 52 | --user-agent "cargo-quickinstall build pipeline (alsuren@gmail.com)" \ 53 | --fail "https://crates.io/api/v1/crates/cargo-quickinstall" \ 54 | | jq -r '.crate|.max_stable_version' 55 | )" 56 | VERSION="$VERSION" ./build-version.sh cargo-quickinstall 57 | ALWAYS_BUILD=1 VERSION="$VERSION" ./build-version.sh cargo-quickinstall 58 | 59 | self-build: 60 | name: Self-Build 61 | runs-on: ${{ matrix.os }} 62 | defaults: 63 | run: 64 | shell: bash 65 | strategy: 66 | matrix: 67 | include: 68 | - target_arch: x86_64-pc-windows-msvc 69 | os: windows-latest 70 | - target_arch: x86_64-apple-darwin 71 | os: macos-latest 72 | - target_arch: aarch64-apple-darwin 73 | os: macos-latest 74 | - target_arch: x86_64-unknown-linux-gnu 75 | os: ubuntu-20.04 76 | - target_arch: x86_64-unknown-linux-musl 77 | os: ubuntu-20.04 78 | - target_arch: aarch64-unknown-linux-gnu 79 | os: ubuntu-20.04 80 | steps: 81 | - uses: actions/checkout@v4 82 | with: 83 | persist-credentials: false 84 | - name: Install latest rust 85 | run: rustup toolchain install stable --no-self-update --profile minimal 86 | - name: Install Thyself 87 | run: cargo install --path cargo-quickinstall 88 | - name: Install Thyself with Thyself (or fallback to sensei on windows) 89 | run: cargo quickinstall cargo-quickinstall || cargo quickinstall sensei 90 | env: 91 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 92 | 93 | build-needing-cc: 94 | name: Build package that needs CC 95 | runs-on: ${{ matrix.os }} 96 | defaults: 97 | run: 98 | shell: bash 99 | strategy: 100 | matrix: 101 | include: 102 | - target_arch: x86_64-pc-windows-msvc 103 | os: windows-latest 104 | - target_arch: armv7-unknown-linux-gnueabihf 105 | os: ubuntu-20.04 106 | - target_arch: armv7-unknown-linux-musleabihf 107 | os: ubuntu-20.04 108 | - target_arch: x86_64-apple-darwin 109 | os: macos-latest 110 | - target_arch: aarch64-apple-darwin 111 | os: macos-latest 112 | - target_arch: x86_64-unknown-linux-gnu 113 | os: ubuntu-20.04 114 | - target_arch: x86_64-unknown-linux-musl 115 | os: ubuntu-20.04 116 | - target_arch: aarch64-unknown-linux-gnu 117 | os: ubuntu-20.04 118 | - target_arch: aarch64-unknown-linux-musl 119 | os: ubuntu-20.04 120 | steps: 121 | - uses: actions/checkout@v4 122 | with: 123 | persist-credentials: false 124 | 125 | - name: Install latest rust 126 | run: rustup toolchain install stable --no-self-update --profile minimal 127 | 128 | # FIXME: find a package that needs a working CC but doesn't take quite so long to build. 129 | - name: Build cargo-deny 130 | env: 131 | TARGET_ARCH: ${{ matrix.target_arch }} 132 | run: | 133 | set -euxo pipefail 134 | touch .env 135 | VERSION="$( 136 | curl \ 137 | --user-agent "cargo-quickinstall build pipeline (alsuren@gmail.com)" \ 138 | --fail "https://crates.io/api/v1/crates/cargo-deny" \ 139 | | jq -r '.crate|.max_stable_version' 140 | )" 141 | ALWAYS_BUILD=1 VERSION="$VERSION" TARGET_ARCH="$TARGET_ARCH" ./build-version.sh cargo-deny 142 | 143 | build-package-test: 144 | name: Build package test 145 | uses: ./.github/workflows/build-package.yml 146 | secrets: inherit 147 | with: 148 | crate: cargo-quickinstall 149 | version: 0.2.9 150 | target_arch: x86_64-unknown-linux-gnu 151 | build_os: ubuntu-latest 152 | branch: ${{ github.head_ref }} 153 | always_build: 1 154 | skip_upload: true 155 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Coding format checker and test runner. 2 | name: Automated tests 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | 10 | jobs: 11 | lint: 12 | name: Linter 13 | runs-on: ubuntu-latest 14 | defaults: 15 | run: 16 | shell: bash 17 | 18 | steps: 19 | - name: Fetch source 20 | uses: actions/checkout@v4 21 | 22 | - name: Run shellcheck 23 | run: shellcheck *.sh 24 | 25 | - name: Install shfmt 26 | run: go install mvdan.cc/sh/v3/cmd/shfmt@latest 27 | 28 | - name: Run shfmt 29 | run: ~/go/bin/shfmt --diff *.sh 30 | 31 | - name: Install latest rust 32 | run: rustup toolchain install stable --no-self-update --profile minimal --component clippy,rustfmt 33 | 34 | - name: Lint 35 | run: cargo fmt --check 36 | 37 | - name: Clippy 38 | run: cargo clippy --no-deps 39 | 40 | test: 41 | name: Test runner 42 | runs-on: ubuntu-latest 43 | defaults: 44 | run: 45 | shell: bash 46 | 47 | steps: 48 | # TODO: actions/cache@v2? 49 | - name: Fetch source 50 | uses: actions/checkout@v4 51 | 52 | - name: Fetch tags required for testing 53 | run: git fetch --tags 54 | 55 | - name: Run cargo clippy for client project 56 | run: cargo clippy --all-targets --all-features -- -D warnings 57 | 58 | - name: Run cargo clippy for stats-server project 59 | run: cargo clippy --all-targets --all-features -- -D warnings 60 | working-directory: stats-server 61 | 62 | - name: Run cargo test 63 | run: cargo test 64 | 65 | - uses: ./.github/actions/cronjob-setup 66 | 67 | - name: Test python cronjob_scripts 68 | run: make test-cronjob-scripts 69 | env: 70 | INFLUXDB_TOKEN: ${{ secrets.INFLUXDB_TOKEN }} 71 | 72 | e2etest: 73 | runs-on: ${{ matrix.os }} 74 | strategy: 75 | matrix: 76 | include: 77 | - os: windows-latest 78 | - os: macos-latest 79 | - os: ubuntu-latest 80 | defaults: 81 | run: 82 | shell: bash 83 | 84 | steps: 85 | - name: Fetch source 86 | uses: actions/checkout@v4 87 | 88 | - name: Install latest rust 89 | run: rustup toolchain install stable --no-self-update --profile minimal 90 | 91 | - name: Test dry-run 92 | run: cargo run -- --dry-run cargo-quickinstall 93 | 94 | - name: Test installing and using binstall 95 | run: | 96 | set -euxo pipefail 97 | cargo run -- cargo-quickinstall 98 | cargo quickinstall -V 99 | env: 100 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 101 | 102 | - name: Test using binstall with it already installed 103 | run: | 104 | set -euxo pipefail 105 | cargo run -- cargo-quickinstall 106 | cargo quickinstall -V 107 | 108 | - name: Test batch installation with binstall 109 | run: | 110 | set -euxo pipefail 111 | cargo run -- cargo-quickinstall cargo-nextest@0.9.50 112 | env: 113 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 114 | 115 | - name: Try running the binary 116 | run: | 117 | cargo quickinstall -V 118 | cargo nextest -V 119 | 120 | - name: Test batch installation with binstall with force 121 | run: | 122 | set -euxo pipefail 123 | echo Rm all binaries installed but do not update manifests, 124 | echo so that binstall will think they are installed. 125 | rm $(which cargo-quickinstall) $(which cargo-nextest@0.9.50) 126 | cargo run -- --force cargo-quickinstall cargo-nextest@0.9.50 127 | env: 128 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 129 | 130 | - name: Try running the binary 131 | run: | 132 | cargo quickinstall -V 133 | cargo nextest -V 134 | - name: Test batch installation with curl 135 | run: | 136 | set -euxo pipefail 137 | cargo run -- --no-binstall cargo-quickinstall cargo-nextest@0.9.50 138 | cargo quickinstall -V 139 | cargo nextest -V 140 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env 3 | .idea 4 | .vagrant 5 | \.DS_Store 6 | __pycache__ 7 | # For now, we rely on scripts/requirements.txt to act as our lockfile, because dependabot doesn't support uv yet. 8 | cronjob_scripts/uv.lock 9 | cronjob_scripts/.python-deps-updated.timestamp 10 | cached_crates_io_popular_crates.parquet 11 | cronjob-scripts/*.egg-info 12 | cronjob-scripts/build 13 | uv.lock 14 | cronjob_scripts.egg-info/ 15 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "bitflags" 7 | version = "2.6.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 10 | 11 | [[package]] 12 | name = "cargo-quickinstall" 13 | version = "0.3.15" 14 | dependencies = [ 15 | "embed-resource", 16 | "guess_host_triple", 17 | "home", 18 | "mktemp", 19 | "pico-args", 20 | "tempfile", 21 | "tinyjson", 22 | ] 23 | 24 | [[package]] 25 | name = "cc" 26 | version = "1.1.8" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549" 29 | 30 | [[package]] 31 | name = "cfg-if" 32 | version = "1.0.0" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 35 | 36 | [[package]] 37 | name = "embed-resource" 38 | version = "3.0.3" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "e8fe7d068ca6b3a5782ca5ec9afc244acd99dd441e4686a83b1c3973aba1d489" 41 | dependencies = [ 42 | "cc", 43 | "memchr", 44 | "rustc_version", 45 | "toml", 46 | "vswhom", 47 | "winreg", 48 | ] 49 | 50 | [[package]] 51 | name = "equivalent" 52 | version = "1.0.1" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 55 | 56 | [[package]] 57 | name = "errno" 58 | version = "0.2.8" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 61 | dependencies = [ 62 | "errno-dragonfly", 63 | "libc", 64 | "winapi", 65 | ] 66 | 67 | [[package]] 68 | name = "errno" 69 | version = "0.3.10" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 72 | dependencies = [ 73 | "libc", 74 | "windows-sys", 75 | ] 76 | 77 | [[package]] 78 | name = "errno-dragonfly" 79 | version = "0.1.2" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 82 | dependencies = [ 83 | "cc", 84 | "libc", 85 | ] 86 | 87 | [[package]] 88 | name = "fastrand" 89 | version = "2.1.1" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" 92 | 93 | [[package]] 94 | name = "getrandom" 95 | version = "0.2.15" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 98 | dependencies = [ 99 | "cfg-if", 100 | "libc", 101 | "wasi 0.11.0+wasi-snapshot-preview1", 102 | ] 103 | 104 | [[package]] 105 | name = "getrandom" 106 | version = "0.3.1" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" 109 | dependencies = [ 110 | "cfg-if", 111 | "libc", 112 | "wasi 0.13.3+wasi-0.2.2", 113 | "windows-targets", 114 | ] 115 | 116 | [[package]] 117 | name = "guess_host_triple" 118 | version = "0.1.4" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "5dd62763349a2c83ed2ce9ce5429158085fa43e0cdd8c60011a7843765d04e18" 121 | dependencies = [ 122 | "errno 0.2.8", 123 | "libc", 124 | "log", 125 | "winapi", 126 | ] 127 | 128 | [[package]] 129 | name = "hashbrown" 130 | version = "0.14.5" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 133 | 134 | [[package]] 135 | name = "home" 136 | version = "0.5.11" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 139 | dependencies = [ 140 | "windows-sys", 141 | ] 142 | 143 | [[package]] 144 | name = "indexmap" 145 | version = "2.3.0" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" 148 | dependencies = [ 149 | "equivalent", 150 | "hashbrown", 151 | ] 152 | 153 | [[package]] 154 | name = "libc" 155 | version = "0.2.170" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" 158 | 159 | [[package]] 160 | name = "linux-raw-sys" 161 | version = "0.9.2" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" 164 | 165 | [[package]] 166 | name = "log" 167 | version = "0.4.22" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 170 | 171 | [[package]] 172 | name = "memchr" 173 | version = "2.7.4" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 176 | 177 | [[package]] 178 | name = "mktemp" 179 | version = "0.5.1" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "69fed8fbcd01affec44ac226784c6476a6006d98d13e33bc0ca7977aaf046bd8" 182 | dependencies = [ 183 | "uuid", 184 | ] 185 | 186 | [[package]] 187 | name = "once_cell" 188 | version = "1.19.0" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 191 | 192 | [[package]] 193 | name = "pico-args" 194 | version = "0.5.0" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" 197 | 198 | [[package]] 199 | name = "proc-macro2" 200 | version = "1.0.86" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 203 | dependencies = [ 204 | "unicode-ident", 205 | ] 206 | 207 | [[package]] 208 | name = "quote" 209 | version = "1.0.36" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 212 | dependencies = [ 213 | "proc-macro2", 214 | ] 215 | 216 | [[package]] 217 | name = "rustc_version" 218 | version = "0.4.0" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 221 | dependencies = [ 222 | "semver", 223 | ] 224 | 225 | [[package]] 226 | name = "rustix" 227 | version = "1.0.0" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "17f8dcd64f141950290e45c99f7710ede1b600297c91818bb30b3667c0f45dc0" 230 | dependencies = [ 231 | "bitflags", 232 | "errno 0.3.10", 233 | "libc", 234 | "linux-raw-sys", 235 | "windows-sys", 236 | ] 237 | 238 | [[package]] 239 | name = "semver" 240 | version = "1.0.23" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 243 | 244 | [[package]] 245 | name = "serde" 246 | version = "1.0.205" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150" 249 | dependencies = [ 250 | "serde_derive", 251 | ] 252 | 253 | [[package]] 254 | name = "serde_derive" 255 | version = "1.0.205" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1" 258 | dependencies = [ 259 | "proc-macro2", 260 | "quote", 261 | "syn", 262 | ] 263 | 264 | [[package]] 265 | name = "serde_spanned" 266 | version = "0.6.7" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" 269 | dependencies = [ 270 | "serde", 271 | ] 272 | 273 | [[package]] 274 | name = "syn" 275 | version = "2.0.72" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" 278 | dependencies = [ 279 | "proc-macro2", 280 | "quote", 281 | "unicode-ident", 282 | ] 283 | 284 | [[package]] 285 | name = "tempfile" 286 | version = "3.20.0" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 289 | dependencies = [ 290 | "fastrand", 291 | "getrandom 0.3.1", 292 | "once_cell", 293 | "rustix", 294 | "windows-sys", 295 | ] 296 | 297 | [[package]] 298 | name = "tinyjson" 299 | version = "2.5.1" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "9ab95735ea2c8fd51154d01e39cf13912a78071c2d89abc49a7ef102a7dd725a" 302 | 303 | [[package]] 304 | name = "toml" 305 | version = "0.8.19" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" 308 | dependencies = [ 309 | "serde", 310 | "serde_spanned", 311 | "toml_datetime", 312 | "toml_edit", 313 | ] 314 | 315 | [[package]] 316 | name = "toml_datetime" 317 | version = "0.6.8" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 320 | dependencies = [ 321 | "serde", 322 | ] 323 | 324 | [[package]] 325 | name = "toml_edit" 326 | version = "0.22.20" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" 329 | dependencies = [ 330 | "indexmap", 331 | "serde", 332 | "serde_spanned", 333 | "toml_datetime", 334 | "winnow", 335 | ] 336 | 337 | [[package]] 338 | name = "unicode-ident" 339 | version = "1.0.12" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 342 | 343 | [[package]] 344 | name = "uuid" 345 | version = "1.4.1" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" 348 | dependencies = [ 349 | "getrandom 0.2.15", 350 | ] 351 | 352 | [[package]] 353 | name = "vswhom" 354 | version = "0.1.0" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" 357 | dependencies = [ 358 | "libc", 359 | "vswhom-sys", 360 | ] 361 | 362 | [[package]] 363 | name = "vswhom-sys" 364 | version = "0.1.2" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" 367 | dependencies = [ 368 | "cc", 369 | "libc", 370 | ] 371 | 372 | [[package]] 373 | name = "wasi" 374 | version = "0.11.0+wasi-snapshot-preview1" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 377 | 378 | [[package]] 379 | name = "wasi" 380 | version = "0.13.3+wasi-0.2.2" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" 383 | dependencies = [ 384 | "wit-bindgen-rt", 385 | ] 386 | 387 | [[package]] 388 | name = "winapi" 389 | version = "0.3.9" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 392 | dependencies = [ 393 | "winapi-i686-pc-windows-gnu", 394 | "winapi-x86_64-pc-windows-gnu", 395 | ] 396 | 397 | [[package]] 398 | name = "winapi-i686-pc-windows-gnu" 399 | version = "0.4.0" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 402 | 403 | [[package]] 404 | name = "winapi-x86_64-pc-windows-gnu" 405 | version = "0.4.0" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 408 | 409 | [[package]] 410 | name = "windows-sys" 411 | version = "0.59.0" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 414 | dependencies = [ 415 | "windows-targets", 416 | ] 417 | 418 | [[package]] 419 | name = "windows-targets" 420 | version = "0.52.6" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 423 | dependencies = [ 424 | "windows_aarch64_gnullvm", 425 | "windows_aarch64_msvc", 426 | "windows_i686_gnu", 427 | "windows_i686_gnullvm", 428 | "windows_i686_msvc", 429 | "windows_x86_64_gnu", 430 | "windows_x86_64_gnullvm", 431 | "windows_x86_64_msvc", 432 | ] 433 | 434 | [[package]] 435 | name = "windows_aarch64_gnullvm" 436 | version = "0.52.6" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 439 | 440 | [[package]] 441 | name = "windows_aarch64_msvc" 442 | version = "0.52.6" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 445 | 446 | [[package]] 447 | name = "windows_i686_gnu" 448 | version = "0.52.6" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 451 | 452 | [[package]] 453 | name = "windows_i686_gnullvm" 454 | version = "0.52.6" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 457 | 458 | [[package]] 459 | name = "windows_i686_msvc" 460 | version = "0.52.6" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 463 | 464 | [[package]] 465 | name = "windows_x86_64_gnu" 466 | version = "0.52.6" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 469 | 470 | [[package]] 471 | name = "windows_x86_64_gnullvm" 472 | version = "0.52.6" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 475 | 476 | [[package]] 477 | name = "windows_x86_64_msvc" 478 | version = "0.52.6" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 481 | 482 | [[package]] 483 | name = "winnow" 484 | version = "0.6.18" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" 487 | dependencies = [ 488 | "memchr", 489 | ] 490 | 491 | [[package]] 492 | name = "winreg" 493 | version = "0.55.0" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" 496 | dependencies = [ 497 | "cfg-if", 498 | "windows-sys", 499 | ] 500 | 501 | [[package]] 502 | name = "wit-bindgen-rt" 503 | version = "0.33.0" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 506 | dependencies = [ 507 | "bitflags", 508 | ] 509 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "cargo-quickinstall", 4 | ] 5 | 6 | [profile.release] 7 | opt-level = "z" 8 | panic = "abort" 9 | strip = "symbols" 10 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-2022 cargo-quickinstall developers 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: publish 2 | publish: release ## alias for `make release` 3 | 4 | .PHONY: release 5 | release: cronjob_scripts/.python-deps-updated.timestamp ## Publish a new release 6 | (cd cargo-quickinstall/ && cargo release patch --execute --no-push) 7 | git push origin HEAD:release --tags 8 | $(MAKE) recheck 9 | 10 | # This is a poor man's lockfile. 11 | # This format is used because it is well supported by dependabot etc. 12 | # but for now, we do not have access to `uv` in CI and don't automatically update requirements.txt 13 | # as part of our make rules. I might revisit this decision later. 14 | requirements.txt: pyproject.toml ## Compile the python dependencies from pyproject.toml into requirements.txt 15 | uv pip compile pyproject.toml --python-version=3.8 --output-file requirements.txt 16 | 17 | .venv/bin/python: 18 | python -m venv .venv 19 | 20 | # install python dependencies and then record that we've done so so we don't do it again 21 | cronjob_scripts/.python-deps-updated.timestamp: pyproject.toml .venv/bin/python 22 | .venv/bin/python --version 23 | .venv/bin/python -m pip install --constraint requirements.txt --editable . 24 | touch cronjob_scripts/.python-deps-updated.timestamp 25 | 26 | .PHONY: windows 27 | windows: cronjob_scripts/.python-deps-updated.timestamp ## trigger a windows build 28 | TARGET_ARCH=x86_64-pc-windows-msvc .venv/bin/trigger-package-build 29 | 30 | .PHONY: mac 31 | mac: cronjob_scripts/.python-deps-updated.timestamp ## trigger a mac build 32 | TARGET_ARCH=x86_64-apple-darwin .venv/bin/trigger-package-build 33 | 34 | .PHONY: m1 35 | m1: cronjob_scripts/.python-deps-updated.timestamp ## trigger a mac m1 build 36 | TARGET_ARCH=aarch64-apple-darwin .venv/bin/trigger-package-build 37 | 38 | .PHONY: linux 39 | linux: cronjob_scripts/.python-deps-updated.timestamp ## trigger a linux build 40 | TARGET_ARCH=x86_64-unknown-linux-gnu .venv/bin/trigger-package-build 41 | 42 | .PHONY: linux-musl 43 | linux-musl: cronjob_scripts/.python-deps-updated.timestamp ## trigger a musl libc-based linux build 44 | TARGET_ARCH=x86_64-unknown-linux-musl .venv/bin/trigger-package-build 45 | 46 | .PHONY: recheck-self-only 47 | recheck-self-only: cronjob_scripts/.python-deps-updated.timestamp ## build ourself on all arches 48 | RECHECK_ONLY=cargo-quickinstall TARGET_ARCH=all .venv/bin/trigger-package-build 49 | 50 | .PHONY: trigger-all 51 | trigger-all: cronjob_scripts/.python-deps-updated.timestamp ## build some random packages on all arches 52 | TARGET_ARCH=all .venv/bin/trigger-package-build 53 | 54 | .PHONY: fmt 55 | fmt: ## run rustfmt and ruff format 56 | cargo fmt 57 | .venv/bin/ruff format cronjob_scripts 58 | 59 | .PHONY: test-cronjob-scripts 60 | test-cronjob-scripts: cronjob_scripts/.python-deps-updated.timestamp ## run the tests for the python cronjob_scripts 61 | .venv/bin/ruff format --check cronjob_scripts 62 | .venv/bin/ruff check cronjob_scripts 63 | .venv/bin/python -m unittest discover -s cronjob_scripts 64 | .venv/bin/trigger-package-build --help 65 | .venv/bin/crates-io-popular-crates 66 | .venv/bin/stats 67 | 68 | .PHONY: help 69 | help: ## Display this help screen 70 | @grep -E '^[a-z.A-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cargo-quickinstall 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/cargo-quickinstall.svg)](https://crates.io/crates/cargo-quickinstall) 4 | [![Join the chat at https://gitter.im/cargo-quickinstall/community](https://badges.gitter.im/cargo-quickinstall/community.svg)](https://gitter.im/cargo-quickinstall/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | `cargo-quickinstall` is a bit like Homebrew's concept of [Bottles (binary packages)](https://docs.brew.sh/Bottles), but for `cargo install`. 7 | 8 | ## Installation 9 | 10 | cargo install cargo-quickinstall 11 | 12 | Recent versions of Windows, MacOS and Linux are supported. 13 | 14 | ## Usage 15 | 16 | Whenever you would usually write something like: 17 | 18 | cargo install ripgrep 19 | 20 | you can now write: 21 | 22 | cargo quickinstall ripgrep 23 | 24 | This will install pre-compiled versions of any binaries in the crate. If we don't have a pre-compiled version, it will fallback to `cargo install` automatically. 25 | 26 | ## Relationship to `cargo-binstall` 27 | 28 | [`cargo-binstall`](https://crates.io/crates/cargo-binstall) (from version 0.6.2 onwards) is also capable of fetching packages from the cargo-quickinstall github releases repo. `cargo-binstall` is an excellent piece of software. If you're looking for something for desktop use, I can recommend using `cargo-binstall`. 29 | 30 | ## Use in CI systems 31 | 32 | If you want to install a rust package on a CI system, you can do it with a `curl | tar` command, directly from the `cargo-quickinstall` github releases repo. 33 | 34 | ```bash 35 | cargo-quickinstall --dry-run --no-binstall ripgrep 36 | ``` 37 | 38 | will print: 39 | 40 | ```bash 41 | curl --user-agent "cargo-quickinstall/0.3.13 client (alsuren@gmail.com)" --location --silent --show-error --fail "https://github.com/cargo-bins/cargo-quickinstall/releases/download/ripgrep-14.1.1/ripgrep-14.1.1-aarch64-apple-darwin.tar.gz" | tar -xzvvf - -C /Users/alsuren/.cargo/bin 42 | ``` 43 | 44 | Edit the command however you need, and paste it into your CI pipeline. 45 | 46 | ## Supported targets 47 | 48 | Check [supported-targets](/supported-targets) for lists of targets quickinstall 49 | can build for. 50 | 51 | ## Limitations 52 | 53 | Non-default features are not supported. 54 | 55 | The `cargo-quickinstall` client is just a glorified bash script at this point. 56 | 57 | Currently it assumes that you have access to: 58 | 59 | - tar 60 | - curl 61 | 62 | Both of these should exist on all recent Windows and MacOS installs. `curl` is available on most Linux systems, and is assumed to exist by the `rustup` installation instructions. I only plan to remove these runtime dependencies if it can be done without increasing how long `cargo install cargo-quickinstall` takes (might be possible to do this using feature flags?). 63 | 64 | There are a few pieces of infrastructure that are also part of this project: 65 | 66 | - [x] A server for distributing the pre-built binaries 67 | - We are using github releases for this. 68 | - [x] A server for report gathering 69 | - This is done using a vercel server that saves counts to redis. 70 | - [x] A periodic task for building the most-requested packages for each OS/architecture 71 | - [ ] Get someone to audit my GitHub Actions sandboxing scheme. 72 | 73 | ## Contributing 74 | 75 | There are a lot of things to figure out at the moment, so now is the perfect time to jump in and help. I created a [Gitter](https://gitter.im/cargo-quickinstall/community) room for collaborating in. You can also poke [@alsuren](https://twitter.com/alsuren) on Twitter or Discord. I'm also up for pairing over zoom to get new contributors onboarded. 76 | 77 | Work is currently tracked on [the kanban board](https://github.com/orgs/cargo-bins/projects/1). If you want help breaking down a ticket, give me a shout in one of the above places. 78 | 79 | ## Releasing 80 | 81 | Releasing of patch versions is handled by the makefile, so can be done by: 82 | 83 | ```bash 84 | make release 85 | ``` 86 | 87 | If you need to make a major version bump then copy-paste the commands out of the Makefile. 88 | 89 | Once a release has been made, post about it on [the rust forums](https://users.rust-lang.org/c/announcements/6), [reddit](https://www.reddit.com/r/rust/) and twitter. 90 | 91 | ## License 92 | 93 | Copyright (c) 2020-2022 cargo-quickinstall developers 94 | 95 | `cargo-quickinstall` is made available under the terms of either the MIT License or the Apache License 2.0, at your option. 96 | 97 | See the [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) files for license details. 98 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - Add `apt-get install protobuf-compiler` to `build-version.sh` 2 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure("2") do |config| 2 | config.vm.box = "teledyne-lecroy/windows10-rust" 3 | config.vm.box_version = "0.1.0" 4 | end 5 | -------------------------------------------------------------------------------- /add-exclude.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | if [ $# != 1 ]; then 5 | echo "Usage: $0 /path/to/trigger/branch" 6 | exit 1 7 | fi 8 | 9 | cd "$1" 10 | 11 | exclude="$(date +'%y-%m-%d')" 12 | echo "${CRATE}" >>"$exclude" 13 | 14 | git add "$exclude" 15 | git --no-pager diff HEAD 16 | 17 | git config user.email "alsuren+quickinstall@gmail.com" 18 | git config user.name "build-package.yml:add-exclude.sh" 19 | 20 | git commit -m "Generates/Updates \"$exclude\"" 21 | 22 | exec git push origin "trigger/$TARGET_ARCH" 23 | -------------------------------------------------------------------------------- /build-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | GLIBC_VERSION="${GLIBC_VERSION:-2.17}" 5 | 6 | cd "$(dirname "$0")" 7 | 8 | CRATE=${1?"USAGE: $0 CRATE"} 9 | date 10 | 11 | features="${FEATURES:-}" 12 | if [ -z "$features" ]; then 13 | feature_flag="" 14 | else 15 | feature_flag="--features" 16 | fi 17 | 18 | no_default_features="" 19 | if [ "${NO_DEFAULT_FEATURES:-}" = "true" ]; then 20 | no_default_features='--no-default-features' 21 | fi 22 | 23 | # FIXME: make a signal handler that cleans this up if we exit early. 24 | if [ ! -d "${TEMPDIR:-}" ]; then 25 | TEMPDIR="$(mktemp -d)" 26 | fi 27 | 28 | # see crawler policy: https://crates.io/policies 29 | curl_slowly() { 30 | sleep 1 && curl --user-agent "cargo-quickinstall build pipeline (alsuren@gmail.com)" "$@" 31 | } 32 | 33 | export CARGO=cargo 34 | 35 | install_zig_cc_and_config_to_use_it() { 36 | # Install cargo-zigbuild 37 | # 38 | # We use cargo-zigbuild instead of zig-cc for cargo-zigbuild has 39 | # built-in for certain quirks when used with cargo-build. 40 | pip3 install -r zigbuild-requirements.txt 41 | 42 | export CARGO=cargo-zigbuild 43 | # Use our own pkg-config that fails for any input, since we cannot use 44 | # locally installed lib in cross-compilation. 45 | export PKG_CONFIG="$PWD/pkg-config-cross.sh" 46 | } 47 | 48 | REPO="$(./get-repo.sh)" 49 | 50 | if [ "${ALWAYS_BUILD:-}" != 1 ] && curl_slowly --fail -I --output /dev/null "${REPO}/releases/download/${CRATE}-${VERSION}/${CRATE}-${VERSION}-${TARGET_ARCH}.tar.gz"; then 51 | echo "${CRATE}/${VERSION}/${CRATE}-${VERSION}-${TARGET_ARCH}.tar.gz already uploaded. Skipping." 52 | exit 0 53 | fi 54 | 55 | # Install llvm 56 | if [ "${RUNNER_OS?}" == "Windows" ]; then 57 | choco install llvm 58 | elif [ "${RUNNER_OS?}" == "Linux" ]; then 59 | sudo apt-get update 60 | sudo apt-get install -y clang llvm lld 61 | llvm_prefix="$(find /usr/lib/llvm-* -maxdepth 0 | sort --reverse | head -n 1)" 62 | 63 | PATH="${llvm_prefix}/bin:${PATH}" 64 | export PATH 65 | 66 | LLVM_CONFIG_PATH="${llvm_prefix}/bin/llvm-config" 67 | export LLVM_CONFIG_PATH 68 | 69 | LIBCLANG_PATH="$(llvm-config --libdir)" 70 | export LIBCLANG_PATH 71 | LLVM_DIR="$(llvm-config --cmakedir)" 72 | export LLVM_DIR 73 | elif [ "${RUNNER_OS?}" == "macOS" ]; then 74 | brew install llvm 75 | 76 | LLVM_PREFIX="$(brew --prefix llvm)" 77 | 78 | PATH="${LLVM_PREFIX}/bin:${PATH:-}" 79 | export PATH 80 | 81 | LLVM_CONFIG_PATH="${LLVM_PREFIX}/bin/llvm-config" 82 | export LLVM_CONFIG_PATH 83 | 84 | DYLD_LIBRARY_PATH="${LLVM_PREFIX}/lib:${LD_LIBRARY_PATH:-}" 85 | export DYLD_LIBRARY_PATH 86 | 87 | LIBCLANG_PATH="$(llvm-config --libdir)" 88 | export LIBCLANG_PATH 89 | 90 | LLVM_DIR="$(llvm-config --cmakedir)" 91 | export LLVM_DIR 92 | else 93 | echo "Unsupported ${RUNNER_OS?}" 94 | exit 1 95 | fi 96 | 97 | if [[ "$TARGET_ARCH" == *"-linux-"* ]]; then 98 | install_zig_cc_and_config_to_use_it 99 | fi 100 | 101 | if [[ "$TARGET_ARCH" == *"-linux-gnu"* ]]; then 102 | CARGO_TARGET_ARCH="${TARGET_ARCH}.${GLIBC_VERSION}" 103 | fi 104 | 105 | # Install rustup target 106 | rustup target add "$TARGET_ARCH" 107 | 108 | # Start building! 109 | CARGO_ROOT=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-root') 110 | export CARGO_PROFILE_RELEASE_CODEGEN_UNITS="1" 111 | export CARGO_PROFILE_RELEASE_LTO="fat" 112 | export OPENSSL_STATIC=1 113 | export CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse 114 | 115 | build_and_install() { 116 | # shellcheck disable=SC2086 117 | $CARGO install "$CRATE" \ 118 | --version "$VERSION" \ 119 | --target "${CARGO_TARGET_ARCH:-$TARGET_ARCH}" \ 120 | --root "$CARGO_ROOT" \ 121 | ${1:-} \ 122 | $no_default_features \ 123 | $feature_flag $features 124 | } 125 | 126 | # Some crates are published without a lockfile, so fallback to no `--locked` 127 | # just in case of spurious failure. 128 | build_and_install '--locked' || build_and_install 129 | 130 | # Collect binaries 131 | CARGO_BIN_DIR="${CARGO_ROOT}/bin" 132 | CRATES2_JSON_PATH="${CARGO_ROOT}/.crates2.json" 133 | 134 | BINARIES=$( 135 | jq -r ' 136 | .installs | to_entries[] | select(.key|startswith("'"${CRATE}"' ")) | .value.bins | .[] 137 | ' "$CRATES2_JSON_PATH" | tr '\r' ' ' 138 | ) 139 | 140 | cd "$CARGO_BIN_DIR" 141 | for file in $BINARIES; do 142 | if file "$file" | grep ': data$'; then 143 | echo "something wrong with $file. Should be recognised as executable." 144 | exit 1 145 | fi 146 | done 147 | 148 | if [ -z "$BINARIES" ]; then 149 | echo "\`cargo-install\` does not install any binaries!" 150 | exit 1 151 | fi 152 | 153 | # Package up the binaries so that they can be untarred in ~/.cargo/bin 154 | # 155 | # TODO: maybe we want to make a ~/.cargo-quickinstall/bin to untar into, 156 | # and add symlinks into ~/.cargo/bin, to aid debugging? 157 | # 158 | # BINARIES is a space-separated list of files, so it can't be quoted 159 | # shellcheck disable=SC2086 160 | tar --format=v7 -c $BINARIES | gzip -9 -c >"${TEMPDIR}/${CRATE}-${VERSION}-${TARGET_ARCH}.tar.gz" 161 | 162 | echo "${TEMPDIR}/${CRATE}-${VERSION}-${TARGET_ARCH}.tar.gz" 163 | -------------------------------------------------------------------------------- /cargo-quickinstall/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.3.15](https://github.com/cargo-bins/cargo-quickinstall/compare/v0.3.14...v0.3.15) - 2025-05-29 10 | 11 | ### Other 12 | 13 | - Bump embed-resource from 3.0.2 to 3.0.3 in the deps group ([#370](https://github.com/cargo-bins/cargo-quickinstall/pull/370)) 14 | 15 | ## [0.3.14](https://github.com/cargo-bins/cargo-quickinstall/compare/v0.3.13...v0.3.14) - 2025-05-13 16 | 17 | ### Other 18 | 19 | - Bump tempfile from 3.19.1 to 3.20.0 in the deps group ([#366](https://github.com/cargo-bins/cargo-quickinstall/pull/366)) 20 | - Correct example of how to use in CI in README ([#364](https://github.com/cargo-bins/cargo-quickinstall/pull/364)) 21 | 22 | ## [0.3.13](https://github.com/cargo-bins/cargo-quickinstall/compare/v0.3.12...v0.3.13) - 2025-03-20 23 | 24 | ### Other 25 | 26 | - Bump tempfile from 3.19.0 to 3.19.1 in the deps group ([#357](https://github.com/cargo-bins/cargo-quickinstall/pull/357)) 27 | 28 | ## [0.3.12](https://github.com/cargo-bins/cargo-quickinstall/compare/v0.3.11...v0.3.12) - 2025-03-14 29 | 30 | ### Other 31 | 32 | - Bump tempfile from 3.18.0 to 3.19.0 in the deps group ([#354](https://github.com/cargo-bins/cargo-quickinstall/pull/354)) 33 | 34 | ## [0.3.11](https://github.com/cargo-bins/cargo-quickinstall/compare/v0.3.10...v0.3.11) - 2025-03-07 35 | 36 | ### Other 37 | 38 | - Bump tempfile from 3.17.1 to 3.18.0 in the deps group ([#350](https://github.com/cargo-bins/cargo-quickinstall/pull/350)) 39 | 40 | ## [0.3.10](https://github.com/cargo-bins/cargo-quickinstall/compare/v0.3.9...v0.3.10) - 2025-03-03 41 | 42 | ### Other 43 | 44 | - Bump embed-resource from 3.0.1 to 3.0.2 in the deps group ([#347](https://github.com/cargo-bins/cargo-quickinstall/pull/347)) 45 | 46 | ## [0.3.9](https://github.com/cargo-bins/cargo-quickinstall/compare/v0.3.8...v0.3.9) - 2025-02-18 47 | 48 | ### Other 49 | 50 | - Bump tempfile from 3.17.0 to 3.17.1 in the deps group ([#343](https://github.com/cargo-bins/cargo-quickinstall/pull/343)) 51 | 52 | ## [0.3.8](https://github.com/cargo-bins/cargo-quickinstall/compare/v0.3.7...v0.3.8) - 2025-02-17 53 | 54 | ### Other 55 | 56 | - Bump tempfile from 3.15.0 to 3.17.0 in the deps group across 1 directory ([#340](https://github.com/cargo-bins/cargo-quickinstall/pull/340)) 57 | 58 | ## [0.3.7](https://github.com/cargo-bins/cargo-quickinstall/compare/v0.3.6...v0.3.7) - 2025-01-03 59 | 60 | ### Other 61 | 62 | - Bump tempfile from 3.14.0 to 3.15.0 in the deps group ([#334](https://github.com/cargo-bins/cargo-quickinstall/pull/334)) 63 | 64 | ## [0.3.6](https://github.com/cargo-bins/cargo-quickinstall/compare/v0.3.5...v0.3.6) - 2024-12-17 65 | 66 | ### Other 67 | 68 | - Bump home from 0.5.9 to 0.5.11 in the deps group ([#329](https://github.com/cargo-bins/cargo-quickinstall/pull/329)) 69 | 70 | ## [0.3.5](https://github.com/cargo-bins/cargo-quickinstall/compare/v0.3.4...v0.3.5) - 2024-11-12 71 | 72 | ### Other 73 | 74 | - Squash warning after embed-resource version bump; enable clippy in ci ([#321](https://github.com/cargo-bins/cargo-quickinstall/pull/321)) 75 | 76 | ## [0.3.4](https://github.com/cargo-bins/cargo-quickinstall/compare/v0.3.3...v0.3.4) - 2024-11-11 77 | 78 | ### Other 79 | 80 | - Bump embed-resource from 2.5.0 to 3.0.1 in the deps group ([#319](https://github.com/cargo-bins/cargo-quickinstall/pull/319)) 81 | 82 | ## [0.3.3](https://github.com/cargo-bins/cargo-quickinstall/compare/v0.3.2...v0.3.3) - 2024-11-08 83 | 84 | ### Other 85 | 86 | - Bump tempfile from 3.13.0 to 3.14.0 in the deps group ([#312](https://github.com/cargo-bins/cargo-quickinstall/pull/312)) 87 | 88 | ## [0.3.2](https://github.com/cargo-bins/cargo-quickinstall/compare/v0.3.1...v0.3.2) - 2024-10-01 89 | 90 | ### Other 91 | 92 | - Bump tempfile from 3.12.0 to 3.13.0 in the deps group ([#304](https://github.com/cargo-bins/cargo-quickinstall/pull/304)) 93 | 94 | ## [0.3.1](https://github.com/cargo-bins/cargo-quickinstall/compare/v0.3.0...v0.3.1) - 2024-09-27 95 | 96 | ### Other 97 | 98 | - Bump embed-resource from 2.4.3 to 2.5.0 in the deps group ([#301](https://github.com/cargo-bins/cargo-quickinstall/pull/301)) 99 | 100 | ## [0.3.0](https://github.com/cargo-bins/cargo-quickinstall/compare/v0.2.12...v0.3.0) - 2024-09-09 101 | 102 | ### Other 103 | 104 | - report stats to new server, with status ([#281](https://github.com/cargo-bins/cargo-quickinstall/pull/281)) 105 | - rewrite Cronjob build triggering logic in python; read from influxdb ([#275](https://github.com/cargo-bins/cargo-quickinstall/pull/275)) 106 | - Fix test_get_latest_version ([#272](https://github.com/cargo-bins/cargo-quickinstall/pull/272)) 107 | 108 | ## [0.2.12](https://github.com/cargo-bins/cargo-quickinstall/compare/v0.2.11...v0.2.12) - 2024-08-26 109 | 110 | ### Other 111 | - Bump guess_host_triple from 0.1.3 to 0.1.4 in the deps group ([#269](https://github.com/cargo-bins/cargo-quickinstall/pull/269)) 112 | 113 | ## [0.2.11](https://github.com/cargo-bins/cargo-quickinstall/compare/v0.2.10...v0.2.11) - 2024-08-08 114 | 115 | ### Other 116 | - Add embedded windows manifest for `cargo-quickinstall` ([#262](https://github.com/cargo-bins/cargo-quickinstall/pull/262)) 117 | - Bump tempfile from 3.11.0 to 3.12.0 ([#260](https://github.com/cargo-bins/cargo-quickinstall/pull/260)) 118 | - Bump tempfile from 3.10.1 to 3.11.0 ([#258](https://github.com/cargo-bins/cargo-quickinstall/pull/258)) 119 | - Bump tempfile from 3.10.0 to 3.10.1 ([#247](https://github.com/cargo-bins/cargo-quickinstall/pull/247)) 120 | - Bump tempfile from 3.9.0 to 3.10.0 ([#245](https://github.com/cargo-bins/cargo-quickinstall/pull/245)) 121 | - Bump tempfile from 3.8.1 to 3.9.0 ([#242](https://github.com/cargo-bins/cargo-quickinstall/pull/242)) 122 | - Bump home from 0.5.5 to 0.5.9 ([#240](https://github.com/cargo-bins/cargo-quickinstall/pull/240)) 123 | -------------------------------------------------------------------------------- /cargo-quickinstall/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-quickinstall" 3 | version = "0.3.15" 4 | authors = ["David Laban "] 5 | edition = "2018" 6 | description = "Precompiled binary installs for `cargo install`" 7 | license = "MIT OR Apache-2.0" 8 | readme = "../README.md" 9 | repository = "https://github.com/cargo-bins/cargo-quickinstall" 10 | 11 | [dependencies] 12 | tinyjson = "2" 13 | home = "0.5.11" 14 | pico-args = { version = "0.5.0", features = ["eq-separator"] } 15 | tempfile = "3.20.0" 16 | guess_host_triple = "0.1.4" 17 | 18 | [build-dependencies] 19 | embed-resource = "3.0.3" 20 | 21 | [dev-dependencies] 22 | mktemp = "0.5.1" 23 | -------------------------------------------------------------------------------- /cargo-quickinstall/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rerun-if-changed=build.rs"); 3 | println!("cargo:rerun-if-changed=manifest.rc"); 4 | println!("cargo:rerun-if-changed=windows.manifest"); 5 | 6 | embed_resource::compile("manifest.rc", embed_resource::NONE) 7 | .manifest_required() 8 | .unwrap(); 9 | } 10 | -------------------------------------------------------------------------------- /cargo-quickinstall/manifest.rc: -------------------------------------------------------------------------------- 1 | #define RT_MANIFEST 24 2 | 1 RT_MANIFEST "windows.manifest" 3 | -------------------------------------------------------------------------------- /cargo-quickinstall/src/args.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | pub const USAGE: &str = "USAGE: 4 | cargo quickinstall [OPTIONS] -- ... 5 | 6 | For more information try --help 7 | "; 8 | pub const HELP: &str = "USAGE: 9 | cargo quickinstall [OPTIONS] -- ... 10 | 11 | ... - can be one or more crates. Each one can be either simply a name, or a name and 12 | version `cargo-quickinstall@0.2.7`. 13 | 14 | OPTIONS: 15 | --version Specify a version to install. It's invalid specify this in 16 | batch installtion mode. 17 | 18 | NOTE that if you specify --version when only providing one crate 19 | to install, then --version takes precedence over the version 20 | specified in the . 21 | 22 | --target Install package for the target triple 23 | --force Install the even if they are already installed. 24 | Only effective when using `cargo-binstall`. 25 | --try-upstream Try looking for official builds from the upstream maintainers. 26 | This takes a few extra seconds. 27 | --no-fallback Don't fall back to `cargo install` 28 | --no-binstall Don't use `cargo binstall` to install packages. `cargo-binstall` 29 | sets metadata required for `cargo uninstall`, so only use 30 | `--no-binstall` if you know you don't need to uninstall packages 31 | (for example on CI builds). 32 | --dry-run Print the `curl | tar` command that would be run to fetch the binary 33 | -V, --print-version Print version info and exit 34 | -h, --help Prints help information 35 | "; 36 | 37 | #[cfg_attr(test, derive(Debug))] 38 | pub struct CliOptions { 39 | pub target: Option, 40 | pub crate_names: Vec, 41 | pub try_upstream: bool, 42 | pub fallback: bool, 43 | pub force: bool, 44 | pub no_binstall: bool, 45 | pub print_version: bool, 46 | pub help: bool, 47 | pub dry_run: bool, 48 | } 49 | 50 | #[cfg_attr(test, derive(Debug, Eq, PartialEq))] 51 | pub struct Crate { 52 | pub name: String, 53 | pub version: Option, 54 | } 55 | 56 | impl Crate { 57 | fn new(s: &str) -> Self { 58 | if let Some((name, version)) = s.split_once('@') { 59 | Self { 60 | name: name.to_string(), 61 | version: Some(version.to_string()), 62 | } 63 | } else { 64 | Self { 65 | name: s.to_string(), 66 | version: None, 67 | } 68 | } 69 | } 70 | } 71 | 72 | impl Crate { 73 | pub fn into_arg(self) -> String { 74 | let mut arg = self.name; 75 | 76 | if let Some(version) = self.version { 77 | arg.push('@'); 78 | arg += version.as_str(); 79 | } 80 | 81 | arg 82 | } 83 | } 84 | 85 | pub fn options_from_cli_args( 86 | mut args: pico_args::Arguments, 87 | ) -> Result> { 88 | let version = args.opt_value_from_str("--version")?; 89 | 90 | let mut opts = CliOptions { 91 | target: args.opt_value_from_str("--target")?, 92 | try_upstream: args.contains("--try-upstream"), 93 | fallback: !args.contains("--no-fallback"), 94 | force: args.contains("--force"), 95 | no_binstall: args.contains("--no-binstall"), 96 | print_version: args.contains(["-V", "--print-version"]), 97 | help: args.contains(["-h", "--help"]), 98 | dry_run: args.contains("--dry-run"), 99 | // WARNING: We MUST parse all --options before parsing positional arguments, 100 | // because .subcommand() errors out if handed an arg with - at the start. 101 | crate_names: crate_names_from_positional_args(args)?, 102 | }; 103 | 104 | if version.is_some() { 105 | match opts.crate_names.len().cmp(&1) { 106 | Ordering::Equal => opts.crate_names[0].version = version, 107 | Ordering::Greater => Err("You cannot specify `--version` in batch installation mode")?, 108 | _ => (), 109 | } 110 | } 111 | 112 | Ok(opts) 113 | } 114 | 115 | pub fn crate_names_from_positional_args( 116 | args: pico_args::Arguments, 117 | ) -> Result, Box> { 118 | let args = args.finish(); 119 | 120 | let args = args 121 | .iter() 122 | .map(|os_str| { 123 | os_str 124 | .to_str() 125 | .ok_or("Invalid crate names: Expected utf-8 strings") 126 | }) 127 | .collect::, _>>()?; 128 | 129 | let mut check_for_slash = true; 130 | 131 | let args_to_skip = match args[..] { 132 | ["quickinstall", "--", ..] => { 133 | check_for_slash = false; 134 | 2 135 | } 136 | ["quickinstall", ..] => 1, 137 | ["--", ..] => { 138 | check_for_slash = false; 139 | 1 140 | } 141 | _ => 0, 142 | }; 143 | 144 | args.into_iter() 145 | .skip(args_to_skip) 146 | .map(|arg| { 147 | if check_for_slash && arg.starts_with('-') { 148 | Err(format!("Invalid crate, crate name cannot starts with '-': {arg}").into()) 149 | } else { 150 | Ok(Crate::new(arg)) 151 | } 152 | }) 153 | .collect() 154 | } 155 | 156 | #[cfg(test)] 157 | mod test { 158 | use super::*; 159 | use std::ffi::OsString; 160 | 161 | const MOCK_CRATE_NAME: &str = "foo"; 162 | const MOCK_CRATE_VERSION: &str = "3.14.15"; 163 | const INVALID_FLAG: &str = "--an-invalid-flag"; 164 | 165 | #[test] 166 | fn test_options_from_env() { 167 | let mock_cli_args: Vec = [ 168 | MOCK_CRATE_NAME, 169 | "--version", 170 | MOCK_CRATE_VERSION, 171 | "--dry-run", 172 | ] 173 | .iter() 174 | .map(OsString::from) 175 | .collect(); 176 | let mock_pico_args = pico_args::Arguments::from_vec(mock_cli_args); 177 | 178 | let options = options_from_cli_args(mock_pico_args); 179 | let cli_options: CliOptions = options.unwrap(); 180 | 181 | assert_eq!( 182 | [Crate { 183 | name: MOCK_CRATE_NAME.to_string(), 184 | version: Some(MOCK_CRATE_VERSION.to_string()), 185 | }] 186 | .as_slice(), 187 | cli_options.crate_names 188 | ); 189 | assert!(cli_options.dry_run); 190 | } 191 | 192 | #[test] 193 | fn test_options_from_env_err() { 194 | let mock_cli_args: Vec = vec![OsString::from(INVALID_FLAG)]; 195 | let mock_pico_args = pico_args::Arguments::from_vec(mock_cli_args); 196 | 197 | let result = options_from_cli_args(mock_pico_args); 198 | assert!(result.is_err(), "{:#?}", result); 199 | } 200 | 201 | #[test] 202 | fn test_crate_name_from_positional_args() { 203 | let mock_cli_args: Vec = vec![OsString::from(MOCK_CRATE_NAME)]; 204 | let mock_pico_args = pico_args::Arguments::from_vec(mock_cli_args); 205 | 206 | let crate_name = crate_names_from_positional_args(mock_pico_args).unwrap(); 207 | assert_eq!( 208 | [Crate { 209 | name: MOCK_CRATE_NAME.to_string(), 210 | version: None 211 | }] 212 | .as_slice(), 213 | crate_name 214 | ); 215 | } 216 | 217 | #[test] 218 | fn test_crate_name_from_positional_args_err() { 219 | let mock_cli_args: Vec = vec![ 220 | OsString::from(MOCK_CRATE_NAME), 221 | OsString::from("cargo-quickinstall@0.2.7"), 222 | ]; 223 | let mock_pico_args = pico_args::Arguments::from_vec(mock_cli_args); 224 | 225 | let crates = crate_names_from_positional_args(mock_pico_args).unwrap(); 226 | 227 | assert_eq!( 228 | [ 229 | Crate { 230 | name: MOCK_CRATE_NAME.to_string(), 231 | version: None 232 | }, 233 | Crate { 234 | name: "cargo-quickinstall".to_string(), 235 | version: Some("0.2.7".to_string()), 236 | } 237 | ] 238 | .as_slice(), 239 | crates 240 | ); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /cargo-quickinstall/src/command_ext.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::OsStr, 3 | fmt::{self, Write}, 4 | process::{self, Child, Command, Output}, 5 | }; 6 | 7 | use crate::{utf8_to_string_lossy, CommandFailed, InstallError}; 8 | 9 | pub trait CommandExt { 10 | fn formattable(&self) -> CommandFormattable<'_>; 11 | 12 | fn output_checked_status(&mut self) -> Result; 13 | 14 | fn spawn_with_cmd(self) -> Result; 15 | } 16 | 17 | impl CommandExt for Command { 18 | fn formattable(&self) -> CommandFormattable<'_> { 19 | CommandFormattable(self) 20 | } 21 | 22 | fn output_checked_status(&mut self) -> Result { 23 | self.output() 24 | .map_err(InstallError::from) 25 | .and_then(|output| check_status(self, output)) 26 | } 27 | 28 | fn spawn_with_cmd(mut self) -> Result { 29 | self.spawn() 30 | .map_err(InstallError::from) 31 | .map(move |child| ChildWithCommand { child, cmd: self }) 32 | } 33 | } 34 | 35 | pub struct CommandFormattable<'a>(&'a Command); 36 | 37 | fn needs_escape(s: &str) -> bool { 38 | s.contains(|ch| !matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '=' | '/' | ',' | '.' | '+')) 39 | } 40 | 41 | fn write_shell_arg_escaped(f: &mut fmt::Formatter<'_>, os_str: &OsStr) -> fmt::Result { 42 | let s = os_str.to_string_lossy(); 43 | 44 | if needs_escape(&s) { 45 | // There is some ascii whitespace (' ', '\n', '\t'), 46 | // or non-ascii characters need to quote them using `"`. 47 | // 48 | // But then, it is possible for the `s` to contains `"`, 49 | // so they needs to be escaped. 50 | f.write_str("\"")?; 51 | 52 | for ch in s.chars() { 53 | if ch == '"' { 54 | // Escape it with `\`. 55 | f.write_char('\\')?; 56 | } 57 | 58 | f.write_char(ch)?; 59 | } 60 | 61 | f.write_str("\"") 62 | } else { 63 | f.write_str(&s) 64 | } 65 | } 66 | 67 | impl fmt::Display for CommandFormattable<'_> { 68 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 69 | let cmd = self.0; 70 | 71 | write_shell_arg_escaped(f, cmd.get_program())?; 72 | 73 | for arg in cmd.get_args() { 74 | f.write_str(" ")?; 75 | write_shell_arg_escaped(f, arg)?; 76 | } 77 | 78 | Ok(()) 79 | } 80 | } 81 | 82 | pub struct ChildWithCommand { 83 | cmd: Command, 84 | child: Child, 85 | } 86 | 87 | fn check_status(cmd: &Command, output: Output) -> Result { 88 | if output.status.success() { 89 | Ok(output) 90 | } else { 91 | Err(CommandFailed { 92 | command: cmd.formattable().to_string(), 93 | stdout: utf8_to_string_lossy(output.stdout), 94 | stderr: utf8_to_string_lossy(output.stderr), 95 | } 96 | .into()) 97 | } 98 | } 99 | 100 | impl ChildWithCommand { 101 | pub fn wait_with_output_checked_status(self) -> Result { 102 | let cmd = self.cmd; 103 | 104 | self.child 105 | .wait_with_output() 106 | .map_err(InstallError::from) 107 | .and_then(|output| check_status(&cmd, output)) 108 | } 109 | 110 | pub fn stdin(&mut self) -> &mut Option { 111 | &mut self.child.stdin 112 | } 113 | 114 | pub fn stdout(&mut self) -> &mut Option { 115 | &mut self.child.stdout 116 | } 117 | 118 | pub fn stderr(&mut self) -> &mut Option { 119 | &mut self.child.stderr 120 | } 121 | } 122 | 123 | #[cfg(test)] 124 | mod tests { 125 | use super::*; 126 | 127 | #[test] 128 | fn test_cmd_format() { 129 | assert_eq!( 130 | Command::new("cargo") 131 | .args(["binstall", "-V"]) 132 | .formattable() 133 | .to_string(), 134 | "cargo binstall -V" 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /cargo-quickinstall/src/install_error.rs: -------------------------------------------------------------------------------- 1 | use crate::{CommandFailed, CrateDetails, JsonExtError}; 2 | use std::fmt::{Debug, Display}; 3 | 4 | use tinyjson::JsonParseError; 5 | 6 | pub enum InstallError { 7 | MissingCrateNameArgument(&'static str), 8 | CommandFailed(CommandFailed), 9 | IoError(std::io::Error), 10 | CargoInstallFailed, 11 | CrateDoesNotExist { crate_name: String }, 12 | NoFallback(CrateDetails), 13 | InvalidJson { url: String, err: JsonParseError }, 14 | JsonErr(JsonExtError), 15 | FailToParseRustcOutput { reason: &'static str }, 16 | } 17 | 18 | impl InstallError { 19 | pub fn is_curl_404(&self) -> bool { 20 | matches!( 21 | self, 22 | Self::CommandFailed(CommandFailed { stderr, .. }) 23 | if stderr.contains("curl: (22) The requested URL returned error: 404") 24 | ) 25 | } 26 | } 27 | 28 | impl std::error::Error for InstallError { 29 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 30 | match self { 31 | Self::IoError(io_err) => Some(io_err), 32 | Self::InvalidJson { err, .. } => Some(err), 33 | Self::JsonErr(err) => Some(err), 34 | _ => None, 35 | } 36 | } 37 | } 38 | 39 | // We implement `Debug` in terms of `Display`, because "Error: {:?}" 40 | // is what is shown to the user if you return an error from `main()`. 41 | impl Debug for InstallError { 42 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 43 | write! {f, "{}", self} 44 | } 45 | } 46 | 47 | impl Display for InstallError { 48 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 49 | match self { 50 | &InstallError::MissingCrateNameArgument(usage_text) => { 51 | write!(f, "No crate name specified.\n\n{}", usage_text) 52 | } 53 | InstallError::CommandFailed(CommandFailed { 54 | command, 55 | stdout, 56 | stderr, 57 | }) => { 58 | write!(f, "Command failed:\n {}\n", command)?; 59 | if !stdout.is_empty() { 60 | write!(f, "Stdout:\n{}\n", stdout)?; 61 | } 62 | if !stderr.is_empty() { 63 | write!(f, "Stderr:\n{}", stderr)?; 64 | } 65 | 66 | Ok(()) 67 | } 68 | InstallError::IoError(e) => write!(f, "{}", e), 69 | InstallError::CargoInstallFailed => { 70 | f.write_str("`cargo install` didn't work either. Looks like you're on your own.") 71 | } 72 | 73 | InstallError::CrateDoesNotExist { crate_name } => { 74 | write!(f, "`{}` does not exist on crates.io.", crate_name) 75 | } 76 | InstallError::NoFallback(crate_details) => { 77 | write!( 78 | f, 79 | "Could not find a pre-built package for {} {} on {}.", 80 | crate_details.crate_name, crate_details.version, crate_details.target 81 | ) 82 | } 83 | InstallError::InvalidJson { url, err } => { 84 | write!(f, "Failed to parse json downloaded from '{url}': {err}",) 85 | } 86 | InstallError::JsonErr(err) => write!(f, "{err}"), 87 | InstallError::FailToParseRustcOutput { reason } => { 88 | write!(f, "Failed to parse `rustc -vV` output: {reason}") 89 | } 90 | } 91 | } 92 | } 93 | 94 | impl From for InstallError { 95 | fn from(err: std::io::Error) -> InstallError { 96 | InstallError::IoError(err) 97 | } 98 | } 99 | impl From for InstallError { 100 | fn from(err: CommandFailed) -> InstallError { 101 | InstallError::CommandFailed(err) 102 | } 103 | } 104 | 105 | impl From for InstallError { 106 | fn from(err: JsonExtError) -> InstallError { 107 | InstallError::JsonErr(err) 108 | } 109 | } 110 | 111 | #[derive(Debug)] 112 | pub enum InstallSuccess { 113 | InstalledFromTarball, 114 | BuiltFromSource, 115 | } 116 | 117 | /** 118 | * Returns a status string for reporting to our stats server. 119 | * 120 | * The return type is a static string to encourage us to keep the cardinality vaguely small-ish, 121 | * and avoid us accidentally dumping personally identifiable information into influxdb. 122 | * 123 | * If we find ourselves getting a lot of a particular genre of error, we can always make a new 124 | * release to split things out a bit more. 125 | * 126 | * There is no requirement for cargo-quickinstall and cargo-binstall to agree on the status strings, 127 | * but it is probably a good idea to keep at least the first two in sync. 128 | */ 129 | pub fn install_result_to_status_str(result: &Result) -> &'static str { 130 | match result { 131 | Ok(InstallSuccess::InstalledFromTarball) => "installed-from-tarball", 132 | Ok(InstallSuccess::BuiltFromSource) => "built-from-source", 133 | Err(InstallError::CargoInstallFailed) => "cargo-install-failed", 134 | Err(InstallError::NoFallback(_)) => "no-fallback", 135 | Err(InstallError::MissingCrateNameArgument(_)) 136 | | Err(InstallError::CommandFailed(_)) 137 | | Err(InstallError::IoError(_)) 138 | | Err(InstallError::CrateDoesNotExist { .. }) 139 | | Err(InstallError::InvalidJson { .. }) 140 | | Err(InstallError::JsonErr(_)) 141 | | Err(InstallError::FailToParseRustcOutput { .. }) => "other-error", 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /cargo-quickinstall/src/json_value_ext.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use tinyjson::JsonValue; 3 | 4 | pub struct JsonExtError(String); 5 | 6 | impl fmt::Display for JsonExtError { 7 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 8 | f.write_str(&self.0) 9 | } 10 | } 11 | 12 | impl fmt::Debug for JsonExtError { 13 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 14 | f.write_str(&self.0) 15 | } 16 | } 17 | 18 | impl std::error::Error for JsonExtError {} 19 | 20 | impl JsonExtError { 21 | fn unexpected(value: &JsonValue, expected: &str) -> Self { 22 | JsonExtError(format!( 23 | "Expecting {expected}, but found {}", 24 | value.get_value_type() 25 | )) 26 | } 27 | } 28 | 29 | pub trait JsonValueExt { 30 | fn get_owned(self, key: &dyn JsonKey) -> Result; 31 | 32 | fn try_into_string(self) -> Result; 33 | 34 | fn get_value_type(&self) -> &'static str; 35 | } 36 | 37 | impl JsonValueExt for JsonValue { 38 | fn get_owned(self, key: &dyn JsonKey) -> Result { 39 | key.extract_from_value(self) 40 | } 41 | 42 | fn try_into_string(self) -> Result { 43 | match self { 44 | JsonValue::String(s) => Ok(s), 45 | value => Err(JsonExtError::unexpected(&value, "String")), 46 | } 47 | } 48 | 49 | fn get_value_type(&self) -> &'static str { 50 | match self { 51 | JsonValue::Number(..) => "Number", 52 | JsonValue::Boolean(..) => "Boolean", 53 | JsonValue::String(..) => "String", 54 | JsonValue::Null => "Null", 55 | JsonValue::Array(..) => "Array", 56 | JsonValue::Object(..) => "Object", 57 | } 58 | } 59 | } 60 | 61 | pub trait JsonKey { 62 | fn extract_from_value(&self, value: JsonValue) -> Result; 63 | } 64 | 65 | impl JsonKey for &T { 66 | fn extract_from_value(&self, value: JsonValue) -> Result { 67 | T::extract_from_value(*self, value) 68 | } 69 | } 70 | 71 | impl JsonKey for usize { 72 | fn extract_from_value(&self, value: JsonValue) -> Result { 73 | let index = *self; 74 | 75 | match value { 76 | JsonValue::Array(mut values) => { 77 | let len = values.len(); 78 | 79 | if index < len { 80 | Ok(values.swap_remove(index)) 81 | } else { 82 | Err(JsonExtError(format!( 83 | "Index {index} is too large for array of len {len}", 84 | ))) 85 | } 86 | } 87 | 88 | value => Err(JsonExtError::unexpected(&value, "Array")), 89 | } 90 | } 91 | } 92 | 93 | impl JsonKey for &str { 94 | fn extract_from_value(&self, value: JsonValue) -> Result { 95 | let key = *self; 96 | 97 | match value { 98 | JsonValue::Object(mut map) => map 99 | .remove(key) 100 | .ok_or_else(|| JsonExtError(format!("Key {key} not found in object"))), 101 | 102 | value => Err(JsonExtError::unexpected(&value, "Object")), 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /cargo-quickinstall/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Pre-built binary crate installer. 2 | //! 3 | //! Tries to install pre-built binary crates whenever possibles. Falls back to 4 | //! `cargo install` otherwise. 5 | 6 | use guess_host_triple::guess_host_triple; 7 | use std::{fs::File, path::Path, process}; 8 | use tempfile::NamedTempFile; 9 | use tinyjson::JsonValue; 10 | 11 | pub mod install_error; 12 | 13 | use install_error::*; 14 | 15 | mod command_ext; 16 | pub use command_ext::{ChildWithCommand, CommandExt, CommandFormattable}; 17 | 18 | mod json_value_ext; 19 | pub use json_value_ext::{JsonExtError, JsonKey, JsonValueExt}; 20 | 21 | mod utils; 22 | pub use utils::{get_cargo_bin_dir, utf8_to_string_lossy}; 23 | 24 | #[derive(Debug)] 25 | pub struct CommandFailed { 26 | pub command: String, 27 | pub stdout: String, 28 | pub stderr: String, 29 | } 30 | 31 | #[derive(Clone)] 32 | pub struct CrateDetails { 33 | pub crate_name: String, 34 | pub version: String, 35 | pub target: String, 36 | } 37 | 38 | /// Return (archive_format, url) 39 | fn get_binstall_upstream_url(target: &str) -> (&'static str, String) { 40 | let archive_format = if target.contains("linux") { 41 | "tgz" 42 | } else { 43 | "zip" 44 | }; 45 | let url = format!("https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-{target}.{archive_format}"); 46 | 47 | (archive_format, url) 48 | } 49 | 50 | /// Attempt to download and install cargo-binstall from upstream. 51 | pub fn download_and_install_binstall_from_upstream(target: &str) -> Result<(), InstallError> { 52 | let (archive_format, url) = get_binstall_upstream_url(target); 53 | 54 | if archive_format == "tgz" { 55 | untar(curl(&url)?)?; 56 | 57 | Ok(()) 58 | } else { 59 | assert_eq!(archive_format, "zip"); 60 | 61 | let (zip_file, zip_file_temp_path) = NamedTempFile::new()?.into_parts(); 62 | 63 | curl_file(&url, zip_file)?; 64 | 65 | unzip(&zip_file_temp_path)?; 66 | 67 | Ok(()) 68 | } 69 | } 70 | 71 | pub fn do_dry_run_download_and_install_binstall_from_upstream( 72 | target: &str, 73 | ) -> Result { 74 | let (archive_format, url) = get_binstall_upstream_url(target); 75 | 76 | curl_head(&url)?; 77 | 78 | let cargo_bin_dir = get_cargo_bin_dir()?; 79 | 80 | if archive_format == "tgz" { 81 | Ok(format_curl_and_untar_cmd(&url, &cargo_bin_dir)) 82 | } else { 83 | Ok(format!( 84 | "temp=\"$(mktemp)\"\n{curl_cmd} >\"$temp\"\nunzip \"$temp\" -d {extdir}", 85 | curl_cmd = prepare_curl_bytes_cmd(&url).formattable(), 86 | extdir = cargo_bin_dir.display(), 87 | )) 88 | } 89 | } 90 | 91 | pub fn unzip(zip_file: &Path) -> Result<(), InstallError> { 92 | let bin_dir = get_cargo_bin_dir()?; 93 | 94 | process::Command::new("unzip") 95 | .arg(zip_file) 96 | .arg("-d") 97 | .arg(bin_dir) 98 | .output_checked_status()?; 99 | 100 | Ok(()) 101 | } 102 | 103 | pub fn get_cargo_binstall_version() -> Option { 104 | let output = std::process::Command::new("cargo") 105 | .args(["binstall", "-V"]) 106 | .output() 107 | .ok()?; 108 | if !output.status.success() { 109 | return None; 110 | } 111 | 112 | String::from_utf8(output.stdout).ok() 113 | } 114 | 115 | pub fn install_crate_curl( 116 | details: &CrateDetails, 117 | fallback: bool, 118 | ) -> Result { 119 | let urls = get_quickinstall_download_urls(details); 120 | 121 | let res = match curl_and_untar(&urls[0]) { 122 | Err(err) if err.is_curl_404() => { 123 | println!("Fallback to old release schema"); 124 | 125 | curl_and_untar(&urls[1]) 126 | } 127 | res => res, 128 | }; 129 | 130 | match res { 131 | Ok(tar_output) => { 132 | let bin_dir = get_cargo_bin_dir()?; 133 | 134 | // tar output contains its own newline. 135 | print!( 136 | "Installed {crate_name}@{version} to {bin_dir}:\n{tar_output}", 137 | crate_name = details.crate_name, 138 | version = details.version, 139 | bin_dir = bin_dir.display(), 140 | ); 141 | Ok(InstallSuccess::InstalledFromTarball) 142 | } 143 | Err(err) if err.is_curl_404() => { 144 | if !fallback { 145 | return Err(InstallError::NoFallback(details.clone())); 146 | } 147 | 148 | println!( 149 | "Could not find a pre-built package for {} {} on {}.", 150 | details.crate_name, details.version, details.target 151 | ); 152 | println!("We have reported your installation request, so it should be built soon."); 153 | 154 | println!("Falling back to `cargo install`."); 155 | 156 | let status = prepare_cargo_install_cmd(details).status()?; 157 | 158 | if status.success() { 159 | Ok(InstallSuccess::BuiltFromSource) 160 | } else { 161 | Err(InstallError::CargoInstallFailed) 162 | } 163 | } 164 | Err(err) => Err(err), 165 | } 166 | } 167 | 168 | fn curl_and_untar(url: &str) -> Result { 169 | untar(curl(url)?) 170 | } 171 | 172 | pub fn get_latest_version(crate_name: &str) -> Result { 173 | let url = format!("https://crates.io/api/v1/crates/{}", crate_name); 174 | 175 | curl_json(&url) 176 | .map_err(|e| { 177 | if e.is_curl_404() { 178 | InstallError::CrateDoesNotExist { 179 | crate_name: crate_name.to_string(), 180 | } 181 | } else { 182 | e 183 | } 184 | })? 185 | .get_owned(&"crate")? 186 | .get_owned(&"max_stable_version")? 187 | .try_into_string() 188 | .map_err(From::from) 189 | } 190 | 191 | pub fn get_target_triple() -> Result { 192 | match get_target_triple_from_rustc() { 193 | Ok(target) => Ok(target), 194 | Err(err) => { 195 | if let Some(target) = guess_host_triple() { 196 | println!("get_target_triple_from_rustc() failed due to {err}, fallback to guess_host_triple"); 197 | Ok(target.to_string()) 198 | } else { 199 | println!("get_target_triple_from_rustc() failed due to {err}, fallback to guess_host_triple also failed"); 200 | Err(err) 201 | } 202 | } 203 | } 204 | } 205 | 206 | fn get_target_triple_from_rustc() -> Result { 207 | // Credit to https://stackoverflow.com/a/63866386 208 | let output = std::process::Command::new("rustc") 209 | .arg("--version") 210 | .arg("--verbose") 211 | .output_checked_status()?; 212 | 213 | let stdout = String::from_utf8_lossy(&output.stdout); 214 | let target = stdout 215 | .lines() 216 | .find_map(|line| line.strip_prefix("host: ")) 217 | .ok_or(InstallError::FailToParseRustcOutput { 218 | reason: "Fail to find any line starts with 'host: '.", 219 | })?; 220 | 221 | // The target triplets have the form of 'arch-vendor-system'. 222 | // 223 | // When building for Linux (e.g. the 'system' part is 224 | // 'linux-something'), replace the vendor with 'unknown' 225 | // so that mapping to rust standard targets happens correctly. 226 | // 227 | // For example, alpine set `rustc` host triple to 228 | // `x86_64-alpine-linux-musl`. 229 | // 230 | // Here we use splitn with n=4 since we just need to check 231 | // the third part to see if it equals to "linux" and verify 232 | // that we have at least three parts. 233 | let mut parts: Vec<&str> = target.splitn(4, '-').collect(); 234 | let os = *parts.get(2).ok_or(InstallError::FailToParseRustcOutput { 235 | reason: "rustc returned an invalid triple, contains less than three parts", 236 | })?; 237 | if os == "linux" { 238 | parts[1] = "unknown"; 239 | } 240 | Ok(parts.join("-")) 241 | } 242 | 243 | pub fn report_stats_in_background( 244 | details: &CrateDetails, 245 | result: &Result, 246 | ) { 247 | let stats_url = format!( 248 | "https://cargo-quickinstall-stats-server.fly.dev/record-install?crate={crate}&version={version}&target={target}&agent={agent}&status={status}", 249 | crate = url_encode(&details.crate_name), 250 | version = url_encode(&details.version), 251 | target = url_encode(&details.target), 252 | agent = url_encode(concat!("cargo-quickinstall/", env!("CARGO_PKG_VERSION"))), 253 | status = install_result_to_status_str(result), 254 | ); 255 | 256 | // Simply spawn the curl command to report stat. 257 | // 258 | // It's ok for it to fail and we would let the init process reap 259 | // the `curl` process. 260 | prepare_curl_post_cmd(&stats_url) 261 | .stdin(process::Stdio::null()) 262 | .stdout(process::Stdio::null()) 263 | .stderr(process::Stdio::null()) 264 | .spawn() 265 | .ok(); 266 | } 267 | 268 | fn url_encode(input: &str) -> String { 269 | let mut encoded = String::with_capacity(input.len()); 270 | 271 | for c in input.chars() { 272 | match c { 273 | 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => encoded.push(c), 274 | _ => encoded.push_str(&format!("%{:02X}", c as u8)), 275 | } 276 | } 277 | 278 | encoded 279 | } 280 | 281 | fn format_curl_and_untar_cmd(url: &str, bin_dir: &Path) -> String { 282 | format!( 283 | "{curl_cmd} | {untar_cmd}", 284 | curl_cmd = prepare_curl_bytes_cmd(url).formattable(), 285 | untar_cmd = prepare_untar_cmd(bin_dir).formattable(), 286 | ) 287 | } 288 | 289 | pub fn do_dry_run_curl( 290 | crate_details: &CrateDetails, 291 | fallback: bool, 292 | ) -> Result { 293 | let urls = get_quickinstall_download_urls(crate_details); 294 | 295 | let (url, res) = match curl_head(&urls[0]) { 296 | Err(err) if err.is_curl_404() => (&urls[1], curl_head(&urls[1])), 297 | res => (&urls[0], res), 298 | }; 299 | 300 | match res { 301 | Ok(_) => { 302 | let cargo_bin_dir = get_cargo_bin_dir()?; 303 | 304 | Ok(format_curl_and_untar_cmd(url, &cargo_bin_dir)) 305 | } 306 | Err(err) if err.is_curl_404() && fallback => { 307 | let cargo_install_cmd = prepare_cargo_install_cmd(crate_details); 308 | Ok(format!("{}", cargo_install_cmd.formattable())) 309 | } 310 | Err(err) => Err(err), 311 | } 312 | } 313 | 314 | fn untar(mut curl: ChildWithCommand) -> Result { 315 | let bin_dir = get_cargo_bin_dir()?; 316 | 317 | let res = prepare_untar_cmd(&bin_dir) 318 | .stdin(curl.stdout().take().unwrap()) 319 | .output_checked_status(); 320 | 321 | // Propagate this error before Propagating error tar since 322 | // if tar fails, it's likely due to curl failed. 323 | // 324 | // For example, this would enable the 404 error to be propagated 325 | // correctly. 326 | curl.wait_with_output_checked_status()?; 327 | 328 | let output = res?; 329 | 330 | let stdout = utf8_to_string_lossy(output.stdout); 331 | let stderr = String::from_utf8_lossy(&output.stderr); 332 | 333 | let mut s = stdout; 334 | s += &stderr; 335 | 336 | Ok(s) 337 | } 338 | 339 | fn prepare_curl_head_cmd(url: &str) -> std::process::Command { 340 | let mut cmd = prepare_curl_cmd(); 341 | cmd.arg("--head").arg(url); 342 | cmd 343 | } 344 | 345 | fn prepare_curl_post_cmd(url: &str) -> std::process::Command { 346 | let mut cmd = prepare_curl_cmd(); 347 | cmd.args(["-X", "POST"]).arg(url); 348 | cmd 349 | } 350 | 351 | fn curl_head(url: &str) -> Result, InstallError> { 352 | prepare_curl_head_cmd(url) 353 | .output_checked_status() 354 | .map(|output| output.stdout) 355 | } 356 | 357 | fn curl(url: &str) -> Result { 358 | let mut cmd = prepare_curl_bytes_cmd(url); 359 | cmd.stdin(process::Stdio::null()) 360 | .stdout(process::Stdio::piped()) 361 | .stderr(process::Stdio::piped()); 362 | cmd.spawn_with_cmd() 363 | } 364 | 365 | fn curl_file(url: &str, file: File) -> Result<(), InstallError> { 366 | prepare_curl_bytes_cmd(url) 367 | .stdin(process::Stdio::null()) 368 | .stdout(file) 369 | .stderr(process::Stdio::piped()) 370 | .output_checked_status()?; 371 | 372 | Ok(()) 373 | } 374 | 375 | fn curl_bytes(url: &str) -> Result, InstallError> { 376 | prepare_curl_bytes_cmd(url) 377 | .output_checked_status() 378 | .map(|output| output.stdout) 379 | } 380 | 381 | fn curl_string(url: &str) -> Result { 382 | curl_bytes(url).map(utf8_to_string_lossy) 383 | } 384 | 385 | pub fn curl_json(url: &str) -> Result { 386 | curl_string(url)? 387 | .parse() 388 | .map_err(|err| InstallError::InvalidJson { 389 | url: url.to_string(), 390 | err, 391 | }) 392 | } 393 | 394 | fn get_quickinstall_download_urls( 395 | CrateDetails { 396 | crate_name, 397 | version, 398 | target, 399 | }: &CrateDetails, 400 | ) -> [String; 2] { 401 | [format!( 402 | "https://github.com/cargo-bins/cargo-quickinstall/releases/download/{crate_name}-{version}/{crate_name}-{version}-{target}.tar.gz", 403 | ), 404 | format!( 405 | "https://github.com/cargo-bins/cargo-quickinstall/releases/download/{crate_name}-{version}-{target}/{crate_name}-{version}-{target}.tar.gz", 406 | )] 407 | } 408 | 409 | fn prepare_curl_cmd() -> std::process::Command { 410 | let mut cmd = std::process::Command::new("curl"); 411 | cmd.args([ 412 | "--user-agent", 413 | concat!( 414 | "cargo-quickinstall/", 415 | env!("CARGO_PKG_VERSION"), 416 | " client (alsuren@gmail.com)", 417 | ), 418 | "--location", 419 | "--silent", 420 | "--show-error", 421 | "--fail", 422 | ]); 423 | cmd 424 | } 425 | 426 | fn prepare_curl_bytes_cmd(url: &str) -> std::process::Command { 427 | let mut cmd = prepare_curl_cmd(); 428 | cmd.arg(url); 429 | cmd 430 | } 431 | 432 | fn prepare_untar_cmd(cargo_bin_dir: &Path) -> std::process::Command { 433 | let mut cmd = std::process::Command::new("tar"); 434 | cmd.arg("-xzvvf").arg("-").arg("-C").arg(cargo_bin_dir); 435 | cmd 436 | } 437 | 438 | fn prepare_cargo_install_cmd(details: &CrateDetails) -> std::process::Command { 439 | let mut cmd = std::process::Command::new("cargo"); 440 | cmd.arg("install") 441 | .arg(&details.crate_name) 442 | .arg("--version") 443 | .arg(&details.version); 444 | cmd 445 | } 446 | -------------------------------------------------------------------------------- /cargo-quickinstall/src/main.rs: -------------------------------------------------------------------------------- 1 | // cargo-quickinstall is optimized so that bootstrapping with 2 | // 3 | // cargo install cargo-quickinstall 4 | // 5 | // is quick. It's basically a glorified bash script. 6 | // 7 | // I suspect that there will be ways to clean this up without increasing 8 | // the bootstrapping time too much. Patches to do this would be very welcome. 9 | 10 | use cargo_quickinstall::install_error::*; 11 | use cargo_quickinstall::*; 12 | 13 | mod args; 14 | use args::Crate; 15 | 16 | fn main() -> Result<(), Box> { 17 | let options = args::options_from_cli_args(pico_args::Arguments::from_env())?; 18 | 19 | if options.print_version { 20 | println!( 21 | "`cargo quickinstall` version: {}", 22 | env!("CARGO_PKG_VERSION") 23 | ); 24 | return Ok(()); 25 | } 26 | 27 | if options.help { 28 | println!("{}", args::HELP); 29 | return Ok(()); 30 | } 31 | 32 | let crate_names = options.crate_names; 33 | 34 | if crate_names.is_empty() { 35 | Err(InstallError::MissingCrateNameArgument(args::USAGE))? 36 | } 37 | 38 | let target = options.target; 39 | 40 | let args = Args { 41 | dry_run: options.dry_run, 42 | try_upstream: options.try_upstream, 43 | fallback: options.fallback, 44 | force: options.force, 45 | }; 46 | 47 | let f = if options.no_binstall { 48 | do_main_curl 49 | } else { 50 | do_main_binstall 51 | }; 52 | 53 | f(crate_names, target, args) 54 | } 55 | 56 | #[derive(Default)] 57 | struct Args { 58 | dry_run: bool, 59 | try_upstream: bool, 60 | fallback: bool, 61 | force: bool, 62 | } 63 | 64 | fn do_main_curl( 65 | crates: Vec, 66 | target: Option, 67 | args: Args, 68 | ) -> Result<(), Box> { 69 | let target = match target { 70 | Some(target) => target, 71 | None => get_target_triple()?, 72 | }; 73 | 74 | for Crate { 75 | name: crate_name, 76 | version, 77 | } in crates 78 | { 79 | let version = match version { 80 | Some(version) => version, 81 | None => get_latest_version(&crate_name)?, 82 | }; 83 | 84 | let crate_details = CrateDetails { 85 | crate_name, 86 | version, 87 | target: target.clone(), 88 | }; 89 | 90 | if args.dry_run { 91 | let shell_cmd = do_dry_run_curl(&crate_details, args.fallback)?; 92 | println!("{}", shell_cmd); 93 | } else { 94 | let result = install_crate_curl(&crate_details, args.fallback); 95 | report_stats_in_background(&crate_details, &result); 96 | result?; 97 | } 98 | } 99 | 100 | Ok(()) 101 | } 102 | 103 | fn do_main_binstall( 104 | mut crates: Vec, 105 | target: Option, 106 | args: Args, 107 | ) -> Result<(), Box> { 108 | let is_binstall_compatible = get_cargo_binstall_version() 109 | .map( 110 | |binstall_version| match binstall_version.splitn(3, '.').collect::>()[..] { 111 | [major, minor, _] => { 112 | // If >=1.0.0 113 | major != "0" 114 | // Or >=0.17.0 115 | || minor 116 | .parse::() 117 | .map(|minor| minor >= 17) 118 | .unwrap_or(false) 119 | } 120 | _ => false, 121 | }, 122 | ) 123 | .unwrap_or(false); 124 | 125 | if !is_binstall_compatible { 126 | crates.retain(|crate_to_install| crate_to_install.name != "cargo-binstall"); 127 | 128 | let args = Args { 129 | dry_run: args.dry_run, 130 | try_upstream: true, 131 | fallback: false, 132 | force: true, 133 | }; 134 | 135 | download_and_install_binstall(args.dry_run)?; 136 | 137 | if args.dry_run { 138 | // cargo-binstall is not installed, so we print out the cargo-binstall 139 | // cmd and exit. 140 | println!("cargo binstall --no-confirm --force cargo-binstall"); 141 | return do_install_binstall(crates, target, BinstallMode::PrintCmd, args); 142 | } else { 143 | println!( 144 | "Bootstrapping cargo-binstall with itself to make `cargo uninstall cargo-binstall` work properly" 145 | ); 146 | do_install_binstall( 147 | vec![Crate { 148 | name: "cargo-binstall".to_string(), 149 | version: None, 150 | }], 151 | None, 152 | BinstallMode::Bootstrapping, 153 | args, 154 | )?; 155 | } 156 | } 157 | 158 | if crates.is_empty() { 159 | println!("No crate to install"); 160 | 161 | Ok(()) 162 | } else { 163 | do_install_binstall(crates, target, BinstallMode::Regular, args) 164 | } 165 | } 166 | 167 | fn download_and_install_binstall( 168 | dry_run: bool, 169 | ) -> Result<(), Box> { 170 | let target = get_target_triple()?; 171 | 172 | if dry_run { 173 | return match do_dry_run_download_and_install_binstall_from_upstream(&target) { 174 | Ok(shell_cmd) => { 175 | println!("{shell_cmd}"); 176 | Ok(()) 177 | } 178 | Err(err) if err.is_curl_404() => do_main_curl( 179 | vec![Crate { 180 | name: "cargo-binstall".to_string(), 181 | version: None, 182 | }], 183 | Some(target), 184 | Args { 185 | dry_run: true, 186 | ..Default::default() 187 | }, 188 | ), 189 | Err(err) => Err(err.into()), 190 | }; 191 | } 192 | 193 | match download_and_install_binstall_from_upstream(&target) { 194 | Err(err) if err.is_curl_404() => { 195 | println!( 196 | "Failed to install cargo-binstall from upstream, fallback to quickinstall: {err}" 197 | ); 198 | 199 | do_main_curl( 200 | vec![Crate { 201 | name: "cargo-binstall".to_string(), 202 | version: None, 203 | }], 204 | Some(target), 205 | Args::default(), 206 | ) 207 | } 208 | res => res.map_err(From::from), 209 | } 210 | } 211 | 212 | enum BinstallMode { 213 | Bootstrapping, 214 | Regular, 215 | PrintCmd, 216 | } 217 | 218 | fn do_install_binstall( 219 | crates: Vec, 220 | target: Option, 221 | mode: BinstallMode, 222 | args: Args, 223 | ) -> Result<(), Box> { 224 | let mut cmd = std::process::Command::new("cargo"); 225 | 226 | cmd.arg("binstall").arg("--no-confirm").arg("--no-symlinks"); 227 | 228 | if let Some(target) = target { 229 | cmd.arg("--targets").arg(target); 230 | } 231 | 232 | if args.force || matches!(mode, BinstallMode::Bootstrapping) { 233 | cmd.arg("--force"); 234 | } 235 | 236 | if args.dry_run || matches!(mode, BinstallMode::PrintCmd) { 237 | cmd.arg("--dry-run"); 238 | } 239 | 240 | if !args.try_upstream { 241 | cmd.args(["--disable-strategies", "crate-meta-data"]); 242 | } 243 | 244 | if !args.fallback { 245 | cmd.args(["--disable-strategies", "compile"]); 246 | } 247 | 248 | cmd.args(crates.into_iter().map(Crate::into_arg)); 249 | 250 | if matches!(mode, BinstallMode::PrintCmd) { 251 | println!("{}", cmd.formattable()); 252 | return Ok(()); 253 | } 254 | 255 | println!("Calling `cargo-binstall` to do the install"); 256 | 257 | #[cfg(unix)] 258 | if !matches!(mode, BinstallMode::Bootstrapping) { 259 | return Err(std::os::unix::process::CommandExt::exec(&mut cmd).into()); 260 | } 261 | 262 | let status = cmd.status()?; 263 | 264 | if !status.success() { 265 | Err(format!("`{}` failed with {status}", cmd.formattable(),).into()) 266 | } else { 267 | Ok(()) 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /cargo-quickinstall/src/utils.rs: -------------------------------------------------------------------------------- 1 | use home::cargo_home; 2 | use std::{io, path::PathBuf}; 3 | 4 | pub fn utf8_to_string_lossy(bytes: Vec) -> String { 5 | String::from_utf8(bytes) 6 | .unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned()) 7 | } 8 | 9 | pub fn get_cargo_bin_dir() -> io::Result { 10 | let mut cargo_bin_dir = cargo_home()?; 11 | cargo_bin_dir.push("bin"); 12 | Ok(cargo_bin_dir) 13 | } 14 | -------------------------------------------------------------------------------- /cargo-quickinstall/tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | use cargo_quickinstall::*; 2 | 3 | /// Tests installation of Ripgrep. 4 | /// 5 | /// A prebuilt package of Ripgrep version 13.0.0 for Linux x86_64 is known to 6 | /// exist. This should lead to a successful quickinstall. 7 | #[test] 8 | fn quickinstall_for_ripgrep() { 9 | let tmp_cargo_home_dir = mktemp::Temp::new_dir().unwrap(); 10 | 11 | let tmp_cargo_bin_dir = tmp_cargo_home_dir.as_path().join("bin"); 12 | std::fs::create_dir(&tmp_cargo_bin_dir).unwrap(); 13 | 14 | std::env::set_var("CARGO_HOME", tmp_cargo_home_dir.as_path()); 15 | 16 | let crate_details = CrateDetails { 17 | crate_name: "ripgrep".to_string(), 18 | version: "13.0.0".to_string(), 19 | target: get_target_triple().unwrap(), 20 | }; 21 | let do_not_fallback_on_cargo_install = false; 22 | 23 | let result = install_crate_curl(&crate_details, do_not_fallback_on_cargo_install); 24 | 25 | assert!(result.is_ok(), "{}", result.err().unwrap()); 26 | 27 | let mut ripgrep_path = tmp_cargo_bin_dir; 28 | ripgrep_path.push("rg"); 29 | 30 | assert!(ripgrep_path.is_file()); 31 | 32 | std::process::Command::new(ripgrep_path) 33 | .arg("-V") 34 | .output_checked_status() 35 | .unwrap(); 36 | } 37 | 38 | /// Tests dry run for Ripgrep. 39 | /// 40 | /// A prebuilt package of Ripgrep version 13.0.0 for Linux x86_64 is known to 41 | /// exist. 42 | #[test] 43 | fn do_dry_run_for_ripgrep() { 44 | let crate_details = CrateDetails { 45 | crate_name: "ripgrep".to_string(), 46 | version: "13.0.0".to_string(), 47 | target: "x86_64-unknown-linux-gnu".to_string(), 48 | }; 49 | 50 | let result = do_dry_run_curl(&crate_details, false).unwrap(); 51 | 52 | let expected_prefix = concat!("curl --user-agent \"cargo-quickinstall/", env!("CARGO_PKG_VERSION"), " client (alsuren@gmail.com)\" --location --silent --show-error --fail \"https://github.com/cargo-bins/cargo-quickinstall/releases/download/ripgrep-13.0.0/ripgrep-13.0.0-x86_64-unknown-linux-gnu.tar.gz\" | tar -xzvvf -"); 53 | assert_eq!(&result[..expected_prefix.len()], expected_prefix); 54 | } 55 | 56 | /// Tests dry run for a non-existent package. 57 | #[test] 58 | fn do_dry_run_for_nonexistent_package() { 59 | let crate_details = CrateDetails { 60 | crate_name: "nonexisting_crate_12345".to_string(), 61 | version: "99".to_string(), 62 | target: "unknown".to_string(), 63 | }; 64 | 65 | let result = do_dry_run_curl(&crate_details, true).unwrap(); 66 | 67 | let expected = "cargo install nonexisting_crate_12345 --version 99"; 68 | assert_eq!(expected, &result); 69 | } 70 | 71 | #[test] 72 | fn test_get_latest_version() { 73 | assert_eq!( 74 | get_latest_version("cargo-quickinstall").unwrap(), 75 | env!("CARGO_PKG_VERSION") 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /cargo-quickinstall/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 | -------------------------------------------------------------------------------- /create_and_upload_tag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | if [ $# != 1 ]; then 5 | echo Usage: "$0" tag 6 | fi 7 | 8 | tag="$1" 9 | 10 | git tag "$tag" 11 | 12 | exec git push origin "$1" 13 | -------------------------------------------------------------------------------- /cronjob_scripts/.python-version: -------------------------------------------------------------------------------- 1 | 3.8 2 | -------------------------------------------------------------------------------- /cronjob_scripts/README.md: -------------------------------------------------------------------------------- 1 | # Scripts 2 | 3 | This folder contains python scripts for use in cargo-quickinstall github actions for triggering builds. 4 | 5 | This code has some quite heavy python dependencies and strong assumptions about running on unix, so we should not use it on the github actions runners that actually do the package building. 6 | 7 | TODO: make a build_scripts/ folder and move all of the package building scripts into there (converting to python as desired). 8 | -------------------------------------------------------------------------------- /cronjob_scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cargo-bins/cargo-quickinstall/cbd39fccd90bb38f23c93e548ebd985efa5bfe3a/cronjob_scripts/__init__.py -------------------------------------------------------------------------------- /cronjob_scripts/architectures.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import subprocess 5 | 6 | 7 | TARGET_ARCH_TO_BUILD_OS = { 8 | "x86_64-apple-darwin": "macos-latest", 9 | "aarch64-apple-darwin": "macos-latest", 10 | "x86_64-unknown-linux-gnu": "ubuntu-20.04", 11 | "x86_64-unknown-linux-musl": "ubuntu-20.04", 12 | "x86_64-pc-windows-msvc": "windows-latest", 13 | "aarch64-pc-windows-msvc": "windows-latest", 14 | "aarch64-unknown-linux-gnu": "ubuntu-20.04", 15 | "aarch64-unknown-linux-musl": "ubuntu-20.04", 16 | "armv7-unknown-linux-musleabihf": "ubuntu-20.04", 17 | "armv7-unknown-linux-gnueabihf": "ubuntu-20.04", 18 | } 19 | 20 | 21 | def get_build_os(target_arch: str) -> str: 22 | try: 23 | return TARGET_ARCH_TO_BUILD_OS[target_arch] 24 | except KeyError: 25 | raise ValueError(f"Unrecognised target arch: {target_arch}") 26 | 27 | 28 | def get_target_architectures() -> list[str]: 29 | target_arch = os.environ.get("TARGET_ARCH", None) 30 | if target_arch in TARGET_ARCH_TO_BUILD_OS: 31 | return [target_arch] 32 | 33 | if target_arch == "all": 34 | return list(TARGET_ARCH_TO_BUILD_OS.keys()) 35 | 36 | rustc_version_output = subprocess.run( 37 | ["rustc", "--version", "--verbose"], capture_output=True, text=True 38 | ) 39 | 40 | assert rustc_version_output.returncode == 0, ( 41 | f"rustc --version --verbose failed: {rustc_version_output}" 42 | ) 43 | 44 | host_values = [ 45 | line.removeprefix("host: ") 46 | for line in rustc_version_output.stdout.splitlines() 47 | if line.startswith("host: ") 48 | ] 49 | assert len(host_values) == 1, ( 50 | f"rustc did not tell us its host, or told us multiple: {rustc_version_output}" 51 | ) 52 | 53 | return host_values 54 | -------------------------------------------------------------------------------- /cronjob_scripts/checkout_worktree.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from functools import lru_cache 4 | import subprocess 5 | 6 | 7 | @lru_cache 8 | def checkout_worktree_for_target(target_arch: str): 9 | """ 10 | Checkout a git worktree for the given target_arch, in /tmp. 11 | 12 | This is required for reading the exclude files. 13 | 14 | This is lifted directly from the old trigger-package-build.sh script, and is only expected to 15 | work on linux/macos with dash/bash. 16 | """ 17 | worktree_path = f"/tmp/cargo-quickinstall-{target_arch}" 18 | bash_script = f""" 19 | set -eux 20 | 21 | rm -rf {worktree_path} 22 | git worktree remove -f {worktree_path} || true 23 | git branch -D "trigger/{target_arch}" || true 24 | 25 | git worktree add --force --force {worktree_path} 26 | cd {worktree_path} 27 | 28 | if git fetch origin "trigger/{target_arch}"; then 29 | git checkout "origin/trigger/{target_arch}" -B "trigger/{target_arch}" 30 | elif ! git checkout "trigger/{target_arch}"; then 31 | # New branch with no history. Credit: https://stackoverflow.com/a/13969482 32 | git checkout --orphan "trigger/{target_arch}" 33 | git rm --cached -r . || true 34 | git commit -m "Initial Commit" --allow-empty 35 | git push origin "trigger/{target_arch}" 36 | fi 37 | """ 38 | subprocess.run(bash_script, shell=True, check=True, text=True) 39 | return worktree_path 40 | 41 | 42 | def main(): 43 | import sys 44 | 45 | if len(sys.argv) != 2: 46 | print(f"Usage: {sys.argv[0]} ") 47 | sys.exit(1) 48 | 49 | worktree_path = checkout_worktree_for_target(sys.argv[1]) 50 | print(f"checked out to {worktree_path}") 51 | 52 | 53 | if __name__ == "__main__": 54 | main() 55 | -------------------------------------------------------------------------------- /cronjob_scripts/crates_io_popular_crates.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import tarfile 4 | import warnings 5 | from pathlib import Path 6 | from typing import TypedDict 7 | 8 | import requests 9 | import polars as pl 10 | 11 | from cronjob_scripts.types import CrateAndMaybeVersion 12 | 13 | warnings.filterwarnings("ignore", message="Polars found a filename") 14 | 15 | 16 | class TarEntry: 17 | def __init__(self, tarball: tarfile.TarFile, member: tarfile.TarInfo): 18 | self.tarball = tarball 19 | self.member = member 20 | 21 | def is_one_of_csvs_interested(self, csvs_to_extract: list[str]) -> str | None: 22 | if not self.member.isfile(): 23 | return None 24 | for name in csvs_to_extract: 25 | if self.member.name.endswith(f"data/{name}"): 26 | return name 27 | return None 28 | 29 | def get_file_stream(self): 30 | stream = self.tarball.extractfile(self.member) 31 | assert stream 32 | return stream 33 | 34 | 35 | def download_tar_gz(url): 36 | response = requests.get(url, stream=True) 37 | response.raise_for_status() 38 | 39 | with tarfile.open(fileobj=response.raw, mode="r:gz") as tarball: 40 | for member in tarball: 41 | yield TarEntry(tarball, member) 42 | 43 | 44 | class Dfs(TypedDict): 45 | crate_downloads: pl.LazyFrame 46 | crates: pl.LazyFrame 47 | default_versions: pl.LazyFrame 48 | versions: pl.LazyFrame 49 | 50 | 51 | def get_dfs() -> Dfs: 52 | files_to_extract = [ 53 | "crate_downloads.csv", 54 | "crates.csv", 55 | "default_versions.csv", 56 | "versions.csv", 57 | ] 58 | files_extracted = 0 59 | dfs = {} 60 | for entry in download_tar_gz("https://static.crates.io/db-dump.tar.gz"): 61 | name = entry.is_one_of_csvs_interested(files_to_extract) 62 | if name: 63 | dfs[name[:-4]] = pl.scan_csv(entry.get_file_stream()) 64 | files_extracted += 1 65 | if files_extracted == len(files_to_extract): 66 | return Dfs(**dfs) 67 | 68 | raise RuntimeError(f"Failed to find all csvs {files_to_extract}") 69 | 70 | 71 | def get_crates_io_popular_crates_inner(minimum_downloads=200000): 72 | dfs = get_dfs() 73 | return ( 74 | dfs["crate_downloads"] 75 | .join(dfs["crates"].select("id", "name"), left_on="crate_id", right_on="id") 76 | .join(dfs["default_versions"], on="crate_id") 77 | .join( 78 | dfs["versions"].select("id", "crate_id", "yanked", "bin_names"), 79 | left_on=("crate_id", "version_id"), 80 | right_on=("crate_id", "id"), 81 | ) 82 | .filter(pl.col("bin_names") != "{}", pl.col("yanked") == "f") 83 | .filter(pl.col("downloads") > minimum_downloads) 84 | .select("name") 85 | ) 86 | 87 | 88 | def get_crates_io_popular_crates( 89 | minimum_downloads: int = 200000, 90 | ) -> list[CrateAndMaybeVersion]: 91 | cached_path = Path("cached_crates_io_popular_crates.parquet") 92 | if cached_path.is_file(): 93 | # TODO: Once `iter_rows()` can be used on LazyFrame, use it for better perf 94 | # and less ram usage. 95 | # https://github.com/pola-rs/polars/issues/10683 96 | df = pl.read_parquet(cached_path) 97 | else: 98 | df = ( 99 | get_crates_io_popular_crates_inner(minimum_downloads) 100 | # TODO: Use streaming, maybe use sink_parquet instead? 101 | # https://github.com/pola-rs/polars/issues/18684 102 | .collect() 103 | ) 104 | df.write_parquet(cached_path, statistics=False) 105 | 106 | df = df.rename({"name": "crate"}) 107 | return df.to_dicts() 108 | 109 | 110 | def main(): 111 | import sys 112 | 113 | if len(sys.argv) == 2 and sys.argv[1] in ("-h", "--help"): 114 | print(f"Usage: {sys.argv[0]} (minimum_downloads)") 115 | sys.exit(1) 116 | 117 | if len(sys.argv) > 1: 118 | minimum_downloads = int(sys.argv[1]) 119 | else: 120 | minimum_downloads = 200000 121 | print(list(get_crates_io_popular_crates(minimum_downloads))) 122 | 123 | 124 | if __name__ == "__main__": 125 | # Warning: it's best to use .venv/bin/crates-io-popular-crates rather than calling this directly, to avoid 126 | # sys.modules ending up with this dir at the front, shadowing stdlib modules. 127 | main() 128 | -------------------------------------------------------------------------------- /cronjob_scripts/find-common-failures.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import annotations 3 | 4 | import json 5 | import os 6 | import subprocess 7 | import sys 8 | from typing import TypedDict 9 | import hashlib 10 | 11 | 12 | import polars as pl 13 | 14 | 15 | # FIXME: add a max_age parameter to this decorator or something (by reading the mtime of the file)? 16 | def cache_on_disk_json(func): 17 | def wrapper(*args, **kwargs): 18 | # If the function's source code is updated, we should invalidate the cache. 19 | # In theory we could also hash all dependencies of the function by looking up each word of 20 | # source code in globals(), but that probably wouldn't be bullet-proof, and this is easy to 21 | # reason about. 22 | hasher = hashlib.sha256() 23 | hasher.update(func.__code__.co_code) 24 | impl_hash = hasher.hexdigest() 25 | 26 | cache_file = f"/tmp/{func.__name__}/{args}-{kwargs}-{impl_hash}.json" 27 | try: 28 | with open(cache_file) as f: 29 | result = json.load(f) 30 | if isinstance(result, str): 31 | with open(cache_file + ".txt", "w") as f: 32 | f.write(result.encode) 33 | return result 34 | except FileNotFoundError: 35 | pass 36 | except json.JSONDecodeError: 37 | print(f"WARNING: Failed to decode {cache_file}. Deleting.", file=sys.stderr) 38 | os.remove(cache_file) 39 | 40 | result = func(*args, **kwargs) 41 | os.makedirs(os.path.dirname(cache_file), exist_ok=True) 42 | with open(cache_file, "w") as f: 43 | json.dump(result, f) 44 | return result 45 | 46 | return wrapper 47 | 48 | 49 | def cache_on_disk_polars(func): 50 | def wrapper(*args, **kwargs): 51 | cache_file = f"/tmp/{func.__name__}/{args}-{kwargs}.parquet" 52 | try: 53 | # It might be possible to use scan_parquet here for added laziness 54 | return pl.read_parquet(cache_file) 55 | except FileNotFoundError: 56 | pass 57 | 58 | result = func(*args, **kwargs) 59 | assert isinstance(result, pl.DataFrame) 60 | 61 | os.makedirs(os.path.dirname(cache_file), exist_ok=True) 62 | result.write_parquet(cache_file) 63 | 64 | return result 65 | 66 | return wrapper 67 | 68 | 69 | class WorkflowRun(TypedDict): 70 | attempt: int 71 | conclusion: str 72 | createdAt: str 73 | databaseId: str 74 | # displayTitle: str 75 | # event: str 76 | # headBranch: str 77 | # headSha: str 78 | # name: str 79 | # number: int 80 | # startedAt: str 81 | status: str 82 | # updatedAt: str 83 | url: str 84 | # workflowDatabaseId: int 85 | # workflowName: str 86 | 87 | 88 | # FIXME: remove this cache when we're done with messing about? 89 | @cache_on_disk_json 90 | def get_runs(limit) -> list[WorkflowRun]: 91 | output = subprocess.run( 92 | [ 93 | "gh", 94 | "run", 95 | "list", 96 | f"--limit={limit}", 97 | f"--json={','.join(WorkflowRun.__annotations__.keys())}", 98 | ], 99 | capture_output=True, 100 | check=True, 101 | ) 102 | return json.loads(output.stdout) 103 | 104 | 105 | def get_failing_runs(limit: int) -> list[WorkflowRun]: 106 | runs = get_runs(limit=limit) 107 | 108 | return [ 109 | run 110 | for run in runs 111 | if run["status"] == "completed" and run["conclusion"] == "failure" 112 | ] 113 | 114 | 115 | @cache_on_disk_polars 116 | def get_logs(databaseId: str) -> pl.DataFrame: 117 | output = subprocess.run( 118 | ["gh", "run", "view", str(databaseId), "--log"], 119 | capture_output=True, 120 | check=True, 121 | ) 122 | 123 | # e.g. build-popular-package-webdriver-install-0.3.2-aarch64-unknown-linux-musl-\tSet up job\t2024-09-11T09:00:33.2327586Z Current runner version: '2.319.1' 124 | result = pl.read_csv( 125 | output.stdout, 126 | separator="\t", 127 | has_header=False, 128 | schema=pl.Schema( 129 | { 130 | "job": pl.String, 131 | "step": pl.String, 132 | "timestamp_and_message": pl.String, 133 | } 134 | ), 135 | # FIXME: some log lines legitimately contain tabs. Can we include them in timestamp_and_message? 136 | truncate_ragged_lines=True, 137 | ) 138 | return result 139 | 140 | 141 | def tidy_logs(logs: pl.DataFrame) -> pl.DataFrame: 142 | # split timestamp_and_message into timestamp and message 143 | logs = logs.with_columns( 144 | logs["job"].str.replace("^build-popular-package-(.*)-$", "${1}").alias("job"), 145 | # in theory we could parse the timestamp, but I don't think we actually need it right now. 146 | # logs["timestamp_and_message"].str.replace(" .*", "").alias("timestamp"), 147 | logs["timestamp_and_message"] 148 | .str.replace("^[^ ]* ", "") 149 | .str.replace( 150 | "^error: failed to run custom build command for `(.*) v.*`$", 151 | "error: failed to run custom build command for `${1} `", 152 | ) 153 | # rewrite some common errors that show up as a result of other errors in the same build 154 | .str.replace( 155 | "^error: could not compile `.*` \(bin .*\) due to .* previous error", 156 | "", 157 | ) 158 | .str.replace( 159 | "^error: failed to compile `.*`, intermediate artifacts can be found at.*", 160 | "", 161 | ) 162 | .str.replace( 163 | "^warning: build failed, waiting for other jobs to finish...", 164 | "", 165 | ) 166 | .alias("message"), 167 | ) 168 | logs.drop_in_place("timestamp_and_message") 169 | return logs 170 | 171 | 172 | def get_unique_errors(logs: pl.DataFrame) -> pl.DataFrame: 173 | errors = logs.filter(logs["message"].str.starts_with("error: ")).unique() 174 | 175 | if errors.shape[0] > 0: 176 | return errors 177 | 178 | warnings = logs.filter( 179 | ( 180 | logs["message"].str.starts_with("warning: ") 181 | & ~logs["message"].str.starts_with( 182 | "warning: no Cargo.lock file published in " 183 | ) 184 | | logs["message"].str.contains( 185 | "failed to select a version for the requirement" 186 | ) 187 | ) 188 | ).unique() 189 | 190 | if warnings.shape[0] > 0: 191 | return warnings 192 | 193 | errors = logs.filter(logs["message"].str.contains("^error(\[E[0-9]*\])?: ")).head() 194 | return errors 195 | 196 | 197 | def df_to_markdown(df: pl.DataFrame) -> str: 198 | """Quick and dirty ChatGPT implementation, because polars doesn't have a to_markdown method.""" 199 | # Get column names 200 | headers = df.columns 201 | 202 | # Create the header row in markdown format 203 | header_row = "| " + " | ".join(headers) + " |" 204 | 205 | # Create the separator row 206 | separator_row = "| " + " | ".join(["---"] * len(headers)) + " |" 207 | 208 | # Create the data rows 209 | data_rows = ["| " + " | ".join(map(str, row)) + " |" for row in df.to_numpy()] 210 | 211 | # Combine all rows 212 | markdown_table = "\n".join([header_row, separator_row] + data_rows) 213 | 214 | return markdown_table 215 | 216 | 217 | def main(): 218 | pl.Config.set_fmt_str_lengths(1000).set_tbl_rows(1000) 219 | failing_runs = get_failing_runs(limit=100) 220 | print("failing run count:", len(failing_runs), file=sys.stderr) 221 | 222 | all_errors = None 223 | for run in failing_runs: 224 | logs = get_logs(run["databaseId"]) 225 | logs = logs.with_columns(pl.lit(run["url"]).alias("url")) 226 | logs = tidy_logs(logs) 227 | 228 | errors = get_unique_errors(logs) 229 | if errors.shape[0] == 0: 230 | print( 231 | "WARNING: No errors found in logs, even though the run failed:", 232 | run["url"], 233 | file=sys.stderr, 234 | ) 235 | all_errors = errors if all_errors is None else all_errors.extend(errors) 236 | 237 | result = ( 238 | all_errors.group_by("message") 239 | .agg( 240 | [ 241 | pl.len().alias("count"), 242 | pl.col("job").first().alias("example_job"), 243 | # Annoyingly, this gives you a link to the top level of the run, and you have to 244 | # click through to the job. I think that we could work at the job level by using 245 | # the github api, but I started using the gh cli, so I'll leave this as an exercise 246 | # for the reader. 247 | pl.col("url").first().alias("example_url"), 248 | ] 249 | ) 250 | .sort("count", descending=True) 251 | ) 252 | print(df_to_markdown(result)) 253 | 254 | 255 | if __name__ == "__main__": 256 | if len(sys.argv) > 1: 257 | # yeah, I should probably have done this as a jupyter notebook, but nevermind. 258 | print( 259 | f""" 260 | USAGE: {sys.argv[0]} > /tmp/fail.md && code /tmp/fail. 261 | 262 | Then use vscode's preview mode to view the result. 263 | """, 264 | file=sys.stderr, 265 | ) 266 | exit(1) 267 | main() 268 | -------------------------------------------------------------------------------- /cronjob_scripts/get_latest_version.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import functools 5 | from typing import TypedDict 6 | import requests 7 | 8 | import semver 9 | 10 | 11 | class CrateVersionDict(TypedDict): 12 | """ 13 | A returned row from the crates.io index API. 14 | 15 | Note that the returned row also includes all of the fields from the jsonschema at 16 | https://doc.rust-lang.org/cargo/reference/registry-index.html#json-schema 17 | but I can't be bothered typing them right now. 18 | """ 19 | 20 | name: str 21 | vers: str 22 | features: dict[str, list[str]] 23 | 24 | 25 | def get_index_url(crate: str): 26 | """ 27 | Packages with 1 character names are placed in a directory named 1. 28 | Packages with 2 character names are placed in a directory named 2. 29 | Packages with 3 character names are placed in the directory 3/{first-character} where {first-character} is the first character of the package name. 30 | All other packages are stored in directories named {first-two}/{second-two} where the top directory is the first two characters of the package name, and the next subdirectory is the third and fourth characters of the package name. For example, cargo would be stored in a file named ca/rg/cargo. 31 | -- https://doc.rust-lang.org/cargo/reference/registry-index.html#index-files 32 | """ 33 | if len(crate) == 0: 34 | raise ValueError("Empty crate name") 35 | if len(crate) == 1: 36 | return f"https://index.crates.io/1/{crate}" 37 | elif len(crate) == 2: 38 | return f"https://index.crates.io/2/{crate}" 39 | elif len(crate) == 3: 40 | return f"https://index.crates.io/3/{crate[0]}/{crate}" 41 | else: 42 | return f"https://index.crates.io/{crate[:2]}/{crate[2:4]}/{crate}" 43 | 44 | 45 | @functools.lru_cache 46 | def get_latest_version( 47 | crate: str, requested_version: str | None 48 | ) -> CrateVersionDict | None: 49 | """ 50 | Calls the crates.io index API to get the latest version of the given crate. 51 | 52 | There is no rate limit on this api, so we can call it as much as we like. 53 | """ 54 | url = get_index_url(crate) 55 | 56 | response = requests.get(url) 57 | if response.status_code == 404: 58 | print(f"No crate named {crate}") 59 | return None 60 | response.raise_for_status() 61 | 62 | max_version: CrateVersionDict | None = None 63 | max_parsed_version: semver.VersionInfo | None = None 64 | for line in response.text.splitlines(): 65 | version = json.loads(line) 66 | if requested_version is not None and version["vers"] != requested_version: 67 | continue 68 | parsed_version = semver.VersionInfo.parse(version["vers"]) 69 | if version["yanked"] or parsed_version.prerelease: 70 | continue 71 | 72 | if max_parsed_version is None or parsed_version > max_parsed_version: 73 | max_version = version 74 | max_parsed_version = parsed_version 75 | 76 | return max_version 77 | 78 | 79 | if __name__ == "__main__": 80 | import sys 81 | 82 | if len(sys.argv) != 2: 83 | print(f"Usage: {sys.argv[0]} ") 84 | sys.exit(1) 85 | 86 | version = get_latest_version(sys.argv[1]) 87 | if version is not None: 88 | print(version["vers"]) 89 | -------------------------------------------------------------------------------- /cronjob_scripts/stats.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from polars import DataFrame 4 | 5 | from functools import lru_cache 6 | import os 7 | 8 | from influxdb_client_3 import InfluxDBClient3 9 | 10 | from cronjob_scripts.types import CrateAndVersion 11 | 12 | TOKEN = os.environ.get("INFLUXDB_TOKEN") 13 | ORG = "cargo-bins" 14 | HOST = "https://us-east-1-1.aws.cloud2.influxdata.com" 15 | DATABASE = "cargo-quickinstall" 16 | 17 | _influxdb_client = None 18 | 19 | 20 | @lru_cache 21 | def get_stats(period: str) -> DataFrame: 22 | """ 23 | To reduce the number of round-trips to the InfluxDB server, we do a single query and cache the 24 | results of this function. 25 | 26 | In theory it would be better if we could use a dataloader-like pattern to batch up requests and 27 | ask for exactly the architectures we want, but that would require us to make the whole script 28 | async, and we know that cronjob will eventually ask for all architectures anyway. 29 | """ 30 | global _influxdb_client 31 | if _influxdb_client is None: 32 | _influxdb_client = InfluxDBClient3( 33 | host=HOST, token=TOKEN, org=ORG, database=DATABASE 34 | ) 35 | 36 | # Old clients (going via the old stats server) don't report status, so we will end up triggering 37 | # re-checks even for things successfully installed from tarball. Hopefully these will gradually 38 | # get phased out and then this method will only return crate versions which ended up installing 39 | # from source. 40 | # 41 | # FIXME: seems like there are some entries of the form: 42 | # {"agent": "binstalk-fetchers/0.10.0", "status": "start", ... } 43 | # Apparently binswap-github is using an old version of binstalk. See: 44 | # https://github.com/cargo-bins/cargo-quickinstall/pull/300/files#r1778063083 45 | query = """ 46 | SELECT DISTINCT crate, target, version, status 47 | FROM "counts" 48 | WHERE 49 | time >= now() - $period::interval and time <= now() 50 | """ 51 | 52 | table: DataFrame = _influxdb_client.query( # type: ignore 53 | query=query, 54 | language="sql", 55 | query_parameters={ 56 | "period": period, 57 | }, 58 | mode="polars", 59 | ) 60 | 61 | return table 62 | 63 | 64 | def get_requested_crates( 65 | period: str, target: str | None, statuses: list[str] | None = None 66 | ) -> list[CrateAndVersion]: 67 | df = get_stats(period=period) 68 | 69 | if target is not None: 70 | df = df.filter(df["target"] == target) 71 | 72 | if statuses: 73 | df = df.filter(df["status"].is_in(statuses)) 74 | 75 | return df[["crate", "version"]].unique().to_dicts() # type: ignore 76 | 77 | 78 | def main(): 79 | table = get_stats(period="1 day") 80 | for crate in table["crate"].unique(): 81 | print(crate) 82 | 83 | 84 | if __name__ == "__main__": 85 | # Warning: it's best to use .venv/bin/stats rather than calling this directly, to avoid 86 | # sys.modules ending up with this dir at the front, shadowing stdlib modules. 87 | main() 88 | -------------------------------------------------------------------------------- /cronjob_scripts/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cargo-bins/cargo-quickinstall/cbd39fccd90bb38f23c93e548ebd985efa5bfe3a/cronjob_scripts/tests/__init__.py -------------------------------------------------------------------------------- /cronjob_scripts/tests/test_architectures.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import unittest 3 | 4 | 5 | from cronjob_scripts.architectures import TARGET_ARCH_TO_BUILD_OS 6 | 7 | 8 | class TestMyScript(unittest.TestCase): 9 | def test_supported_targets(self): 10 | with open( 11 | Path(__file__).resolve().parent.parent.parent.joinpath("supported-targets"), 12 | "r", 13 | ) as file: 14 | supported_targets = [word for word in file.read().split() if word] 15 | 16 | assert set(TARGET_ARCH_TO_BUILD_OS.keys()) == set(supported_targets), ( 17 | "please keep /supported-targets and TARGET_ARCH_TO_BUILD_OS in sync" 18 | ) 19 | -------------------------------------------------------------------------------- /cronjob_scripts/trigger_package_build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import annotations 4 | 5 | 6 | from collections import Counter 7 | from dataclasses import dataclass 8 | from functools import lru_cache 9 | import json 10 | import os 11 | from pathlib import Path 12 | import random 13 | import subprocess 14 | import sys 15 | import time 16 | 17 | from cronjob_scripts.types import CrateAndMaybeVersion, CrateAndVersion, GithubAsset 18 | from cronjob_scripts.architectures import get_build_os, get_target_architectures 19 | from cronjob_scripts.checkout_worktree import checkout_worktree_for_target 20 | from cronjob_scripts.get_latest_version import CrateVersionDict, get_latest_version 21 | from cronjob_scripts.stats import get_requested_crates 22 | from cronjob_scripts.crates_io_popular_crates import get_crates_io_popular_crates 23 | 24 | MAX_CHECKS_PER_QUEUE = 1000 25 | 26 | 27 | @dataclass 28 | class QueueInfo: 29 | type: str 30 | target: str 31 | queue: list[CrateAndMaybeVersion] 32 | 33 | 34 | def main(): 35 | """ 36 | Triggers a package build in a vaguely fair way. 37 | 38 | General approach: 39 | * construct the following list of crates (n lists for each target): 40 | * for each target: 41 | * TODO (probably never): the list of install requests with the "built-from-source" or 42 | "not-found" status in the last day (ordered by popularity, including versions. 43 | WARNING: this is very likely to result in the same crate being built many times in 44 | parallel if someone runs the cronjob manually on ci/locally) 45 | * the list of install requests with the "built-from-source" or 46 | "not-found" status in the last day (shuffled, including versions) 47 | * the list of install requests with any other status in the last day (shuffled, with 48 | versions stripped) 49 | * popular-crates.txt (shuffled) 50 | * a list with just cargo-quickinstall and nothing else (if doing the weekly recheck job) 51 | * shuffle the list of lists and then round-robin between each list: 52 | * take the head of the list 53 | * if we already checked this package for this target, skip it 54 | * if we already tried to build this package 5 times in the last 7 days, skip it 55 | (TODO: also record the version in the failure log?) 56 | * check its latest version + if we have a package built for it. If we don't have a package 57 | built for it, trigger a build 58 | * if we already triggered m package builds since the start of the run, exit (probably 59 | set this to n times the number of targets?) 60 | * if we hit a github rate limit when doing this, this is fine and we just bomb out having made some progress? 61 | * if we get to the end without triggering any builds then that's fine too: there's nothing to do. 62 | 63 | Shuffling the lists means that: 64 | * we will at least make some progress fairly on all architectures even if something goes wrong or we hit rate limits 65 | * we don't need to track any state to avoid head-of-line blocking (the "trigger/*" branches used 66 | to track where we got to for each target for fairness, but shuffled builds are simpler) 67 | """ 68 | if len(sys.argv) > 1: 69 | print( 70 | "Usage: INFLUXDB_TOKEN= target= CRATE_CHECK_LIMIT= RECHECK= GITHUB_REPOSITORY= trigger-package-build.py" 71 | ) 72 | if sys.argv[1] == "--help": 73 | exit(0) 74 | exit(1) 75 | 76 | targets = get_target_architectures() 77 | queues: list[QueueInfo] = [] 78 | 79 | # we shuffle the list of popular crates from crates.io once, and use it for every arch, to slightly 80 | # reduce the number of requests we need to make to crates.io. It probably isn't worth it though. 81 | popular_crates = get_crates_io_popular_crates() 82 | random.shuffle(popular_crates) 83 | 84 | for target in targets: 85 | recheck_crate = os.environ.get("RECHECK_ONLY") 86 | if recheck_crate: 87 | queues.append( 88 | QueueInfo( 89 | type="recheck", 90 | target=target, 91 | queue=[CrateAndMaybeVersion(crate=recheck_crate, version=None)], 92 | ) 93 | ) 94 | continue 95 | 96 | tracking_worktree_path = checkout_worktree_for_target(target) 97 | excluded = get_excluded(tracking_worktree_path, days=7, max_failures=5) 98 | 99 | # This should be a small list of crates, so we should get around to checking everything here 100 | # pretty often. This doesn't include failures from old clients (via the old stats server) 101 | # because they don't report status. 102 | failed = get_requested_crates( 103 | period="1 day", target=target, statuses=["built-from-source", "not-found"] 104 | ) 105 | failed = without_excluded(failed, excluded) 106 | random.shuffle(failed) 107 | queues.append(QueueInfo(type="failed", target=target, queue=failed)) 108 | 109 | # This will be a large list of crates, so it might take a while to get around to checking 110 | # everything here. 111 | all_requested = get_requested_crates(period="1 day", target=target) 112 | all_requested = without_excluded(all_requested, excluded) 113 | random.shuffle(all_requested) 114 | queues.append(QueueInfo(type="requested", target=target, queue=all_requested)) 115 | 116 | # This is also large, and will take a while to check. 117 | queues.append( 118 | QueueInfo( 119 | type="popular", 120 | target=target, 121 | queue=without_excluded(popular_crates, excluded), 122 | ) 123 | ) 124 | 125 | print("Queues:") 126 | for queue in queues: 127 | print(f"{queue.target} {queue.type}: {len(queue.queue)} items") 128 | random.shuffle(queues) 129 | 130 | max_index = min(max(len(q.queue) for q in queues), MAX_CHECKS_PER_QUEUE) 131 | max_triggers = 2 * len(targets) 132 | print( 133 | f"Checking {max_index} crates per queue ({len(queues)} queues), to trigger at most {max_triggers} builds" 134 | ) 135 | triggered_count = 0 136 | for index in range(max_index + 1): 137 | for queue in queues: 138 | triggered = trigger_for_target(queue, index) 139 | time.sleep(1) 140 | if triggered: 141 | triggered_count += 1 142 | if triggered_count >= max_triggers: 143 | print(f"Triggered enough builds ({triggered_count}), exiting") 144 | return 145 | print( 146 | f"Checked {max_index} crates per queue ({len(queues)} queues) and triggered {triggered_count} builds, exiting" 147 | ) 148 | 149 | 150 | def trigger_for_target(queue: QueueInfo, index: int) -> bool: 151 | if len(queue.queue) <= index: 152 | return False 153 | crate_and_maybe_version = queue.queue[index] 154 | print( 155 | f"Checking build for {queue.target} '{queue.type}': {crate_and_maybe_version}" 156 | ) 157 | crate = crate_and_maybe_version["crate"] 158 | requested_version = crate_and_maybe_version.get("version") 159 | 160 | print(f"Checking {crate} {requested_version} for {queue.target}") 161 | repo_url = get_repo_url() 162 | version = get_current_version_if_unbuilt( 163 | repo_url=repo_url, 164 | crate=crate, 165 | target=queue.target, 166 | requested_version=requested_version, 167 | ) 168 | if not version: 169 | return False 170 | 171 | no_default_features = "false" 172 | if crate == "gitoxide": 173 | features = "max-pure" 174 | no_default_features = "true" 175 | elif crate == "sccache": 176 | features = "vendored-openssl" 177 | else: 178 | features = ",".join( 179 | feat 180 | for feat in version["features"].keys() 181 | if "vendored" in feat or "bundled" in feat 182 | ) 183 | build_os = get_build_os(queue.target) 184 | branch = get_branch() 185 | workflow_run_input = { 186 | "crate": crate, 187 | "version": version["vers"], 188 | "target_arch": queue.target, 189 | "features": features, 190 | "no_default_features": no_default_features, 191 | "build_os": build_os, 192 | "branch": branch, 193 | } 194 | print(f"Triggering build of {crate} {version['vers']} for {queue.target}") 195 | subprocess.run( 196 | ["gh", "workflow", "run", "build-package.yml", "--json", f"--ref={branch}"], 197 | input=json.dumps(workflow_run_input).encode(), 198 | check=True, 199 | ) 200 | return True 201 | 202 | 203 | def without_excluded( 204 | queue: list[CrateAndMaybeVersion] | list[CrateAndVersion], excluded: set[str] 205 | ) -> list[CrateAndMaybeVersion]: 206 | return [c for c in queue if c["crate"] not in excluded] # type: ignore - sneaky downcast 207 | 208 | 209 | @lru_cache 210 | def get_excluded(tracking_worktree_path: str, days: int, max_failures: int) -> set[str]: 211 | """ 212 | if a crate has reached `max_failures` failures in the last `days` days then we exclude it 213 | """ 214 | failures: Counter[str] = Counter() 215 | 216 | failure_filenames = list(Path(tracking_worktree_path).glob("*-*-*")) 217 | failure_filenames.sort(reverse=True) 218 | 219 | for failure_filename in failure_filenames[:days]: 220 | with open(failure_filename, "r") as file: 221 | failures.update( 222 | Counter(stripped for line in file if (stripped := line.strip())) 223 | ) 224 | 225 | return set(crate for crate, count in failures.items() if count >= max_failures) 226 | 227 | 228 | @lru_cache 229 | def get_repo_url() -> str: 230 | repo_env_var = os.environ.get("GITHUB_REPOSITORY") 231 | if repo_env_var: 232 | return f"https://github.com/{repo_env_var}" 233 | return subprocess.run( 234 | ["gh", "repo", "view", "--json", "url", "--jq", ".url"], 235 | capture_output=True, 236 | text=True, 237 | ).stdout.strip() 238 | 239 | 240 | def get_branch() -> str: 241 | return subprocess.run( 242 | ["git", "branch", "--show-current"], 243 | capture_output=True, 244 | text=True, 245 | ).stdout.strip() 246 | 247 | 248 | def get_current_version_if_unbuilt( 249 | repo_url: str, 250 | crate: str, 251 | target: str, 252 | requested_version: str | None = None, 253 | ) -> CrateVersionDict | None: 254 | # FIXME(probably never): in theory we could skip a round-trip to crates.io if we already know the version, 255 | # but the code ends up looking ugly so I decided not to bother. 256 | version = get_latest_version(crate, requested_version) 257 | if not version: 258 | return None 259 | 260 | tag = f"{crate}-{version['vers']}" 261 | tarball = f"{crate}-{version['vers']}-{target}.tar.gz" 262 | 263 | if any(asset["name"] == tarball for asset in get_assets_for_tag(tag)): 264 | print(f"{tarball} already uploaded") 265 | return None 266 | 267 | return version 268 | 269 | 270 | @lru_cache 271 | def get_assets_for_tag(tag: str) -> list[GithubAsset]: 272 | try: 273 | out = subprocess.check_output( 274 | ["gh", "release", "view", tag, "--json=assets"], 275 | stderr=subprocess.PIPE, 276 | ) 277 | except subprocess.CalledProcessError as e: 278 | if b"release not found" in e.stderr: 279 | return [] 280 | print(e.stderr, file=sys.stderr) 281 | raise 282 | parsed = json.loads(out) 283 | return parsed["assets"] 284 | 285 | 286 | if __name__ == "__main__": 287 | main() 288 | -------------------------------------------------------------------------------- /cronjob_scripts/types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TypedDict 4 | 5 | 6 | class CrateAndVersion(TypedDict): 7 | """May also be a Struct with the same fields, which would be hashable. Don't tell anyone.""" 8 | 9 | crate: str 10 | version: str 11 | 12 | 13 | class CrateAndMaybeVersion(TypedDict): 14 | """May also be a Struct with the same fields, which would be hashable. Don't tell anyone.""" 15 | 16 | crate: str 17 | version: str | None 18 | 19 | 20 | class GithubAsset(TypedDict): 21 | """ 22 | an element in the output of `gh release view $tag --json assets` 23 | 24 | Also contains a bunch of other fields but I don't care right now. 25 | """ 26 | 27 | name: str 28 | -------------------------------------------------------------------------------- /get-popular-crates.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | which htmlq || ( 6 | echo "htmlq can be installed using cargo-binstall" 7 | exit 1 8 | ) 9 | 10 | function get_top() { 11 | curl --fail -L "https://lib.rs/$1" | 12 | htmlq ':parent-of(:parent-of(:parent-of(.bin))) json{}' | 13 | jq -r '.[] | 14 | (.children[1].children|map(select(.class == "downloads").title)[0]// "0 ") 15 | + ":" + 16 | (.children[0].children[0].text)' | 17 | sort -gr | 18 | grep -v / | 19 | grep -v ^0 | 20 | head -n 100 | 21 | tee /dev/fd/2 | # debugging goes to stderr 22 | sed s/^.*:// 23 | 24 | echo "done with $1" 1>&2 25 | } 26 | 27 | ( 28 | get_top command-line-utilities 29 | get_top development-tools/cargo-plugins 30 | ) | sort 31 | -------------------------------------------------------------------------------- /get-repo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | if [ -z "${GITHUB_REPOSITORY+x}" ]; then 5 | gh repo view --json url --jq '.url' 6 | else 7 | echo "https://github.com/$GITHUB_REPOSITORY" 8 | fi 9 | -------------------------------------------------------------------------------- /minisign.pub: -------------------------------------------------------------------------------- 1 | untrusted comment: minisign public key 6B2490DA9B769EDD 2 | RWTdnnab2pAka9OdwgCMYyOE66M/BlQoFWaJ/JjwcPV+f3n24IRTj97t 3 | -------------------------------------------------------------------------------- /pkg-config-cross.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | echo "$0 is invoked with args" "${@:-}" >&2 5 | echo Since we are doing cross compilation, we cannot use any locally installed lib >&2 6 | echo Return 1 to signal error >&2 7 | 8 | exit 1 9 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "cronjob-scripts" 3 | version = "0.0.0" 4 | description = "Scripts for use in cargo-quickinstall github actions" 5 | readme = "README.md" 6 | # This is what ubuntu-latest has installed by default 7 | requires-python = ">=3.8" 8 | # Note that we don't attempt to do any locking of python deps 9 | dependencies = [ 10 | "influxdb3-python>=0.8.0", 11 | "requests>=2.32.3", 12 | "semver>=3.0.2", 13 | "polars>=1.7.0", 14 | "ruff>=0.7.2", 15 | ] 16 | 17 | [tool.setuptools] 18 | py-modules = ["cronjob_scripts"] 19 | 20 | [project.scripts] 21 | trigger-package-build = "cronjob_scripts.trigger_package_build:main" 22 | crates-io-popular-crates = "cronjob_scripts.crates_io_popular_crates:main" 23 | stats = "cronjob_scripts.stats:main" 24 | 25 | [build-system] 26 | requires = ["hatchling"] 27 | build-backend = "hatchling.build" 28 | -------------------------------------------------------------------------------- /rebuild-popular-crates.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script rebuilds the popular-crates.txt file from lib.rs 4 | # In theory we could use the crates.io db dump. 5 | # See https://github.com/cargo-bins/cargo-quickinstall/issues/268#issuecomment-2329308074 6 | # 7 | # get-popular-crates.sh also seems to re-implement the functionality of this script. 8 | # 9 | # pup can be installed via: go get github.com/ericchiang/pup 10 | # uq can be installed using cargo-quickinstall 11 | 12 | set -euo pipefail 13 | 14 | which pup || ( 15 | echo "pup can be installed via: go get github.com/ericchiang/pup" 16 | exit 1 17 | ) 18 | which uq || ( 19 | echo "uq can be installed using cargo-quickinstall" 20 | exit 1 21 | ) 22 | 23 | function get_top() { 24 | curl --fail "https://lib.rs/$1" | 25 | pup ':parent-of(:parent-of(:parent-of(.bin))) json{}' | 26 | jq -r '.[] | 27 | (.children[1].children|map(select(.class == "downloads").title)[0]// "0 ") 28 | + ":" + 29 | (.children[0].children[0].text)' | 30 | sort -gr | 31 | grep -v / | 32 | grep -v ^0 | 33 | head -n 100 | 34 | tee /dev/fd/2 | # debugging goes to stderr 35 | sed s/^.*:// 36 | 37 | echo "done with $1" 1>&2 38 | } 39 | 40 | function get_top_both() { 41 | ( 42 | get_top command-line-utilities 43 | get_top development-tools/cargo-plugins 44 | ) | sort 45 | } 46 | 47 | function get_new_file_contents() { 48 | ( 49 | grep -B10000 '####################################' popular-crates.txt 50 | get_top_both 51 | ) | uq 52 | } 53 | 54 | get_new_file_contents >popular-crates.txt.new 55 | mv popular-crates.txt.new popular-crates.txt 56 | 57 | echo "popular-crates.txt has been rebuilt. Please check the changes into git" 58 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv pip compile pyproject.toml --python-version=3.8 --output-file requirements.txt 3 | certifi==2024.8.30 4 | # via 5 | # influxdb3-python 6 | # requests 7 | charset-normalizer==3.4.0 8 | # via requests 9 | idna==3.10 10 | # via requests 11 | influxdb3-python==0.9.0 12 | # via cronjob-scripts (pyproject.toml) 13 | numpy==1.24.4 14 | # via pyarrow 15 | polars==1.8.2 16 | # via cronjob-scripts (pyproject.toml) 17 | pyarrow==17.0.0 18 | # via influxdb3-python 19 | python-dateutil==2.9.0.post0 20 | # via influxdb3-python 21 | reactivex==4.0.4 22 | # via influxdb3-python 23 | requests==2.32.3 24 | # via cronjob-scripts (pyproject.toml) 25 | semver==3.0.2 26 | # via cronjob-scripts (pyproject.toml) 27 | setuptools==75.3.0 28 | # via influxdb3-python 29 | six==1.16.0 30 | # via python-dateutil 31 | typing-extensions==4.12.2 32 | # via reactivex 33 | urllib3==2.2.3 34 | # via 35 | # influxdb3-python 36 | # requests 37 | -------------------------------------------------------------------------------- /stats-server/.dockerignore: -------------------------------------------------------------------------------- 1 | /target -------------------------------------------------------------------------------- /stats-server/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /stats-server/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.22.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "async-channel" 22 | version = "1.9.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" 25 | dependencies = [ 26 | "concurrent-queue", 27 | "event-listener", 28 | "futures-core", 29 | ] 30 | 31 | [[package]] 32 | name = "autocfg" 33 | version = "1.3.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 36 | 37 | [[package]] 38 | name = "axum" 39 | version = "0.8.4" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" 42 | dependencies = [ 43 | "axum-core", 44 | "bytes", 45 | "form_urlencoded", 46 | "futures-util", 47 | "http 1.1.0", 48 | "http-body", 49 | "http-body-util", 50 | "hyper", 51 | "hyper-util", 52 | "itoa", 53 | "matchit", 54 | "memchr", 55 | "mime", 56 | "percent-encoding", 57 | "pin-project-lite", 58 | "rustversion", 59 | "serde", 60 | "serde_json", 61 | "serde_path_to_error", 62 | "serde_urlencoded", 63 | "sync_wrapper", 64 | "tokio", 65 | "tower 0.5.2", 66 | "tower-layer", 67 | "tower-service", 68 | "tracing", 69 | ] 70 | 71 | [[package]] 72 | name = "axum-core" 73 | version = "0.5.2" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" 76 | dependencies = [ 77 | "bytes", 78 | "futures-core", 79 | "http 1.1.0", 80 | "http-body", 81 | "http-body-util", 82 | "mime", 83 | "pin-project-lite", 84 | "rustversion", 85 | "sync_wrapper", 86 | "tower-layer", 87 | "tower-service", 88 | "tracing", 89 | ] 90 | 91 | [[package]] 92 | name = "backtrace" 93 | version = "0.3.73" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" 96 | dependencies = [ 97 | "addr2line", 98 | "cc", 99 | "cfg-if", 100 | "libc", 101 | "miniz_oxide", 102 | "object", 103 | "rustc-demangle", 104 | ] 105 | 106 | [[package]] 107 | name = "bitflags" 108 | version = "1.3.2" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 111 | 112 | [[package]] 113 | name = "bytes" 114 | version = "1.7.1" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" 117 | 118 | [[package]] 119 | name = "cargo-quickinstall-stats-server" 120 | version = "0.0.0" 121 | dependencies = [ 122 | "axum", 123 | "influxrs", 124 | "tokio", 125 | ] 126 | 127 | [[package]] 128 | name = "castaway" 129 | version = "0.1.2" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" 132 | 133 | [[package]] 134 | name = "cc" 135 | version = "1.1.18" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" 138 | dependencies = [ 139 | "shlex", 140 | ] 141 | 142 | [[package]] 143 | name = "cfg-if" 144 | version = "1.0.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 147 | 148 | [[package]] 149 | name = "concurrent-queue" 150 | version = "2.5.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 153 | dependencies = [ 154 | "crossbeam-utils", 155 | ] 156 | 157 | [[package]] 158 | name = "crossbeam-utils" 159 | version = "0.8.20" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 162 | 163 | [[package]] 164 | name = "csv" 165 | version = "1.3.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" 168 | dependencies = [ 169 | "csv-core", 170 | "itoa", 171 | "ryu", 172 | "serde", 173 | ] 174 | 175 | [[package]] 176 | name = "csv-core" 177 | version = "0.1.11" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" 180 | dependencies = [ 181 | "memchr", 182 | ] 183 | 184 | [[package]] 185 | name = "curl" 186 | version = "0.4.46" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "1e2161dd6eba090ff1594084e95fd67aeccf04382ffea77999ea94ed42ec67b6" 189 | dependencies = [ 190 | "curl-sys", 191 | "libc", 192 | "openssl-probe", 193 | "openssl-sys", 194 | "schannel", 195 | "socket2", 196 | "windows-sys 0.52.0", 197 | ] 198 | 199 | [[package]] 200 | name = "curl-sys" 201 | version = "0.4.74+curl-8.9.0" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "8af10b986114528fcdc4b63b6f5f021b7057618411046a4de2ba0f0149a097bf" 204 | dependencies = [ 205 | "cc", 206 | "libc", 207 | "libnghttp2-sys", 208 | "libz-sys", 209 | "openssl-sys", 210 | "pkg-config", 211 | "vcpkg", 212 | "windows-sys 0.52.0", 213 | ] 214 | 215 | [[package]] 216 | name = "encoding_rs" 217 | version = "0.8.34" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" 220 | dependencies = [ 221 | "cfg-if", 222 | ] 223 | 224 | [[package]] 225 | name = "event-listener" 226 | version = "2.5.3" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" 229 | 230 | [[package]] 231 | name = "fastrand" 232 | version = "1.9.0" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 235 | dependencies = [ 236 | "instant", 237 | ] 238 | 239 | [[package]] 240 | name = "fnv" 241 | version = "1.0.7" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 244 | 245 | [[package]] 246 | name = "form_urlencoded" 247 | version = "1.2.1" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 250 | dependencies = [ 251 | "percent-encoding", 252 | ] 253 | 254 | [[package]] 255 | name = "futures-channel" 256 | version = "0.3.30" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 259 | dependencies = [ 260 | "futures-core", 261 | ] 262 | 263 | [[package]] 264 | name = "futures-core" 265 | version = "0.3.30" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 268 | 269 | [[package]] 270 | name = "futures-io" 271 | version = "0.3.30" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 274 | 275 | [[package]] 276 | name = "futures-lite" 277 | version = "1.13.0" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" 280 | dependencies = [ 281 | "fastrand", 282 | "futures-core", 283 | "futures-io", 284 | "memchr", 285 | "parking", 286 | "pin-project-lite", 287 | "waker-fn", 288 | ] 289 | 290 | [[package]] 291 | name = "futures-task" 292 | version = "0.3.30" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 295 | 296 | [[package]] 297 | name = "futures-util" 298 | version = "0.3.30" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 301 | dependencies = [ 302 | "futures-core", 303 | "futures-task", 304 | "pin-project-lite", 305 | "pin-utils", 306 | ] 307 | 308 | [[package]] 309 | name = "gimli" 310 | version = "0.29.0" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 313 | 314 | [[package]] 315 | name = "hermit-abi" 316 | version = "0.3.9" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 319 | 320 | [[package]] 321 | name = "http" 322 | version = "0.2.12" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 325 | dependencies = [ 326 | "bytes", 327 | "fnv", 328 | "itoa", 329 | ] 330 | 331 | [[package]] 332 | name = "http" 333 | version = "1.1.0" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 336 | dependencies = [ 337 | "bytes", 338 | "fnv", 339 | "itoa", 340 | ] 341 | 342 | [[package]] 343 | name = "http-body" 344 | version = "1.0.1" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 347 | dependencies = [ 348 | "bytes", 349 | "http 1.1.0", 350 | ] 351 | 352 | [[package]] 353 | name = "http-body-util" 354 | version = "0.1.2" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" 357 | dependencies = [ 358 | "bytes", 359 | "futures-util", 360 | "http 1.1.0", 361 | "http-body", 362 | "pin-project-lite", 363 | ] 364 | 365 | [[package]] 366 | name = "httparse" 367 | version = "1.9.4" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" 370 | 371 | [[package]] 372 | name = "httpdate" 373 | version = "1.0.3" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 376 | 377 | [[package]] 378 | name = "hyper" 379 | version = "1.4.1" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" 382 | dependencies = [ 383 | "bytes", 384 | "futures-channel", 385 | "futures-util", 386 | "http 1.1.0", 387 | "http-body", 388 | "httparse", 389 | "httpdate", 390 | "itoa", 391 | "pin-project-lite", 392 | "smallvec", 393 | "tokio", 394 | ] 395 | 396 | [[package]] 397 | name = "hyper-util" 398 | version = "0.1.7" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" 401 | dependencies = [ 402 | "bytes", 403 | "futures-util", 404 | "http 1.1.0", 405 | "http-body", 406 | "hyper", 407 | "pin-project-lite", 408 | "tokio", 409 | "tower 0.4.13", 410 | "tower-service", 411 | ] 412 | 413 | [[package]] 414 | name = "idna" 415 | version = "0.5.0" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 418 | dependencies = [ 419 | "unicode-bidi", 420 | "unicode-normalization", 421 | ] 422 | 423 | [[package]] 424 | name = "influxrs" 425 | version = "2.0.1" 426 | source = "git+https://github.com/alsuren/influxrs?rev=bc037531ef31e0a19014556120ecdd151427561b#bc037531ef31e0a19014556120ecdd151427561b" 427 | dependencies = [ 428 | "csv", 429 | "isahc", 430 | "log", 431 | "serde", 432 | ] 433 | 434 | [[package]] 435 | name = "instant" 436 | version = "0.1.13" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" 439 | dependencies = [ 440 | "cfg-if", 441 | ] 442 | 443 | [[package]] 444 | name = "isahc" 445 | version = "1.7.2" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9" 448 | dependencies = [ 449 | "async-channel", 450 | "castaway", 451 | "crossbeam-utils", 452 | "curl", 453 | "curl-sys", 454 | "encoding_rs", 455 | "event-listener", 456 | "futures-lite", 457 | "http 0.2.12", 458 | "log", 459 | "mime", 460 | "once_cell", 461 | "polling", 462 | "slab", 463 | "sluice", 464 | "tracing", 465 | "tracing-futures", 466 | "url", 467 | "waker-fn", 468 | ] 469 | 470 | [[package]] 471 | name = "itoa" 472 | version = "1.0.11" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 475 | 476 | [[package]] 477 | name = "libc" 478 | version = "0.2.169" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 481 | 482 | [[package]] 483 | name = "libnghttp2-sys" 484 | version = "0.1.10+1.61.0" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "959c25552127d2e1fa72f0e52548ec04fc386e827ba71a7bd01db46a447dc135" 487 | dependencies = [ 488 | "cc", 489 | "libc", 490 | ] 491 | 492 | [[package]] 493 | name = "libz-sys" 494 | version = "1.1.20" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" 497 | dependencies = [ 498 | "cc", 499 | "libc", 500 | "pkg-config", 501 | "vcpkg", 502 | ] 503 | 504 | [[package]] 505 | name = "log" 506 | version = "0.4.22" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 509 | 510 | [[package]] 511 | name = "matchit" 512 | version = "0.8.4" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 515 | 516 | [[package]] 517 | name = "memchr" 518 | version = "2.7.4" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 521 | 522 | [[package]] 523 | name = "mime" 524 | version = "0.3.17" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 527 | 528 | [[package]] 529 | name = "miniz_oxide" 530 | version = "0.7.4" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 533 | dependencies = [ 534 | "adler", 535 | ] 536 | 537 | [[package]] 538 | name = "mio" 539 | version = "1.0.2" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 542 | dependencies = [ 543 | "hermit-abi", 544 | "libc", 545 | "wasi", 546 | "windows-sys 0.52.0", 547 | ] 548 | 549 | [[package]] 550 | name = "object" 551 | version = "0.36.4" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" 554 | dependencies = [ 555 | "memchr", 556 | ] 557 | 558 | [[package]] 559 | name = "once_cell" 560 | version = "1.19.0" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 563 | 564 | [[package]] 565 | name = "openssl-probe" 566 | version = "0.1.5" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 569 | 570 | [[package]] 571 | name = "openssl-sys" 572 | version = "0.9.103" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" 575 | dependencies = [ 576 | "cc", 577 | "libc", 578 | "pkg-config", 579 | "vcpkg", 580 | ] 581 | 582 | [[package]] 583 | name = "parking" 584 | version = "2.2.1" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 587 | 588 | [[package]] 589 | name = "percent-encoding" 590 | version = "2.3.1" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 593 | 594 | [[package]] 595 | name = "pin-project" 596 | version = "1.1.5" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" 599 | dependencies = [ 600 | "pin-project-internal", 601 | ] 602 | 603 | [[package]] 604 | name = "pin-project-internal" 605 | version = "1.1.5" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" 608 | dependencies = [ 609 | "proc-macro2", 610 | "quote", 611 | "syn", 612 | ] 613 | 614 | [[package]] 615 | name = "pin-project-lite" 616 | version = "0.2.14" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 619 | 620 | [[package]] 621 | name = "pin-utils" 622 | version = "0.1.0" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 625 | 626 | [[package]] 627 | name = "pkg-config" 628 | version = "0.3.30" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 631 | 632 | [[package]] 633 | name = "polling" 634 | version = "2.8.0" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" 637 | dependencies = [ 638 | "autocfg", 639 | "bitflags", 640 | "cfg-if", 641 | "concurrent-queue", 642 | "libc", 643 | "log", 644 | "pin-project-lite", 645 | "windows-sys 0.48.0", 646 | ] 647 | 648 | [[package]] 649 | name = "proc-macro2" 650 | version = "1.0.86" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 653 | dependencies = [ 654 | "unicode-ident", 655 | ] 656 | 657 | [[package]] 658 | name = "quote" 659 | version = "1.0.37" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 662 | dependencies = [ 663 | "proc-macro2", 664 | ] 665 | 666 | [[package]] 667 | name = "rustc-demangle" 668 | version = "0.1.24" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 671 | 672 | [[package]] 673 | name = "rustversion" 674 | version = "1.0.17" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 677 | 678 | [[package]] 679 | name = "ryu" 680 | version = "1.0.18" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 683 | 684 | [[package]] 685 | name = "schannel" 686 | version = "0.1.24" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" 689 | dependencies = [ 690 | "windows-sys 0.59.0", 691 | ] 692 | 693 | [[package]] 694 | name = "serde" 695 | version = "1.0.210" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 698 | dependencies = [ 699 | "serde_derive", 700 | ] 701 | 702 | [[package]] 703 | name = "serde_derive" 704 | version = "1.0.210" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 707 | dependencies = [ 708 | "proc-macro2", 709 | "quote", 710 | "syn", 711 | ] 712 | 713 | [[package]] 714 | name = "serde_json" 715 | version = "1.0.128" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" 718 | dependencies = [ 719 | "itoa", 720 | "memchr", 721 | "ryu", 722 | "serde", 723 | ] 724 | 725 | [[package]] 726 | name = "serde_path_to_error" 727 | version = "0.1.16" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" 730 | dependencies = [ 731 | "itoa", 732 | "serde", 733 | ] 734 | 735 | [[package]] 736 | name = "serde_urlencoded" 737 | version = "0.7.1" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 740 | dependencies = [ 741 | "form_urlencoded", 742 | "itoa", 743 | "ryu", 744 | "serde", 745 | ] 746 | 747 | [[package]] 748 | name = "shlex" 749 | version = "1.3.0" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 752 | 753 | [[package]] 754 | name = "slab" 755 | version = "0.4.9" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 758 | dependencies = [ 759 | "autocfg", 760 | ] 761 | 762 | [[package]] 763 | name = "sluice" 764 | version = "0.5.5" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" 767 | dependencies = [ 768 | "async-channel", 769 | "futures-core", 770 | "futures-io", 771 | ] 772 | 773 | [[package]] 774 | name = "smallvec" 775 | version = "1.13.2" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 778 | 779 | [[package]] 780 | name = "socket2" 781 | version = "0.5.7" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 784 | dependencies = [ 785 | "libc", 786 | "windows-sys 0.52.0", 787 | ] 788 | 789 | [[package]] 790 | name = "syn" 791 | version = "2.0.77" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" 794 | dependencies = [ 795 | "proc-macro2", 796 | "quote", 797 | "unicode-ident", 798 | ] 799 | 800 | [[package]] 801 | name = "sync_wrapper" 802 | version = "1.0.1" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" 805 | 806 | [[package]] 807 | name = "tinyvec" 808 | version = "1.8.0" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 811 | dependencies = [ 812 | "tinyvec_macros", 813 | ] 814 | 815 | [[package]] 816 | name = "tinyvec_macros" 817 | version = "0.1.1" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 820 | 821 | [[package]] 822 | name = "tokio" 823 | version = "1.45.1" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" 826 | dependencies = [ 827 | "backtrace", 828 | "libc", 829 | "mio", 830 | "pin-project-lite", 831 | "socket2", 832 | "tokio-macros", 833 | "windows-sys 0.52.0", 834 | ] 835 | 836 | [[package]] 837 | name = "tokio-macros" 838 | version = "2.5.0" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 841 | dependencies = [ 842 | "proc-macro2", 843 | "quote", 844 | "syn", 845 | ] 846 | 847 | [[package]] 848 | name = "tower" 849 | version = "0.4.13" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 852 | dependencies = [ 853 | "futures-core", 854 | "futures-util", 855 | "pin-project", 856 | "pin-project-lite", 857 | "tokio", 858 | "tower-layer", 859 | "tower-service", 860 | ] 861 | 862 | [[package]] 863 | name = "tower" 864 | version = "0.5.2" 865 | source = "registry+https://github.com/rust-lang/crates.io-index" 866 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 867 | dependencies = [ 868 | "futures-core", 869 | "futures-util", 870 | "pin-project-lite", 871 | "sync_wrapper", 872 | "tokio", 873 | "tower-layer", 874 | "tower-service", 875 | "tracing", 876 | ] 877 | 878 | [[package]] 879 | name = "tower-layer" 880 | version = "0.3.3" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 883 | 884 | [[package]] 885 | name = "tower-service" 886 | version = "0.3.3" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 889 | 890 | [[package]] 891 | name = "tracing" 892 | version = "0.1.40" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 895 | dependencies = [ 896 | "log", 897 | "pin-project-lite", 898 | "tracing-attributes", 899 | "tracing-core", 900 | ] 901 | 902 | [[package]] 903 | name = "tracing-attributes" 904 | version = "0.1.27" 905 | source = "registry+https://github.com/rust-lang/crates.io-index" 906 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 907 | dependencies = [ 908 | "proc-macro2", 909 | "quote", 910 | "syn", 911 | ] 912 | 913 | [[package]] 914 | name = "tracing-core" 915 | version = "0.1.32" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 918 | dependencies = [ 919 | "once_cell", 920 | ] 921 | 922 | [[package]] 923 | name = "tracing-futures" 924 | version = "0.2.5" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" 927 | dependencies = [ 928 | "pin-project", 929 | "tracing", 930 | ] 931 | 932 | [[package]] 933 | name = "unicode-bidi" 934 | version = "0.3.15" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 937 | 938 | [[package]] 939 | name = "unicode-ident" 940 | version = "1.0.12" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 943 | 944 | [[package]] 945 | name = "unicode-normalization" 946 | version = "0.1.23" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 949 | dependencies = [ 950 | "tinyvec", 951 | ] 952 | 953 | [[package]] 954 | name = "url" 955 | version = "2.5.2" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" 958 | dependencies = [ 959 | "form_urlencoded", 960 | "idna", 961 | "percent-encoding", 962 | ] 963 | 964 | [[package]] 965 | name = "vcpkg" 966 | version = "0.2.15" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 969 | 970 | [[package]] 971 | name = "waker-fn" 972 | version = "1.2.0" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" 975 | 976 | [[package]] 977 | name = "wasi" 978 | version = "0.11.0+wasi-snapshot-preview1" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 981 | 982 | [[package]] 983 | name = "windows-sys" 984 | version = "0.48.0" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 987 | dependencies = [ 988 | "windows-targets 0.48.5", 989 | ] 990 | 991 | [[package]] 992 | name = "windows-sys" 993 | version = "0.52.0" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 996 | dependencies = [ 997 | "windows-targets 0.52.6", 998 | ] 999 | 1000 | [[package]] 1001 | name = "windows-sys" 1002 | version = "0.59.0" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1005 | dependencies = [ 1006 | "windows-targets 0.52.6", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "windows-targets" 1011 | version = "0.48.5" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1014 | dependencies = [ 1015 | "windows_aarch64_gnullvm 0.48.5", 1016 | "windows_aarch64_msvc 0.48.5", 1017 | "windows_i686_gnu 0.48.5", 1018 | "windows_i686_msvc 0.48.5", 1019 | "windows_x86_64_gnu 0.48.5", 1020 | "windows_x86_64_gnullvm 0.48.5", 1021 | "windows_x86_64_msvc 0.48.5", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "windows-targets" 1026 | version = "0.52.6" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1029 | dependencies = [ 1030 | "windows_aarch64_gnullvm 0.52.6", 1031 | "windows_aarch64_msvc 0.52.6", 1032 | "windows_i686_gnu 0.52.6", 1033 | "windows_i686_gnullvm", 1034 | "windows_i686_msvc 0.52.6", 1035 | "windows_x86_64_gnu 0.52.6", 1036 | "windows_x86_64_gnullvm 0.52.6", 1037 | "windows_x86_64_msvc 0.52.6", 1038 | ] 1039 | 1040 | [[package]] 1041 | name = "windows_aarch64_gnullvm" 1042 | version = "0.48.5" 1043 | source = "registry+https://github.com/rust-lang/crates.io-index" 1044 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1045 | 1046 | [[package]] 1047 | name = "windows_aarch64_gnullvm" 1048 | version = "0.52.6" 1049 | source = "registry+https://github.com/rust-lang/crates.io-index" 1050 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1051 | 1052 | [[package]] 1053 | name = "windows_aarch64_msvc" 1054 | version = "0.48.5" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1057 | 1058 | [[package]] 1059 | name = "windows_aarch64_msvc" 1060 | version = "0.52.6" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1063 | 1064 | [[package]] 1065 | name = "windows_i686_gnu" 1066 | version = "0.48.5" 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" 1068 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1069 | 1070 | [[package]] 1071 | name = "windows_i686_gnu" 1072 | version = "0.52.6" 1073 | source = "registry+https://github.com/rust-lang/crates.io-index" 1074 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1075 | 1076 | [[package]] 1077 | name = "windows_i686_gnullvm" 1078 | version = "0.52.6" 1079 | source = "registry+https://github.com/rust-lang/crates.io-index" 1080 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1081 | 1082 | [[package]] 1083 | name = "windows_i686_msvc" 1084 | version = "0.48.5" 1085 | source = "registry+https://github.com/rust-lang/crates.io-index" 1086 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1087 | 1088 | [[package]] 1089 | name = "windows_i686_msvc" 1090 | version = "0.52.6" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1093 | 1094 | [[package]] 1095 | name = "windows_x86_64_gnu" 1096 | version = "0.48.5" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1099 | 1100 | [[package]] 1101 | name = "windows_x86_64_gnu" 1102 | version = "0.52.6" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1105 | 1106 | [[package]] 1107 | name = "windows_x86_64_gnullvm" 1108 | version = "0.48.5" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1111 | 1112 | [[package]] 1113 | name = "windows_x86_64_gnullvm" 1114 | version = "0.52.6" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1117 | 1118 | [[package]] 1119 | name = "windows_x86_64_msvc" 1120 | version = "0.48.5" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1123 | 1124 | [[package]] 1125 | name = "windows_x86_64_msvc" 1126 | version = "0.52.6" 1127 | source = "registry+https://github.com/rust-lang/crates.io-index" 1128 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1129 | -------------------------------------------------------------------------------- /stats-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-quickinstall-stats-server" 3 | version = "0.0.0" 4 | authors = ["David Laban "] 5 | # based on https://github.com/fly-apps/hello-rust by Jerome Gravel-Niquet 6 | edition = "2021" 7 | publish = false 8 | 9 | # Make a new workspace so that we get our own Cargo.lock and target dir for the docker build. 10 | [workspace] 11 | 12 | [dependencies] 13 | tokio = { version = "1", features = ["rt-multi-thread", "macros"] } 14 | axum = "0.8" 15 | # FIXME: revert back to using crates.io once https://github.com/ijagberg/influx/pull/5 is released 16 | influxrs = { git = "https://github.com/alsuren/influxrs", rev = "bc037531ef31e0a19014556120ecdd151427561b" } 17 | 18 | [profile.release] 19 | lto = "thin" 20 | -------------------------------------------------------------------------------- /stats-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1-bookworm as builder 2 | 3 | WORKDIR /usr/src/app 4 | COPY . . 5 | # Will build and cache the binary and dependent crates in release mode 6 | RUN --mount=type=cache,target=/usr/local/cargo,from=rust:latest,source=/usr/local/cargo \ 7 | --mount=type=cache,target=target \ 8 | cargo build --release && mv ./target/release/cargo-quickinstall-stats-server ./cargo-quickinstall-stats-server 9 | 10 | # Runtime image 11 | FROM debian:bookworm-slim 12 | 13 | # Install libssl1.1 and ca-certificates 14 | RUN apt-get update && apt-get install -y libssl3 ca-certificates && rm -rf /var/lib/apt/lists/* 15 | 16 | # Run as "app" user 17 | RUN useradd -ms /bin/bash app 18 | 19 | USER app 20 | WORKDIR /app 21 | 22 | # Get compiled binaries from builder's cargo install directory 23 | COPY --from=builder /usr/src/app/cargo-quickinstall-stats-server /app/cargo-quickinstall-stats-server 24 | 25 | # Run the app 26 | CMD ./cargo-quickinstall-stats-server 27 | -------------------------------------------------------------------------------- /stats-server/README.md: -------------------------------------------------------------------------------- 1 | # cargo-quickinstall stats server 2 | 3 | ## Deploy 4 | 5 | ```bash 6 | brew install flyctl 7 | flyctl auth login 8 | ``` 9 | 10 | Then: 11 | 12 | ```bash 13 | fly deploy stats-server 14 | ``` 15 | -------------------------------------------------------------------------------- /stats-server/fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml file generated for cargo-quickinstall-stats-server on 2023-03-04T20:27:42Z 2 | 3 | app = "cargo-quickinstall-stats-server" 4 | kill_signal = "SIGINT" 5 | kill_timeout = 5 6 | deploy.strategy = "bluegreen" 7 | 8 | [env] 9 | PORT = "8080" 10 | # Run `fly deploy --env KEY=VALUE` to override any of these. 11 | # INFLUX_TOKEN is provided as a secret. 12 | INFLUX_URL='https://us-east-1-1.aws.cloud2.influxdata.com' 13 | INFLUX_ORG = "cargo-bins" 14 | INFLUX_BUCKET = "cargo-quickinstall" 15 | 16 | 17 | [[services]] 18 | internal_port = 8080 19 | protocol = "tcp" 20 | [services.concurrency] 21 | hard_limit = 25 22 | soft_limit = 20 23 | 24 | [[services.ports]] 25 | handlers = ["http"] 26 | port = "80" 27 | 28 | [[services.ports]] 29 | handlers = ["tls", "http"] 30 | port = "443" 31 | 32 | [[services.tcp_checks]] 33 | grace_period = "1s" 34 | interval = "15s" 35 | port = "8080" 36 | restart_limit = 6 37 | timeout = "2s" 38 | -------------------------------------------------------------------------------- /stats-server/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::{collections::BTreeMap, sync::LazyLock}; 3 | 4 | use axum::{ 5 | extract::Query, 6 | response::Redirect, 7 | routing::{get, post}, 8 | Router, 9 | }; 10 | use influxrs::{InfluxClient, Measurement}; 11 | use tokio::net::TcpListener; 12 | 13 | static INFLUX_CLIENT: LazyLock = LazyLock::new(|| { 14 | let url = get_env("INFLUX_URL"); 15 | let token = get_env("INFLUX_TOKEN"); 16 | let org = get_env("INFLUX_ORG"); 17 | InfluxClient::builder(url, token, org).build().unwrap() 18 | }); 19 | 20 | static INFLUX_BUCKET: LazyLock = LazyLock::new(|| get_env("INFLUX_BUCKET")); 21 | 22 | fn main() { 23 | let rt = tokio::runtime::Runtime::new().unwrap(); 24 | let task = rt.spawn(async move { 25 | let app = Router::new() 26 | .route("/", get(root)) 27 | .route("/record-install", get(redirect_to_root)) 28 | .route("/record-install", post(record_install)); 29 | 30 | // Smoke test that we can write to influxdb before listening on the socket. 31 | // This is a poor man's startup probe to avoid serving traffic before we can write to influxdb. 32 | INFLUX_CLIENT 33 | .write( 34 | &INFLUX_BUCKET, 35 | &[Measurement::builder("startups") 36 | .field("count", 1) 37 | .build() 38 | .unwrap()], 39 | ) 40 | .await 41 | .unwrap(); 42 | // ipv6 + ipv6 any addr 43 | let addr = SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 0], 8080)); 44 | let listener = TcpListener::bind(addr).await.unwrap(); 45 | 46 | axum::serve(listener, app).await.unwrap(); 47 | }); 48 | rt.block_on(task).unwrap(); 49 | } 50 | 51 | async fn root() -> &'static str { 52 | "This is the stats server for cargo-quickinstall. Go to https://github.com/cargo-bins/cargo-quickinstall for more information." 53 | } 54 | 55 | fn get_env(key: &str) -> String { 56 | std::env::var(key).unwrap_or_else(|_| panic!("{key} must be set")) 57 | } 58 | 59 | async fn redirect_to_root() -> Redirect { 60 | Redirect::to("/") 61 | } 62 | 63 | async fn record_install(Query(params): Query>) -> String { 64 | println!("Hi there {params:?}"); 65 | 66 | let mut point = Measurement::builder("counts").field("count", 1); 67 | for (tag, value) in ¶ms { 68 | if !["crate", "version", "target", "agent", "status"].contains(&tag.as_str()) { 69 | println!("Skipping unknown query param: {tag}={value}"); 70 | continue; 71 | } 72 | point = point.tag(tag, value.as_str()) 73 | } 74 | INFLUX_CLIENT 75 | .write(&INFLUX_BUCKET, &[point.build().unwrap()]) 76 | .await 77 | .unwrap(); 78 | format!("Hi there {params:?}") 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use std::collections::BTreeMap; 84 | 85 | use axum::extract::Query; 86 | 87 | use crate::record_install; 88 | 89 | #[tokio::test] 90 | async fn smoke_test_against_real_server() { 91 | if std::env::var("INFLUX_TOKEN").is_err() { 92 | println!("set INFLUX_URL, INFLUX_ORG and INFLUX_TOKEN to enable this test"); 93 | return; 94 | } 95 | record_install(Query( 96 | [("x".to_string(), "y".to_string())] 97 | .into_iter() 98 | .collect::>(), 99 | )) 100 | .await; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /supported-targets: -------------------------------------------------------------------------------- 1 | x86_64-pc-windows-msvc x86_64-apple-darwin aarch64-apple-darwin x86_64-unknown-linux-gnu x86_64-unknown-linux-musl aarch64-unknown-linux-gnu aarch64-unknown-linux-musl aarch64-pc-windows-msvc armv7-unknown-linux-musleabihf armv7-unknown-linux-gnueabihf 2 | -------------------------------------------------------------------------------- /zigbuild-requirements.txt: -------------------------------------------------------------------------------- 1 | ###### Requirements without Version Specifiers ###### 2 | cargo-zigbuild 3 | 4 | ###### Requirements with Version Specifiers ###### 5 | ziglang 6 | --------------------------------------------------------------------------------