├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── docs.yml │ └── publish.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── MAINTAINING.md ├── README.md ├── TUTORIAL.md ├── build.rs ├── guide ├── .gitignore ├── book.toml └── src │ ├── SUMMARY.md │ ├── alt.md │ ├── boundaries.md │ ├── examples │ ├── clippy.md │ ├── components.md │ ├── diagnostics.md │ ├── doc-change.md │ ├── flaky.md │ ├── incremental.md │ ├── index.md │ ├── preserve.md │ ├── rustdoc.md │ ├── slow.md │ ├── windows-scripting.md │ └── without-cargo.md │ ├── git-bisect.md │ ├── installation.md │ ├── introduction.md │ ├── rust-src-repo.md │ ├── rustup.md │ ├── tutorial.md │ └── usage.md ├── src ├── bounds.rs ├── git.rs ├── github.rs ├── least_satisfying.rs ├── main.rs ├── repo_access.rs └── toolchains.rs └── tests ├── README.md ├── cli_tests.rs └── cmd ├── bare-h.stderr ├── bare-h.stdout ├── bare-h.toml ├── bare-help.stderr ├── bare-help.stdout ├── bare-help.toml ├── by-commit-no-start.stderr ├── by-commit-no-start.stdout ├── by-commit-no-start.toml ├── end-before-start.stderr ├── end-before-start.stdout ├── end-before-start.toml ├── end-in-future.stderr ├── end-in-future.stdout ├── end-in-future.toml ├── h.stderr ├── h.stdout ├── h.toml ├── help.stderr ├── help.stdout ├── help.toml ├── mixed-commit-date.stderr ├── mixed-commit-date.stdout ├── mixed-commit-date.toml ├── mixed-date-commit.stderr ├── mixed-date-commit.stdout ├── mixed-date-commit.toml ├── start-and-end-in-future.stderr ├── start-and-end-in-future.stdout ├── start-and-end-in-future.toml ├── start-in-future.stderr ├── start-in-future.stdout └── start-in-future.toml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | open-pull-requests-limit: 20 8 | ignore: 9 | - dependency-name: "*" 10 | update-types: ["version-update:semver-patch"] 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches-ignore: 6 | - 'dependabot/**' 7 | permissions: 8 | contents: read 9 | jobs: 10 | test: 11 | name: Test 12 | strategy: 13 | matrix: 14 | os: ["ubuntu-latest", "windows-latest", "macos-latest"] 15 | rust: ["stable"] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | - name: Install Rust 21 | uses: dtolnay/rust-toolchain@master 22 | with: 23 | toolchain: ${{ matrix.rust }} 24 | - uses: Swatinem/rust-cache@v2 25 | - name: Build 26 | run: cargo test --no-run 27 | - name: Test 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | run: cargo test 31 | - name: Verify that binary works 32 | run: | 33 | cargo run -- bisect-rustc --help | grep "Examples:" 34 | 35 | fmt: 36 | name: rustfmt 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout source 40 | uses: actions/checkout@v4 41 | - name: Install Rust 42 | uses: dtolnay/rust-toolchain@stable 43 | with: 44 | components: rustfmt 45 | - name: Run rustfmt check 46 | run: | 47 | cargo fmt --version 48 | cargo fmt --check || (echo "Please reformat your code with 'cargo fmt' (version $(cargo fmt --version))"; false) 49 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation Deploy 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | deploy: 12 | permissions: 13 | contents: write # for Git to git push 14 | runs-on: ubuntu-latest 15 | env: 16 | MDBOOK_VERSION: 0.4.51 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Install mdbook 22 | run: | 23 | mkdir mdbook 24 | curl -Lf https://github.com/rust-lang/mdBook/releases/download/v${MDBOOK_VERSION}/mdbook-v${MDBOOK_VERSION}-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=./mdbook 25 | echo `pwd`/mdbook >> $GITHUB_PATH 26 | - name: Deploy docs 27 | run: | 28 | cd guide 29 | mdbook build 30 | git worktree add gh-pages 31 | git config user.name "Deploy from CI" 32 | git config user.email "" 33 | cd gh-pages 34 | # Delete the ref to avoid keeping history. 35 | git update-ref -d refs/heads/gh-pages 36 | rm -rf * 37 | mv ../book/* . 38 | git add . 39 | git commit -m "Deploy $GITHUB_SHA to gh-pages" 40 | git push --force --set-upstream origin gh-pages 41 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | release: 4 | types: [created] 5 | 6 | defaults: 7 | run: 8 | shell: bash 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | publish: 15 | name: Publish to crates.io 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Install Rust (rustup) 20 | run: rustup update stable --no-self-update && rustup default stable 21 | - name: Publish 22 | env: 23 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 24 | run: cargo publish --no-verify 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /rust.git 2 | /target 3 | **/*.rs.bk 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.6.10 4 | [v0.6.9...v0.6.10](https://github.com/rust-lang/cargo-bisect-rustc/compare/v0.6.9...v0.6.10) 5 | 6 | ### Added 7 | - Added the `--pretend-to-be-stable` flag. 8 | [#335](https://github.com/rust-lang/cargo-bisect-rustc/pull/335) 9 | - Documented how to bisect an individual clippy warning. 10 | [#368](https://github.com/rust-lang/cargo-bisect-rustc/pull/368) 11 | - Documented another example of a hanging compilation. 12 | [#374](https://github.com/rust-lang/cargo-bisect-rustc/pull/374) 13 | 14 | ### Changed 15 | - Print the command that is run with `--verbose`. 16 | [#361](https://github.com/rust-lang/cargo-bisect-rustc/pull/361) 17 | - Updated all dependencies. 18 | [#383](https://github.com/rust-lang/cargo-bisect-rustc/pull/383) 19 | - Unrolled CI builds have moved from the `rust-lang-ci/rust` repository to the `rust-lang/rust` repository. 20 | [#381](https://github.com/rust-lang/cargo-bisect-rustc/pull/381) 21 | 22 | ### Fixed 23 | - Fixed printing of args in the final report. 24 | [#356](https://github.com/rust-lang/cargo-bisect-rustc/pull/356) 25 | - Fixed so that `cargo-bisect-rustc` can be run with the hyphen after `cargo` or a space. 26 | [#362](https://github.com/rust-lang/cargo-bisect-rustc/pull/362) 27 | 28 | ## v0.6.9 29 | 30 | ### Added 31 | - Added flags `--term-old` and `--term-new` to allow custom messages when bisecting a regression. 32 | [#330](https://github.com/rust-lang/cargo-bisect-rustc/pull/330) 33 | [#339](https://github.com/rust-lang/cargo-bisect-rustc/pull/339) 34 | 35 | 36 | ### Changed 37 | - Updated dependencies. 38 | [#314](https://github.com/rust-lang/cargo-bisect-rustc/pull/314) 39 | [#313](https://github.com/rust-lang/cargo-bisect-rustc/pull/313) 40 | [#315](https://github.com/rust-lang/cargo-bisect-rustc/pull/315) 41 | [#319](https://github.com/rust-lang/cargo-bisect-rustc/pull/319) 42 | [#326](https://github.com/rust-lang/cargo-bisect-rustc/pull/326) 43 | [#327](https://github.com/rust-lang/cargo-bisect-rustc/pull/327) 44 | [#329](https://github.com/rust-lang/cargo-bisect-rustc/pull/329) 45 | [#340](https://github.com/rust-lang/cargo-bisect-rustc/pull/340) 46 | - No longer defaults to cross-compile mode when `--target` is not specified. This more closely matches `cargo`'s behavior, which can affect reproducability. 47 | [#323](https://github.com/rust-lang/cargo-bisect-rustc/pull/323) 48 | - Removed LTO and stripping of building `cargo-bisect-rustc` itself. 49 | [#334](https://github.com/rust-lang/cargo-bisect-rustc/pull/334) 50 | 51 | ### Fixed 52 | - Don't assume the date before the regressed nightly is the good nightly if there are missing nightlies. 53 | [#320](https://github.com/rust-lang/cargo-bisect-rustc/pull/320) 54 | - Fixed building `cargo-bisect-rustc` itself to avoid unnecessary build-script rebuilds. 55 | [#324](https://github.com/rust-lang/cargo-bisect-rustc/pull/324) 56 | - Fixed doc-change example documentation. 57 | [#336](https://github.com/rust-lang/cargo-bisect-rustc/pull/336) 58 | - Replaced a panic with an error message if a given SHA commit is not from bors using the GitHub backend. 59 | [#318](https://github.com/rust-lang/cargo-bisect-rustc/pull/318) 60 | - Fixed determination of what the latest nightly is when `--end` is not specified, and it is past UTC midnight, but the release process has not yet finished. 61 | [#325](https://github.com/rust-lang/cargo-bisect-rustc/pull/325) 62 | - Fixed panic with `--by-commit` but no `--start`. 63 | [#325](https://github.com/rust-lang/cargo-bisect-rustc/pull/325) 64 | 65 | ## v0.6.8 66 | 67 | ### Added 68 | - Added documentation for `--alt` builds. 69 | [#289](https://github.com/rust-lang/cargo-bisect-rustc/pull/289) 70 | 71 | ### Changed 72 | - ❗️ Changed the default access method to "github", meaning it will use the GitHub API to fetch commit information instead of using a local git clone. See the [access documentation](https://rust-lang.github.io/cargo-bisect-rustc/rust-src-repo.html) for more information. 73 | [#307](https://github.com/rust-lang/cargo-bisect-rustc/pull/307) 74 | - Updated dependencies. 75 | [#290](https://github.com/rust-lang/cargo-bisect-rustc/pull/290) 76 | [#291](https://github.com/rust-lang/cargo-bisect-rustc/pull/291) 77 | [#296](https://github.com/rust-lang/cargo-bisect-rustc/pull/296) 78 | [#302](https://github.com/rust-lang/cargo-bisect-rustc/pull/302) 79 | [#301](https://github.com/rust-lang/cargo-bisect-rustc/pull/301) 80 | [#300](https://github.com/rust-lang/cargo-bisect-rustc/pull/300) 81 | [#304](https://github.com/rust-lang/cargo-bisect-rustc/pull/304) 82 | [#305](https://github.com/rust-lang/cargo-bisect-rustc/pull/305) 83 | [#306](https://github.com/rust-lang/cargo-bisect-rustc/pull/306) 84 | [#308](https://github.com/rust-lang/cargo-bisect-rustc/pull/308) 85 | 86 | ## Fixed 87 | - Fixed an issue when attempting to bisect a rollup, but the perf commits have been garbage collected, to display information about the rollup so that you can see which PRs were involved. 88 | [#298](https://github.com/rust-lang/cargo-bisect-rustc/pull/298) 89 | 90 | ## v0.6.7 91 | 92 | ### Changed 93 | - Updated dependencies. 94 | [#271](https://github.com/rust-lang/cargo-bisect-rustc/pull/271) 95 | [#270](https://github.com/rust-lang/cargo-bisect-rustc/pull/270) 96 | [#273](https://github.com/rust-lang/cargo-bisect-rustc/pull/273) 97 | [#278](https://github.com/rust-lang/cargo-bisect-rustc/pull/278) 98 | [#279](https://github.com/rust-lang/cargo-bisect-rustc/pull/279) 99 | [#281](https://github.com/rust-lang/cargo-bisect-rustc/pull/281) 100 | [#285](https://github.com/rust-lang/cargo-bisect-rustc/pull/285) 101 | - CI artifacts are now downloaded from https://ci-artifacts.rust-lang.org instead of https://s3-us-west-1.amazonaws.com/rust-lang-ci2 which should help with performance. 102 | 103 | ### Fixed 104 | - Fix bisecting into rollups via unrolled perf builds 105 | [#280](https://github.com/rust-lang/cargo-bisect-rustc/pull/280) 106 | 107 | ## v0.6.6 108 | 109 | ### Added 110 | 111 | - 🎉 Added bisecting of rollups. This depends on the artifacts generated for rustc-perf which is only available for x86_64-unknown-linux-gnu. 112 | [#256](https://github.com/rust-lang/cargo-bisect-rustc/pull/256) 113 | - 🎉 Added a new User Guide with more detailed documentation and a set of examples illustrating different ways to use `cargo-bisect-rustc`. The guide is available at . 114 | [#266](https://github.com/rust-lang/cargo-bisect-rustc/pull/266) 115 | 116 | ### Changed 117 | 118 | - Added another kind of ICE output that is auto-detected. 119 | [#261](https://github.com/rust-lang/cargo-bisect-rustc/pull/261) 120 | - Updated dependencies: 121 | - tokio [#245](https://github.com/rust-lang/cargo-bisect-rustc/pull/245) [#255](https://github.com/rust-lang/cargo-bisect-rustc/pull/255) 122 | - git2 [#246](https://github.com/rust-lang/cargo-bisect-rustc/pull/246) [#249](https://github.com/rust-lang/cargo-bisect-rustc/pull/249) 123 | - bumpalo [#250](https://github.com/rust-lang/cargo-bisect-rustc/pull/250) 124 | - pbr [#257](https://github.com/rust-lang/cargo-bisect-rustc/pull/257) 125 | - tempfile [#260](https://github.com/rust-lang/cargo-bisect-rustc/pull/260) 126 | - openssl [#267](https://github.com/rust-lang/cargo-bisect-rustc/pull/267) 127 | - chrono [#268](https://github.com/rust-lang/cargo-bisect-rustc/pull/268) 128 | 129 | ### Fixed 130 | 131 | - Fixed bounds checking when `--start` or `--end` is not specified. 132 | [#243](https://github.com/rust-lang/cargo-bisect-rustc/pull/243) 133 | - The remote tags are now fetched from the `rust-lang/rust` repo to ensure that tag boundaries (`--start 1.65.0`) work if the tag hasn't been downloaded. 134 | [#263](https://github.com/rust-lang/cargo-bisect-rustc/pull/263) 135 | 136 | ## v0.6.5 137 | 138 | ### Changed 139 | 140 | - Stack overflow on any thread (not just 'rustc') is treated as an ICE. 141 | [#194](https://github.com/rust-lang/cargo-bisect-rustc/pull/194) 142 | - Clap (the CLI argument processor) has been updated, which may result in some minor CLI output and parsing changes. 143 | [#225](https://github.com/rust-lang/cargo-bisect-rustc/pull/225) 144 | [#229](https://github.com/rust-lang/cargo-bisect-rustc/pull/229) 145 | - The check for the Rust upstream remote in the git repository has been loosened to only scan for `rust-lang/rust` so that non-https remotes like `git@github.com:rust-lang/rust.git` will work. 146 | [#235](https://github.com/rust-lang/cargo-bisect-rustc/pull/235) 147 | - The `--script` option will now look for a script in the current directory (so that it no longer requires the `./` prefix). 148 | [#236](https://github.com/rust-lang/cargo-bisect-rustc/pull/236) 149 | [#238](https://github.com/rust-lang/cargo-bisect-rustc/pull/238) 150 | - Specifying `--start` without `--end` will default the end to be the current date. Previously it would use the date of whatever nightly is currently installed. 151 | [#240](https://github.com/rust-lang/cargo-bisect-rustc/pull/240) 152 | 153 | ### Fixed 154 | 155 | - Fixed using either `cargo bisect-rustc` (with a space) or `cargo-bisect-rustc` (with a dash). 156 | [#187](https://github.com/rust-lang/cargo-bisect-rustc/pull/187) 157 | - Show the CLI help page if no arguments are passed. 158 | [#206](https://github.com/rust-lang/cargo-bisect-rustc/pull/206) 159 | - The CLI argument validator for `--script` has been removed to allow running scripts on PATH. This also removes the `--host` validator which was not needed. 160 | [#207](https://github.com/rust-lang/cargo-bisect-rustc/pull/207) 161 | - Fixed showing the full chain of errors instead of just the top-level one. 162 | [#237](https://github.com/rust-lang/cargo-bisect-rustc/pull/237) 163 | 164 | ## v0.6.4 165 | 166 | ### Added 167 | 168 | - Added the `--component` option to choose optional components to install. 169 | [#131](https://github.com/rust-lang/cargo-bisect-rustc/pull/131) 170 | - An estimate of the number of steps left to run is now displayed. 171 | [#178](https://github.com/rust-lang/cargo-bisect-rustc/pull/178) 172 | 173 | ### Changed 174 | 175 | - Various code refactorings and dependency updates. These shouldn't have 176 | significant noticeable changes. 177 | [#151](https://github.com/rust-lang/cargo-bisect-rustc/pull/151) 178 | [#152](https://github.com/rust-lang/cargo-bisect-rustc/pull/152) 179 | [#153](https://github.com/rust-lang/cargo-bisect-rustc/pull/153) 180 | [#155](https://github.com/rust-lang/cargo-bisect-rustc/pull/155) 181 | [#156](https://github.com/rust-lang/cargo-bisect-rustc/pull/156) 182 | - The `CARGO_BUILD_TARGET` environment variable is now set with the target triple. 183 | [#159](https://github.com/rust-lang/cargo-bisect-rustc/pull/159) 184 | - The default release profile now uses stripping and LTO to significantly 185 | reduce the binary size. 186 | [#157](https://github.com/rust-lang/cargo-bisect-rustc/pull/157) 187 | - Bounds with tags like `--start=1.62.0` are now translated to a nightly date 188 | instead of a master git commit. This allows using tags from releases more 189 | than 6 months old. 190 | [#177](https://github.com/rust-lang/cargo-bisect-rustc/pull/177) 191 | 192 | ## v0.6.3 193 | 194 | ### Fixed 195 | 196 | - Fixed assumption that the rust-lang/rust remote repo is named "origin". 197 | [#149](https://github.com/rust-lang/cargo-bisect-rustc/pull/149) 198 | 199 | ## v0.6.2 200 | 201 | ### Added 202 | 203 | - `--start` and `--end` now support git tags (like `1.59.0`) to bisect between stable releases. 204 | [#147](https://github.com/rust-lang/cargo-bisect-rustc/pull/147) 205 | 206 | ### Changed 207 | 208 | - Stack overflow is now treated as an ICE. 209 | [#142](https://github.com/rust-lang/cargo-bisect-rustc/pull/142) 210 | 211 | ## v0.6.1 212 | 213 | ### Added 214 | 215 | - Added `--with-dev` option to download rustc-dev component. 216 | [#101](https://github.com/rust-lang/cargo-bisect-rustc/pull/101) 217 | - Added `--timeout` option to trigger a failure if compilation takes too long. 218 | [#135](https://github.com/rust-lang/cargo-bisect-rustc/pull/135) 219 | 220 | ### Changed 221 | - Use the `git` CLI to fetch the `rust-lang/rust` repo when looking for CI commits to improve performance. 222 | [#130](https://github.com/rust-lang/cargo-bisect-rustc/pull/130) 223 | 224 | ### Fixed 225 | 226 | - Fixed off-by-one error when examining the date of the local nightly toolchain. 227 | [#113](https://github.com/rust-lang/cargo-bisect-rustc/pull/113) 228 | - Fixed issue with `--preserve` when linking the nightly toolchain leaving a stale link. 229 | [#125](https://github.com/rust-lang/cargo-bisect-rustc/pull/125) 230 | 231 | ## v0.6.0 232 | 233 | ### Added 234 | 235 | - Support specifying the path to a rust-lang/rust clone at runtime with `RUST_SRC_REPO` 236 | 237 | ### Changed 238 | 239 | - Make `--with-cargo` the default to allow bisecting past changes in rustc options. Add `--without-cargo` flag to use the old behavior. 240 | - Use an anonymous remote that always points to rust-lang/rust when refreshing repository 241 | 242 | ### Fixed 243 | 244 | - Add nightly start and end date validations against the current date – previously would attempt to install nightly even if date was in the future 245 | - Verify that `--test-dir` is a directory instead of assuming it is and then panicking 246 | 247 | ## v0.5.2 248 | 249 | - Fix: revert the revised internal compiler error definition in commit a3891cdd26d1c5d35257c351c7c86fa7e72604bb 250 | 251 | ## v0.5.1 252 | 253 | - Fix: Windows build fails due to dependency of `console` dependency issue. Updated `winapi-util` package to v0.1.5 (from 0.1.2) 254 | 255 | ## v0.5.0 256 | 257 | - New: include compiler crashes in ICE regression definition 258 | - New: ANSI escape code colored standard stream output 259 | - New: Add bisect-rustc version to the final report 260 | - New: Add host triple to the final report 261 | - New: Add command line args to reproduce the reporter's bisect-rustc tests to final report 262 | - Fix: end date reporting when `--end` option used without `--start` option 263 | - Updated: Standard stream reporting format for improved readability during execution 264 | - Updated: Final report instructions for regression reporting 265 | - Updated: Eliminated Markdown elements in the final report that are not typically included in rust-lang/rust issues by reporting users 266 | 267 | ## v0.4.1 268 | 269 | - Fix: bug on git commit retrieval from local rust git repository when `--end` commit is not specified 270 | - Fix: bug on git commit retrieval from GitHub API when `--end` commit is not specified 271 | - Updated dependencies 272 | - rustfmt source code 273 | 274 | ## v0.4.0 275 | 276 | - Add support for GitHub API queries for Rust commit history 277 | - Add support for `--regress=non-ice` regression definition 278 | - Add support for `--script` arguments 279 | - Fix duplicated start date range pulls/checks 280 | - Reformat standard stream reporting 281 | 282 | ## v0.3.0 283 | 284 | - Transition to Rustlang edition 2018 285 | - Add test stage that can process output to decide outcome based on more subtle predicate than just `exit_status.success()` 286 | - Add support for optional `--regress=ice` regression testing definition (default is `--regress=error`) 287 | - Add support for optional `--regress=success` regression testing definition (default is `--regress=error`) 288 | - Add support for optional `--regress=non-error` regression testing definition (default is `--regress=error`) 289 | - Update the `remove` function to use an explicit `bisector` string at the beginning of the path name 290 | - Update the `remove` function to guard against deleting state not managed by `cargo-bisect-rustc` 291 | - Edit short and long help strings to fit on a single line 292 | - Fix: support reuse of an already installed nightly, previously we would unconditionally fail the run 293 | 294 | ## v0.2.1 295 | 296 | - Fix: refactor date bounds to assume that start date equals end date at the beginning of testing with `--end` option only 297 | 298 | ## v0.2.0 299 | 300 | - Add automated regression report generation at the end of the test runs 301 | - Add validation of date bounds 302 | - Updated dependencies to avoid yanked dependency versions 303 | - Improve documentation: Add documentation on how to list bors' commits for bisections to a PR 304 | - Improve documentation: Update tutorial 305 | 306 | ## v0.1.0 307 | 308 | - initial release 309 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Mark Simulacrum "] 3 | build = "build.rs" 4 | categories = ["development-tools"] 5 | description = "Bisects rustc toolchains with rustup" 6 | keywords = ["rustup"] 7 | license = "MIT OR Apache-2.0" 8 | name = "cargo-bisect-rustc" 9 | readme = "README.md" 10 | repository = "https://github.com/rust-lang/cargo-bisect-rustc" 11 | version = "0.6.10" 12 | edition = "2021" 13 | 14 | [dependencies] 15 | dialoguer = { version = "0.11.0", default-features = false } 16 | home = "0.5" 17 | env_logger = "0.11.0" 18 | thiserror = "2" 19 | anyhow = "1" 20 | flate2 = "1.1.0" 21 | git2 = "0.20.2" 22 | log = "0.4" 23 | pbr = "1.1.1" 24 | reqwest = { version = "0.12.1", features = ["blocking", "json"] } 25 | rustc_version = "0.4.0" 26 | serde = { version = "1.0.145", features = ["derive"] } 27 | serde_json = "1.0" 28 | clap = { version = "4.5", features = ["derive", "wrap_help"] } 29 | tar = "0.4" 30 | tee = "0.1" 31 | tempfile = "3.20.0" 32 | xz2 = "0.1.7" 33 | chrono = "0.4.22" 34 | colored = "3" 35 | regex = "1.11.0" 36 | 37 | [dev-dependencies] 38 | quickcheck = "1" 39 | trycmd = "0.15.0" 40 | -------------------------------------------------------------------------------- /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 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /MAINTAINING.md: -------------------------------------------------------------------------------- 1 | # Maintaining cargo-bisect-rustc 2 | 3 | ## Publishing 4 | 5 | To publish a new release: 6 | 7 | 1. Create a PR to bump the version in `Cargo.toml` and `Cargo.lock`, and update [`CHANGELOG.md`](CHANGELOG.md). 8 | 2. After the merge is complete, create a new release. There are two approaches: 9 | - GUI: Create a new release in the UI, tag and title should be `v` and the version number. Copy a link to the changelog. 10 | - CLI: Run the following in the repo: 11 | ```bash 12 | VERSION="`cargo read-manifest | jq -r .version`" ; \ 13 | gh release create -R rust-lang/cargo-bisect-rustc v$VERSION \ 14 | --title v$VERSION \ 15 | --notes "See https://github.com/rust-lang/cargo-bisect-rustc/blob/master/CHANGELOG.md#v${VERSION//.} for a complete list of changes." 16 | ``` 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cargo Bisection 2 | 3 | [![CI](https://github.com/rust-lang/cargo-bisect-rustc/actions/workflows/ci.yml/badge.svg)](https://github.com/rust-lang/cargo-bisect-rustc/actions/workflows/ci.yml) 4 | 5 | This tool bisects either Rust nightlies or CI artifacts. 6 | 7 | [**Documentation**](https://rust-lang.github.io/cargo-bisect-rustc/) 8 | 9 | To run the documentation book locally, install [mdBook](https://github.com/rust-lang/mdBook): 10 | 11 | ``` sh 12 | cd guide 13 | mdbook serve # launch a local server to allow you to easily see and update changes you make 14 | mdbook build # build the book HTML 15 | ``` 16 | 17 | ## License 18 | 19 | Licensed under either of 20 | 21 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) 22 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) 23 | 24 | at your option. 25 | 26 | ### Contribution 27 | 28 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the 29 | work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 30 | additional terms or conditions. 31 | -------------------------------------------------------------------------------- /TUTORIAL.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | The tutorial has moved to the new guide at . 4 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | fn main() { 4 | println!("cargo:rustc-env=HOST={}", env::var("TARGET").unwrap()); 5 | // Prevents cargo from scanning the whole directory for changes. 6 | println!("cargo:rerun-if-changed=build.rs"); 7 | } 8 | -------------------------------------------------------------------------------- /guide/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /guide/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | language = "en" 3 | multilingual = false 4 | src = "src" 5 | title = "cargo-bisect-rustc" 6 | 7 | [output.html] 8 | curly-quotes = true 9 | git-repository-url = "https://github.com/rust-lang/cargo-bisect-rustc/tree/master/guide/src" 10 | edit-url-template = "https://github.com/rust-lang/cargo-bisect-rustc/edit/master/guide/{path}" 11 | -------------------------------------------------------------------------------- /guide/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](introduction.md) 4 | - [Installation](installation.md) 5 | - [Basic usage](usage.md) 6 | - [Tutorial](tutorial.md) 7 | - [Rust source repo](rust-src-repo.md) 8 | - [Bisection boundaries](boundaries.md) 9 | - [Rustup toolchains](rustup.md) 10 | - [Git bisect a custom build](git-bisect.md) 11 | - [Alt builds](alt.md) 12 | - [Examples](examples/index.md) 13 | - [Checking diagnostics](examples/diagnostics.md) 14 | - [Scripting on Windows](examples/windows-scripting.md) 15 | - [Incremental compilation](examples/incremental.md) 16 | - [Slow or hung compilation](examples/slow.md) 17 | - [Using extra components](examples/components.md) 18 | - [Running without Cargo](examples/without-cargo.md) 19 | - [Preserving toolchains](examples/preserve.md) 20 | - [Bisecting Rustdoc](examples/rustdoc.md) 21 | - [Bisecting Clippy](examples/clippy.md) 22 | - [Documentation changes](examples/doc-change.md) 23 | - [Flaky errors](examples/flaky.md) 24 | -------------------------------------------------------------------------------- /guide/src/alt.md: -------------------------------------------------------------------------------- 1 | # Alt builds 2 | 3 | Each commit also generates what are called "alt" builds. 4 | These are builds of rustc with some different options set. 5 | As of August 2023, these include: 6 | 7 | * `rust.parallel-compiler` 8 | * `llvm.assertions` 9 | * `rust.verify-llvm-ir` 10 | 11 | For more information on these settings, see the [`config.toml` docs]. 12 | These alt settings are defined in [`ci/run.sh`]. 13 | 14 | Alt builds are only available for a few targets. 15 | Look for the `-alt` builds in [`ci.yml`]. 16 | 17 | This can be useful if you are bisecting an LLVM issue. 18 | With LLVM assertions enabled, alt builds have checks that can help identify broken assumptions. 19 | 20 | Alt builds are only made for commit builds, and not nightly releases. 21 | You will need to specify `--by-commit` (or use a hash in the `--start` or `--end` flags) to only use commit builds. 22 | 23 | ```sh 24 | cargo bisect-rustc --alt --by-commit 25 | ``` 26 | 27 | [`config.toml` docs]: https://github.com/rust-lang/rust/blob/master/config.example.toml 28 | [`ci/run.sh`]: https://github.com/rust-lang/rust/blob/c0b6ffaaea3ebdf5f7a58fc4cf7ee52c91077fb9/src/ci/run.sh#L99-L105 29 | [`ci.yml`]: https://github.com/rust-lang/rust/blob/HEAD/src/ci/github-actions/ci.yml 30 | -------------------------------------------------------------------------------- /guide/src/boundaries.md: -------------------------------------------------------------------------------- 1 | # Bisection boundaries 2 | 3 | `cargo-bisect-rustc` does a binary search for the regression using a *start* and *end* boundary. 4 | You can specify these boundaries with the `--start` and `--end` CLI flags. 5 | There are several ways to specify what those boundaries are. 6 | If you run the command without specifying the boundaries, it will search for them automatically: 7 | 8 | ```sh 9 | # No --start or --end flags 10 | cargo bisect-rustc 11 | ``` 12 | 13 | This will assume the latest nightly is a regression (the *end* boundary). 14 | It will then search backwards until it can find a nightly that passes to use as the *start* boundary. 15 | Bisection can usually go faster if you happen to know the start boundary, so that it doesn't need to search for it. 16 | 17 | `--start` and `--end` are optional. 18 | If `--start` is not specified, then it will try to find the start range automatically. 19 | If `--end` is not specified, it will assume it is the most recently available. 20 | 21 | ## Date boundaries 22 | 23 | You can pass a date in the form YYYY-MM-DD to the `--start` and `--end` flags. 24 | It will download the nightly corresponding to that date, and then begin bisecting those nightlies. 25 | 26 | ```sh 27 | cargo bisect-rustc --start=2018-08-14 --end=2018-10-11 28 | ``` 29 | 30 | If the nightly with the regression was within the past 167 days, then it will automatically start bisecting the individual PRs merged on that day using [Git commit boundaries](#git-commit-boundaries). 31 | 32 | ## Git commit boundaries 33 | 34 | You can pass the particular git commit hash of a PR as a boundary. 35 | The Rust project keeps the builds of every merged PR for the last 167 days. 36 | If you happen to know the PR to use as a boundary, you can pass the SHA-1 hash of that PR. 37 | 38 | ```sh 39 | cargo bisect-rustc \ 40 | --start=6323d9a45bdf0ac2a9319a6a558537e0a7e6abd1 \ 41 | --end=866a713258915e6cbb212d135f751a6a8c9e1c0a 42 | ``` 43 | 44 | There are several ways to determine the SHA-1 hash for a PR. 45 | 46 | - On the PR itself, you should see a message like "bors merged commit c50c62d into `rust-lang:master`". 47 | You can copy that hash to use as a boundary. 48 | If the PR was merged as part of a rollup, you will need to use the hash of the rollup instead. 49 | You'll need to look through the PR messages to see if the PR was mentioned from a rollup PR. 50 | - In the rust repo, run `git log --first-parent upstream/master` (where `upstream` is your origin name for `rust-lang/rust`). 51 | This will show all the top-level commits. 52 | You can then search for your PR. 53 | 54 | > **Note**: If the PR was merged after the most recent nightly, you'll need to be sure to also specify the `--end` range. 55 | > Otherwise it will assume the most recent nightly is the *end* and it won't work if the start is after the end. 56 | 57 | If the regression is found in a [rollup PR], then `cargo-bisect-rustc` will bisect the individual PRs within the rollup. 58 | This final bisection is only available for `x86_64-unknown-linux-gnu` since it is using the builds made for the [rustc performance tracker]. 59 | 60 | > **Note**: If you specify date boundaries, then you can use the `--by-commit` CLI option to force it to use PR commits instead of nightlies. 61 | 62 | [rollup PR]: https://forge.rust-lang.org/release/rollups.html 63 | [rustc performance tracker]: https://perf.rust-lang.org/ 64 | 65 | ## Git tag boundaries 66 | 67 | The boundary can be specified with a git release tag. 68 | This is useful if you know something works in one release and not another, but you don't happen to know which nightly this corresponds with. 69 | When given a tag, `cargo-bisect-rustc` will try to find the nightly that corresponds with that release. 70 | For example: 71 | 72 | ```sh 73 | cargo bisect-rustc --start=1.58.0 --end=1.59.0 74 | ``` 75 | 76 | ## Monotonicity 77 | 78 | When writing your test and picking a bisection range, you should be careful to ensure that the test won't vary between pass/fail over the bisection range. 79 | It should only transition from good to bad once in the bisection range (it must change 80 | [monotonically]). 81 | 82 | In the following example, `cargo-bisect-rustc` will find one of the transitions, but that may not be the true root cause of the issue you are investigating. 83 | 84 | ```text 85 | nightly-2023-02-01 baseline **start** 86 | nightly-2023-02-02 baseline 87 | nightly-2023-02-03 baseline 88 | nightly-2023-02-04 regression 89 | nightly-2023-02-05 regression 90 | nightly-2023-02-06 baseline 91 | nightly-2023-02-07 regression 92 | nightly-2023-02-08 regression **end** 93 | ``` 94 | 95 | Here it may either find 2023-02-04 or 2023-02-07 as the regression. 96 | 97 | The following are some suggestions for avoiding or dealing with this problem: 98 | 99 | - Make sure your test reliably exhibits the issue you are looking for, and does not generate any false positives or false negatives. 100 | - Analyze the PR that was reported as the regression. 101 | Do the changes in the PR seem to be a probable cause? 102 | - Try to keep the bisection range small to reduce the probability that you will encounter multiple regression transitions. 103 | - Use the `-vv` flag (very verbose) to display the output from the compiler to make sure it is what you expect. 104 | - Use the [`--prompt`](tutorial.md#testing-interactively) flag to inspect the output and verify each step. 105 | - Beware that some issues may get fixed and then regress multiple times. 106 | Try to keep the bisection range as close to the present day as possible. 107 | Compare the output of the "regressed" commit to the latest nightly to see if they are the same. 108 | - If the test only fails sporadically, use a [script](examples/flaky.md) to run the compiler many times until it fails or it passes enough iterations that you feel confident that it is good. 109 | - If the code requires relatively new language features, be careful not to pick a starting range that is too old. 110 | - Beware of code-generation bugs that can be sensitive to code layout. 111 | Since the code to rustc changes rapidly over time, code can shift around causing different layouts and optimizations, which might cause an issue to appear and disappear several times over the bisection range. 112 | 113 | [monotonically]: https://en.wikipedia.org/wiki/Bisection_(software_engineering)#Monotonicity 114 | -------------------------------------------------------------------------------- /guide/src/examples/clippy.md: -------------------------------------------------------------------------------- 1 | # Bisecting clippy 2 | 3 | `cargo-bisect-rustc` can be used to check for Clippy regressions, too. 4 | You'll need to instruct it to download clippy, and run the command correctly: 5 | 6 | ```sh 7 | cargo bisect-rustc --start=1.67.0 --end=1.68.0 -c clippy -- clippy 8 | ``` 9 | 10 | Note that depending on what you are looking for, this may just find a PR that syncs the [`rust-clippy`] repo to `rust-lang/rust`. 11 | You may be able to scan the list of changes in that PR to discover what you are looking for. 12 | If the list of changes is too big or nothing is jumping out as a possible culprit, then consider using [`git bisect`] on the clippy repo itself (which will require building clippy). 13 | 14 | To bisect a clippy warning, you can upgrade the warning to an error: 15 | 16 | ```sh 17 | cargo bisect-rustc --start=1.84.0 --end=1.85.0 -c clippy -- clippy -- --forbid clippy::useless_conversion 18 | ``` 19 | 20 | [`rust-clippy`]: https://github.com/rust-lang/rust-clippy/ 21 | [`git bisect`]: https://git-scm.com/docs/git-bisect 22 | -------------------------------------------------------------------------------- /guide/src/examples/components.md: -------------------------------------------------------------------------------- 1 | # Using extra components 2 | 3 | By default, `cargo-bisect-rustc` only fetches `rustc`, `cargo`, `rustdoc`, and the standard library for the host. 4 | You may need additional [Rustup Components](https://rust-lang.github.io/rustup/concepts/components.html) to run your test. 5 | Some examples of when this might be needed are: 6 | 7 | * You want to find a regression in Clippy (see [Bisecting Clippy](clippy.md)), or miri. 8 | * Scanning for when some documentation changed (see [Documentation changes](doc-change.md)). 9 | * The platform needs additional things. 10 | For example, bisecting `x86_64-pc-windows-gnu` host may need the `rust-mingw` component. 11 | 12 | If you are testing cross-compilation, use the `--target` option to download the standard library for the target you are using. 13 | 14 | The following example shows how to use components to do a bisection with Cargo's [build-std](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std) feature. 15 | 16 | ```sh 17 | cargo-bisect-rustc --start=2022-11-01 --end=2022-11-20 -c rust-src -- build -Zbuild-std 18 | ``` 19 | 20 | > **Note**: The `--with-src` option is an alias for `-c rust-src`. \ 21 | > The `--with-dev` option is an alias for `-c rustc-dev -c llvm-tools`. 22 | -------------------------------------------------------------------------------- /guide/src/examples/diagnostics.md: -------------------------------------------------------------------------------- 1 | # Checking diagnostics 2 | 3 | The following is an example of checking when the diagnostic output of `rustc` *changes*. 4 | For example, this can check when either the wording has changed, or a different error or warning is produced. 5 | 6 | [#109067](https://github.com/rust-lang/rust/issues/109067) is an example of where this is necessary. 7 | A warning started being emitted, and it is the kind of warning that cannot be turned into an error with `deny(warnings)`. 8 | 9 | The following script is intended to be used with the `--script` option (set the executable flag on the script, `chmod u+x`): 10 | 11 | ```sh 12 | #!/bin/sh 13 | 14 | OUTPUT=`cargo check 2>&1` 15 | # Comment out this test if your example is intended to fail. 16 | if [ $? -ne 0 ] 17 | then 18 | echo "Build unexpectedly failed: $OUTPUT" 19 | exit 1 20 | fi 21 | # Display the output for debugging purposes. 22 | # Run `cargo-bisect-rustc` with `-vv` to view the output. 23 | echo "$OUTPUT" 24 | # This indicates a regression when the text "non-ASCII" is in the output. 25 | # 26 | # If the regression is when the text is *not* in the output, remove the `!` prefix 27 | # (and customize the `--term-old` and `--term-new` CLI options if you want). 28 | ! echo "$OUTPUT" | grep "non-ASCII" 29 | ``` 30 | 31 | Then run something like: 32 | 33 | ```sh 34 | cargo bisect-rustc --start=1.67.0 --end=1.68.0 --script ./test.sh \ 35 | --term-old="No warning" --term-new="Found non-ASCII warning" 36 | ``` 37 | -------------------------------------------------------------------------------- /guide/src/examples/doc-change.md: -------------------------------------------------------------------------------- 1 | # Documentation changes 2 | 3 | `cargo-bisect-rustc` can be used to scan for changes in the documentation shipped with each release. 4 | This includes all the books and standard library documentation. 5 | To do this, instruct it to download the component, and use a script that scans for whatever you are looking for. 6 | You can use `rustup doc --path` or `rustc --print=sysroot` to find the proper location. 7 | For example: 8 | 9 | `test.sh`: 10 | ```sh 11 | #!/bin/sh 12 | 13 | # Exit if any command fails. 14 | set -e 15 | 16 | STD=`dirname $(rustup doc --std --path)` 17 | 18 | # Checks if a particular file exists. 19 | # This could also be `grep` or any other kinds of tests you need. 20 | if [ -e $STD/io/error/type.RawOsError.html ] 21 | then 22 | echo "found" 23 | exit 1 24 | fi 25 | ``` 26 | 27 | And run with: 28 | 29 | ```sh 30 | cargo bisect-rustc --start 1.68.0 --end 1.69.0 -c rust-docs --script ./test.sh \ 31 | --term-old="Did not find" --term-new="Found" 32 | ``` 33 | 34 | > **Note**: This may not work on all targets since `cargo-bisect-rustc` doesn't properly handle rustup manifests, which alias some targets to other targets. 35 | > Use `--host x86_64-unknown-linux-gnu` in that situation. 36 | -------------------------------------------------------------------------------- /guide/src/examples/flaky.md: -------------------------------------------------------------------------------- 1 | # Flaky errors 2 | 3 | Some tests may fail randomly. 4 | The following script is an example that will run `rustc` repeatedly to check for a failure. 5 | This example is from [#108216](https://github.com/rust-lang/rust/issues/108216) (which requires macOS). 6 | 7 | `test.sh`: 8 | ```sh 9 | #!/bin/sh 10 | 11 | rm -rf *.o incremental foo 12 | 13 | echo "fn main() { let a: i64 = 1 << 64; }" > foo1.rs 14 | echo "fn main() { let a: i64 = 1 << 63; }" > foo2.rs 15 | 16 | ARGS="--crate-name foo -C split-debuginfo=unpacked -C debuginfo=2 -C incremental=incremental" 17 | 18 | for i in {1..20} 19 | do 20 | echo run $i 21 | rustc foo1.rs $ARGS && { echo "ERROR: first build should have failed"; exit 1; } 22 | rustc foo2.rs $ARGS || { echo "ERROR: second build should have passed"; exit 1; } 23 | ./foo || { echo "ERROR: executing should have passed"; exit 1; } 24 | done 25 | ``` 26 | 27 | This test can be run with: 28 | 29 | ```sh 30 | cargo bisect-rustc --start=1.57.0 --end=1.58.0 --script=./test.sh 31 | ``` 32 | 33 | In general, configure the script to perform whichever actions you need in a `for` loop that runs enough times that you have a high confidence it has found the regression. 34 | -------------------------------------------------------------------------------- /guide/src/examples/incremental.md: -------------------------------------------------------------------------------- 1 | # Incremental compilation 2 | 3 | Testing for regressions with incremental compilation may require running a command multiple times. 4 | The following illustrates an example for [#87384](https://github.com/rust-lang/rust/issues/87384) which only generates a warning the second time a build is run with incremental. 5 | Previously no warning was emitted. 6 | 7 | `foo.rs`: 8 | ```rust 9 | #![type_length_limit = "95595489"] 10 | 11 | pub fn main() { 12 | println!("Hello, world!"); 13 | } 14 | ``` 15 | 16 | Create a script `test.sh`: 17 | 18 | ```sh 19 | #!/bin/sh 20 | 21 | # Exit if any command fails. 22 | set -e 23 | 24 | rm -rf incremental 25 | rustc foo.rs --crate-type lib -C incremental=incremental 26 | echo second 27 | OUTPUT=`rustc foo.rs --crate-type lib -C incremental=incremental 2>&1` 28 | echo $OUTPUT 29 | ! echo "$OUTPUT" | grep \ 30 | "crate-level attribute should be in the root module" 31 | ``` 32 | 33 | Run this script with: 34 | 35 | ```sh 36 | cargo-bisect-rustc --start 1.54.0 --end 1.55.0 --script ./test.sh 37 | ``` 38 | -------------------------------------------------------------------------------- /guide/src/examples/index.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | The following chapters show examples of different ways of using `cargo-bisect-rustc`. 4 | -------------------------------------------------------------------------------- /guide/src/examples/preserve.md: -------------------------------------------------------------------------------- 1 | # Preserving toolchains 2 | 3 | You may want to reuse the toolchains downloaded by `cargo-bisect-rustc` for doing further analysis or debugging. 4 | Or, while setting up your regression test, you may need to adjust your test and script several times, and downloading the same toolchains multiple times can be quite slow. 5 | 6 | You can do this with the `--preserve` option. 7 | 8 | ```sh 9 | cargo bisect-rustc --start=2023-01-01 --end=2023-02-01 --preserve 10 | ``` 11 | 12 | The toolchains will be kept in your Rustup home directory (typically `~/.rustup/toolchains`). 13 | 14 | Toolchains for nightlies will have the form of `bisector-nightly-YYYY-MM-DD-`. 15 | Toolchains for PR artifacts will have the form of `bisector-ci--`. 16 | 17 | You can run these toolchains using a Rustup override, like this: 18 | 19 | ```sh 20 | cargo +bisector-nightly-2023-03-18-x86_64-unknown-linux-gnu build 21 | # or... 22 | cargo +bisector-ci-e187f8871e3d553181c9d2d4ac111197a139ca0d-x86_64-unknown-linux-gnu build 23 | ``` 24 | 25 | When you are done, you'll probably want to clean up these directories since they use a lot of space. 26 | The easiest method is to just delete the directories: 27 | 28 | ```sh 29 | rm -rf ~/.rustup/toolchains/bisector-* 30 | ``` 31 | 32 | ## Manually installing 33 | 34 | The `--install` option can be used to only install a toolchain. 35 | This won't do a bisection, it is just for fetching a toolchain for testing. 36 | 37 | ```sh 38 | cargo bisect-rustc --install e187f8871e3d553181c9d2d4ac111197a139ca0d 39 | ``` 40 | 41 | > **Note**: See also [`rustup-toolchain-install-master`](https://github.com/kennytm/rustup-toolchain-install-master) which is specialized for installing CI artifacts. 42 | -------------------------------------------------------------------------------- /guide/src/examples/rustdoc.md: -------------------------------------------------------------------------------- 1 | # Bisecting Rustdoc 2 | 3 | `cargo-bisect-rustc` can be used to check for Rustdoc regressions, too. 4 | All you need to do is instruct it to use the correct command. 5 | 6 | The following example will check to find a regression when `cargo doc` suddenly starts to fail. 7 | 8 | ```sh 9 | cargo bisect-rustc --start=2022-08-05 --end=2022-09-09 -- doc 10 | ``` 11 | 12 | Some rustdoc regressions might be in the generated HTML output. 13 | To scan the output, you can use a script like the following: 14 | 15 | `test.sh`: 16 | ```sh 17 | #!/bin/sh 18 | 19 | # Exit if any command fails. 20 | set -e 21 | 22 | cargo doc 23 | 24 | grep "some example text" $CARGO_TARGET_DIR/doc/mycrate/fn.foo.html 25 | ``` 26 | 27 | This can be used with the `--script` option: 28 | 29 | ```sh 30 | cargo-bisect-rustc --start=2023-01-22 --end=2023-03-18 --script=./test.sh \ 31 | --term-old="Found example text" --term-new="Failed, or did not find text" 32 | ``` 33 | -------------------------------------------------------------------------------- /guide/src/examples/slow.md: -------------------------------------------------------------------------------- 1 | # Slow or hung compilation 2 | 3 | Some regressions may involve the compiler hanging or taking an unusually long time to run. 4 | The `--timeout` CLI option can be used to check for this. 5 | Let's use [#89524](https://github.com/rust-lang/rust/issues/89524) as an example. 6 | A particular combination of factors caused the compiler to start to hang. 7 | 8 | Change `Cargo.toml` to the following: 9 | 10 | ```toml 11 | [package] 12 | name = "slow" 13 | version = "0.1.0" 14 | 15 | [dependencies] 16 | config = "=0.9.3" 17 | 18 | [profile.release] 19 | panic = "abort" 20 | codegen-units = 1 21 | ``` 22 | 23 | Then use the timeout option: 24 | 25 | ```sh 26 | cargo-bisect-rustc --start=2021-09-01 --end=2021-10-02 --timeout 30 -- build --release 27 | ``` 28 | 29 | You may need to adjust the timeout value based on the speed of your system. 30 | 31 | > **Note**: `--timeout` is currently not working on macOS. See . 32 | 33 | In some cases bisecting if a timeout happens is not enough, there might also be a compilation error (see for example [rustc#139197](https://github.com/rust-lang/rust/issues/139197)). In this case the script should handle all the work, here's a Bash example (given `main.rs` is the Rust code to reproduce the hanging compilation): 34 | ```sh 35 | #!/bin/bash 36 | res=$( timeout 3 rustc main.rs ) 37 | if [ "$?" -eq 124 ]; then 38 | # Excessive compile time 39 | exit 1 40 | else 41 | # Compilation fails as expected *but* it doesn't hang 42 | exit 0 43 | fi 44 | ``` 45 | 46 | and then run (example): 47 | ```sh 48 | cargo-bisect-rustc [...params...] --script test.sh 49 | ``` 50 | -------------------------------------------------------------------------------- /guide/src/examples/windows-scripting.md: -------------------------------------------------------------------------------- 1 | # Scripting on Windows 2 | 3 | Using the `--script` option on Windows can be cumbersome because Windows does not support `#!` scripts like Unix does, and the built-in scripting can also be awkward. 4 | The following sections show the different ways you can use scripting. 5 | 6 | ## Batch file 7 | 8 | You can use DOS-style `.bat` files: 9 | 10 | `test.bat`: 11 | ```bat 12 | (cargo check 2>&1) | find "E0642" 13 | ``` 14 | 15 | This can be executed directly with: 16 | 17 | ```sh 18 | cargo-bisect-rustc --script ./test.bat 19 | ``` 20 | 21 | But `.bat` can be challenging to do more complex options, or you may not be familiar with it. 22 | 23 | ## Powershell 24 | 25 | You can't execute `.ps1` Powershell files directly, so you will need to use `pwsh` to launch them: 26 | 27 | `test.ps1`: 28 | ```powershell 29 | ( cargo check 2>&1 ) | grep E0642 30 | if ( -Not $? ) { 31 | exit 1 32 | } 33 | ``` 34 | 35 | This can be run with: 36 | 37 | ```sh 38 | cargo-bisect-rustc --script pwsh -- -File ./test.ps1 39 | ``` 40 | 41 | ## Bash 42 | 43 | If you have Git-for-Windows installed, then you can use its copy of bash to run bash scripts: 44 | 45 | `test.sh`: 46 | ```sh 47 | #!/bin/bash 48 | 49 | cargo check 2>&1 | grep E0642 50 | ``` 51 | 52 | This can be run with: 53 | 54 | ```sh 55 | cargo-bisect-rustc --script "C:\\Program Files\\Git\\usr\\bin\\bash.exe" -- ./test.sh 56 | ``` 57 | 58 | This also works if you have bash from something like msys2 installed. 59 | -------------------------------------------------------------------------------- /guide/src/examples/without-cargo.md: -------------------------------------------------------------------------------- 1 | # Running without cargo 2 | 3 | Some bisections don't require Cargo. 4 | You can use the `--without-cargo` option to skip installing cargo which can speed up the bisection since it doesn't need to download cargo, and doesn't have the overhead of running cargo. 5 | You will need to pair this with `--script` since `cargo-bisect-rustc` assumes projects use Cargo. 6 | 7 | For example, using a simple `rustc` command: 8 | 9 | ```sh 10 | cargo-bisect-rustc --start=2022-11-01 --end=2022-11-20 --without-cargo --script=rustc -- foo.rs 11 | ``` 12 | 13 | > **Note**: You can use `--without-cargo` while still using a Cargo project. 14 | > Rustup will fall back to using `cargo` from your installed nightly, beta, or stable toolchain. 15 | > However, this isn't recommended since `cargo` is only intended to work with the version it is released with, and can sometimes be incompatible with different versions. 16 | > But if you are bisecting a very recent change, then you can probably get away with it. 17 | -------------------------------------------------------------------------------- /guide/src/git-bisect.md: -------------------------------------------------------------------------------- 1 | # Git bisect a custom build 2 | 3 | There are some rare cases where you may need to build `rustc` with custom options, or otherwise work around issues with pre-built compilers not being available. 4 | For this you can use [`git bisect`] to build the compiler locally. 5 | 6 | It can be helpful to use the `--first-parent` option so that it only bisects the merge commits directly reachable on the master branch. 7 | Otherwise the bisecting may land on intermediate commits from within a PR which may not build or test correctly. 8 | 9 | To start the bisection, specifying the boundaries where the bisection will start: 10 | 11 | ```sh 12 | git bisect start --first-parent 13 | git bisect good 96ddd32c4bfb1d78f0cd03eb068b1710a8cebeef 14 | git bisect bad a00f8ba7fcac1b27341679c51bf5a3271fa82df3 15 | ``` 16 | 17 | Then, build the compiler as needed and run your tests to check for a regression: 18 | 19 | ```sh 20 | ./x.py build std 21 | rustc +stage1 foo.rs 22 | ``` 23 | 24 | You may want to consider running `./x.py clean` if you are running into issues since changes to the internal structures of build artifacts aren't always versioned, and those changes can be incompatible. 25 | Incremental caches are particularly susceptible, so you may want to turn that off if you have turned them on. 26 | 27 | If you determine the current version is good or bad, run `git bisect good` or `git bisect bad` to mark that, and then repeat building and marking until finished. 28 | 29 | Similar to `cargo-bisect-rustc`, `git bisect` supports scripting and lots of other goodies. 30 | Check out its documentation for more. 31 | 32 | [`git bisect`]: https://git-scm.com/docs/git-bisect 33 | -------------------------------------------------------------------------------- /guide/src/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | The basic method for installing `cargo-bisect-rustc` is: 4 | 5 | ```sh 6 | cargo install cargo-bisect-rustc 7 | ``` 8 | 9 | Additional options are described below. 10 | 11 | ## Requirements 12 | 13 | Besides having a working Rust installation, you may need a few other things installed on your system: 14 | 15 | - Unix: 16 | - pkg-config 17 | - OpenSSL (`libssl-dev` on Ubuntu, `openssl-devel` on Fedora or Alpine) 18 | - macOS: 19 | - OpenSSL ([homebrew] is recommended to install the `openssl` package) 20 | - [rustup] 21 | 22 | [homebrew]: https://brew.sh/ 23 | [rustup]: https://rustup.rs/ 24 | 25 | If you're having trouble using the system OpenSSL installation, it can be built from scratch. 26 | The following will enable the vendored OpenSSL build: 27 | 28 | ```sh 29 | cargo install cargo-bisect-rustc --features git2/vendored-openssl 30 | ``` 31 | 32 | Beware that this also requires `perl` and `make` to be installed. 33 | 34 | ## `RUST_SRC_REPO` 35 | 36 | `cargo-bisect-rustc` needs to access the git log of the rust repo. 37 | You can set the default location of that when installing it: 38 | 39 | ```sh 40 | RUST_SRC_REPO=/path/to/rust cargo install cargo-bisect-rustc 41 | ``` 42 | 43 | See [Rust source repo] for more about configuring how `cargo-bisect-rustc` retrieves this information. 44 | 45 | [Rust source repo]: rust-src-repo.md 46 | -------------------------------------------------------------------------------- /guide/src/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | The [`cargo-bisect-rustc`] tool makes it super easy to find exactly when behavior has regressed in rustc. 4 | It automatically downloads rustc artifacts and tests them against a project you provide until it finds the regression. 5 | 6 | The [Installation](installation.md) chapter shows how to install `cargo-bisect-rustc`. 7 | For a quick introduction, see the [Tutorial](tutorial.md). 8 | Otherwise, start at the [Basic usage](usage.md) chapter to learn how `cargo-bisect-rustc` works. 9 | 10 | [`cargo-bisect-rustc`]: https://github.com/rust-lang/cargo-bisect-rustc 11 | -------------------------------------------------------------------------------- /guide/src/rust-src-repo.md: -------------------------------------------------------------------------------- 1 | # Rust source repo 2 | 3 | For `cargo-bisect-rustc` to work, it needs to be able to read the git log of the [`rust-lang/rust`] repo. 4 | `cargo-bisect-rustc` supports several methods for this described below. 5 | 6 | ## GitHub API 7 | 8 | By default, `cargo-bisect-rustc` uses the GitHub API to fetch the information instead of using a local checkout. 9 | 10 | ```sh 11 | cargo bisect-rustc --access=github 12 | ``` 13 | 14 | Beware that GitHub has restrictive rate limits for unauthenticated requests. 15 | It allows 60 requests per hour, and `cargo-bisect-rustc` will use about 10 requests each time you run it (which can vary depending on the bisection). 16 | If you run into the rate limit, you can raise it to 5000 requests per hour by setting the `GITHUB_TOKEN` environment variable to a [GitHub personal token]. 17 | If you use the [`gh` CLI tool], you can use it to get a token: 18 | 19 | ```sh 20 | GITHUB_TOKEN=`gh auth token` cargo bisect-rustc --access=github 21 | ``` 22 | 23 | If you don't use `gh`, you'll just need to copy and paste the token. 24 | 25 | ## Local clone 26 | 27 | `cargo-bisect-rustc` can also clone the rust repo in the current directory (as `rust.git`). 28 | This option can be quite slow if you don't specify the repo path at build time. 29 | You can specify this with the `--access` CLI argument: 30 | ```sh 31 | cargo bisect-rustc --access=checkout 32 | ``` 33 | 34 | ## `RUST_SRC_REPO` environment variable 35 | 36 | You can specify the location of the rust repo with the `RUST_SRC_REPO` environment variable at runtime. 37 | This is useful if you already have it checked out somewhere, but is cumbersome to use. 38 | 39 | ```sh 40 | RUST_SRC_REPO=/path/to/rust cargo bisect-rustc 41 | ``` 42 | 43 | ## `RUST_SRC_REPO` environment variable (build-time) 44 | 45 | Setting the `RUST_SRC_REPO` environment variable when installing `cargo-bisect-rustc` will set the default location for the rust repo. 46 | This is recommended if you already have the rust repo checked out somewhere. 47 | 48 | ```sh 49 | RUST_SRC_REPO=/path/to/rust cargo install cargo-bisect-rustc 50 | ``` 51 | 52 | [`rust-lang/rust`]: https://github.com/rust-lang/rust/ 53 | [GitHub personal token]: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token 54 | [`gh` CLI tool]: https://cli.github.com/ 55 | -------------------------------------------------------------------------------- /guide/src/rustup.md: -------------------------------------------------------------------------------- 1 | # Rustup toolchains 2 | 3 | `cargo-bisect-rustc` takes advantage of [rustup toolchains] for installation and selecting the correct `rustc` to run. 4 | It will essentially run `cargo +bisector-nightly-2023-03-18-x86_64-unknown-linux-gnu build` using rustup [toolchain override shorthand] to run the toolchains that it downloads. 5 | This sets the `RUSTUP_TOOLCHAIN` environment variable to the toolchain name, which ensures that any call to `rustc` will use the correct toolchain. 6 | 7 | By default, `cargo-bisect-rustc` will delete toolchains immediately after using them. 8 | You can use the `--preserve` option to keep the toolchains so that you can use them manually. 9 | See the [Preserving toolchains] example for more details. 10 | 11 | When using the `--script` option, the script should just invoke `cargo` or `rustc` normally, and rely on the `RUSTUP_TOOLCHAIN` environment variable to pick the correct toolchain. 12 | 13 | [rustup toolchains]: https://rust-lang.github.io/rustup/concepts/toolchains.html 14 | [toolchain override shorthand]: https://rust-lang.github.io/rustup/overrides.html#toolchain-override-shorthand 15 | [Preserving toolchains]: examples/preserve.md 16 | -------------------------------------------------------------------------------- /guide/src/tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | `cargo-bisect-rustc` works by building a Cargo project and checking if it succeeds or fails. 4 | This tutorial walks through an example of this process. 5 | 6 | ## Finding a regression 7 | 8 | Create a cargo project that demonstrates the regression. 9 | Let's use [issue #53157] as an example: 10 | 11 | ```sh 12 | cargo new foo 13 | cd foo 14 | ``` 15 | 16 | Edit `src/main.rs` with the example from the issue: 17 | 18 | ```rust 19 | macro_rules! m { 20 | () => {{ 21 | fn f(_: impl Sized) {} 22 | f 23 | }} 24 | } 25 | 26 | fn main() { 27 | fn f() -> impl Sized {}; 28 | m!()(f()); 29 | } 30 | ``` 31 | 32 | Since we are testing an old regression, also edit `Cargo.toml` to remove the `edition = "2021"` field which isn't supported in these versions. 33 | 34 | Then run `cargo bisect-rustc --end=2018-08-04`. 35 | 36 | We need to provide the end point for this particular example because that's an old regression already fixed on the latest nightlies. 37 | We could also provide a start point if we know one; 38 | that's going to make it faster by avoiding scanning for the start. 39 | For instance: 40 | 41 | ```sh 42 | cargo bisect-rustc --start=2018-05-07 --end=2018-08-04 43 | ``` 44 | 45 | It will run `cargo build` in the project and check whether or not it fails. 46 | It will do a binary search between the start and end range to find exactly where the regression occurred. 47 | 48 | > **Note**: You can also use the flag [`--regress`] to specify other common regression criteria, e.g. `--regress=ice` for internal compiler errors. 49 | 50 | [`--regress`]: usage.md#regression-check 51 | 52 | In our example, in just a few steps, we can we find that it stopped working on `nightly-2018-07-30`. 53 | 54 | If the regression is recent enough, then it will print out a list of PRs that were committed on that date. 55 | In this particular example, it is too old, so we'll need to manually inspect the git log to see which PR's were merged. 56 | 57 | If the nightly was within the last 167 days, then `cargo-bisect-rustc` will then start bisecting those individual PRs. 58 | 59 | After finding potential candidates, you can go inspect those PRs to see which one is the likely cause. 60 | In this case, since the ICE was in MIR const propagation, and #51361 is the likely candidate since it modified const evaluation. 61 | 62 | ## Testing interactively 63 | 64 | Pass/fail of `cargo build` may not be what you're after. 65 | Perhaps the issue is an error message changed, so both the "good" and "bad" version will fail to 66 | compile, just with a different message. 67 | Or maybe something used to fail, and now erroneously passes. 68 | You can use the interactive feature with the `--prompt` flag to visually inspect a build and tell `cargo-bisect-rustc` what's "good" and what's "bad". 69 | Let's use [issue #55036] as an example where an error message changed: 70 | 71 | In `Cargo.toml`, remove the `edition` field (this example was before editions). 72 | 73 | `src/main.rs`: 74 | ```rust 75 | struct Foo { 76 | bar: i32 77 | } 78 | 79 | trait Baz { 80 | fn f(Foo { bar }: Foo) {} 81 | } 82 | 83 | fn main() {} 84 | ``` 85 | 86 | This historically emitted a bad error, was updated to emit a nice error (E0642 added in #53051), but then that nice error was lost somewhere (on the 2015 edition). 87 | Let's find where it was lost! 88 | Grab the ranges between where it was added and where we know it fails: 89 | 90 | ```sh 91 | cargo bisect-rustc --prompt \ 92 | --start=2018-08-14 \ 93 | --end=2018-10-11 94 | ``` 95 | 96 | At each step, `cargo-bisect-rustc` will show the output and ask you: 97 | 98 | ```text 99 | nightly-2018-08-14 finished with exit code Some(101). 100 | please select an action to take: 101 | > mark regressed 102 | mark baseline 103 | retry 104 | ``` 105 | 106 | Choose `mark baseline` with the nice E0642 message, and `mark regressed` with the less-favorable token error. 107 | Fairly quickly we find it regressed in nightly-2018-10-11. 108 | The most likely candidate is #54457 which is a rollup PR. 109 | It's usually not too hard to look through the commits and find a likely culprit. 110 | Indeed in this example, #54415 modified function parameter parsing. 111 | 112 | ## Testing with a script 113 | 114 | Using the `--script` option allows you to do something more fancy than just `cargo build`. 115 | Maybe you need to run cargo multiple times, or just call `rustc` directly, or you want to automatically grep through the output. 116 | The possibilities are endless! 117 | Just write a little shell script that exits 0 for the baseline, and exits nonzero for the regression. 118 | As an example, the previous interactive session can be hands-free automated with this script: 119 | 120 | `test.sh`: 121 | ```sh 122 | #!/bin/sh 123 | 124 | # Fail if we no longer get a `E0642` error: 125 | cargo check 2>&1 | grep E0642 126 | ``` 127 | 128 | And then run: 129 | 130 | ```sh 131 | cargo bisect-rustc --script=./test.sh \ 132 | --start=2018-08-14 \ 133 | --end=2018-10-11 134 | ``` 135 | 136 | [issue #53157]: https://github.com/rust-lang/rust/issues/53157 137 | [issue #55036]: https://github.com/rust-lang/rust/issues/55036 138 | 139 | ## Custom bisection messages 140 | 141 | *Available from v0.6.9* 142 | 143 | You can add custom messages when bisecting a regression. Taking inspiration from git-bisect, with `term-new` and `term-old` you can set custom messages to indicate if a regression matches the condition set by the bisection. 144 | 145 | Example: 146 | ```sh 147 | cargo bisect-rustc \ 148 | --start=2018-08-14 \ 149 | --end=2018-10-11 \ 150 | --term-old "No, this build did not reproduce the regression, compile successful" \ 151 | --term-new "Yes, this build reproduces the regression, compile error" 152 | ``` 153 | 154 | In other words, `--term-old` is displayed for older compilers that **do not** exhibit the regression. `--term-new` is for newer compilers which do exhibit the regression. 155 | 156 | What counts as a "regression" is defined by the [`--regress`](usage.html#regression-check) CLI option. By default, a regression is a compile-error (which is equivalent to `--term-new`). If you flip the definition of a "regression" with `--regress=success`, then a regression is a successful compile (which is *also* equivalent to `--term-new`). 157 | 158 | There are default terms based on the current `--regress` setting. Customizing the terms is most useful when using [scripting](#testing-with-a-script). For example, in the [Documentation changes](examples/doc-change.md) example, the customized terms can more clearly express the results of the script of whether or not it found what it was looking for in the documentation. 159 | -------------------------------------------------------------------------------- /guide/src/usage.md: -------------------------------------------------------------------------------- 1 | # Basic usage 2 | 3 | Using `cargo-bisect-rustc` simply involves running it inside a Cargo project that reproduces the regression: 4 | 5 | ```sh 6 | cargo bisect-rustc 7 | ``` 8 | 9 | > For a quick introduction, see the [Tutorial](tutorial.md). 10 | 11 | `cargo-bisect-rustc` works by building a Cargo project, and detecting if it succeeds or fails. 12 | It will download and use nightly Rust toolchains. 13 | It begins with two nightly boundaries, known as the *start* where the project successfully builds (the *baseline*), and the *end* where it is known to fail (the *regression*). 14 | It will then do a binary search between those dates to find the nightly where the project started to fail. 15 | 16 | Once it finds the nightly where it started to fail, `cargo-bisect-rustc` will then try to find the individual PR where it regressed. 17 | The Rust project keeps the builds of every merged PR for the last 167 days. 18 | If the nightly is within that range, then it will bisect between those PRs. 19 | 20 | And even further, if the regression is in a [rollup PR], then it will bisect the individual PRs within the rollup. 21 | This final bisection is only available for `x86_64-unknown-linux-gnu` since it is using the builds made for the [rustc performance tracker]. 22 | 23 | [rollup PR]: https://forge.rust-lang.org/release/rollups.html 24 | [rustc performance tracker]: https://perf.rust-lang.org/ 25 | 26 | ## Rust src repo 27 | 28 | `cargo-bisect-rustc` needs to read the git log of the [`rust-lang/rust`] repo in order to scan individual commits. 29 | See the [Rust src repo] chapter for details on how to configure how it finds the git repo. 30 | 31 | [Rust src repo]: rust-src-repo.md 32 | [`rust-lang/rust`]: https://github.com/rust-lang/rust/ 33 | 34 | ## Boundaries 35 | 36 | Without setting any options, `cargo-bisect-rustc` will try to automatically find the *start* where the build succeeds and the *end* where it fails. 37 | This can take some time, depending on how far back it needs to scan. 38 | It is recommended to use the `--start` and `--end` CLI options to tell it where the boundaries are. 39 | 40 | ```sh 41 | cargo bisect-rustc --start=2022-11-01 --end=2023-02-14 42 | ``` 43 | 44 | See the [Bisection boundaries] chapter for more details on setting these options. 45 | 46 | [Bisection boundaries]: boundaries.md 47 | 48 | ## Regression check 49 | 50 | By default, `cargo-bisect-rustc` assumes the *start* boundary successfully builds, and the *end* boundary fails to build. 51 | You can change this using the `--regress` CLI option. 52 | For example, you can tell it that the *start* should fail, and the *end* should pass. 53 | There are several options you can use with the `--regress` flag: 54 | 55 | 60 | 61 | | Option | Start | End | Description | 62 | |--------|-------|-----|-------------| 63 | | `error` | Succeed | Fail | The default setting checks for a failure as the regression. | 64 | | `success` | Fail | Succeed | Reverses the check to find when something is *fixed*. | 65 | | `ice` | No ICE | ICE | Scans when an Internal Compiler Error (ICE) was introduced. | 66 | | `non-ice` | ICE | No ICE | Scans when an ICE was fixed. | 67 | | `non-error` | Non-ICE Failure | Succeed or ICE | Scans when an ill-formed program stops being properly rejected, or the compiler starts generating an ICE. | 68 | 69 | See [Scripting](#scripting) for customizing this behavior. 70 | 71 | ## Custom commands 72 | 73 | By default, `cargo-bisect-rustc` runs `cargo build`. 74 | You can change which `cargo` command is run by passing additional arguments after `--`: 75 | 76 | ```sh 77 | cargo bisect-rustc -- test --test mytest 78 | ``` 79 | 80 | ## Scripting 81 | 82 | You can use an arbitrary script for determining what is a baseline and regression. 83 | This is an extremely flexible option that allows you to perform any action automatically. 84 | Just pass the path to the script to the `--script` CLI command: 85 | 86 | ```sh 87 | cargo bisect-rustc --script ./test.sh 88 | ``` 89 | 90 | The script should exit 0 for the baseline, and nonzero for a regression. 91 | Since `cargo-bisect-rustc` sets `RUSTUP_TOOLCHAIN` (see [Rustup toolchains](rustup.md)), all you need to do is call `cargo` or `rustc`, and the script should automatically use the toolchain that is currently being tested. 92 | 93 | ```sh 94 | #!/bin/sh 95 | 96 | set -ex 97 | 98 | # This checks that a warning is only printed once. 99 | # See https://github.com/rust-lang/rust/issues/88256 for a regression where it 100 | # started printing twice. 101 | 102 | OUTPUT=`cargo check 2>&1` 103 | COUNT=`echo "$OUTPUT" | grep -c "unnecessary parentheses"` 104 | test $COUNT -eq 1 105 | ``` 106 | 107 | If you need to use the targets directly without using `cargo` in the script, they are available in `$CARGO_TARGET_DIR/[release|debug]/...`, since `cargo-bisect-rustc` sets `$CARGO_TARGET_DIR`. 108 | 109 | Check out the [examples chapters](examples/index.md) for several examples of how to use this option. 110 | -------------------------------------------------------------------------------- /src/bounds.rs: -------------------------------------------------------------------------------- 1 | //! Definitions of bisection bounds. 2 | 3 | use crate::toolchains::{ 4 | download_progress, parse_to_naive_date, Toolchain, NIGHTLY_SERVER, YYYY_MM_DD, 5 | }; 6 | use crate::GitDate; 7 | use crate::Opts; 8 | use crate::{today, EPOCH_COMMIT}; 9 | use anyhow::bail; 10 | use chrono::NaiveDate; 11 | use reqwest::blocking::Client; 12 | use std::io::Read; 13 | use std::str::FromStr; 14 | 15 | /// A bisection boundary. 16 | #[derive(Clone, Debug)] 17 | pub enum Bound { 18 | Commit(String), 19 | Date(GitDate), 20 | } 21 | 22 | impl FromStr for Bound { 23 | type Err = std::convert::Infallible; 24 | fn from_str(s: &str) -> Result { 25 | parse_to_naive_date(s) 26 | .map(Self::Date) 27 | .or_else(|_| Ok(Self::Commit(s.to_string()))) 28 | } 29 | } 30 | 31 | impl Bound { 32 | /// Returns the SHA of this boundary. 33 | /// 34 | /// For nightlies, this will fetch from the network. 35 | pub fn sha(&self) -> anyhow::Result { 36 | match self { 37 | Bound::Commit(commit) => Ok(commit.clone()), 38 | Bound::Date(date) => { 39 | let date_str = date.format(YYYY_MM_DD); 40 | let url = 41 | format!("{NIGHTLY_SERVER}/{date_str}/channel-rust-nightly-git-commit-hash.txt"); 42 | 43 | eprintln!("fetching {url}"); 44 | let client = Client::new(); 45 | let name = format!("nightly manifest {date_str}"); 46 | let mut response = download_progress(&client, &name, &url)?; 47 | let mut commit = String::new(); 48 | response.read_to_string(&mut commit)?; 49 | 50 | eprintln!("converted {date_str} to {commit}"); 51 | 52 | Ok(commit) 53 | } 54 | } 55 | } 56 | } 57 | 58 | /// The starting bisection bounds. 59 | pub enum Bounds { 60 | /// Indicates to search backwards from the given date to find the start 61 | /// date where the regression does not occur. 62 | SearchNightlyBackwards { end: GitDate }, 63 | /// Search between two commits. 64 | /// 65 | /// `start` and `end` must be SHA1 hashes of the commit object. 66 | Commits { start: String, end: String }, 67 | /// Search between two dates. 68 | Dates { start: GitDate, end: GitDate }, 69 | } 70 | 71 | impl Bounds { 72 | pub fn from_args(args: &Opts) -> anyhow::Result { 73 | let (start, end) = translate_tags(&args)?; 74 | let today = today(); 75 | let check_in_future = |which, date: &NaiveDate| -> anyhow::Result<()> { 76 | if date > &today { 77 | bail!( 78 | "{which} date should be on or before current date, \ 79 | got {which} date request: {date} and current date is {today}" 80 | ); 81 | } 82 | Ok(()) 83 | }; 84 | let bounds = match (start, end) { 85 | // Neither --start or --end specified. 86 | (None, None) => Bounds::SearchNightlyBackwards { 87 | end: installed_nightly_or_latest()?, 88 | }, 89 | 90 | // --start or --end is a commit 91 | (Some(Bound::Commit(start)), Some(Bound::Commit(end))) => { 92 | Bounds::Commits { start, end } 93 | } 94 | (Some(Bound::Commit(start)), None) => Bounds::Commits { 95 | start, 96 | end: args.access.repo().commit("origin/master")?.sha, 97 | }, 98 | (None, Some(Bound::Commit(end))) => Bounds::Commits { 99 | start: EPOCH_COMMIT.to_string(), 100 | end, 101 | }, 102 | 103 | // --start or --end is a date 104 | (Some(Bound::Date(start)), Some(Bound::Date(end))) => { 105 | check_in_future("start", &start)?; 106 | check_in_future("end", &end)?; 107 | Bounds::Dates { start, end } 108 | } 109 | (Some(Bound::Date(start)), None) => { 110 | check_in_future("start", &start)?; 111 | Bounds::Dates { 112 | start, 113 | end: find_latest_nightly()?, 114 | } 115 | } 116 | (None, Some(Bound::Date(end))) => { 117 | check_in_future("end", &end)?; 118 | if args.by_commit { 119 | bail!("--by-commit with an end date requires --start to be specified"); 120 | } 121 | Bounds::SearchNightlyBackwards { end } 122 | } 123 | 124 | // Mixed not supported. 125 | (Some(Bound::Commit(_)), Some(Bound::Date(_))) 126 | | (Some(Bound::Date(_)), Some(Bound::Commit(_))) => bail!( 127 | "cannot take different types of bounds for start/end, \ 128 | got start: {:?} and end {:?}", 129 | args.start, 130 | args.end 131 | ), 132 | }; 133 | if let Bounds::Dates { start, end } = &bounds { 134 | if end < start { 135 | bail!("end should be after start, got start: {start} and end {end}"); 136 | } 137 | if args.by_commit { 138 | eprintln!("finding commit range that corresponds to dates specified"); 139 | let bounds = Bounds::Commits { 140 | start: date_to_sha(&start)?, 141 | end: date_to_sha(&end)?, 142 | }; 143 | return Ok(bounds); 144 | } 145 | } 146 | Ok(bounds) 147 | } 148 | } 149 | 150 | /// Translates a tag-like bound (such as `1.62.0`) to a `Bound::Date` so that 151 | /// bisecting works for versions older than 167 days. 152 | fn translate_tags(args: &Opts) -> anyhow::Result<(Option, Option)> { 153 | let is_tag = |bound: &Option| -> bool { 154 | match bound { 155 | Some(Bound::Commit(commit)) => commit.contains('.'), 156 | None | Some(Bound::Date(_)) => false, 157 | } 158 | }; 159 | let is_datelike = |bound: &Option| -> bool { 160 | matches!(bound, None | Some(Bound::Date(_))) || is_tag(bound) 161 | }; 162 | if !(is_datelike(&args.start) && is_datelike(&args.end)) { 163 | // If the user specified an actual commit for one bound, then don't 164 | // even try to convert the other bound to a date. 165 | return Ok((args.start.clone(), args.end.clone())); 166 | } 167 | let fixup = |which: &str, bound: &Option| -> anyhow::Result> { 168 | if is_tag(bound) { 169 | if let Some(Bound::Commit(tag)) = bound { 170 | let date = args 171 | .access 172 | .repo() 173 | .bound_to_date(Bound::Commit(tag.clone()))?; 174 | eprintln!( 175 | "translating --{which}={tag} to {date}", 176 | date = date.format(YYYY_MM_DD) 177 | ); 178 | return Ok(Some(Bound::Date(date))); 179 | } 180 | } 181 | Ok(bound.clone()) 182 | }; 183 | Ok((fixup("start", &args.start)?, fixup("end", &args.end)?)) 184 | } 185 | 186 | /// Returns the commit SHA of the nightly associated with the given date. 187 | fn date_to_sha(date: &NaiveDate) -> anyhow::Result { 188 | let date_str = date.format(YYYY_MM_DD); 189 | let url = format!("{NIGHTLY_SERVER}/{date_str}/channel-rust-nightly-git-commit-hash.txt"); 190 | 191 | eprintln!("fetching {url}"); 192 | let client = Client::new(); 193 | let name = format!("nightly manifest {date_str}"); 194 | let mut response = download_progress(&client, &name, &url)?; 195 | let mut commit = String::new(); 196 | response.read_to_string(&mut commit)?; 197 | 198 | eprintln!("converted {date_str} to {commit}"); 199 | 200 | Ok(commit) 201 | } 202 | 203 | /// Returns the date of the nightly toolchain currently installed. If no 204 | /// nightly is found, then it goes to the network to determine the date of the 205 | /// latest nightly. 206 | fn installed_nightly_or_latest() -> anyhow::Result { 207 | if let Some(date) = Toolchain::default_nightly() { 208 | return Ok(date); 209 | } 210 | find_latest_nightly() 211 | } 212 | 213 | /// Returns the date of the latest nightly (fetched from the network). 214 | fn find_latest_nightly() -> anyhow::Result { 215 | let url = format!("{NIGHTLY_SERVER}/channel-rust-nightly-date.txt"); 216 | eprintln!("fetching {url}"); 217 | let client = Client::new(); 218 | let mut response = download_progress(&client, "nightly date", &url)?; 219 | let mut body = String::new(); 220 | response.read_to_string(&mut body)?; 221 | let date = NaiveDate::parse_from_str(&body, "%Y-%m-%d")?; 222 | eprintln!("determined the latest nightly is {date}"); 223 | Ok(date) 224 | } 225 | -------------------------------------------------------------------------------- /src/git.rs: -------------------------------------------------------------------------------- 1 | //! Get git commits with help of the libgit2 library 2 | 3 | const RUST_SRC_URL: &str = "https://github.com/rust-lang/rust"; 4 | const RUST_SRC_REPO: Option<&str> = option_env!("RUST_SRC_REPO"); 5 | 6 | use std::env; 7 | use std::ops::Deref; 8 | use std::path::Path; 9 | 10 | use anyhow::{bail, Context}; 11 | use chrono::{TimeZone, Utc}; 12 | use git2::build::RepoBuilder; 13 | use git2::{Commit as Git2Commit, Repository}; 14 | use log::debug; 15 | 16 | use crate::{Author, Commit, GitDate, BORS_AUTHOR}; 17 | 18 | impl Commit { 19 | // Takes &mut because libgit2 internally caches summaries 20 | fn from_git2_commit(commit: &mut Git2Commit<'_>) -> Self { 21 | let committer = commit.committer(); 22 | Commit { 23 | sha: commit.id().to_string(), 24 | date: time_to_date(&commit.time()), 25 | summary: String::from_utf8_lossy(commit.summary_bytes().unwrap()).to_string(), 26 | committer: Author { 27 | name: committer.name().unwrap_or("").to_string(), 28 | email: committer.email().unwrap_or("").to_string(), 29 | date: time_to_date(&committer.when()), 30 | }, 31 | } 32 | } 33 | } 34 | 35 | fn time_to_date(time: &git2::Time) -> GitDate { 36 | Utc.timestamp_opt(time.seconds(), 0).unwrap().date_naive() 37 | } 38 | 39 | struct RustcRepo { 40 | repository: Repository, 41 | origin_remote: String, 42 | } 43 | 44 | impl Deref for RustcRepo { 45 | type Target = Repository; 46 | 47 | fn deref(&self) -> &Self::Target { 48 | &self.repository 49 | } 50 | } 51 | 52 | fn lookup_rev<'rev>(repo: &'rev RustcRepo, rev: &str) -> anyhow::Result> { 53 | let revision = repo.revparse_single(rev)?; 54 | 55 | // Find the merge-base between the revision and master. 56 | // If revision is a normal commit contained in master, the merge-base will be the commit itself. 57 | // If revision is a tag (e.g. a release version), the merge-base will contain the latest master 58 | // commit contained in that tag. 59 | let master_id = repo 60 | .revparse_single(&format!("{}/master", repo.origin_remote))? 61 | .id(); 62 | let revision_id = revision 63 | .as_tag() 64 | .map_or_else(|| revision.id(), git2::Tag::target_id); 65 | 66 | let common_base = repo.merge_base(master_id, revision_id)?; 67 | 68 | if let Ok(c) = repo.find_commit(common_base) { 69 | return Ok(c); 70 | } 71 | bail!("Could not find a commit for revision specifier '{}'", rev) 72 | } 73 | 74 | fn get_repo() -> anyhow::Result { 75 | fn open(path: &Path) -> anyhow::Result<(Repository, String)> { 76 | eprintln!("opening existing repository at {:?}", path); 77 | let repo = Repository::open(path)?; 78 | 79 | let origin_remote = find_origin_remote(&repo)?; 80 | eprintln!("Found origin remote under name `{origin_remote}`"); 81 | 82 | eprintln!("refreshing repository at {:?}", path); 83 | // This uses the CLI because libgit2 is quite slow to fetch a large repository. 84 | let status = std::process::Command::new("git") 85 | .args(&["fetch", "--tags"]) 86 | .arg(&origin_remote) 87 | .current_dir(path) 88 | .status() 89 | .context("expected `git` command-line executable to be installed".to_string())?; 90 | if !status.success() { 91 | bail!("git fetch failed exit status {}", status); 92 | } 93 | 94 | Ok((repo, origin_remote)) 95 | } 96 | 97 | let loc = Path::new("rust.git"); 98 | let (repository, origin_remote) = match (env::var_os("RUST_SRC_REPO"), RUST_SRC_REPO) { 99 | (Some(repo), _) => open(Path::new(&repo)), 100 | (None, _) if loc.exists() => open(loc), 101 | (None, Some(repo)) => open(Path::new(repo)), 102 | _ => { 103 | eprintln!("cloning rust repository"); 104 | Ok(( 105 | RepoBuilder::new().bare(true).clone(RUST_SRC_URL, loc)?, 106 | "origin".to_string(), 107 | )) 108 | } 109 | }?; 110 | 111 | Ok(RustcRepo { 112 | repository, 113 | origin_remote, 114 | }) 115 | } 116 | 117 | fn find_origin_remote(repo: &Repository) -> anyhow::Result { 118 | repo.remotes()? 119 | .iter() 120 | .filter_map(|name| name.and_then(|name| repo.find_remote(name).ok())) 121 | .find(|remote| { 122 | remote 123 | .url() 124 | .map_or(false, |url| url.contains("rust-lang/rust")) 125 | }) 126 | .and_then(|remote| remote.name().map(std::string::ToString::to_string)) 127 | .with_context(|| { 128 | format!( 129 | "rust-lang/rust remote not found. \ 130 | Try adding a remote pointing to `{RUST_SRC_URL}` in the rust repository at `{}`.", 131 | repo.path().display() 132 | ) 133 | }) 134 | } 135 | 136 | pub(crate) fn get_commit(sha: &str) -> anyhow::Result { 137 | let repo = get_repo()?; 138 | let mut rev = lookup_rev(&repo, sha)?; 139 | Ok(Commit::from_git2_commit(&mut rev)) 140 | } 141 | 142 | /// Returns the bors merge commits between the two specified boundaries 143 | /// (boundaries inclusive). 144 | pub fn get_commits_between(first_commit: &str, last_commit: &str) -> anyhow::Result> { 145 | let repo = get_repo()?; 146 | eprintln!("looking up first commit"); 147 | let mut first = lookup_rev(&repo, first_commit)?; 148 | eprintln!("looking up second commit"); 149 | let last = lookup_rev(&repo, last_commit)?; 150 | 151 | // Sanity check -- our algorithm below only works reliably if the 152 | // two commits are merge commits made by bors 153 | let assert_by_bors = |c: &Git2Commit<'_>| -> anyhow::Result<()> { 154 | match c.author().name() { 155 | Some(author) if author == BORS_AUTHOR => Ok(()), 156 | Some(author) => bail!( 157 | "Expected author {author} to be {BORS_AUTHOR} for {}.\n \ 158 | Make sure specified commits are on the master branch!", 159 | c.id() 160 | ), 161 | None => bail!("No author for {}", c.id()), 162 | } 163 | }; 164 | 165 | eprintln!("checking that commits are by bors and thus have ci artifacts..."); 166 | assert_by_bors(&first)?; 167 | assert_by_bors(&last)?; 168 | // Now find the commits 169 | // We search from the last and always take the first of its parents, 170 | // to only get merge commits. 171 | // This uses the fact that all bors merge commits have the earlier 172 | // merge commit as their first parent. 173 | eprintln!("finding bors merge commits"); 174 | let mut res = Vec::new(); 175 | let mut current = last; 176 | loop { 177 | assert_by_bors(¤t)?; 178 | res.push(Commit::from_git2_commit(&mut current)); 179 | match current.parents().next() { 180 | Some(c) => { 181 | if c.author().name() != Some(BORS_AUTHOR) { 182 | debug!( 183 | "{:?} has non-bors author: {:?}, skipping", 184 | c.id(), 185 | c.author().name() 186 | ); 187 | current = c.parents().next().unwrap(); 188 | continue; 189 | } 190 | current = c; 191 | if current.id() == first.id() { 192 | // Reached the first commit, our end of the search. 193 | break; 194 | } 195 | } 196 | None => bail!("reached end of repo without encountering the first commit"), 197 | } 198 | } 199 | res.push(Commit::from_git2_commit(&mut first)); 200 | // Reverse in order to obtain chronological order 201 | res.reverse(); 202 | eprintln!( 203 | "found {} bors merge commits in the specified range", 204 | res.len() 205 | ); 206 | Ok(res) 207 | } 208 | -------------------------------------------------------------------------------- /src/github.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Context}; 2 | use reqwest::header::{HeaderMap, HeaderValue, InvalidHeaderValue, AUTHORIZATION, USER_AGENT}; 3 | use reqwest::{blocking::Client, blocking::Response}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::{parse_to_naive_date, Author, Commit, GitDate, BORS_AUTHOR}; 7 | 8 | #[derive(Serialize, Deserialize, Debug)] 9 | struct GithubCommitComparison { 10 | merge_base_commit: GithubCommitElem, 11 | } 12 | #[derive(Serialize, Deserialize, Debug)] 13 | struct GithubCommitElem { 14 | commit: GithubCommit, 15 | sha: String, 16 | } 17 | #[derive(Serialize, Deserialize, Debug)] 18 | struct GithubCommit { 19 | author: Option, 20 | committer: Option, 21 | message: String, 22 | } 23 | #[derive(Serialize, Deserialize, Debug)] 24 | struct GithubAuthor { 25 | date: String, 26 | email: String, 27 | name: String, 28 | } 29 | #[derive(Serialize, Deserialize, Debug)] 30 | pub(crate) struct GithubCommentAuthor { 31 | pub(crate) login: String, 32 | } 33 | #[derive(Serialize, Deserialize, Debug)] 34 | pub(crate) struct GithubComment { 35 | pub(crate) user: GithubCommentAuthor, 36 | pub(crate) body: String, 37 | } 38 | 39 | impl GithubCommitElem { 40 | fn date(&self) -> anyhow::Result { 41 | let (date_str, _) = self 42 | .commit 43 | .committer 44 | .as_ref() 45 | .ok_or_else(|| anyhow::anyhow!("commit should have committer"))? 46 | .date 47 | .split_once('T') 48 | .context("commit date should folllow the ISO 8061 format, eg: 2022-05-04T09:55:51Z")?; 49 | Ok(parse_to_naive_date(date_str)?) 50 | } 51 | 52 | fn git_commit(self) -> anyhow::Result { 53 | let date = self.date()?; 54 | let committer = self 55 | .commit 56 | .committer 57 | .ok_or_else(|| anyhow::anyhow!("commit should have committer"))?; 58 | let committer = Author { 59 | name: committer.name, 60 | email: committer.email, 61 | date, 62 | }; 63 | Ok(Commit { 64 | sha: self.sha, 65 | date, 66 | summary: self.commit.message, 67 | committer, 68 | }) 69 | } 70 | } 71 | 72 | fn headers() -> Result { 73 | let mut headers = HeaderMap::new(); 74 | let user_agent = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); 75 | let user_agent = HeaderValue::from_static(user_agent); 76 | headers.insert(USER_AGENT, user_agent); 77 | if let Ok(token) = std::env::var("GITHUB_TOKEN") { 78 | eprintln!("adding local env GITHUB_TOKEN value to headers in github query"); 79 | let value = HeaderValue::from_str(&format!("token {token}"))?; 80 | headers.insert(AUTHORIZATION, value); 81 | } 82 | Ok(headers) 83 | } 84 | 85 | pub(crate) fn get_commit(sha: &str) -> anyhow::Result { 86 | let url = CommitDetailsUrl { sha }.url(); 87 | let client = Client::builder().default_headers(headers()?).build()?; 88 | let response: Response = client.get(&url).send()?; 89 | let status = response.status(); 90 | if !status.is_success() { 91 | bail!( 92 | "error: url <{}> response {}: {}", 93 | url, 94 | status, 95 | response.text().unwrap_or_else(|_| format!("")) 96 | ); 97 | } 98 | let elem: GithubCommitComparison = response 99 | .json() 100 | .with_context(|| "failed to decode GitHub JSON response")?; 101 | elem.merge_base_commit.git_commit() 102 | } 103 | 104 | pub(crate) fn get_pr_comments(pr: &str) -> anyhow::Result> { 105 | let url = format!("https://api.github.com/repos/rust-lang/rust/issues/{pr}/comments"); 106 | let client = Client::builder().default_headers(headers()?).build()?; 107 | let response: Response = client.get(&url).send()?; 108 | let status = response.status(); 109 | if !status.is_success() { 110 | bail!( 111 | "error: url <{}> response {}: {}", 112 | url, 113 | status, 114 | response.text().unwrap_or_else(|_| format!("")) 115 | ); 116 | } 117 | let comments: Vec = response 118 | .json() 119 | .with_context(|| "failed to decode GitHub JSON response")?; 120 | Ok(comments) 121 | } 122 | 123 | #[derive(Copy, Clone, Debug)] 124 | pub(crate) struct CommitsQuery<'a> { 125 | pub since_date: &'a str, 126 | pub most_recent_sha: &'a str, 127 | pub earliest_sha: &'a str, 128 | } 129 | 130 | /// Returns the bors merge commits between the two specified boundaries 131 | /// (boundaries inclusive). 132 | 133 | impl CommitsQuery<'_> { 134 | pub fn get_commits(&self) -> anyhow::Result> { 135 | // build up commit sequence, by feeding in `sha` as the starting point, and 136 | // working way backwards to max(`self.since_date`, `self.earliest_sha`). 137 | let mut commits = Vec::new(); 138 | 139 | // focus on Pull Request merges, all authored and committed by bors. 140 | let client = Client::builder().default_headers(headers()?).build()?; 141 | for page in 1.. { 142 | let url = CommitsUrl { 143 | page, 144 | author: BORS_AUTHOR, 145 | since: self.since_date, 146 | sha: self.most_recent_sha, 147 | } 148 | .url(); 149 | 150 | let response: Response = client.get(&url).send()?; 151 | let status = response.status(); 152 | if !status.is_success() { 153 | bail!( 154 | "error: url <{}> response {}: {}", 155 | url, 156 | status, 157 | response.text().unwrap_or_else(|_| format!("")) 158 | ); 159 | } 160 | 161 | let action = parse_paged_elems(response, |elem: GithubCommitElem| { 162 | let found_last = elem.sha == self.earliest_sha; 163 | if found_last { 164 | eprintln!( 165 | "ending github query because we found starting sha: {}", 166 | elem.sha 167 | ); 168 | } 169 | let commit = elem.git_commit()?; 170 | commits.push(commit); 171 | 172 | Ok(if found_last { Loop::Break } else { Loop::Next }) 173 | })?; 174 | 175 | if let Loop::Break = action { 176 | break; 177 | } 178 | } 179 | 180 | eprintln!( 181 | "get_commits_between returning commits, len: {}", 182 | commits.len() 183 | ); 184 | 185 | // reverse to obtain chronological order 186 | commits.reverse(); 187 | Ok(commits) 188 | } 189 | } 190 | 191 | const PER_PAGE: usize = 100; 192 | const OWNER: &str = "rust-lang"; 193 | const REPO: &str = "rust"; 194 | 195 | trait ToUrl { 196 | fn url(&self) -> String; 197 | } 198 | struct CommitsUrl<'a> { 199 | page: usize, 200 | author: &'a str, 201 | since: &'a str, 202 | sha: &'a str, 203 | } 204 | struct CommitDetailsUrl<'a> { 205 | sha: &'a str, 206 | } 207 | 208 | impl ToUrl for CommitsUrl<'_> { 209 | fn url(&self) -> String { 210 | format!( 211 | "https://api.github.com/repos/{OWNER}/{REPO}/commits\ 212 | ?page={page}&per_page={PER_PAGE}\ 213 | &author={author}&since={since}&sha={sha}", 214 | page = self.page, 215 | author = self.author, 216 | since = self.since, 217 | sha = self.sha 218 | ) 219 | } 220 | } 221 | 222 | impl ToUrl for CommitDetailsUrl<'_> { 223 | fn url(&self) -> String { 224 | // "origin/master" is set as `sha` when there is no `--end=` definition 225 | // specified on the command line. We define the GitHub master branch 226 | // HEAD commit as the end commit in this case 227 | let reference = if self.sha == "origin/master" { 228 | "master" 229 | } else { 230 | self.sha 231 | }; 232 | 233 | format!("https://api.github.com/repos/{OWNER}/{REPO}/compare/master...{reference}") 234 | } 235 | } 236 | 237 | enum Loop { 238 | Break, 239 | Next, 240 | } 241 | 242 | fn parse_paged_elems( 243 | response: Response, 244 | mut k: impl FnMut(GithubCommitElem) -> anyhow::Result, 245 | ) -> anyhow::Result { 246 | let elems: Vec = response.json()?; 247 | 248 | if elems.is_empty() { 249 | // we've run out of useful pages to lookup 250 | return Ok(Loop::Break); 251 | } 252 | 253 | for elem in elems { 254 | let act = k(elem)?; 255 | 256 | // the callback will tell us if we should terminate loop early (e.g. due to matching `sha`) 257 | match act { 258 | Loop::Break => return Ok(Loop::Break), 259 | Loop::Next => continue, 260 | } 261 | } 262 | 263 | // by default, we keep searching on next page from github. 264 | Ok(Loop::Next) 265 | } 266 | 267 | #[cfg(test)] 268 | mod tests { 269 | use super::*; 270 | 271 | #[test] 272 | fn test_github() { 273 | let c = get_commit("25674202bb7415e0c0ecd07856749cfb7f591be6").unwrap(); 274 | let committer = Author { 275 | name: String::from("bors"), 276 | email: String::from("bors@rust-lang.org"), 277 | date: GitDate::from_ymd_opt(2022, 5, 4).unwrap(), 278 | }; 279 | let expected_c = Commit { sha: "25674202bb7415e0c0ecd07856749cfb7f591be6".to_string(), 280 | date: parse_to_naive_date("2022-05-04").unwrap(), 281 | summary: "Auto merge of #96695 - JohnTitor:rollup-oo4fc1h, r=JohnTitor\n\nRollup of 6 pull requests\n\nSuccessful merges:\n\n - #96597 (openbsd: unbreak build on native platform)\n - #96662 (Fix typo in lint levels doc)\n - #96668 (Fix flaky rustdoc-ui test because it did not replace time result)\n - #96679 (Quick fix for #96223.)\n - #96684 (Update `ProjectionElem::Downcast` documentation)\n - #96686 (Add some TAIT-related tests)\n\nFailed merges:\n\nr? `@ghost`\n`@rustbot` modify labels: rollup".to_string(), 282 | committer, 283 | }; 284 | assert_eq!(c, expected_c) 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/least_satisfying.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::fmt; 3 | 4 | pub fn least_satisfying(slice: &[T], mut predicate: P) -> usize 5 | where 6 | T: fmt::Display + fmt::Debug, 7 | P: FnMut(&T, usize, usize) -> Satisfies, 8 | { 9 | let mut cache = BTreeMap::new(); 10 | let mut predicate = |idx: usize, rm_no, lm_yes| { 11 | let range: usize = lm_yes - rm_no + 1; 12 | // FIXME: This does not consider unknown_ranges. 13 | let remaining = range / 2; 14 | let estimate = if range < 3 { 0 } else { range.ilog2() as usize }; 15 | *cache 16 | .entry(idx) 17 | .or_insert_with(|| predicate(&slice[idx], remaining, estimate)) 18 | }; 19 | let mut unknown_ranges: Vec<(usize, usize)> = Vec::new(); 20 | // presume that the slice starts with a no 21 | // this should be tested before call 22 | let mut rm_no = 0; 23 | 24 | // presume that the slice ends with a yes 25 | // this should be tested before the call 26 | let mut lm_yes = slice.len() - 1; 27 | 28 | let mut next = (rm_no + lm_yes) / 2; 29 | 30 | loop { 31 | // simple case with no unknown ranges 32 | if rm_no + 1 == lm_yes { 33 | return lm_yes; 34 | } 35 | for (left, right) in unknown_ranges.iter().copied() { 36 | // if we're straddling an unknown range, then pretend it doesn't exist 37 | if rm_no + 1 == left && right + 1 == lm_yes { 38 | return lm_yes; 39 | } 40 | // check if we're checking inside an unknown range and set the next check outside of it 41 | if left <= next && next <= right { 42 | if rm_no < left - 1 { 43 | next = left - 1; 44 | } else if right < lm_yes { 45 | next = right + 1; 46 | } 47 | break; 48 | } 49 | } 50 | 51 | let r = predicate(next, rm_no, lm_yes); 52 | match r { 53 | Satisfies::Yes => { 54 | lm_yes = next; 55 | next = (rm_no + lm_yes) / 2; 56 | } 57 | Satisfies::No => { 58 | rm_no = next; 59 | next = (rm_no + lm_yes) / 2; 60 | } 61 | Satisfies::Unknown => { 62 | let mut left = next; 63 | while left > 0 && predicate(left, rm_no, lm_yes) == Satisfies::Unknown { 64 | left -= 1; 65 | } 66 | let mut right = next; 67 | while right + 1 < slice.len() 68 | && predicate(right, rm_no, lm_yes) == Satisfies::Unknown 69 | { 70 | right += 1; 71 | } 72 | unknown_ranges.push((left + 1, right - 1)); 73 | next = left; 74 | } 75 | } 76 | } 77 | } 78 | 79 | #[cfg(test)] 80 | mod tests { 81 | use super::Satisfies::{No, Unknown, Yes}; 82 | use super::{least_satisfying, Satisfies}; 83 | use quickcheck::{QuickCheck, TestResult}; 84 | 85 | fn prop(xs: Vec>) -> TestResult { 86 | let mut satisfies_v = xs 87 | .into_iter() 88 | .map(std::convert::Into::into) 89 | .collect::>(); 90 | satisfies_v.insert(0, Satisfies::No); 91 | satisfies_v.push(Satisfies::Yes); 92 | 93 | let mut first_yes = None; 94 | for (i, &s) in satisfies_v.iter().enumerate() { 95 | match s { 96 | Satisfies::Yes if first_yes.is_none() => first_yes = Some(i), 97 | Satisfies::No if first_yes.is_some() => return TestResult::discard(), 98 | _ => {} 99 | } 100 | } 101 | 102 | let res = least_satisfying(&satisfies_v, |i, _, _| *i); 103 | let exp = first_yes.unwrap(); 104 | TestResult::from_bool(res == exp) 105 | } 106 | 107 | #[test] 108 | fn least_satisfying_1() { 109 | assert_eq!( 110 | least_satisfying(&[No, Unknown, Unknown, No, Yes], |i, _, _| *i), 111 | 4 112 | ); 113 | } 114 | 115 | #[test] 116 | fn least_satisfying_2() { 117 | assert_eq!( 118 | least_satisfying(&[No, Unknown, Yes, Unknown, Yes], |i, _, _| *i), 119 | 2 120 | ); 121 | } 122 | 123 | #[test] 124 | fn least_satisfying_3() { 125 | assert_eq!(least_satisfying(&[No, No, No, No, Yes], |i, _, _| *i), 4); 126 | } 127 | 128 | #[test] 129 | fn least_satisfying_4() { 130 | assert_eq!(least_satisfying(&[No, No, Yes, Yes, Yes], |i, _, _| *i), 2); 131 | } 132 | 133 | #[test] 134 | fn least_satisfying_5() { 135 | assert_eq!(least_satisfying(&[No, Yes, Yes, Yes, Yes], |i, _, _| *i), 1); 136 | } 137 | 138 | #[test] 139 | fn least_satisfying_6() { 140 | assert_eq!( 141 | least_satisfying( 142 | &[No, Yes, Yes, Unknown, Unknown, Yes, Unknown, Yes], 143 | |i, _, _| *i 144 | ), 145 | 1 146 | ); 147 | } 148 | 149 | #[test] 150 | fn least_satisfying_7() { 151 | assert_eq!(least_satisfying(&[No, Yes, Unknown, Yes], |i, _, _| *i), 1); 152 | } 153 | 154 | #[test] 155 | fn least_satisfying_8() { 156 | assert_eq!( 157 | least_satisfying(&[No, Unknown, No, No, Unknown, Yes, Yes], |i, _, _| *i), 158 | 5 159 | ); 160 | } 161 | 162 | #[test] 163 | fn qc_prop() { 164 | QuickCheck::new().quickcheck(prop as fn(_) -> _); 165 | } 166 | } 167 | 168 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 169 | pub enum Satisfies { 170 | Yes, 171 | No, 172 | Unknown, 173 | } 174 | 175 | impl Satisfies { 176 | pub fn msg_with_context<'a>(&self, term_old: &'a str, term_new: &'a str) -> &'a str { 177 | match self { 178 | Self::Yes => term_new, 179 | Self::No => term_old, 180 | Self::Unknown => "Unable to figure out if the condition matched", 181 | } 182 | } 183 | } 184 | 185 | impl fmt::Display for Satisfies { 186 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 187 | write!(f, "{:?}", self) 188 | } 189 | } 190 | 191 | impl From> for Satisfies { 192 | fn from(o: Option) -> Self { 193 | match o { 194 | Some(true) => Satisfies::Yes, 195 | Some(false) => Satisfies::No, 196 | None => Satisfies::Unknown, 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::pedantic)] 2 | #![warn(clippy::cargo)] 3 | #![allow(clippy::semicolon_if_nothing_returned)] 4 | #![allow(clippy::let_underscore_drop)] 5 | #![allow(clippy::single_match_else)] 6 | 7 | use std::env; 8 | use std::ffi::OsString; 9 | use std::fmt; 10 | use std::fs; 11 | use std::path::PathBuf; 12 | use std::process; 13 | 14 | use anyhow::{bail, Context}; 15 | use chrono::{Duration, NaiveDate, Utc}; 16 | use clap::{ArgAction, Parser, ValueEnum}; 17 | use colored::Colorize; 18 | use github::get_pr_comments; 19 | use log::debug; 20 | use regex::RegexBuilder; 21 | use reqwest::blocking::Client; 22 | 23 | mod bounds; 24 | mod git; 25 | mod github; 26 | mod least_satisfying; 27 | mod repo_access; 28 | mod toolchains; 29 | 30 | use crate::bounds::{Bound, Bounds}; 31 | use crate::github::get_commit; 32 | use crate::least_satisfying::{least_satisfying, Satisfies}; 33 | use crate::repo_access::{AccessViaGithub, AccessViaLocalGit, RustRepositoryAccessor}; 34 | use crate::toolchains::{ 35 | parse_to_naive_date, DownloadError, DownloadParams, InstallError, TestOutcome, Toolchain, 36 | ToolchainSpec, YYYY_MM_DD, 37 | }; 38 | 39 | const BORS_AUTHOR: &str = "bors"; 40 | 41 | #[derive(Debug, Clone, PartialEq)] 42 | pub struct Commit { 43 | pub sha: String, 44 | pub date: GitDate, 45 | pub summary: String, 46 | pub committer: Author, 47 | } 48 | 49 | #[derive(Debug, Clone, PartialEq)] 50 | pub struct Author { 51 | pub name: String, 52 | pub email: String, 53 | pub date: GitDate, 54 | } 55 | 56 | /// The first commit which build artifacts are made available through the CI for 57 | /// bisection. 58 | /// 59 | /// Due to our deletion policy which expires builds after 167 days, the build 60 | /// artifacts of this commit itself is no longer available, so this may not be entirely useful; 61 | /// however, it does limit the amount of commits somewhat. 62 | const EPOCH_COMMIT: &str = "927c55d86b0be44337f37cf5b0a76fb8ba86e06c"; 63 | 64 | const REPORT_HEADER: &str = "\ 65 | ================================================================================== 66 | = Please file this regression report on the rust-lang/rust GitHub repository = 67 | = New issue: https://github.com/rust-lang/rust/issues/new = 68 | = Known issues: https://github.com/rust-lang/rust/issues = 69 | = Copy and paste the text below into the issue report thread. Thanks! = 70 | =================================================================================="; 71 | 72 | #[derive(Debug, Parser)] 73 | #[command( 74 | bin_name = "cargo bisect-rustc", 75 | version, 76 | about, 77 | next_display_order = None, 78 | after_help = "Examples: 79 | Run a fully automatic nightly bisect doing `cargo check`: 80 | ``` 81 | cargo bisect-rustc --start 2018-07-07 --end 2018-07-30 --test-dir ../my_project/ -- check 82 | ``` 83 | 84 | Run a PR-based bisect with manual prompts after each run doing `cargo build`: 85 | ``` 86 | cargo bisect-rustc --start 6a1c0637ce44aeea6c60527f4c0e7fb33f2bcd0d \\ 87 | --end 866a713258915e6cbb212d135f751a6a8c9e1c0a --test-dir ../my_project/ --prompt -- build 88 | ```" 89 | )] 90 | #[allow(clippy::struct_excessive_bools)] 91 | struct Opts { 92 | #[arg( 93 | long, 94 | help = "Custom regression definition", 95 | value_enum, 96 | default_value_t = RegressOn::Error, 97 | )] 98 | regress: RegressOn, 99 | 100 | #[arg(short, long, help = "Download the alt build instead of normal build")] 101 | alt: bool, 102 | 103 | #[arg( 104 | long, 105 | help = "Host triple for the compiler", 106 | default_value = env!("HOST"), 107 | )] 108 | host: String, 109 | 110 | #[arg(long, help = "Cross-compilation target platform")] 111 | target: Option, 112 | 113 | #[arg(long, help = "Preserve the downloaded artifacts")] 114 | preserve: bool, 115 | 116 | #[arg(long, help = "Preserve the target directory used for builds")] 117 | preserve_target: bool, 118 | 119 | #[arg(long, help = "Download rust-src [default: no download]")] 120 | with_src: bool, 121 | 122 | #[arg(long, help = "Download rustc-dev [default: no download]")] 123 | with_dev: bool, 124 | 125 | #[arg(short, long = "component", help = "additional components to install")] 126 | components: Vec, 127 | 128 | #[arg( 129 | long, 130 | help = "Root directory for tests", 131 | default_value = ".", 132 | value_parser = validate_dir 133 | )] 134 | test_dir: PathBuf, 135 | 136 | #[arg(long, help = "Manually evaluate for regression with prompts")] 137 | prompt: bool, 138 | 139 | #[arg( 140 | long, 141 | short, 142 | help = "Assume failure after specified number of seconds (for bisecting hangs)" 143 | )] 144 | timeout: Option, 145 | 146 | #[arg(short, long = "verbose", action = ArgAction::Count)] 147 | verbosity: u8, 148 | 149 | #[arg( 150 | help = "Arguments to pass to cargo or the file specified by --script during tests", 151 | num_args = 1.., 152 | last = true 153 | )] 154 | command_args: Vec, 155 | 156 | #[arg( 157 | long, 158 | help = "Pretend to be a stable compiler (disable features, \ 159 | report a version that looks like a stable version)" 160 | )] 161 | pretend_to_be_stable: bool, 162 | 163 | #[arg( 164 | long, 165 | help = "Left bound for search (*without* regression). You can use \ 166 | a date (YYYY-MM-DD), git tag name (e.g. 1.58.0) or git commit SHA." 167 | )] 168 | start: Option, 169 | 170 | #[arg( 171 | long, 172 | help = "Right bound for search (*with* regression). You can use \ 173 | a date (YYYY-MM-DD), git tag name (e.g. 1.58.0) or git commit SHA." 174 | )] 175 | end: Option, 176 | 177 | #[arg(long, help = "Bisect via commit artifacts")] 178 | by_commit: bool, 179 | 180 | #[arg(long, value_enum, help = "How to access Rust git repository", default_value_t = Access::Github)] 181 | access: Access, 182 | 183 | #[arg(long, help = "Install the given artifact")] 184 | install: Option, 185 | 186 | #[arg(long, help = "Force installation over existing artifacts")] 187 | force_install: bool, 188 | 189 | #[arg(long, help = "Script replacement for `cargo build` command")] 190 | script: Option, 191 | 192 | #[arg(long, help = "Do not install cargo [default: install cargo]")] 193 | without_cargo: bool, 194 | 195 | #[arg( 196 | long, 197 | help = "Text shown when a test does match the condition requested" 198 | )] 199 | term_new: Option, 200 | 201 | #[arg( 202 | long, 203 | help = "Text shown when a test fails to match the condition requested" 204 | )] 205 | term_old: Option, 206 | } 207 | 208 | pub type GitDate = NaiveDate; 209 | 210 | pub fn today() -> NaiveDate { 211 | Utc::now().date_naive() 212 | } 213 | 214 | fn validate_dir(s: &str) -> anyhow::Result { 215 | let path: PathBuf = s.parse()?; 216 | if path.is_dir() { 217 | Ok(path) 218 | } else { 219 | bail!( 220 | "{} is not an existing directory", 221 | path.canonicalize()?.display() 222 | ) 223 | } 224 | } 225 | 226 | impl Opts { 227 | fn emit_cargo_output(&self) -> bool { 228 | self.verbosity >= 2 229 | } 230 | 231 | fn emit_cmd(&self) -> bool { 232 | self.verbosity >= 1 233 | } 234 | } 235 | 236 | #[derive(Debug, thiserror::Error)] 237 | struct ExitError(i32); 238 | 239 | impl fmt::Display for ExitError { 240 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 241 | write!(f, "exiting with {}", self.0) 242 | } 243 | } 244 | 245 | impl Config { 246 | fn default_outcome_of_output(&self, output: &process::Output) -> TestOutcome { 247 | let status = output.status; 248 | let stdout_utf8 = String::from_utf8_lossy(&output.stdout).to_string(); 249 | let stderr_utf8 = String::from_utf8_lossy(&output.stderr).to_string(); 250 | 251 | debug!( 252 | "status: {:?} stdout: {:?} stderr: {:?}", 253 | status, stdout_utf8, stderr_utf8 254 | ); 255 | 256 | let saw_ice = stderr_utf8.contains("error: internal compiler error") 257 | || stderr_utf8.contains("' has overflowed its stack") 258 | || stderr_utf8.contains("error: the compiler unexpectedly panicked"); 259 | 260 | let input = (self.args.regress, status.success()); 261 | let result = match input { 262 | (RegressOn::Error, true) | (RegressOn::Success, false) => TestOutcome::Baseline, 263 | (RegressOn::Error, false) | (RegressOn::Success | RegressOn::NonError, true) => { 264 | TestOutcome::Regressed 265 | } 266 | (RegressOn::Ice, _) | (RegressOn::NonError, false) => { 267 | if saw_ice { 268 | TestOutcome::Regressed 269 | } else { 270 | TestOutcome::Baseline 271 | } 272 | } 273 | (RegressOn::NonIce, _) => { 274 | if saw_ice { 275 | TestOutcome::Baseline 276 | } else { 277 | TestOutcome::Regressed 278 | } 279 | } 280 | }; 281 | debug!( 282 | "default_outcome_of_output: input: {:?} result: {:?}", 283 | input, result 284 | ); 285 | result 286 | } 287 | } 288 | 289 | #[derive(Clone, Debug, ValueEnum)] 290 | enum Access { 291 | Checkout, 292 | Github, 293 | } 294 | 295 | impl Access { 296 | fn repo(&self) -> Box { 297 | match self { 298 | Self::Checkout => Box::new(AccessViaLocalGit), 299 | Self::Github => Box::new(AccessViaGithub), 300 | } 301 | } 302 | } 303 | 304 | #[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)] 305 | /// Customize what is treated as regression. 306 | enum RegressOn { 307 | /// Marks test outcome as `Regressed` if and only if the `rustc` 308 | /// process reports a non-success status. This corresponds to when `rustc` 309 | /// has an internal compiler error (ICE) or when it detects an error in the 310 | /// input program. 311 | /// This covers the most common use case for `cargo-bisect-rustc` and is 312 | /// thus the default setting. 313 | Error, 314 | 315 | /// Marks test outcome as `Regressed` if and only if the `rustc` 316 | /// process reports a success status. This corresponds to when `rustc` 317 | /// believes it has successfully compiled the program. This covers the use 318 | /// case for when you want to bisect to see when a bug was fixed. 319 | Success, 320 | 321 | /// Marks test outcome as `Regressed` if and only if the `rustc` 322 | /// process issues a diagnostic indicating that an internal compiler error 323 | /// (ICE) occurred. This covers the use case for when you want to bisect to 324 | /// see when an ICE was introduced on a codebase that is meant to produce 325 | /// a clean error. 326 | Ice, 327 | 328 | /// Marks test outcome as `Regressed` if and only if the `rustc` 329 | /// process does not issue a diagnostic indicating that an internal 330 | /// compiler error (ICE) occurred. This covers the use case for when you 331 | /// want to bisect to see when an ICE was fixed. 332 | NonIce, 333 | 334 | /// Marks test outcome as `Baseline` if and only if the `rustc` 335 | /// process reports error status and does not issue any diagnostic 336 | /// indicating that an internal compiler error (ICE) occurred. This is the 337 | /// use case if the regression is a case where an ill-formed program has 338 | /// stopped being properly rejected by the compiler. 339 | /// (The main difference between this case and `success` is the handling of 340 | /// ICE: `success` assumes that ICE should be considered baseline; 341 | /// `non-error` assumes ICE should be considered a sign of a regression.) 342 | NonError, 343 | } 344 | 345 | impl RegressOn { 346 | fn must_process_stderr(self) -> bool { 347 | match self { 348 | RegressOn::Error | RegressOn::Success => false, 349 | RegressOn::NonError | RegressOn::Ice | RegressOn::NonIce => true, 350 | } 351 | } 352 | } 353 | 354 | struct Config { 355 | args: Opts, 356 | bounds: Bounds, 357 | rustup_tmp_path: PathBuf, 358 | toolchains_path: PathBuf, 359 | target: String, 360 | client: Client, 361 | } 362 | 363 | impl Config { 364 | fn from_args(args: Opts) -> anyhow::Result { 365 | let target = args.target.clone().unwrap_or_else(|| args.host.clone()); 366 | 367 | let mut toolchains_path = home::rustup_home()?; 368 | 369 | // We will download and extract the tarballs into this directory before installing. 370 | // Using `~/.rustup/tmp` instead of $TMPDIR ensures we could always perform installation by 371 | // renaming instead of copying the whole directory. 372 | let rustup_tmp_path = toolchains_path.join("tmp"); 373 | if !rustup_tmp_path.exists() { 374 | fs::create_dir(&rustup_tmp_path)?; 375 | } 376 | 377 | toolchains_path.push("toolchains"); 378 | if !toolchains_path.is_dir() { 379 | bail!( 380 | "`{}` is not a directory. Please install rustup.", 381 | toolchains_path.display() 382 | ); 383 | } 384 | 385 | let bounds = Bounds::from_args(&args)?; 386 | 387 | Ok(Config { 388 | args, 389 | bounds, 390 | target, 391 | toolchains_path, 392 | rustup_tmp_path, 393 | client: Client::new(), 394 | }) 395 | } 396 | } 397 | 398 | // Application entry point 399 | fn run() -> anyhow::Result<()> { 400 | env_logger::try_init()?; 401 | let mut os_args: Vec<_> = std::env::args_os().collect(); 402 | // This allows both `cargo-bisect-rustc` (with a hyphen) and 403 | // `cargo bisect-rustc` (with a space) to work identically. 404 | if let Some(command) = os_args.get(1) { 405 | if command == "bisect-rustc" { 406 | os_args.remove(1); 407 | } 408 | } 409 | let args = Opts::parse_from(os_args); 410 | let cfg = Config::from_args(args)?; 411 | 412 | if let Some(ref bound) = cfg.args.install { 413 | cfg.install(bound) 414 | } else { 415 | cfg.bisect() 416 | } 417 | } 418 | 419 | impl Config { 420 | fn install(&self, bound: &Bound) -> anyhow::Result<()> { 421 | match *bound { 422 | Bound::Commit(ref sha) => { 423 | let sha = self.args.access.repo().commit(sha)?.sha; 424 | let mut t = Toolchain { 425 | spec: ToolchainSpec::Ci { 426 | commit: sha, 427 | alt: self.args.alt, 428 | }, 429 | host: self.args.host.clone(), 430 | std_targets: vec![self.args.host.clone(), self.target.clone()], 431 | }; 432 | t.std_targets.sort(); 433 | t.std_targets.dedup(); 434 | let dl_params = DownloadParams::for_ci(self); 435 | t.install(&self.client, &dl_params)?; 436 | } 437 | Bound::Date(date) => { 438 | let mut t = Toolchain { 439 | spec: ToolchainSpec::Nightly { date }, 440 | host: self.args.host.clone(), 441 | std_targets: vec![self.args.host.clone(), self.target.clone()], 442 | }; 443 | t.std_targets.sort(); 444 | t.std_targets.dedup(); 445 | let dl_params = DownloadParams::for_nightly(self); 446 | t.install(&self.client, &dl_params)?; 447 | } 448 | } 449 | 450 | Ok(()) 451 | } 452 | 453 | fn do_perf_search(&self, result: &BisectionResult) { 454 | let toolchain = &result.searched[result.found]; 455 | match self.search_perf_builds(toolchain) { 456 | Ok(result) => { 457 | let bisection = result.bisection; 458 | let url = format!( 459 | "https://github.com/rust-lang/rust/commit/{}", 460 | bisection.searched[bisection.found] 461 | ) 462 | .red() 463 | .bold(); 464 | let legacy_url = format!( 465 | "https://github.com/rust-lang-ci/rust/commit/{}", 466 | bisection.searched[bisection.found] 467 | ) 468 | .red() 469 | .bold(); 470 | eprintln!("Regression in {url}. Note that if it is a legacy rollup build, it might be available in {legacy_url}."); 471 | 472 | // In case the bisected commit has been garbage-collected by github, we show its 473 | // additional context here. 474 | let context = &result.toolchain_descriptions[bisection.found]; 475 | eprintln!("The PR introducing the regression in this rollup is {context}"); 476 | } 477 | Err(e) => { 478 | eprintln!("ERROR: {e}"); 479 | } 480 | } 481 | } 482 | 483 | // bisection entry point 484 | fn bisect(&self) -> anyhow::Result<()> { 485 | if let Bounds::Commits { start, end } = &self.bounds { 486 | let bisection_result = self.bisect_ci(start, end)?; 487 | self.print_results(&bisection_result); 488 | self.do_perf_search(&bisection_result); 489 | } else { 490 | let nightly_bisection_result = self.bisect_nightlies()?; 491 | self.print_results(&nightly_bisection_result); 492 | let nightly_regression = 493 | &nightly_bisection_result.searched[nightly_bisection_result.found]; 494 | 495 | if let ToolchainSpec::Nightly { date } = nightly_regression.spec { 496 | let mut previous_date = date.pred_opt().unwrap(); 497 | let working_commit = loop { 498 | match Bound::Date(previous_date).sha() { 499 | Ok(sha) => break sha, 500 | Err(err) 501 | if matches!( 502 | err.downcast_ref::(), 503 | Some(DownloadError::NotFound(_)), 504 | ) => 505 | { 506 | eprintln!("missing nightly for {}", previous_date.format(YYYY_MM_DD)); 507 | previous_date = previous_date.pred_opt().unwrap(); 508 | } 509 | Err(err) => return Err(err), 510 | } 511 | }; 512 | 513 | let bad_commit = Bound::Date(date).sha()?; 514 | eprintln!( 515 | "looking for regression commit between {} and {}", 516 | previous_date.format(YYYY_MM_DD), 517 | date.format(YYYY_MM_DD), 518 | ); 519 | 520 | let ci_bisection_result = self.bisect_ci_via(&working_commit, &bad_commit)?; 521 | 522 | self.print_results(&ci_bisection_result); 523 | self.do_perf_search(&ci_bisection_result); 524 | print_final_report(self, &nightly_bisection_result, &ci_bisection_result); 525 | } 526 | } 527 | 528 | Ok(()) 529 | } 530 | } 531 | 532 | fn searched_range( 533 | cfg: &Config, 534 | searched_toolchains: &[Toolchain], 535 | ) -> (ToolchainSpec, ToolchainSpec) { 536 | let first_toolchain = searched_toolchains.first().unwrap().spec.clone(); 537 | let last_toolchain = searched_toolchains.last().unwrap().spec.clone(); 538 | 539 | match (&first_toolchain, &last_toolchain) { 540 | (ToolchainSpec::Ci { .. }, ToolchainSpec::Ci { .. }) => (first_toolchain, last_toolchain), 541 | 542 | _ => { 543 | // The searched_toolchains is a subset of the range actually 544 | // searched since they don't always include the complete bounds 545 | // due to `Config::bisect_nightlies` narrowing the range. Show the 546 | // true range of dates searched. 547 | match cfg.bounds { 548 | Bounds::SearchNightlyBackwards { end } => { 549 | (first_toolchain, ToolchainSpec::Nightly { date: end }) 550 | } 551 | Bounds::Commits { .. } => unreachable!("expected nightly bisect"), 552 | Bounds::Dates { start, end } => ( 553 | ToolchainSpec::Nightly { date: start }, 554 | ToolchainSpec::Nightly { date: end }, 555 | ), 556 | } 557 | } 558 | } 559 | } 560 | 561 | impl Config { 562 | fn print_results(&self, bisection_result: &BisectionResult) { 563 | let BisectionResult { 564 | searched: toolchains, 565 | dl_spec, 566 | found, 567 | } = bisection_result; 568 | 569 | let (start, end) = searched_range(self, toolchains); 570 | 571 | eprintln!("searched toolchains {} through {}", start, end); 572 | 573 | if toolchains[*found] == *toolchains.last().unwrap() { 574 | // FIXME: Ideally the BisectionResult would contain the final result. 575 | // This ends up testing a toolchain that was already tested. 576 | // I believe this is one of the duplicates mentioned in 577 | // https://github.com/rust-lang/cargo-bisect-rustc/issues/85 578 | eprintln!("checking last toolchain to determine final result"); 579 | let t = &toolchains[*found]; 580 | let r = match t.install(&self.client, dl_spec) { 581 | Ok(()) => { 582 | let outcome = t.test(self); 583 | remove_toolchain(self, t, dl_spec); 584 | // we want to fail, so a successful build doesn't satisfy us 585 | match outcome { 586 | TestOutcome::Baseline => Satisfies::No, 587 | TestOutcome::Regressed => Satisfies::Yes, 588 | } 589 | } 590 | Err(_) => { 591 | let _ = t.remove(dl_spec); 592 | Satisfies::Unknown 593 | } 594 | }; 595 | match r { 596 | Satisfies::Yes => {} 597 | Satisfies::No | Satisfies::Unknown => { 598 | eprintln!( 599 | "error: The regression was not found. Expanding the bounds may help." 600 | ); 601 | return; 602 | } 603 | } 604 | } 605 | 606 | let tc_found = format!("Regression in {}", toolchains[*found]); 607 | eprintln!(); 608 | eprintln!(); 609 | eprintln!("{}", "*".repeat(80).dimmed().bold()); 610 | eprintln!("{}", tc_found.red()); 611 | eprintln!("{}", "*".repeat(80).dimmed().bold()); 612 | eprintln!(); 613 | } 614 | } 615 | 616 | fn remove_toolchain(cfg: &Config, toolchain: &Toolchain, dl_params: &DownloadParams) { 617 | if cfg.args.preserve { 618 | // If `rustup toolchain link` was used to link to nightly, then even 619 | // with --preserve, the toolchain link should be removed, otherwise it 620 | // will go stale after 24 hours. 621 | let toolchain_dir = cfg.toolchains_path.join(toolchain.rustup_name()); 622 | match fs::symlink_metadata(&toolchain_dir) { 623 | Ok(meta) => { 624 | #[cfg(windows)] 625 | let is_junction = { 626 | use std::os::windows::fs::MetadataExt; 627 | (meta.file_attributes() & 1024) != 0 628 | }; 629 | #[cfg(not(windows))] 630 | let is_junction = false; 631 | if !meta.file_type().is_symlink() && !is_junction { 632 | return; 633 | } 634 | debug!("removing linked toolchain {}", toolchain); 635 | } 636 | Err(e) => { 637 | debug!( 638 | "remove_toolchain: cannot stat toolchain {}: {}", 639 | toolchain, e 640 | ); 641 | return; 642 | } 643 | } 644 | } 645 | if let Err(e) = toolchain.remove(dl_params) { 646 | debug!( 647 | "failed to remove toolchain {} in {}: {}", 648 | toolchain, 649 | cfg.toolchains_path.display(), 650 | e 651 | ); 652 | } 653 | } 654 | 655 | fn print_final_report( 656 | cfg: &Config, 657 | nightly_bisection_result: &BisectionResult, 658 | ci_bisection_result: &BisectionResult, 659 | ) { 660 | let BisectionResult { 661 | searched: nightly_toolchains, 662 | found: nightly_found, 663 | .. 664 | } = nightly_bisection_result; 665 | 666 | let BisectionResult { 667 | searched: ci_toolchains, 668 | found: ci_found, 669 | .. 670 | } = ci_bisection_result; 671 | 672 | eprintln!("{}", REPORT_HEADER.dimmed()); 673 | eprintln!(); 674 | 675 | let (start, end) = searched_range(cfg, nightly_toolchains); 676 | 677 | eprintln!("searched nightlies: from {} to {}", start, end); 678 | 679 | eprintln!("regressed nightly: {}", nightly_toolchains[*nightly_found],); 680 | 681 | eprintln!( 682 | "searched commit range: https://github.com/rust-lang/rust/compare/{0}...{1}", 683 | ci_toolchains.first().unwrap(), 684 | ci_toolchains.last().unwrap(), 685 | ); 686 | 687 | eprintln!( 688 | "regressed commit: https://github.com/rust-lang/rust/commit/{}", 689 | ci_toolchains[*ci_found], 690 | ); 691 | 692 | eprintln!(); 693 | eprintln!("
"); 694 | eprintln!( 695 | "bisected with cargo-bisect-rustc v{}", 696 | env!("CARGO_PKG_REPOSITORY"), 697 | env!("CARGO_PKG_VERSION"), 698 | ); 699 | eprintln!(); 700 | eprintln!(); 701 | if let Some(host) = option_env!("HOST") { 702 | eprintln!("Host triple: {}", host); 703 | } 704 | 705 | eprintln!("Reproduce with:"); 706 | eprintln!("```bash"); 707 | eprint!("cargo bisect-rustc "); 708 | for arg in env::args_os() 709 | .map(|arg| arg.to_string_lossy().into_owned()) 710 | .skip_while(|arg| arg.ends_with("bisect-rustc")) 711 | { 712 | eprint!("{arg} "); 713 | } 714 | eprintln!(); 715 | eprintln!("```"); 716 | eprintln!("
"); 717 | } 718 | 719 | struct NightlyFinderIter { 720 | start_date: GitDate, 721 | current_date: GitDate, 722 | } 723 | 724 | impl NightlyFinderIter { 725 | fn new(start_date: GitDate) -> Self { 726 | Self { 727 | start_date, 728 | current_date: start_date, 729 | } 730 | } 731 | } 732 | 733 | impl Iterator for NightlyFinderIter { 734 | type Item = GitDate; 735 | 736 | fn next(&mut self) -> Option { 737 | let current_distance = self.start_date - self.current_date; 738 | 739 | let jump_length = if current_distance.num_days() < 7 { 740 | // first week jump by two days 741 | 2 742 | } else if current_distance.num_days() < 49 { 743 | // from 2nd to 7th week jump weekly 744 | 7 745 | } else { 746 | // from 7th week jump by two weeks 747 | 14 748 | }; 749 | 750 | self.current_date = self.current_date - Duration::days(jump_length); 751 | Some(self.current_date) 752 | } 753 | } 754 | 755 | impl Config { 756 | fn install_and_test( 757 | &self, 758 | t: &Toolchain, 759 | dl_spec: &DownloadParams, 760 | ) -> Result { 761 | let regress = self.args.regress; 762 | let term_old = self.args.term_old.as_deref().unwrap_or_else(|| { 763 | if self.args.script.is_some() { 764 | match regress { 765 | RegressOn::Error => "Script returned success", 766 | RegressOn::Success => "Script returned error", 767 | RegressOn::Ice => "Script did not ICE", 768 | RegressOn::NonIce => "Script found ICE", 769 | RegressOn::NonError => "Script returned error (no ICE)", 770 | } 771 | } else { 772 | match regress { 773 | RegressOn::Error => "Successfully compiled", 774 | RegressOn::Success => "Compile error", 775 | RegressOn::Ice => "Did not ICE", 776 | RegressOn::NonIce => "Found ICE", 777 | RegressOn::NonError => "Compile error (no ICE)", 778 | } 779 | } 780 | }); 781 | let term_new = self.args.term_new.as_deref().unwrap_or_else(|| { 782 | if self.args.script.is_some() { 783 | match regress { 784 | RegressOn::Error => "Script returned error", 785 | RegressOn::Success => "Script returned success", 786 | RegressOn::Ice => "Script found ICE", 787 | RegressOn::NonIce => "Script did not ICE", 788 | RegressOn::NonError => "Script returned success or ICE", 789 | } 790 | } else { 791 | match regress { 792 | RegressOn::Error => "Compile error", 793 | RegressOn::Success => "Successfully compiled", 794 | RegressOn::Ice => "Found ICE", 795 | RegressOn::NonIce => "Did not ICE", 796 | RegressOn::NonError => "Successfully compiled or ICE", 797 | } 798 | } 799 | }); 800 | match t.install(&self.client, dl_spec) { 801 | Ok(()) => { 802 | let outcome = t.test(self); 803 | // we want to fail, so a successful build doesn't satisfy us 804 | let r = match outcome { 805 | TestOutcome::Baseline => Satisfies::No, 806 | TestOutcome::Regressed => Satisfies::Yes, 807 | }; 808 | eprintln!( 809 | "RESULT: {}, ===> {}", 810 | t, 811 | r.msg_with_context(term_old, term_new) 812 | ); 813 | remove_toolchain(self, t, dl_spec); 814 | eprintln!(); 815 | Ok(r) 816 | } 817 | Err(error) => { 818 | remove_toolchain(self, t, dl_spec); 819 | Err(error) 820 | } 821 | } 822 | } 823 | 824 | fn bisect_to_regression(&self, toolchains: &[Toolchain], dl_spec: &DownloadParams) -> usize { 825 | least_satisfying(toolchains, |t, remaining, estimate| { 826 | eprintln!( 827 | "{remaining} versions remaining to test after this (roughly {estimate} steps)" 828 | ); 829 | self.install_and_test(t, dl_spec) 830 | .unwrap_or(Satisfies::Unknown) 831 | }) 832 | } 833 | } 834 | 835 | impl Config { 836 | // nightlies branch of bisect execution 837 | fn bisect_nightlies(&self) -> anyhow::Result { 838 | if self.args.alt { 839 | bail!("cannot bisect nightlies with --alt: not supported"); 840 | } 841 | 842 | let dl_spec = DownloadParams::for_nightly(self); 843 | 844 | // before this date we didn't have -std packages 845 | let end_at = NaiveDate::from_ymd_opt(2015, 10, 20).unwrap(); 846 | // The date where a passing build is first found. This becomes 847 | // the new start point of the bisection range. 848 | let mut first_success = None; 849 | 850 | // nightly_date is the date we are currently testing to find the start 851 | // point. The loop below modifies nightly_date towards older dates 852 | // as it tries to find the starting point. It will become the basis 853 | // for setting first_success once a passing toolchain is found. 854 | // 855 | // last_failure is the oldest date where a regression was found while 856 | // walking backwards. This becomes the new endpoint of the bisection 857 | // range. 858 | let (mut nightly_date, mut last_failure) = match self.bounds { 859 | Bounds::SearchNightlyBackwards { end } => (end, end), 860 | Bounds::Commits { .. } => unreachable!(), 861 | Bounds::Dates { start, end } => (start, end), 862 | }; 863 | 864 | let has_start = self.args.start.is_some(); 865 | 866 | let mut nightly_iter = NightlyFinderIter::new(nightly_date); 867 | 868 | // this loop tests nightly toolchains to: 869 | // (1) validate that start date does not have regression (if defined on command line) 870 | // (2) identify a nightly date range for the bisection routine 871 | // 872 | // The tests here must be constrained to dates after 2015-10-20 (`end_at` date) 873 | // because -std packages were not available prior 874 | while nightly_date > end_at { 875 | let mut t = Toolchain { 876 | spec: ToolchainSpec::Nightly { date: nightly_date }, 877 | host: self.args.host.clone(), 878 | std_targets: vec![self.args.host.clone(), self.target.clone()], 879 | }; 880 | t.std_targets.sort(); 881 | t.std_targets.dedup(); 882 | if t.is_current_nightly() { 883 | eprintln!( 884 | "checking {} from the currently installed default nightly \ 885 | toolchain as the last failure", 886 | t 887 | ); 888 | } 889 | 890 | eprintln!("checking the start range to find a passing nightly"); 891 | match self.install_and_test(&t, &dl_spec) { 892 | Ok(r) => { 893 | // If Satisfies::No, then the regression was not identified in this nightly. 894 | // Break out of the loop and use this as the start date for the 895 | // bisection range 896 | if r == Satisfies::No { 897 | first_success = Some(nightly_date); 898 | break; 899 | } else if has_start { 900 | // If this date was explicitly defined on the command line & 901 | // has regression, then this is an error in the test definition. 902 | // The user must re-define the start date and try again 903 | bail!( 904 | "the start of the range ({}) must not reproduce the regression", 905 | t 906 | ); 907 | } 908 | last_failure = nightly_date; 909 | nightly_date = nightly_iter.next().unwrap(); 910 | } 911 | Err(InstallError::NotFound { .. }) => { 912 | // go back just one day, presumably missing a nightly 913 | nightly_date = nightly_date.pred_opt().unwrap(); 914 | eprintln!( 915 | "*** unable to install {}. roll back one day and try again...", 916 | t 917 | ); 918 | if has_start { 919 | bail!("could not find {}", t); 920 | } 921 | } 922 | Err(error) => return Err(error.into()), 923 | } 924 | } 925 | 926 | let first_success = first_success.context("could not find a nightly that built")?; 927 | 928 | // confirm that the end of the date range has the regression 929 | let mut t_end = Toolchain { 930 | spec: ToolchainSpec::Nightly { date: last_failure }, 931 | host: self.args.host.clone(), 932 | std_targets: vec![self.args.host.clone(), self.target.clone()], 933 | }; 934 | t_end.std_targets.sort(); 935 | t_end.std_targets.dedup(); 936 | 937 | eprintln!("checking the end range to verify it does not pass"); 938 | let result_nightly = self.install_and_test(&t_end, &dl_spec)?; 939 | // The regression was not identified in this nightly. 940 | if result_nightly == Satisfies::No { 941 | bail!( 942 | "the end of the range ({}) does not reproduce the regression", 943 | t_end 944 | ); 945 | } 946 | 947 | let toolchains = toolchains_between( 948 | self, 949 | ToolchainSpec::Nightly { 950 | date: first_success, 951 | }, 952 | ToolchainSpec::Nightly { date: last_failure }, 953 | ); 954 | 955 | let found = self.bisect_to_regression(&toolchains, &dl_spec); 956 | 957 | Ok(BisectionResult { 958 | dl_spec, 959 | searched: toolchains, 960 | found, 961 | }) 962 | } 963 | } 964 | 965 | fn toolchains_between(cfg: &Config, a: ToolchainSpec, b: ToolchainSpec) -> Vec { 966 | match (a, b) { 967 | (ToolchainSpec::Nightly { date: a }, ToolchainSpec::Nightly { date: b }) => { 968 | let mut toolchains = Vec::new(); 969 | let mut date = a; 970 | let mut std_targets = vec![cfg.args.host.clone(), cfg.target.clone()]; 971 | std_targets.sort(); 972 | std_targets.dedup(); 973 | while date <= b { 974 | let t = Toolchain { 975 | spec: ToolchainSpec::Nightly { date }, 976 | host: cfg.args.host.clone(), 977 | std_targets: std_targets.clone(), 978 | }; 979 | toolchains.push(t); 980 | date = date.succ_opt().unwrap(); 981 | } 982 | toolchains 983 | } 984 | _ => unimplemented!(), 985 | } 986 | } 987 | 988 | impl Config { 989 | // CI branch of bisect execution 990 | fn bisect_ci(&self, start: &str, end: &str) -> anyhow::Result { 991 | eprintln!("bisecting ci builds starting at {start}, ending at {end}"); 992 | self.bisect_ci_via(start, end) 993 | } 994 | 995 | fn bisect_ci_via(&self, start_sha: &str, end_sha: &str) -> anyhow::Result { 996 | let access = self.args.access.repo(); 997 | let start = access.commit(start_sha)?; 998 | let end = access.commit(end_sha)?; 999 | let assert_by_bors = |c: &Commit| -> anyhow::Result<()> { 1000 | if c.committer.name != BORS_AUTHOR { 1001 | bail!( 1002 | "Expected author {} to be {BORS_AUTHOR} for {}.\n \ 1003 | Make sure specified commits are on the master branch \ 1004 | and refer to a bors merge commit!", 1005 | c.committer.name, 1006 | c.sha 1007 | ); 1008 | } 1009 | Ok(()) 1010 | }; 1011 | assert_by_bors(&start)?; 1012 | assert_by_bors(&end)?; 1013 | let commits = access.commits(start_sha, &end.sha)?; 1014 | 1015 | let Some(last) = commits.last() else { 1016 | bail!("expected at least one commit"); 1017 | }; 1018 | if !last.sha.starts_with(&end.sha) { 1019 | bail!( 1020 | "expected the last commit to be {end_sha}, but got {}", 1021 | last.sha 1022 | ); 1023 | } 1024 | 1025 | commits.iter().zip(commits.iter().skip(1)).all(|(a, b)| { 1026 | let sorted_by_date = a.date <= b.date; 1027 | assert!( 1028 | sorted_by_date, 1029 | "commits must chronologically ordered,\ 1030 | but {:?} comes after {:?}", 1031 | a, b 1032 | ); 1033 | sorted_by_date 1034 | }); 1035 | 1036 | for (j, commit) in commits.iter().enumerate() { 1037 | eprintln!( 1038 | " commit[{}] {}: {}", 1039 | j, 1040 | commit.date, 1041 | commit.summary.split('\n').next().unwrap() 1042 | ) 1043 | } 1044 | 1045 | self.bisect_ci_in_commits(start_sha, &end.sha, commits) 1046 | } 1047 | 1048 | fn bisect_ci_in_commits( 1049 | &self, 1050 | start: &str, 1051 | end: &str, 1052 | mut commits: Vec, 1053 | ) -> anyhow::Result { 1054 | let dl_spec = DownloadParams::for_ci(self); 1055 | commits.retain(|c| today() - c.date < Duration::days(167)); 1056 | 1057 | if commits.is_empty() { 1058 | bail!( 1059 | "no CI builds available between {} and {} within last 167 days", 1060 | start, 1061 | end 1062 | ); 1063 | } 1064 | 1065 | if let Some(c) = commits.last() { 1066 | if !c.sha.starts_with(end) { 1067 | bail!("expected to end with {}, but ended with {}", end, c.sha); 1068 | } 1069 | } 1070 | 1071 | eprintln!("validated commits found, specifying toolchains"); 1072 | eprintln!(); 1073 | 1074 | let toolchains = commits 1075 | .into_iter() 1076 | .map(|commit| { 1077 | let mut t = Toolchain { 1078 | spec: ToolchainSpec::Ci { 1079 | commit: commit.sha, 1080 | alt: self.args.alt, 1081 | }, 1082 | host: self.args.host.clone(), 1083 | std_targets: vec![self.args.host.clone(), self.target.clone()], 1084 | }; 1085 | t.std_targets.sort(); 1086 | t.std_targets.dedup(); 1087 | t 1088 | }) 1089 | .collect::>(); 1090 | 1091 | if !toolchains.is_empty() { 1092 | // validate commit at start of range 1093 | eprintln!("checking the start range to verify it passes"); 1094 | let start_range_result = self.install_and_test(&toolchains[0], &dl_spec)?; 1095 | if start_range_result == Satisfies::Yes { 1096 | bail!( 1097 | "the commit at the start of the range ({}) includes the regression", 1098 | &toolchains[0] 1099 | ); 1100 | } 1101 | 1102 | // validate commit at end of range 1103 | eprintln!("checking the end range to verify it does not pass"); 1104 | let end_range_result = 1105 | self.install_and_test(&toolchains[toolchains.len() - 1], &dl_spec)?; 1106 | if end_range_result == Satisfies::No { 1107 | bail!( 1108 | "the commit at the end of the range ({}) does not reproduce the regression", 1109 | &toolchains[toolchains.len() - 1] 1110 | ); 1111 | } 1112 | } 1113 | 1114 | let found = self.bisect_to_regression(&toolchains, &dl_spec); 1115 | 1116 | Ok(BisectionResult { 1117 | searched: toolchains, 1118 | found, 1119 | dl_spec, 1120 | }) 1121 | } 1122 | 1123 | fn search_perf_builds(&self, toolchain: &Toolchain) -> anyhow::Result { 1124 | eprintln!("Attempting to search unrolled perf builds"); 1125 | let Toolchain { 1126 | spec: ToolchainSpec::Ci { commit, .. }, 1127 | .. 1128 | } = toolchain 1129 | else { 1130 | bail!("not a ci commit"); 1131 | }; 1132 | let summary = get_commit(commit)?.summary; 1133 | if !summary.starts_with("Auto merge of #") && !summary.contains("Rollup of") { 1134 | bail!("not a rollup pr"); 1135 | } 1136 | let pr = summary.split(' ').nth(3).unwrap(); 1137 | // remove '#' 1138 | let pr = pr.chars().skip(1).collect::(); 1139 | let comments = get_pr_comments(&pr)?; 1140 | let perf_comment = comments 1141 | .iter() 1142 | .filter(|c| c.user.login == "rust-timer") 1143 | .find(|c| c.body.contains("Perf builds for each rolled up PR")) 1144 | .context("couldn't find perf build comment")?; 1145 | let context = extract_perf_builds(&perf_comment.body)?; 1146 | let short_sha = context 1147 | .builds 1148 | .iter() 1149 | .map(|sha| sha.chars().take(8).collect()) 1150 | .collect::>(); 1151 | eprintln!("Found commits {short_sha:?}"); 1152 | 1153 | let bisection = self.linear_in_commits(&context.builds)?; 1154 | Ok(PerfBisectionResult { 1155 | bisection, 1156 | toolchain_descriptions: context.descriptions, 1157 | }) 1158 | } 1159 | 1160 | fn linear_in_commits(&self, commits: &[&str]) -> anyhow::Result { 1161 | let dl_spec = DownloadParams::for_ci(self); 1162 | 1163 | let toolchains = commits 1164 | .into_iter() 1165 | .map(|commit| { 1166 | let mut t = Toolchain { 1167 | spec: ToolchainSpec::Ci { 1168 | commit: commit.to_string(), 1169 | alt: self.args.alt, 1170 | }, 1171 | host: self.args.host.clone(), 1172 | std_targets: vec![self.args.host.clone(), self.target.clone()], 1173 | }; 1174 | t.std_targets.sort(); 1175 | t.std_targets.dedup(); 1176 | t 1177 | }) 1178 | .collect::>(); 1179 | 1180 | let Some(found) = toolchains.iter().position(|t| { 1181 | self.install_and_test(t, &dl_spec) 1182 | .unwrap_or(Satisfies::Unknown) 1183 | == Satisfies::Yes 1184 | }) else { 1185 | bail!("none of the toolchains satisfied the predicate"); 1186 | }; 1187 | 1188 | Ok(BisectionResult { 1189 | searched: toolchains, 1190 | found, 1191 | dl_spec, 1192 | }) 1193 | } 1194 | } 1195 | 1196 | #[derive(Clone)] 1197 | struct BisectionResult { 1198 | searched: Vec, 1199 | found: usize, 1200 | dl_spec: DownloadParams, 1201 | } 1202 | 1203 | /// The results of a bisection through the unrolled perf builds in a rollup: 1204 | /// - the regular bisection results 1205 | /// - a description of the rolled-up PRs for clearer diagnostics, in case the bisected commit 1206 | /// doesn't exist anymore on github. 1207 | #[derive(Clone)] 1208 | struct PerfBisectionResult { 1209 | bisection: BisectionResult, 1210 | toolchain_descriptions: Vec, 1211 | } 1212 | 1213 | fn main() { 1214 | if let Err(err) = run() { 1215 | match err.downcast::() { 1216 | Ok(ExitError(code)) => process::exit(code), 1217 | Err(err) => { 1218 | let error_str = "ERROR:".red().bold(); 1219 | eprintln!("{} {:?}", error_str, err); 1220 | process::exit(1); 1221 | } 1222 | } 1223 | } 1224 | } 1225 | 1226 | /// An in-order mapping from perf build SHA to its description. 1227 | struct PerfBuildsContext<'a> { 1228 | builds: Vec<&'a str>, 1229 | descriptions: Vec, 1230 | } 1231 | 1232 | /// Extracts the commits posted by the rust-timer bot on rollups, for unrolled perf builds, with 1233 | /// their associated context: the PR number and title if available. 1234 | /// 1235 | /// We're looking for a commit SHA, in a comment whose format has changed (and could change in the 1236 | /// future), for example: 1237 | /// - v1: https://github.com/rust-lang/rust/pull/113014#issuecomment-1605868471 1238 | /// - v2, the current: https://github.com/rust-lang/rust/pull/113105#issuecomment-1610393473 1239 | /// 1240 | /// The SHA comes in later columns, so we'll look for a 40-char hex string and give priority to the 1241 | /// last we find (to avoid possible conflicts with commits in the PR title column). 1242 | /// 1243 | /// Depending on how recent the perf build commit is, it may have been garbage-collected by github: 1244 | /// perf-builds are force pushed to the `try-perf` branch, and accessing that commit can 1245 | /// 404. Therefore, we try to map back from that commit to the rolled-up PR present in the list of 1246 | /// unrolled builds. 1247 | fn extract_perf_builds(body: &str) -> anyhow::Result> { 1248 | let mut builds = Vec::new(); 1249 | let mut descriptions = Vec::new(); 1250 | 1251 | let sha_regex = RegexBuilder::new(r"([0-9a-f]{40})") 1252 | .case_insensitive(true) 1253 | .build()?; 1254 | for line in body 1255 | .lines() 1256 | // Only look at the lines of the unrolled perf builds table. 1257 | .filter(|l| l.starts_with("|#")) 1258 | { 1259 | // Get the last SHA we find, to prioritize the 3rd or 2nd columns. 1260 | let sha = sha_regex 1261 | .find_iter(line) 1262 | .last() 1263 | .and_then(|m| Some(m.as_str())); 1264 | 1265 | // If we did find one, we try to extract the associated description. 1266 | let Some(sha) = sha else { continue }; 1267 | 1268 | let mut description = String::new(); 1269 | 1270 | // In v1 and v2, we know that the first column is the PR number. 1271 | // 1272 | // In the unlikely event it's missing because of a parsing discrepancy, we don't want to 1273 | // ignore it, and ask for feedback: we always want to have *some* context per PR, matching 1274 | // the number of SHAs we found. 1275 | let Some(pr) = line.split('|').nth(1) else { 1276 | bail!("Couldn't get rolled-up PR number for SHA {sha}, please open an issue."); 1277 | }; 1278 | 1279 | description.push_str(pr); 1280 | 1281 | // The second column could be a link to the commit (which we don't want in the description), 1282 | // or the PR title (which we want). 1283 | if let Some(title) = line.split('|').nth(2) { 1284 | // For v1, this column would contain the commit, and we won't have the PR title 1285 | // anywhere. So we try to still give some context for that older format: if the column 1286 | // contains the SHA, we don't add that to the description. 1287 | if !title.contains(sha) { 1288 | description.push_str(": "); 1289 | description.push_str(title); 1290 | } 1291 | } 1292 | 1293 | builds.push(sha); 1294 | descriptions.push(description); 1295 | } 1296 | 1297 | Ok(PerfBuildsContext { 1298 | builds, 1299 | descriptions, 1300 | }) 1301 | } 1302 | 1303 | #[cfg(test)] 1304 | mod tests { 1305 | use super::*; 1306 | 1307 | #[test] 1308 | fn test_nightly_finder_iterator() { 1309 | let start_date = NaiveDate::from_ymd_opt(2019, 01, 01).unwrap(); 1310 | 1311 | let iter = NightlyFinderIter::new(start_date); 1312 | 1313 | for (date, i) in iter.zip([2, 4, 6, 8, 15, 22, 29, 36, 43, 50, 64, 78]) { 1314 | assert_eq!(start_date - Duration::days(i), date) 1315 | } 1316 | } 1317 | 1318 | #[test] 1319 | fn test_validate_dir() { 1320 | let current_dir = "."; 1321 | assert!(validate_dir(current_dir).is_ok()); 1322 | let main = "src/main.rs"; 1323 | assert!( 1324 | validate_dir(main).is_err(), 1325 | "{}", 1326 | validate_dir(main).unwrap_err() 1327 | ) 1328 | } 1329 | 1330 | // Ensure the first version of the comment posted by the perf-bot works 1331 | #[test] 1332 | fn test_perf_builds_v1_format() { 1333 | // Body extracted from this v1 comment 1334 | // https://github.com/rust-lang/rust/pull/113014#issuecomment-1605868471 1335 | let body = "📌 Perf builds for each rolled up PR: 1336 | 1337 | |PR# | Perf Build Sha| 1338 | |----|:-----:| 1339 | |#113009|[05b07dad146a6d43ead9bcd1e8bc10cbd017a5f5](https://github.com/rust-lang/rust/commit/05b07dad146a6d43ead9bcd1e8bc10cbd017a5f5)| 1340 | |#113008|[581913b6789370def5158093b799baa6d4d875eb](https://github.com/rust-lang/rust/commit/581913b6789370def5158093b799baa6d4d875eb)| 1341 | |#112956|[e294bd3827eb2e878167329648f3c8178ef344e7](https://github.com/rust-lang/rust/commit/e294bd3827eb2e878167329648f3c8178ef344e7)| 1342 | |#112950|[0ed6ba504649ca1cb2672572b4ab41acfb06c86c](https://github.com/rust-lang/rust/commit/0ed6ba504649ca1cb2672572b4ab41acfb06c86c)| 1343 | |#112937|[18e108ab85b78e6966c5b5bdadfd5b8efeadf080](https://github.com/rust-lang/rust/commit/18e108ab85b78e6966c5b5bdadfd5b8efeadf080)| 1344 | 1345 | 1346 | *previous master*: [f7ca9df695](https://github.com/rust-lang/rust/commit/f7ca9df69549470541fbf542f87a03eb9ed024b6) 1347 | 1348 | In the case of a perf regression, run the following command for each PR you suspect might be the cause: `@rust-timer build $SHA` 1349 | "; 1350 | let context = 1351 | extract_perf_builds(body).expect("extracting perf builds context on v1 format failed"); 1352 | assert_eq!( 1353 | vec![ 1354 | "05b07dad146a6d43ead9bcd1e8bc10cbd017a5f5", 1355 | "581913b6789370def5158093b799baa6d4d875eb", 1356 | "e294bd3827eb2e878167329648f3c8178ef344e7", 1357 | "0ed6ba504649ca1cb2672572b4ab41acfb06c86c", 1358 | "18e108ab85b78e6966c5b5bdadfd5b8efeadf080", 1359 | ], 1360 | context.builds, 1361 | ); 1362 | assert_eq!( 1363 | vec!["#113009", "#113008", "#112956", "#112950", "#112937",], 1364 | context.descriptions, 1365 | ); 1366 | } 1367 | 1368 | // Ensure the second version of the comment posted by the perf-bot works 1369 | #[test] 1370 | fn test_perf_builds_v2_format() { 1371 | // Body extracted from this v2 comment 1372 | // https://github.com/rust-lang/rust/pull/113105#issuecomment-1610393473 1373 | let body = "📌 Perf builds for each rolled up PR: 1374 | 1375 | | PR# | Message | Perf Build Sha | 1376 | |----|----|:-----:| 1377 | |#112207|Add trustzone and virtualization target features for aarch3…|`bbec6d6e413aa144c8b9346da27a0f2af299cbeb` ([link](https://github.com/rust-lang/rust/commit/bbec6d6e413aa144c8b9346da27a0f2af299cbeb))| 1378 | |#112454|Make compiletest aware of targets without dynamic linking|`70b67c09ead52f4582471650202b1a189821ed5f` ([link](https://github.com/rust-lang/rust/commit/70b67c09ead52f4582471650202b1a189821ed5f))| 1379 | |#112628|Allow comparing `Box`es with different allocators|`3043f4e577f41565443f38a6a16b7a1a08b063ad` ([link](https://github.com/rust-lang/rust/commit/3043f4e577f41565443f38a6a16b7a1a08b063ad))| 1380 | |#112692|Provide more context for `rustc +nightly -Zunstable-options…|`4ab6f33fd50237b105999cc6d32d85cce5dad61a` ([link](https://github.com/rust-lang/rust/commit/4ab6f33fd50237b105999cc6d32d85cce5dad61a))| 1381 | |#112972|Make `UnwindAction::Continue` explicit in MIR dump|`e1df9e306054655d7d41ec1ad75ade5d76a6888d` ([link](https://github.com/rust-lang/rust/commit/e1df9e306054655d7d41ec1ad75ade5d76a6888d))| 1382 | |#113020|Add tests impl via obj unless denied|`affe009b94eba41777cf02997b1780e50445d6af` ([link](https://github.com/rust-lang/rust/commit/affe009b94eba41777cf02997b1780e50445d6af))| 1383 | |#113084|Simplify some conditions|`0ce4618dbf5810aabb389edd4950c060b6b4d049` ([link](https://github.com/rust-lang/rust/commit/0ce4618dbf5810aabb389edd4950c060b6b4d049))| 1384 | |#113103|Normalize types when applying uninhabited predicate.|`241cd8cd818cdc865cdf02f0c32a40081420b772` ([link](https://github.com/rust-lang/rust/commit/241cd8cd818cdc865cdf02f0c32a40081420b772))| 1385 | 1386 | 1387 | *previous master*: [5ea6668646](https://github.com/rust-lang/rust/commit/5ea66686467d3ec5f8c81570e7f0f16ad8dd8cc3) 1388 | 1389 | In the case of a perf regression, run the following command for each PR you suspect might be the cause: `@rust-timer build $SHA` 1390 | "; 1391 | let context = 1392 | extract_perf_builds(body).expect("extracting perf builds context on v2 format failed"); 1393 | assert_eq!( 1394 | vec![ 1395 | "bbec6d6e413aa144c8b9346da27a0f2af299cbeb", 1396 | "70b67c09ead52f4582471650202b1a189821ed5f", 1397 | "3043f4e577f41565443f38a6a16b7a1a08b063ad", 1398 | "4ab6f33fd50237b105999cc6d32d85cce5dad61a", 1399 | "e1df9e306054655d7d41ec1ad75ade5d76a6888d", 1400 | "affe009b94eba41777cf02997b1780e50445d6af", 1401 | "0ce4618dbf5810aabb389edd4950c060b6b4d049", 1402 | "241cd8cd818cdc865cdf02f0c32a40081420b772", 1403 | ], 1404 | context.builds, 1405 | ); 1406 | assert_eq!( 1407 | vec![ 1408 | "#112207: Add trustzone and virtualization target features for aarch3…", 1409 | "#112454: Make compiletest aware of targets without dynamic linking", 1410 | "#112628: Allow comparing `Box`es with different allocators", 1411 | "#112692: Provide more context for `rustc +nightly -Zunstable-options…", 1412 | "#112972: Make `UnwindAction::Continue` explicit in MIR dump", 1413 | "#113020: Add tests impl via obj unless denied", 1414 | "#113084: Simplify some conditions", 1415 | "#113103: Normalize types when applying uninhabited predicate.", 1416 | ], 1417 | context.descriptions, 1418 | ); 1419 | } 1420 | } 1421 | -------------------------------------------------------------------------------- /src/repo_access.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | 3 | use crate::{git, github, Bound, Commit, GitDate}; 4 | 5 | pub(crate) trait RustRepositoryAccessor { 6 | /// Maps `bound` to its associated date, looking up its commit if necessary. 7 | fn bound_to_date(&self, bound: Bound) -> anyhow::Result { 8 | match bound { 9 | Bound::Date(date) => Ok(date), 10 | Bound::Commit(ref commit_ref) => self.commit(commit_ref).map(|commit| commit.date), 11 | } 12 | } 13 | 14 | /// Looks up commit associated with `commit_ref`, which can be either a sha 15 | /// or a more general reference like "origin/master". 16 | fn commit(&self, commit_ref: &str) -> anyhow::Result; 17 | 18 | /// Looks up a series of commits ending with `end_sha`; the resulting series 19 | /// should start with `start_sha`. If `start_sha` is not a predecessor of 20 | /// `end_sha` in the history, then the series will cover all commits as far 21 | /// back as the date associated with `start_sha`. 22 | fn commits(&self, start_sha: &str, end_sha: &str) -> anyhow::Result>; 23 | } 24 | 25 | pub(crate) struct AccessViaLocalGit; 26 | 27 | pub(crate) struct AccessViaGithub; 28 | 29 | impl RustRepositoryAccessor for AccessViaLocalGit { 30 | fn commit(&self, commit_ref: &str) -> anyhow::Result { 31 | git::get_commit(commit_ref) 32 | } 33 | fn commits(&self, start_sha: &str, end_sha: &str) -> anyhow::Result> { 34 | let end_sha = if end_sha == "origin/master" { 35 | "FETCH_HEAD" 36 | } else { 37 | end_sha 38 | }; 39 | eprintln!( 40 | "fetching (via local git) commits from {} to {}", 41 | start_sha, end_sha 42 | ); 43 | git::get_commits_between(start_sha, end_sha) 44 | .context("failed during attempt to create/access local git repository") 45 | } 46 | } 47 | 48 | impl RustRepositoryAccessor for AccessViaGithub { 49 | fn commit(&self, commit_ref: &str) -> anyhow::Result { 50 | github::get_commit(commit_ref) 51 | } 52 | 53 | fn commits(&self, start_sha: &str, end_sha: &str) -> anyhow::Result> { 54 | // `earliest_date` is an lower bound on what we should search in our 55 | // github query. Why is it `start` date minus 1? 56 | // 57 | // Because: the "since" parameter in the github API is an exclusive 58 | // bound. We need an inclusive bound, so we go yet another day prior for 59 | // this bound on the github search. 60 | let since_date = self 61 | .bound_to_date(Bound::Commit(start_sha.to_string()))? 62 | .pred_opt() 63 | .unwrap(); 64 | 65 | eprintln!( 66 | "fetching (via remote github) commits from max({}, {}) to {}", 67 | start_sha, 68 | since_date.format(crate::YYYY_MM_DD), 69 | end_sha 70 | ); 71 | 72 | let query = github::CommitsQuery { 73 | since_date: &since_date.format(crate::YYYY_MM_DD).to_string(), 74 | earliest_sha: start_sha, 75 | most_recent_sha: end_sha, 76 | }; 77 | 78 | query.get_commits() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/toolchains.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fs; 3 | use std::io::{self, Read, Write}; 4 | use std::path::{Path, PathBuf}; 5 | use std::process::{self, Command, Stdio}; 6 | 7 | use chrono::NaiveDate; 8 | use colored::Colorize; 9 | use dialoguer::Select; 10 | use flate2::read::GzDecoder; 11 | use log::debug; 12 | use pbr::{ProgressBar, Units}; 13 | use reqwest::blocking::{Client, Response}; 14 | use reqwest::header::CONTENT_LENGTH; 15 | use rustc_version::Channel; 16 | use tar::Archive; 17 | use tee::TeeReader; 18 | use xz2::read::XzDecoder; 19 | 20 | use crate::{Config, GitDate}; 21 | 22 | pub const YYYY_MM_DD: &str = "%Y-%m-%d"; 23 | 24 | pub(crate) const NIGHTLY_SERVER: &str = "https://static.rust-lang.org/dist"; 25 | const CI_SERVER: &str = "https://ci-artifacts.rust-lang.org"; 26 | 27 | #[derive(thiserror::Error, Debug)] 28 | pub(crate) enum InstallError { 29 | #[error("Could not find {spec}; url: {url}")] 30 | NotFound { url: String, spec: ToolchainSpec }, 31 | #[error("Could not download toolchain: {0}")] 32 | Download(#[source] DownloadError), 33 | #[error("Could not create tempdir: {0}")] 34 | TempDir(#[source] io::Error), 35 | #[error("Could not move tempdir into destination: {0}")] 36 | Move(#[source] io::Error), 37 | #[error("Could not run subcommand {cmd}: {err}")] 38 | Subcommand { 39 | cmd: String, 40 | #[source] 41 | err: io::Error, 42 | }, 43 | } 44 | 45 | #[derive(Debug)] 46 | pub(crate) enum TestOutcome { 47 | Baseline, 48 | Regressed, 49 | } 50 | 51 | #[derive(Clone, PartialEq, Eq, Debug)] 52 | pub(crate) struct Toolchain { 53 | pub(crate) spec: ToolchainSpec, 54 | pub(crate) host: String, 55 | pub(crate) std_targets: Vec, 56 | } 57 | 58 | impl fmt::Display for Toolchain { 59 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 60 | write!(f, "{}", self.spec) 61 | } 62 | } 63 | 64 | impl Toolchain { 65 | pub(crate) fn rustup_name(&self) -> String { 66 | match self.spec { 67 | ToolchainSpec::Ci { ref commit, alt } => { 68 | let alt_s = if alt { 69 | "-alt".to_string() 70 | } else { 71 | String::new() 72 | }; 73 | format!("bisector-ci-{commit}{alt_s}-{}", self.host) 74 | } 75 | // N.B. We need to call this with a nonstandard name so that rustup utilizes the 76 | // fallback cargo logic. 77 | ToolchainSpec::Nightly { ref date } => { 78 | format!("bisector-nightly-{}-{}", date.format(YYYY_MM_DD), self.host) 79 | } 80 | } 81 | } 82 | /// This returns the date of the default toolchain, if it is a nightly toolchain. 83 | /// Returns `None` if the installed toolchain is not a nightly toolchain. 84 | pub(crate) fn default_nightly() -> Option { 85 | rustc_version::version_meta() 86 | .ok() 87 | .filter(|v| v.channel == Channel::Nightly) 88 | // rustc commit date is off-by-one, see #112 89 | .and_then(|v| { 90 | parse_to_naive_date(&v.commit_date?) 91 | .ok() 92 | .map(|d| d.succ_opt().unwrap()) 93 | }) 94 | } 95 | 96 | pub(crate) fn is_current_nightly(&self) -> bool { 97 | if let ToolchainSpec::Nightly { date } = self.spec { 98 | if let Some(default_date) = Self::default_nightly() { 99 | return default_date == date; 100 | } 101 | } 102 | 103 | false 104 | } 105 | 106 | pub(crate) fn install( 107 | &self, 108 | client: &Client, 109 | dl_params: &DownloadParams, 110 | ) -> Result<(), InstallError> { 111 | let tc_stdstream_str = format!("{self}"); 112 | eprintln!("installing {}", tc_stdstream_str.green()); 113 | let tmpdir = tempfile::Builder::new() 114 | .prefix(&self.rustup_name()) 115 | .tempdir_in(&dl_params.tmp_dir) 116 | .map_err(InstallError::TempDir)?; 117 | let dest = dl_params.install_dir.join(self.rustup_name()); 118 | if dl_params.force_install { 119 | let _ = self.do_remove(dl_params); 120 | } 121 | 122 | if dest.is_dir() { 123 | // already installed 124 | return Ok(()); 125 | } 126 | 127 | if self.is_current_nightly() { 128 | // make link to pre-existing installation 129 | debug!("installing (via link) {}", self); 130 | 131 | let nightly_path: String = { 132 | let mut cmd = Command::new("rustc"); 133 | cmd.args(["--print", "sysroot"]); 134 | 135 | let stdout = cmd 136 | .output() 137 | .map_err(|err| InstallError::Subcommand { 138 | cmd: format!("{cmd:?}"), 139 | err, 140 | })? 141 | .stdout; 142 | let output = String::from_utf8_lossy(&stdout); 143 | // the output should be the path, terminated by a newline 144 | let mut path = output.to_string(); 145 | let last = path.pop(); 146 | assert_eq!(last, Some('\n')); 147 | path 148 | }; 149 | let mut cmd = Command::new("rustup"); 150 | cmd.args(["toolchain", "link", &self.rustup_name(), &nightly_path]); 151 | let status = cmd.status().map_err(|err| InstallError::Subcommand { 152 | cmd: format!("{cmd:?}"), 153 | err, 154 | })?; 155 | return if status.success() { 156 | Ok(()) 157 | } else { 158 | Err(InstallError::Subcommand { 159 | cmd: format!("{cmd:?}"), 160 | err: io::Error::new( 161 | io::ErrorKind::Other, 162 | "thiserror::Errored to link via `rustup`", 163 | ), 164 | }) 165 | }; 166 | } 167 | 168 | debug!("installing via download {}", self); 169 | 170 | let location = match self.spec { 171 | ToolchainSpec::Ci { ref commit, .. } => commit.to_string(), 172 | ToolchainSpec::Nightly { ref date } => date.format(YYYY_MM_DD).to_string(), 173 | }; 174 | 175 | let components = dl_params 176 | .components 177 | .iter() 178 | .map(|component| { 179 | if component == "rust-src" { 180 | // rust-src is target-independent 181 | "rust-src-nightly".to_string() 182 | } else { 183 | format!("{component}-nightly-{}", self.host) 184 | } 185 | }) 186 | .chain( 187 | self.std_targets 188 | .iter() 189 | .map(|target| format!("rust-std-nightly-{target}")), 190 | ); 191 | 192 | for component in components { 193 | download_tarball( 194 | client, 195 | &component, 196 | &format!("{}/{location}/{component}.tar", dl_params.url_prefix), 197 | tmpdir.path(), 198 | ) 199 | .map_err(|e| { 200 | if let DownloadError::NotFound(url) = e { 201 | InstallError::NotFound { 202 | url, 203 | spec: self.spec.clone(), 204 | } 205 | } else { 206 | InstallError::Download(e) 207 | } 208 | })?; 209 | } 210 | 211 | fs::rename(tmpdir.keep(), dest).map_err(InstallError::Move) 212 | } 213 | 214 | pub(crate) fn remove(&self, dl_params: &DownloadParams) -> io::Result<()> { 215 | eprintln!("uninstalling {}", self); 216 | self.do_remove(dl_params) 217 | } 218 | 219 | /// Removes the (previously installed) bisector rustc described by `dl_params`. 220 | /// 221 | /// The main reason to call this (instead of `fs::remove_dir_all` directly) 222 | /// is to guard against deleting state not managed by `cargo-bisect-rustc`. 223 | fn do_remove(&self, dl_params: &DownloadParams) -> io::Result<()> { 224 | let rustup_name = self.rustup_name(); 225 | 226 | // Guard against destroying directories that this tool didn't create. 227 | assert!( 228 | rustup_name.starts_with("bisector-nightly") || rustup_name.starts_with("bisector-ci") 229 | ); 230 | 231 | let dir = dl_params.install_dir.join(rustup_name); 232 | fs::remove_dir_all(&dir) 233 | } 234 | 235 | pub(crate) fn run_test(&self, cfg: &Config) -> process::Output { 236 | if !cfg.args.preserve_target { 237 | let _ = fs::remove_dir_all( 238 | cfg.args 239 | .test_dir 240 | .join(&format!("target-{}", self.rustup_name())), 241 | ); 242 | } 243 | let script = cfg.args.script.as_ref().map(|script| { 244 | if script.exists() { 245 | std::env::current_dir().unwrap().join(script) 246 | } else { 247 | script.to_owned() 248 | } 249 | }); 250 | 251 | let mut cmd = match (script, cfg.args.timeout) { 252 | (Some(script), None) => { 253 | let mut cmd = Command::new(script); 254 | cmd.env("RUSTUP_TOOLCHAIN", self.rustup_name()); 255 | cmd.args(&cfg.args.command_args); 256 | cmd 257 | } 258 | (None, None) => { 259 | let mut cmd = Command::new("cargo"); 260 | self.set_cargo_args_and_envs(&mut cmd, cfg); 261 | cmd 262 | } 263 | (Some(script), Some(timeout)) => { 264 | let mut cmd = Command::new("timeout"); 265 | cmd.arg(timeout.to_string()); 266 | cmd.arg(script); 267 | cmd.args(&cfg.args.command_args); 268 | cmd.env("RUSTUP_TOOLCHAIN", self.rustup_name()); 269 | cmd 270 | } 271 | (None, Some(timeout)) => { 272 | let mut cmd = Command::new("timeout"); 273 | cmd.arg(timeout.to_string()); 274 | cmd.arg("cargo"); 275 | self.set_cargo_args_and_envs(&mut cmd, cfg); 276 | cmd 277 | } 278 | }; 279 | cmd.current_dir(&cfg.args.test_dir); 280 | cmd.env("CARGO_TARGET_DIR", format!("target-{}", self.rustup_name())); 281 | if let Some(target) = &cfg.args.target { 282 | cmd.env("CARGO_BUILD_TARGET", target); 283 | } 284 | 285 | // let `cmd` capture stderr for us to process afterward. 286 | let must_capture_output = cfg.args.regress.must_process_stderr(); 287 | let emit_output = cfg.args.emit_cargo_output() || cfg.args.prompt; 288 | 289 | let default_stdio = if must_capture_output { 290 | Stdio::piped 291 | } else if emit_output { 292 | Stdio::inherit 293 | } else { 294 | Stdio::null 295 | }; 296 | 297 | cmd.stdout(default_stdio()); 298 | cmd.stderr(default_stdio()); 299 | 300 | if cfg.args.emit_cmd() { 301 | eprintln!("Running `{cmd:?}`"); 302 | } 303 | 304 | let output = match cmd.output() { 305 | Ok(output) => output, 306 | Err(err) => { 307 | panic!("thiserror::Errored to run {:?}: {:?}", cmd, err); 308 | } 309 | }; 310 | 311 | // if we captured the stdout above but still need to emit it, then do so now 312 | if must_capture_output && emit_output { 313 | io::stdout().write_all(&output.stdout).unwrap(); 314 | io::stderr().write_all(&output.stderr).unwrap(); 315 | } 316 | output 317 | } 318 | 319 | fn set_cargo_args_and_envs(&self, cmd: &mut Command, cfg: &Config) { 320 | let rustup_name = format!("+{}", self.rustup_name()); 321 | cmd.arg(&rustup_name); 322 | if cfg.args.command_args.is_empty() { 323 | cmd.arg("build"); 324 | } else { 325 | cmd.args(&cfg.args.command_args); 326 | } 327 | if cfg.args.pretend_to_be_stable && self.is_current_nightly() { 328 | // Forbid using features 329 | cmd.env( 330 | "RUSTFLAGS", 331 | format!( 332 | "{} -Zallow-features=", 333 | std::env::var("RUSTFLAGS").unwrap_or_default() 334 | ), 335 | ); 336 | // Make rustc report a stable version string derived from the current nightly's version string. 337 | let version = rustc_version::version_meta().unwrap().semver; 338 | cmd.env( 339 | "RUSTC_OVERRIDE_VERSION_STRING", 340 | format!("{}.{}.{}", version.major, version.minor, version.patch), 341 | ); 342 | } 343 | } 344 | 345 | pub(crate) fn test(&self, cfg: &Config) -> TestOutcome { 346 | eprintln!("testing..."); 347 | let outcome = if cfg.args.prompt { 348 | loop { 349 | let output = self.run_test(cfg); 350 | let status = output.status; 351 | 352 | //timeout returns exit code 124 on expiration 353 | if status.code() == Some(124) { 354 | match cfg.args.timeout { 355 | Some(_) => break TestOutcome::Regressed, 356 | None => panic!("Process timed out but no timeout was specified. Please check host configuration for timeouts and try again.") 357 | } 358 | } 359 | 360 | eprintln!("\n\n{} finished with exit code {:?}.", self, status.code()); 361 | eprintln!("please select an action to take:"); 362 | 363 | let default_choice = match cfg.default_outcome_of_output(&output) { 364 | TestOutcome::Regressed => 0, 365 | TestOutcome::Baseline => 1, 366 | }; 367 | 368 | match Select::new() 369 | .items(&["mark regressed", "mark baseline", "retry"]) 370 | .default(default_choice) 371 | .interact() 372 | .unwrap() 373 | { 374 | 0 => break TestOutcome::Regressed, 375 | 1 => break TestOutcome::Baseline, 376 | 2 => continue, 377 | _ => unreachable!(), 378 | } 379 | } 380 | } else { 381 | let output = self.run_test(cfg); 382 | cfg.default_outcome_of_output(&output) 383 | }; 384 | 385 | outcome 386 | } 387 | } 388 | 389 | pub fn parse_to_naive_date(s: &str) -> chrono::ParseResult { 390 | NaiveDate::parse_from_str(s, YYYY_MM_DD) 391 | } 392 | 393 | #[derive(Clone, PartialEq, Eq, Debug)] 394 | pub(crate) enum ToolchainSpec { 395 | Ci { commit: String, alt: bool }, 396 | Nightly { date: GitDate }, 397 | } 398 | 399 | impl fmt::Display for ToolchainSpec { 400 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 401 | match *self { 402 | ToolchainSpec::Ci { ref commit, alt } => { 403 | let alt_s = if alt { 404 | "-alt".to_string() 405 | } else { 406 | String::new() 407 | }; 408 | write!(f, "{}{}", commit, alt_s) 409 | } 410 | ToolchainSpec::Nightly { ref date } => write!(f, "nightly-{}", date.format(YYYY_MM_DD)), 411 | } 412 | } 413 | } 414 | 415 | #[derive(Clone, Debug)] 416 | pub(crate) struct DownloadParams { 417 | url_prefix: String, 418 | tmp_dir: PathBuf, 419 | install_dir: PathBuf, 420 | components: Vec, 421 | force_install: bool, 422 | } 423 | 424 | impl DownloadParams { 425 | pub(crate) fn for_ci(cfg: &Config) -> Self { 426 | let url_prefix = format!( 427 | "{CI_SERVER}/rustc-builds{}", 428 | if cfg.args.alt { "-alt" } else { "" } 429 | ); 430 | 431 | Self::from_cfg_with_url_prefix(cfg, url_prefix) 432 | } 433 | 434 | pub(crate) fn for_nightly(cfg: &Config) -> Self { 435 | Self::from_cfg_with_url_prefix(cfg, NIGHTLY_SERVER.to_string()) 436 | } 437 | 438 | fn from_cfg_with_url_prefix(cfg: &Config, url_prefix: String) -> Self { 439 | let mut components = vec!["rustc".to_string()]; 440 | if !cfg.args.without_cargo { 441 | components.push("cargo".to_string()); 442 | } 443 | if cfg.args.with_dev { 444 | components.push("rustc-dev".to_string()); 445 | // llvm-tools-(preview) is currently required for using rustc-dev 446 | // https://github.com/rust-lang/rust/issues/72594 447 | components.push("llvm-tools".to_string()); 448 | } 449 | if cfg.args.with_src { 450 | components.push("rust-src".to_string()); 451 | } 452 | components.extend(cfg.args.components.clone()); 453 | 454 | DownloadParams { 455 | url_prefix, 456 | tmp_dir: cfg.rustup_tmp_path.clone(), 457 | install_dir: cfg.toolchains_path.clone(), 458 | components, 459 | force_install: cfg.args.force_install, 460 | } 461 | } 462 | } 463 | 464 | #[derive(thiserror::Error, Debug)] 465 | pub(crate) enum ArchiveError { 466 | #[error("thiserror::Errored to parse archive: {0}")] 467 | Archive(#[source] io::Error), 468 | #[error("thiserror::Errored to create directory: {0}")] 469 | CreateDir(#[source] io::Error), 470 | } 471 | 472 | #[derive(thiserror::Error, Debug)] 473 | pub(crate) enum DownloadError { 474 | #[error("Tarball not found at {0}")] 475 | NotFound(String), 476 | #[error("A reqwest error occurred: {0}")] 477 | Reqwest(#[from] reqwest::Error), 478 | #[error("An archive error occurred: {0}")] 479 | Archive(#[from] ArchiveError), 480 | } 481 | 482 | pub(crate) fn download_progress( 483 | client: &Client, 484 | name: &str, 485 | url: &str, 486 | ) -> Result>, DownloadError> { 487 | debug!("downloading <{}>...", url); 488 | 489 | let response = client.get(url).send()?; 490 | 491 | if response.status() == reqwest::StatusCode::NOT_FOUND { 492 | return Err(DownloadError::NotFound(url.to_string())); 493 | } 494 | let response = response.error_for_status()?; 495 | 496 | let length = response 497 | .headers() 498 | .get(CONTENT_LENGTH) 499 | .and_then(|c| c.to_str().ok()?.parse().ok()) 500 | .unwrap_or(0); 501 | let mut bar = ProgressBar::new(length); 502 | bar.set_units(Units::Bytes); 503 | bar.message(&format!("{name}: ")); 504 | 505 | Ok(TeeReader::new(response, bar)) 506 | } 507 | 508 | fn download_tar_xz( 509 | client: &Client, 510 | name: &str, 511 | url: &str, 512 | dest: &Path, 513 | ) -> Result<(), DownloadError> { 514 | let response = XzDecoder::new(download_progress(client, name, url)?); 515 | unarchive(response, dest).map_err(DownloadError::Archive) 516 | } 517 | 518 | fn download_tar_gz( 519 | client: &Client, 520 | name: &str, 521 | url: &str, 522 | dest: &Path, 523 | ) -> Result<(), DownloadError> { 524 | let response = GzDecoder::new(download_progress(client, name, url)?); 525 | unarchive(response, dest).map_err(DownloadError::Archive) 526 | } 527 | 528 | fn unarchive(r: R, dest: &Path) -> Result<(), ArchiveError> { 529 | for entry in Archive::new(r).entries().map_err(ArchiveError::Archive)? { 530 | let mut entry = entry.map_err(ArchiveError::Archive)?; 531 | let entry_path = entry.path().map_err(ArchiveError::Archive)?; 532 | let dest_path = { 533 | let mut components = entry_path.components(); 534 | // Remove the first two components, which are usually of the form 535 | // COMPONENT-nightly-HOST/COMPONENT. 536 | components.next(); 537 | // The second component here may also include some top-level 538 | // things like license files and install scripts. These will be 539 | // skipped in the check below if the path is empty. 540 | components.next(); 541 | dest.join(components.as_path()) 542 | }; 543 | if dest_path == dest { 544 | // Skip root dir and files outside of "COMPONENT". 545 | continue; 546 | } 547 | fs::create_dir_all(dest_path.parent().unwrap()).map_err(ArchiveError::CreateDir)?; 548 | entry.unpack(dest_path).map_err(ArchiveError::Archive)?; 549 | } 550 | 551 | Ok(()) 552 | } 553 | 554 | fn download_tarball( 555 | client: &Client, 556 | name: &str, 557 | url: &str, 558 | dest: &Path, 559 | ) -> Result<(), DownloadError> { 560 | match download_tar_xz(client, name, &format!("{url}.xz"), dest) { 561 | Err(DownloadError::NotFound { .. }) => { 562 | download_tar_gz(client, name, &format!("{url}.gz"), dest) 563 | } 564 | res => res, 565 | } 566 | } 567 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Quick guidelines for tests 2 | 3 | If you change the command line parameters of cargo-bisect, tests will fail, the crate `trycmd` is used to keep track of these changes. 4 | 5 | In order to update files under `tests/cmd/*.{stdout,stderr}`, run the test generating the new expected results: 6 | 7 | `TRYCMD=dump cargo test` 8 | 9 | it will create a `dump` directory in the project root. Then move `dump/*.{stdout,stderr}` into `./tests/cmd` and run tests again. They should be all green now. 10 | 11 | Note: if the local tests generate output specific for your machine, please replace that output with `[..]`, else CI tests will fail. Example: 12 | 13 | ``` diff 14 | - --host Host triple for the compiler [default: x86_64-unknown-linux-gnu] 15 | + --host Host triple for the compiler [default: [..]] 16 | ``` 17 | 18 | See the trycmd [documentation](https://docs.rs/trycmd/latest/trycmd/) for more info. 19 | -------------------------------------------------------------------------------- /tests/cli_tests.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn cli_tests() { 3 | trycmd::TestCases::new().case("tests/cmd/*.toml"); 4 | } 5 | -------------------------------------------------------------------------------- /tests/cmd/bare-h.stderr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/cargo-bisect-rustc/bd2811b4b53f39119dfbb09129ef2340d5f139dd/tests/cmd/bare-h.stderr -------------------------------------------------------------------------------- /tests/cmd/bare-h.stdout: -------------------------------------------------------------------------------- 1 | Bisects rustc toolchains with rustup 2 | 3 | Usage: cargo bisect-rustc [OPTIONS] [-- ...] 4 | 5 | Arguments: 6 | [COMMAND_ARGS]... Arguments to pass to cargo or the file specified by --script during tests 7 | 8 | Options: 9 | -a, --alt Download the alt build instead of normal build 10 | --access How to access Rust git repository [default: github] [possible 11 | values: checkout, github] 12 | --by-commit Bisect via commit artifacts 13 | -c, --component additional components to install 14 | --end Right bound for search (*with* regression). You can use a date 15 | (YYYY-MM-DD), git tag name (e.g. 1.58.0) or git commit SHA. 16 | --force-install Force installation over existing artifacts 17 | -h, --help Print help (see more with '--help') 18 | --host Host triple for the compiler [default: [..]] 19 | --install Install the given artifact 20 | --preserve Preserve the downloaded artifacts 21 | --preserve-target Preserve the target directory used for builds 22 | --pretend-to-be-stable Pretend to be a stable compiler (disable features, report a version 23 | that looks like a stable version) 24 | --prompt Manually evaluate for regression with prompts 25 | --regress Custom regression definition [default: error] [possible values: 26 | error, success, ice, non-ice, non-error] 27 | --script