├── .clippy.toml ├── .dockerignore ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── book.yml │ ├── build-release-binaries.yaml │ ├── coverage.yml │ ├── docker.yml │ ├── fmt.yml │ ├── licenses_and_advisories.yml │ ├── linter.yml │ ├── msrv.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── about.hbs ├── about.toml ├── book ├── .gitignore ├── book.toml └── src │ ├── SUMMARY.md │ ├── ci │ ├── gitlab.md │ └── index.md │ ├── commands │ ├── find.md │ ├── help.md │ ├── index.md │ ├── list.md │ ├── set.md │ ├── show.md │ └── verify.md │ ├── concepts │ └── index.md │ ├── getting-started │ ├── cargo-workspace.md │ ├── index.md │ ├── installation.md │ ├── quick-start.md │ └── rust-releases-proxy.md │ ├── index.md │ ├── output-formats │ ├── human.md │ ├── index.md │ ├── json.md │ ├── minimal.md │ └── no-user-output.md │ └── releases │ ├── index.md │ ├── v0.15_v0.16_highlights.md │ └── v0.15_v0.16_json.md ├── build.rs ├── codecov.yml ├── crates └── msrv │ ├── Cargo.toml │ └── src │ ├── lib.rs │ └── msrv.rs ├── deny.toml ├── design └── 20230519_visualizer_ui.excalidraw ├── src ├── bin │ └── cargo-msrv.rs ├── cli │ ├── custom_check_opts.rs │ ├── mod.rs │ ├── rust_releases_opts.rs │ ├── shared_opts.rs │ └── toolchain_opts.rs ├── compatibility │ ├── mod.rs │ ├── rustup_toolchain_check.rs │ └── testing.rs ├── context │ ├── find.rs │ ├── list.rs │ ├── mod.rs │ ├── set.rs │ ├── show.rs │ └── verify.rs ├── dependency_graph │ ├── mod.rs │ └── resolver.rs ├── error.rs ├── exit_code.rs ├── external_command │ ├── cargo_command.rs │ ├── mod.rs │ └── rustup_command.rs ├── io.rs ├── lib.rs ├── lockfile.rs ├── log_level.rs ├── manifest │ ├── bare_version.rs │ └── mod.rs ├── msrv.rs ├── outcome.rs ├── reporter │ ├── event │ │ ├── auxiliary_output.rs │ │ ├── check_method.rs │ │ ├── check_result.rs │ │ ├── check_toolchain.rs │ │ ├── fetch_index.rs │ │ ├── meta.rs │ │ ├── mod.rs │ │ ├── progress.rs │ │ ├── scope.rs │ │ ├── search_method.rs │ │ ├── selected_packages.rs │ │ ├── setup_toolchain.rs │ │ ├── shared │ │ │ ├── compatibility.rs │ │ │ └── mod.rs │ │ ├── subcommand_init.rs │ │ ├── subcommand_result.rs │ │ ├── termination.rs │ │ ├── types │ │ │ ├── find_result.rs │ │ │ ├── list_result │ │ │ │ ├── direct_deps.rs │ │ │ │ ├── metadata.rs │ │ │ │ ├── mod.rs │ │ │ │ └── ordered_by_msrv.rs │ │ │ ├── mod.rs │ │ │ ├── set_result.rs │ │ │ ├── show_result.rs │ │ │ └── verify_result.rs │ │ └── unable_to_confirm_valid_release_version.rs │ ├── formatting.rs │ ├── mod.rs │ ├── testing.rs │ └── ui │ │ ├── discard_output.rs │ │ ├── human.rs │ │ ├── json │ │ ├── mod.rs │ │ ├── test_find.rs │ │ ├── test_set.rs │ │ ├── test_show.rs │ │ └── test_verify.rs │ │ ├── minimal.rs │ │ ├── mod.rs │ │ └── testing.rs ├── rust │ ├── default_target.rs │ ├── mod.rs │ ├── release.rs │ ├── release_index.rs │ ├── releases_filter.rs │ ├── setup_toolchain.rs │ └── toolchain.rs ├── search_method │ ├── bisect.rs │ ├── linear.rs │ └── mod.rs ├── sub_command │ ├── find │ │ ├── mod.rs │ │ └── tests.rs │ ├── list.rs │ ├── mod.rs │ ├── set.rs │ ├── show.rs │ └── verify.rs ├── typed_bool.rs └── writer │ ├── mod.rs │ ├── toolchain_file.rs │ └── write_msrv.rs └── tests ├── common ├── mod.rs ├── reporter.rs ├── runner.rs ├── sub_cmd_find.rs └── sub_cmd_verify.rs ├── find_msrv.rs ├── fixtures ├── 1.29.2 │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── 1.30.0 │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── 1.35.0 │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── 1.36.0 │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── 1.37.0 │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── 1.38.0 │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── 1.56.0-edition-2018 │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── 1.56.0-edition-2021 │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── cargo-feature-required │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── cargo-feature-requires-none │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── rustc │ └── hello.rs ├── unbuildable-with-msrv │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── unbuildable │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── virtual-workspace │ ├── .gitignore │ ├── Cargo.toml │ ├── a │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── b │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ └── description.md └── workspace-inheritance │ ├── .gitignore │ ├── Cargo.toml │ ├── a │ ├── Cargo.toml │ └── src │ │ └── lib.rs │ ├── b │ ├── Cargo.toml │ └── src │ │ └── lib.rs │ └── c │ ├── Cargo.toml │ └── src │ └── lib.rs ├── user_output.rs └── verify_msrv.rs /.clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.85" 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /.github/ 2 | /target/ 3 | /book/ 4 | /tests/ -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ["foresterre"] 2 | buy_me_a_coffee: "foresterre" 3 | thanks_dev: "u/gh/foresterre" 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | labels: 9 | - C-dependency-update 10 | - package-ecosystem: github-actions 11 | directory: "/" 12 | schedule: 13 | interval: daily 14 | open-pull-requests-limit: 10 15 | labels: 16 | - C-dependency-update 17 | -------------------------------------------------------------------------------- /.github/workflows/book.yml: -------------------------------------------------------------------------------- 1 | name: "ci-book" 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | merge_group: 7 | jobs: 8 | book: 9 | name: build-and-publish-book 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: checkout_repository 13 | uses: actions/checkout@v4 14 | 15 | - name: setup_mdbook 16 | uses: peaceiris/actions-mdbook@v2 17 | with: 18 | mdbook-version: '0.4.13' 19 | 20 | - name: build_mdbook 21 | run: cd book && mdbook build -d ../output 22 | 23 | - name: deploy_mdbook 24 | uses: peaceiris/actions-gh-pages@v4 25 | if: ${{ github.ref == 'refs/heads/main' }} 26 | with: 27 | github_token: ${{ secrets.GITHUB_TOKEN }} 28 | publish_dir: ./output 29 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: "ci-coverage" 2 | on: 3 | pull_request: 4 | jobs: 5 | coverage: 6 | name: coverage 7 | runs-on: ubuntu-latest 8 | env: 9 | CARGO_TERM_COLOR: always 10 | steps: 11 | - name: checkout_repository 12 | uses: actions/checkout@v4 13 | 14 | - name: install_rust_nightly 15 | uses: dtolnay/rust-toolchain@master 16 | with: 17 | toolchain: nightly-2025-03-12 18 | 19 | - name: install_code_coverage_tool 20 | uses: taiki-e/install-action@cargo-llvm-cov 21 | 22 | - name: generate_code_coverage 23 | # `--lib` ensures we only run unit tests, no integration tests. 24 | # Integration tests are disabled, because they rely on Rust toolchains which do not support the rustc option 25 | # `-C instrument-coverage`, needed by llvm-cov. 26 | run: cargo llvm-cov --all-features --locked --lcov --output-path lcov.info --lib 27 | 28 | - name: upload_code_coverage 29 | uses: codecov/codecov-action@v5 30 | with: 31 | files: lcov.info 32 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: "ci-docker" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | merge_group: 9 | jobs: 10 | docker: 11 | name: Docker Build and Push 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Docker meta 18 | id: meta 19 | uses: docker/metadata-action@v5 20 | with: 21 | images: | 22 | foresterre/cargo-msrv 23 | ghcr.io/${{ github.repository_owner }}/cargo-msrv 24 | tags: | 25 | type=schedule 26 | type=ref,event=branch 27 | type=ref,event=pr 28 | type=sha 29 | type=raw,value=latest 30 | type=semver,pattern={{version}} 31 | 32 | - name: Set up Docker Buildx 33 | id: buildx 34 | uses: docker/setup-buildx-action@v3 35 | 36 | - name: Cache Docker layers 37 | uses: actions/cache@v4 38 | with: 39 | path: /tmp/.buildx-cache 40 | key: ${{ runner.os }}-buildx-${{ github.sha }} 41 | restore-keys: | 42 | ${{ runner.os }}-buildx- 43 | 44 | - name: Login to Docker Hub 45 | if: github.event_name != 'pull_request' 46 | uses: docker/login-action@v3 47 | with: 48 | username: foresterre 49 | password: ${{ secrets.DOCKER_TOKEN }} 50 | 51 | - name: Login to GHCR 52 | if: github.event_name != 'pull_request' 53 | uses: docker/login-action@v3 54 | with: 55 | registry: ghcr.io 56 | username: ${{ github.repository_owner }} 57 | password: ${{ secrets.GITHUB_TOKEN }} 58 | 59 | - name: Build and push 60 | id: docker_build 61 | uses: docker/build-push-action@v6 62 | with: 63 | context: ./ 64 | file: ./Dockerfile 65 | builder: ${{ steps.buildx.outputs.name }} 66 | push: ${{ github.event_name != 'pull_request' }} 67 | tags: ${{ steps.meta.outputs.tags }} 68 | labels: ${{ steps.meta.outputs.labels }} 69 | cache-from: type=local,src=/tmp/.buildx-cache 70 | cache-to: type=local,dest=/tmp/.buildx-cache 71 | 72 | - name: Update Docker Hub description 73 | if: github.event_name != 'pull_request' 74 | uses: peter-evans/dockerhub-description@v4 75 | with: 76 | username: foresterre 77 | password: ${{ secrets.DOCKER_TOKEN }} 78 | 79 | - name: Image digest 80 | run: echo ${{ steps.docker_build.outputs.digest }} 81 | -------------------------------------------------------------------------------- /.github/workflows/fmt.yml: -------------------------------------------------------------------------------- 1 | name: "ci-fmt" 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | merge_group: 9 | jobs: 10 | fmt: 11 | name: fmt 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: checkout_repository 15 | uses: actions/checkout@v4 16 | 17 | - name: install_rust 18 | uses: dtolnay/rust-toolchain@stable 19 | with: 20 | components: rustfmt 21 | 22 | - name: check_formatting 23 | run: | 24 | cargo fmt -- --check 25 | -------------------------------------------------------------------------------- /.github/workflows/licenses_and_advisories.yml: -------------------------------------------------------------------------------- 1 | name: "ci-licenses_and_advisories" 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | merge_group: 9 | jobs: 10 | licenses_and_advisories: 11 | name: licenses_and_advisories 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | checks: 16 | - advisories 17 | - bans licenses sources 18 | 19 | continue-on-error: ${{ matrix.checks == 'advisories' }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: EmbarkStudios/cargo-deny-action@v2 23 | with: 24 | log-level: error 25 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: "ci-linter" 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | merge_group: 9 | jobs: 10 | linter: 11 | name: linter 12 | runs-on: ubuntu-latest 13 | continue-on-error: true 14 | steps: 15 | - name: checkout_repo 16 | uses: actions/checkout@v4 17 | 18 | - name: install_rust 19 | uses: dtolnay/rust-toolchain@stable 20 | with: 21 | components: clippy 22 | 23 | - name: check_clippy 24 | uses: actions-rs/clippy-check@v1 25 | with: 26 | token: ${{ secrets.GITHUB_TOKEN }} 27 | args: --all-features --all-targets --workspace 28 | -------------------------------------------------------------------------------- /.github/workflows/msrv.yml: -------------------------------------------------------------------------------- 1 | name: "ci-msrv" 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | merge_group: 9 | schedule: 10 | - cron: '00 06 * * *' 11 | jobs: 12 | # MSRV check and e2e test 13 | msrv: 14 | name: msrv 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | build: [ ubuntu, macos ] # [, windows] ... Disabled Windows for now, see #1036 20 | include: 21 | - build: ubuntu 22 | os: ubuntu-latest 23 | 24 | - build: macos 25 | os: macos-latest 26 | 27 | # - build: windows 28 | # os: windows-latest 29 | continue-on-error: true 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: dtolnay/rust-toolchain@stable 33 | - if: matrix.build == 'ubuntu' 34 | run: cargo install cargo-msrv 35 | - if: matrix.build != 'ubuntu' 36 | run: cargo install cargo-msrv --no-default-features 37 | - run: cargo msrv --version 38 | - run: cargo msrv verify --output-format json 39 | - if: ${{ failure() }} 40 | run: cargo msrv find --output-format json 41 | 42 | msrv_workspace_crates: 43 | runs-on: ubuntu-latest 44 | continue-on-error: true 45 | strategy: 46 | matrix: 47 | crate: [ 48 | "msrv" 49 | ] 50 | steps: 51 | - uses: actions/checkout@v4 52 | - uses: dtolnay/rust-toolchain@stable 53 | - run: cargo install cargo-msrv 54 | - run: cargo msrv --version 55 | - run: cargo msrv verify --output-format json -- cargo check --all-features -p ${{ matrix.crate }} 56 | - if: ${{ failure() }} 57 | run: cargo msrv find --output-format json -- cargo check --all-features -p ${{ matrix.crate }} 58 | 59 | # The same as the 'msrv' job, except it takes the latest release, including beta releases 60 | msrv_pre_release: 61 | name: msrv_pre_release 62 | runs-on: ubuntu-latest 63 | continue-on-error: true 64 | steps: 65 | - uses: actions/checkout@v4 66 | - uses: dtolnay/rust-toolchain@stable 67 | - uses: taiki-e/install-action@v2 68 | with: 69 | tool: cargo-binstall 70 | - run: cargo binstall --no-confirm cargo-msrv 71 | - run: cargo msrv --version 72 | - run: cargo msrv verify --output-format json 73 | - if: ${{ failure() }} 74 | run: cargo msrv find --output-format json 75 | 76 | # The same as the 'msrv' job, except it takes the latest development branch, as a form of test 77 | # we don't use --all-features here! 78 | msrv_development: 79 | name: msrv_development 80 | runs-on: ubuntu-latest 81 | continue-on-error: true 82 | steps: 83 | - uses: actions/checkout@v4 84 | - uses: dtolnay/rust-toolchain@stable 85 | - run: cargo install --git https://github.com/foresterre/cargo-msrv.git cargo-msrv 86 | - run: cargo msrv --version 87 | - run: cargo msrv verify --output-format json 88 | - if: ${{ failure() }} 89 | run: cargo msrv find --output-format json 90 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "ci-test" 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | merge_group: 9 | schedule: 10 | - cron: "00 05 * * *" 11 | jobs: 12 | test: 13 | name: test 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | build: [ubuntu-stable, macos-stable, win-msvc-stable] # win-gnu-stable 19 | include: 20 | - build: ubuntu-stable 21 | os: ubuntu-latest 22 | rust: stable 23 | 24 | - build: macos-stable 25 | os: macos-13 26 | rust: stable 27 | 28 | # FIXME: This is currently broken, see below. 29 | # * https://github.com/foresterre/cargo-msrv/pull/842 30 | # * https://github.com/rust-lang/rust/issues/112368 31 | # - build: win-gnu-stable 32 | # os: windows-latest 33 | # rust: stable-x86_64-gnu 34 | 35 | - build: win-msvc-stable 36 | os: windows-latest 37 | rust: stable 38 | 39 | steps: 40 | - name: checkout_repository 41 | uses: actions/checkout@v4 42 | 43 | # We would prefer to use `dtolnay/rust-toolchain@master with toolchain=${{ matrix.rust }}` or `rustup update ${{ matrix.rust }}`. 44 | # However, when using either we run into a linking issue: 45 | # `error: could not create link from 'C:\\Users\\runneradmin\\.cargo\\bin\\rustup.exe' to 'C:\\Users\\runneradmin\\.cargo\\bin\\cargo.exe'\n" })', tests\find_msrv.rs:156:39` 46 | # I find the message odd, because we only need to install the toolchain, not set it as the default, and replace the 47 | # binary; so what is meant by "link" in the above error? 48 | - name: install_rust 49 | uses: actions-rs/toolchain@v1 50 | with: 51 | toolchain: ${{ matrix.rust }} 52 | override: true 53 | profile: minimal 54 | target: x86_64-unknown-linux-musl 55 | 56 | - name: fetch 57 | run: cargo fetch --verbose 58 | 59 | - name: build 60 | run: cargo build --verbose 61 | 62 | - name: install musl-gcc 63 | if: matrix.os == 'ubuntu-latest' 64 | uses: awalsh128/cache-apt-pkgs-action@v1 65 | with: 66 | packages: musl-tools # provides musl-gcc 67 | version: 1.0 68 | 69 | - name: build using musl 70 | if: matrix.os == 'ubuntu-latest' 71 | run: cargo build --verbose --target=x86_64-unknown-linux-musl 72 | 73 | - name: test_all 74 | run: cargo test --verbose --all -- --test-threads=1 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | 4 | .idea/ 5 | .vscode/ 6 | *.iml 7 | .fleet 8 | 9 | licenses.html 10 | third-party-licenses.html -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-msrv" 3 | version = "0.18.4" 4 | authors = ["Martijn Gribnau "] 5 | description = "Find your minimum supported Rust version (MSRV)!" 6 | license = "Apache-2.0 OR MIT" 7 | edition = "2021" 8 | repository = "https://github.com/foresterre/cargo-msrv" 9 | 10 | keywords = ["msrv", "rust-version", "toolchain", "find", "minimum"] 11 | categories = ["development-tools", "development-tools::cargo-plugins", "command-line-utilities"] 12 | 13 | build = "build.rs" 14 | exclude = ["/design"] 15 | rust-version = "1.85" 16 | 17 | [package.metadata.release] 18 | tag-name = "v{{version}}" 19 | 20 | [dependencies] 21 | # workspace 22 | msrv = { path = "crates/msrv", version = "0.0.2" } 23 | 24 | # external 25 | bisector = "0.4.0" # bisection with a custom comparator 26 | camino = "1.1" # utf-8 paths 27 | cargo_metadata = "0.19.2" # resolving Cargo manifest metadata (consider `guppy`!) 28 | clap = { version = "4.5.39", features = ["derive"] } # parse CLI arguments 29 | clap-cargo = { version = "0.15.2", features = ["cargo_metadata"] } 30 | dirs = "6.0.0" # common directories 31 | dunce = "1.0.5" # better canonicalize for Windows 32 | indicatif = "0.17.11" # UI 33 | indoc = "2.0.6" 34 | owo-colors = "4.2.1" # color support for the terminal 35 | petgraph = "0.8.1" # graph data structures 36 | rust-releases = { version = "0.30.0", default-features = false, features = ["rust-changelog"] } # get the available rust versions 37 | serde = { version = "1.0", features = ["derive"] } # serialization and deserialization 38 | serde_json = "1.0.140" # JSON serialization and deserialization 39 | storyteller = "1.0.0" # minimal multi user output architecture 40 | tabled = { version = "~0.16.0", features = ["ansi"] } # pretty print tables 41 | terminal_size = "0.4.2" # determine the terminal size 42 | thiserror = "2.0.11" # error handling 43 | toml_edit = "0.22.26" # read and write the Cargo.toml 44 | tracing = "0.1" # tracing 45 | tracing-appender = "0.2" # tracing 46 | tracing-subscriber = { version = "0.3", features = ["json"] } 47 | 48 | [features] 49 | default = ["rust-releases-dist-source"] 50 | rust-releases-dist-source = ["rust-releases/rust-dist"] 51 | 52 | [dev-dependencies] 53 | parameterized = "2.0.0" 54 | yare = "3.0.0" 55 | phenomenon = "~1.0.0" 56 | assert_fs = "1.1.3" 57 | 58 | [build-dependencies] 59 | vergen = { version = "8.3.2", default-features = false, features = ["build", "cargo", "git", "gitcl", "rustc"] } 60 | 61 | [workspace] 62 | members = ["crates/msrv"] 63 | 64 | [profile.release] 65 | lto = true 66 | codegen-units = 1 67 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lukemathwalker/cargo-chef:latest-rust-latest AS chef 2 | WORKDIR app 3 | 4 | FROM chef AS planner 5 | COPY . . 6 | RUN cargo chef prepare --recipe-path recipe.json 7 | 8 | FROM chef AS builder 9 | COPY --from=planner /app/recipe.json recipe.json 10 | RUN cargo chef cook --release --recipe-path recipe.json 11 | COPY . . 12 | RUN cargo build --verbose --locked --release 13 | 14 | FROM rust:slim-bookworm AS runtime 15 | WORKDIR app 16 | COPY --from=builder /app/target/release/cargo-msrv /usr/local/bin 17 | ENTRYPOINT ["cargo-msrv", "msrv"] 18 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Martijn Gribnau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /about.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 36 | 37 | 38 | 39 |
40 |
41 |

Third Party Licenses

42 |

This page lists the licenses of the projects used in cargo-msrv.

43 |
44 | 45 |

Overview of licenses:

46 | 51 | 52 |

All license text:

53 | 67 |
68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /about.toml: -------------------------------------------------------------------------------- 1 | accepted = [ 2 | "0BSD", 3 | "Apache-2.0", 4 | "Apache-2.0 WITH LLVM-exception", 5 | "BSD-3-Clause", 6 | "ISC", 7 | "MIT", 8 | "OpenSSL", 9 | "Unicode-3.0", 10 | "Zlib", 11 | ] 12 | workarounds = ["ring", "rustls"] 13 | 14 | ignore-build-dependencies = true 15 | ignore-dev-dependencies = true 16 | 17 | [cbindgen] 18 | accepted = ["MPL-2.0"] 19 | 20 | [option-ext] 21 | accepted = ["MPL-2.0"] 22 | 23 | [ring] 24 | accepted = ["ISC", "OpenSSL"] 25 | 26 | [webpki] 27 | accepted = ["ISC", "BSD-3-Clause"] 28 | 29 | [webpki.clarify] 30 | license = "ISC AND BSD-3-Clause" 31 | 32 | [[webpki.clarify.files]] 33 | path = "LICENSE" 34 | license = "ISC" 35 | checksum = "5B698CA13897BE3AFDB7174256FA1574F8C6892B8BEA1A66DD6469D3FE27885A" 36 | 37 | [[webpki.clarify.files]] 38 | path = "third-party/chromium/LICENSE" 39 | license = "BSD-3-Clause" 40 | checksum = "845022E0C1DB1ABB41A6BA4CD3C4B674EC290F3359D9D3C78AE558D4C0ED9308" 41 | 42 | [webpki-roots] 43 | accepted = ["MPL-2.0"] 44 | -------------------------------------------------------------------------------- /book/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Martijn Gribnau"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "The cargo-msrv Book 🦀" 7 | 8 | [output.html] 9 | git-repository-url = "https://github.com/foresterre/cargo-msrv" 10 | 11 | [output.html.print] 12 | enable = true 13 | 14 | [output.html.search] 15 | enable = true 16 | limit-results = 30 17 | teaser-word-count = 30 18 | use-boolean-and = true 19 | boost-title = 2 20 | boost-hierarchy = 1 21 | boost-paragraph = 1 22 | expand = true 23 | heading-split-level = 3 24 | copy-js = true -------------------------------------------------------------------------------- /book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](./index.md) 4 | - [Getting Started](./getting-started/index.md) 5 | - [Installation](./getting-started/installation.md) 6 | - [Quick Start](./getting-started/quick-start.md) 7 | - [Cargo Workspace](./getting-started/cargo-workspace.md) 8 | - [HTTP PROXY](./getting-started/rust-releases-proxy.md) 9 | - [Releases](releases/index.md) 10 | - [v0.15 to v0.16](releases/v0.15_v0.16_highlights.md) 11 | - [v0.15 to v0.16 JSON](releases/v0.15_v0.16_json.md) 12 | - [Concepts](./concepts/index.md) 13 | - [Output Formats](output-formats/index.md) 14 | - [human](output-formats/human.md) 15 | - [json](output-formats/json.md) 16 | - [minimal](output-formats/minimal.md) 17 | - [no-user-output](output-formats/no-user-output.md) 18 | - [Commands](./commands/index.md) 19 | - [cargo-msrv find](./commands/find.md) 20 | - [cargo-msrv help](./commands/help.md) 21 | - [cargo-msrv list](./commands/list.md) 22 | - [cargo-msrv set](./commands/set.md) 23 | - [cargo-msrv show](./commands/show.md) 24 | - [cargo-msrv verify](./commands/verify.md) 25 | - [Verification in CI](./ci/index.md) 26 | - [GitLab](./ci/gitlab.md) 27 | -------------------------------------------------------------------------------- /book/src/ci/gitlab.md: -------------------------------------------------------------------------------- 1 | # GitLab CI/CD 2 | 3 | Use this snippet to have a dedicated [job](https://docs.gitlab.com/ee/ci/jobs/) 4 | in the [test stage of your pipeline](https://docs.gitlab.com/ee/ci/pipelines/): 5 | 6 | ```yml 7 | msrv: 8 | stage: test 9 | image: 10 | name: foresterre/cargo-msrv:latest 11 | entrypoint: [""] 12 | before_script: 13 | - rustc --version 14 | - cargo --version 15 | - cargo msrv --version 16 | script: 17 | - cargo msrv --output-format minimal verify 18 | ``` 19 | 20 | **Note:** The empty `entrypoint` is necessary because the image has 21 | `cargo-msrv` as its entrypoint. Since we want to run other commands, like 22 | `cargo --version`, GitLab requires either an empty entrypoint or a shell. 23 | -------------------------------------------------------------------------------- /book/src/ci/index.md: -------------------------------------------------------------------------------- 1 | # Verification in CI 2 | 3 | You can run `cargo msrv verify` in continuous integration services to check the 4 | MSRV with every contribution. 5 | -------------------------------------------------------------------------------- /book/src/commands/help.md: -------------------------------------------------------------------------------- 1 | # cargo-msrv help 2 | 3 | # COMMAND 4 | 5 | * Standalone: `cargo-msrv help [subcommand]` 6 | * Through Cargo: `cargo msrv help [subcommand]` 7 | 8 | # PREVIEW 9 | 10 | [![asciicast](https://asciinema.org/a/lBKWtUonNo4lP9f5ecQHh36ss.svg)](https://asciinema.org/a/lBKWtUonNo4lP9f5ecQHh36ss) 11 | 12 | # DESCRIPTION 13 | 14 | Help users navigate the cargo-msrv CLI by printing a help message. 15 | 16 | 17 | 18 | # EXAMPLES 19 | 20 | 1. Get help for cargo-msrv 21 | 22 | ```shell 23 | cargo msrv help # OR cargo msrv --help 24 | ``` 25 | 26 | 2. Get help for a cargo-msrv subcommand, for example `list`: 27 | 28 | ```shell 29 | cargo msrv help list # OR cargo msrv list --help 30 | ``` -------------------------------------------------------------------------------- /book/src/commands/index.md: -------------------------------------------------------------------------------- 1 | 🚧 Section is work-in-progress. 2 | 3 | # 🕹️ cargo-msrv commands 4 | 5 | * [cargo-msrv find](./find.md): The `find` subcommand is used to find the MSRV for your crate. 6 | * [cargo-msrv help](./help.md): The `help` subcommand is used to learn more about the usage and the knobs and handles of 7 | the application. 8 | * [cargo-msrv list](./list.md): The `list` subcommand is used to list the known MSRV's of the dependencies of your 9 | crate. 10 | * [cargo-msrv set](./set.md): The `set` subcommand is used to quickly set the MSRV of a crate. 11 | * [cargo-msrv show](./show.md): The `show` subcommand is used to quickly show the MSRV of a crate. 12 | * [cargo-msrv verify](./verify.md): The `verify` subcommand is used to check whether the pinned MSRV is acceptable. 13 | 14 | # Program wide options 15 | 16 | See `cargo msrv --help` for a full list of program wide options. 17 | -------------------------------------------------------------------------------- /book/src/commands/set.md: -------------------------------------------------------------------------------- 1 | # cargo-msrv set 2 | 3 | # COMMAND 4 | 5 | * Standalone: `cargo-msrv set` 6 | * Through Cargo: `cargo msrv set` 7 | 8 | # PREVIEW 9 | 10 | [![asciicast](https://asciinema.org/a/679858.svg)](https://asciinema.org/a/679858) 11 | 12 | # DESCRIPTION 13 | 14 | Set the MSRV in the Cargo manifest. 15 | 16 | This is either the `package.rust-version` field or the `package.metadata.msrv` field in the Cargo manifest ( 17 | `Cargo.toml`). 18 | 19 | 20 | 21 | # EXAMPLES 22 | 23 | 1. Set an MSRV by providing a two component Rust version 24 | 25 | ```shell 26 | cargo msrv set 1.56 27 | ``` 28 | 29 | 2. Set an MSRV by providing a three component Rust version 30 | 31 | ```shell 32 | cargo msrv set 1.58.1 33 | ``` 34 | -------------------------------------------------------------------------------- /book/src/commands/show.md: -------------------------------------------------------------------------------- 1 | # cargo-msrv show 2 | 3 | # COMMAND 4 | 5 | * Standalone: `cargo-msrv show` 6 | * Through Cargo: `cargo msrv show` 7 | 8 | # PREVIEW 9 | 10 | [![asciicast](https://asciinema.org/a/679864.svg)](https://asciinema.org/a/679864) 11 | 12 | # DESCRIPTION 13 | 14 | Print the crate author specified MSRV. 15 | 16 | This is either the `package.rust-version` field or the `package.metadata.msrv` field in the Cargo manifest ( 17 | `Cargo.toml`). 18 | 19 | 20 | 21 | # EXAMPLES 22 | 23 | 1. Show the MSRV specified by a crate author 24 | 25 | ```shell 26 | cargo msrv show 27 | ``` 28 | -------------------------------------------------------------------------------- /book/src/commands/verify.md: -------------------------------------------------------------------------------- 1 | # cargo-msrv verify 2 | 3 | # COMMAND 4 | 5 | * Standalone: `cargo-msrv verify` 6 | * Through Cargo: `cargo msrv verify` 7 | 8 | # PREVIEW 9 | 10 | [![asciicast](https://asciinema.org/a/679863.svg)](https://asciinema.org/a/679863) 11 | 12 | # DESCRIPTION 13 | 14 | Verify whether the MSRV can be satisfied. 15 | 16 | The MSRV can be specified in the Cargo manifest (`Cargo.toml`) using either the `package.rust-version` (Rust >=1.56, 17 | recommended), 18 | or the `package.metadata.msrv` field. 19 | 20 | If the check fails, the program returns with a non-zero exit code. 21 | 22 | # OPTIONS 23 | 24 | **`--rust-version` version** 25 | 26 | Specify the Rust version of a Rust toolchain, against which the crate will be checked for compatibility. 27 | 28 | # EXAMPLES 29 | 30 | 1. Verify whether the MSRV specified in the Cargo manifest is satisfiable (Good case). 31 | 32 | Given a minimal rust crate with the following Cargo.toml manifest: 33 | 34 | ```toml 35 | [package] 36 | name = "example" 37 | version = "0.1.0" 38 | edition = "2021" 39 | rust-version = "1.56.0" 40 | ``` 41 | 42 | and this minimal lib.rs file: 43 | 44 | ```rust 45 | fn main() { 46 | println!("Hello world"); 47 | } 48 | ``` 49 | 50 | We check whether the MSRV's check command, in this case the default `cargo check`, can be satisfied. 51 | The crate author specified the MSRV in the Cargo.toml, using the `package.rust-version` key. 52 | Since the example crate used no features requiring a more recent version than Rust 1.56, the check will be satisfied, 53 | and the program returns a with exit code 0 (success). 54 | 55 | ```shell 56 | cargo msrv verify # Will succeed, and return with exit code 0 57 | ``` 58 | 59 | 2. Verify whether the MSRV specified in the Cargo manifest is satisfiable (Bad case). 60 | 61 | Given a minimal rust crate with the following Cargo.toml manifest: 62 | 63 | ```toml 64 | [package] 65 | name = "example" 66 | version = "0.1.0" 67 | edition = "2021" 68 | rust-version = "1.56.0" 69 | ``` 70 | 71 | and this minimal lib.rs file: 72 | 73 | ```rust 74 | fn main() { 75 | let cmd = Command::new("ls"); 76 | assert_eq!(cmd.get_program(), "ls"); // will fail because Command::get_program was introduced in 1.57, which is greater than 1.56 (the MSRV) 77 | } 78 | ``` 79 | 80 | We check whether the MSRV's check command, in this case the default `cargo check`, can be satisfied. 81 | The crate author specified the MSRV in the Cargo.toml, using the `package.rust-version` key. 82 | Since the example crate used a feature requiring a more recent version than Rust 1.56, the check cannot be satisfied, 83 | and the program returns a with a non-zero exit code (failure). 84 | 85 | ```shell 86 | cargo msrv verify # Will fail, and return a non-zero exit code 87 | ``` 88 | 89 | 3. Run the 'verify' subcommand on a crate not in our current working directory. 90 | 91 | ```shell 92 | cargo msrv --path path/to/my/crate verify 93 | ``` 94 | 95 | This example shows how to use arguments (in this case `--path`) shared between the default cargo-msrv command and 96 | verify. 97 | Note that shared arguments must be specified before the subcommand (here `verify`). 98 | 99 | 4. Run the 'verify' subcommand using a self-determined Rust version. 100 | 101 | ```shell 102 | cargo msrv verify --rust-version 1.56 103 | ``` 104 | 105 | -------------------------------------------------------------------------------- /book/src/concepts/index.md: -------------------------------------------------------------------------------- 1 | 🚧 Section is work-in-progress. 2 | 3 | # 🌱 Concepts 4 | 5 | ## Rust Releases Index 6 | 7 | ### Release Source 8 | 9 | * rust-changelog (default) 10 | * rust-dist 11 | 12 | ## Resolver 13 | 14 | * run-toolchain resolver (default): resolver which runs actual toolchains against a crate 15 | * rust-version resolver: author defined resolver, used by `cargo-msrv list` 16 | -------------------------------------------------------------------------------- /book/src/getting-started/cargo-workspace.md: -------------------------------------------------------------------------------- 1 | ### Cargo Workspace 2 | 3 | When developing a Rust project with cargo, you may use a cargo [workspace](https://doc.rust-lang.org/cargo/reference/workspaces.html) 4 | to manage a set of related packages together. 5 | 6 | `cargo-msrv` currently partially supports cargo workspaces although full support is on the way. 7 | 8 | #### Finding the MSRV of a workspace member 9 | 10 | To find the MSRV of a workspace crate, you can run: 11 | 12 | ```shell 13 | cargo msrv find -- cargo check -p $crate_name 14 | ``` 15 | 16 | To verify the MSRV of a workspace, you can run: 17 | 18 | ```shell 19 | cargo msrv verify -- cargo check -p $crate_name 20 | ``` 21 | 22 | #### Workspace support in cargo-msrv 23 | 24 | `cargo-msrv` should support the follow for a cargo workspace: 25 | 26 | - Run `cargo msrv find` on a workspace, and find the MSRV of all, or the selected workspace packages 27 | - Run `cargo msrv find --write-msrv` to write the found MSRV's of the selected workspace packages 28 | - Run `cargo msrv verify` on a workspace, and verify the MSRV of all, or the selected workspace packages 29 | - Run `cargo msrv set --package ` to set the MSRV of a specific package in the workspace 30 | - Run `cargo msrv show` on a workspace, and present the MSRV of all, or the selected workspace packages, to the user 31 | - Add `cargo msrv --workspace`, `cargo msrv --package `, `cargo msrv --exclude ` flags to select workspace packages 32 | - User selection of workspace packages was added in [#1025](https://github.com/foresterre/cargo-msrv/pull/1025/files) 33 | - JSON reporting of the selected workspace was added in [#1030](https://github.com/foresterre/cargo-msrv/pull/1030/files) 34 | - `cargo msrv find`, `cargo msrv verify` and others should support `workspace.package` [inheritance](https://doc.rust-lang.org/cargo/reference/workspaces.html#the-package-table), for example for: 35 | - the `rust-version` field, used by `cargo msrv verify` to detect the MSRV to verify 36 | - the `edition` field, used by `cargo msrv find` to restrict the search space 37 | - the `include` and `exclude` fields to define the workspace members 38 | 39 | The following features are under consideration: 40 | - Run `cargo msrv set --workspace ` on a workspace to set a common MSRV 41 | - Run `cargo msrv set --workspace-package ` to set the MSRV to the workspace.package table, if in a workspace 42 | - TODO: determine the name of the flag 43 | - Run `cargo msrv list` on a workspace to list the MSRV of dependencies of each of the workspace crates. 44 | 45 | Please open an [issue](https://github.com/foresterre/cargo-msrv/issues) if your use case is not described in the above list. 46 | 47 | #### Follow progress on GitHub 48 | 49 | Tracking issue: [#1026](https://github.com/foresterre/cargo-msrv/issues/1026) 50 | 51 | **cargo msrv find & cargo msrv verify** 52 | 53 | - [Add --workspace flag to subcommand find #873](https://github.com/foresterre/cargo-msrv/issues/873) 54 | 55 | **cargo msrv list** 56 | 57 | - No dedicated issue yet 58 | 59 | **cargo msrv set** 60 | 61 | - No dedicated issue yet 62 | 63 | **cargo msrv show** 64 | 65 | - [cargo msrv show should show all workspace crate MSRV's #1024](https://github.com/foresterre/cargo-msrv/issues/1024) 66 | -------------------------------------------------------------------------------- /book/src/getting-started/index.md: -------------------------------------------------------------------------------- 1 | # Getting started with cargo-msrv 2 | 3 | * [Installation](installation.md) 4 | * [Quick Start](quick-start.md) 5 | * [Cargo Workspace](cargo-workspace.md) 6 | * [HTTP PROXY](rust-releases-proxy.md) -------------------------------------------------------------------------------- /book/src/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | ## 🌞 Installation 2 | 3 | Packages marked with 🔸 are maintained by community members (i.e. not the cargo-msrv authors). A big thank you to them! 4 | 5 | ### Using [Cargo](https://doc.rust-lang.org/cargo/commands/cargo-install.html): 6 | 7 | You can install cargo-msrv from source by using Cargo, the Rust package manager and build tool ([package](https://crates.io/crates/cargo-msrv)). 8 | 9 | **How to install the latest stable release?** 10 | 11 | ```shell 12 | cargo install cargo-msrv 13 | ``` 14 | 15 | **How to install the latest stable release more quickly?** 16 | 17 | Similar to the above, but allows for only the default channel to obtain a list of rustc releases. 18 | This compiles about 40% faster and produces binaries about half the size in the range of 4.5MB. 19 | 20 | ```shell 21 | cargo install cargo-msrv --no-default-features 22 | ``` 23 | 24 | **How to install the latest development release?** 25 | 26 | You may install _cargo-msrv_ from GitHub: 27 | 28 | ```shell 29 | cargo install cargo-msrv --git https://github.com/foresterre/cargo-msrv.git --branch main 30 | ``` 31 | 32 | ### Arch Linux 🔸 33 | 34 | cargo-msrv is available from the Arch Linux [extra repository](https://archlinux.org/packages/extra/x86_64/cargo-msrv/). 35 | 36 | **How to install?** 37 | 38 | ```shell 39 | pacman -S cargo-msrv 40 | ``` 41 | 42 | ### Nix 🔸 43 | 44 | cargo-msrv is available from the Nix package manager and in NixOS ([package](https://search.nixos.org/packages?channel=21.05&show=cargo-msrv&from=0&size=50&sort=relevance&type=packages&query=cargo-msrv)): 45 | 46 | **How to install (nixpkgs)?** 47 | 48 | ```shell 49 | nix-env -iA nixpkgs.cargo-msrv 50 | ``` 51 | 52 | **How to install (NixOS)?** 53 | 54 | ```shell 55 | nix-env -iA nixos.cargo-msrv 56 | ``` 57 | 58 | NB: When installing with `nix-shell --pure`, ensure that `rustup` is available in the environment. 59 | 60 | ### Other options 61 | 62 | You may also build the program from source by cloning the [repository](https://github.com/foresterre/cargo-msrv) 63 | and building a release from there. 64 | 65 | **How to build a release?** 66 | 67 | ```shell 68 | git clone git@github.com:foresterre/cargo-msrv.git 69 | git checkout v0.16.0 # NB: Find the latest release tag here: https://github.com/foresterre/cargo-msrv/tags 70 | cd cargo-msrv 71 | cargo install cargo-msrv --path . # OR cargo build --release && mv ./target/cargo-msrv ./my/install/directory 72 | ``` 73 | 74 | **How to build the latest development version from source?** 75 | 76 | ```shell 77 | git clone git@github.com:foresterre/cargo-msrv.git 78 | cd cargo-msrv 79 | cargo install cargo-msrv --path . # OR cargo build --release && mv ./target/cargo-msrv ./my/install/directory 80 | ``` 81 | 82 | 83 | You may find additional installation options in the [README](https://github.com/foresterre/cargo-msrv#install). 84 | -------------------------------------------------------------------------------- /book/src/getting-started/quick-start.md: -------------------------------------------------------------------------------- 1 | ### ⏱️ Quick start 2 | 3 | If all you want to do is find the MSRV for your package, you can run: 4 | 5 | ```shell 6 | cargo msrv find 7 | ``` 8 | 9 | This command will attempt to determine the MSRV by doing a binary search on 10 | acceptable Rust releases. If you require additional options, please refer to the 11 | [`cargo-msrv commands`] section, or run `cargo msrv help` to view the program's help 12 | output. 13 | 14 | [`cargo-msrv commands`]: ../commands/index.md 15 | -------------------------------------------------------------------------------- /book/src/getting-started/rust-releases-proxy.md: -------------------------------------------------------------------------------- 1 | ### Rust Releases: HTTP PROXY 2 | 3 | `cargo-msrv` depends on the [rust-releases](https://github.com/foresterre/rust-releases/) crate to determine which Rust versions exist. This is a necessary evil for 4 | the `cargo msrv find` and `cargo msrv verify` subcommands. 5 | 6 | To fetch an index of known Rust releases, it accesses the network. By default, the Rust GitHub repository is used to determine 7 | which stable releases and toolchains are available. As an alternative, this data can also be fetched from the Rust AWS S3 8 | distribution bucket. 9 | 10 | The source can be set with the `--release-source ` flag. The possible values are respectively `rust-changelog` and `rust-dist`, 11 | for the Rust GitHub repository and the Rust AWS S3 distribution bucket. For example: `cargo msrv find --release-source rust-changelog`. 12 | 13 | 14 | 15 | #### Release source: `rust-changelog` 16 | 17 | [rust-releases](https://github.com/foresterre/rust-releases/) uses [ureq](https://crates.io/crates/ureq) as HTTP client 18 | for the `rust-changelog` source. From `cargo-msrv 0.17.1` (and [rust-releases 0.29.0](https://github.com/foresterre/rust-releases/releases/tag/v0.29.0) 19 | respectively), `ureq` has been configured to support configuring a network proxy from the environment. 20 | 21 | The environment variable, `ureq` uses [are](https://docs.rs/ureq/2.11.0/src/ureq/proxy.rs.html#87-92): 22 | 23 | - `ALL_PROXY` or `all_proxy` or, 24 | - `HTTPS_PROXY` or `https_proxy` or, 25 | - `HTTP_PROXY` or `http_proxy` 26 | 27 | The environment variable can be configured as follows: 28 | 29 | `://:@:port`, where all parts except host are optional. 30 | 31 | The `` must be one of: `http` (`socks4`, `socks4a` and `socks5` are currently not enabled). The default is `http`. 32 | 33 | The default `` is 80 when the `` is `http` . 34 | 35 | Examples: 36 | - `localhost` 37 | - `http://127.0.0.1:8080` 38 | 39 | 40 | #### Release source: `rust-dist` 41 | 42 | TODO: Not configured specifically by cargo-msrv, but could be the case. 43 | 44 | The following crates are used for the `rust-dist` source: 45 | 46 | - [aws-config](https://crates.io/crates/aws-config) 47 | - [aws-sdk-s3](https://crates.io/crates/aws-sdk-s3) 48 | 49 | Probably also relevant as transitive dependencies are: 50 | 51 | - [aws-smithy-http](https://crates.io/crates/aws-smithy-http) 52 | -------------------------------------------------------------------------------- /book/src/index.md: -------------------------------------------------------------------------------- 1 | # ✨ Introduction 2 | 3 | `cargo-msrv` is a program which can help you find, set, show or verify the MSRV for a Rust crate. You can also list the 4 | MSRV's of dependencies. 5 | 6 | MSRV stands for 'Minimum Supported Rust Version', which is exactly what it says on the tin: the earliest 7 | Rust release which a given Rust crate promises to support. Most often support for earlier Rust versions is 8 | limited by newly introduced Rust language features, library functions or Rust editions. 9 | 10 | For example, if you want to use const generics and be generic over integers, bool's or char's, you must use a Rust 11 | compiler which supports the const generics MVP. This feature was introduced 12 | in [Rust 1.51](https://blog.rust-lang.org/2021/03/25/Rust-1.51.0.html#const-generics-mvp). 13 | If you do not have any other code, or configuration, which requires an even newer Rust release, your MSRV would 14 | be '1.51'. 15 | 16 | While the MSRV has been a well-known concept within the Rust community for a long time, it was also introduced to the 17 | Cargo build tool and package manager, as the `rust-version` 18 | in [Cargo 1.56](https://github.com/rust-lang/cargo/blob/master/CHANGELOG.md#cargo-156-2021-10-21), 19 | which is part of the [Rust 1.56](https://blog.rust-lang.org/2021/10/21/Rust-1.56.0.html#cargo-rust-version) release 20 | distribution. 21 | 22 | In the [commands](./commands/index.md) section for more. 23 | 24 | # 🔬 How it works 25 | 26 | Cargo-msrv will test your project by running various Rust toolchains against your project. The order in which the 27 | toolchains will be tested, and the amount of tests ran, depends on the search strategy, the set of available toolchains 28 | and of course the limiting factor of the project which will determine the MSRV. We usually call each test a 29 | cargo-msrv _check_. By default, the check command, the command used to test whether toolchain passes or fails a check, 30 | is `cargo check`. 31 | 32 | There are currently two search strategies: _bisect_ (default) and _linear_. When using the linear strategy, your crate 33 | will be checked against toolchains from most-recent to least-recent. When a check fails, the previous Rust (if any) 34 | version is returned as the MSRV (i.e. the highest toolchain for which a check command passes). The bisect strategy uses 35 | a binary search to find the MSRV. This can be significantly faster, so it's usually advisable to keep it enabled by 36 | default. 37 | 38 | In addition to these two strategies, you can inspect the MSRV's set by the crate authors on which your project depends. 39 | This is achieved by resolving the dependency graph of your crate, and querying each crate for its author specified MSRV. 40 | Resolving the dependency graph is usually much quicker than running a toolchain command against your project, and may 41 | give 42 | you an indication of what your MSRV will be like. You can supply the highest listed version 43 | as the `--min ` option: `cargo msrv --min `. This will reduce the possible search space, and speed 44 | up the search for the MSRV of your crate. 45 | 46 | See [cargo-msrv find](./commands/find.md) and [cargo-msrv list](./commands/list.md) for more. 47 | 48 | # 🥰 Thanks 49 | 50 | Thanks for using cargo-msrv! If you found an issue, or have an issue request, or any other question, feel free to open 51 | an issue at our GitHub [repository](https://github.com/foresterre/cargo-msrv/issues). 52 | 53 | A special thanks goes to everyone who took the time to report an issue, discuss new features and contributed to the 54 | documentation or the code! Thank you! 55 | -------------------------------------------------------------------------------- /book/src/output-formats/human.md: -------------------------------------------------------------------------------- 1 | # Output format: human 2 | 3 | This is the default output format. It can also be specified using the `--output-format human` option. 4 | 5 | The output of the 'human' output format is intended to be interpreted by humans. It uses colour and custom printed 6 | layouts to convey its information to the user. 7 | 8 | In the next section, examples are given for each subcommand and a specific use case. You may run `cargo msrv help` to 9 | review all flags and options available. 10 | 11 | # Output by subcommand 12 | 13 | ## \# cargo msrv (find) 14 | 15 | **I want to find the MSRV of my project** 16 | 17 | Use: `cargo msrv` 18 | 19 | [![Screencast: find the MSRV](https://asciinema.org/a/530521.svg)](https://asciinema.org/a/530521) 20 | 21 | The output shows for each checked toolchain whether it is determined to be compatible or not. 22 | If a toolchain is not compatible, a reason is printed which may help you discover why it is not deemed compatible. 23 | 24 | cargo-msrv will show a summary after the search completes. The summary consists of the search space considered, 25 | the search method used, the compiler target and of course the MSRV. 26 | 27 | It is also possible that no MSRV could be found, for example if the program is not valid Rust code (i.e. would not 28 | compile). 29 | 30 | **I want to find the MSRV and write the result to the Cargo manifest** 31 | 32 | Use the `--write-msrv` flag: `cargo msrv find --write-msrv` 33 | 34 | [![Screencast: find the MSRV and write the result to the Cargo manifest](https://asciinema.org/a/530521.svg)](https://asciinema.org/a/530521) 35 | 36 | The output is the same as for `cargo msrv`, plus an additional message which states that the MSRV has been written to 37 | the Cargo manifest. 38 | 39 | _Support for also writing to the clippy config is tracked in 40 | [issue 529](https://github.com/foresterre/cargo-msrv/issues/529)_. 41 | 42 | **I want to find the MSRV and limit or increase the search space** 43 | 44 | Use the `--min` and/or `--max` options: `cargo msrv find --min --max ` 45 | 46 | [![Sceencast: find the MSRV with a customized search space](https://asciinema.org/a/SEqHCRxI5xe0eizaBbIraHZcV.svg)](https://asciinema.org/a/SEqHCRxI5xe0eizaBbIraHZcV) 47 | 48 | By default, the search space is limited by the edition specified in the Cargo manifest. You may use the above 49 | options to override the limits of the search space. The output will be the same as otherwise running `cargo msrv`. 50 | 51 | In the example we specified the minimal version by specifying a Rust edition. We also could've specified a Rust version 52 | instead, e.g. `1.10` or `1.20.0`. It is not possible for the maximum considered version to specify an edition. 53 | 54 | **I want to find the MSRV, but use a linear search** 55 | 56 | Use the `--linear` flag: `cargo msrv find --linear` 57 | 58 | [![Screencast: find the MSRV using a linear search](https://asciinema.org/a/530645.svg)](https://asciinema.org/a/530645) 59 | 60 | We use the bisection search method to speed up the search for the MSRV considerably, but sometimes a linear search 61 | can be useful, for example if the search space is very small. The output will be the same as otherwise running 62 | `cargo msrv`, except of course for the order in which the search is performed. 63 | 64 | ## \# cargo msrv list 65 | 66 | **I want to list the MSRV's of all dependencies** 67 | 68 | Use: `cargo msrv list` 69 | 70 | [![Screencast: list MSRV's of dependencies](https://asciinema.org/a/530652.svg)](https://asciinema.org/a/530652) 71 | 72 | This example shows how to list the MSRV's of dependencies. The MSRV's are sourced from their Cargo manifests. 73 | 74 | **I want to list the MSRV's of my direct dependencies** 75 | 76 | Use the `--variant` option: `cargo msrv list --variant direct-deps` 77 | 78 | [![Screencast: list MSRV's of direct dependencies](https://asciinema.org/a/AU2Xaq1hrXUYfjLdUvDzZHaCC.svg)](https://asciinema.org/a/AU2Xaq1hrXUYfjLdUvDzZHaCC) 79 | 80 | In this example, we instead list the MSRV's of the dependencies specified in the Cargo manifest. 81 | 82 | ## \# cargo msrv set 83 | 84 | **I want to set or update the MSRV of my project** 85 | 86 | Use: `cargo msrv set ` 87 | 88 | [![asciicast](https://asciinema.org/a/530670.svg)](https://asciinema.org/a/530670) 89 | 90 | ## \# cargo msrv show 91 | 92 | ## \# cargo msrv verify 93 | -------------------------------------------------------------------------------- /book/src/output-formats/index.md: -------------------------------------------------------------------------------- 1 | # Output formats 2 | 3 | In `cargo-msrv` we to status of the program is reported via events. These events are issued at several stages of the 4 | program execution. As a user of `cargo-msrv`, you may choose how these events are formatted into a human-readable 5 | or machine-readable representation. User output may also be disabled altogether. 6 | 7 | The next section gives an overview of the supported representations. Thereafter, you may find an index to 8 | the chapters which detail each representation. 9 | 10 | ## Choosing an output format 11 | 12 | The first output format is the `human` output format. As the name suggests, 13 | its output is meant to be interpreted by humans, and consists of elaborate colouring and 14 | custom styling. 15 | 16 | The second output format is the `json` output format. This is a detailed machine-readable 17 | format and also the format which most closely mimics the events as they are reported internally. 18 | Events are printed in a `json-lines` (`jsonl`) format to *stderr*. 19 | 20 | The third output-format is the `minimal` output format. This format is intended to be used by shell scripts 21 | or programs which do not require detailed output. Its format does not require complex parsing, and only 22 | reports the final results of commands. 23 | 24 | The fourth option is to not print any user output. This is uncommon, but may be used in conjunction with 25 | printing debug (i.e. developer) output only, so the debug output is not overwritten by the user output. 26 | 27 | ## The output formats 28 | 29 | * [human](human.md) (default) 30 | * [json](json.md) 31 | * [minimal](minimal.md) 32 | * [no-user-output](no-user-output.md) -------------------------------------------------------------------------------- /book/src/output-formats/minimal.md: -------------------------------------------------------------------------------- 1 | # Output format: minimal 2 | 3 | The purpose of the `minimal` output format option is to provide just enough output for a machine (or shell script!) 4 | to be understandable, while not requiring elaborate parsers like the `json` output format. It may also be used as 5 | a minimal human-readable format. 6 | 7 | This output format can be summarized by the following two statements: 8 | 9 | * If the command was successful, it prints the commands final result and exits with a zero exit code. Output is printed 10 | to `stdout`. 11 | * If the command was unsuccessful, it prints an error message, and exits with a non-zero exit code. Output is printed 12 | to `stderr`. 13 | 14 | You may also refer to the 🚧 TODO 🚧 section to determine which kind of errors result in a non-zero 15 | exit code, and how different errors are categorised. 16 | 17 | # Output by subcommand 18 | 19 | ## \# cargo msrv (find) 20 | 21 | If the MSRV was found, we report this minimal supported Rust version by writing it to `stdout`. 22 | If it could not be found, we report `none` instead, and write this value to `stderr`. 23 | 24 | ### Example 1 25 | 26 | If the MSRV is `1.60.0`, the output will be just `1.60.0`. 27 | 28 | ```shell 29 | $ cargo msrv find --output-format minimal 30 | # stdout 31 | 1.60.0 32 | ``` 33 | 34 | ### Example 2 35 | 36 | If the MSRV can't be found, for example if your project requires a nightly compiler feature 37 | or has incorrect syntax, the output will be `none`. 38 | 39 | ```shell 40 | $ cargo msrv find --output-format minimal 41 | # stderr 42 | none 43 | ``` 44 | 45 | ## \# cargo msrv list 46 | 47 | The `list` subcommand is not supported by the `minimal` output format, and "unsupported" will be printed. 48 | Support may be added in the future. 49 | 50 | ## \# cargo msrv set 51 | 52 | The `set` subcommand prints the version set as MSRV. 53 | 54 | ### Example 1 55 | 56 | If we set our MSRV to be `1.31`, the output will be `1.31`. 57 | 58 | ```shell 59 | cargo msrv find --output-format minimal set 1.31 60 | # stdout 61 | 1.31 62 | ``` 63 | 64 | ## \# cargo msrv show 65 | 66 | The `show` subcommand prints the detected MSRV, if specified in the Cargo Manifest. 67 | 68 | ### Example 1 69 | 70 | Assuming our Cargo manifest contains a `1.60` MSRV, `cargo-msrv` will print `1.60`. 71 | 72 | **Cargo.toml** 73 | 74 | ```toml 75 | [package] 76 | rust-version = "1.60" 77 | ``` 78 | 79 | **Shell** 80 | 81 | ```shell 82 | $ cargo msrv find --output-format minimal show 83 | # stdout 84 | 1.60 85 | ``` 86 | 87 | ### Example 2 88 | 89 | Assuming our Cargo manifest lists the MSRV in the `package.metadata.msrv` field, `cargo-msrv` will print `1.21.0`. 90 | The `package.rust-version` field has precedence over the `package.metadata.msrv`. You may see the 91 | `package.metadata.msrv` 92 | key for crates which use a Cargo version which does not yet support the `package.rust-version` field. `cargo-msrv` 93 | supports both fields. 94 | 95 | **Cargo.toml** 96 | 97 | ```toml 98 | [package.metadata] 99 | msrv = "1.21.0" 100 | ``` 101 | 102 | **Shell** 103 | 104 | ```shell 105 | $ cargo msrv find --output-format minimal show 106 | # stdout 107 | 1.21.0 108 | ``` 109 | 110 | ## \# cargo msrv verify 111 | 112 | The `verify` subcommand prints `true` (to stdout) with exit code zero if checking the toolchain for this platform which 113 | matches the MSRV succeeds. Else, it prints `false` (to stderr) with an exit code which is non-zero. 114 | 115 | ### Example 1 116 | 117 | Assuming our Cargo manifest contains an MSRV definition in the `package.rust-version` or `package.metadata.msrv` field, 118 | and the compatibility check succeeds: 119 | 120 | ```toml 121 | [package.metadata] 122 | msrv = "1.31" 123 | ``` 124 | 125 | **Shell** 126 | 127 | ```shell 128 | $ cargo msrv find --output-format minimal verify 129 | # stdout 130 | true 131 | ``` 132 | 133 | ### Example 2 134 | 135 | Assuming the given crate is incompatibility with the given MSRV: 136 | 137 | **Shell** 138 | 139 | ```shell 140 | $ cargo msrv find --output-format minimal verify --rust-version 1.31 141 | # stderr 142 | false 143 | ``` 144 | -------------------------------------------------------------------------------- /book/src/output-formats/no-user-output.md: -------------------------------------------------------------------------------- 1 | # Output format: no-user-output 2 | 3 | The 'no-user-output' is not really a "user output" variant. Choosing this option disables user output of events 4 | altogether. Disabling the user output may be achieved by providing the `--no-user-output` flag. -------------------------------------------------------------------------------- /book/src/releases/index.md: -------------------------------------------------------------------------------- 1 | # Migration Guide 2 | 3 | - **[v0.15 to v0.16](./v0.15_v0.16_highlights.md): Release highlights** 4 | - **[v0.15 to v0.16](./v0.15_v0.16_json.md): The new JSON output format** -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use vergen::EmitBuilder; 2 | 3 | fn main() { 4 | // generate build info 5 | if let Err(e) = EmitBuilder::builder() 6 | .cargo_target_triple() 7 | .cargo_features() 8 | .git_sha(true) 9 | .rustc_semver() 10 | .emit() 11 | { 12 | eprintln!("Unable to set build metadata: '{}'", e); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | patch: 7 | default: 8 | informational: true -------------------------------------------------------------------------------- /crates/msrv/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "msrv" 3 | version = "0.0.2" 4 | authors = ["Martijn Gribnau "] 5 | description = "Find your minimum supported Rust version (MSRV), library edition! See cargo-msrv!" 6 | license = "Apache-2.0 OR MIT" 7 | edition = "2021" 8 | repository = "https://github.com/foresterre/cargo-msrv" 9 | keywords = ["msrv", "rust-version", "cargo-msrv", "find", "minimum"] 10 | rust-version = "1.82" 11 | 12 | [dependencies] 13 | rust-toolchain = "1.1.0" 14 | version-number = "0.4.0" 15 | -------------------------------------------------------------------------------- /crates/msrv/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod msrv; 2 | 3 | pub use msrv::MSRV; 4 | -------------------------------------------------------------------------------- /crates/msrv/src/msrv.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | /// A type to represent the Minimal Supported Rust Version, also known as the 4 | /// MSRV. 5 | #[derive(Clone, Debug, Eq, PartialEq)] 6 | pub struct MSRV { 7 | /// A toolchain is compatible, if the outcome of a toolchain check results in a success 8 | version: rust_toolchain::RustVersion, 9 | } 10 | 11 | impl MSRV { 12 | pub fn new(msrv: rust_toolchain::RustVersion) -> Self { 13 | Self { version: msrv } 14 | } 15 | 16 | pub fn msrv(&self) -> rust_toolchain::RustVersion { 17 | self.version 18 | } 19 | 20 | pub fn version(&self) -> impl fmt::Display { 21 | self.version 22 | } 23 | 24 | pub fn short_version(&self) -> impl fmt::Display { 25 | version_number::BaseVersion::new(self.version.major(), self.version.minor()) 26 | } 27 | } 28 | 29 | impl fmt::Display for MSRV { 30 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 31 | f.write_fmt(format_args!("{}", &self.version)) 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use crate::msrv::MSRV; 38 | 39 | #[test] 40 | fn msrv_method() { 41 | let version = rust_toolchain::RustVersion::new(1, 2, 3); 42 | let msrv = MSRV::new(version); 43 | 44 | assert_eq!(msrv.msrv(), version); 45 | } 46 | 47 | #[test] 48 | fn version_str() { 49 | let version = rust_toolchain::RustVersion::new(1, 2, 3); 50 | let msrv = MSRV::new(version); 51 | 52 | assert_eq!(msrv.version().to_string(), "1.2.3".to_string()); 53 | } 54 | 55 | #[test] 56 | fn short_version_str() { 57 | let version = rust_toolchain::RustVersion::new(1, 2, 3); 58 | let msrv = MSRV::new(version); 59 | 60 | assert_eq!(msrv.short_version().to_string(), "1.2".to_string()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [advisories] 2 | ignore = [ 3 | "RUSTSEC-2024-0370", # https://rustsec.org/advisories/RUSTSEC-2024-0370 `proc-macro-error` is unmaintained (via `tabled` > `tabled_derive`) 4 | ] 5 | 6 | [licenses] 7 | confidence-threshold = 0.925 8 | allow = [ 9 | "0BSD", 10 | "Apache-2.0", 11 | "Apache-2.0 WITH LLVM-exception", 12 | "BSD-3-Clause", 13 | "ISC", 14 | "MIT", 15 | "OpenSSL", 16 | "Unicode-3.0", 17 | "Zlib", 18 | ] 19 | exceptions = [ 20 | { allow = ["MPL-2.0"], name = "webpki-roots", version = "*" }, 21 | { allow = ["MPL-2.0"], name = "option-ext", version = "*" }, 22 | { allow = ["MPL-2.0"], name = "cbindgen", version = "*" }, 23 | ] 24 | 25 | [[licenses.clarify]] 26 | name = "ring" 27 | expression = "MIT AND ISC AND OpenSSL" 28 | license-files = [ 29 | { path = "LICENSE", hash = 0xbd0eed23 } 30 | ] 31 | 32 | [[licenses.clarify]] 33 | name = "webpki" 34 | expression = "ISC" 35 | license-files = [ 36 | { path = "LICENSE", hash = 0x001c7e6c } 37 | ] 38 | 39 | [[licenses.clarify]] 40 | name = "rustls-webpki" 41 | expression = "ISC" 42 | license-files = [ 43 | { path = "LICENSE", hash = 0x001c7e6c } 44 | ] 45 | -------------------------------------------------------------------------------- /src/cli/custom_check_opts.rs: -------------------------------------------------------------------------------- 1 | use clap::Args; 2 | 3 | #[derive(Debug, Args)] 4 | #[command(next_help_heading = "Custom check options")] 5 | pub struct CustomCheckOpts { 6 | /// Forwards the provided features to cargo, when running cargo-msrv with the default compatibility 7 | /// check command. 8 | /// 9 | /// If a custom compatibility check command is used, this option is ignored. 10 | #[arg(long, value_delimiter = ' ')] 11 | pub features: Option>, 12 | 13 | /// Forwards the --all-features flag to cargo, when running cargo-msrv with the default compatibility 14 | /// check command. 15 | /// 16 | /// If a custom compatibility check command is used, this option is ignored. 17 | #[arg(long)] 18 | pub all_features: bool, 19 | 20 | /// Forwards the --no-default-features flag to cargo, when running cargo-msrv with the default compatibility 21 | /// check command. 22 | /// 23 | /// If a custom compatibility check command is used, this option is ignored. 24 | #[arg(long)] 25 | pub no_default_features: bool, 26 | 27 | /// Supply a custom command to be used by cargo msrv. 28 | /// Example: `cargo check --ignore-rust-version` to ignore the `rust-version` field of crates. 29 | /// Note that `--ignore-rust-version` is only available on Rust >= 1.56 30 | #[arg(last = true)] 31 | pub custom_check_opts: Option>, 32 | } 33 | -------------------------------------------------------------------------------- /src/cli/rust_releases_opts.rs: -------------------------------------------------------------------------------- 1 | use crate::manifest::bare_version; 2 | use crate::manifest::bare_version::BareVersion; 3 | use crate::ReleaseSource; 4 | use clap::Args; 5 | use std::str::FromStr; 6 | 7 | #[derive(Debug, Args)] 8 | #[command(next_help_heading = "Rust releases options")] 9 | pub struct RustReleasesOpts { 10 | /// Least recent version or edition to take into account 11 | /// 12 | /// Given version must match a valid Rust toolchain, and be semver compatible, 13 | /// be a two component `major.minor` version. or match a Rust edition alias. 14 | /// 15 | /// For example, the edition alias "2018" would match Rust version `1.31.0`, since that's the 16 | /// first version which added support for the Rust 2018 edition. 17 | #[arg(long, value_name = "VERSION_SPEC or EDITION", alias = "minimum")] 18 | pub min: Option, 19 | 20 | /// Most recent version to take into account 21 | /// 22 | /// Given version must match a valid Rust toolchain, and be semver compatible, or 23 | /// be a two component `major.minor` version. 24 | #[arg(long, value_name = "VERSION_SPEC", alias = "maximum")] 25 | pub max: Option, 26 | 27 | /// Include all patch releases, instead of only the last 28 | #[arg(long)] 29 | pub include_all_patch_releases: bool, 30 | 31 | #[arg(long, value_enum, default_value_t, value_name = "SOURCE")] 32 | pub release_source: ReleaseSource, 33 | } 34 | 35 | #[derive(Clone, Debug)] 36 | pub enum EditionOrVersion { 37 | Edition(Edition), 38 | Version(BareVersion), 39 | } 40 | 41 | impl EditionOrVersion { 42 | pub fn as_bare_version(&self) -> BareVersion { 43 | match self { 44 | Self::Edition(edition) => edition.as_bare_version(), 45 | Self::Version(version) => version.clone(), 46 | } 47 | } 48 | } 49 | 50 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 51 | pub enum Edition { 52 | Edition2015, 53 | Edition2018, 54 | Edition2021, 55 | Edition2024, 56 | } 57 | 58 | impl FromStr for Edition { 59 | type Err = ParseEditionError; 60 | 61 | fn from_str(input: &str) -> Result { 62 | match input { 63 | "2015" => Ok(Self::Edition2015), 64 | "2018" => Ok(Self::Edition2018), 65 | "2021" => Ok(Self::Edition2021), 66 | "2024" => Ok(Self::Edition2024), 67 | unknown => Err(ParseEditionError::UnknownEdition(unknown.to_string())), 68 | } 69 | } 70 | } 71 | 72 | impl Edition { 73 | pub fn as_bare_version(&self) -> BareVersion { 74 | match self { 75 | Self::Edition2015 => BareVersion::ThreeComponents(1, 0, 0), 76 | Self::Edition2018 => BareVersion::ThreeComponents(1, 31, 0), 77 | Self::Edition2021 => BareVersion::ThreeComponents(1, 56, 0), 78 | // Actual stable version is pending; planning: https://doc.rust-lang.org/nightly/edition-guide/rust-2024/index.html 79 | Self::Edition2024 => BareVersion::ThreeComponents(1, 85, 0), 80 | } 81 | } 82 | } 83 | 84 | #[derive(Debug, thiserror::Error)] 85 | pub enum ParseEditionError { 86 | #[error("Edition '{0}' is not supported")] 87 | UnknownEdition(String), 88 | } 89 | 90 | impl FromStr for EditionOrVersion { 91 | type Err = ParseEditionOrVersionError; 92 | 93 | fn from_str(input: &str) -> Result { 94 | input 95 | .parse::() 96 | .map(EditionOrVersion::Edition) 97 | .or_else(|edition_err| { 98 | BareVersion::from_str(input) 99 | .map(EditionOrVersion::Version) 100 | .map_err(|parse_version_err| { 101 | ParseEditionOrVersionError::EditionOrVersion( 102 | input.to_string(), 103 | edition_err, 104 | parse_version_err, 105 | ) 106 | }) 107 | }) 108 | } 109 | } 110 | 111 | #[derive(Debug, thiserror::Error)] 112 | pub enum ParseEditionOrVersionError { 113 | #[error("Value '{0}' could not be parsed as a valid Rust version: {1} + {2}")] 114 | EditionOrVersion(String, ParseEditionError, bare_version::Error), 115 | } 116 | -------------------------------------------------------------------------------- /src/cli/shared_opts.rs: -------------------------------------------------------------------------------- 1 | use crate::context::{OutputFormat, TracingTargetOption}; 2 | use crate::log_level::LogLevel; 3 | use clap::{ArgGroup, Args, ValueHint}; 4 | use std::path::PathBuf; 5 | 6 | // Cli Options shared between subcommands 7 | #[derive(Debug, Args)] 8 | #[command(group(ArgGroup::new("paths").args(&["path", "manifest_path"])))] 9 | pub struct SharedOpts { 10 | /// Path to project root directory 11 | /// 12 | /// This should be used over `--manifest-path` if not in a Cargo project. 13 | /// If you have a Cargo project, prefer `--manifest-path`. 14 | #[arg(long, value_name = "Crate Directory", global = true, value_hint = ValueHint::DirPath)] 15 | pub path: Option, 16 | 17 | /// Path to cargo manifest file 18 | #[arg(long, value_name = "Cargo Manifest", global = true, value_hint = ValueHint::FilePath)] 19 | pub manifest_path: Option, 20 | 21 | #[command(flatten)] 22 | pub workspace: clap_cargo::Workspace, 23 | 24 | #[command(flatten)] 25 | pub user_output_opts: UserOutputOpts, 26 | 27 | #[command(flatten)] 28 | pub debug_output_opts: DebugOutputOpts, 29 | } 30 | 31 | #[derive(Debug, Args)] 32 | #[command(next_help_heading = "User output options")] 33 | pub struct UserOutputOpts { 34 | /// Set the format of user output 35 | #[arg( 36 | long, 37 | value_enum, 38 | default_value_t, 39 | value_name = "FORMAT", 40 | global = true 41 | )] 42 | output_format: OutputFormat, 43 | 44 | /// Disable user output 45 | #[arg(long, global = true, conflicts_with = "output_format")] 46 | no_user_output: bool, 47 | } 48 | 49 | impl UserOutputOpts { 50 | pub fn effective_output_format(&self) -> OutputFormat { 51 | if self.no_user_output { 52 | OutputFormat::None 53 | } else { 54 | self.output_format 55 | } 56 | } 57 | } 58 | 59 | #[derive(Debug, Args)] 60 | #[command(next_help_heading = "Debug output options")] 61 | pub struct DebugOutputOpts { 62 | /// Disable logging 63 | #[arg(long, global = true)] 64 | pub no_log: bool, 65 | 66 | /// Specify where the program should output its logs 67 | #[arg( 68 | long, 69 | value_enum, 70 | default_value_t, 71 | value_name = "LOG TARGET", 72 | global = true 73 | )] 74 | pub log_target: TracingTargetOption, 75 | 76 | /// Specify the severity of logs which should be 77 | #[arg(long, value_enum, default_value_t, value_name = "LEVEL", global = true)] 78 | pub log_level: LogLevel, 79 | } 80 | -------------------------------------------------------------------------------- /src/cli/toolchain_opts.rs: -------------------------------------------------------------------------------- 1 | use clap::Args; 2 | 3 | // Cli Options for commands which invoke Rust toolchains, such as the top level cargo msrv command 4 | // (find) or cargo msrv verify 5 | #[derive(Debug, Args)] 6 | #[command(next_help_heading = "Toolchain options")] 7 | pub struct ToolchainOpts { 8 | /// Check against a custom target (instead of the rustup default) 9 | // Unfortunately, Clap will not reject the 10 | #[arg(long, value_name = "TARGET", global = true)] 11 | pub target: Option, 12 | 13 | /// Components be added to the toolchain 14 | /// 15 | /// Can be supplied multiple times to add multiple components. 16 | /// 17 | /// For example: --component rustc --component cargo 18 | #[arg(long, value_name = "COMPONENT", global = true)] 19 | pub component: Vec, 20 | } 21 | -------------------------------------------------------------------------------- /src/compatibility/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::rust::Toolchain; 2 | 3 | mod rustup_toolchain_check; 4 | #[cfg(test)] 5 | mod testing; 6 | 7 | use crate::{Compatibility, TResult}; 8 | pub use rustup_toolchain_check::{RunCommand, RustupToolchainCheck}; 9 | 10 | #[cfg(test)] 11 | pub use testing::TestRunner; 12 | 13 | /// Implementers of this trait must determine whether a Rust toolchain is _supported_ 14 | /// for a Rust project. This is a step in the process of determining the _minimally 15 | /// supported_ Rust version; the MSRV. 16 | pub trait IsCompatible { 17 | fn before(&self, _toolchain: &Toolchain) -> TResult<()> { 18 | Ok(()) 19 | } 20 | 21 | fn is_compatible(&self, toolchain: &Toolchain) -> TResult; 22 | 23 | fn after(&self, _toolchain: &Toolchain) -> TResult<()> { 24 | Ok(()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/compatibility/testing.rs: -------------------------------------------------------------------------------- 1 | use crate::compatibility::IsCompatible; 2 | use crate::outcome::Compatibility; 3 | use crate::rust::Toolchain; 4 | use crate::semver::Version; 5 | use crate::TResult; 6 | use std::collections::HashSet; 7 | 8 | pub struct TestRunner { 9 | accept_versions: HashSet, 10 | target: &'static str, 11 | } 12 | 13 | impl TestRunner { 14 | pub fn with_ok<'v, T: IntoIterator>(target: &'static str, iter: T) -> Self { 15 | Self { 16 | accept_versions: iter.into_iter().cloned().collect(), 17 | target, 18 | } 19 | } 20 | } 21 | 22 | impl IsCompatible for TestRunner { 23 | fn is_compatible(&self, toolchain: &Toolchain) -> TResult { 24 | let v = toolchain.version(); 25 | 26 | if self.accept_versions.contains(toolchain.version()) { 27 | Ok(Compatibility::new_success(Toolchain::new( 28 | v.clone(), 29 | self.target, 30 | &[], 31 | ))) 32 | } else { 33 | Ok(Compatibility::new_failure( 34 | Toolchain::new(v.clone(), self.target, &[]), 35 | "f".to_string(), 36 | )) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/context/find.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::{CargoMsrvOpts, SubCommand}; 2 | use crate::compatibility::RunCommand; 3 | use crate::context::{ 4 | CheckCommandContext, EnvironmentContext, RustReleasesContext, SearchMethod, ToolchainContext, 5 | }; 6 | use crate::error::CargoMSRVError; 7 | use crate::external_command::cargo_command::CargoCommand; 8 | use std::convert::{TryFrom, TryInto}; 9 | 10 | #[derive(Debug)] 11 | pub struct FindContext { 12 | /// Use a binary (bisect) or linear search to find the MSRV 13 | pub search_method: SearchMethod, 14 | 15 | /// Write the toolchain file if the MSRV is found 16 | pub write_toolchain_file: bool, 17 | 18 | /// Ignore the lockfile for the MSRV search 19 | pub ignore_lockfile: bool, 20 | 21 | /// Don't print the result of compatibility checks 22 | pub no_check_feedback: bool, 23 | 24 | /// Write the MSRV to the Cargo manifest 25 | pub write_msrv: bool, 26 | 27 | /// The context for Rust releases 28 | pub rust_releases: RustReleasesContext, 29 | 30 | /// The context for Rust toolchains 31 | pub toolchain: ToolchainContext, 32 | 33 | /// The context for checks to be used with rustup 34 | pub check_cmd: CheckCommandContext, 35 | 36 | /// Resolved environment options 37 | pub environment: EnvironmentContext, 38 | } 39 | 40 | impl TryFrom for FindContext { 41 | type Error = CargoMSRVError; 42 | 43 | fn try_from(opts: CargoMsrvOpts) -> Result { 44 | let CargoMsrvOpts { 45 | shared_opts, 46 | subcommand, 47 | } = opts; 48 | 49 | let find_opts = match subcommand { 50 | SubCommand::Find(opts) => opts, 51 | _ => unreachable!("This should never happen. The subcommand is not `find`!"), 52 | }; 53 | 54 | let toolchain = find_opts.toolchain_opts.try_into()?; 55 | let environment = (&shared_opts).try_into()?; 56 | 57 | Ok(Self { 58 | search_method: if find_opts.linear { 59 | SearchMethod::Linear 60 | } else { 61 | SearchMethod::Bisect 62 | }, 63 | write_toolchain_file: find_opts.write_toolchain_file, 64 | ignore_lockfile: find_opts.ignore_lockfile, 65 | no_check_feedback: find_opts.no_check_feedback, 66 | write_msrv: find_opts.write_msrv, 67 | rust_releases: find_opts.rust_releases_opts.into(), 68 | toolchain, 69 | check_cmd: find_opts.custom_check_opts.into(), 70 | environment, 71 | }) 72 | } 73 | } 74 | 75 | impl FindContext { 76 | pub fn run_command(&self) -> RunCommand { 77 | if let Some(custom) = &self.check_cmd.rustup_command { 78 | RunCommand::custom(custom.clone()) 79 | } else { 80 | let cargo_command = CargoCommand::default() 81 | .target(Some(self.toolchain.target)) 82 | .features(self.check_cmd.cargo_features.clone()) 83 | .all_features(self.check_cmd.cargo_all_features) 84 | .no_default_features(self.check_cmd.cargo_no_default_features); 85 | 86 | RunCommand::from_cargo_command(cargo_command) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/context/list.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::{CargoMsrvOpts, SubCommand}; 2 | use crate::context::EnvironmentContext; 3 | use crate::error::CargoMSRVError; 4 | use clap::ValueEnum; 5 | use std::convert::{TryFrom, TryInto}; 6 | use std::fmt; 7 | use std::fmt::Formatter; 8 | use std::str::FromStr; 9 | 10 | #[derive(Debug)] 11 | pub struct ListContext { 12 | /// The type of output expected by the user 13 | pub variant: ListMsrvVariant, 14 | 15 | /// Resolved environment options 16 | pub environment: EnvironmentContext, 17 | } 18 | 19 | impl TryFrom for ListContext { 20 | type Error = CargoMSRVError; 21 | 22 | fn try_from(opts: CargoMsrvOpts) -> Result { 23 | let CargoMsrvOpts { 24 | shared_opts, 25 | subcommand, 26 | .. 27 | } = opts; 28 | 29 | let list_opts = match subcommand { 30 | SubCommand::List(opts) => opts, 31 | _ => unreachable!("This should never happen. The subcommand is not `list`!"), 32 | }; 33 | 34 | let environment = (&shared_opts).try_into()?; 35 | 36 | Ok(Self { 37 | variant: list_opts.variant, 38 | environment, 39 | }) 40 | } 41 | } 42 | 43 | #[derive(Copy, Clone, Debug, Default, Eq, PartialEq, ValueEnum)] 44 | pub enum ListMsrvVariant { 45 | DirectDeps, 46 | #[default] 47 | OrderedByMSRV, 48 | } 49 | 50 | pub(crate) const DIRECT_DEPS: &str = "direct-deps"; 51 | pub(crate) const ORDERED_BY_MSRV: &str = "ordered-by-msrv"; 52 | 53 | impl FromStr for ListMsrvVariant { 54 | type Err = CargoMSRVError; 55 | 56 | fn from_str(s: &str) -> Result { 57 | Ok(match s { 58 | DIRECT_DEPS => Self::DirectDeps, 59 | ORDERED_BY_MSRV => Self::OrderedByMSRV, 60 | elsy => { 61 | return Err(crate::CargoMSRVError::InvalidConfig(format!( 62 | "No such list variant '{}'", 63 | elsy 64 | ))) 65 | } 66 | }) 67 | } 68 | } 69 | 70 | impl fmt::Display for ListMsrvVariant { 71 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 72 | match self { 73 | Self::DirectDeps => write!(f, "{}", DIRECT_DEPS), 74 | Self::OrderedByMSRV => write!(f, "{}", ORDERED_BY_MSRV), 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/context/set.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::{CargoMsrvOpts, SubCommand}; 2 | use crate::context::{EnvironmentContext, RustReleasesContext}; 3 | use crate::error::CargoMSRVError; 4 | use crate::manifest::bare_version::BareVersion; 5 | use std::convert::{TryFrom, TryInto}; 6 | 7 | #[derive(Debug)] 8 | pub struct SetContext { 9 | /// MSRV to set. 10 | pub msrv: BareVersion, 11 | 12 | /// The context for Rust releases 13 | pub rust_releases: RustReleasesContext, 14 | 15 | /// Resolved environment options 16 | pub environment: EnvironmentContext, 17 | } 18 | 19 | impl TryFrom for SetContext { 20 | type Error = CargoMSRVError; 21 | 22 | fn try_from(opts: CargoMsrvOpts) -> Result { 23 | let CargoMsrvOpts { 24 | shared_opts, 25 | subcommand, 26 | .. 27 | } = opts; 28 | 29 | let set_opts = match subcommand { 30 | SubCommand::Set(opts) => opts, 31 | _ => unreachable!("This should never happen. The subcommand is not `set`!"), 32 | }; 33 | 34 | let environment = (&shared_opts).try_into()?; 35 | 36 | Ok(Self { 37 | msrv: set_opts.msrv, 38 | rust_releases: set_opts.rust_releases_opts.into(), 39 | environment, 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/context/show.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::CargoMsrvOpts; 2 | use crate::context::EnvironmentContext; 3 | use crate::error::CargoMSRVError; 4 | use std::convert::{TryFrom, TryInto}; 5 | 6 | #[derive(Debug)] 7 | pub struct ShowContext { 8 | /// Resolved environment options 9 | pub environment: EnvironmentContext, 10 | } 11 | 12 | impl TryFrom for ShowContext { 13 | type Error = CargoMSRVError; 14 | 15 | fn try_from(opts: CargoMsrvOpts) -> Result { 16 | let CargoMsrvOpts { shared_opts, .. } = opts; 17 | 18 | Ok(Self { 19 | environment: (&shared_opts).try_into()?, 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/context/verify.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::{CargoMsrvOpts, SubCommand}; 2 | use crate::context::{ 3 | CheckCommandContext, EnvironmentContext, RustReleasesContext, ToolchainContext, 4 | }; 5 | 6 | use crate::compatibility::RunCommand; 7 | use crate::error::CargoMSRVError; 8 | use crate::external_command::cargo_command::CargoCommand; 9 | use crate::sub_command::verify::RustVersion; 10 | use std::convert::{TryFrom, TryInto}; 11 | 12 | #[derive(Debug)] 13 | pub struct VerifyContext { 14 | /// The resolved Rust version, to check against for toolchain compatibility. 15 | pub rust_version: RustVersion, 16 | 17 | /// Ignore the lockfile for the MSRV verification 18 | pub ignore_lockfile: bool, 19 | 20 | /// Don't print the result of compatibility check 21 | pub no_check_feedback: bool, 22 | 23 | /// The context for Rust releases 24 | pub rust_releases: RustReleasesContext, 25 | 26 | /// The context for Rust toolchains 27 | pub toolchain: ToolchainContext, 28 | 29 | /// The context for custom checks to be used with rustup 30 | pub check_cmd: CheckCommandContext, 31 | 32 | /// Resolved environment options 33 | pub environment: EnvironmentContext, 34 | } 35 | 36 | impl TryFrom for VerifyContext { 37 | type Error = CargoMSRVError; 38 | 39 | fn try_from(opts: CargoMsrvOpts) -> Result { 40 | let CargoMsrvOpts { 41 | shared_opts, 42 | subcommand, 43 | } = opts; 44 | 45 | let verify_opts = match subcommand { 46 | SubCommand::Verify(opts) => opts, 47 | _ => unreachable!("This should never happen. The subcommand is not `verify`!"), 48 | }; 49 | 50 | let toolchain = verify_opts.toolchain_opts.try_into()?; 51 | let environment = (&shared_opts).try_into()?; 52 | 53 | let rust_version = match verify_opts.rust_version { 54 | Some(v) => RustVersion::from_arg(v), 55 | None => RustVersion::try_from_environment(&environment)?, 56 | }; 57 | 58 | Ok(Self { 59 | rust_version, 60 | ignore_lockfile: verify_opts.ignore_lockfile, 61 | no_check_feedback: verify_opts.no_check_feedback, 62 | rust_releases: verify_opts.rust_releases_opts.into(), 63 | toolchain, 64 | check_cmd: verify_opts.custom_check_opts.into(), 65 | environment, 66 | }) 67 | } 68 | } 69 | 70 | impl VerifyContext { 71 | pub fn run_command(&self) -> RunCommand { 72 | if let Some(custom) = &self.check_cmd.rustup_command { 73 | RunCommand::custom(custom.clone()) 74 | } else { 75 | let cargo_command = CargoCommand::default() 76 | .target(Some(self.toolchain.target)) 77 | .features(self.check_cmd.cargo_features.clone()) 78 | .all_features(self.check_cmd.cargo_all_features) 79 | .no_default_features(self.check_cmd.cargo_no_default_features); 80 | 81 | RunCommand::from_cargo_command(cargo_command) 82 | } 83 | } 84 | } 85 | 86 | #[cfg(test)] 87 | mod tests { 88 | mod issue_936_target { 89 | use crate::cli::CargoCli; 90 | use crate::context::VerifyContext; 91 | use std::convert::TryFrom; 92 | 93 | #[test] 94 | fn target_at_subcommand_level() { 95 | let opts = CargoCli::parse_args(["cargo", "msrv", "verify", "--target", "x"]); 96 | let context = VerifyContext::try_from(opts.to_cargo_msrv_cli().to_opts()).unwrap(); 97 | 98 | assert_eq!(context.toolchain.target, "x"); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/dependency_graph/mod.rs: -------------------------------------------------------------------------------- 1 | use cargo_metadata::{Package, PackageId}; 2 | use petgraph::visit::Dfs; 3 | use std::collections::HashMap; 4 | 5 | pub(crate) mod resolver; 6 | 7 | type PackageGraphIndex = usize; 8 | // NB: stable graph because we need our DependencyGraph::index to be able to bridge between id's 9 | // even after removals, which we do to remove dev- and build dependencies. 10 | type PackageGraph = 11 | petgraph::stable_graph::StableDiGraph; 12 | 13 | /// A graph of dependencies from a designated root crate 14 | /// 15 | /// Why a graph instead of simply a set of packages? 16 | /// To find the MSRV, all we need is the set, however, to locate where that dependency originates from, 17 | /// it is useful to have a graph. 18 | #[derive(Clone, Debug)] 19 | pub struct DependencyGraph { 20 | // Useful to translate between the packages known to cargo_metadata and our petgraph. 21 | index: HashMap, 22 | // A directed graph of packages 23 | packages: PackageGraph, 24 | // The root crate is the crate we're creating the dependency graph for. 25 | root_crate: PackageId, 26 | } 27 | 28 | impl DependencyGraph { 29 | pub fn empty(root_crate: PackageId) -> Self { 30 | Self { 31 | index: HashMap::default(), 32 | packages: PackageGraph::with_capacity(0, 0), 33 | root_crate, 34 | } 35 | } 36 | 37 | pub fn with_capacity(root_crate: PackageId, cap: usize) -> Self { 38 | Self { 39 | index: HashMap::default(), 40 | packages: PackageGraph::with_capacity(cap, cap), 41 | root_crate, 42 | } 43 | } 44 | 45 | pub fn index(&self) -> &HashMap { 46 | &self.index 47 | } 48 | 49 | pub fn packages(&self) -> &PackageGraph { 50 | &self.packages 51 | } 52 | 53 | pub fn root_crate(&self) -> &PackageId { 54 | &self.root_crate 55 | } 56 | } 57 | 58 | impl PartialEq for DependencyGraph { 59 | fn eq(&self, other: &Self) -> bool { 60 | fn packages(graph: &DependencyGraph) -> Vec<&Package> { 61 | let package_id = graph.root_crate(); 62 | let root_index = graph.index()[package_id].into(); 63 | 64 | let mut res = Vec::with_capacity(graph.packages().node_count()); 65 | 66 | while let Some(dep) = Dfs::new(graph.packages(), root_index).next(graph.packages()) { 67 | let package = &graph.packages()[dep]; 68 | res.push(package); 69 | } 70 | 71 | res 72 | } 73 | 74 | self.root_crate() == other.root_crate() && packages(self) == packages(other) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/dependency_graph/resolver.rs: -------------------------------------------------------------------------------- 1 | use crate::dependency_graph::DependencyGraph; 2 | use crate::error::{CargoMSRVError, TResult}; 3 | use camino::Utf8Path; 4 | use cargo_metadata::MetadataCommand; 5 | 6 | pub(crate) trait DependencyResolver { 7 | fn resolve(&self) -> TResult; 8 | } 9 | 10 | pub(crate) struct CargoMetadataResolver { 11 | metadata_command: MetadataCommand, 12 | } 13 | 14 | impl CargoMetadataResolver { 15 | pub fn from_manifest_path(path: &Utf8Path) -> Self { 16 | let mut metadata_command = MetadataCommand::new(); 17 | metadata_command.manifest_path(path); 18 | 19 | Self { metadata_command } 20 | } 21 | } 22 | 23 | impl DependencyResolver for CargoMetadataResolver { 24 | fn resolve(&self) -> TResult { 25 | let result = self.metadata_command.exec()?; 26 | 27 | let our_crate = result 28 | .root_package() 29 | .ok_or(CargoMSRVError::NoCrateRootFound) 30 | .map(|pkg| pkg.id.clone())?; 31 | 32 | if let Some(dependencies) = result.resolve { 33 | let node_alloc = dependencies.nodes.len(); 34 | let mut graph = DependencyGraph::with_capacity(our_crate, node_alloc); 35 | 36 | build_package_graph(&mut graph, result.packages, dependencies.nodes); 37 | 38 | Ok(graph) 39 | } else { 40 | Ok(DependencyGraph::empty(our_crate)) 41 | } 42 | } 43 | } 44 | 45 | /// Builds a package graph from 1) a set of packages and 2) a given dependency graph. 46 | fn build_package_graph(graph: &mut DependencyGraph, packages: Ip, dependencies: Id) 47 | where 48 | Ip: IntoIterator, 49 | Id: IntoIterator, 50 | { 51 | // Add nodes to the petgraph 52 | for package in packages { 53 | let package_id = package.id.clone(); 54 | let node_index = graph.packages.add_node(package); 55 | let _ = graph.index.insert(package_id, node_index.index()); 56 | } 57 | 58 | for dependency in dependencies { 59 | for child in dependency.deps { 60 | use cargo_metadata::DependencyKind; 61 | // do not include dev-dependencies 62 | // you need normal and build dependencies to build crates, but not dev 63 | if child 64 | .dep_kinds 65 | .iter() 66 | .all(|k| k.kind == DependencyKind::Normal || k.kind == DependencyKind::Build) 67 | { 68 | let child = graph.index[&child.pkg]; 69 | let ancestor = graph.index[&dependency.id]; 70 | 71 | // add link 72 | graph.packages.add_edge(ancestor.into(), child.into(), ()); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/exit_code.rs: -------------------------------------------------------------------------------- 1 | /// Exit codes returned by cargo-msrv 2 | pub enum ExitCode { 3 | Success, 4 | Failure, 5 | } 6 | 7 | impl From for i32 { 8 | fn from(code: ExitCode) -> Self { 9 | match code { 10 | ExitCode::Success => 0, 11 | ExitCode::Failure => 1, 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/external_command/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cargo_command; 2 | pub mod rustup_command; 3 | -------------------------------------------------------------------------------- /src/io.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | // Alias trait for Write + Send 4 | pub trait SendWriter: io::Write + Send {} 5 | 6 | impl SendWriter for io::Stdout {} 7 | 8 | impl SendWriter for io::Stderr {} 9 | 10 | #[cfg(test)] 11 | impl SendWriter for Vec {} 12 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Documentation can be found on the project [README](https://github.com/foresterre/cargo-msrv/blob/main/README.md) page 2 | //! and in the cargo-msrv [book](https://foresterre.github.io/cargo-msrv/). If you can't find an answer on your question, 3 | //! feel free to open an [issue](https://github.com/foresterre/cargo-msrv/issues/new). 4 | //! 5 | //! Issues and ideas may be reported via the [issue tracker](https://github.com/foresterre/cargo-msrv/issues), 6 | //! and questions can be asked on the [discussion forum](https://github.com/foresterre/cargo-msrv/discussions). 7 | //! 8 | //! The docs focus on how to use `cargo-msrv` from the command line. If you want to also use it as a library, 9 | //! please feel free to open an [issue](https://github.com/foresterre/cargo-msrv/issues/new). 10 | 11 | #![deny(clippy::all)] 12 | #![allow( 13 | clippy::upper_case_acronyms, 14 | clippy::unnecessary_wraps, 15 | clippy::uninlined_format_args, 16 | clippy::items_after_test_module 17 | )] 18 | 19 | extern crate core; 20 | #[macro_use] 21 | extern crate tracing; 22 | 23 | pub use crate::context::{Context, OutputFormat, TracingOptions, TracingTargetOption}; 24 | pub use crate::outcome::Compatibility; 25 | pub use crate::sub_command::{Find, List, Set, Show, SubCommand, Verify}; 26 | 27 | use crate::compatibility::RustupToolchainCheck; 28 | use crate::context::ReleaseSource; 29 | use crate::error::{CargoMSRVError, TResult}; 30 | use crate::reporter::event::{Meta, SelectedPackages, SubcommandInit}; 31 | use crate::reporter::{Event, Reporter}; 32 | use rust::release_index; 33 | use rust_releases::semver; 34 | 35 | pub mod cli; 36 | pub mod compatibility; 37 | 38 | pub mod context; 39 | pub mod dependency_graph; 40 | pub mod error; 41 | pub mod exit_code; 42 | mod external_command; 43 | pub mod io; 44 | pub mod lockfile; 45 | pub mod log_level; 46 | pub mod manifest; 47 | pub mod msrv; 48 | pub mod outcome; 49 | pub mod reporter; 50 | pub mod rust; 51 | pub mod search_method; 52 | pub mod sub_command; 53 | pub mod typed_bool; 54 | pub mod writer; 55 | 56 | pub fn run_app(ctx: &Context, reporter: &impl Reporter) -> TResult<()> { 57 | reporter.report_event(Meta::default())?; 58 | reporter.report_event(SelectedPackages::new( 59 | ctx.environment_context().workspace_packages.selected(), 60 | ))?; 61 | reporter.report_event(SubcommandInit::new(ctx.reporting_name()))?; 62 | 63 | match ctx { 64 | Context::Find(ctx) => { 65 | let index = release_index::fetch_index(reporter, ctx.rust_releases.release_source)?; 66 | 67 | let runner = RustupToolchainCheck::new( 68 | reporter, 69 | ctx.ignore_lockfile, 70 | ctx.no_check_feedback, 71 | &ctx.environment, 72 | ctx.run_command(), 73 | ); 74 | Find::new(&index, runner).run(ctx, reporter)?; 75 | } 76 | Context::List(ctx) => { 77 | List.run(ctx, reporter)?; 78 | } 79 | Context::Set(ctx) => { 80 | let index = release_index::fetch_index(reporter, ctx.rust_releases.release_source).ok(); 81 | Set::new(index.as_ref()).run(ctx, reporter)?; 82 | } 83 | Context::Show(ctx) => { 84 | Show.run(ctx, reporter)?; 85 | } 86 | Context::Verify(ctx) => { 87 | let index = release_index::fetch_index(reporter, ctx.rust_releases.release_source)?; 88 | 89 | let runner = RustupToolchainCheck::new( 90 | reporter, 91 | ctx.ignore_lockfile, 92 | ctx.no_check_feedback, 93 | &ctx.environment, 94 | ctx.run_command(), 95 | ); 96 | 97 | Verify::new(&index, runner).run(ctx, reporter)?; 98 | } 99 | } 100 | 101 | Ok(()) 102 | } 103 | -------------------------------------------------------------------------------- /src/lockfile.rs: -------------------------------------------------------------------------------- 1 | use camino::{Utf8Path, Utf8PathBuf}; 2 | use std::marker::PhantomData; 3 | 4 | use crate::error::{IoError, IoErrorSource, TResult}; 5 | 6 | pub struct LockfileHandler { 7 | state: Utf8PathBuf, 8 | marker: PhantomData, 9 | } 10 | 11 | pub struct Start; 12 | pub struct Moved; 13 | pub struct Complete; 14 | 15 | pub trait LockfileState {} 16 | impl LockfileState for Start {} 17 | impl LockfileState for Moved {} 18 | impl LockfileState for Complete {} 19 | 20 | const CARGO_LOCK_REPLACEMENT: &str = "Cargo.lock-ignored-for-cargo-msrv"; 21 | 22 | impl LockfileHandler { 23 | pub fn new>(lock_file: P) -> Self { 24 | Self { 25 | state: lock_file.as_ref().to_path_buf(), 26 | marker: PhantomData, 27 | } 28 | } 29 | 30 | pub fn move_lockfile(self) -> TResult> { 31 | let folder = self.state.parent().unwrap(); 32 | std::fs::rename(self.state.as_path(), folder.join(CARGO_LOCK_REPLACEMENT)).map_err( 33 | |error| IoError { 34 | error, 35 | source: IoErrorSource::RenameFile(self.state.clone()), 36 | }, 37 | )?; 38 | 39 | Ok(LockfileHandler { 40 | state: self.state, 41 | marker: PhantomData, 42 | }) 43 | } 44 | } 45 | 46 | impl LockfileHandler { 47 | pub fn move_lockfile_back(self) -> TResult> { 48 | let folder = self.state.parent().unwrap(); 49 | std::fs::rename(folder.join(CARGO_LOCK_REPLACEMENT), self.state.as_path()).map_err( 50 | |err| IoError { 51 | error: err, 52 | source: IoErrorSource::RenameFile(self.state.clone()), 53 | }, 54 | )?; 55 | 56 | Ok(LockfileHandler { 57 | state: self.state, 58 | marker: PhantomData, 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/log_level.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | use std::str::FromStr; 3 | 4 | use clap::ValueEnum; 5 | 6 | #[derive(Copy, Clone, Debug, Default, Eq, PartialEq, ValueEnum)] 7 | pub enum LogLevel { 8 | Trace, 9 | Debug, 10 | #[default] 11 | Info, 12 | Warn, 13 | Error, 14 | } 15 | 16 | impl LogLevel { 17 | fn variants() -> &'static [&'static str] { 18 | &["trace", "debug", "info", "warn", "error"] 19 | } 20 | } 21 | 22 | impl Display for LogLevel { 23 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 24 | f.write_fmt(format_args!("{:?}", self)) 25 | } 26 | } 27 | 28 | impl FromStr for LogLevel { 29 | type Err = ParseLogLevelError; 30 | 31 | fn from_str(input: &str) -> Result { 32 | fn parse_num_level(input: &str) -> Option { 33 | input.parse::().ok().and_then(|number| match number { 34 | 1 => Some(LogLevel::Error), 35 | 2 => Some(LogLevel::Warn), 36 | 3 => Some(LogLevel::Info), 37 | 4 => Some(LogLevel::Debug), 38 | 5 => Some(LogLevel::Trace), 39 | _ => None, 40 | }) 41 | } 42 | 43 | fn parse_str_level(input: &str) -> Option { 44 | match input { 45 | s if s.eq_ignore_ascii_case("error") => Some(LogLevel::Error), 46 | s if s.eq_ignore_ascii_case("warn") => Some(LogLevel::Warn), 47 | s if s.eq_ignore_ascii_case("info") => Some(LogLevel::Info), 48 | s if s.eq_ignore_ascii_case("debug") => Some(LogLevel::Debug), 49 | s if s.eq_ignore_ascii_case("trace") => Some(LogLevel::Trace), 50 | _ => None, 51 | } 52 | } 53 | 54 | parse_num_level(input) 55 | .or_else(|| parse_str_level(input)) 56 | .ok_or_else(|| ParseLogLevelError::NoMatchingLevel { 57 | given_input: input.to_string(), 58 | valid_options_formatted: Self::variants().join(","), 59 | }) 60 | } 61 | } 62 | 63 | impl From for tracing::Level { 64 | fn from(level: LogLevel) -> Self { 65 | match level { 66 | LogLevel::Trace => tracing::Level::TRACE, 67 | LogLevel::Debug => tracing::Level::DEBUG, 68 | LogLevel::Info => tracing::Level::INFO, 69 | LogLevel::Warn => tracing::Level::WARN, 70 | LogLevel::Error => tracing::Level::ERROR, 71 | } 72 | } 73 | } 74 | 75 | #[derive(Debug, thiserror::Error)] 76 | pub enum ParseLogLevelError { 77 | #[error("The given log level '{given_input}' does not exist, valid options are: {valid_options_formatted}]")] 78 | NoMatchingLevel { 79 | given_input: String, 80 | valid_options_formatted: String, 81 | }, 82 | } 83 | 84 | #[cfg(test)] 85 | mod tests { 86 | use crate::log_level::{LogLevel, ParseLogLevelError}; 87 | 88 | #[yare::parameterized( 89 | trace_numeric = { "5", LogLevel::Trace }, 90 | trace_alphabetic = { "trace", LogLevel::Trace }, 91 | debug_numeric = { "4", LogLevel::Debug }, 92 | debug_alphabetic = { "debug", LogLevel::Debug }, 93 | debug_alphabetic_uppercase = { "DEBUG", LogLevel::Debug }, 94 | info_numeric = { "3", LogLevel::Info }, 95 | info_alphabetic = { "info", LogLevel::Info }, 96 | info_alphabetic_uppercase = { "INFO", LogLevel::Info }, 97 | warn_numeric = { "2", LogLevel::Warn }, 98 | warn_alphabetic = { "warn", LogLevel::Warn }, 99 | warn_alphabetic_uppercase = { "WARN", LogLevel::Warn }, 100 | error_numeric = { "1", LogLevel::Error }, 101 | error_alphabetic = { "error", LogLevel::Error }, 102 | error_alphabetic_uppercase = { "ERROR", LogLevel::Error }, 103 | )] 104 | fn valid_log_levels(input: &str, expected: LogLevel) { 105 | assert_eq!(input.parse::().unwrap(), expected); 106 | } 107 | 108 | #[yare::parameterized( 109 | numeric_floor = { "0" }, 110 | numeric_ceil = { "6" }, 111 | non_existing = { "null" }, 112 | empty = { "" }, 113 | )] 114 | fn invalid_log_levels(input: &str) { 115 | let expected_err = input.parse::().unwrap_err(); 116 | 117 | match expected_err { 118 | ParseLogLevelError::NoMatchingLevel { 119 | ref given_input, .. 120 | } => assert_eq!(given_input, input), 121 | }; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/msrv.rs: -------------------------------------------------------------------------------- 1 | use crate::rust::RustRelease; 2 | use crate::rust::Toolchain; 3 | 4 | /// An enum to represent the minimal compatibility 5 | #[derive(Clone, Debug, Eq, PartialEq)] 6 | pub enum MinimumSupportedRustVersion { 7 | /// A toolchain is compatible, if the outcome of a toolchain check results in a success 8 | Toolchain { 9 | // toolchain 10 | toolchain: Toolchain, 11 | }, 12 | /// Compatibility is none, if the check on the last available toolchain fails 13 | NoCompatibleToolchain, 14 | } 15 | 16 | impl MinimumSupportedRustVersion { 17 | pub fn toolchain(msrv: &RustRelease) -> Self { 18 | let toolchain = msrv.to_toolchain_spec().to_owned(); 19 | 20 | Self::Toolchain { toolchain } 21 | } 22 | 23 | pub fn from_option(msrv: Option<&RustRelease>) -> Self { 24 | msrv.map_or( 25 | MinimumSupportedRustVersion::NoCompatibleToolchain, 26 | MinimumSupportedRustVersion::toolchain, 27 | ) 28 | } 29 | } 30 | 31 | impl MinimumSupportedRustVersion { 32 | #[cfg(test)] 33 | pub fn unwrap_version(&self) -> rust_releases::semver::Version { 34 | if let Self::Toolchain { toolchain, .. } = self { 35 | return toolchain.version().clone(); 36 | } 37 | 38 | panic!("Unable to unwrap MinimalCompatibility (CapableToolchain::version)") 39 | } 40 | } 41 | 42 | #[cfg(test)] 43 | mod tests { 44 | use crate::msrv::MinimumSupportedRustVersion; 45 | use crate::rust::RustRelease; 46 | use cargo_metadata::semver; 47 | 48 | #[test] 49 | fn accept() { 50 | let version = semver::Version::new(1, 2, 3); 51 | let rust_release = RustRelease::new( 52 | rust_releases::Release::new_stable(version.clone()), 53 | "x", 54 | &[], 55 | ); 56 | let msrv = MinimumSupportedRustVersion::toolchain(&rust_release); 57 | 58 | assert!(matches!( 59 | msrv, 60 | MinimumSupportedRustVersion::Toolchain { toolchain } if toolchain.version() == &version && toolchain.target() == "x")); 61 | } 62 | 63 | #[test] 64 | fn accept_from_option() { 65 | let version = semver::Version::new(1, 2, 3); 66 | let rust_release = RustRelease::new( 67 | rust_releases::Release::new_stable(version.clone()), 68 | "x", 69 | &[], 70 | ); 71 | let msrv = MinimumSupportedRustVersion::from_option(Some(&rust_release)); 72 | 73 | assert!(matches!( 74 | msrv, 75 | MinimumSupportedRustVersion::Toolchain { toolchain } if toolchain.version() == &version && toolchain.target() == "x")); 76 | } 77 | 78 | #[test] 79 | fn reject_from_option() { 80 | let msrv = MinimumSupportedRustVersion::from_option(None); 81 | 82 | assert!(matches!( 83 | msrv, 84 | MinimumSupportedRustVersion::NoCompatibleToolchain 85 | )); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/outcome.rs: -------------------------------------------------------------------------------- 1 | //! The outcome of a single toolchain [`check`] run. 2 | //! 3 | //! [`check`]: crate::compatibility::IsCompatible 4 | 5 | use crate::rust::Toolchain; 6 | use rust_releases::semver; 7 | 8 | #[derive(Clone, Debug)] 9 | pub enum Compatibility { 10 | Compatible(Compatible), 11 | Incompatible(Incompatible), 12 | } 13 | 14 | impl Compatibility { 15 | pub fn new_success(toolchain_spec: Toolchain) -> Self { 16 | Self::Compatible(Compatible { toolchain_spec }) 17 | } 18 | 19 | pub fn new_failure(toolchain_spec: Toolchain, error_message: String) -> Self { 20 | Self::Incompatible(Incompatible { 21 | toolchain_spec, 22 | error_message, 23 | }) 24 | } 25 | 26 | pub fn is_success(&self) -> bool { 27 | match self { 28 | Self::Compatible { .. } => true, 29 | Self::Incompatible { .. } => false, 30 | } 31 | } 32 | 33 | pub fn version(&self) -> &semver::Version { 34 | match self { 35 | Self::Compatible(outcome) => outcome.toolchain_spec.version(), 36 | Self::Incompatible(outcome) => outcome.toolchain_spec.version(), 37 | } 38 | } 39 | 40 | pub fn toolchain_spec(&self) -> &Toolchain { 41 | match self { 42 | Self::Compatible(outcome) => &outcome.toolchain_spec, 43 | Self::Incompatible(outcome) => &outcome.toolchain_spec, 44 | } 45 | } 46 | } 47 | 48 | #[derive(Clone, Debug, Eq, PartialEq)] 49 | pub struct Compatible { 50 | pub(crate) toolchain_spec: Toolchain, 51 | } 52 | 53 | #[derive(Clone, Debug, Eq, PartialEq)] 54 | pub struct Incompatible { 55 | pub(crate) toolchain_spec: Toolchain, 56 | pub(crate) error_message: String, 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use crate::rust::Toolchain; 62 | use crate::Compatibility; 63 | use rust_releases::semver; 64 | 65 | #[test] 66 | fn success_outcome() { 67 | let version = semver::Version::new(1, 2, 3); 68 | let toolchain = Toolchain::new(version, "x", &[]); 69 | 70 | let outcome = Compatibility::new_success(toolchain.clone()); 71 | 72 | assert!(outcome.is_success()); 73 | assert_eq!(outcome.version(), &semver::Version::new(1, 2, 3)); 74 | assert_eq!(outcome.toolchain_spec(), &toolchain); 75 | } 76 | 77 | #[test] 78 | fn failure_outcome() { 79 | let version = semver::Version::new(1, 2, 3); 80 | let toolchain = Toolchain::new(version, "x", &[]); 81 | 82 | let outcome = Compatibility::new_failure(toolchain.clone(), "msg".to_string()); 83 | 84 | assert!(!outcome.is_success()); 85 | assert_eq!(outcome.version(), &semver::Version::new(1, 2, 3)); 86 | assert_eq!(outcome.toolchain_spec(), &toolchain); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/reporter/event/auxiliary_output.rs: -------------------------------------------------------------------------------- 1 | use crate::reporter::event::Message; 2 | use crate::Event; 3 | use camino::Utf8PathBuf; 4 | 5 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 6 | #[serde(rename_all = "snake_case")] 7 | pub struct AuxiliaryOutput { 8 | destination: Destination, 9 | item: Item, 10 | } 11 | 12 | impl AuxiliaryOutput { 13 | pub fn new(destination: Destination, item: Item) -> Self { 14 | Self { destination, item } 15 | } 16 | } 17 | 18 | impl From for Event { 19 | fn from(it: AuxiliaryOutput) -> Self { 20 | Message::AuxiliaryOutput(it).into() 21 | } 22 | } 23 | 24 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 25 | #[serde(rename_all = "snake_case")] 26 | #[serde(tag = "type")] 27 | pub enum Destination { 28 | File { path: Utf8PathBuf }, 29 | } 30 | 31 | impl Destination { 32 | pub fn file(path: Utf8PathBuf) -> Self { 33 | Self::File { path } 34 | } 35 | } 36 | 37 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 38 | #[serde(rename_all = "snake_case")] 39 | #[serde(tag = "type")] 40 | pub enum Item { 41 | Msrv { kind: MsrvKind }, 42 | ToolchainFile { kind: ToolchainFileKind }, 43 | } 44 | 45 | impl Item { 46 | pub fn msrv(kind: MsrvKind) -> Self { 47 | Self::Msrv { kind } 48 | } 49 | 50 | pub fn toolchain_file(kind: ToolchainFileKind) -> Self { 51 | Self::ToolchainFile { kind } 52 | } 53 | } 54 | 55 | #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)] 56 | #[serde(rename_all = "snake_case")] 57 | pub enum MsrvKind { 58 | // The package.rust-version as supported by the Cargo Manifest format. 59 | RustVersion, 60 | // The package.metadata.msrv key used as fallback for crates where the Cargo Manifest format did 61 | // not support the package.rust-version key yet. 62 | MetadataFallback, 63 | } 64 | 65 | #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)] 66 | #[serde(rename_all = "snake_case")] 67 | pub enum ToolchainFileKind { 68 | /* Legacy, : Unsupported right now */ 69 | Toml, 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use super::*; 75 | use crate::reporter::event::Message; 76 | use crate::reporter::TestReporterWrapper; 77 | use crate::Event; 78 | use camino::Utf8Path; 79 | use storyteller::EventReporter; 80 | 81 | #[yare::parameterized( 82 | rust_version_msrv = { Item::msrv(MsrvKind::RustVersion) }, 83 | metadata_fallback_msrv = { Item::msrv(MsrvKind::MetadataFallback) }, 84 | toolchain_file_toml = { Item::toolchain_file(ToolchainFileKind::Toml) }, 85 | )] 86 | fn reported_action(item: Item) { 87 | let reporter = TestReporterWrapper::default(); 88 | let path = Utf8Path::new("hello"); 89 | let event = AuxiliaryOutput::new(Destination::file(path.to_path_buf()), item); 90 | 91 | reporter.get().report_event(event.clone()).unwrap(); 92 | 93 | assert_eq!( 94 | reporter.wait_for_events(), 95 | vec![Event::unscoped(Message::AuxiliaryOutput(event)),] 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/reporter/event/check_method.rs: -------------------------------------------------------------------------------- 1 | use crate::reporter::event::Message; 2 | use crate::rust::Toolchain; 3 | use crate::Event; 4 | use camino::{Utf8Path, Utf8PathBuf}; 5 | 6 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 7 | #[serde(rename_all = "snake_case")] 8 | pub struct CheckMethod { 9 | toolchain: Toolchain, 10 | method: Method, 11 | } 12 | 13 | impl CheckMethod { 14 | pub fn new(toolchain: impl Into, method: Method) -> Self { 15 | Self { 16 | toolchain: toolchain.into(), 17 | method, 18 | } 19 | } 20 | } 21 | 22 | impl From for Event { 23 | fn from(it: CheckMethod) -> Self { 24 | Message::CheckMethod(it).into() 25 | } 26 | } 27 | 28 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 29 | #[serde(rename_all = "snake_case")] 30 | #[serde(tag = "type")] 31 | pub enum Method { 32 | RustupRun { 33 | args: Vec, 34 | path: Utf8PathBuf, 35 | }, 36 | #[cfg(test)] 37 | TestRunner, 38 | } 39 | 40 | impl Method { 41 | pub fn rustup_run( 42 | args: impl IntoIterator>, 43 | path: impl AsRef, 44 | ) -> Self { 45 | Self::RustupRun { 46 | args: args.into_iter().map(|s| s.as_ref().to_string()).collect(), 47 | path: path.as_ref().to_path_buf(), 48 | } 49 | } 50 | } 51 | 52 | #[cfg(test)] 53 | mod tests { 54 | use super::*; 55 | use crate::reporter::event::Message; 56 | use crate::reporter::TestReporterWrapper; 57 | use crate::semver; 58 | use camino::Utf8Path; 59 | use storyteller::EventReporter; 60 | 61 | #[yare::parameterized( 62 | rustup_run_with_path = { Method::rustup_run(["hello"], Utf8Path::new("haha")) }, 63 | test_runner = { Method::TestRunner }, 64 | )] 65 | fn reported_event(method: Method) { 66 | let reporter = TestReporterWrapper::default(); 67 | let event = CheckMethod::new( 68 | Toolchain::new(semver::Version::new(1, 2, 3), "test_target", &[]), 69 | method, 70 | ); 71 | 72 | reporter.get().report_event(event.clone()).unwrap(); 73 | 74 | assert_eq!( 75 | reporter.wait_for_events(), 76 | vec![Event::unscoped(Message::CheckMethod(event)),] 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/reporter/event/check_result.rs: -------------------------------------------------------------------------------- 1 | use crate::reporter::event::shared::compatibility::Compatibility; 2 | use crate::reporter::event::Message; 3 | use crate::rust::Toolchain; 4 | use crate::Event; 5 | 6 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 7 | #[serde(rename_all = "snake_case")] 8 | pub struct CheckResult { 9 | #[serde(flatten)] 10 | pub compatibility: Compatibility, 11 | } 12 | 13 | impl CheckResult { 14 | pub fn compatible(toolchain: impl Into) -> Self { 15 | Self { 16 | compatibility: Compatibility::compatible(toolchain), 17 | } 18 | } 19 | 20 | pub fn incompatible(toolchain: impl Into, error: Option) -> Self { 21 | Self { 22 | compatibility: Compatibility::incompatible(toolchain, error), 23 | } 24 | } 25 | 26 | pub fn toolchain(&self) -> &Toolchain { 27 | self.compatibility.toolchain() 28 | } 29 | 30 | pub fn is_compatible(&self) -> bool { 31 | self.compatibility.is_compatible() 32 | } 33 | } 34 | 35 | impl From for Event { 36 | fn from(it: CheckResult) -> Self { 37 | Message::CheckResult(it).into() 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | use crate::reporter::event::Message; 45 | use crate::reporter::TestReporterWrapper; 46 | use crate::{semver, Event}; 47 | use storyteller::EventReporter; 48 | 49 | #[test] 50 | fn reported_compatible_toolchain() { 51 | let reporter = TestReporterWrapper::default(); 52 | let event = CheckResult::compatible(Toolchain::new( 53 | semver::Version::new(1, 2, 3), 54 | "test_target", 55 | &[], 56 | )); 57 | 58 | reporter.get().report_event(event.clone()).unwrap(); 59 | 60 | assert_eq!( 61 | reporter.wait_for_events(), 62 | vec![Event::unscoped(Message::CheckResult(event)),] 63 | ); 64 | } 65 | 66 | #[yare::parameterized( 67 | none = { None }, 68 | some = {Some("whoo!".to_string()) }, 69 | )] 70 | fn reported_incompatible_toolchain(error_message: Option) { 71 | let reporter = TestReporterWrapper::default(); 72 | let event = CheckResult::incompatible( 73 | Toolchain::new(semver::Version::new(1, 2, 3), "test_target", &[]), 74 | error_message, 75 | ); 76 | 77 | reporter.get().report_event(event.clone()).unwrap(); 78 | 79 | assert_eq!( 80 | reporter.wait_for_events(), 81 | vec![Event::unscoped(Message::CheckResult(event)),] 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/reporter/event/check_toolchain.rs: -------------------------------------------------------------------------------- 1 | use crate::reporter::event::Message; 2 | use crate::rust::Toolchain; 3 | use crate::Event; 4 | 5 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 6 | #[serde(rename_all = "snake_case")] 7 | pub struct CheckToolchain { 8 | pub toolchain: Toolchain, 9 | } 10 | 11 | impl CheckToolchain { 12 | pub fn new(toolchain: impl Into) -> Self { 13 | Self { 14 | toolchain: toolchain.into(), 15 | } 16 | } 17 | } 18 | 19 | impl From for Event { 20 | fn from(it: CheckToolchain) -> Self { 21 | Message::CheckToolchain(it).into() 22 | } 23 | } 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | use super::*; 28 | use crate::reporter::event::Message; 29 | use crate::reporter::TestReporterWrapper; 30 | use crate::semver; 31 | use storyteller::EventReporter; 32 | 33 | #[test] 34 | fn reported_event() { 35 | let reporter = TestReporterWrapper::default(); 36 | let event = CheckToolchain::new(Toolchain::new( 37 | semver::Version::new(1, 2, 3), 38 | "test_target", 39 | &[], 40 | )); 41 | 42 | reporter.get().report_event(event.clone()).unwrap(); 43 | 44 | assert_eq!( 45 | reporter.wait_for_events(), 46 | vec![Event::unscoped(Message::CheckToolchain(event)),] 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/reporter/event/fetch_index.rs: -------------------------------------------------------------------------------- 1 | use crate::reporter::event::Message; 2 | use crate::{Event, ReleaseSource}; 3 | 4 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 5 | #[serde(rename_all = "snake_case")] 6 | pub struct FetchIndex { 7 | #[serde(rename = "source")] 8 | from_source: ReleaseSource, 9 | } 10 | 11 | impl FetchIndex { 12 | pub fn new(source: ReleaseSource) -> Self { 13 | Self { 14 | from_source: source, 15 | } 16 | } 17 | } 18 | 19 | impl From for Event { 20 | fn from(it: FetchIndex) -> Self { 21 | Message::FetchIndex(it).into() 22 | } 23 | } 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | use super::*; 28 | use crate::reporter::event::Message; 29 | use crate::reporter::TestReporterWrapper; 30 | use storyteller::EventReporter; 31 | 32 | #[test] 33 | fn reported_rust_changelog_source() { 34 | let reporter = TestReporterWrapper::default(); 35 | let event = FetchIndex::new(ReleaseSource::RustChangelog); 36 | 37 | reporter.get().report_event(event.clone()).unwrap(); 38 | 39 | assert_eq!( 40 | reporter.wait_for_events(), 41 | vec![Event::unscoped(Message::FetchIndex(event)),] 42 | ); 43 | } 44 | 45 | #[cfg(feature = "rust-releases-dist-source")] 46 | #[test] 47 | fn reported_rust_dist_source() { 48 | let reporter = TestReporterWrapper::default(); 49 | let event = FetchIndex::new(ReleaseSource::RustDist); 50 | 51 | reporter.get().report_event(event.clone()).unwrap(); 52 | 53 | assert_eq!( 54 | reporter.wait_for_events(), 55 | vec![Event::unscoped(Message::FetchIndex(event)),] 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/reporter/event/meta.rs: -------------------------------------------------------------------------------- 1 | use crate::reporter::event::Message; 2 | use crate::Event; 3 | 4 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 5 | #[serde(rename_all = "snake_case")] 6 | pub struct Meta { 7 | instance: &'static str, 8 | version: &'static str, 9 | sha_short: Option<&'static str>, 10 | target_triple: Option<&'static str>, 11 | cargo_features: Option<&'static str>, 12 | rustc: Option<&'static str>, 13 | } 14 | 15 | impl Meta { 16 | pub fn instance(&self) -> &'static str { 17 | self.instance 18 | } 19 | 20 | pub fn version(&self) -> &'static str { 21 | self.version 22 | } 23 | 24 | pub fn sha_short(&self) -> Option<&'static str> { 25 | self.sha_short 26 | } 27 | pub fn target_triple(&self) -> Option<&'static str> { 28 | self.target_triple 29 | } 30 | 31 | pub fn cargo_features(&self) -> Option<&'static str> { 32 | self.cargo_features 33 | } 34 | 35 | pub fn rustc(&self) -> Option<&'static str> { 36 | self.rustc 37 | } 38 | } 39 | 40 | const UNKNOWN_VERSION: &str = "?"; 41 | 42 | impl Default for Meta { 43 | fn default() -> Self { 44 | Self { 45 | instance: option_env!("CARGO_PKG_NAME").unwrap_or("cargo-msrv"), 46 | version: option_env!("CARGO_PKG_VERSION").unwrap_or(UNKNOWN_VERSION), 47 | sha_short: option_env!("VERGEN_GIT_SHA_SHORT"), 48 | target_triple: option_env!("VERGEN_CARGO_TARGET_TRIPLE"), 49 | cargo_features: option_env!("VERGEN_CARGO_FEATURES"), 50 | rustc: option_env!("VERGEN_RUSTC_SEMVER"), 51 | } 52 | } 53 | } 54 | 55 | impl From for Event { 56 | fn from(it: Meta) -> Self { 57 | Message::Meta(it).into() 58 | } 59 | } 60 | 61 | #[cfg(test)] 62 | mod tests { 63 | use super::*; 64 | use crate::reporter::event::Message; 65 | use crate::reporter::TestReporterWrapper; 66 | use storyteller::EventReporter; 67 | 68 | #[test] 69 | fn reported_event() { 70 | let reporter = TestReporterWrapper::default(); 71 | let event = Meta::default(); 72 | 73 | reporter.get().report_event(event.clone()).unwrap(); 74 | 75 | assert_eq!( 76 | reporter.wait_for_events(), 77 | vec![Event::unscoped(Message::Meta(event)),] 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/reporter/event/progress.rs: -------------------------------------------------------------------------------- 1 | use crate::reporter::event::Message; 2 | use crate::Event; 3 | 4 | /// Progression indicates how far we are 5 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 6 | #[serde(rename_all = "snake_case")] 7 | pub struct Progress { 8 | // index of the currently running check into the sorted search space 9 | current: u64, 10 | // size of the search space 11 | search_space_size: u64, 12 | // how many iterations have been completed, including the currently running one 13 | iteration: u64, 14 | } 15 | 16 | impl From for Event { 17 | fn from(it: Progress) -> Self { 18 | Message::Progress(it).into() 19 | } 20 | } 21 | 22 | impl Progress { 23 | pub fn new(current: u64, search_space_size: u64, iteration: u64) -> Self { 24 | Self { 25 | current, 26 | search_space_size, 27 | iteration, 28 | } 29 | } 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | use super::*; 35 | use crate::reporter::event::Message; 36 | use crate::reporter::TestReporterWrapper; 37 | use storyteller::EventReporter; 38 | 39 | #[test] 40 | fn reported_event() { 41 | let reporter = TestReporterWrapper::default(); 42 | let event = Progress::new(10, 100, 30); 43 | 44 | reporter.get().report_event(event.clone()).unwrap(); 45 | 46 | assert_eq!( 47 | reporter.wait_for_events(), 48 | vec![Event::unscoped(Message::Progress(event)),] 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/reporter/event/scope.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicUsize, Ordering}; 2 | 3 | #[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize)] 4 | #[serde(rename_all = "snake_case")] 5 | pub struct Scope { 6 | pub id: usize, 7 | pub marker: Marker, 8 | } 9 | 10 | impl Scope { 11 | pub fn new(id: usize, marker: Marker) -> Self { 12 | Self { id, marker } 13 | } 14 | 15 | /// Tests whether this is marked as the start of the scope or not. 16 | pub fn is_start(&self) -> bool { 17 | matches!(self.marker, Marker::Start) 18 | } 19 | } 20 | 21 | #[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize)] 22 | #[serde(rename_all = "snake_case")] 23 | pub enum Marker { 24 | Start, 25 | End, 26 | } 27 | 28 | pub trait SupplyScopeGenerator { 29 | type ScopeGen: ScopeGenerator; 30 | 31 | fn scope_generator(&self) -> &Self::ScopeGen; 32 | } 33 | 34 | /// Generator to generate a unique scope for a scoped event. 35 | pub trait ScopeGenerator { 36 | /// Returns a pair of scope's with opposite markers. The first 37 | /// scope will mark the start of the scope, while the second scope 38 | /// will mark the end of the scope. 39 | /// 40 | /// The id of the scope data structures can be used to identify a scope. 41 | /// They will be identical for the returned pair. 42 | /// While the generated id must be unique for the programs' execution, no 43 | /// other guarantees are made about the id itself. 44 | fn generate(&self) -> (Scope, Scope); 45 | } 46 | 47 | /// A counter based scope generator. 48 | pub struct ScopeCounter { 49 | counter: AtomicUsize, 50 | } 51 | 52 | impl ScopeCounter { 53 | pub const fn new() -> Self { 54 | Self { 55 | counter: AtomicUsize::new(0), 56 | } 57 | } 58 | } 59 | 60 | impl ScopeGenerator for ScopeCounter { 61 | fn generate(&self) -> (Scope, Scope) { 62 | let id = self.counter.fetch_add(1, Ordering::Relaxed); 63 | 64 | (Scope::new(id, Marker::Start), Scope::new(id, Marker::End)) 65 | } 66 | } 67 | 68 | #[cfg(test)] 69 | #[derive(Default)] 70 | pub struct TestScopeGenerator; 71 | 72 | #[cfg(test)] 73 | impl ScopeGenerator for TestScopeGenerator { 74 | fn generate(&self) -> (Scope, Scope) { 75 | let id = 0; 76 | 77 | (Scope::new(id, Marker::Start), Scope::new(id, Marker::End)) 78 | } 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use crate::reporter::event::scope::{ScopeCounter, ScopeGenerator}; 84 | use crate::reporter::event::Marker; 85 | use std::sync::atomic::Ordering; 86 | 87 | #[test] 88 | fn unused() { 89 | let gen = ScopeCounter::new(); 90 | assert_eq!(gen.counter.load(Ordering::Relaxed), 0); 91 | } 92 | 93 | #[test] 94 | fn first_id() { 95 | let gen = ScopeCounter::new(); 96 | let (start, end) = gen.generate(); 97 | 98 | assert_eq!(start.id, 0); 99 | assert_eq!(end.id, 0); 100 | 101 | assert_eq!(start.marker, Marker::Start); 102 | assert_eq!(end.marker, Marker::End); 103 | } 104 | 105 | #[test] 106 | fn second_id() { 107 | let gen = ScopeCounter::new(); 108 | let (start, end) = gen.generate(); 109 | 110 | assert_eq!(start.id, 0); 111 | assert_eq!(end.id, 0); 112 | 113 | assert_eq!(start.marker, Marker::Start); 114 | assert_eq!(end.marker, Marker::End); 115 | 116 | let (start, end) = gen.generate(); 117 | 118 | assert_eq!(start.id, 1); 119 | assert_eq!(end.id, 1); 120 | 121 | assert_eq!(start.marker, Marker::Start); 122 | assert_eq!(end.marker, Marker::End); 123 | } 124 | 125 | #[test] 126 | fn thousand() { 127 | let gen = ScopeCounter::new(); 128 | 129 | for _ in 0..1000 { 130 | gen.generate(); 131 | } 132 | 133 | let (start, end) = gen.generate(); 134 | 135 | assert_eq!(start.id, 1000); 136 | assert_eq!(end.id, 1000); 137 | 138 | assert_eq!(start.marker, Marker::Start); 139 | assert_eq!(end.marker, Marker::End); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/reporter/event/search_method.rs: -------------------------------------------------------------------------------- 1 | use crate::context::SearchMethod as Method; 2 | use crate::reporter::event::Message; 3 | use crate::Event; 4 | 5 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 6 | #[serde(rename_all = "snake_case")] 7 | pub struct FindMsrv { 8 | search_method: Method, 9 | } 10 | 11 | impl FindMsrv { 12 | pub(crate) fn new(method: Method) -> Self { 13 | Self { 14 | search_method: method, 15 | } 16 | } 17 | } 18 | 19 | impl From for Event { 20 | fn from(it: FindMsrv) -> Self { 21 | Message::FindMsrv(it).into() 22 | } 23 | } 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | use super::*; 28 | use crate::reporter::event::Message; 29 | use crate::reporter::TestReporterWrapper; 30 | use storyteller::EventReporter; 31 | 32 | #[yare::parameterized( 33 | linear = { Method::Linear }, 34 | bisect = { Method::Bisect }, 35 | )] 36 | fn reported_event(method: Method) { 37 | let reporter = TestReporterWrapper::default(); 38 | let event = FindMsrv::new(method); 39 | 40 | reporter.get().report_event(event.clone()).unwrap(); 41 | 42 | assert_eq!( 43 | reporter.wait_for_events(), 44 | vec![Event::unscoped(Message::FindMsrv(event)),] 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/reporter/event/selected_packages.rs: -------------------------------------------------------------------------------- 1 | use crate::reporter::{Event, Message}; 2 | use camino::Utf8PathBuf; 3 | 4 | /// Workspace packages selected 5 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 6 | #[serde(rename_all = "snake_case")] 7 | pub struct SelectedPackages { 8 | package_names: Option>, 9 | } 10 | 11 | impl SelectedPackages { 12 | pub fn new(package_names: Option>) -> Self { 13 | Self { package_names } 14 | } 15 | } 16 | 17 | impl From for Event { 18 | fn from(it: SelectedPackages) -> Self { 19 | Message::SelectedPackages(it).into() 20 | } 21 | } 22 | 23 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 24 | #[serde(rename_all = "snake_case")] 25 | pub struct SelectedPackage { 26 | pub name: String, 27 | pub path: Utf8PathBuf, 28 | } 29 | -------------------------------------------------------------------------------- /src/reporter/event/setup_toolchain.rs: -------------------------------------------------------------------------------- 1 | use crate::reporter::event::Message; 2 | use crate::rust::Toolchain; 3 | use crate::Event; 4 | 5 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 6 | #[serde(rename_all = "snake_case")] 7 | pub struct SetupToolchain { 8 | toolchain: Toolchain, 9 | } 10 | 11 | impl SetupToolchain { 12 | pub fn new(toolchain: impl Into) -> Self { 13 | Self { 14 | toolchain: toolchain.into(), 15 | } 16 | } 17 | } 18 | 19 | impl From for Event { 20 | fn from(it: SetupToolchain) -> Self { 21 | Message::SetupToolchain(it).into() 22 | } 23 | } 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | use super::*; 28 | use crate::reporter::event::Message; 29 | use crate::reporter::TestReporterWrapper; 30 | use crate::semver; 31 | use storyteller::EventReporter; 32 | 33 | #[test] 34 | fn reported_event() { 35 | let reporter = TestReporterWrapper::default(); 36 | let event = SetupToolchain::new(Toolchain::new( 37 | semver::Version::new(1, 2, 3), 38 | "test_target", 39 | &[], 40 | )); 41 | 42 | reporter.get().report_event(event.clone()).unwrap(); 43 | 44 | assert_eq!( 45 | reporter.wait_for_events(), 46 | vec![Event::unscoped(Message::SetupToolchain(event)),] 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/reporter/event/shared/compatibility.rs: -------------------------------------------------------------------------------- 1 | use crate::rust::Toolchain; 2 | 3 | /// Reports whether a crate is compatible with a certain toolchain, or not. 4 | /// If it's not compatible, it may specify a reason why it is not compatible. 5 | 6 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 7 | #[serde(rename_all = "snake_case")] 8 | pub struct Compatibility { 9 | toolchain: Toolchain, 10 | is_compatible: bool, 11 | #[serde(skip_serializing_if = "Option::is_none")] 12 | error: Option, 13 | } 14 | 15 | impl Compatibility { 16 | pub fn compatible(toolchain: impl Into) -> Self { 17 | Self { 18 | toolchain: toolchain.into(), 19 | is_compatible: true, 20 | error: None, 21 | } 22 | } 23 | 24 | pub fn incompatible(toolchain: impl Into, error: Option) -> Self { 25 | Self { 26 | toolchain: toolchain.into(), 27 | is_compatible: false, 28 | error, 29 | } 30 | } 31 | 32 | pub fn toolchain(&self) -> &Toolchain { 33 | &self.toolchain 34 | } 35 | 36 | pub fn is_compatible(&self) -> bool { 37 | self.is_compatible 38 | } 39 | 40 | pub fn error(&self) -> Option<&str> { 41 | self.error.as_deref() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/reporter/event/shared/mod.rs: -------------------------------------------------------------------------------- 1 | //! Event context which is shared between several events 2 | 3 | /// Reports whether a given Rust toolchain is compatible with the given crate. 4 | pub mod compatibility; 5 | -------------------------------------------------------------------------------- /src/reporter/event/subcommand_init.rs: -------------------------------------------------------------------------------- 1 | use crate::reporter::event::Message; 2 | use crate::Event; 3 | 4 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 5 | #[serde(rename_all = "snake_case")] 6 | pub struct SubcommandInit { 7 | subcommand_id: &'static str, 8 | } 9 | 10 | impl SubcommandInit { 11 | pub fn new(subcommand_id: &'static str) -> Self { 12 | Self { subcommand_id } 13 | } 14 | 15 | pub fn subcommand_id(&self) -> &'static str { 16 | self.subcommand_id 17 | } 18 | } 19 | 20 | impl From for Event { 21 | fn from(it: SubcommandInit) -> Self { 22 | Message::SubcommandInit(it).into() 23 | } 24 | } 25 | 26 | #[cfg(test)] 27 | mod tests { 28 | use crate::reporter::event::Message; 29 | use crate::reporter::TestReporterWrapper; 30 | use crate::{Event, SubcommandInit}; 31 | use storyteller::EventReporter; 32 | 33 | #[test] 34 | fn reported_action() { 35 | let reporter = TestReporterWrapper::default(); 36 | let event = SubcommandInit::new("find"); 37 | 38 | reporter.get().report_event(event.clone()).unwrap(); 39 | 40 | assert_eq!( 41 | reporter.wait_for_events(), 42 | vec![Event::unscoped(Message::SubcommandInit(event)),] 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/reporter/event/subcommand_result.rs: -------------------------------------------------------------------------------- 1 | use crate::reporter::event::{FindResult, ListResult, SetResult, ShowResult, VerifyResult}; 2 | use crate::reporter::Message; 3 | use crate::Event; 4 | 5 | #[derive(Clone, Debug, PartialEq, serde::Serialize)] 6 | #[serde(rename_all = "snake_case")] 7 | #[serde(tag = "subcommand_id")] 8 | pub enum SubcommandResult { 9 | Find(FindResult), 10 | List(ListResult), 11 | Set(SetResult), 12 | Show(ShowResult), 13 | Verify(VerifyResult), 14 | } 15 | 16 | impl From for Event { 17 | fn from(this: SubcommandResult) -> Self { 18 | Message::SubcommandResult(this).into() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/reporter/event/termination.rs: -------------------------------------------------------------------------------- 1 | use crate::reporter::event::Message; 2 | use crate::{CargoMSRVError, Event}; 3 | 4 | /// Represents a serializable reason why the program should terminate with a failure (a non-zero 5 | /// exit code). 6 | #[derive(Clone, Debug, PartialEq, serde::Serialize)] 7 | #[serde(rename_all = "snake_case")] 8 | pub struct TerminateWithFailure { 9 | // Whether the reason should be highlighted or not. 10 | #[serde(skip)] 11 | highlight: bool, 12 | reason: SerializableReason, 13 | } 14 | 15 | impl TerminateWithFailure { 16 | pub fn new(error: CargoMSRVError) -> Self { 17 | let highlight = matches!( 18 | error, 19 | CargoMSRVError::UnableToFindAnyGoodVersion { .. } | CargoMSRVError::InvalidMsrvSet(_) 20 | ); 21 | 22 | Self { 23 | highlight, 24 | reason: SerializableReason { 25 | description: format!("{}", &error), 26 | }, 27 | } 28 | } 29 | 30 | pub fn should_highlight(&self) -> bool { 31 | self.highlight 32 | } 33 | 34 | pub fn as_message(&self) -> &str { 35 | &self.reason.description 36 | } 37 | } 38 | 39 | impl From for Event { 40 | fn from(it: TerminateWithFailure) -> Self { 41 | Message::TerminateWithFailure(it).into() 42 | } 43 | } 44 | 45 | #[derive(Clone, Debug, PartialEq, serde::Serialize)] 46 | #[serde(rename_all = "snake_case")] 47 | struct SerializableReason { 48 | description: String, 49 | } 50 | 51 | #[cfg(test)] 52 | mod tests { 53 | use super::*; 54 | use crate::reporter::event::Message; 55 | use crate::reporter::TestReporterWrapper; 56 | use storyteller::EventReporter; 57 | 58 | #[test] 59 | fn reported_non_is_not_error_event() { 60 | let reporter = TestReporterWrapper::default(); 61 | 62 | let event = TerminateWithFailure::new(CargoMSRVError::Storyteller); 63 | 64 | reporter.get().report_event(event.clone()).unwrap(); 65 | let events = reporter.wait_for_events(); 66 | 67 | assert_eq!( 68 | &events, 69 | &[Event::unscoped(Message::TerminateWithFailure(event))] 70 | ); 71 | 72 | if let Message::TerminateWithFailure(msg) = &events[0].message { 73 | assert!(!msg.should_highlight()); 74 | assert_eq!(msg.as_message(), "Unable to print event output"); 75 | } 76 | } 77 | 78 | #[test] 79 | fn reported_non_is_error_event() { 80 | let reporter = TestReporterWrapper::default(); 81 | 82 | let event = TerminateWithFailure::new(CargoMSRVError::UnableToFindAnyGoodVersion { 83 | command: "cargo build --all".to_string(), 84 | }); 85 | 86 | reporter.get().report_event(event.clone()).unwrap(); 87 | let events = reporter.wait_for_events(); 88 | 89 | assert_eq!( 90 | &events, 91 | &[Event::unscoped(Message::TerminateWithFailure(event))] 92 | ); 93 | 94 | if let Message::TerminateWithFailure(msg) = &events[0].message { 95 | assert!(msg.should_highlight()); 96 | assert!(msg 97 | .as_message() 98 | .starts_with("Unable to find a Minimum Supported Rust Version (MSRV)")); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/reporter/event/types/find_result.rs: -------------------------------------------------------------------------------- 1 | use crate::context::SearchMethod; 2 | use crate::manifest::bare_version::BareVersion; 3 | use crate::reporter::event::subcommand_result::SubcommandResult; 4 | use crate::reporter::event::Message; 5 | use crate::typed_bool::{False, True}; 6 | use crate::{semver, Event}; 7 | 8 | #[derive(Clone, Debug, PartialEq, serde::Serialize)] 9 | #[serde(rename_all = "snake_case")] 10 | pub struct FindResult { 11 | #[serde(skip)] 12 | pub target: String, 13 | #[serde(skip)] 14 | pub minimum_version: BareVersion, 15 | #[serde(skip)] 16 | pub maximum_version: BareVersion, 17 | #[serde(skip)] 18 | pub search_method: SearchMethod, 19 | 20 | result: ResultDetails, 21 | } 22 | 23 | impl FindResult { 24 | pub fn new_msrv( 25 | version: semver::Version, 26 | target: impl Into, 27 | min: BareVersion, 28 | max: BareVersion, 29 | search_method: SearchMethod, 30 | ) -> Self { 31 | Self { 32 | target: target.into(), 33 | minimum_version: min, 34 | maximum_version: max, 35 | 36 | search_method, 37 | 38 | result: ResultDetails::Determined { 39 | version, 40 | success: True, 41 | }, 42 | } 43 | } 44 | 45 | pub fn none( 46 | target: impl Into, 47 | min: BareVersion, 48 | max: BareVersion, 49 | search_method: SearchMethod, 50 | ) -> Self { 51 | Self { 52 | target: target.into(), 53 | minimum_version: min, 54 | maximum_version: max, 55 | 56 | search_method, 57 | 58 | result: ResultDetails::Undetermined { success: False }, 59 | } 60 | } 61 | 62 | pub fn msrv(&self) -> Option<&semver::Version> { 63 | if let Self { 64 | result: ResultDetails::Determined { version, .. }, 65 | .. 66 | } = self 67 | { 68 | Some(version) 69 | } else { 70 | None 71 | } 72 | } 73 | } 74 | 75 | impl From for SubcommandResult { 76 | fn from(it: FindResult) -> Self { 77 | SubcommandResult::Find(it) 78 | } 79 | } 80 | 81 | impl From for Event { 82 | fn from(it: FindResult) -> Self { 83 | Message::SubcommandResult(it.into()).into() 84 | } 85 | } 86 | 87 | #[derive(Clone, Debug, PartialEq, serde::Serialize)] 88 | #[serde(rename_all = "snake_case")] 89 | #[serde(untagged)] 90 | enum ResultDetails { 91 | Determined { 92 | version: semver::Version, 93 | success: True, 94 | }, 95 | Undetermined { 96 | success: False, 97 | }, 98 | } 99 | 100 | #[cfg(test)] 101 | mod tests { 102 | use super::*; 103 | use crate::reporter::event::Message; 104 | use crate::reporter::TestReporterWrapper; 105 | use storyteller::EventReporter; 106 | 107 | #[test] 108 | fn reported_msrv_determined_event() { 109 | let reporter = TestReporterWrapper::default(); 110 | let version = semver::Version::new(1, 3, 0); 111 | let min = BareVersion::TwoComponents(1, 0); 112 | let max = BareVersion::ThreeComponents(1, 4, 0); 113 | 114 | let event = FindResult::new_msrv(version, "x", min, max, SearchMethod::Linear); 115 | reporter.get().report_event(event.clone()).unwrap(); 116 | 117 | let events = reporter.wait_for_events(); 118 | assert_eq!( 119 | &events, 120 | &[Event::unscoped(Message::SubcommandResult( 121 | SubcommandResult::Find(event) 122 | ))] 123 | ); 124 | 125 | let inner = &events[0].message; 126 | assert!( 127 | matches!(inner, Message::SubcommandResult(SubcommandResult::Find(res)) if res.msrv() == Some(&semver::Version::new(1, 3, 0))) 128 | ); 129 | } 130 | 131 | #[test] 132 | fn reported_msrv_undetermined_event() { 133 | let reporter = TestReporterWrapper::default(); 134 | let min = BareVersion::TwoComponents(1, 0); 135 | let max = BareVersion::ThreeComponents(1, 4, 0); 136 | 137 | let event = FindResult::none("x", min, max, SearchMethod::Linear); 138 | 139 | reporter.get().report_event(event.clone()).unwrap(); 140 | 141 | let events = reporter.wait_for_events(); 142 | assert_eq!( 143 | &events, 144 | &[Event::unscoped(Message::SubcommandResult( 145 | SubcommandResult::Find(event) 146 | ))] 147 | ); 148 | 149 | let inner = &events[0].message; 150 | assert!( 151 | matches!(inner, Message::SubcommandResult(SubcommandResult::Find(res)) if res.msrv().is_none()) 152 | ); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/reporter/event/types/list_result/direct_deps.rs: -------------------------------------------------------------------------------- 1 | use super::display_option; 2 | use super::display_vec; 3 | use super::metadata::*; 4 | use crate::context::list::DIRECT_DEPS; 5 | use crate::dependency_graph::DependencyGraph; 6 | use crate::reporter::formatting::table; 7 | use std::fmt; 8 | use std::fmt::Formatter; 9 | use tabled::Tabled; 10 | 11 | pub struct DirectDepsFormatter<'g> { 12 | graph: &'g DependencyGraph, 13 | } 14 | 15 | impl<'g> DirectDepsFormatter<'g> { 16 | pub fn new(graph: &'g DependencyGraph) -> Self { 17 | Self { graph } 18 | } 19 | } 20 | 21 | impl fmt::Display for DirectDepsFormatter<'_> { 22 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 23 | let values = dependencies(self.graph); 24 | 25 | f.write_fmt(format_args!("{}", table(values))) 26 | } 27 | } 28 | 29 | impl serde::Serialize for DirectDepsFormatter<'_> { 30 | fn serialize(&self, serializer: S) -> Result 31 | where 32 | S: serde::Serializer, 33 | { 34 | let serializable = SerializableValues { 35 | variant: DIRECT_DEPS, 36 | list: dependencies(self.graph).collect(), 37 | }; 38 | 39 | serializable.serialize(serializer) 40 | } 41 | } 42 | 43 | fn dependencies(graph: &DependencyGraph) -> impl Iterator { 44 | let package_id = graph.root_crate(); 45 | let root_index = graph.index()[package_id].into(); 46 | let neighbors = graph 47 | .packages() 48 | .neighbors_directed(root_index, petgraph::Direction::Outgoing); 49 | 50 | neighbors.map(move |dependency| { 51 | let package = &graph.packages()[dependency]; 52 | let msrv = package_msrv(package); 53 | 54 | Values { 55 | name: &package.name, 56 | version: &package.version, 57 | msrv: format_version(msrv.as_ref()), 58 | dependencies: package 59 | .dependencies 60 | .iter() 61 | .map(|d| d.name.clone()) 62 | .collect(), 63 | } 64 | }) 65 | } 66 | 67 | #[derive(Debug, serde::Serialize, Tabled)] 68 | struct Values<'a> { 69 | #[tabled(rename = "Name")] 70 | name: &'a str, 71 | #[tabled(rename = "Version")] 72 | version: &'a crate::semver::Version, 73 | #[tabled(rename = "MSRV", display_with = "display_option")] 74 | msrv: Option, 75 | #[tabled(rename = "Depends on", display_with = "display_vec")] 76 | dependencies: Vec, 77 | } 78 | 79 | #[derive(serde::Serialize)] 80 | struct SerializableValues<'v> { 81 | variant: &'static str, 82 | list: Vec>, 83 | } 84 | -------------------------------------------------------------------------------- /src/reporter/event/types/list_result/metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::manifest::bare_version::BareVersion; 2 | use crate::manifest::CargoManifest; 3 | use crate::semver; 4 | use cargo_metadata::{MetadataCommand, Package}; 5 | use std::convert::TryFrom; 6 | use std::path::Path; 7 | 8 | pub fn package_msrv(package: &Package) -> Option { 9 | package 10 | .rust_version 11 | .clone() 12 | .or_else(|| get_package_metadata_msrv(package)) 13 | .or_else(|| parse_manifest_workaround(package.manifest_path.as_path())) // todo: add last one as option to config 14 | } 15 | 16 | pub fn format_version(version: Option<&semver::Version>) -> Option { 17 | version.map(ToString::to_string) 18 | } 19 | 20 | // Workaround: manual parsing since current (1.56) version of cargo-metadata doesn't yet output the 21 | // rust-version 22 | pub fn parse_manifest_workaround>(path: P) -> Option { 23 | fn parse(path: &Path) -> Option { 24 | MetadataCommand::new() 25 | .manifest_path(path) 26 | .exec() 27 | .ok() 28 | .and_then(|metadata| CargoManifest::try_from(metadata).ok()) 29 | .and_then(|manifest| manifest.minimum_rust_version().map(ToOwned::to_owned)) 30 | .map(|version: BareVersion| version.to_semver_version()) 31 | } 32 | 33 | parse(path.as_ref()) 34 | } 35 | 36 | pub(in crate::reporter::event) fn get_package_metadata_msrv( 37 | package: &Package, 38 | ) -> Option { 39 | package 40 | .metadata 41 | .get("msrv") 42 | .and_then(|v| v.as_str()) 43 | .and_then(|v| semver::Version::parse(v).ok()) 44 | } 45 | -------------------------------------------------------------------------------- /src/reporter/event/types/list_result/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::dependency_graph::DependencyGraph; 2 | use crate::reporter::event::Message; 3 | use crate::Event; 4 | use std::borrow::Cow; 5 | use std::fmt; 6 | use std::fmt::Formatter; 7 | 8 | use crate::context::list::ListMsrvVariant; 9 | use crate::reporter::event::subcommand_result::SubcommandResult; 10 | use crate::reporter::event::types::list_result::ordered_by_msrv::OrderedByMsrvFormatter; 11 | use direct_deps::DirectDepsFormatter; 12 | 13 | mod direct_deps; 14 | mod metadata; 15 | mod ordered_by_msrv; 16 | 17 | #[derive(Clone, Debug, PartialEq, serde::Serialize)] 18 | #[serde(rename_all = "snake_case")] 19 | pub struct ListResult { 20 | result: ResultDetails, 21 | } 22 | 23 | impl ListResult { 24 | pub fn new(variant: ListMsrvVariant, graph: DependencyGraph) -> Self { 25 | Self { 26 | result: ResultDetails { variant, graph }, 27 | } 28 | } 29 | } 30 | 31 | impl fmt::Display for ListResult { 32 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 33 | f.write_fmt(format_args!("{}", self.result)) 34 | } 35 | } 36 | 37 | impl From for SubcommandResult { 38 | fn from(it: ListResult) -> Self { 39 | SubcommandResult::List(it) 40 | } 41 | } 42 | 43 | impl From for Event { 44 | fn from(it: ListResult) -> Self { 45 | Message::SubcommandResult(it.into()).into() 46 | } 47 | } 48 | 49 | #[derive(Clone, Debug, PartialEq)] 50 | struct ResultDetails { 51 | variant: ListMsrvVariant, 52 | graph: DependencyGraph, 53 | } 54 | 55 | impl fmt::Display for ResultDetails { 56 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 57 | match self.variant { 58 | ListMsrvVariant::DirectDeps => { 59 | f.write_fmt(format_args!("{}", DirectDepsFormatter::new(&self.graph))) 60 | } 61 | ListMsrvVariant::OrderedByMSRV => { 62 | f.write_fmt(format_args!("{}", OrderedByMsrvFormatter::new(&self.graph))) 63 | } 64 | } 65 | } 66 | } 67 | 68 | impl serde::Serialize for ResultDetails { 69 | fn serialize(&self, serializer: S) -> Result 70 | where 71 | S: serde::Serializer, 72 | { 73 | match self.variant { 74 | ListMsrvVariant::DirectDeps => { 75 | DirectDepsFormatter::new(&self.graph).serialize(serializer) 76 | } 77 | ListMsrvVariant::OrderedByMSRV => { 78 | OrderedByMsrvFormatter::new(&self.graph).serialize(serializer) 79 | } 80 | } 81 | } 82 | } 83 | 84 | fn display_option(option: &Option) -> Cow<'static, str> { 85 | match option { 86 | Some(s) => Cow::from(s.to_string()), 87 | None => Cow::from(""), 88 | } 89 | } 90 | 91 | fn display_vec(vec: &[String]) -> Cow<'static, str> { 92 | Cow::from(vec.join(", ")) 93 | } 94 | -------------------------------------------------------------------------------- /src/reporter/event/types/list_result/ordered_by_msrv.rs: -------------------------------------------------------------------------------- 1 | use super::display_option; 2 | use super::display_vec; 3 | use crate::context::list::ORDERED_BY_MSRV; 4 | use crate::dependency_graph::DependencyGraph; 5 | use crate::reporter::event::types::list_result::metadata::{format_version, package_msrv}; 6 | use crate::reporter::formatting::table; 7 | use crate::semver; 8 | use cargo_metadata::Package; 9 | use petgraph::visit::Bfs; 10 | use std::collections::BTreeMap; 11 | use std::fmt; 12 | use std::fmt::Formatter; 13 | use tabled::Tabled; 14 | 15 | pub struct OrderedByMsrvFormatter<'g> { 16 | graph: &'g DependencyGraph, 17 | } 18 | 19 | impl<'g> OrderedByMsrvFormatter<'g> { 20 | pub fn new(graph: &'g DependencyGraph) -> Self { 21 | Self { graph } 22 | } 23 | } 24 | 25 | impl fmt::Display for OrderedByMsrvFormatter<'_> { 26 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 27 | let values = dependencies(self.graph); 28 | 29 | f.write_fmt(format_args!("{}", table(values))) 30 | } 31 | } 32 | 33 | impl serde::Serialize for OrderedByMsrvFormatter<'_> { 34 | fn serialize(&self, serializer: S) -> Result 35 | where 36 | S: serde::Serializer, 37 | { 38 | let serializable = SerializableValues { 39 | variant: ORDERED_BY_MSRV, 40 | list: dependencies(self.graph).collect(), 41 | }; 42 | 43 | serializable.serialize(serializer) 44 | } 45 | } 46 | 47 | fn dependencies(graph: &DependencyGraph) -> impl Iterator + '_ { 48 | let package_id = &graph.root_crate(); 49 | let root_index = graph.index()[package_id].into(); 50 | let mut bfs = Bfs::new(graph.packages(), root_index); 51 | 52 | let mut version_map: BTreeMap, Vec<&Package>> = BTreeMap::new(); 53 | 54 | while let Some(nx) = bfs.next(graph.packages()) { 55 | let package = &graph.packages()[nx]; 56 | 57 | let msrv = package_msrv(package); 58 | 59 | version_map.entry(msrv).or_default().push(package); 60 | } 61 | 62 | version_map 63 | .into_iter() 64 | .rev() 65 | .map(|(version, packages)| Values { 66 | msrv: format_version(version.as_ref()), 67 | dependencies: packages.iter().map(|p| p.name.clone()).collect(), 68 | }) 69 | } 70 | 71 | #[derive(Debug, serde::Serialize, Tabled)] 72 | #[serde(rename_all = "snake_case")] 73 | struct Values { 74 | #[tabled(rename = "MSRV", display_with = "display_option")] 75 | msrv: Option, 76 | #[tabled(rename = "Dependency", display_with = "display_vec")] 77 | dependencies: Vec, 78 | } 79 | 80 | #[derive(serde::Serialize)] 81 | #[serde(rename_all = "snake_case")] 82 | struct SerializableValues { 83 | variant: &'static str, 84 | list: Vec, 85 | } 86 | -------------------------------------------------------------------------------- /src/reporter/event/types/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod find_result; 2 | pub mod list_result; 3 | pub mod set_result; 4 | pub mod show_result; 5 | pub mod verify_result; 6 | -------------------------------------------------------------------------------- /src/reporter/event/types/set_result.rs: -------------------------------------------------------------------------------- 1 | use crate::manifest::bare_version::BareVersion; 2 | use crate::reporter::event::subcommand_result::SubcommandResult; 3 | use crate::reporter::event::Message; 4 | use crate::Event; 5 | use camino::{Utf8Path, Utf8PathBuf}; 6 | 7 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 8 | #[serde(rename_all = "snake_case")] 9 | pub struct SetResult { 10 | result: ResultDetails, 11 | } 12 | 13 | impl SetResult { 14 | pub fn new(version: impl Into, manifest_path: Utf8PathBuf) -> Self { 15 | Self { 16 | result: ResultDetails { 17 | version: version.into(), 18 | manifest_path, 19 | }, 20 | } 21 | } 22 | 23 | pub fn version(&self) -> &BareVersion { 24 | &self.result.version 25 | } 26 | 27 | pub fn manifest_path(&self) -> &Utf8Path { 28 | &self.result.manifest_path 29 | } 30 | } 31 | 32 | impl From for SubcommandResult { 33 | fn from(it: SetResult) -> Self { 34 | Self::Set(it) 35 | } 36 | } 37 | 38 | impl From for Event { 39 | fn from(it: SetResult) -> Self { 40 | Message::SubcommandResult(it.into()).into() 41 | } 42 | } 43 | 44 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 45 | struct ResultDetails { 46 | version: BareVersion, 47 | manifest_path: Utf8PathBuf, 48 | } 49 | 50 | #[cfg(test)] 51 | mod tests { 52 | use super::*; 53 | use crate::reporter::event::Message; 54 | use crate::reporter::TestReporterWrapper; 55 | use storyteller::EventReporter; 56 | 57 | #[test] 58 | fn reported_event() { 59 | let reporter = TestReporterWrapper::default(); 60 | 61 | let version = BareVersion::TwoComponents(14, 10); 62 | let event = SetResult::new(version, Utf8Path::new("wave").to_path_buf()); 63 | 64 | reporter.get().report_event(event.clone()).unwrap(); 65 | let events = reporter.wait_for_events(); 66 | 67 | assert_eq!( 68 | &events, 69 | &[Event::unscoped(Message::SubcommandResult( 70 | SubcommandResult::Set(event) 71 | ))] 72 | ); 73 | 74 | if let Message::SubcommandResult(SubcommandResult::Set(msg)) = &events[0].message { 75 | assert_eq!(msg.version(), &BareVersion::TwoComponents(14, 10)); 76 | assert_eq!(&msg.manifest_path(), &Utf8Path::new("wave")); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/reporter/event/types/show_result.rs: -------------------------------------------------------------------------------- 1 | use crate::manifest::bare_version::BareVersion; 2 | use crate::reporter::event::subcommand_result::SubcommandResult; 3 | use crate::reporter::event::Message; 4 | use crate::Event; 5 | use camino::{Utf8Path, Utf8PathBuf}; 6 | 7 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 8 | #[serde(rename_all = "snake_case")] 9 | pub struct ShowResult { 10 | result: ResultDetails, 11 | } 12 | 13 | impl ShowResult { 14 | pub fn new(version: impl Into, manifest_path: Utf8PathBuf) -> Self { 15 | Self { 16 | result: ResultDetails { 17 | version: version.into(), 18 | manifest_path, 19 | }, 20 | } 21 | } 22 | 23 | pub fn version(&self) -> &BareVersion { 24 | &self.result.version 25 | } 26 | 27 | pub fn manifest_path(&self) -> &Utf8Path { 28 | &self.result.manifest_path 29 | } 30 | } 31 | 32 | impl From for SubcommandResult { 33 | fn from(it: ShowResult) -> Self { 34 | Self::Show(it) 35 | } 36 | } 37 | 38 | impl From for Event { 39 | fn from(it: ShowResult) -> Self { 40 | Message::SubcommandResult(it.into()).into() 41 | } 42 | } 43 | 44 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 45 | struct ResultDetails { 46 | version: BareVersion, 47 | manifest_path: Utf8PathBuf, 48 | } 49 | 50 | #[cfg(test)] 51 | mod tests { 52 | use super::*; 53 | use crate::reporter::event::Message; 54 | use crate::reporter::TestReporterWrapper; 55 | use storyteller::EventReporter; 56 | 57 | #[test] 58 | fn reported_event() { 59 | let reporter = TestReporterWrapper::default(); 60 | 61 | let version = BareVersion::ThreeComponents(1, 2, 3); 62 | let path = Utf8Path::new("lv").to_path_buf(); 63 | let event = ShowResult::new(version, path); 64 | 65 | reporter.get().report_event(event.clone()).unwrap(); 66 | 67 | let events = reporter.wait_for_events(); 68 | 69 | assert_eq!( 70 | &events, 71 | &[Event::unscoped(Message::SubcommandResult( 72 | SubcommandResult::Show(event) 73 | ))] 74 | ); 75 | 76 | if let Message::SubcommandResult(SubcommandResult::Show(msg)) = &events[0].message { 77 | assert_eq!(msg.version(), &BareVersion::ThreeComponents(1, 2, 3)); 78 | assert_eq!(&msg.manifest_path(), &Utf8Path::new("lv")); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/reporter/event/types/verify_result.rs: -------------------------------------------------------------------------------- 1 | use crate::reporter::event::shared::compatibility::Compatibility; 2 | use crate::reporter::event::subcommand_result::SubcommandResult; 3 | use crate::reporter::event::Message; 4 | use crate::rust::Toolchain; 5 | use crate::Event; 6 | 7 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 8 | #[serde(rename_all = "snake_case")] 9 | pub struct VerifyResult { 10 | pub result: Compatibility, 11 | } 12 | 13 | impl VerifyResult { 14 | pub fn compatible(toolchain: impl Into) -> Self { 15 | Self { 16 | result: Compatibility::compatible(toolchain), 17 | } 18 | } 19 | 20 | pub fn incompatible(toolchain: impl Into, error: Option) -> Self { 21 | Self { 22 | result: Compatibility::incompatible(toolchain, error), 23 | } 24 | } 25 | 26 | pub fn toolchain(&self) -> &Toolchain { 27 | self.result.toolchain() 28 | } 29 | 30 | pub fn is_compatible(&self) -> bool { 31 | self.result.is_compatible() 32 | } 33 | } 34 | 35 | impl From for SubcommandResult { 36 | fn from(it: VerifyResult) -> Self { 37 | Self::Verify(it) 38 | } 39 | } 40 | 41 | impl From for Event { 42 | fn from(it: VerifyResult) -> Self { 43 | Message::SubcommandResult(it.into()).into() 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use super::*; 50 | use crate::reporter::event::Message; 51 | use crate::reporter::TestReporterWrapper; 52 | use crate::{semver, Event}; 53 | use storyteller::EventReporter; 54 | 55 | #[test] 56 | fn reported_compatible_toolchain() { 57 | let reporter = TestReporterWrapper::default(); 58 | let event = VerifyResult::compatible(Toolchain::new( 59 | semver::Version::new(1, 2, 3), 60 | "test_target", 61 | &[], 62 | )); 63 | 64 | reporter.get().report_event(event.clone()).unwrap(); 65 | 66 | assert_eq!( 67 | reporter.wait_for_events(), 68 | vec![Event::unscoped(Message::SubcommandResult( 69 | SubcommandResult::Verify(event) 70 | )),] 71 | ); 72 | } 73 | 74 | #[yare::parameterized( 75 | none = { None }, 76 | some = { Some("whoo!".to_string()) }, 77 | )] 78 | fn reported_incompatible_toolchain(error_message: Option) { 79 | let reporter = TestReporterWrapper::default(); 80 | let event = VerifyResult::incompatible( 81 | Toolchain::new(semver::Version::new(1, 2, 3), "test_target", &[]), 82 | error_message, 83 | ); 84 | 85 | reporter.get().report_event(event.clone()).unwrap(); 86 | 87 | assert_eq!( 88 | reporter.wait_for_events(), 89 | vec![Event::unscoped(Message::SubcommandResult( 90 | SubcommandResult::Verify(event) 91 | ))] 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/reporter/event/unable_to_confirm_valid_release_version.rs: -------------------------------------------------------------------------------- 1 | use crate::reporter::{Event, Message}; 2 | 3 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] 4 | #[serde(rename_all = "snake_case")] 5 | pub struct UnableToConfirmValidReleaseVersion {} 6 | 7 | impl From for Event { 8 | fn from(it: UnableToConfirmValidReleaseVersion) -> Self { 9 | Message::UnableToConfirmValidReleaseVersion(it).into() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/reporter/formatting.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | use tabled::settings::{Margin, Style}; 3 | use tabled::{Table, Tabled}; 4 | 5 | static TERM_WIDTH: OnceLock = OnceLock::new(); 6 | 7 | static TABLE_CORRECTION: usize = 4; 8 | 9 | pub fn term_width() -> usize { 10 | let minimum = 40; 11 | 12 | let width = *TERM_WIDTH.get_or_init(|| { 13 | terminal_size::terminal_size() 14 | .map(|(w, _)| w.0) 15 | .unwrap_or(80) as usize 16 | }); 17 | 18 | width.checked_sub(TABLE_CORRECTION).unwrap_or(minimum) 19 | } 20 | 21 | // NB: This is only a macro because tabled uses lots of generics, which 22 | // aren't fun to type manually. 23 | #[macro_export] 24 | macro_rules! table_settings { 25 | () => {{ 26 | let width = $crate::reporter::formatting::term_width(); 27 | 28 | tabled::settings::Settings::default() 29 | .with( 30 | tabled::settings::Width::wrap(width) 31 | .priority(tabled::settings::peaker::PriorityMax), 32 | ) 33 | .with(tabled::settings::Width::increase(width)) 34 | }}; 35 | } 36 | 37 | pub fn table(iter: impl IntoIterator) -> Table { 38 | Table::new(iter) 39 | .with(Style::modern_rounded()) 40 | .with(table_settings!()) 41 | .with(Margin::new(2, 0, 1, 0)) 42 | .to_owned() 43 | } 44 | -------------------------------------------------------------------------------- /src/reporter/testing.rs: -------------------------------------------------------------------------------- 1 | use crate::reporter::event::{ScopeCounter, SupplyScopeGenerator, TestScopeGenerator}; 2 | use crate::reporter::ui::TestingHandler; 3 | use crate::{Event, Reporter}; 4 | use std::sync::Arc; 5 | use storyteller::{ 6 | event_channel, ChannelEventListener, ChannelFinalizeHandler, ChannelReporter, EventListener, 7 | EventReporter, EventReporterError, FinishProcessing, 8 | }; 9 | 10 | pub struct TestReporterWrapper { 11 | reporter: TestReporter, 12 | #[allow(unused)] 13 | listener: ChannelEventListener, 14 | handler: Arc, 15 | finalizer: ChannelFinalizeHandler, 16 | } 17 | 18 | impl Default for TestReporterWrapper { 19 | fn default() -> Self { 20 | let (sender, receiver) = event_channel::(); 21 | 22 | let reporter = TestReporter::new(ChannelReporter::new(sender)); 23 | let listener = ChannelEventListener::new(receiver); 24 | let handler = Arc::new(TestingHandler::default()); 25 | let finalizer = listener.run_handler(handler.clone()); 26 | 27 | Self { 28 | reporter, 29 | listener, 30 | handler, 31 | finalizer, 32 | } 33 | } 34 | } 35 | 36 | impl TestReporterWrapper { 37 | pub fn events(&self) -> Vec { 38 | self.handler 39 | .clone() 40 | .events() 41 | .iter() 42 | .cloned() 43 | .collect::>() 44 | } 45 | 46 | pub fn wait_for_events(self) -> Vec { 47 | self.reporter.disconnect().unwrap(); 48 | self.finalizer.finish_processing().unwrap(); 49 | 50 | let handler = Arc::try_unwrap(self.handler).unwrap(); 51 | 52 | handler.unwrap_events() 53 | } 54 | 55 | pub fn get(&self) -> &impl Reporter { 56 | &self.reporter 57 | } 58 | } 59 | 60 | struct TestReporter { 61 | inner: ChannelReporter, 62 | scope_generator: ScopeCounter, 63 | } 64 | 65 | impl TestReporter { 66 | pub fn new(reporter: ChannelReporter) -> Self { 67 | Self { 68 | inner: reporter, 69 | scope_generator: ScopeCounter::new(), 70 | } 71 | } 72 | } 73 | 74 | impl EventReporter for TestReporter { 75 | type Event = Event; 76 | type Err = storyteller::EventReporterError; 77 | 78 | fn report_event(&self, event: impl Into) -> Result<(), Self::Err> { 79 | self.inner.report_event(event) 80 | } 81 | 82 | fn disconnect(self) -> Result<(), Self::Err> { 83 | self.inner.disconnect() 84 | } 85 | } 86 | 87 | impl SupplyScopeGenerator for TestReporter { 88 | type ScopeGen = ScopeCounter; 89 | 90 | fn scope_generator(&self) -> &Self::ScopeGen { 91 | &self.scope_generator 92 | } 93 | } 94 | 95 | /// A test reporter which does absolutely nothing. 96 | #[derive(Default)] 97 | pub struct FakeTestReporter(TestScopeGenerator); 98 | 99 | impl EventReporter for FakeTestReporter { 100 | type Event = Event; 101 | type Err = EventReporterError; 102 | 103 | fn report_event(&self, _event: impl Into) -> Result<(), Self::Err> { 104 | Ok(()) 105 | } 106 | 107 | fn disconnect(self) -> Result<(), Self::Err> { 108 | Ok(()) 109 | } 110 | } 111 | 112 | impl SupplyScopeGenerator for FakeTestReporter { 113 | type ScopeGen = TestScopeGenerator; 114 | 115 | fn scope_generator(&self) -> &Self::ScopeGen { 116 | &self.0 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/reporter/ui/discard_output.rs: -------------------------------------------------------------------------------- 1 | use crate::Event; 2 | use storyteller::EventHandler; 3 | 4 | pub struct DiscardOutputHandler; 5 | 6 | impl EventHandler for DiscardOutputHandler { 7 | type Event = Event; 8 | 9 | fn handle(&self, _event: Self::Event) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/reporter/ui/json/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::io::SendWriter; 2 | use crate::Event; 3 | use std::io; 4 | use std::io::Stderr; 5 | #[cfg(test)] 6 | use std::sync::MutexGuard; 7 | use std::sync::{Arc, Mutex}; 8 | use storyteller::EventHandler; 9 | 10 | #[cfg(test)] 11 | mod test_find; 12 | 13 | #[cfg(test)] 14 | mod test_set; 15 | 16 | #[cfg(test)] 17 | mod test_show; 18 | 19 | #[cfg(test)] 20 | mod test_verify; 21 | 22 | pub struct JsonHandler { 23 | writer: Arc>, 24 | } 25 | 26 | impl JsonHandler { 27 | const LOCK_FAILURE_MSG: &'static str = 28 | "{ \"panic\": true, \"cause\": \"Unable to lock writer for JsonHandle\", \"experimental\": true }"; 29 | const SERIALIZE_FAILURE_MSG: &'static str = 30 | "{ \"panic\": true, \"cause\": \"Unable to serialize event for JsonHandle\", \"experimental\": true }"; 31 | const WRITE_FAILURE_MSG: &'static str = 32 | "{ \"panic\": true, \"cause\": \"Unable to write serialized event for JsonHandle\", \"experimental\": true }"; 33 | } 34 | 35 | #[cfg(test)] 36 | impl JsonHandler { 37 | pub fn new(writer: W) -> Self { 38 | Self { 39 | writer: Arc::new(Mutex::new(writer)), 40 | } 41 | } 42 | 43 | pub fn inner_writer(&self) -> MutexGuard<'_, W> { 44 | self.writer.lock().expect("Unable to unlock writer") 45 | } 46 | } 47 | 48 | impl JsonHandler { 49 | pub fn stderr() -> Self { 50 | Self { 51 | writer: Arc::new(Mutex::new(io::stderr())), 52 | } 53 | } 54 | } 55 | 56 | impl EventHandler for JsonHandler { 57 | type Event = Event; 58 | 59 | fn handle(&self, event: Self::Event) { 60 | let mut w = self.writer.lock().expect(Self::LOCK_FAILURE_MSG); 61 | let serialized_event = serde_json::to_string(&event).expect(Self::SERIALIZE_FAILURE_MSG); 62 | 63 | writeln!(&mut w, "{}", &serialized_event).expect(Self::WRITE_FAILURE_MSG); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/reporter/ui/json/test_find.rs: -------------------------------------------------------------------------------- 1 | use crate::context::SearchMethod; 2 | use crate::manifest::bare_version::BareVersion; 3 | use crate::reporter::event::FindResult; 4 | use crate::reporter::JsonHandler; 5 | use crate::semver; 6 | use storyteller::EventHandler; 7 | 8 | #[test] 9 | fn compatible_handler() { 10 | let version = semver::Version::new(1, 2, 3); 11 | 12 | let event = FindResult::new_msrv( 13 | version, 14 | "x", 15 | BareVersion::TwoComponents(1, 0), 16 | BareVersion::TwoComponents(1, 10), 17 | SearchMethod::Linear, 18 | ); 19 | 20 | let writer = Vec::new(); 21 | let handler = JsonHandler::new(writer); 22 | handler.handle(event.into()); 23 | 24 | let buffer = handler.inner_writer(); 25 | let actual: serde_json::Value = serde_json::from_slice(buffer.as_slice()).unwrap(); 26 | 27 | let expected = serde_json::json!({ 28 | "type": "subcommand_result", 29 | "subcommand_id": "find", 30 | "result": { 31 | "success" : true, 32 | "version" : "1.2.3", 33 | }, 34 | }); 35 | 36 | assert_eq!(actual, expected); 37 | } 38 | 39 | #[test] 40 | fn incompatible_handler() { 41 | let event = FindResult::none( 42 | "x", 43 | BareVersion::TwoComponents(1, 0), 44 | BareVersion::TwoComponents(1, 10), 45 | SearchMethod::Bisect, 46 | ); 47 | 48 | let writer = Vec::new(); 49 | let handler = JsonHandler::new(writer); 50 | handler.handle(event.into()); 51 | 52 | let buffer = handler.inner_writer(); 53 | let actual: serde_json::Value = serde_json::from_slice(buffer.as_slice()).unwrap(); 54 | 55 | let expected = serde_json::json!({ 56 | "type": "subcommand_result", 57 | "subcommand_id": "find", 58 | "result": { 59 | "success" : false, 60 | }, 61 | }); 62 | 63 | assert_eq!(actual, expected); 64 | } 65 | 66 | #[test] 67 | fn compatible() { 68 | let version = semver::Version::new(1, 2, 3); 69 | let event = FindResult::new_msrv( 70 | version, 71 | "x", 72 | BareVersion::TwoComponents(1, 0), 73 | BareVersion::TwoComponents(1, 10), 74 | SearchMethod::Bisect, 75 | ); 76 | 77 | let expected = serde_json::json!({ 78 | "result": { 79 | "success" : true, 80 | "version" : "1.2.3", 81 | }, 82 | }); 83 | 84 | let actual = serde_json::to_value(event).unwrap(); 85 | assert_eq!(actual, expected); 86 | } 87 | 88 | #[test] 89 | fn incompatible() { 90 | let event = FindResult::none( 91 | "x", 92 | BareVersion::TwoComponents(1, 0), 93 | BareVersion::TwoComponents(1, 10), 94 | SearchMethod::Bisect, 95 | ); 96 | 97 | let expected = serde_json::json!({ 98 | "result": { 99 | "success" : false, 100 | }, 101 | }); 102 | 103 | let actual = serde_json::to_value(event).unwrap(); 104 | assert_eq!(actual, expected); 105 | } 106 | -------------------------------------------------------------------------------- /src/reporter/ui/json/test_set.rs: -------------------------------------------------------------------------------- 1 | use crate::manifest::bare_version::BareVersion; 2 | use crate::reporter::event::SetResult; 3 | use crate::reporter::JsonHandler; 4 | use camino::Utf8Path; 5 | use storyteller::EventHandler; 6 | 7 | #[test] 8 | fn handler() { 9 | let event = SetResult::new( 10 | BareVersion::TwoComponents(1, 10), 11 | Utf8Path::new("/hello/world").to_path_buf(), 12 | ); 13 | 14 | let writer = Vec::new(); 15 | let handler = JsonHandler::new(writer); 16 | 17 | handler.handle(event.into()); 18 | 19 | let buffer = handler.inner_writer(); 20 | let actual: serde_json::Value = serde_json::from_slice(buffer.as_slice()).unwrap(); 21 | 22 | let expected = serde_json::json!({ 23 | "type": "subcommand_result", 24 | "subcommand_id": "set", 25 | "result": { 26 | "version": "1.10", 27 | "manifest_path": "/hello/world" 28 | } 29 | }); 30 | 31 | assert_eq!(actual, expected); 32 | } 33 | 34 | #[test] 35 | fn event() { 36 | let event = SetResult::new( 37 | BareVersion::TwoComponents(1, 10), 38 | Utf8Path::new("/hello/world").to_path_buf(), 39 | ); 40 | 41 | let expected = serde_json::json!({ 42 | "result": { 43 | "version": "1.10", 44 | "manifest_path": "/hello/world" 45 | } 46 | }); 47 | 48 | let actual = serde_json::to_value(event).unwrap(); 49 | assert_eq!(actual, expected); 50 | } 51 | -------------------------------------------------------------------------------- /src/reporter/ui/json/test_show.rs: -------------------------------------------------------------------------------- 1 | use crate::manifest::bare_version::BareVersion; 2 | use crate::reporter::event::ShowResult; 3 | use crate::reporter::JsonHandler; 4 | use camino::Utf8Path; 5 | use storyteller::EventHandler; 6 | 7 | #[test] 8 | fn handler() { 9 | let event = ShowResult::new( 10 | BareVersion::ThreeComponents(1, 2, 3), 11 | Utf8Path::new("/hello/world").to_path_buf(), 12 | ); 13 | 14 | let writer = Vec::new(); 15 | let handler = JsonHandler::new(writer); 16 | handler.handle(event.into()); 17 | 18 | let buffer = handler.inner_writer(); 19 | let actual: serde_json::Value = serde_json::from_slice(buffer.as_slice()).unwrap(); 20 | 21 | let expected = serde_json::json!({ 22 | "type": "subcommand_result", 23 | "subcommand_id": "show", 24 | "result": { 25 | "version": "1.2.3", 26 | "manifest_path": "/hello/world" 27 | } 28 | }); 29 | 30 | assert_eq!(actual, expected); 31 | } 32 | 33 | #[test] 34 | fn event() { 35 | let event = ShowResult::new( 36 | BareVersion::ThreeComponents(1, 10, 100), 37 | Utf8Path::new("/hello/world").to_path_buf(), 38 | ); 39 | 40 | let expected = serde_json::json!({ 41 | "result": { 42 | "version": "1.10.100", 43 | "manifest_path": "/hello/world" 44 | } 45 | }); 46 | 47 | let actual = serde_json::to_value(event).unwrap(); 48 | assert_eq!(actual, expected); 49 | } 50 | -------------------------------------------------------------------------------- /src/reporter/ui/mod.rs: -------------------------------------------------------------------------------- 1 | mod discard_output; 2 | mod human; 3 | mod json; 4 | mod minimal; 5 | 6 | #[cfg(test)] 7 | mod testing; 8 | 9 | pub use discard_output::DiscardOutputHandler; 10 | pub use human::HumanProgressHandler; 11 | pub use json::JsonHandler; 12 | pub use minimal::MinimalOutputHandler; 13 | 14 | #[cfg(test)] 15 | pub use testing::TestingHandler; 16 | -------------------------------------------------------------------------------- /src/reporter/ui/testing.rs: -------------------------------------------------------------------------------- 1 | use crate::Event; 2 | use std::sync::{Arc, Mutex, MutexGuard}; 3 | use storyteller::EventHandler; 4 | 5 | #[derive(Debug)] 6 | pub struct TestingHandler { 7 | event_log: Arc>>, 8 | } 9 | 10 | impl Default for TestingHandler { 11 | fn default() -> Self { 12 | Self { 13 | event_log: Arc::new(Mutex::new(Vec::new())), 14 | } 15 | } 16 | } 17 | 18 | impl Clone for TestingHandler { 19 | fn clone(&self) -> Self { 20 | Self { 21 | event_log: self.event_log.clone(), 22 | } 23 | } 24 | } 25 | 26 | impl TestingHandler { 27 | pub fn events(&self) -> MutexGuard<'_, Vec> { 28 | self.event_log.lock().unwrap() 29 | } 30 | 31 | pub fn unwrap_events(self) -> Vec { 32 | let mutex = Arc::try_unwrap(self.event_log).unwrap(); 33 | mutex.into_inner().unwrap() 34 | } 35 | } 36 | 37 | impl EventHandler for TestingHandler { 38 | type Event = Event; 39 | 40 | fn handle(&self, event: Self::Event) { 41 | self.event_log.lock().unwrap().push(event); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/rust/default_target.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{CargoMSRVError, TResult}; 2 | use crate::external_command::rustup_command::RustupCommand; 3 | 4 | /// Uses the `.rustup/settings.toml` file to determine the default target (aka the 5 | /// `default_host_triple`) if not set by a user. 6 | pub fn default_target() -> TResult { 7 | let output = RustupCommand::new().with_stdout().show()?; 8 | 9 | let stdout = output.stdout(); 10 | 11 | stdout 12 | .lines() 13 | .next() 14 | .ok_or(CargoMSRVError::DefaultHostTripleNotFound) 15 | .and_then(|line| { 16 | line.split_ascii_whitespace() 17 | .nth(2) 18 | .ok_or(CargoMSRVError::DefaultHostTripleNotFound) 19 | .map(String::from) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/rust/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod default_target; 2 | mod release; 3 | pub mod release_index; 4 | pub(crate) mod releases_filter; 5 | pub(crate) mod setup_toolchain; 6 | mod toolchain; 7 | 8 | pub use release::RustRelease; 9 | pub use toolchain::Toolchain; 10 | -------------------------------------------------------------------------------- /src/rust/release.rs: -------------------------------------------------------------------------------- 1 | use crate::rust::Toolchain; 2 | 3 | /// A `cargo-msrv` Rust release. 4 | /// 5 | // FIXME: The next rust-releases version will also contain target information. 6 | // FIXME: There should be a difference between the available releases, and the requested set of (toolchain, target, component)'s. 7 | // Only the "Release" part of this struct has been sourced from some Rust release channel, the target and components have been added later 8 | // but are really items which we want to have; they may not exist. This can be a bit confusing when running cargo msrv with with debug logs on. 9 | #[derive(Clone, Debug, PartialEq)] 10 | pub struct RustRelease { 11 | release: rust_releases::Release, 12 | target: &'static str, 13 | components: &'static [&'static str], 14 | } 15 | 16 | impl RustRelease { 17 | pub fn new( 18 | release: rust_releases::Release, 19 | target: &'static str, 20 | components: &'static [&'static str], 21 | ) -> Self { 22 | Self { 23 | release, 24 | target, 25 | components, 26 | } 27 | } 28 | 29 | /// Get the [`Toolchain`] for the given Rust release. 30 | pub fn to_toolchain_spec(&self) -> Toolchain { 31 | let version = self.release.version(); 32 | Toolchain::new(version.clone(), self.target, self.components) 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use crate::rust::RustRelease; 39 | use crate::rust::Toolchain; 40 | use rust_releases::semver; 41 | 42 | #[test] 43 | fn spec() { 44 | let version = semver::Version::new(1, 2, 3); 45 | let rust_release = RustRelease::new( 46 | rust_releases::Release::new_stable(version.clone()), 47 | "x", 48 | &[], 49 | ); 50 | let spec = rust_release.to_toolchain_spec(); 51 | 52 | let expected = Toolchain::new(version, "x", &[]); 53 | assert_eq!(spec, expected); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/rust/release_index.rs: -------------------------------------------------------------------------------- 1 | use crate::context::ReleaseSource; 2 | use crate::error::TResult; 3 | use crate::reporter::event::FetchIndex; 4 | use crate::reporter::Reporter; 5 | #[cfg(feature = "rust-releases-dist-source")] 6 | use rust_releases::RustDist; 7 | use rust_releases::{Channel, FetchResources, ReleaseIndex, RustChangelog, Source}; 8 | 9 | pub fn fetch_index( 10 | reporter: &impl Reporter, 11 | release_source: ReleaseSource, 12 | ) -> TResult { 13 | reporter.run_scoped_event(FetchIndex::new(release_source), || { 14 | let source: &'static str = release_source.into(); 15 | info!(source = source, "fetching index"); 16 | 17 | let index = match release_source { 18 | ReleaseSource::RustChangelog => { 19 | RustChangelog::fetch_channel(Channel::Stable)?.build_index()? 20 | } 21 | #[cfg(feature = "rust-releases-dist-source")] 22 | ReleaseSource::RustDist => RustDist::fetch_channel(Channel::Stable)?.build_index()?, 23 | }; 24 | 25 | Ok(index) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /src/rust/toolchain.rs: -------------------------------------------------------------------------------- 1 | use rust_releases::semver; 2 | use std::sync::OnceLock; 3 | 4 | #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize)] 5 | #[serde(rename_all = "snake_case")] 6 | pub struct Toolchain { 7 | version: semver::Version, 8 | target: &'static str, 9 | components: &'static [&'static str], 10 | #[serde(skip)] 11 | spec: OnceLock, 12 | } 13 | 14 | impl Toolchain { 15 | pub fn new( 16 | version: semver::Version, 17 | target: &'static str, 18 | components: &'static [&'static str], 19 | ) -> Self { 20 | Self { 21 | version, 22 | target, 23 | components, 24 | spec: OnceLock::new(), 25 | } 26 | } 27 | 28 | pub fn spec(&self) -> &str { 29 | self.spec 30 | .get_or_init(|| make_toolchain_spec(&self.version, self.target)) 31 | } 32 | 33 | pub fn version(&self) -> &semver::Version { 34 | &self.version 35 | } 36 | 37 | pub fn components(&self) -> &'static [&'static str] { 38 | self.components 39 | } 40 | 41 | pub fn target(&self) -> &str { 42 | self.target 43 | } 44 | } 45 | 46 | impl std::fmt::Display for Toolchain { 47 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 48 | f.write_fmt(format_args!("{}", self.spec())) 49 | } 50 | } 51 | 52 | fn make_toolchain_spec(version: &semver::Version, target: &str) -> String { 53 | format!("{}-{}", version, target) 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests_toolchain_spec { 58 | use super::*; 59 | 60 | #[test] 61 | fn get_spec() { 62 | let version = semver::Version::new(1, 2, 3); 63 | let toolchain = Toolchain::new(version, "x", &[]); 64 | 65 | assert_eq!(toolchain.spec(), "1.2.3-x"); 66 | } 67 | 68 | #[test] 69 | fn get_version() { 70 | let version = semver::Version::new(1, 2, 3); 71 | let toolchain = Toolchain::new(version, "x", &[]); 72 | 73 | assert_eq!(toolchain.version(), &semver::Version::new(1, 2, 3)); 74 | } 75 | 76 | #[test] 77 | fn get_target() { 78 | let version = semver::Version::new(1, 2, 3); 79 | let toolchain = Toolchain::new(version, "x", &[]); 80 | 81 | assert_eq!(toolchain.target(), "x"); 82 | } 83 | 84 | #[test] 85 | fn get_components() { 86 | let version = semver::Version::new(1, 2, 3); 87 | let toolchain = Toolchain::new(version, "x", &["hello", "chris!"]); 88 | 89 | assert_eq!(toolchain.components(), &["hello", "chris!"]); 90 | } 91 | } 92 | 93 | #[cfg(test)] 94 | mod tests_make_toolchain_spec { 95 | use super::*; 96 | 97 | #[test] 98 | fn display() { 99 | let version = semver::Version::new(1, 2, 3); 100 | let toolchain = Toolchain::new(version, "y", &[]); 101 | 102 | let spec = format!("{}", toolchain); 103 | 104 | assert_eq!(spec, "1.2.3-y"); 105 | } 106 | 107 | #[test] 108 | fn make_spec() { 109 | let version = semver::Version::new(1, 2, 3); 110 | let spec = make_toolchain_spec(&version, "y"); 111 | 112 | assert_eq!(spec, "1.2.3-y"); 113 | } 114 | 115 | #[test] 116 | fn display_ignores_components() { 117 | let version = semver::Version::new(1, 2, 3); 118 | let toolchain = Toolchain::new(version, "y", &["to", "be", "ignored"]); 119 | 120 | let spec = format!("{}", toolchain); 121 | 122 | assert_eq!(spec, "1.2.3-y"); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/search_method/mod.rs: -------------------------------------------------------------------------------- 1 | pub use {bisect::Bisect, linear::Linear}; 2 | 3 | use crate::msrv::MinimumSupportedRustVersion; 4 | use crate::reporter::Reporter; 5 | use crate::rust::RustRelease; 6 | use crate::TResult; 7 | 8 | /// Use a bisection method to find the MSRV. By using a binary search, we halve our search space each 9 | /// step, making this an efficient search function. 10 | pub mod bisect; 11 | /// Find the MSRV by stepping through the most-recent to least-recent version, one-by-one. This is 12 | /// not very efficient, but is useful as a baseline, or if you're certain the MSRV is very close to 13 | /// the head. 14 | pub mod linear; 15 | 16 | pub trait FindMinimalSupportedRustVersion { 17 | /// Method to find the minimum capable toolchain. 18 | /// 19 | /// The search space must be ordered from most to least recent. 20 | /// 21 | /// This method returns TODO desc error variants, success case 22 | fn find_toolchain( 23 | &self, 24 | search_space: &[RustRelease], 25 | reporter: &impl Reporter, 26 | ) -> TResult; 27 | } 28 | -------------------------------------------------------------------------------- /src/sub_command/list.rs: -------------------------------------------------------------------------------- 1 | use crate::context::ListContext; 2 | use crate::dependency_graph::resolver::{CargoMetadataResolver, DependencyResolver}; 3 | use crate::error::TResult; 4 | use crate::reporter::event::ListResult; 5 | use crate::reporter::Reporter; 6 | use crate::SubCommand; 7 | 8 | #[derive(Default)] 9 | pub struct List; 10 | 11 | impl SubCommand for List { 12 | type Context = ListContext; 13 | type Output = (); 14 | 15 | fn run(&self, ctx: &Self::Context, reporter: &impl Reporter) -> TResult { 16 | list_msrv(ctx, reporter) 17 | } 18 | } 19 | 20 | fn list_msrv(ctx: &ListContext, reporter: &impl Reporter) -> TResult<()> { 21 | let resolver = CargoMetadataResolver::from_manifest_path(&ctx.environment.manifest()); 22 | let graph = resolver.resolve()?; 23 | let variant = ctx.variant; 24 | 25 | reporter.report_event(ListResult::new(variant, graph))?; 26 | 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /src/sub_command/mod.rs: -------------------------------------------------------------------------------- 1 | /// Find the MSRV of a Rust package. 2 | /// 3 | /// # Example (CLI) 4 | /// 5 | /// `cargo msrv` 6 | pub use find::Find; 7 | 8 | /// List the MSRV's of libraries you depend on. 9 | /// 10 | /// # Example (CLI) 11 | /// 12 | /// `cargo msrv list` 13 | pub use list::List; 14 | 15 | /// Check whether the MSRV of a crate is valid as an MSRV. 16 | /// 17 | /// # Use case 18 | /// 19 | /// - Integrate into a continuous integration (CI) pipeline, to check that your 20 | /// crate fulfills its promised minimally supported Rust version. 21 | /// 22 | /// # Example (CLI) 23 | /// 24 | /// `cargo msrv verify` 25 | pub use verify::Verify; 26 | 27 | /// Write a given MSRV to a Cargo manifest 28 | /// 29 | /// # Example (CLI) 30 | /// 31 | /// `cargo msrv set 1.50` 32 | pub use set::Set; 33 | 34 | /// Show the MSRV present in the Cargo manifest 35 | /// 36 | /// # Example (CLI) 37 | /// 38 | /// `cargo msrv show` 39 | pub use show::Show; 40 | 41 | use crate::reporter::Reporter; 42 | use crate::TResult; 43 | 44 | pub mod find; 45 | pub mod list; 46 | pub mod set; 47 | pub mod show; 48 | pub mod verify; 49 | 50 | /// A sub-command of `cargo-msrv`. 51 | /// 52 | /// It takes a set of inputs, from the `config`, and reports it's results via the `reporter`. 53 | pub trait SubCommand { 54 | type Context; 55 | type Output; 56 | 57 | /// Run the sub-command 58 | fn run(&self, ctx: &Self::Context, reporter: &impl Reporter) -> TResult; 59 | } 60 | -------------------------------------------------------------------------------- /src/sub_command/show.rs: -------------------------------------------------------------------------------- 1 | use camino::Utf8PathBuf; 2 | use cargo_metadata::MetadataCommand; 3 | use std::convert::TryFrom; 4 | 5 | use crate::context::ShowContext; 6 | use crate::error::TResult; 7 | 8 | use crate::manifest::CargoManifest; 9 | use crate::reporter::event::ShowResult; 10 | use crate::reporter::Reporter; 11 | use crate::SubCommand; 12 | 13 | #[derive(Default)] 14 | pub struct Show; 15 | 16 | impl SubCommand for Show { 17 | type Context = ShowContext; 18 | type Output = (); 19 | 20 | fn run(&self, ctx: &Self::Context, reporter: &impl Reporter) -> TResult { 21 | show_msrv(ctx, reporter) 22 | } 23 | } 24 | 25 | fn show_msrv(ctx: &ShowContext, reporter: &impl Reporter) -> TResult<()> { 26 | // TODO: Add support for workspaces, but take care to also still support raw `rustup run`. 27 | 28 | let cargo_toml = ctx.environment.manifest(); 29 | 30 | let metadata = MetadataCommand::new().manifest_path(&cargo_toml).exec()?; 31 | let manifest = CargoManifest::try_from(metadata)?; 32 | 33 | let msrv = manifest 34 | .minimum_rust_version() 35 | .ok_or_else(|| Error::NoMSRVInCargoManifest(cargo_toml.to_path_buf()))?; 36 | 37 | reporter.report_event(ShowResult::new(msrv.clone(), cargo_toml.clone()))?; 38 | 39 | Ok(()) 40 | } 41 | 42 | #[derive(Debug, thiserror::Error)] 43 | pub enum Error { 44 | #[error("MSRV was not specified in Cargo manifest at '{0}'")] 45 | NoMSRVInCargoManifest(Utf8PathBuf), 46 | } 47 | -------------------------------------------------------------------------------- /src/typed_bool.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserializer; 2 | use std::fmt; 3 | 4 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 5 | pub struct True; 6 | 7 | impl serde::Serialize for True { 8 | fn serialize(&self, serializer: S) -> Result 9 | where 10 | S: serde::Serializer, 11 | { 12 | serializer.serialize_bool(true) 13 | } 14 | } 15 | 16 | impl<'de> serde::Deserialize<'de> for True { 17 | fn deserialize(deserializer: D) -> Result 18 | where 19 | D: Deserializer<'de>, 20 | { 21 | deserializer.deserialize_bool(TrueVisitor) 22 | } 23 | } 24 | 25 | struct TrueVisitor; 26 | 27 | impl serde::de::Visitor<'_> for TrueVisitor { 28 | type Value = True; 29 | 30 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 31 | formatter.write_str("true") 32 | } 33 | 34 | fn visit_bool(self, value: bool) -> Result 35 | where 36 | E: serde::de::Error, 37 | { 38 | if value { 39 | Ok(True) 40 | } else { 41 | Err(E::custom(format!("Value '{}' must be 'true'", value))) 42 | } 43 | } 44 | } 45 | 46 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 47 | pub struct False; 48 | 49 | impl serde::Serialize for False { 50 | fn serialize(&self, serializer: S) -> Result 51 | where 52 | S: serde::Serializer, 53 | { 54 | serializer.serialize_bool(false) 55 | } 56 | } 57 | 58 | impl<'de> serde::Deserialize<'de> for False { 59 | fn deserialize(deserializer: D) -> Result 60 | where 61 | D: Deserializer<'de>, 62 | { 63 | deserializer.deserialize_bool(FalseVisitor) 64 | } 65 | } 66 | 67 | struct FalseVisitor; 68 | 69 | impl serde::de::Visitor<'_> for FalseVisitor { 70 | type Value = False; 71 | 72 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 73 | formatter.write_str("false") 74 | } 75 | 76 | fn visit_bool(self, value: bool) -> Result 77 | where 78 | E: serde::de::Error, 79 | { 80 | if !value { 81 | Ok(False) 82 | } else { 83 | Err(E::custom(format!("Value '{}' must be 'false'", value))) 84 | } 85 | } 86 | } 87 | 88 | #[cfg(test)] 89 | mod tests { 90 | use super::*; 91 | 92 | #[test] 93 | fn serialize_false() { 94 | let serialized = serde_json::to_value(False).unwrap(); 95 | 96 | assert!(matches!(serialized, serde_json::Value::Bool(false))); 97 | } 98 | 99 | #[test] 100 | fn serialize_true() { 101 | let serialized = serde_json::to_value(True).unwrap(); 102 | 103 | assert!(matches!(serialized, serde_json::Value::Bool(true))); 104 | } 105 | 106 | #[test] 107 | fn deserialize_false() { 108 | let value = serde_json::from_str("false").unwrap(); 109 | 110 | assert!(matches!(value, False)); 111 | } 112 | 113 | #[test] 114 | fn deserialize_true() { 115 | let value = serde_json::from_str("true").unwrap(); 116 | 117 | assert!(matches!(value, True)); 118 | } 119 | 120 | #[test] 121 | fn deserialize_false_failed() { 122 | let error = serde_json::from_str::("true").unwrap_err(); 123 | let error_message = format!("{}", error); 124 | assert!(error_message.contains("Value 'true' must be 'false'"),) 125 | } 126 | 127 | #[test] 128 | fn deserialize_true_failed() { 129 | let error = serde_json::from_str::("false").unwrap_err(); 130 | let error_message = format!("{}", error); 131 | 132 | assert!(error_message.contains("Value 'false' must be 'true'"),) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/writer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod toolchain_file; 2 | pub mod write_msrv; 3 | -------------------------------------------------------------------------------- /src/writer/write_msrv.rs: -------------------------------------------------------------------------------- 1 | use crate::context::{EnvironmentContext, RustReleasesContext, SetContext}; 2 | use crate::manifest::bare_version::BareVersion; 3 | use crate::reporter::Reporter; 4 | use crate::{Set, SubCommand, TResult}; 5 | use rust_releases::ReleaseIndex; 6 | 7 | /// Write the MSRV to the Cargo manifest 8 | /// 9 | /// Repurposes the Set MSRV subcommand for this action. 10 | pub fn write_msrv( 11 | reporter: &impl Reporter, 12 | msrv: BareVersion, 13 | release_index: Option<&ReleaseIndex>, 14 | environment: EnvironmentContext, 15 | rust_releases: RustReleasesContext, 16 | ) -> TResult<()> { 17 | let context = SetContext { 18 | msrv, 19 | environment, 20 | rust_releases, 21 | }; 22 | 23 | Set::new(release_index).run(&context, reporter)?; 24 | 25 | Ok(()) 26 | } 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use crate::context::{EnvironmentContext, RustReleasesContext, WorkspacePackages}; 31 | use crate::error::CargoMSRVError; 32 | use crate::manifest::bare_version::BareVersion; 33 | use crate::reporter::FakeTestReporter; 34 | use crate::writer::write_msrv::write_msrv; 35 | use assert_fs::prelude::*; 36 | use camino::Utf8Path; 37 | use rust_releases::{semver, ReleaseIndex}; 38 | use std::iter::FromIterator; 39 | 40 | #[test] 41 | fn set_release_in_index() { 42 | let tmp = assert_fs::TempDir::new().unwrap(); 43 | tmp.child("Cargo.toml").touch().unwrap(); 44 | 45 | let manifest = tmp.join("Cargo.toml"); 46 | std::fs::write(&manifest, "[package]").unwrap(); 47 | 48 | let root = Utf8Path::from_path(tmp.path()).unwrap(); 49 | 50 | let fake_reporter = FakeTestReporter::default(); 51 | let version = BareVersion::ThreeComponents(2, 0, 5); 52 | 53 | let env = EnvironmentContext { 54 | root_crate_path: root.to_path_buf(), 55 | workspace_packages: WorkspacePackages::default(), 56 | }; 57 | 58 | let index = ReleaseIndex::from_iter(vec![rust_releases::Release::new_stable( 59 | semver::Version::new(2, 0, 5), 60 | )]); 61 | 62 | write_msrv( 63 | &fake_reporter, 64 | version, 65 | Some(&index), 66 | env, 67 | RustReleasesContext::default(), 68 | ) 69 | .unwrap(); 70 | 71 | let content = std::fs::read_to_string(&manifest).unwrap(); 72 | assert_eq!(content, "[package]\nrust-version = \"2.0.5\"\n"); 73 | } 74 | 75 | #[test] 76 | fn fail_to_set_release_not_in_index() { 77 | let tmp = assert_fs::TempDir::new().unwrap(); 78 | tmp.child("Cargo.toml").touch().unwrap(); 79 | 80 | let manifest = tmp.join("Cargo.toml"); 81 | std::fs::write(manifest, "[package]").unwrap(); 82 | 83 | let root = Utf8Path::from_path(tmp.path()).unwrap(); 84 | 85 | let fake_reporter = FakeTestReporter::default(); 86 | let version = BareVersion::ThreeComponents(2, 0, 5); 87 | 88 | let env = EnvironmentContext { 89 | root_crate_path: root.to_path_buf(), 90 | workspace_packages: WorkspacePackages::default(), 91 | }; 92 | 93 | let index = ReleaseIndex::from_iter(vec![]); 94 | 95 | let err = write_msrv( 96 | &fake_reporter, 97 | version, 98 | Some(&index), 99 | env, 100 | RustReleasesContext::default(), 101 | ) 102 | .unwrap_err(); 103 | 104 | assert!(matches!(err, CargoMSRVError::InvalidMsrvSet(_))); 105 | } 106 | 107 | #[test] 108 | fn set_release_without_index_check() { 109 | let tmp = assert_fs::TempDir::new().unwrap(); 110 | tmp.child("Cargo.toml").touch().unwrap(); 111 | 112 | let manifest = tmp.join("Cargo.toml"); 113 | std::fs::write(&manifest, "[package]").unwrap(); 114 | 115 | let root = Utf8Path::from_path(tmp.path()).unwrap(); 116 | 117 | let fake_reporter = FakeTestReporter::default(); 118 | let version = BareVersion::ThreeComponents(2, 0, 5); 119 | 120 | let env = EnvironmentContext { 121 | root_crate_path: root.to_path_buf(), 122 | workspace_packages: WorkspacePackages::default(), 123 | }; 124 | 125 | write_msrv( 126 | &fake_reporter, 127 | version, 128 | None, 129 | env, 130 | RustReleasesContext::default(), 131 | ) 132 | .unwrap(); 133 | 134 | let content = std::fs::read_to_string(&manifest).unwrap(); 135 | assert_eq!(content, "[package]\nrust-version = \"2.0.5\"\n"); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::path::{Path, PathBuf}; 4 | 5 | pub mod reporter; 6 | pub mod runner; 7 | pub mod sub_cmd_find; 8 | pub mod sub_cmd_verify; 9 | 10 | pub struct Fixture { 11 | fixture_dir: PathBuf, 12 | temp_dir: assert_fs::TempDir, 13 | } 14 | 15 | impl Fixture { 16 | pub fn new(fixture_dir: impl AsRef) -> Self { 17 | use assert_fs::fixture::PathCopy; 18 | 19 | let fixture_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) 20 | .join("tests") 21 | .join("fixtures") 22 | .join(fixture_dir); 23 | 24 | let temp_dir = assert_fs::TempDir::new().unwrap(); 25 | temp_dir.copy_from(&fixture_dir, &["**/*"]).unwrap(); 26 | 27 | Self { 28 | fixture_dir, 29 | temp_dir, 30 | } 31 | } 32 | 33 | pub fn tmp_path(&self, path: impl AsRef) -> PathBuf { 34 | self.temp_dir.join(path) 35 | } 36 | 37 | pub fn to_str(&self) -> &str { 38 | self.temp_dir.to_str().unwrap() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/common/reporter.rs: -------------------------------------------------------------------------------- 1 | // Copied from src/reporter.rs for integration and end-to-end testing 2 | // To do: Merge them back together in a testing dev-dep crate: 3 | // * Requires: traits for Check, Output etc. to be separated to a library crate as 4 | // well. 5 | 6 | use cargo_msrv::reporter::{Event, Marker, Reporter, Scope, ScopeGenerator, SupplyScopeGenerator}; 7 | use std::sync::{Arc, Mutex, MutexGuard}; 8 | use storyteller::{ 9 | event_channel, ChannelEventListener, ChannelFinalizeHandler, ChannelReporter, EventHandler, 10 | EventListener, EventReporter, EventReporterError, EventSender, FinishProcessing, 11 | }; 12 | 13 | pub struct IntegrationTestReporter { 14 | inner: ChannelReporter, 15 | id_gen: IntegrationTestScopeGenerator, 16 | } 17 | 18 | impl EventReporter for IntegrationTestReporter { 19 | type Event = Event; 20 | type Err = EventReporterError; 21 | 22 | fn report_event(&self, event: impl Into) -> Result<(), Self::Err> { 23 | self.inner.report_event(event) 24 | } 25 | 26 | fn disconnect(self) -> Result<(), Self::Err> { 27 | self.inner.disconnect() 28 | } 29 | } 30 | 31 | impl IntegrationTestReporter { 32 | pub fn new(sender: EventSender) -> Self { 33 | Self { 34 | inner: ChannelReporter::new(sender), 35 | id_gen: IntegrationTestScopeGenerator, 36 | } 37 | } 38 | } 39 | 40 | impl SupplyScopeGenerator for IntegrationTestReporter { 41 | type ScopeGen = IntegrationTestScopeGenerator; 42 | 43 | fn scope_generator(&self) -> &Self::ScopeGen { 44 | &self.id_gen 45 | } 46 | } 47 | 48 | #[derive(Default)] 49 | pub struct IntegrationTestScopeGenerator; 50 | 51 | impl ScopeGenerator for IntegrationTestScopeGenerator { 52 | fn generate(&self) -> (Scope, Scope) { 53 | let id = 0; 54 | 55 | (Scope::new(id, Marker::Start), Scope::new(id, Marker::End)) 56 | } 57 | } 58 | 59 | pub struct EventTestDevice { 60 | reporter: IntegrationTestReporter, 61 | #[allow(unused)] 62 | listener: ChannelEventListener, 63 | handler: Arc, 64 | finalizer: ChannelFinalizeHandler, 65 | } 66 | 67 | impl Default for EventTestDevice { 68 | fn default() -> Self { 69 | let (sender, receiver) = event_channel::(); 70 | 71 | let reporter = IntegrationTestReporter::new(sender); 72 | let listener = ChannelEventListener::new(receiver); 73 | let handler = Arc::new(TestingHandler::default()); 74 | let finalizer = listener.run_handler(handler.clone()); 75 | 76 | Self { 77 | reporter, 78 | listener, 79 | handler, 80 | finalizer, 81 | } 82 | } 83 | } 84 | 85 | impl EventTestDevice { 86 | pub fn events(&self) -> Vec { 87 | self.handler 88 | .clone() 89 | .events() 90 | .iter() 91 | .cloned() 92 | .collect::>() 93 | } 94 | 95 | pub fn wait_for_events(self) -> Vec { 96 | self.reporter.disconnect().unwrap(); 97 | self.finalizer.finish_processing().unwrap(); 98 | 99 | let handler = Arc::try_unwrap(self.handler).unwrap(); 100 | 101 | handler.unwrap_events() 102 | } 103 | 104 | pub fn reporter(&self) -> &impl Reporter { 105 | &self.reporter 106 | } 107 | } 108 | 109 | #[derive(Debug)] 110 | pub struct TestingHandler { 111 | event_log: Arc>>, 112 | } 113 | 114 | impl Default for TestingHandler { 115 | fn default() -> Self { 116 | Self { 117 | event_log: Arc::new(Mutex::new(Vec::new())), 118 | } 119 | } 120 | } 121 | 122 | impl Clone for TestingHandler { 123 | fn clone(&self) -> Self { 124 | Self { 125 | event_log: self.event_log.clone(), 126 | } 127 | } 128 | } 129 | 130 | impl TestingHandler { 131 | pub fn events(&self) -> MutexGuard<'_, Vec> { 132 | self.event_log.lock().unwrap() 133 | } 134 | 135 | pub fn unwrap_events(self) -> Vec { 136 | let mutex = Arc::try_unwrap(self.event_log).unwrap(); 137 | mutex.into_inner().unwrap() 138 | } 139 | } 140 | 141 | impl EventHandler for TestingHandler { 142 | type Event = Event; 143 | 144 | fn handle(&self, event: Self::Event) { 145 | self.event_log.lock().unwrap().push(event); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /tests/common/runner.rs: -------------------------------------------------------------------------------- 1 | // Copied from src/testing.rs for integration and end-to-end testing 2 | // To do: Merge them back together in a testing dev-dep crate: 3 | // * Requires: traits for Check, Output etc. to be separated to a library crate as 4 | // well. 5 | 6 | use std::collections::HashSet; 7 | 8 | use cargo_msrv::compatibility::IsCompatible; 9 | use cargo_msrv::error::CargoMSRVError; 10 | use cargo_msrv::rust::Toolchain; 11 | use cargo_msrv::Compatibility; 12 | use rust_releases::semver::Version; 13 | 14 | pub struct TestRunner { 15 | accept_versions: HashSet, 16 | target: &'static str, 17 | } 18 | 19 | impl TestRunner { 20 | pub fn with_ok<'v, T: IntoIterator>(target: &'static str, iter: T) -> Self { 21 | Self { 22 | accept_versions: iter.into_iter().cloned().collect(), 23 | target, 24 | } 25 | } 26 | } 27 | 28 | impl IsCompatible for TestRunner { 29 | fn is_compatible(&self, toolchain: &Toolchain) -> Result { 30 | let version = toolchain.version(); 31 | let components = toolchain.components(); 32 | 33 | if self.accept_versions.contains(toolchain.version()) { 34 | Ok(Compatibility::new_success(Toolchain::new( 35 | version.clone(), 36 | self.target, 37 | components, 38 | ))) 39 | } else { 40 | Ok(Compatibility::new_failure( 41 | Toolchain::new(version.clone(), self.target, components), 42 | "f".to_string(), 43 | )) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/common/sub_cmd_verify.rs: -------------------------------------------------------------------------------- 1 | use crate::common::reporter::EventTestDevice; 2 | use cargo_msrv::cli::CargoCli; 3 | use cargo_msrv::compatibility::RustupToolchainCheck; 4 | use cargo_msrv::error::CargoMSRVError; 5 | use cargo_msrv::{Context, SubCommand, Verify}; 6 | use rust_releases::{Release, ReleaseIndex}; 7 | use std::convert::TryFrom; 8 | use std::ffi::OsString; 9 | use std::iter::FromIterator; 10 | 11 | pub fn run_verify(with_args: I, releases: S) -> Result<(), CargoMSRVError> 12 | where 13 | T: Into + Clone, 14 | I: IntoIterator, 15 | S: IntoIterator, 16 | { 17 | let matches = CargoCli::parse_args(with_args); 18 | let opts = matches.to_cargo_msrv_cli().to_opts(); 19 | let ctx = Context::try_from(opts)?; 20 | let verify_ctx = ctx.to_verify_context().unwrap(); 21 | 22 | // Limit the available versions: this ensures we don't need to incrementally install more toolchains 23 | // as more Rust toolchains become available. 24 | let available_versions = ReleaseIndex::from_iter(releases); 25 | 26 | let device = EventTestDevice::default(); 27 | 28 | let ignore_toolchain = verify_ctx.ignore_lockfile; 29 | let no_check_feedback = verify_ctx.no_check_feedback; 30 | let env = &verify_ctx.environment; 31 | 32 | let runner = RustupToolchainCheck::new( 33 | device.reporter(), 34 | ignore_toolchain, 35 | no_check_feedback, 36 | env, 37 | verify_ctx.run_command(), 38 | ); 39 | 40 | // Determine the MSRV from the index of available releases. 41 | let cmd = Verify::new(&available_versions, runner); 42 | 43 | cmd.run(&verify_ctx, device.reporter()) 44 | } 45 | -------------------------------------------------------------------------------- /tests/fixtures/1.29.2/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | !Cargo.lock -------------------------------------------------------------------------------- /tests/fixtures/1.29.2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "v_1_29_2_with_lockfile_v2" 3 | description = "If the Cargo lockfile v1 is understood by the Cargo version currently under test, the crate will build from v1.29.0. Otherwise the version will be restricted to v1.38.0, which is the first version to use the new lockfile v2, since the lockfile included was generated by Cargo 1.52.0" 4 | version = "0.1.0" 5 | authors = ["foresterre "] 6 | 7 | [dependencies] 8 | stringslice = "=0.1.2" # include a dependency, so the lockfile will contain a format not supported by the lockfile v1 parser 9 | 10 | [workspace] -------------------------------------------------------------------------------- /tests/fixtures/1.29.2/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let words = vec!["hello", "world"]; 3 | 4 | // .flatten was stabilized in 1.29 5 | let concatenated: String = words.into_iter().map(str::chars).flatten().collect(); 6 | 7 | assert_eq!(concatenated.as_str(), "helloworld") 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/1.30.0/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /tests/fixtures/1.30.0/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "v_1_30_with_edition_2018" 3 | description = "If the edition field in this Cargo lockfile is being used to set the minimum value, the 2018 value will result in 1.31. Otherwise, the feature used in the main.rs is supported since 1.30." 4 | version = "0.1.0" 5 | edition = "2018" 6 | authors = ["foresterre "] 7 | 8 | [workspace] -------------------------------------------------------------------------------- /tests/fixtures/1.30.0/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[allow(unused_variables)] 3 | let r#for = true; 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/1.35.0/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /tests/fixtures/1.35.0/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "v_1_35_0" 3 | version = "0.1.0" 4 | authors = ["foresterre "] 5 | edition = "2018" 6 | 7 | [package.metadata] 8 | msrv = "1.35.0" -------------------------------------------------------------------------------- /tests/fixtures/1.35.0/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | dbg!() 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/1.36.0/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /tests/fixtures/1.36.0/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "v_1_36_0" 3 | version = "0.1.0" 4 | authors = ["foresterre "] 5 | edition = "2018" 6 | 7 | [package.metadata] 8 | msrv = "1.36" 9 | 10 | [dependencies] 11 | -------------------------------------------------------------------------------- /tests/fixtures/1.36.0/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | fn main() { 4 | let mut example: VecDeque<_> = (0..5).collect(); 5 | example.rotate_left(4); 6 | } 7 | -------------------------------------------------------------------------------- /tests/fixtures/1.37.0/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /tests/fixtures/1.37.0/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "v_1_37_0" 3 | version = "0.1.0" 4 | authors = ["foresterre "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /tests/fixtures/1.37.0/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let _ = Some(1).xor(Some(0)); 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/1.38.0/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /tests/fixtures/1.38.0/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "v_1_38_0" 3 | version = "0.1.0" 4 | authors = ["foresterre "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /tests/fixtures/1.38.0/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let _ = std::time::Duration::new(1, 1).as_secs_f32(); 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/1.56.0-edition-2018/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /tests/fixtures/1.56.0-edition-2018/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "v_1_56_0" 3 | version = "0.1.0" 4 | authors = ["foresterre "] 5 | edition = "2018" 6 | rust-version = "1.56" 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /tests/fixtures/1.56.0-edition-2018/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let mut vec = Vec::with_capacity(10); 3 | vec.extend([1]); 4 | vec.shrink_to(0); 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixtures/1.56.0-edition-2021/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /tests/fixtures/1.56.0-edition-2021/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "v_1_56_0" 3 | version = "0.1.0" 4 | authors = ["foresterre "] 5 | edition = "2021" 6 | rust-version = "1.56.0" 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /tests/fixtures/1.56.0-edition-2021/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let _: u8 = 1u32.try_into().unwrap(); 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/cargo-feature-required/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /tests/fixtures/cargo-feature-required/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "requires-cargo-feature" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [features] 7 | required_feature = [] 8 | -------------------------------------------------------------------------------- /tests/fixtures/cargo-feature-required/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "required_feature")] 2 | fn compile() { 3 | // Rust >= 1.56 4 | let _: u8 = 1u32.try_into().unwrap(); 5 | } 6 | 7 | #[cfg(not(feature = "required_feature"))] 8 | fn compile() { 9 | compile_error!("Requires 'required_feature' to compile!"); 10 | } 11 | -------------------------------------------------------------------------------- /tests/fixtures/cargo-feature-requires-none/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /tests/fixtures/cargo-feature-requires-none/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "requires-cargo-feature" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [features] 7 | default = ["unrequired_feature"] 8 | unrequired_feature = [] 9 | -------------------------------------------------------------------------------- /tests/fixtures/cargo-feature-requires-none/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | fn some() { 4 | // Rust >= 1.56 5 | let _: u8 = 1u32.try_into().unwrap(); 6 | } 7 | 8 | #[cfg(feature = "unrequired_feature")] 9 | fn compile() { 10 | compile_error!("If the 'unrequired_feature' is set, compiling will fail!"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/rustc/hello.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | dbg!("Hello world!"); 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/unbuildable-with-msrv/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /tests/fixtures/unbuildable-with-msrv/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "v_unbuildable" 3 | version = "0.1.0" 4 | authors = ["foresterre "] 5 | edition = "2021" 6 | rust-version = "1.57" 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /tests/fixtures/unbuildable-with-msrv/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let _ = no_such_macro!(); 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/unbuildable/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /tests/fixtures/unbuildable/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "v_unbuildable" 3 | version = "0.1.0" 4 | authors = ["foresterre "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /tests/fixtures/unbuildable/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let _ = no_such_macro!(); 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/virtual-workspace/.gitignore: -------------------------------------------------------------------------------- 1 | *.lock 2 | /target 3 | -------------------------------------------------------------------------------- /tests/fixtures/virtual-workspace/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ "a", "b"] -------------------------------------------------------------------------------- /tests/fixtures/virtual-workspace/a/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "a" 3 | version = "0.1.0" 4 | edition = "2021" 5 | rust-version = "1.56" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /tests/fixtures/virtual-workspace/a/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub fn hello_from_a(name: &str) -> String { 2 | format!("Hello {}, I'm A", name) 3 | } 4 | 5 | #[cfg(test)] 6 | mod tests { 7 | use super::hello_from_a; 8 | 9 | #[test] 10 | fn a_works() { 11 | let result = hello_from_a("Christopher"); 12 | assert_eq!(result.as_str(), "Hello Christopher, I'm A"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/fixtures/virtual-workspace/b/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "b" 3 | version = "0.1.0" 4 | edition = "2021" 5 | rust-version = "1.58" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /tests/fixtures/virtual-workspace/b/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub fn hello_from_b(name: &str) -> String { 2 | format!("Hello {}, I'm B", name) 3 | } 4 | 5 | #[cfg(test)] 6 | mod tests { 7 | use super::hello_from_b; 8 | 9 | #[test] 10 | fn b_works() { 11 | let result = hello_from_b("Christopher"); 12 | assert_eq!(result.as_str(), "Hello Christopher, I'm B"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/fixtures/virtual-workspace/description.md: -------------------------------------------------------------------------------- 1 | The `virtual-workspace` fixture consists of a virtual workspace, and two packages within the virtual workspace named 'a' 2 | and `b`. The package `a` has a defined `rust-version` of `1.56` and package `b` has a defined `rust-version` of `1.58`. 3 | 4 | When determining the MSRV for package `a` in the workspace, e.g. by running `cargo msrv --path a` (with default check 5 | command `cargo check`), it should find an MSRV of `1.56`, while running `cargo msrv --path b` should return an MSRV of 6 | `1.58`. 7 | 8 | Prior to cargo-msrv 0.15.0, the default check command was `cargo check --all`. This would instead always find the 9 | greatest common MSRV of the workspace, regardless of which package we were in. We can simulate this behaviour by 10 | running `cargo msrv -- cargo check --all`. The result of this command instead should be an MSRV of `1.58`, as package 11 | `b` is the greatest common MSRV in the workspace. -------------------------------------------------------------------------------- /tests/fixtures/workspace-inheritance/.gitignore: -------------------------------------------------------------------------------- 1 | *.lock 2 | /target 3 | -------------------------------------------------------------------------------- /tests/fixtures/workspace-inheritance/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["a", "b", "c"] 3 | 4 | [workspace.package] 5 | rust-version = "1.66.0" 6 | -------------------------------------------------------------------------------- /tests/fixtures/workspace-inheritance/a/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "a" 3 | version = "0.1.0" 4 | edition = "2021" 5 | rust-version = "1.64.0" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /tests/fixtures/workspace-inheritance/a/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/workspace-inheritance/b/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "b" 3 | version = "0.1.0" 4 | edition = "2021" 5 | rust-version = { workspace = true } 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /tests/fixtures/workspace-inheritance/b/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/workspace-inheritance/c/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "c" 3 | version = "0.1.0" 4 | edition = "2021" 5 | # While uncommon, seems not to be against Cargo manifest spec; i.e. it just states Table, 6 | # not specifying whether only the regular or also the inline variant. This is similar to 7 | # [dependencies] which also accept 'Table' items for each dependency, and where inline tables 8 | # are more regular (and clearly supported!) 9 | # 10 | # NB: when using an inline metadata table, you won't be able to add another [metadata] table, 11 | # you must use the same inline table 12 | metadata = { msrv = "1.64" } 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | -------------------------------------------------------------------------------- /tests/fixtures/workspace-inheritance/c/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/user_output.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::process::{Command, Stdio}; 3 | 4 | use crate::common::Fixture; 5 | 6 | mod common; 7 | 8 | #[test] 9 | fn expect_no_user_output() { 10 | let cargo_msrv_manifest = Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml"); 11 | let test_subject = Fixture::new("1.36.0"); 12 | 13 | let process = Command::new("cargo") 14 | .args([ 15 | "run", 16 | "--quiet", 17 | "--manifest-path", 18 | cargo_msrv_manifest.to_str().unwrap(), 19 | "--", 20 | "--path", 21 | test_subject.to_str(), 22 | "--no-user-output", // this is the command we're testing 23 | "verify", 24 | ]) 25 | .stdout(Stdio::piped()) 26 | .stderr(Stdio::piped()) 27 | .spawn() 28 | .expect("Unable to spawn cargo-msrv via cargo in test"); 29 | 30 | let output = process 31 | .wait_with_output() 32 | .expect("Waiting for process failed during test"); 33 | 34 | let stdout = String::from_utf8_lossy(&output.stdout); 35 | let stderr = String::from_utf8_lossy(&output.stderr); 36 | 37 | // The empty string, "", is preferred over is_empty() because if the assertion fails, we'll 38 | // see the diff between the expected and actual strings. 39 | assert_eq!(stdout.as_ref(), ""); 40 | assert_eq!(stderr.as_ref(), ""); 41 | } 42 | --------------------------------------------------------------------------------