├── .clippy.toml ├── .cspell.json ├── .deny.toml ├── .editorconfig ├── .git-blame-ignore-revs ├── .gitattributes ├── .github ├── .cspell │ ├── project-dictionary.txt │ └── rust-dependencies.txt ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .markdownlint-cli2.yaml ├── .rustfmt.toml ├── .shellcheckrc ├── .taplo.toml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src ├── cargo.rs ├── cli.rs ├── context.rs ├── features.rs ├── fs.rs ├── main.rs ├── manifest.rs ├── metadata.rs ├── process.rs ├── restore.rs ├── rustup.rs ├── term.rs └── version.rs ├── tests ├── auxiliary │ └── mod.rs ├── fixtures │ ├── default_feature_behavior │ │ ├── Cargo.toml │ │ ├── has_default │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ ├── lib.rs │ │ │ │ └── main.rs │ │ └── no_default │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ ├── lib.rs │ │ │ └── main.rs │ ├── keep_going │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── lib.rs │ │ │ └── main.rs │ ├── namespaced_features │ │ ├── Cargo.toml │ │ ├── explicit │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ └── lib.rs │ │ ├── implicit │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ └── lib.rs │ │ ├── member1 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ └── lib.rs │ │ ├── member2 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ └── lib.rs │ │ ├── member3 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ └── lib.rs │ │ ├── member4 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ └── lib.rs │ │ └── src │ │ │ ├── lib.rs │ │ │ └── main.rs │ ├── optional_deps │ │ ├── Cargo.toml │ │ ├── member2 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ ├── lib.rs │ │ │ │ └── main.rs │ │ ├── member3 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ ├── lib.rs │ │ │ │ └── main.rs │ │ ├── real │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ ├── lib.rs │ │ │ │ └── main.rs │ │ └── src │ │ │ ├── lib.rs │ │ │ └── main.rs │ ├── package_collision │ │ ├── Cargo.toml │ │ ├── member1 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ ├── lib.rs │ │ │ │ └── main.rs │ │ └── member2 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ ├── lib.rs │ │ │ └── main.rs │ ├── powerset_deduplication │ │ ├── Cargo.toml │ │ ├── member1 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ ├── lib.rs │ │ │ │ └── main.rs │ │ └── src │ │ │ ├── lib.rs │ │ │ └── main.rs │ ├── real │ │ ├── Cargo.toml │ │ ├── member1 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ ├── lib.rs │ │ │ │ └── main.rs │ │ ├── member2 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ ├── lib.rs │ │ │ │ └── main.rs │ │ ├── member3 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ ├── lib.rs │ │ │ │ └── main.rs │ │ └── src │ │ │ ├── lib.rs │ │ │ └── main.rs │ ├── real_root_public │ │ ├── Cargo.toml │ │ ├── member1 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ ├── lib.rs │ │ │ │ └── main.rs │ │ ├── member2 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ ├── lib.rs │ │ │ │ └── main.rs │ │ ├── member3 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ ├── lib.rs │ │ │ │ └── main.rs │ │ └── src │ │ │ ├── lib.rs │ │ │ └── main.rs │ ├── rust-version │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ ├── member1 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ ├── lib.rs │ │ │ │ └── main.rs │ │ ├── member2 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ ├── lib.rs │ │ │ │ └── main.rs │ │ ├── member3 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ ├── lib.rs │ │ │ │ └── main.rs │ │ └── src │ │ │ ├── lib.rs │ │ │ └── main.rs │ ├── virtual │ │ ├── Cargo.toml │ │ ├── dir │ │ │ └── not_find_manifest │ │ │ │ ├── Cargo.toml │ │ │ │ └── src │ │ │ │ ├── lib.rs │ │ │ │ └── main.rs │ │ ├── member1 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ ├── lib.rs │ │ │ │ └── main.rs │ │ └── member2 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ ├── lib.rs │ │ │ └── main.rs │ ├── weak_dep_features │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── weak_dep_features_implicit │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ └── weak_dep_features_namespaced │ │ ├── Cargo.toml │ │ └── src │ │ └── lib.rs ├── long-help.txt ├── short-help.txt └── test.rs └── tools ├── .tidy-check-license-headers ├── publish.sh └── tidy.sh /.clippy.toml: -------------------------------------------------------------------------------- 1 | # Clippy configuration 2 | # https://doc.rust-lang.org/nightly/clippy/lint_configuration.html 3 | 4 | allow-private-module-inception = true 5 | avoid-breaking-exported-api = false 6 | disallowed-names = [] 7 | disallowed-macros = [ 8 | { path = "std::dbg", reason = "it is okay to use during development, but please do not include it in main branch" }, 9 | ] 10 | disallowed-methods = [ 11 | ] 12 | disallowed-types = [ 13 | ] 14 | -------------------------------------------------------------------------------- /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "gitignoreRoot": ".", 4 | "useGitignore": true, 5 | "dictionaryDefinitions": [ 6 | { 7 | "name": "organization-dictionary", 8 | "path": "https://raw.githubusercontent.com/taiki-e/github-actions/HEAD/.github/.cspell/organization-dictionary.txt", 9 | "addWords": true 10 | }, 11 | { 12 | "name": "project-dictionary", 13 | "path": "./.github/.cspell/project-dictionary.txt", 14 | "addWords": true 15 | }, 16 | { 17 | "name": "rust-dependencies", 18 | "path": "./.github/.cspell/rust-dependencies.txt", 19 | "addWords": true 20 | } 21 | ], 22 | "dictionaries": [ 23 | "organization-dictionary", 24 | "project-dictionary", 25 | "rust-dependencies" 26 | ], 27 | "ignoreRegExpList": [ 28 | // Copyright notice 29 | "Copyright .*", 30 | "SPDX-(File|Snippet)CopyrightText: .*", 31 | // GHA actions/workflows 32 | "uses: .+@[\\w_.-]+", 33 | // GHA context (repo name, owner name, etc.) 34 | "github.[\\w_.-]+ (=|!)= '[^']+'", 35 | // GH username 36 | "( |\\[)@[\\w_-]+", 37 | // Git config username 38 | "git config( --[^ ]+)? user.name .*", 39 | // Username in TODO|FIXME comment 40 | "(TODO|FIXME)\\([\\w_., -]+\\)", 41 | // Cargo.toml authors 42 | "authors *= *\\[[^\\]]*\\]", 43 | "\"[^\"]* <[\\w_.+-]+@[\\w.-]+>\"" 44 | ], 45 | "languageSettings": [ 46 | { 47 | "languageId": ["*"], 48 | "dictionaries": ["bash", "cpp-refined", "rust"] 49 | } 50 | ], 51 | "ignorePaths": [] 52 | } 53 | -------------------------------------------------------------------------------- /.deny.toml: -------------------------------------------------------------------------------- 1 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 2 | [advisories] 3 | yanked = "deny" 4 | git-fetch-with-cli = true 5 | ignore = [ 6 | ] 7 | 8 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 9 | [bans] 10 | multiple-versions = "warn" 11 | wildcards = "deny" 12 | allow-wildcard-paths = true 13 | build.executables = "deny" 14 | build.interpreted = "deny" 15 | build.include-dependencies = true 16 | build.include-workspace = false # covered by tools/tidy.sh 17 | build.include-archives = true 18 | build.allow-build-scripts = [ 19 | { name = "anyhow" }, 20 | { name = "libc" }, # via ctrlc 21 | { name = "nix" }, # via ctrlc 22 | { name = "proc-macro2" }, # via serde_derive via cargo-config2 23 | { name = "serde_json" }, 24 | { name = "serde" }, 25 | { name = "windows_aarch64_gnullvm" }, # via ctrlc & same-file & termcolor 26 | { name = "windows_aarch64_msvc" }, # via ctrlc & same-file & termcolor 27 | { name = "windows_i686_gnu" }, # via ctrlc & same-file & termcolor 28 | { name = "windows_i686_gnullvm" }, # via ctrlc & same-file & termcolor 29 | { name = "windows_i686_msvc" }, # via ctrlc & same-file & termcolor 30 | { name = "windows_x86_64_gnu" }, # via ctrlc & same-file & termcolor 31 | { name = "windows_x86_64_gnullvm" }, # via ctrlc & same-file & termcolor 32 | { name = "windows_x86_64_msvc" }, # via ctrlc & same-file & termcolor 33 | ] 34 | build.bypass = [ 35 | # Import libraries are necessary because raw-dylib (requires 1.71+ for x86, 1.65+ for others) is not available on MSRV of them. 36 | { name = "windows_aarch64_gnullvm", allow-globs = ["lib/*.a"] }, # via ctrlc & same-file & termcolor 37 | { name = "windows_aarch64_msvc", allow-globs = ["lib/*.lib"] }, # via ctrlc & same-file & termcolor 38 | { name = "windows_i686_gnu", allow-globs = ["lib/*.a"] }, # via ctrlc & same-file & termcolor 39 | { name = "windows_i686_gnullvm", allow-globs = ["lib/*.a"] }, # via ctrlc & same-file & termcolor 40 | { name = "windows_i686_msvc", allow-globs = ["lib/*.lib"] }, # via ctrlc & same-file & termcolor 41 | { name = "windows_x86_64_gnu", allow-globs = ["lib/*.a"] }, # via ctrlc & same-file & termcolor 42 | { name = "windows_x86_64_gnullvm", allow-globs = ["lib/*.a"] }, # via ctrlc & same-file & termcolor 43 | { name = "windows_x86_64_msvc", allow-globs = ["lib/*.lib"] }, # via ctrlc & same-file & termcolor 44 | ] 45 | 46 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 47 | [licenses] 48 | unused-allowed-license = "deny" 49 | private.ignore = true 50 | allow = [ 51 | "Apache-2.0", 52 | "MIT", 53 | "Unicode-3.0", # unicode-ident 54 | ] 55 | 56 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 57 | [sources] 58 | unknown-registry = "deny" 59 | unknown-git = "deny" 60 | allow-git = [ 61 | ] 62 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig configuration 2 | # https://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | indent_size = 4 10 | indent_style = space 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.{css,html,json,md,rb,sh,yml,yaml}] 15 | indent_size = 2 16 | 17 | [*.{js,yml,yaml}] 18 | quote_type = single 19 | 20 | [*.sh] 21 | # https://google.github.io/styleguide/shellguide.html#s5.3-pipelines 22 | binary_next_line = true 23 | # https://google.github.io/styleguide/shellguide.html#s5.5-case-statement 24 | switch_case_indent = true 25 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Change indent size of shell script files to match scripts in CI config 2 | 3867ddf936df66ae86e7c644cd6b42c15ed257d9 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | tools/tidy.sh linguist-detectable=false 3 | .github/.cspell/rust-dependencies.txt linguist-generated 4 | -------------------------------------------------------------------------------- /.github/.cspell/project-dictionary.txt: -------------------------------------------------------------------------------- 1 | binstall 2 | qpmember 3 | subcrate 4 | vvpmember 5 | -------------------------------------------------------------------------------- /.github/.cspell/rust-dependencies.txt: -------------------------------------------------------------------------------- 1 | // This file is @generated by tidy.sh. 2 | // It is not intended for manual editing. 3 | 4 | argfile 5 | easytime 6 | lexopt 7 | termcolor 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: / 5 | schedule: 6 | interval: daily 7 | commit-message: 8 | prefix: '' 9 | labels: [] 10 | - package-ecosystem: github-actions 11 | directory: / 12 | schedule: 13 | interval: daily 14 | commit-message: 15 | prefix: '' 16 | labels: [] 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | push: 9 | branches: 10 | - main 11 | - dev 12 | schedule: 13 | - cron: '0 2 * * *' 14 | workflow_dispatch: 15 | 16 | env: 17 | CARGO_INCREMENTAL: 0 18 | CARGO_NET_GIT_FETCH_WITH_CLI: true 19 | CARGO_NET_RETRY: 10 20 | CARGO_TERM_COLOR: always 21 | RUST_BACKTRACE: 1 22 | RUST_TEST_THREADS: 1 23 | RUSTFLAGS: -D warnings 24 | RUSTUP_MAX_RETRIES: 10 25 | CARGO_HACK_DENY_WARNINGS: 1 26 | 27 | defaults: 28 | run: 29 | shell: bash --noprofile --norc -CeEuxo pipefail {0} 30 | 31 | concurrency: 32 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} 33 | cancel-in-progress: true 34 | 35 | jobs: 36 | miri: 37 | uses: taiki-e/github-actions/.github/workflows/miri.yml@main 38 | msrv: 39 | uses: taiki-e/github-actions/.github/workflows/msrv.yml@main 40 | release-dry-run: 41 | uses: taiki-e/github-actions/.github/workflows/release-dry-run.yml@main 42 | tidy: 43 | uses: taiki-e/github-actions/.github/workflows/tidy.yml@main 44 | permissions: 45 | contents: read 46 | pull-requests: write # for gh pr edit --add-assignee 47 | repository-projects: read # for gh pr edit --add-assignee 48 | secrets: inherit 49 | 50 | test: 51 | strategy: 52 | fail-fast: false 53 | matrix: 54 | include: 55 | - rust: stable 56 | - rust: beta 57 | - rust: nightly 58 | - rust: nightly 59 | os: macos-latest 60 | - rust: nightly 61 | os: windows-latest 62 | runs-on: ${{ matrix.os || 'ubuntu-latest' }} 63 | timeout-minutes: 60 64 | steps: 65 | - uses: taiki-e/checkout-action@v1 66 | - uses: taiki-e/github-actions/install-rust@main 67 | with: 68 | toolchain: ${{ matrix.rust }} 69 | - run: cargo test --workspace --all-features 70 | - run: | 71 | cargo install --path . --debug 72 | cd -- tests/fixtures/real 73 | cargo hack check --feature-powerset --workspace 74 | cargo hack check --feature-powerset --workspace --message-format=json 75 | cd -- ../rust-version 76 | rustup toolchain remove 1.63 1.64 1.65 77 | cargo hack check --rust-version --workspace --locked 78 | cargo uninstall cargo-hack 79 | - uses: taiki-e/install-action@cargo-hack 80 | - uses: taiki-e/install-action@cargo-minimal-versions 81 | - run: cargo hack build --workspace --no-private --feature-powerset --no-dev-deps 82 | - run: cargo minimal-versions build --workspace --no-private --detach-path-deps=skip-exact --all-features 83 | - run: cargo minimal-versions build --workspace --no-private --detach-path-deps=skip-exact --all-features --direct 84 | 85 | test-compat: 86 | name: test (1.${{ matrix.rust }}) 87 | strategy: 88 | fail-fast: false 89 | matrix: 90 | rust: 91 | # cargo-hack is usually runnable with Cargo versions older than the Rust version required for installation. 92 | # When updating this, the reminder to update the minimum supported Rust version in README.md. 93 | - 26 94 | - 30 95 | - 31 96 | - 36 97 | - 39 98 | - 41 99 | runs-on: ubuntu-latest 100 | timeout-minutes: 60 101 | steps: 102 | - uses: taiki-e/checkout-action@v1 103 | - uses: taiki-e/github-actions/install-rust@nightly 104 | - run: CARGO_HACK_TEST_TOOLCHAIN=${{ matrix.rust }} cargo test --workspace --all-features 105 | # Remove stable toolchain to disable https://github.com/taiki-e/cargo-hack/pull/138's behavior. 106 | - run: rustup toolchain remove stable 107 | - run: CARGO_HACK_TEST_TOOLCHAIN=${{ matrix.rust }} cargo test --workspace --all-features 108 | 109 | test-no-rustup: 110 | name: test (no rustup) 111 | runs-on: ubuntu-latest 112 | timeout-minutes: 60 113 | container: alpine 114 | steps: 115 | - uses: taiki-e/checkout-action@v1 116 | - name: Install Rust 117 | run: apk --no-cache add cargo 118 | - run: cargo test --workspace --all-features 119 | - run: | 120 | cargo install --path . --debug 121 | cd -- tests/fixtures/real 122 | cargo hack check --feature-powerset --workspace 123 | cargo uninstall cargo-hack 124 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | tags: 9 | - v[0-9]+.* 10 | 11 | env: 12 | CARGO_INCREMENTAL: 0 13 | CARGO_NET_GIT_FETCH_WITH_CLI: true 14 | CARGO_NET_RETRY: 10 15 | CARGO_TERM_COLOR: always 16 | RUST_BACKTRACE: 1 17 | RUSTFLAGS: -D warnings 18 | RUSTUP_MAX_RETRIES: 10 19 | 20 | defaults: 21 | run: 22 | shell: bash --noprofile --norc -CeEuxo pipefail {0} 23 | 24 | jobs: 25 | create-release: 26 | if: github.repository_owner == 'taiki-e' 27 | uses: taiki-e/github-actions/.github/workflows/create-release.yml@main 28 | permissions: 29 | contents: write 30 | secrets: inherit 31 | 32 | upload-assets: 33 | name: ${{ matrix.target }} 34 | if: github.repository_owner == 'taiki-e' 35 | needs: create-release 36 | strategy: 37 | matrix: 38 | include: 39 | - target: aarch64-unknown-linux-gnu 40 | os: ubuntu-22.04 41 | - target: aarch64-unknown-linux-musl 42 | - target: aarch64-apple-darwin 43 | os: macos-13 44 | - target: aarch64-pc-windows-msvc 45 | os: windows-2022 46 | - target: x86_64-unknown-linux-gnu 47 | os: ubuntu-22.04 48 | - target: x86_64-unknown-linux-musl 49 | - target: x86_64-apple-darwin 50 | os: macos-13 51 | - target: x86_64-pc-windows-msvc 52 | os: windows-2022 53 | - target: x86_64-unknown-freebsd 54 | - target: x86_64-unknown-illumos 55 | - target: universal-apple-darwin 56 | os: macos-13 57 | runs-on: ${{ matrix.os || 'ubuntu-latest' }} 58 | timeout-minutes: 60 59 | permissions: 60 | contents: write 61 | steps: 62 | - uses: taiki-e/checkout-action@v1 63 | - uses: taiki-e/github-actions/install-rust@stable 64 | - uses: taiki-e/setup-cross-toolchain-action@v1 65 | with: 66 | target: ${{ matrix.target }} 67 | - run: printf '%s\n' "RUSTFLAGS=${RUSTFLAGS} -C target-feature=+crt-static" >>"${GITHUB_ENV}" 68 | if: contains(matrix.target, '-windows-msvc') 69 | - run: printf '%s\n' "RUSTFLAGS=${RUSTFLAGS} -C target-feature=+crt-static -C link-self-contained=yes" >>"${GITHUB_ENV}" 70 | if: contains(matrix.target, '-linux-musl') 71 | # https://doc.rust-lang.org/rustc/platform-support.html 72 | - run: printf 'MACOSX_DEPLOYMENT_TARGET=10.12\n' >>"${GITHUB_ENV}" 73 | if: matrix.target == 'x86_64-apple-darwin' 74 | - run: printf 'MACOSX_DEPLOYMENT_TARGET=11.0\n' >>"${GITHUB_ENV}" 75 | if: matrix.target == 'aarch64-apple-darwin' || matrix.target == 'universal-apple-darwin' 76 | - uses: taiki-e/upload-rust-binary-action@v1 77 | with: 78 | bin: cargo-hack 79 | target: ${{ matrix.target }} 80 | tar: all 81 | zip: windows 82 | token: ${{ secrets.GITHUB_TOKEN }} 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | # There is binary in the workspace, but intentionally not committing lockfile. 3 | # See https://github.com/taiki-e/cargo-llvm-cov/pull/152#issuecomment-1107055622 for more. 4 | Cargo.lock 5 | 6 | # For platform and editor specific settings, it is recommended to add to 7 | # a global .gitignore file. 8 | # Refs: https://docs.github.com/en/github/using-git/ignoring-files#configuring-ignored-files-for-all-repositories-on-your-computer 9 | -------------------------------------------------------------------------------- /.markdownlint-cli2.yaml: -------------------------------------------------------------------------------- 1 | # https://github.com/DavidAnson/markdownlint/blob/HEAD/doc/Rules.md 2 | config: 3 | line-length: false # MD013 4 | no-duplicate-heading: false # MD024 5 | no-blanks-blockquote: false # MD028 (this warns valid GFM alerts usage) 6 | no-inline-html: false # MD033 7 | no-emphasis-as-heading: false # MD036 8 | 9 | # https://github.com/DavidAnson/markdownlint-cli2#markdownlint-cli2jsonc 10 | noBanner: true 11 | noProgress: true 12 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Rustfmt configuration 2 | # https://github.com/rust-lang/rustfmt/blob/HEAD/Configurations.md 3 | 4 | # Rustfmt cannot format long lines inside macros, but this option detects this. 5 | # This is unstable (tracking issue: https://github.com/rust-lang/rustfmt/issues/3391) 6 | error_on_line_overflow = true 7 | 8 | # Override the default formatting style. 9 | # See https://internals.rust-lang.org/t/running-rustfmt-on-rust-lang-rust-and-other-rust-lang-repositories/8732/81. 10 | use_small_heuristics = "Max" 11 | # This is unstable (tracking issue: https://github.com/rust-lang/rustfmt/issues/3370) 12 | overflow_delimited_expr = true 13 | # This is unstable (tracking issue: https://github.com/rust-lang/rustfmt/issues/4991). 14 | imports_granularity = "Crate" 15 | # This is unstable (tracking issue: https://github.com/rust-lang/rustfmt/issues/5083). 16 | group_imports = "StdExternalCrate" 17 | 18 | # Apply rustfmt to more places. 19 | # This is unstable (tracking issue: https://github.com/rust-lang/rustfmt/issues/3348). 20 | format_code_in_doc_comments = true 21 | 22 | # Automatically fix deprecated style. 23 | use_field_init_shorthand = true 24 | use_try_shorthand = true 25 | 26 | # Set the default settings again to always apply the proper formatting without 27 | # being affected by the editor settings. 28 | edition = "2021" 29 | style_edition = "2024" 30 | hard_tabs = false 31 | newline_style = "Unix" 32 | tab_spaces = 4 33 | -------------------------------------------------------------------------------- /.shellcheckrc: -------------------------------------------------------------------------------- 1 | # ShellCheck configuration 2 | # https://github.com/koalaman/shellcheck/blob/HEAD/shellcheck.1.md#rc-files 3 | 4 | # See also: 5 | # https://github.com/koalaman/shellcheck/wiki/Optional 6 | # https://google.github.io/styleguide/shellguide.html 7 | 8 | # https://github.com/koalaman/shellcheck/wiki/SC2249 9 | # enable=add-default-case 10 | 11 | # https://github.com/koalaman/shellcheck/wiki/SC2244 12 | enable=avoid-nullary-conditions 13 | 14 | # https://github.com/koalaman/shellcheck/wiki/SC2312 15 | # enable=check-extra-masked-returns 16 | 17 | # https://github.com/koalaman/shellcheck/wiki/SC2310 18 | # https://github.com/koalaman/shellcheck/wiki/SC2311 19 | # enable=check-set-e-suppressed 20 | 21 | # enable=check-unassigned-uppercase 22 | 23 | # https://github.com/koalaman/shellcheck/wiki/SC2230 24 | enable=deprecate-which 25 | 26 | # https://github.com/koalaman/shellcheck/wiki/SC2248 27 | enable=quote-safe-variables 28 | 29 | # https://github.com/koalaman/shellcheck/wiki/SC2292 30 | # https://google.github.io/styleguide/shellguide.html#s6.3-tests 31 | enable=require-double-brackets 32 | 33 | # https://github.com/koalaman/shellcheck/wiki/SC2250 34 | # https://google.github.io/styleguide/shellguide.html#s5.6-variable-expansion 35 | enable=require-variable-braces 36 | -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | # Taplo configuration 2 | # https://taplo.tamasfe.dev/configuration/formatter-options.html 3 | 4 | [formatting] 5 | align_comments = false 6 | allowed_blank_lines = 1 7 | array_auto_collapse = false 8 | array_auto_expand = false 9 | indent_string = " " 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-hack" 3 | version = "0.6.36" #publish:version 4 | edition = "2021" 5 | rust-version = "1.70" # For cargo-config2 6 | license = "Apache-2.0 OR MIT" 7 | repository = "https://github.com/taiki-e/cargo-hack" 8 | keywords = ["cargo", "subcommand", "testing"] 9 | categories = ["command-line-utilities", "development-tools::cargo-plugins", "development-tools::testing"] 10 | exclude = ["/.*", "/tools"] 11 | description = """ 12 | Cargo subcommand to provide various options useful for testing and continuous integration. 13 | """ 14 | # The official tools/services of rust-lang no longer refer to it since RFC 3052, 15 | # but it seems still useful for packaging. https://github.com/taiki-e/cargo-hack/pull/173 16 | authors = ["Taiki Endo ", "cargo-hack Contributors"] 17 | 18 | [package.metadata.docs.rs] 19 | targets = ["x86_64-unknown-linux-gnu"] 20 | 21 | [package.metadata.binstall] 22 | disabled-strategies = ["quick-install"] 23 | pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ target }.tar.gz" 24 | bin-dir = "{ bin }{ binary-ext }" 25 | pkg-fmt = "tgz" 26 | 27 | [dependencies] 28 | anyhow = "1.0.47" 29 | cargo-config2 = "0.1.13" 30 | ctrlc = { version = "3.4.4", features = ["termination"] } 31 | lexopt = "0.3" 32 | same-file = "1.0.1" 33 | serde_json = "1" 34 | termcolor = "1" 35 | toml_edit = "0.22.7" 36 | 37 | [dev-dependencies] 38 | build-context = "0.1" 39 | easy-ext = "1" 40 | tempfile = { version = "3", default-features = false } 41 | test-helper = { features = ["cli", "doc", "git"], git = "https://github.com/taiki-e/test-helper.git", rev = "f38a7f5" } 42 | 43 | [lints] 44 | workspace = true 45 | 46 | [workspace] 47 | resolver = "2" 48 | 49 | # This table is shared by projects under github.com/taiki-e. 50 | # Expect for unexpected_cfgs.check-cfg, it is not intended for manual editing. 51 | [workspace.lints.rust] 52 | deprecated_safe = "warn" 53 | improper_ctypes = "warn" 54 | improper_ctypes_definitions = "warn" 55 | non_ascii_idents = "warn" 56 | rust_2018_idioms = "warn" 57 | single_use_lifetimes = "warn" 58 | unexpected_cfgs = { level = "warn", check-cfg = [ 59 | ] } 60 | unnameable_types = "warn" 61 | unreachable_pub = "warn" 62 | unsafe_op_in_unsafe_fn = "warn" 63 | [workspace.lints.clippy] 64 | all = "warn" # Downgrade deny-by-default lints 65 | pedantic = "warn" 66 | as_ptr_cast_mut = "warn" 67 | as_underscore = "warn" 68 | default_union_representation = "warn" 69 | inline_asm_x86_att_syntax = "warn" 70 | trailing_empty_array = "warn" 71 | transmute_undefined_repr = "warn" 72 | undocumented_unsafe_blocks = "warn" 73 | unused_trait_names = "warn" 74 | # Suppress buggy or noisy clippy lints 75 | bool_assert_comparison = { level = "allow", priority = 1 } 76 | borrow_as_ptr = { level = "allow", priority = 1 } # https://github.com/rust-lang/rust-clippy/issues/8286 77 | cast_lossless = { level = "allow", priority = 1 } # https://godbolt.org/z/Pv6vbGG6E 78 | declare_interior_mutable_const = { level = "allow", priority = 1 } # https://github.com/rust-lang/rust-clippy/issues/7665 79 | doc_markdown = { level = "allow", priority = 1 } 80 | float_cmp = { level = "allow", priority = 1 } # https://github.com/rust-lang/rust-clippy/issues/7725 81 | incompatible_msrv = { level = "allow", priority = 1 } # buggy: doesn't consider cfg, https://github.com/rust-lang/rust-clippy/issues/12280, https://github.com/rust-lang/rust-clippy/issues/12257#issuecomment-2093667187 82 | lint_groups_priority = { level = "allow", priority = 1 } # https://github.com/rust-lang/rust-clippy/issues/12920 83 | manual_assert = { level = "allow", priority = 1 } 84 | manual_range_contains = { level = "allow", priority = 1 } # https://github.com/rust-lang/rust-clippy/issues/6455#issuecomment-1225966395 85 | missing_errors_doc = { level = "allow", priority = 1 } 86 | module_name_repetitions = { level = "allow", priority = 1 } # buggy: https://github.com/rust-lang/rust-clippy/issues?q=is%3Aissue+is%3Aopen+module_name_repetitions 87 | naive_bytecount = { level = "allow", priority = 1 } 88 | nonminimal_bool = { level = "allow", priority = 1 } # buggy: https://github.com/rust-lang/rust-clippy/issues?q=is%3Aissue+is%3Aopen+nonminimal_bool 89 | range_plus_one = { level = "allow", priority = 1 } # buggy: https://github.com/rust-lang/rust-clippy/issues?q=is%3Aissue+is%3Aopen+range_plus_one 90 | similar_names = { level = "allow", priority = 1 } 91 | single_match = { level = "allow", priority = 1 } 92 | single_match_else = { level = "allow", priority = 1 } 93 | struct_excessive_bools = { level = "allow", priority = 1 } 94 | struct_field_names = { level = "allow", priority = 1 } 95 | too_many_arguments = { level = "allow", priority = 1 } 96 | too_many_lines = { level = "allow", priority = 1 } 97 | type_complexity = { level = "allow", priority = 1 } 98 | unreadable_literal = { level = "allow", priority = 1 } 99 | 100 | [profile.release] 101 | codegen-units = 1 102 | lto = true 103 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cargo-hack 2 | 3 | [![crates.io](https://img.shields.io/crates/v/cargo-hack?style=flat-square&logo=rust)](https://crates.io/crates/cargo-hack) 4 | [![license](https://img.shields.io/badge/license-Apache--2.0_OR_MIT-blue?style=flat-square)](#license) 5 | [![github actions](https://img.shields.io/github/actions/workflow/status/taiki-e/cargo-hack/ci.yml?branch=main&style=flat-square&logo=github)](https://github.com/taiki-e/cargo-hack/actions) 6 | 7 | Cargo subcommand to provide various options useful for testing and continuous 8 | integration. 9 | 10 | - [Usage](#usage) 11 | - [--each-feature](#--each-feature) 12 | - [--feature-powerset](#--feature-powerset) 13 | - [Options for adjusting the behavior of --each-feature and --feature-powerset](#options-for-adjusting-the-behavior-of---each-feature-and---feature-powerset) 14 | - [--optional-deps](#--optional-deps) 15 | - [--exclude-features, --skip](#--exclude-features---skip) 16 | - [--rust-version](#--rust-version) 17 | - [--version-range](#--version-range) 18 | - [Improvement of the behavior of existing cargo flags](#improvement-of-the-behavior-of-existing-cargo-flags) 19 | - [Installation](#installation) 20 | - [Related Projects](#related-projects) 21 | - [License](#license) 22 | 23 | ## Usage 24 | 25 |
26 | Complete list of options (click to show) 27 | 28 | 29 | ```console 30 | $ cargo hack --help 31 | cargo-hack 32 | Cargo subcommand to provide various options useful for testing and continuous integration. 33 | 34 | USAGE: 35 | cargo hack [OPTIONS] [SUBCOMMAND] 36 | 37 | Use -h for short descriptions and --help for more details. 38 | 39 | OPTIONS: 40 | -p, --package ... 41 | Package(s) to check. 42 | 43 | --all 44 | Alias for --workspace. 45 | 46 | --workspace 47 | Perform command for all packages in the workspace. 48 | 49 | --exclude ... 50 | Exclude packages from the check. 51 | 52 | --manifest-path 53 | Path to Cargo.toml. 54 | 55 | --locked 56 | Require Cargo.lock is up to date. 57 | 58 | -F, --features ... 59 | Space or comma separated list of features to activate. 60 | 61 | --each-feature 62 | Perform for each feature of the package. 63 | 64 | This also includes runs with just --no-default-features flag, and default features. 65 | 66 | When this flag is not used together with --exclude-features (--skip) and 67 | --include-features and there are multiple features, this also includes runs with just 68 | --all-features flag. 69 | 70 | --feature-powerset 71 | Perform for the feature powerset of the package. 72 | 73 | This also includes runs with just --no-default-features flag, and default features. 74 | 75 | When this flag is used together with --depth or namespaced features (-Z 76 | namespaced-features) and not used together with --exclude-features (--skip) and 77 | --include-features and there are multiple features, this also includes runs with just 78 | --all-features flag. 79 | 80 | --optional-deps [DEPS]... 81 | Use optional dependencies as features. 82 | 83 | If DEPS are not specified, all optional dependencies are considered as features. 84 | 85 | This flag can only be used together with either --each-feature flag or 86 | --feature-powerset flag. 87 | 88 | --skip ... 89 | Alias for --exclude-features. 90 | 91 | --exclude-features ... 92 | Space or comma separated list of features to exclude. 93 | 94 | To exclude run of default feature, using value `--exclude-features default`. 95 | 96 | To exclude run of just --no-default-features flag, using --exclude-no-default-features 97 | flag. 98 | 99 | To exclude run of just --all-features flag, using --exclude-all-features flag. 100 | 101 | This flag can only be used together with either --each-feature flag or 102 | --feature-powerset flag. 103 | 104 | --exclude-no-default-features 105 | Exclude run of just --no-default-features flag. 106 | 107 | This flag can only be used together with either --each-feature flag or 108 | --feature-powerset flag. 109 | 110 | --exclude-all-features 111 | Exclude run of just --all-features flag. 112 | 113 | This flag can only be used together with either --each-feature flag or 114 | --feature-powerset flag. 115 | 116 | --depth 117 | Specify a max number of simultaneous feature flags of --feature-powerset. 118 | 119 | If NUM is set to 1, --feature-powerset is equivalent to --each-feature. 120 | 121 | This flag can only be used together with --feature-powerset flag. 122 | 123 | --group-features ... 124 | Space or comma separated list of features to group. 125 | 126 | This treats the specified features as if it were a single feature. 127 | 128 | To specify multiple groups, use this option multiple times: `--group-features a,b 129 | --group-features c,d` 130 | 131 | This flag can only be used together with --feature-powerset flag. 132 | 133 | --target 134 | Build for specified target triple. 135 | 136 | Comma-separated lists of targets are not supported, but you can specify the whole 137 | --target option multiple times to do multiple targets. 138 | 139 | This is actually not a cargo-hack option, it is interpreted by Cargo itself. 140 | 141 | --mutually-exclusive-features ... 142 | Space or comma separated list of features to not use together. 143 | 144 | To specify multiple groups, use this option multiple times: 145 | `--mutually-exclusive-features a,b --mutually-exclusive-features c,d` 146 | 147 | This flag can only be used together with --feature-powerset flag. 148 | 149 | --at-least-one-of ... 150 | Space or comma separated list of features. Skips sets of features that don't enable any 151 | of the features listed. 152 | 153 | To specify multiple groups, use this option multiple times: `--at-least-one-of a,b 154 | --at-least-one-of c,d` 155 | 156 | This flag can only be used together with --feature-powerset flag. 157 | 158 | --include-features ... 159 | Include only the specified features in the feature combinations instead of package 160 | features. 161 | 162 | This flag can only be used together with either --each-feature flag or 163 | --feature-powerset flag. 164 | 165 | --no-dev-deps 166 | Perform without dev-dependencies. 167 | 168 | Note that this flag removes dev-dependencies from real `Cargo.toml` while cargo-hack is 169 | running and restores it when finished. 170 | 171 | --remove-dev-deps 172 | Equivalent to --no-dev-deps flag except for does not restore the original `Cargo.toml` 173 | after performed. 174 | 175 | --no-private 176 | Perform without `publish = false` crates. 177 | 178 | --ignore-private 179 | Skip to perform on `publish = false` packages. 180 | 181 | --ignore-unknown-features 182 | Skip passing --features flag to `cargo` if that feature does not exist in the package. 183 | 184 | This flag can be used with --features, --include-features, or --group-features. 185 | 186 | --rust-version 187 | Perform commands on `package.rust-version`. 188 | 189 | This cannot be used with --version-range. 190 | 191 | --version-range [START]..[=END] 192 | Perform commands on a specified (inclusive) range of Rust versions. 193 | 194 | If the upper bound of the range is omitted, the latest stable compiler is used as the 195 | upper bound. 196 | 197 | If the lower bound of the range is omitted, the value of the `rust-version` field in 198 | `Cargo.toml` is used as the lower bound. 199 | 200 | Note that ranges are always inclusive ranges. 201 | 202 | --version-step 203 | Specify the version interval of --version-range (default to `1`). 204 | 205 | This flag can only be used together with --version-range flag. 206 | 207 | --clean-per-run 208 | Remove artifacts for that package before running the command. 209 | 210 | If used this flag with --workspace, --each-feature, or --feature-powerset, artifacts 211 | will be removed before each run. 212 | 213 | Note that dependencies artifacts will be preserved. 214 | 215 | --clean-per-version 216 | Remove artifacts per Rust version. 217 | 218 | Note that dependencies artifacts will also be removed. 219 | 220 | This flag can only be used together with --version-range flag. 221 | 222 | --keep-going 223 | Keep going on failure. 224 | 225 | --partition 226 | Partition runs and execute only its subset according to M/N. 227 | 228 | --log-group 229 | Log grouping: none, github-actions. 230 | 231 | If this option is not used, the environment will be automatically detected. 232 | 233 | --print-command-list 234 | Print commands without run (Unstable). 235 | 236 | --no-manifest-path 237 | Do not pass --manifest-path option to cargo (Unstable). 238 | 239 | -v, --verbose 240 | Use verbose output. 241 | 242 | --color 243 | Coloring: auto, always, never. 244 | 245 | This flag will be propagated to cargo. 246 | 247 | -h, --help 248 | Prints help information. 249 | 250 | -V, --version 251 | Prints version information. 252 | 253 | Some common cargo commands are (see all commands with --list): 254 | build Compile the current package 255 | check Analyze the current package and report errors, but don't build object files 256 | run Run a binary or example of the local package 257 | test Run the tests 258 | ``` 259 | 260 | 261 |
262 | 263 | `cargo-hack` is basically wrapper of `cargo` that propagates subcommand and most 264 | of the passed flags to `cargo`, but provides additional flags and changes the 265 | behavior of some existing flags. 266 | 267 | ### --each-feature 268 | 269 | Perform for each feature which includes default features and 270 | `--no-default-features` of the package. 271 | 272 | This is useful to check that each feature is working properly. (When used for 273 | this purpose, it is recommended to use with `--no-dev-deps` to avoid 274 | [cargo#4866].) 275 | 276 | ```sh 277 | cargo hack check --each-feature --no-dev-deps 278 | ``` 279 | 280 | See also [Options for adjusting the behavior of --each-feature and --feature-powerset](#options-for-adjusting-the-behavior-of---each-feature-and---feature-powerset) section. 281 | 282 | ### --feature-powerset 283 | 284 | Perform for the feature powerset which includes `--no-default-features` and 285 | default features of the package. 286 | 287 | This is useful to check that every combination of features is working 288 | properly. (When used for this purpose, it is recommended to use with 289 | `--no-dev-deps` to avoid [cargo#4866].) 290 | 291 | ```sh 292 | cargo hack check --feature-powerset --no-dev-deps 293 | ``` 294 | 295 | cargo-hack deduplicate any fully equivalent feature combinations based on how the cargo features work. Therefore, it may be more efficient than checking all feature combinations in other ways. 296 | 297 | > [!TIP] 298 | > When using this flag results in a very large number of feature combinations, consider using [`--depth`](#--depth) option. 299 | 300 | See also [Options for adjusting the behavior of --each-feature and --feature-powerset](#options-for-adjusting-the-behavior-of---each-feature-and---feature-powerset) section. 301 | 302 | ### Options for adjusting the behavior of --each-feature and --feature-powerset 303 | 304 | The following flags can be used with `--each-feature` and `--feature-powerset`. 305 | 306 | #### --optional-deps 307 | 308 | Use optional dependencies as features. 309 | 310 | This flag treats all option dependencies as features by default. 311 | To treat only specific dependencies as features, pass a space or comma separated list. 312 | 313 | ```sh 314 | cargo hack check --feature-powerset --optional-deps deps1,deps2 315 | ``` 316 | 317 | #### --exclude-features, --skip 318 | 319 | Space or comma separated list of features to exclude. 320 | 321 | ```sh 322 | cargo hack check --feature-powerset --exclude-features feature1,feature2 323 | cargo hack check --feature-powerset --skip feature1,feature2 324 | ``` 325 | 326 | 327 | #### --depth 328 | 329 | Specify a max number of simultaneous feature flags of `--feature-powerset`. 330 | 331 | If the number is set to 1, `--feature-powerset` is equivalent to 332 | `--each-feature`. 333 | 334 | 335 | #### --group-features 336 | 337 | Space or comma separated list of features to group. 338 | 339 | This treats the specified features as if it were a single feature. 340 | 341 | To specify multiple groups, use this option multiple times: 342 | `--group-features a,b --group-features c,d` 343 | 344 | ### --rust-version 345 | 346 | Perform commands on the Rust version of `package.rust-version` field in `Cargo.toml` 347 | 348 | ### --version-range 349 | 350 | Perform commands on a specified (inclusive) range of Rust versions. 351 | 352 | ```console 353 | $ cargo hack check --version-range 1.46..=1.47 354 | info: running `rustup run 1.46 cargo check` on cargo-hack (1/2) 355 | ... 356 | info: running `rustup run 1.47 cargo check` on cargo-hack (2/2) 357 | ... 358 | ``` 359 | 360 | (We use `rustup run cargo` instead of `cargo +` to work around a [rustup bug](https://github.com/rust-lang/rustup/issues/3036).) 361 | 362 | This might be useful for catching issues like [termcolor#35], [regex#685], 363 | [rust-clippy#6324]. 364 | 365 | If the upper bound of the range is omitted, the latest stable compiler is used as the upper bound. 366 | 367 | If the lower bound of the range is omitted, the value of the `rust-version` field in `Cargo.toml` is used as the lower bound. 368 | 369 | You can specify the version interval by using `--version-step`. 370 | 371 | 372 | ### --no-dev-deps 373 | 374 | Perform without dev-dependencies. 375 | 376 | This is a workaround for an issue that dev-dependencies leaking into normal 377 | build ([cargo#4866]). 378 | 379 | Also, this can be used as a workaround for an issue that `cargo` does not 380 | allow publishing a package with cyclic dev-dependencies. ([cargo#4242]) 381 | 382 | ```sh 383 | cargo hack publish --no-dev-deps --dry-run --allow-dirty 384 | ``` 385 | 386 | > [!NOTE] 387 | > Currently, using `--no-dev-deps` flag removes dev-dependencies from 388 | > real manifest while cargo-hack is running and restores it when finished. 389 | > Any changes you made to those files during running will not be preserved. 390 | > See [cargo#4242] for why this is necessary. 391 | > Also, this behavior may change in the future on some subcommands. See also 392 | > [#15]. 393 | 394 | 395 | ### --remove-dev-deps 396 | 397 | Equivalent to `--no-dev-deps` except for does not restore the original 398 | `Cargo.toml` after execution. 399 | 400 | This is useful to know what Cargo.toml that cargo-hack is actually using 401 | with `--no-dev-deps`. 402 | 403 | *This flag also works without subcommands.* 404 | 405 | 406 | ### --ignore-private 407 | 408 | Skip to perform on `publish = false` crates. 409 | 410 | 411 | ### --no-private 412 | 413 | Perform without `publish = false` crates. This is similar to `--ignore-private`, but is more powerful because this also prevents private crates from affecting lockfile and metadata. 414 | 415 | > [!NOTE] 416 | > `--no-private` flag modifies `Cargo.toml` while cargo-hack is running and restores it when finished. Any changes you made to those files during running will not be preserved. 417 | 418 | 419 | ### --ignore-unknown-features 420 | 421 | Skip passing `--features` to `cargo` if that feature does not exist. 422 | 423 | 424 | ### --clean-per-run 425 | 426 | Remove artifacts for that package before running the command. 427 | 428 | This also works as a workaround for [rust-clippy#4612]. 429 | 430 | ### Improvement of the behavior of existing cargo flags 431 | 432 | `cargo-hack` changes the behavior of the following existing flags. 433 | 434 | 435 | #### --features, --no-default-features 436 | 437 | Unlike `cargo` ([cargo#3620], [cargo#4106], [cargo#4463], [cargo#4753], 438 | [cargo#5015], [cargo#5364], [cargo#6195]), it can also be applied to 439 | sub-crates. 440 | 441 | 442 | #### --all, --workspace 443 | 444 | Perform command for all packages in the workspace. 445 | 446 | Unlike cargo, it does not compile all members at once. 447 | 448 | For example, running `cargo hack check --all` in a workspace with members 449 | `foo` and `bar` behaves almost the same as the following script: 450 | 451 | ```sh 452 | # If you use cargo-hack, you don't need to maintain this list manually. 453 | members=("foo" "bar") 454 | 455 | for member in "${members[@]}"; do 456 | cargo check --manifest-path "${member}/Cargo.toml" 457 | done 458 | ``` 459 | 460 | *Workspace members will be performed according to the order of the 'packages' 461 | fields of [`cargo metadata`][cargo-metadata].* 462 | 463 | ## Installation 464 | 465 | 466 | ### From source 467 | 468 | ```sh 469 | cargo +stable install cargo-hack --locked 470 | ``` 471 | 472 | Currently, installing cargo-hack requires rustc 1.70+. 473 | 474 | cargo-hack is usually runnable with Cargo versions older than the Rust version 475 | required for installation (e.g., `cargo +1.31 hack check`). Currently, to run 476 | cargo-hack requires Cargo 1.26+. 477 | 478 | 479 | ### From prebuilt binaries 480 | 481 | You can download prebuilt binaries from the [Release page](https://github.com/taiki-e/cargo-hack/releases). 482 | Prebuilt binaries are available for macOS, Linux (gnu and musl), Windows (static executable), FreeBSD, and illumos. 483 | 484 |
485 | Example of script to install from the Release page (click to show) 486 | 487 | ```sh 488 | # Get host target 489 | host=$(rustc -vV | grep '^host:' | cut -d' ' -f2) 490 | # Download binary and install to $HOME/.cargo/bin 491 | curl --proto '=https' --tlsv1.2 -fsSL "https://github.com/taiki-e/cargo-hack/releases/latest/download/cargo-hack-$host.tar.gz" \ 492 | | tar xzf - -C "$HOME/.cargo/bin" 493 | ``` 494 | 495 |
496 | 497 | 498 | ### On GitHub Actions 499 | 500 | You can use [taiki-e/install-action](https://github.com/taiki-e/install-action) to install prebuilt binaries on Linux, macOS, and Windows. 501 | This makes the installation faster and may avoid the impact of [problems caused by upstream changes](https://github.com/tokio-rs/bytes/issues/506). 502 | 503 | ```yaml 504 | - uses: taiki-e/install-action@cargo-hack 505 | ``` 506 | 507 | 508 | ### Via Homebrew 509 | 510 | You can install cargo-hack from [homebrew-core](https://formulae.brew.sh/formula/cargo-hack) (x86_64/AArch64 macOS, x86_64 Linux): 511 | 512 | ```sh 513 | brew install cargo-hack 514 | ``` 515 | 516 | Alternatively, you can also install from the [Homebrew tap maintained by us](https://github.com/taiki-e/homebrew-tap/blob/HEAD/Formula/cargo-hack.rb) (x86_64/AArch64 macOS, x86_64/AArch64 Linux): 517 | 518 | ```sh 519 | brew install taiki-e/tap/cargo-hack 520 | ``` 521 | 522 | 523 | ### Via Scoop (Windows) 524 | 525 | You can install cargo-hack from the [Scoop bucket maintained by us](https://github.com/taiki-e/scoop-bucket/blob/HEAD/bucket/cargo-hack.json): 526 | 527 | ```sh 528 | scoop bucket add taiki-e https://github.com/taiki-e/scoop-bucket 529 | scoop install cargo-hack 530 | ``` 531 | 532 | 533 | ### Via cargo-binstall 534 | 535 | You can install cargo-hack using [cargo-binstall](https://github.com/cargo-bins/cargo-binstall): 536 | 537 | ```sh 538 | cargo binstall cargo-hack 539 | ``` 540 | 541 | 542 | ### Via pacman (Arch Linux) 543 | 544 | You can install cargo-hack from the [extra repository](https://archlinux.org/packages/extra/x86_64/cargo-hack): 545 | 546 | ```sh 547 | pacman -S cargo-hack 548 | ``` 549 | 550 | 551 | ### Via ports (FreeBSD) 552 | 553 | You can install cargo-hack from the [official ports](https://www.freshports.org/devel/cargo-hack): 554 | 555 | ```sh 556 | pkg install cargo-hack 557 | ``` 558 | 559 | 560 | ### Via other package managers 561 | 562 | [![Packaging status](https://repology.org/badge/vertical-allrepos/cargo-hack.svg?columns=4)](https://repology.org/project/cargo-hack/versions) 563 | 564 | ## Related Projects 565 | 566 | - [cargo-llvm-cov]: Cargo subcommand to easily use LLVM source-based code coverage. 567 | - [cargo-minimal-versions]: Cargo subcommand for proper use of `-Z minimal-versions`. 568 | - [cargo-config2]: Library to load and resolve Cargo configuration. 569 | - [cargo-no-dev-deps]: Cargo subcommand for running cargo without dev-dependencies. This is an extraction of the [`--no-dev-deps` flag of cargo-hack](#--no-dev-deps) to be used as a stand-alone cargo subcommand. 570 | 571 | [#15]: https://github.com/taiki-e/cargo-hack/issues/15 572 | [cargo-config2]: https://github.com/taiki-e/cargo-config2 573 | [cargo-llvm-cov]: https://github.com/taiki-e/cargo-llvm-cov 574 | [cargo-metadata]: https://doc.rust-lang.org/cargo/commands/cargo-metadata.html 575 | [cargo-minimal-versions]: https://github.com/taiki-e/cargo-minimal-versions 576 | [cargo-no-dev-deps]: https://github.com/taiki-e/cargo-no-dev-deps 577 | [cargo#3620]: https://github.com/rust-lang/cargo/issues/3620 578 | [cargo#4106]: https://github.com/rust-lang/cargo/issues/4106 579 | [cargo#4242]: https://github.com/rust-lang/cargo/issues/4242 580 | [cargo#4463]: https://github.com/rust-lang/cargo/issues/4463 581 | [cargo#4753]: https://github.com/rust-lang/cargo/issues/4753 582 | [cargo#4866]: https://github.com/rust-lang/cargo/issues/4866 583 | [cargo#5015]: https://github.com/rust-lang/cargo/issues/4463 584 | [cargo#5364]: https://github.com/rust-lang/cargo/issues/5364 585 | [cargo#6195]: https://github.com/rust-lang/cargo/issues/6195 586 | [regex#685]: https://github.com/rust-lang/regex/issues/685 587 | [rust-clippy#4612]: https://github.com/rust-lang/rust-clippy/issues/4612 588 | [rust-clippy#6324]: https://github.com/rust-lang/rust-clippy/issues/6324 589 | [termcolor#35]: https://github.com/BurntSushi/termcolor/issues/35 590 | 591 | ## License 592 | 593 | Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or 594 | [MIT license](LICENSE-MIT) at your option. 595 | 596 | Unless you explicitly state otherwise, any contribution intentionally submitted 597 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall 598 | be dual licensed as above, without any additional terms or conditions. 599 | -------------------------------------------------------------------------------- /src/cargo.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 OR MIT 2 | 3 | use anyhow::{Result, bail, format_err}; 4 | 5 | use crate::{ProcessBuilder, version::Version}; 6 | 7 | pub(crate) fn version(mut cmd: ProcessBuilder<'_>) -> Result { 8 | // Use verbose version output because the packagers add extra strings to the normal version output. 9 | cmd.arg("-vV"); 10 | let verbose_version = cmd.read()?; 11 | let release = verbose_version 12 | .lines() 13 | .find_map(|line| line.strip_prefix("release: ")) 14 | .ok_or_else(|| format_err!("unexpected output from {cmd}: {verbose_version}"))?; 15 | let (version, _channel) = release.split_once('-').unwrap_or((release, "")); 16 | 17 | let version: Version = version.parse()?; 18 | if version.major != 1 || version.patch.is_none() { 19 | bail!("unexpected output from {cmd}: {verbose_version}"); 20 | } 21 | 22 | Ok(version) 23 | } 24 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 OR MIT 2 | 3 | use std::{ 4 | borrow::Cow, 5 | collections::HashMap, 6 | env, 7 | ffi::OsString, 8 | ops, 9 | path::{Path, PathBuf}, 10 | }; 11 | 12 | use anyhow::{Context as _, Result, bail}; 13 | 14 | use crate::{ 15 | ProcessBuilder, cargo, 16 | cli::Args, 17 | features::Features, 18 | manifest::Manifest, 19 | metadata::{Metadata, Package, PackageId}, 20 | restore, term, 21 | }; 22 | 23 | pub(crate) struct Context { 24 | args: Args, 25 | pub(crate) metadata: Metadata, 26 | manifests: HashMap, 27 | pkg_features: HashMap, 28 | cargo: PathBuf, 29 | pub(crate) cargo_version: u32, 30 | pub(crate) restore: restore::Manager, 31 | pub(crate) current_dir: PathBuf, 32 | pub(crate) current_package: Option, 33 | } 34 | 35 | impl Context { 36 | pub(crate) fn new(args: Args, cargo: OsString) -> Result { 37 | assert!( 38 | args.subcommand.is_some() || args.remove_dev_deps, 39 | "no subcommand or valid flag specified" 40 | ); 41 | 42 | // If failed to determine cargo version, assign 0 to skip all version-dependent decisions. 43 | let cargo_version = cargo::version(cmd!(&cargo)) 44 | .map_err(|e| warn!("unable to determine cargo version: {e:#}")) 45 | .map(|v| v.minor) 46 | .unwrap_or(0); 47 | 48 | // if `--remove-dev-deps` flag is off, restore manifest file. 49 | let mut restore = restore::Manager::new(!args.remove_dev_deps); 50 | let metadata = Metadata::new( 51 | args.manifest_path.as_deref(), 52 | &cargo, 53 | cargo_version, 54 | &args, 55 | &mut restore, 56 | )?; 57 | if metadata.cargo_version < 41 && args.include_deps_features { 58 | bail!("--include-deps-features requires Cargo 1.41 or later"); 59 | } 60 | 61 | let mut manifests = HashMap::with_capacity(metadata.workspace_members.len()); 62 | let mut pkg_features = HashMap::with_capacity(metadata.workspace_members.len()); 63 | 64 | for id in &metadata.workspace_members { 65 | let manifest_path = &metadata.packages[id].manifest_path; 66 | let manifest = Manifest::new(manifest_path, metadata.cargo_version)?; 67 | let features = Features::new(&metadata, &manifest, id, args.include_deps_features); 68 | manifests.insert(id.clone(), manifest); 69 | pkg_features.insert(id.clone(), features); 70 | } 71 | 72 | let mut cmd = cmd!(&cargo, "locate-project"); 73 | if let Some(manifest_path) = &args.manifest_path { 74 | cmd.arg("--manifest-path"); 75 | cmd.arg(manifest_path); 76 | } 77 | // Use json format because `--message-format plain` option of 78 | // `cargo locate-project` has been added in Rust 1.48. 79 | let locate_project: serde_json::Map = 80 | serde_json::from_str(&cmd.read()?) 81 | .with_context(|| format!("failed to parse output from {cmd}"))?; 82 | let locate_project = Path::new(locate_project["root"].as_str().unwrap()); 83 | let mut current_package = None; 84 | for id in &metadata.workspace_members { 85 | let manifest_path = &metadata.packages[id].manifest_path; 86 | // no need to use same_file as cargo-metadata and cargo-locate-project 87 | // as they return absolute paths resolved in the same way. 88 | if locate_project == manifest_path { 89 | current_package = Some(id.clone()); 90 | break; 91 | } 92 | } 93 | 94 | let this = Self { 95 | args, 96 | metadata, 97 | manifests, 98 | pkg_features, 99 | cargo: cargo.into(), 100 | cargo_version, 101 | restore, 102 | current_dir: env::current_dir()?, 103 | current_package, 104 | }; 105 | 106 | // TODO: Ideally, we should do this, but for now, we allow it as cargo-hack 107 | // may mistakenly interpret the specified valid feature flag as unknown. 108 | // if this.ignore_unknown_features && !this.workspace && !this.current_manifest().is_virtual() { 109 | // bail!( 110 | // "--ignore-unknown-features can only be used in the root of a virtual workspace or together with --workspace" 111 | // ) 112 | // } 113 | 114 | Ok(this) 115 | } 116 | 117 | // Accessor methods. 118 | 119 | pub(crate) fn packages(&self, id: &PackageId) -> &Package { 120 | &self.metadata.packages[id] 121 | } 122 | 123 | pub(crate) fn workspace_members(&self) -> impl ExactSizeIterator { 124 | self.metadata.workspace_members.iter() 125 | } 126 | 127 | pub(crate) fn current_package(&self) -> Option<&PackageId> { 128 | self.current_package.as_ref() 129 | } 130 | 131 | pub(crate) fn workspace_root(&self) -> &Path { 132 | &self.metadata.workspace_root 133 | } 134 | 135 | pub(crate) fn manifests(&self, id: &PackageId) -> &Manifest { 136 | &self.manifests[id] 137 | } 138 | 139 | pub(crate) fn pkg_features(&self, id: &PackageId) -> &Features { 140 | &self.pkg_features[id] 141 | } 142 | 143 | pub(crate) fn is_private(&self, id: &PackageId) -> bool { 144 | if self.metadata.cargo_version >= 39 { 145 | !self.packages(id).publish 146 | } else { 147 | !self.manifests(id).package.publish.unwrap() 148 | } 149 | } 150 | 151 | pub(crate) fn rust_version(&self, id: &PackageId) -> Option<&str> { 152 | if self.metadata.cargo_version >= 58 { 153 | self.packages(id).rust_version.as_deref() 154 | } else { 155 | self.manifests(id).package.rust_version.as_ref().unwrap().as_deref() 156 | } 157 | } 158 | 159 | pub(crate) fn name_verbose(&self, id: &PackageId) -> Cow<'_, str> { 160 | let package = self.packages(id); 161 | if term::verbose() { 162 | Cow::Owned(format!( 163 | "{} ({})", 164 | package.name, 165 | package.manifest_path.parent().unwrap().display() 166 | )) 167 | } else { 168 | Cow::Borrowed(&package.name) 169 | } 170 | } 171 | 172 | pub(crate) fn cargo(&self) -> ProcessBuilder<'_> { 173 | cmd!(&self.cargo) 174 | } 175 | } 176 | 177 | impl ops::Deref for Context { 178 | type Target = Args; 179 | 180 | fn deref(&self) -> &Self::Target { 181 | &self.args 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/features.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 OR MIT 2 | 3 | use std::{ 4 | collections::{BTreeMap, BTreeSet}, 5 | fmt, slice, 6 | }; 7 | 8 | use crate::{PackageId, manifest::Manifest, metadata::Metadata}; 9 | 10 | #[derive(Debug)] 11 | pub(crate) struct Features { 12 | features: Vec, 13 | optional_deps_start: usize, 14 | deps_features_start: usize, 15 | } 16 | 17 | impl Features { 18 | pub(crate) fn new( 19 | metadata: &Metadata, 20 | manifest: &Manifest, 21 | id: &PackageId, 22 | include_deps_features: bool, 23 | ) -> Self { 24 | let package = &metadata.packages[id]; 25 | 26 | let mut features: Vec<_> = manifest.features.keys().map(Feature::from).collect(); 27 | let mut referenced_deps = BTreeSet::new(); // referenced in features with `dep:` prefix 28 | 29 | // package.features.values() does not provide a way to determine the `dep:` specified by the user. 30 | for names in manifest.features.values() { 31 | for name in names { 32 | if let Some(dep) = name.strip_prefix("dep:") { 33 | referenced_deps.insert(dep); 34 | } 35 | } 36 | } 37 | let optional_deps_start = features.len(); 38 | for name in package.optional_deps() { 39 | // Dependencies explicitly referenced with dep: are no longer implicit features. 40 | if referenced_deps.contains(name) { 41 | continue; 42 | } 43 | let feature = Feature::from(name); 44 | if !features.contains(&feature) { 45 | features.push(feature); 46 | } 47 | } 48 | let deps_features_start = features.len(); 49 | 50 | if include_deps_features { 51 | let node = &metadata.resolve.nodes[id]; 52 | // TODO: Unpublished dependencies are not included in `node.deps`. 53 | for dep in node.deps.iter().filter(|dep| { 54 | // ignore if `dep_kinds` is empty (i.e., not Rust 1.41+), target specific or not a normal dependency. 55 | dep.dep_kinds.iter().any(|kind| kind.kind.is_none() && kind.target.is_none()) 56 | }) { 57 | let dep_package = &metadata.packages[&dep.pkg]; 58 | // TODO: `dep.name` (`resolve.nodes[].deps[].name`) is a valid rust identifier, not a valid feature flag. 59 | // And `packages[].dependencies` doesn't have package identifier, 60 | // so I'm not sure if there is a way to find the actual feature name exactly. 61 | if let Some(d) = package.dependencies.iter().find(|d| d.name == dep_package.name) { 62 | let name = d.rename.as_ref().unwrap_or(&d.name); 63 | features.extend(dep_package.features.keys().map(|f| Feature::path(name, f))); 64 | } 65 | // TODO: Optional deps of `dep_package`. 66 | } 67 | } 68 | 69 | Self { features, optional_deps_start, deps_features_start } 70 | } 71 | 72 | pub(crate) fn normal(&self) -> &[Feature] { 73 | &self.features[..self.optional_deps_start] 74 | } 75 | 76 | pub(crate) fn optional_deps(&self) -> &[Feature] { 77 | &self.features[self.optional_deps_start..self.deps_features_start] 78 | } 79 | 80 | pub(crate) fn deps_features(&self) -> &[Feature] { 81 | &self.features[self.deps_features_start..] 82 | } 83 | 84 | pub(crate) fn contains(&self, name: &str) -> bool { 85 | self.features.iter().any(|f| f == name) 86 | } 87 | } 88 | 89 | /// The representation of Cargo feature. 90 | #[derive(PartialEq, Eq, PartialOrd, Ord)] 91 | pub(crate) enum Feature { 92 | /// A feature of the current crate. 93 | Normal { 94 | /// Feature name. It is considered indivisible. 95 | name: String, 96 | }, 97 | /// Grouped features. 98 | Group { 99 | /// Feature name concatenated with `,`. 100 | name: String, 101 | /// Original feature list. 102 | list: Vec, 103 | }, 104 | /// A feature of a dependency. 105 | Path { 106 | /// Feature path separated with `/`. 107 | name: String, 108 | /// Index of `/`. 109 | _slash: usize, 110 | }, 111 | } 112 | 113 | impl fmt::Debug for Feature { 114 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 115 | use fmt::Write as _; 116 | match self { 117 | Self::Normal { name } | Self::Path { name, .. } => f.write_str(name), 118 | Self::Group { name, .. } => { 119 | f.write_char('[')?; 120 | f.write_str(name)?; 121 | f.write_char(']') 122 | } 123 | } 124 | } 125 | } 126 | 127 | impl Feature { 128 | pub(crate) fn group(group: impl IntoIterator>) -> Self { 129 | let list: Vec<_> = group.into_iter().map(Into::into).collect(); 130 | Self::Group { name: list.join(","), list } 131 | } 132 | 133 | pub(crate) fn path(parent: &str, name: &str) -> Self { 134 | Self::Path { name: format!("{parent}/{name}"), _slash: parent.len() } 135 | } 136 | 137 | pub(crate) fn name(&self) -> &str { 138 | match self { 139 | Self::Normal { name } | Self::Group { name, .. } | Self::Path { name, .. } => name, 140 | } 141 | } 142 | 143 | pub(crate) fn as_group(&self) -> &[String] { 144 | match self { 145 | Self::Group { list, .. } => list, 146 | Self::Normal { name } | Self::Path { name, .. } => slice::from_ref(name), 147 | } 148 | } 149 | 150 | pub(crate) fn matches(&self, s: &str) -> bool { 151 | self.as_group().iter().any(|n| **n == *s) 152 | } 153 | 154 | pub(crate) fn matches_recursive(&self, s: &str, map: &BTreeMap>) -> bool { 155 | fn rec( 156 | group: &Feature, 157 | map: &BTreeMap>, 158 | cur: &str, 159 | root: &str, 160 | ) -> bool { 161 | if let Some(v) = map.get(cur) { 162 | for cur in v { 163 | if cur != root && (group.matches(cur) || rec(group, map, cur, root)) { 164 | return true; 165 | } 166 | } 167 | } 168 | false 169 | } 170 | self.matches(s) || rec(self, map, s, s) 171 | } 172 | } 173 | 174 | impl PartialEq for Feature { 175 | fn eq(&self, other: &str) -> bool { 176 | self.name() == other 177 | } 178 | } 179 | 180 | impl PartialEq for Feature { 181 | fn eq(&self, other: &String) -> bool { 182 | self.name() == other 183 | } 184 | } 185 | 186 | impl> From for Feature { 187 | fn from(name: S) -> Self { 188 | Self::Normal { name: name.into() } 189 | } 190 | } 191 | 192 | impl AsRef for Feature { 193 | fn as_ref(&self) -> &str { 194 | self.name() 195 | } 196 | } 197 | 198 | pub(crate) fn feature_powerset<'a>( 199 | features: impl IntoIterator, 200 | depth: Option, 201 | at_least_one_of: &[Feature], 202 | mutually_exclusive_features: &[Feature], 203 | package_features: &BTreeMap>, 204 | ) -> Vec> { 205 | let deps_map = feature_deps(package_features); 206 | let at_least_one_of = at_least_one_of_for_package(at_least_one_of, &deps_map); 207 | 208 | powerset(features, depth) 209 | .into_iter() 210 | .skip(1) // The first element of a powerset is `[]` so it should be skipped. 211 | .filter(|fs| { 212 | !fs.iter().any(|f| { 213 | f.as_group().iter().filter_map(|f| deps_map.get(&&**f)).any(|deps| { 214 | fs.iter().any(|f| f.as_group().iter().all(|f| deps.contains(&&**f))) 215 | }) 216 | }) 217 | }) 218 | .filter(move |fs| { 219 | // all() returns true if at_least_one_of is empty 220 | at_least_one_of.iter().all(|required_set| { 221 | fs 222 | .iter() 223 | .flat_map(|f| f.as_group()) 224 | .any(|f| required_set.contains(f.as_str())) 225 | }) 226 | }) 227 | .filter(move |fs| { 228 | // Filter any feature set containing more than one feature from the same mutually 229 | // exclusive group. 230 | for group in mutually_exclusive_features { 231 | let mut count = 0; 232 | for f in fs.iter().flat_map(|f| f.as_group()) { 233 | if group.matches_recursive(f, package_features) { 234 | count += 1; 235 | if count > 1 { 236 | return false; 237 | } 238 | } 239 | } 240 | } 241 | true 242 | }) 243 | .collect() 244 | } 245 | 246 | fn feature_deps(map: &BTreeMap>) -> BTreeMap<&str, BTreeSet<&str>> { 247 | fn rec<'a>( 248 | map: &'a BTreeMap>, 249 | set: &mut BTreeSet<&'a str>, 250 | cur: &str, 251 | root: &str, 252 | ) { 253 | if let Some(v) = map.get(cur) { 254 | for cur in v { 255 | // dep: actions aren't features, and can't enable other features in the same crate 256 | if cur.starts_with("dep:") { 257 | continue; 258 | } 259 | if cur != root && set.insert(cur) { 260 | rec(map, set, cur, root); 261 | } 262 | } 263 | } 264 | } 265 | let mut feat_deps = BTreeMap::new(); 266 | for feat in map.keys() { 267 | let mut set = BTreeSet::new(); 268 | rec(map, &mut set, feat, feat); 269 | feat_deps.insert(&**feat, set); 270 | } 271 | feat_deps 272 | } 273 | 274 | fn powerset(iter: impl IntoIterator, depth: Option) -> Vec> { 275 | iter.into_iter().fold(vec![vec![]], |mut acc, elem| { 276 | let ext = acc.clone().into_iter().map(|mut cur| { 277 | cur.push(elem); 278 | cur 279 | }); 280 | if let Some(depth) = depth { 281 | acc.extend(ext.filter(|f| f.len() <= depth)); 282 | } else { 283 | acc.extend(ext); 284 | } 285 | acc 286 | }) 287 | } 288 | 289 | // Leave only features that are possible to enable in the package. 290 | pub(crate) fn at_least_one_of_for_package<'a>( 291 | at_least_one_of: &[Feature], 292 | package_features_flattened: &BTreeMap<&'a str, BTreeSet<&'a str>>, 293 | ) -> Vec> { 294 | if at_least_one_of.is_empty() { 295 | return vec![]; 296 | } 297 | 298 | let mut all_features_enabled_by = BTreeMap::new(); 299 | for (&enabled_by, enables) in package_features_flattened { 300 | all_features_enabled_by.entry(enabled_by).or_insert_with(BTreeSet::new).insert(enabled_by); 301 | for &enabled_feature in enables { 302 | all_features_enabled_by 303 | .entry(enabled_feature) 304 | .or_insert_with(BTreeSet::new) 305 | .insert(enabled_by); 306 | } 307 | } 308 | 309 | at_least_one_of 310 | .iter() 311 | .map(|set| { 312 | set.as_group() 313 | .iter() 314 | .filter_map(|f| all_features_enabled_by.get(f.as_str())) 315 | .flat_map(|f| f.iter().copied()) 316 | .collect::>() 317 | }) 318 | .filter(|set| !set.is_empty()) 319 | .collect::>() 320 | } 321 | 322 | #[cfg(test)] 323 | mod tests { 324 | use std::collections::{BTreeMap, BTreeSet}; 325 | 326 | use super::{Feature, at_least_one_of_for_package, feature_deps, feature_powerset, powerset}; 327 | 328 | macro_rules! v { 329 | ($($expr:expr),* $(,)?) => { 330 | vec![$($expr.into()),*] 331 | }; 332 | } 333 | 334 | macro_rules! map { 335 | ($(($key:expr, $value:expr)),* $(,)?) => { 336 | BTreeMap::from_iter([$(($key.into(), $value)),*]) 337 | }; 338 | } 339 | 340 | macro_rules! set { 341 | ($($expr:expr),* $(,)?) => { 342 | BTreeSet::from_iter([$($expr),*]) 343 | }; 344 | } 345 | 346 | #[test] 347 | fn at_least_one_of_for_package_filter() { 348 | let map = map![("a", v![]), ("b", v!["a"]), ("c", v!["b"]), ("d", v!["a", "b"])]; 349 | let fd = feature_deps(&map); 350 | let list: Vec = v!["b", "x", "y", "z"]; 351 | let filtered = at_least_one_of_for_package(&list, &fd); 352 | assert_eq!(filtered, vec![set!("b", "c", "d")]); 353 | } 354 | 355 | #[test] 356 | fn powerset_with_filter() { 357 | let map = map![("a", v![]), ("b", v!["a"]), ("c", v!["b"]), ("d", v!["a", "b"])]; 358 | 359 | let list = v!["a", "b", "c", "d"]; 360 | let filtered = feature_powerset(&list, None, &[], &[], &map); 361 | assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec!["c", "d"]]); 362 | 363 | let filtered = feature_powerset(&list, None, &["a".into()], &[], &map); 364 | assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec!["c", "d"]]); 365 | 366 | let filtered = feature_powerset(&list, None, &["c".into()], &[], &map); 367 | assert_eq!(filtered, vec![vec!["c"], vec!["c", "d"]]); 368 | 369 | let filtered = feature_powerset(&list, None, &["a".into(), "c".into()], &[], &map); 370 | assert_eq!(filtered, vec![vec!["c"], vec!["c", "d"]]); 371 | 372 | let map = map![("tokio", v![]), ("async-std", v![]), ("a", v![]), ("b", v!["a"])]; 373 | let list = v!["a", "b", "tokio", "async-std"]; 374 | let mutually_exclusive_features = [Feature::group(["tokio", "async-std"])]; 375 | let filtered = feature_powerset(&list, None, &[], &mutually_exclusive_features, &map); 376 | assert_eq!(filtered, vec![ 377 | vec!["a"], 378 | vec!["b"], 379 | vec!["tokio"], 380 | vec!["a", "tokio"], 381 | vec!["b", "tokio"], 382 | vec!["async-std"], 383 | vec!["a", "async-std"], 384 | vec!["b", "async-std"] 385 | ]); 386 | 387 | let mutually_exclusive_features = 388 | [Feature::group(["tokio", "a"]), Feature::group(["tokio", "async-std"])]; 389 | let filtered = feature_powerset(&list, None, &[], &mutually_exclusive_features, &map); 390 | assert_eq!(filtered, vec![ 391 | vec!["a"], 392 | vec!["b"], 393 | vec!["tokio"], 394 | vec!["async-std"], 395 | vec!["a", "async-std"], 396 | vec!["b", "async-std"] 397 | ]); 398 | 399 | let map = map![("a", v![]), ("b", v!["a"]), ("c", v![]), ("d", v!["b"])]; 400 | let list = v!["a", "b", "c", "d"]; 401 | let mutually_exclusive_features = [Feature::group(["a", "c"])]; 402 | let filtered = feature_powerset(&list, None, &[], &mutually_exclusive_features, &map); 403 | assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"]]); 404 | } 405 | 406 | #[test] 407 | fn feature_deps1() { 408 | let map = map![("a", v![]), ("b", v!["a"]), ("c", v!["b"]), ("d", v!["a", "b"])]; 409 | let fd = feature_deps(&map); 410 | assert_eq!(fd, map![ 411 | ("a", set![]), 412 | ("b", set!["a"]), 413 | ("c", set!["a", "b"]), 414 | ("d", set!["a", "b"]) 415 | ]); 416 | let list: Vec = v!["a", "b", "c", "d"]; 417 | let ps = powerset(&list, None); 418 | assert_eq!(ps, vec![ 419 | vec![], 420 | vec!["a"], 421 | vec!["b"], 422 | vec!["a", "b"], 423 | vec!["c"], 424 | vec!["a", "c"], 425 | vec!["b", "c"], 426 | vec!["a", "b", "c"], 427 | vec!["d"], 428 | vec!["a", "d"], 429 | vec!["b", "d"], 430 | vec!["a", "b", "d"], 431 | vec!["c", "d"], 432 | vec!["a", "c", "d"], 433 | vec!["b", "c", "d"], 434 | vec!["a", "b", "c", "d"], 435 | ]); 436 | let filtered = feature_powerset(&list, None, &[], &[], &map); 437 | assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec!["c", "d"]]); 438 | } 439 | 440 | #[test] 441 | fn powerset_full() { 442 | let v = powerset(vec![1, 2, 3, 4], None); 443 | assert_eq!(v, vec![ 444 | vec![], 445 | vec![1], 446 | vec![2], 447 | vec![1, 2], 448 | vec![3], 449 | vec![1, 3], 450 | vec![2, 3], 451 | vec![1, 2, 3], 452 | vec![4], 453 | vec![1, 4], 454 | vec![2, 4], 455 | vec![1, 2, 4], 456 | vec![3, 4], 457 | vec![1, 3, 4], 458 | vec![2, 3, 4], 459 | vec![1, 2, 3, 4], 460 | ]); 461 | } 462 | 463 | #[test] 464 | fn powerset_depth1() { 465 | let v = powerset(vec![1, 2, 3, 4], Some(1)); 466 | assert_eq!(v, vec![vec![], vec![1], vec![2], vec![3], vec![4],]); 467 | } 468 | 469 | #[test] 470 | fn powerset_depth2() { 471 | let v = powerset(vec![1, 2, 3, 4], Some(2)); 472 | assert_eq!(v, vec![ 473 | vec![], 474 | vec![1], 475 | vec![2], 476 | vec![1, 2], 477 | vec![3], 478 | vec![1, 3], 479 | vec![2, 3], 480 | vec![4], 481 | vec![1, 4], 482 | vec![2, 4], 483 | vec![3, 4], 484 | ]); 485 | } 486 | 487 | #[test] 488 | fn powerset_depth3() { 489 | let v = powerset(vec![1, 2, 3, 4], Some(3)); 490 | assert_eq!(v, vec![ 491 | vec![], 492 | vec![1], 493 | vec![2], 494 | vec![1, 2], 495 | vec![3], 496 | vec![1, 3], 497 | vec![2, 3], 498 | vec![1, 2, 3], 499 | vec![4], 500 | vec![1, 4], 501 | vec![2, 4], 502 | vec![1, 2, 4], 503 | vec![3, 4], 504 | vec![1, 3, 4], 505 | vec![2, 3, 4], 506 | ]); 507 | } 508 | } 509 | -------------------------------------------------------------------------------- /src/fs.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 OR MIT 2 | 3 | use std::path::Path; 4 | 5 | use anyhow::{Context as _, Result}; 6 | 7 | /// A wrapper for [`std::fs::write`]. 8 | pub(crate) fn write(path: impl AsRef, contents: impl AsRef<[u8]>) -> Result<()> { 9 | let path = path.as_ref(); 10 | let res = std::fs::write(path, contents.as_ref()); 11 | res.with_context(|| format!("failed to write to file `{}`", path.display())) 12 | } 13 | 14 | /// A wrapper for [`std::fs::read`]. 15 | pub(crate) fn read(path: impl AsRef) -> Result> { 16 | let path = path.as_ref(); 17 | let res = std::fs::read(path); 18 | res.with_context(|| format!("failed to read from file `{}`", path.display())) 19 | } 20 | 21 | /// A wrapper for [`std::fs::read_to_string`]. 22 | pub(crate) fn read_to_string(path: impl AsRef) -> Result { 23 | let path = path.as_ref(); 24 | let res = std::fs::read_to_string(path); 25 | res.with_context(|| format!("failed to read from file `{}`", path.display())) 26 | } 27 | -------------------------------------------------------------------------------- /src/manifest.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 OR MIT 2 | 3 | use std::{ 4 | collections::{BTreeMap, BTreeSet}, 5 | path::Path, 6 | }; 7 | 8 | use anyhow::{Context as _, Result, bail, format_err}; 9 | 10 | use crate::{context::Context, fs, term}; 11 | 12 | type ParseResult = Result; 13 | 14 | // Cargo manifest 15 | // https://doc.rust-lang.org/nightly/cargo/reference/manifest.html 16 | pub(crate) struct Manifest { 17 | raw: String, 18 | doc: toml_edit::DocumentMut, 19 | pub(crate) package: Package, 20 | pub(crate) features: BTreeMap>, 21 | } 22 | 23 | impl Manifest { 24 | pub(crate) fn new(path: &Path, metadata_cargo_version: u32) -> Result { 25 | let raw = fs::read_to_string(path)?; 26 | let doc: toml_edit::DocumentMut = raw 27 | .parse() 28 | .with_context(|| format!("failed to parse manifest `{}` as toml", path.display()))?; 29 | let package = Package::from_table(&doc, metadata_cargo_version).map_err(|s| { 30 | format_err!("failed to parse `{s}` field from manifest `{}`", path.display()) 31 | })?; 32 | let features = Features::from_table(&doc).map_err(|s| { 33 | format_err!("failed to parse `{s}` field from manifest `{}`", path.display()) 34 | })?; 35 | Ok(Self { raw, doc, package, features }) 36 | } 37 | } 38 | 39 | pub(crate) struct Package { 40 | // `metadata.package.publish` requires Rust 1.39 41 | pub(crate) publish: Option, 42 | // `metadata.package.rust_version` requires Rust 1.58 43 | #[allow(clippy::option_option)] 44 | pub(crate) rust_version: Option>, 45 | } 46 | 47 | impl Package { 48 | fn from_table(doc: &toml_edit::DocumentMut, metadata_cargo_version: u32) -> ParseResult { 49 | let package = doc.get("package").and_then(toml_edit::Item::as_table).ok_or("package")?; 50 | 51 | Ok(Self { 52 | // Publishing is unrestricted if `true` or the field is not 53 | // specified, and forbidden if `false` or the array is empty. 54 | publish: if metadata_cargo_version >= 39 { 55 | None // Use `metadata.package.publish` instead. 56 | } else { 57 | Some(match package.get("publish") { 58 | None => true, 59 | Some(toml_edit::Item::Value(toml_edit::Value::Boolean(b))) => *b.value(), 60 | Some(toml_edit::Item::Value(toml_edit::Value::Array(a))) => !a.is_empty(), 61 | Some(_) => return Err("publish"), 62 | }) 63 | }, 64 | rust_version: if metadata_cargo_version >= 58 { 65 | None // use `metadata.package.rust_version` instead. 66 | } else { 67 | Some(match package.get("rust-version").map(toml_edit::Item::as_str) { 68 | None => None, 69 | Some(Some(v)) => Some(v.to_owned()), 70 | Some(None) => return Err("rust-version"), 71 | }) 72 | }, 73 | }) 74 | } 75 | } 76 | 77 | struct Features {} 78 | 79 | impl Features { 80 | fn from_table(doc: &toml_edit::DocumentMut) -> ParseResult>> { 81 | let features = match doc.get("features") { 82 | Some(features) => features.as_table().ok_or("features")?, 83 | None => return Ok(BTreeMap::new()), 84 | }; 85 | let mut res = BTreeMap::new(); 86 | for (name, values) in features { 87 | res.insert( 88 | name.to_owned(), 89 | values 90 | .as_array() 91 | .ok_or("features")? 92 | .into_iter() 93 | .filter_map(toml_edit::Value::as_str) 94 | .map(str::to_owned) 95 | .collect(), 96 | ); 97 | } 98 | Ok(res) 99 | } 100 | } 101 | 102 | pub(crate) fn with(cx: &Context, f: impl FnOnce() -> Result<()>) -> Result<()> { 103 | // TODO: provide option to keep updated Cargo.lock 104 | let restore_lockfile = true; 105 | let no_dev_deps = cx.no_dev_deps | cx.remove_dev_deps; 106 | let no_private = cx.no_private; 107 | if no_dev_deps || no_private { 108 | let workspace_root = &cx.metadata.workspace_root; 109 | let root_manifest = &workspace_root.join("Cargo.toml"); 110 | let mut root_id = None; 111 | let mut private_crates = BTreeSet::new(); 112 | for id in &cx.metadata.workspace_members { 113 | let package = cx.packages(id); 114 | let manifest_path = &*package.manifest_path; 115 | let is_root = manifest_path == root_manifest; 116 | if is_root { 117 | root_id = Some(id); 118 | } 119 | let is_private = cx.is_private(id); 120 | if is_private && no_private { 121 | if is_root { 122 | bail!( 123 | "--no-private is not supported yet with workspace with private root crate" 124 | ); 125 | } 126 | private_crates.insert(manifest_path); 127 | } else if is_root && no_private { 128 | // This case is handled in the if block after loop. 129 | } else if no_dev_deps { 130 | let manifest = cx.manifests(id); 131 | let mut doc = manifest.doc.clone(); 132 | if term::verbose() { 133 | info!("removing dev-dependencies from {}", manifest_path.display()); 134 | } 135 | remove_dev_deps(&mut doc); 136 | cx.restore.register(manifest.raw.clone(), manifest_path); 137 | fs::write(manifest_path, doc.to_string())?; 138 | } 139 | } 140 | if no_private && (no_dev_deps && root_id.is_some() || !private_crates.is_empty()) { 141 | let manifest_path = root_manifest; 142 | let (mut doc, orig) = match root_id { 143 | Some(id) => { 144 | let manifest = cx.manifests(id); 145 | (manifest.doc.clone(), manifest.raw.clone()) 146 | } 147 | None => { 148 | let orig = fs::read_to_string(manifest_path)?; 149 | ( 150 | orig.parse().with_context(|| { 151 | format!( 152 | "failed to parse manifest `{}` as toml", 153 | manifest_path.display() 154 | ) 155 | })?, 156 | orig, 157 | ) 158 | } 159 | }; 160 | if no_dev_deps && root_id.is_some() { 161 | if term::verbose() { 162 | info!("removing dev-dependencies from {}", manifest_path.display()); 163 | } 164 | remove_dev_deps(&mut doc); 165 | } 166 | if !private_crates.is_empty() { 167 | if term::verbose() { 168 | info!("removing private crates from {}", manifest_path.display()); 169 | } 170 | remove_private_crates(&mut doc, workspace_root, private_crates); 171 | } 172 | cx.restore.register(orig, manifest_path); 173 | fs::write(manifest_path, doc.to_string())?; 174 | } 175 | if restore_lockfile { 176 | let lockfile = &workspace_root.join("Cargo.lock"); 177 | if lockfile.exists() { 178 | cx.restore.register(fs::read(lockfile)?, lockfile); 179 | } 180 | } 181 | } 182 | 183 | f()?; 184 | 185 | // Restore original Cargo.toml and Cargo.lock. 186 | cx.restore.restore_all(); 187 | 188 | Ok(()) 189 | } 190 | 191 | fn remove_dev_deps(doc: &mut toml_edit::DocumentMut) { 192 | const KEY: &str = "dev-dependencies"; 193 | let table = doc.as_table_mut(); 194 | table.remove(KEY); 195 | if let Some(table) = table.get_mut("target").and_then(toml_edit::Item::as_table_like_mut) { 196 | for (_, val) in table.iter_mut() { 197 | if let Some(table) = val.as_table_like_mut() { 198 | table.remove(KEY); 199 | } 200 | } 201 | } 202 | } 203 | 204 | fn remove_private_crates( 205 | doc: &mut toml_edit::DocumentMut, 206 | workspace_root: &Path, 207 | mut private_crates: BTreeSet<&Path>, 208 | ) { 209 | let table = doc.as_table_mut(); 210 | if let Some(workspace) = table.get_mut("workspace").and_then(toml_edit::Item::as_table_like_mut) 211 | { 212 | if let Some(members) = workspace.get_mut("members").and_then(toml_edit::Item::as_array_mut) 213 | { 214 | let mut i = 0; 215 | while i < members.len() { 216 | if let Some(member) = members.get(i).and_then(toml_edit::Value::as_str) { 217 | let manifest_path = workspace_root.join(member).join("Cargo.toml"); 218 | if let Some(p) = private_crates.iter().find_map(|p| { 219 | same_file::is_same_file(p, &manifest_path) 220 | .ok() 221 | .and_then(|v| if v { Some(*p) } else { None }) 222 | }) { 223 | members.remove(i); 224 | private_crates.remove(p); 225 | continue; 226 | } 227 | } 228 | i += 1; 229 | } 230 | } 231 | if private_crates.is_empty() { 232 | return; 233 | } 234 | // Handles the case that the members field contains glob. 235 | // TODO: test that it also works when public and private crates are nested. 236 | if let Some(exclude) = workspace.get_mut("exclude").and_then(toml_edit::Item::as_array_mut) 237 | { 238 | for private_crate in private_crates { 239 | exclude.push(private_crate.parent().unwrap().to_str().unwrap()); 240 | } 241 | } else { 242 | workspace.insert( 243 | "exclude", 244 | toml_edit::Item::Value(toml_edit::Value::Array( 245 | private_crates 246 | .iter() 247 | .map(|p| { 248 | toml_edit::Value::String(toml_edit::Formatted::new( 249 | p.parent().unwrap().to_str().unwrap().to_owned(), 250 | )) 251 | }) 252 | .collect::(), 253 | )), 254 | ); 255 | } 256 | } 257 | } 258 | 259 | #[cfg(test)] 260 | mod tests { 261 | use super::remove_dev_deps; 262 | 263 | macro_rules! test { 264 | ($name:ident, $input:expr, $expected:expr) => { 265 | #[test] 266 | fn $name() { 267 | let mut doc: toml_edit::DocumentMut = $input.parse().unwrap(); 268 | remove_dev_deps(&mut doc); 269 | assert_eq!($expected, doc.to_string()); 270 | } 271 | }; 272 | } 273 | 274 | test!( 275 | a, 276 | "\ 277 | [package] 278 | [dependencies] 279 | [[example]] 280 | [dev-dependencies.serde] 281 | [dev-dependencies]", 282 | "\ 283 | [package] 284 | [dependencies] 285 | [[example]] 286 | " 287 | ); 288 | 289 | test!( 290 | b, 291 | "\ 292 | [package] 293 | [dependencies] 294 | [[example]] 295 | [dev-dependencies.serde] 296 | [dev-dependencies] 297 | ", 298 | "\ 299 | [package] 300 | [dependencies] 301 | [[example]] 302 | " 303 | ); 304 | 305 | test!( 306 | c, 307 | "\ 308 | [dev-dependencies] 309 | foo = { features = [] } 310 | bar = \"0.1\" 311 | ", 312 | "\ 313 | " 314 | ); 315 | 316 | test!( 317 | d, 318 | "\ 319 | [dev-dependencies.foo] 320 | features = [] 321 | 322 | [dev-dependencies] 323 | bar = { features = [], a = [] } 324 | 325 | [dependencies] 326 | bar = { features = [], a = [] } 327 | ", 328 | " 329 | [dependencies] 330 | bar = { features = [], a = [] } 331 | " 332 | ); 333 | 334 | test!( 335 | many_lines, 336 | "\ 337 | [package]\n\n 338 | 339 | [dev-dependencies.serde] 340 | 341 | 342 | [dev-dependencies] 343 | ", 344 | "\ 345 | [package] 346 | " 347 | ); 348 | 349 | test!( 350 | target_deps1, 351 | "\ 352 | [package] 353 | 354 | [target.'cfg(unix)'.dev-dependencies] 355 | 356 | [dependencies] 357 | ", 358 | "\ 359 | [package] 360 | 361 | [dependencies] 362 | " 363 | ); 364 | 365 | test!( 366 | target_deps2, 367 | "\ 368 | [package] 369 | 370 | [target.'cfg(unix)'.dev-dependencies] 371 | foo = \"0.1\" 372 | 373 | [target.'cfg(unix)'.dev-dependencies.bar] 374 | 375 | [dev-dependencies] 376 | foo = \"0.1\" 377 | 378 | [target.'cfg(unix)'.dependencies] 379 | foo = \"0.1\" 380 | ", 381 | "\ 382 | [package] 383 | 384 | [target.'cfg(unix)'.dependencies] 385 | foo = \"0.1\" 386 | " 387 | ); 388 | 389 | test!( 390 | target_deps3, 391 | "\ 392 | [package] 393 | 394 | [target.'cfg(unix)'.dependencies] 395 | 396 | [dev-dependencies] 397 | ", 398 | "\ 399 | [package] 400 | 401 | [target.'cfg(unix)'.dependencies] 402 | " 403 | ); 404 | 405 | test!( 406 | target_deps4, 407 | "\ 408 | [package] 409 | 410 | [target.'cfg(unix)'.dev-dependencies] 411 | ", 412 | "\ 413 | [package] 414 | " 415 | ); 416 | 417 | test!( 418 | not_table_multi_line, 419 | "\ 420 | [package] 421 | foo = [ 422 | ['dev-dependencies'], 423 | [\"dev-dependencies\"] 424 | ] 425 | ", 426 | "\ 427 | [package] 428 | foo = [ 429 | ['dev-dependencies'], 430 | [\"dev-dependencies\"] 431 | ] 432 | " 433 | ); 434 | } 435 | -------------------------------------------------------------------------------- /src/metadata.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 OR MIT 2 | 3 | // Refs: 4 | // - https://doc.rust-lang.org/nightly/cargo/commands/cargo-metadata.html#output-format 5 | // - https://github.com/rust-lang/cargo/blob/0.47.0/src/cargo/ops/cargo_output_metadata.rs#L56-L63 6 | // - https://github.com/rust-lang/cargo/blob/0.47.0/src/cargo/core/package.rs#L57-L80 7 | // - https://github.com/oli-obk/cargo_metadata 8 | 9 | use std::{ 10 | collections::{BTreeMap, HashMap}, 11 | ffi::OsStr, 12 | path::{Path, PathBuf}, 13 | rc::Rc, 14 | }; 15 | 16 | use anyhow::{Context as _, Result, format_err}; 17 | use cargo_config2::Config; 18 | use serde_json::{Map, Value}; 19 | 20 | use crate::{cargo, cli::Args, fs, process::ProcessBuilder, restore, term}; 21 | 22 | type Object = Map; 23 | type ParseResult = Result; 24 | 25 | /// An opaque unique identifier for referring to the package. 26 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 27 | pub(crate) struct PackageId { 28 | repr: Rc, 29 | } 30 | 31 | impl From for PackageId { 32 | fn from(repr: String) -> Self { 33 | Self { repr: repr.into() } 34 | } 35 | } 36 | 37 | pub(crate) struct Metadata { 38 | pub(crate) cargo_version: u32, 39 | /// List of all packages in the workspace and all feature-enabled dependencies. 40 | // 41 | /// This doesn't contain dependencies if cargo-metadata is run with --no-deps. 42 | pub(crate) packages: HashMap, 43 | /// List of members of the workspace. 44 | pub(crate) workspace_members: Vec, 45 | /// The resolved dependency graph for the entire workspace. 46 | pub(crate) resolve: Resolve, 47 | /// The absolute path to the root of the workspace. 48 | pub(crate) workspace_root: PathBuf, 49 | } 50 | 51 | impl Metadata { 52 | pub(crate) fn new( 53 | manifest_path: Option<&str>, 54 | cargo: &OsStr, 55 | mut cargo_version: u32, 56 | args: &Args, 57 | restore: &mut restore::Manager, 58 | ) -> Result { 59 | let stable_cargo_version = 60 | cargo::version(cmd!("rustup", "run", "stable", "cargo")).map(|v| v.minor).unwrap_or(0); 61 | 62 | let config; 63 | let include_deps_features = if args.include_deps_features { 64 | config = Config::load()?; 65 | let targets = config.build_target_for_cli(&args.target)?; 66 | let host = config.host_triple()?; 67 | Some((targets, host)) 68 | } else { 69 | None 70 | }; 71 | 72 | let mut cmd; 73 | let append_metadata_args = |cmd: &mut ProcessBuilder<'_>| { 74 | cmd.arg("metadata"); 75 | cmd.arg("--format-version=1"); 76 | if let Some(manifest_path) = manifest_path { 77 | cmd.arg("--manifest-path"); 78 | cmd.arg(manifest_path); 79 | } 80 | if let Some((targets, host)) = &include_deps_features { 81 | if targets.is_empty() { 82 | cmd.arg("--filter-platform"); 83 | cmd.arg(host); 84 | } else { 85 | for target in targets { 86 | cmd.arg("--filter-platform"); 87 | cmd.arg(target); 88 | } 89 | } 90 | // features-related flags are unneeded when --no-deps is used. 91 | // TODO: 92 | // cmd.arg("--all-features"); 93 | } else { 94 | cmd.arg("--no-deps"); 95 | } 96 | }; 97 | let json = if stable_cargo_version > cargo_version { 98 | cmd = cmd!(cargo, "metadata", "--format-version=1", "--no-deps"); 99 | if let Some(manifest_path) = manifest_path { 100 | cmd.arg("--manifest-path"); 101 | cmd.arg(manifest_path); 102 | } 103 | let no_deps_raw = cmd.read()?; 104 | let no_deps: Object = serde_json::from_str(&no_deps_raw) 105 | .with_context(|| format!("failed to parse output from {cmd}"))?; 106 | let lockfile = 107 | Path::new(no_deps["workspace_root"].as_str().unwrap()).join("Cargo.lock"); 108 | if !lockfile.exists() { 109 | let mut cmd = cmd!(cargo, "generate-lockfile"); 110 | if let Some(manifest_path) = manifest_path { 111 | cmd.arg("--manifest-path"); 112 | cmd.arg(manifest_path); 113 | } 114 | cmd.run_with_output()?; 115 | } 116 | let guard = term::verbose::scoped(false); 117 | restore.register_always(fs::read(&lockfile)?, lockfile); 118 | // Try with stable cargo because if workspace member has 119 | // a dependency that requires newer cargo features, `cargo metadata` 120 | // with older cargo may fail. 121 | cmd = cmd!("rustup", "run", "stable", "cargo"); 122 | append_metadata_args(&mut cmd); 123 | let json = cmd.read(); 124 | restore.restore_last()?; 125 | drop(guard); 126 | match json { 127 | Ok(json) => { 128 | cargo_version = stable_cargo_version; 129 | json 130 | } 131 | Err(_e) => { 132 | if include_deps_features.is_some() { 133 | // If failed, try again with the version of cargo we will actually use. 134 | cmd = cmd!(cargo); 135 | append_metadata_args(&mut cmd); 136 | cmd.read()? 137 | } else { 138 | no_deps_raw 139 | } 140 | } 141 | } 142 | } else { 143 | cmd = cmd!(cargo); 144 | append_metadata_args(&mut cmd); 145 | cmd.read()? 146 | }; 147 | 148 | let map = serde_json::from_str(&json) 149 | .with_context(|| format!("failed to parse output from {cmd}"))?; 150 | Self::from_obj(map, cargo_version) 151 | .map_err(|s| format_err!("failed to parse `{s}` field from metadata")) 152 | } 153 | 154 | fn from_obj(mut map: Object, cargo_version: u32) -> ParseResult { 155 | let workspace_members: Vec<_> = map 156 | .remove_array("workspace_members")? 157 | .into_iter() 158 | .map(|v| into_string(v).ok_or("workspace_members")) 159 | .collect::>()?; 160 | Ok(Self { 161 | cargo_version, 162 | packages: map 163 | .remove_array("packages")? 164 | .into_iter() 165 | .map(|v| Package::from_value(v, cargo_version)) 166 | .collect::>()?, 167 | workspace_members, 168 | resolve: match map.remove_nullable("resolve", into_object)? { 169 | Some(resolve) => Resolve::from_obj(resolve, cargo_version)?, 170 | None => Resolve { nodes: HashMap::default() }, 171 | }, 172 | workspace_root: map.remove_string("workspace_root")?, 173 | }) 174 | } 175 | } 176 | 177 | /// The resolved dependency graph for the entire workspace. 178 | pub(crate) struct Resolve { 179 | /// Nodes in a dependency graph. 180 | /// 181 | /// This is always empty if cargo-metadata is run with --no-deps. 182 | pub(crate) nodes: HashMap, 183 | } 184 | 185 | impl Resolve { 186 | fn from_obj(mut map: Object, cargo_version: u32) -> ParseResult { 187 | Ok(Self { 188 | nodes: map 189 | .remove_array("nodes")? 190 | .into_iter() 191 | .map(|v| Node::from_value(v, cargo_version)) 192 | .collect::>()?, 193 | }) 194 | } 195 | } 196 | 197 | /// A node in a dependency graph. 198 | pub(crate) struct Node { 199 | /// The dependencies of this package. 200 | /// 201 | /// This is always empty if running with a version of Cargo older than 1.30. 202 | pub(crate) deps: Vec, 203 | } 204 | 205 | impl Node { 206 | fn from_value(mut value: Value, cargo_version: u32) -> ParseResult<(PackageId, Self)> { 207 | let map = value.as_object_mut().ok_or("nodes")?; 208 | 209 | let id = map.remove_string("id")?; 210 | Ok((id, Self { 211 | // This field was added in Rust 1.30. 212 | deps: if cargo_version >= 30 { 213 | map.remove_array("deps")? 214 | .into_iter() 215 | .map(|v| NodeDep::from_value(v, cargo_version)) 216 | .collect::>()? 217 | } else { 218 | vec![] 219 | }, 220 | })) 221 | } 222 | } 223 | 224 | /// A dependency in a node. 225 | pub(crate) struct NodeDep { 226 | /// The Package ID of the dependency. 227 | pub(crate) pkg: PackageId, 228 | /// The kinds of dependencies. 229 | /// 230 | /// This is always empty if running with a version of Cargo older than 1.41. 231 | pub(crate) dep_kinds: Vec, 232 | } 233 | 234 | impl NodeDep { 235 | fn from_value(mut value: Value, cargo_version: u32) -> ParseResult { 236 | let map = value.as_object_mut().ok_or("deps")?; 237 | 238 | Ok(Self { 239 | pkg: map.remove_string("pkg")?, 240 | // This field was added in Rust 1.41. 241 | dep_kinds: if cargo_version >= 41 { 242 | map.remove_array("dep_kinds")? 243 | .into_iter() 244 | .map(DepKindInfo::from_value) 245 | .collect::>()? 246 | } else { 247 | vec![] 248 | }, 249 | }) 250 | } 251 | } 252 | 253 | /// Information about a dependency kind. 254 | pub(crate) struct DepKindInfo { 255 | /// The kind of dependency. 256 | pub(crate) kind: Option, 257 | /// The target platform for the dependency. 258 | /// This is `None` if it is not a target dependency. 259 | pub(crate) target: Option, 260 | } 261 | 262 | impl DepKindInfo { 263 | fn from_value(mut value: Value) -> ParseResult { 264 | let map = value.as_object_mut().ok_or("dep_kinds")?; 265 | 266 | Ok(Self { 267 | kind: map.remove_nullable("kind", into_string)?, 268 | target: map.remove_nullable("target", into_string)?, 269 | }) 270 | } 271 | } 272 | 273 | pub(crate) struct Package { 274 | /// The name of the package. 275 | pub(crate) name: String, 276 | // /// The version of the package. 277 | // pub(crate) version: String, 278 | /// List of dependencies of this particular package. 279 | pub(crate) dependencies: Vec, 280 | /// Features provided by the crate, mapped to the features required by that feature. 281 | pub(crate) features: BTreeMap>, 282 | /// Absolute path to this package's manifest. 283 | pub(crate) manifest_path: PathBuf, 284 | /// List of registries to which this package may be published. 285 | /// 286 | /// This is always `true` if running with a version of Cargo older than 1.39. 287 | pub(crate) publish: bool, 288 | /// The minimum supported Rust version of this package. 289 | /// 290 | /// This is always `None` if running with a version of Cargo older than 1.58. 291 | pub(crate) rust_version: Option, 292 | } 293 | 294 | impl Package { 295 | fn from_value(mut value: Value, cargo_version: u32) -> ParseResult<(PackageId, Self)> { 296 | let map = value.as_object_mut().ok_or("packages")?; 297 | 298 | let id = map.remove_string("id")?; 299 | Ok((id, Self { 300 | name: map.remove_string("name")?, 301 | // version: map.remove_string("version")?, 302 | dependencies: map 303 | .remove_array("dependencies")? 304 | .into_iter() 305 | .map(Dependency::from_value) 306 | .collect::>()?, 307 | features: map 308 | .remove_object("features")? 309 | .into_iter() 310 | .map(|(k, v)| { 311 | into_array(v) 312 | .and_then(|v| v.into_iter().map(into_string).collect::>()) 313 | .map(|v| (k, v)) 314 | }) 315 | .collect::>() 316 | .ok_or("features")?, 317 | manifest_path: map.remove_string("manifest_path")?, 318 | // This field was added in Rust 1.39. 319 | publish: if cargo_version >= 39 { 320 | // Publishing is unrestricted if null, and forbidden if an empty array. 321 | map.remove_nullable("publish", into_array)?.map_or(true, |a| !a.is_empty()) 322 | } else { 323 | true 324 | }, 325 | // This field was added in Rust 1.58. 326 | rust_version: if cargo_version >= 58 { 327 | map.remove_nullable("rust_version", into_string)? 328 | } else { 329 | None 330 | }, 331 | })) 332 | } 333 | 334 | pub(crate) fn optional_deps(&self) -> impl Iterator + '_ { 335 | self.dependencies.iter().filter_map(Dependency::as_feature) 336 | } 337 | } 338 | 339 | /// A dependency of the main crate. 340 | pub(crate) struct Dependency { 341 | /// The name of the dependency. 342 | pub(crate) name: String, 343 | // /// The version requirement for the dependency. 344 | // pub(crate) req: String, 345 | /// Whether or not this is an optional dependency. 346 | pub(crate) optional: bool, 347 | // TODO: support this 348 | // /// The target platform for the dependency. 349 | // /// This is `None` if it is not a target dependency. 350 | // pub(crate) target: Option, 351 | /// If the dependency is renamed, this is the new name for the dependency 352 | /// as a string. 353 | /// This is `None` if it is not renamed. 354 | /// 355 | /// This is always `None` if running with a version of Cargo older than 1.26. 356 | pub(crate) rename: Option, 357 | } 358 | 359 | impl Dependency { 360 | fn from_value(mut value: Value) -> ParseResult { 361 | let map = value.as_object_mut().ok_or("dependencies")?; 362 | 363 | Ok(Self { 364 | name: map.remove_string("name")?, 365 | // req: map.remove_string("req")?, 366 | optional: map.get("optional").and_then(Value::as_bool).ok_or("optional")?, 367 | // This field was added in Rust 1.26. 368 | rename: map.remove_nullable("rename", into_string)?, 369 | }) 370 | } 371 | 372 | pub(crate) fn as_feature(&self) -> Option<&str> { 373 | if self.optional { Some(self.rename.as_ref().unwrap_or(&self.name)) } else { None } 374 | } 375 | } 376 | 377 | #[allow(clippy::option_option)] 378 | fn allow_null(value: Value, f: impl FnOnce(Value) -> Option) -> Option> { 379 | if value.is_null() { Some(None) } else { f(value).map(Some) } 380 | } 381 | 382 | fn into_string>(value: Value) -> Option { 383 | if let Value::String(string) = value { Some(string.into()) } else { None } 384 | } 385 | fn into_array(value: Value) -> Option> { 386 | if let Value::Array(array) = value { Some(array) } else { None } 387 | } 388 | fn into_object(value: Value) -> Option { 389 | if let Value::Object(object) = value { Some(object) } else { None } 390 | } 391 | 392 | trait ObjectExt { 393 | fn remove_string>(&mut self, key: &'static str) -> ParseResult; 394 | fn remove_array(&mut self, key: &'static str) -> ParseResult>; 395 | fn remove_object(&mut self, key: &'static str) -> ParseResult; 396 | fn remove_nullable( 397 | &mut self, 398 | key: &'static str, 399 | f: impl FnOnce(Value) -> Option, 400 | ) -> ParseResult>; 401 | } 402 | 403 | impl ObjectExt for Object { 404 | fn remove_string>(&mut self, key: &'static str) -> ParseResult { 405 | self.remove(key).and_then(into_string).ok_or(key) 406 | } 407 | fn remove_array(&mut self, key: &'static str) -> ParseResult> { 408 | self.remove(key).and_then(into_array).ok_or(key) 409 | } 410 | fn remove_object(&mut self, key: &'static str) -> ParseResult { 411 | self.remove(key).and_then(into_object).ok_or(key) 412 | } 413 | fn remove_nullable( 414 | &mut self, 415 | key: &'static str, 416 | f: impl FnOnce(Value) -> Option, 417 | ) -> ParseResult> { 418 | self.remove(key).and_then(|v| allow_null(v, f)).ok_or(key) 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /src/process.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 OR MIT 2 | 3 | use std::{ 4 | env, 5 | ffi::{OsStr, OsString}, 6 | fmt, 7 | path::Path, 8 | process::{Command, ExitStatus, Output}, 9 | rc::Rc, 10 | str, 11 | }; 12 | 13 | use anyhow::{Context as _, Error, Result}; 14 | 15 | use crate::{Context, PackageId, term}; 16 | 17 | macro_rules! cmd { 18 | ($program:expr $(, $arg:expr)* $(,)?) => {{ 19 | let mut _cmd = $crate::process::ProcessBuilder::new($program); 20 | $( 21 | _cmd.arg($arg); 22 | )* 23 | _cmd 24 | }}; 25 | } 26 | 27 | // A builder for an external process, inspired by https://github.com/rust-lang/cargo/blob/0.47.0/src/cargo/util/process_builder.rs 28 | // 29 | // The fields will be expanded in the following order: 30 | // [--features ] [ -- ] 31 | #[derive(Clone)] 32 | #[must_use] 33 | pub(crate) struct ProcessBuilder<'a> { 34 | /// The program to execute. 35 | program: Rc, 36 | /// A list of arguments to pass to the program (until '--'). 37 | propagated_leading_args: &'a [String], 38 | /// A list of arguments to pass to the program (after '--'). 39 | trailing_args: &'a [String], 40 | 41 | /// A list of arguments to pass to the program (between `program` and 'propagated_leading_args'). 42 | leading_args: Vec, 43 | /// A list of arguments to pass to the program (between `propagated_leading_args` and '--'). 44 | args: Vec, 45 | /// A comma-separated list of features. 46 | /// This list always has a trailing comma if it is not empty. 47 | // cargo less than Rust 1.38 cannot handle multiple '--features' flags, so it creates another String. 48 | features: String, 49 | pub(crate) strip_program_path: bool, 50 | } 51 | 52 | impl<'a> ProcessBuilder<'a> { 53 | /// Creates a new `ProcessBuilder`. 54 | pub(crate) fn new(program: impl Into) -> Self { 55 | Self { 56 | program: program.into().into(), 57 | propagated_leading_args: &[], 58 | trailing_args: &[], 59 | leading_args: vec![], 60 | args: vec![], 61 | features: String::new(), 62 | strip_program_path: false, 63 | } 64 | } 65 | 66 | /// Adds an argument to pass to the program. 67 | pub(crate) fn arg(&mut self, arg: impl Into) -> &mut Self { 68 | self.args.push(arg.into()); 69 | self 70 | } 71 | 72 | /// Adds multiple arguments to pass to the program. 73 | pub(crate) fn args( 74 | &mut self, 75 | args: impl IntoIterator>, 76 | ) -> &mut Self { 77 | self.args.extend(args.into_iter().map(Into::into)); 78 | self 79 | } 80 | 81 | /// Adds an argument to the leading arguments list. 82 | pub(crate) fn leading_arg(&mut self, arg: impl Into) -> &mut Self { 83 | self.leading_args.push(arg.into()); 84 | self 85 | } 86 | 87 | pub(crate) fn apply_context(&mut self, cx: &'a Context) -> &mut Self { 88 | self.propagated_leading_args = &cx.leading_args; 89 | self.trailing_args = &cx.trailing_args; 90 | self 91 | } 92 | 93 | pub(crate) fn append_features(&mut self, features: impl IntoIterator>) { 94 | for feature in features { 95 | self.features.push_str(feature.as_ref()); 96 | self.features.push(','); 97 | } 98 | } 99 | 100 | pub(crate) fn append_features_from_args(&mut self, cx: &Context, id: &PackageId) { 101 | if cx.ignore_unknown_features { 102 | self.append_features(cx.features.iter().filter(|&f| { 103 | if cx.pkg_features(id).contains(f) { 104 | true 105 | } else { 106 | // ignored 107 | info!("skipped applying unknown `{f}` feature to {}", cx.packages(id).name); 108 | false 109 | } 110 | })); 111 | } else if !cx.features.is_empty() { 112 | self.append_features(&cx.features); 113 | } 114 | } 115 | 116 | /// Gets the comma-separated features list 117 | fn get_features(&self) -> &str { 118 | // drop a trailing comma if it is not empty. 119 | &self.features[..self.features.len().saturating_sub(1)] 120 | } 121 | 122 | /// Executes a process, waiting for completion, and mapping non-zero exit 123 | /// status to an error. 124 | pub(crate) fn run(&self) -> Result<()> { 125 | let status = self.build().status().with_context(|| { 126 | process_error(format!("could not execute process {self:#}"), None, None) 127 | })?; 128 | if status.success() { 129 | Ok(()) 130 | } else { 131 | Err(process_error( 132 | format!("process didn't exit successfully: {self:#}"), 133 | Some(status), 134 | None, 135 | )) 136 | } 137 | } 138 | 139 | /// Executes a process, captures its stdio output, returning the captured 140 | /// output, or an error if non-zero exit status. 141 | pub(crate) fn run_with_output(&self) -> Result { 142 | let output = self.build().output().with_context(|| { 143 | process_error(format!("could not execute process {self:#}"), None, None) 144 | })?; 145 | if output.status.success() { 146 | Ok(output) 147 | } else { 148 | Err(process_error( 149 | format!("process didn't exit successfully: {self:#}"), 150 | Some(output.status), 151 | Some(&output), 152 | )) 153 | } 154 | } 155 | 156 | /// Executes a process, captures its stdio output, returning the captured 157 | /// standard output as a `String`. 158 | pub(crate) fn read(&self) -> Result { 159 | let mut output = String::from_utf8(self.run_with_output()?.stdout) 160 | .with_context(|| format!("failed to parse output from {self:#}"))?; 161 | while output.ends_with('\n') || output.ends_with('\r') { 162 | output.pop(); 163 | } 164 | Ok(output) 165 | } 166 | 167 | fn build(&self) -> Command { 168 | let mut cmd = Command::new(&*self.program); 169 | 170 | cmd.args(&*self.leading_args); 171 | cmd.args(self.propagated_leading_args); 172 | cmd.args(&self.args); 173 | if !self.features.is_empty() { 174 | cmd.arg("--features"); 175 | cmd.arg(self.get_features()); 176 | } 177 | if !self.trailing_args.is_empty() { 178 | cmd.arg("--"); 179 | cmd.args(self.trailing_args); 180 | } 181 | 182 | cmd 183 | } 184 | } 185 | 186 | impl fmt::Display for ProcessBuilder<'_> { 187 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 188 | f.write_str("`")?; 189 | 190 | if !self.strip_program_path && (f.alternate() || term::verbose()) { 191 | f.write_str(&self.program.to_string_lossy())?; 192 | } else { 193 | f.write_str(&Path::new(&*self.program).file_stem().unwrap().to_string_lossy())?; 194 | } 195 | 196 | for arg in &self.leading_args { 197 | write!(f, " {arg}")?; 198 | } 199 | 200 | for arg in self.propagated_leading_args { 201 | write!(f, " {arg}")?; 202 | } 203 | 204 | let mut args = self.args.iter(); 205 | while let Some(arg) = args.next() { 206 | if arg == "--manifest-path" { 207 | let path = Path::new(args.next().unwrap()); 208 | // Displaying `--manifest-path` is redundant. 209 | if f.alternate() || term::verbose() { 210 | let path = env::current_dir() 211 | .ok() 212 | .and_then(|cwd| path.strip_prefix(cwd).ok()) 213 | .unwrap_or(path); 214 | write!(f, " --manifest-path {}", path.display())?; 215 | } 216 | } else { 217 | write!(f, " {}", arg.to_string_lossy())?; 218 | } 219 | } 220 | 221 | if !self.features.is_empty() { 222 | write!(f, " --features {}", self.get_features())?; 223 | } 224 | 225 | if !self.trailing_args.is_empty() { 226 | f.write_str(" --")?; 227 | for arg in self.trailing_args { 228 | write!(f, " {arg}")?; 229 | } 230 | } 231 | 232 | f.write_str("`") 233 | } 234 | } 235 | 236 | // Based on https://github.com/rust-lang/cargo/blob/0.47.0/src/cargo/util/errors.rs 237 | /// Creates a new process error. 238 | /// 239 | /// `status` can be `None` if the process did not launch. 240 | /// `output` can be `None` if the process did not launch, or output was not captured. 241 | fn process_error(mut msg: String, status: Option, output: Option<&Output>) -> Error { 242 | match status { 243 | Some(s) => { 244 | msg.push_str(" ("); 245 | msg.push_str(&s.to_string()); 246 | msg.push(')'); 247 | } 248 | None => msg.push_str(" (never executed)"), 249 | } 250 | 251 | if let Some(out) = output { 252 | match str::from_utf8(&out.stdout) { 253 | Ok(s) if !s.trim_start().is_empty() => { 254 | msg.push_str("\n--- stdout\n"); 255 | msg.push_str(s); 256 | } 257 | Ok(_) | Err(_) => {} 258 | } 259 | match str::from_utf8(&out.stderr) { 260 | Ok(s) if !s.trim_start().is_empty() => { 261 | msg.push_str("\n--- stderr\n"); 262 | msg.push_str(s); 263 | } 264 | Ok(_) | Err(_) => {} 265 | } 266 | } 267 | 268 | Error::msg(msg) 269 | } 270 | -------------------------------------------------------------------------------- /src/restore.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 OR MIT 2 | 3 | use std::{ 4 | mem, 5 | path::PathBuf, 6 | sync::{Arc, Mutex}, 7 | }; 8 | 9 | use anyhow::Result; 10 | 11 | use crate::{fs, term}; 12 | 13 | #[derive(Clone)] 14 | pub(crate) struct Manager { 15 | // A flag that indicates restore is needed. 16 | needs_restore: bool, 17 | /// Information on files that need to be restored. 18 | files: Arc>>, 19 | } 20 | 21 | impl Manager { 22 | pub(crate) fn new(needs_restore: bool) -> Self { 23 | let this = Self { needs_restore, files: Arc::new(Mutex::new(vec![])) }; 24 | 25 | let cloned = this.clone(); 26 | ctrlc::set_handler(move || { 27 | cloned.restore_all(); 28 | std::process::exit(1) 29 | }) 30 | .unwrap(); 31 | 32 | this 33 | } 34 | 35 | /// Registers the given path if `needs_restore` is `true`. 36 | pub(crate) fn register(&self, contents: impl Into>, path: impl Into) { 37 | if !self.needs_restore { 38 | return; 39 | } 40 | 41 | self.register_always(contents.into(), path.into()); 42 | } 43 | 44 | /// Registers the given path regardless of the value of `needs_restore`. 45 | pub(crate) fn register_always(&self, contents: impl Into>, path: impl Into) { 46 | let mut files = self.files.lock().unwrap(); 47 | files.push(File { contents: contents.into(), path: path.into() }); 48 | } 49 | 50 | // This takes `&mut self` instead of `&self` to prevent misuse in multi-thread contexts. 51 | pub(crate) fn restore_last(&mut self) -> Result<()> { 52 | let mut files = self.files.lock().unwrap(); 53 | if let Some(file) = files.pop() { 54 | file.restore()?; 55 | } 56 | Ok(()) 57 | } 58 | 59 | pub(crate) fn restore_all(&self) { 60 | let mut files = self.files.lock().unwrap(); 61 | if !files.is_empty() { 62 | for file in mem::take(&mut *files) { 63 | if let Err(e) = file.restore() { 64 | error!("{e:#}"); 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | impl Drop for Manager { 72 | fn drop(&mut self) { 73 | self.restore_all(); 74 | } 75 | } 76 | 77 | struct File { 78 | /// The original contents of this file. 79 | contents: Vec, 80 | /// Path to this file. 81 | path: PathBuf, 82 | } 83 | 84 | impl File { 85 | fn restore(self) -> Result<()> { 86 | if term::verbose() { 87 | info!("restoring {}", self.path.display()); 88 | } 89 | fs::write(&self.path, &self.contents) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/rustup.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 OR MIT 2 | 3 | use std::str; 4 | 5 | use anyhow::{Result, bail, format_err}; 6 | 7 | use crate::{ 8 | LogGroup, PackageRuns, cargo, 9 | context::Context, 10 | version::{MaybeVersion, Version, VersionRange}, 11 | }; 12 | 13 | pub(crate) struct Rustup { 14 | pub(crate) version: u32, 15 | } 16 | 17 | impl Rustup { 18 | pub(crate) fn new() -> Self { 19 | // If failed to determine rustup version, assume the latest version. 20 | let version = minor_version() 21 | .map_err(|e| { 22 | warn!("unable to determine rustup version; assuming latest stable rustup: {e:#}"); 23 | }) 24 | .unwrap_or(u32::MAX); 25 | 26 | Self { version } 27 | } 28 | } 29 | 30 | pub(crate) fn version_range( 31 | range: VersionRange, 32 | step: u16, 33 | packages: &[PackageRuns<'_>], 34 | cx: &Context, 35 | ) -> Result> { 36 | let check = |version: &Version| { 37 | if version.major != 1 { 38 | bail!("major version must be 1"); 39 | } 40 | if let Some(patch) = version.patch { 41 | warn!( 42 | "--version-range always selects the latest patch release per minor release, \ 43 | not the specified patch release `{patch}`", 44 | ); 45 | } 46 | Ok(()) 47 | }; 48 | 49 | let mut stable_version = None; 50 | let mut get_stable_version = || -> Result { 51 | if let Some(stable_version) = stable_version { 52 | Ok(stable_version) 53 | } else { 54 | let print_output = false; 55 | install_toolchain("stable", &[], print_output, LogGroup::None)?; 56 | let version = cargo::version(cmd!("rustup", "run", "stable", "cargo"))?; 57 | stable_version = Some(version); 58 | Ok(version) 59 | } 60 | }; 61 | 62 | let mut rust_version = None; 63 | let mut get_rust_version = || -> Result { 64 | if let Some(rust_version) = rust_version { 65 | Ok(rust_version) 66 | } else { 67 | let mut lowest_msrv = None; 68 | for pkg in packages { 69 | let pkg_msrv = cx 70 | .rust_version(pkg.id) 71 | .map(str::parse::) 72 | .transpose()? 73 | .map(Version::strip_patch); 74 | lowest_msrv = match (lowest_msrv, pkg_msrv) { 75 | (Some(workspace), Some(pkg)) => { 76 | if workspace < pkg { 77 | Some(workspace) 78 | } else { 79 | Some(pkg) 80 | } 81 | } 82 | (Some(msrv), None) | (None, Some(msrv)) => Some(msrv), 83 | (None, None) => None, 84 | }; 85 | } 86 | let Some(lowest_msrv) = lowest_msrv else { 87 | bail!("no rust-version field in selected Cargo.toml's is specified") 88 | }; 89 | rust_version = Some(lowest_msrv); 90 | Ok(lowest_msrv) 91 | } 92 | }; 93 | 94 | let VersionRange { start_inclusive, end_inclusive } = range; 95 | 96 | let start_inclusive = match start_inclusive { 97 | MaybeVersion::Version(start) => { 98 | check(&start)?; 99 | start 100 | } 101 | MaybeVersion::Msrv => { 102 | let start = get_rust_version()?; 103 | check(&start)?; 104 | start 105 | } 106 | MaybeVersion::Stable => get_stable_version()?, 107 | }; 108 | 109 | let end_inclusive = match end_inclusive { 110 | MaybeVersion::Version(end) => { 111 | check(&end)?; 112 | end 113 | } 114 | MaybeVersion::Msrv => get_rust_version()?, 115 | MaybeVersion::Stable => get_stable_version()?, 116 | }; 117 | 118 | let versions: Vec<_> = (start_inclusive.minor..=end_inclusive.minor) 119 | .step_by(step as usize) 120 | .map(|minor| Version { major: 1, minor, patch: None }) 121 | .collect(); 122 | if versions.is_empty() { 123 | bail!("specified version range `{range}` is empty"); 124 | } 125 | Ok(versions) 126 | } 127 | 128 | pub(crate) fn install_toolchain( 129 | mut toolchain: &str, 130 | target: &[String], 131 | print_output: bool, 132 | log_group: LogGroup, 133 | ) -> Result<()> { 134 | toolchain = toolchain.strip_prefix('+').unwrap_or(toolchain); 135 | 136 | if target.is_empty() 137 | && cmd!("rustup", "run", toolchain, "cargo", "--version").run_with_output().is_ok() 138 | { 139 | // Do not run `rustup toolchain add` if the toolchain already has installed. 140 | return Ok(()); 141 | } 142 | 143 | // In Github Actions and Azure Pipelines, --no-self-update is necessary 144 | // because the windows environment cannot self-update rustup.exe. 145 | let mut cmd = cmd!("rustup", "toolchain", "add", toolchain, "--no-self-update"); 146 | if !target.is_empty() { 147 | cmd.args(["--target", &target.join(",")]); 148 | } 149 | 150 | if print_output { 151 | let _guard = log_group.print(&format!("running {cmd}")); 152 | // The toolchain installation can take some time, so we'll show users 153 | // the progress. 154 | cmd.run() 155 | } else { 156 | // However, in certain situations, it may be preferable not to display it. 157 | cmd.run_with_output().map(drop) 158 | } 159 | } 160 | 161 | fn minor_version() -> Result { 162 | let cmd = cmd!("rustup", "--version"); 163 | let output = cmd.read()?; 164 | 165 | let version = (|| { 166 | let mut output = output.split(' '); 167 | if output.next()? != "rustup" { 168 | return None; 169 | } 170 | output.next() 171 | })() 172 | .ok_or_else(|| format_err!("unexpected output from {cmd}: {output}"))?; 173 | let version: Version = version.parse()?; 174 | if version.major != 1 || version.patch.is_none() { 175 | bail!("unexpected output from {cmd}: {output}"); 176 | } 177 | 178 | Ok(version.minor) 179 | } 180 | -------------------------------------------------------------------------------- /src/term.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 OR MIT 2 | 3 | use std::{ 4 | env, 5 | io::Write as _, 6 | str::FromStr, 7 | sync::atomic::{AtomicBool, AtomicU8, Ordering}, 8 | }; 9 | 10 | use anyhow::{Result, format_err}; 11 | use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor as _}; 12 | 13 | #[derive(PartialEq)] 14 | #[repr(u8)] 15 | enum Coloring { 16 | Auto = 0, 17 | Always, 18 | Never, 19 | } 20 | 21 | impl Coloring { 22 | const AUTO: u8 = Self::Auto as u8; 23 | const ALWAYS: u8 = Self::Always as u8; 24 | const NEVER: u8 = Self::Never as u8; 25 | } 26 | 27 | impl FromStr for Coloring { 28 | type Err = String; 29 | 30 | fn from_str(color: &str) -> Result { 31 | match color { 32 | "auto" => Ok(Self::Auto), 33 | "always" => Ok(Self::Always), 34 | "never" => Ok(Self::Never), 35 | other => Err(format!("must be auto, always, or never, but found `{other}`")), 36 | } 37 | } 38 | } 39 | 40 | static COLORING: AtomicU8 = AtomicU8::new(Coloring::AUTO); 41 | // Errors during argument parsing are returned before set_coloring, so check is_terminal first. 42 | pub(crate) fn init_coloring() { 43 | if !std::io::IsTerminal::is_terminal(&std::io::stderr()) { 44 | COLORING.store(Coloring::NEVER, Ordering::Relaxed); 45 | } 46 | } 47 | pub(crate) fn set_coloring(color: Option<&str>) -> Result<()> { 48 | let new = match color { 49 | Some(color) => color.parse().map_err(|e| format_err!("argument for --color {e}"))?, 50 | // https://doc.rust-lang.org/nightly/cargo/reference/config.html#termcolor 51 | None => match env::var_os("CARGO_TERM_COLOR") { 52 | Some(color) => { 53 | color.to_string_lossy().parse().map_err(|e| format_err!("CARGO_TERM_COLOR {e}"))? 54 | } 55 | None => Coloring::Auto, 56 | }, 57 | }; 58 | if new == Coloring::Auto && coloring() == ColorChoice::Never { 59 | // If coloring is already set to never by init_coloring, respect it. 60 | } else { 61 | COLORING.store(new as u8, Ordering::Relaxed); 62 | } 63 | Ok(()) 64 | } 65 | fn coloring() -> ColorChoice { 66 | match COLORING.load(Ordering::Relaxed) { 67 | Coloring::AUTO => ColorChoice::Auto, 68 | Coloring::ALWAYS => ColorChoice::Always, 69 | Coloring::NEVER => ColorChoice::Never, 70 | _ => unreachable!(), 71 | } 72 | } 73 | 74 | macro_rules! global_flag { 75 | ($name:ident: $value:ty = $ty:ident::new($($default:expr)?)) => { 76 | pub(crate) mod $name { 77 | use super::*; 78 | pub(super) static VALUE: $ty = $ty::new($($default)?); 79 | pub(crate) fn set(value: $value) { 80 | VALUE.store(value, Ordering::Relaxed); 81 | } 82 | pub(crate) struct Guard { 83 | prev: $value, 84 | } 85 | impl Drop for Guard { 86 | fn drop(&mut self) { 87 | set(self.prev); 88 | } 89 | } 90 | #[allow(dead_code)] 91 | pub(crate) fn scoped(value: $value) -> Guard { 92 | Guard { prev: VALUE.swap(value, Ordering::Relaxed) } 93 | } 94 | } 95 | pub(crate) fn $name() -> $value { 96 | $name::VALUE.load(Ordering::Relaxed) 97 | } 98 | }; 99 | } 100 | global_flag!(verbose: bool = AtomicBool::new(false)); 101 | global_flag!(error: bool = AtomicBool::new(false)); 102 | global_flag!(warn: bool = AtomicBool::new(false)); 103 | 104 | pub(crate) fn print_status(status: &str, color: Option) -> StandardStream { 105 | let mut stream = StandardStream::stderr(coloring()); 106 | let _ = stream.set_color(ColorSpec::new().set_bold(true).set_fg(color)); 107 | let _ = write!(stream, "{status}"); 108 | let _ = stream.set_color(ColorSpec::new().set_bold(true)); 109 | let _ = write!(stream, ":"); 110 | let _ = stream.reset(); 111 | let _ = write!(stream, " "); 112 | stream 113 | } 114 | 115 | macro_rules! error { 116 | ($($msg:expr),* $(,)?) => {{ 117 | use std::io::Write as _; 118 | crate::term::error::set(true); 119 | let mut stream = crate::term::print_status("error", Some(termcolor::Color::Red)); 120 | let _ = writeln!(stream, $($msg),*); 121 | }}; 122 | } 123 | 124 | macro_rules! warn { 125 | ($($msg:expr),* $(,)?) => {{ 126 | use std::io::Write as _; 127 | crate::term::warn::set(true); 128 | let mut stream = crate::term::print_status("warning", Some(termcolor::Color::Yellow)); 129 | let _ = writeln!(stream, $($msg),*); 130 | }}; 131 | } 132 | 133 | macro_rules! info { 134 | ($($msg:expr),* $(,)?) => {{ 135 | use std::io::Write as _; 136 | let mut stream = crate::term::print_status("info", None); 137 | let _ = writeln!(stream, $($msg),*); 138 | }}; 139 | } 140 | -------------------------------------------------------------------------------- /src/version.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 OR MIT 2 | 3 | use std::{fmt, str::FromStr}; 4 | 5 | use anyhow::{Context as _, Error, Result, bail}; 6 | 7 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 8 | pub(crate) struct Version { 9 | pub(crate) major: u32, 10 | pub(crate) minor: u32, 11 | pub(crate) patch: Option, 12 | } 13 | 14 | impl Version { 15 | pub(crate) fn strip_patch(mut self) -> Self { 16 | self.patch = None; 17 | self 18 | } 19 | } 20 | 21 | impl fmt::Display for Version { 22 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 23 | let major = self.major; 24 | let minor = self.minor; 25 | write!(f, "{major}.{minor}")?; 26 | if let Some(patch) = self.patch { 27 | write!(f, ".{patch}")?; 28 | } 29 | Ok(()) 30 | } 31 | } 32 | 33 | impl FromStr for Version { 34 | type Err = Error; 35 | 36 | fn from_str(s: &str) -> Result { 37 | let mut digits = s.splitn(3, '.'); 38 | let major = digits.next().context("missing major version")?.parse()?; 39 | let minor = digits.next().context("missing minor version")?.parse()?; 40 | let patch = digits.next().map(str::parse).transpose()?; 41 | Ok(Self { major, minor, patch }) 42 | } 43 | } 44 | 45 | #[derive(Copy, Clone, PartialEq, Eq)] 46 | pub(crate) enum MaybeVersion { 47 | Version(Version), 48 | Msrv, 49 | Stable, 50 | } 51 | 52 | #[derive(Copy, Clone, PartialEq, Eq)] 53 | pub(crate) struct VersionRange { 54 | pub(crate) start_inclusive: MaybeVersion, 55 | pub(crate) end_inclusive: MaybeVersion, 56 | } 57 | 58 | impl VersionRange { 59 | pub(crate) fn msrv() -> Self { 60 | Self { start_inclusive: MaybeVersion::Msrv, end_inclusive: MaybeVersion::Msrv } 61 | } 62 | } 63 | 64 | impl fmt::Display for VersionRange { 65 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 66 | if let MaybeVersion::Version(start) = self.start_inclusive { 67 | write!(f, "{start}")?; 68 | } 69 | f.write_str("..")?; 70 | if let MaybeVersion::Version(end) = self.end_inclusive { 71 | write!(f, "={end}")?; 72 | } 73 | Ok(()) 74 | } 75 | } 76 | 77 | impl FromStr for VersionRange { 78 | type Err = Error; 79 | 80 | fn from_str(s: &str) -> Result { 81 | let (start, end) = if let Some((start, end)) = s.split_once("..") { 82 | let end = match end.strip_prefix('=') { 83 | Some(end) => { 84 | if end.is_empty() { 85 | // Reject inclusive range without end expression (`..=` and `..=`). (same behavior as Rust's inclusive range) 86 | bail!( 87 | "inclusive range `{s}` must have end expression; consider using `{}` or `{s}`", 88 | s.replace("..=", "..") 89 | ) 90 | } 91 | end 92 | } 93 | None => { 94 | // `..` and `..` are okay, so only warn `..`. 95 | if !end.is_empty() { 96 | warn!( 97 | "using `..` for inclusive range is deprecated; consider using `{}`", 98 | s.replace("..", "..=") 99 | ); 100 | } 101 | end 102 | } 103 | }; 104 | (start, maybe_version(end)?) 105 | } else { 106 | (s, None) 107 | }; 108 | let start_inclusive = maybe_version(start)?.unwrap_or(MaybeVersion::Msrv); 109 | let end_inclusive = end.unwrap_or(MaybeVersion::Stable); 110 | Ok(Self { start_inclusive, end_inclusive }) 111 | } 112 | } 113 | 114 | fn maybe_version(s: &str) -> Result, Error> { 115 | if s.is_empty() { Ok(None) } else { s.parse().map(MaybeVersion::Version).map(Some) } 116 | } 117 | -------------------------------------------------------------------------------- /tests/auxiliary/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 OR MIT 2 | 3 | use std::{ 4 | env, 5 | ffi::OsStr, 6 | path::{Path, PathBuf}, 7 | process::Command, 8 | str, 9 | sync::LazyLock, 10 | }; 11 | 12 | use anyhow::Context as _; 13 | pub(crate) use build_context::TARGET; 14 | use easy_ext::ext; 15 | 16 | pub(crate) fn manifest_dir() -> &'static Path { 17 | Path::new(env!("CARGO_MANIFEST_DIR")) 18 | } 19 | pub(crate) fn fixtures_dir() -> &'static Path { 20 | Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures")) 21 | } 22 | 23 | pub(crate) fn cargo_bin_exe() -> Command { 24 | let mut cmd = Command::new(env!("CARGO_BIN_EXE_cargo-hack")); 25 | cmd.env("CARGO_HACK_DENY_WARNINGS", "1"); 26 | cmd.env_remove("RUSTFLAGS"); 27 | cmd.env_remove("CARGO_TERM_COLOR"); 28 | cmd.env_remove("GITHUB_ACTIONS"); 29 | cmd 30 | } 31 | 32 | pub(crate) fn has_rustup() -> bool { 33 | Command::new("rustup").arg("--version").output().is_ok() 34 | } 35 | 36 | static TEST_VERSION: LazyLock> = LazyLock::new(|| { 37 | let toolchain = env::var_os("CARGO_HACK_TEST_TOOLCHAIN")?.to_string_lossy().parse().unwrap(); 38 | // Install toolchain first to avoid toolchain installation conflicts. 39 | let _ = Command::new("rustup") 40 | .args(["toolchain", "add", &format!("1.{toolchain}"), "--no-self-update"]) 41 | .output(); 42 | Some(toolchain) 43 | }); 44 | 45 | pub(crate) static HAS_STABLE_TOOLCHAIN: LazyLock = LazyLock::new(|| { 46 | let Ok(output) = Command::new("rustup").args(["toolchain", "list"]).output() else { 47 | return false; 48 | }; 49 | String::from_utf8(output.stdout).unwrap_or_default().contains("stable") 50 | }); 51 | 52 | pub(crate) fn cargo_hack>(args: impl AsRef<[O]>) -> Command { 53 | let args = args.as_ref(); 54 | let mut cmd = cargo_bin_exe(); 55 | cmd.arg("hack"); 56 | if let Some(toolchain) = *TEST_VERSION { 57 | if !args.iter().any(|a| { 58 | let s = a.as_ref().to_str().unwrap(); 59 | s.starts_with("--version-range") || s.starts_with("--rust-version") 60 | }) { 61 | cmd.arg(format!("--version-range=1.{toolchain}..=1.{toolchain}")); 62 | } 63 | } 64 | cmd.args(args); 65 | cmd 66 | } 67 | 68 | #[ext(CommandExt)] 69 | impl Command { 70 | #[track_caller] 71 | pub(crate) fn assert_output(&mut self, test_model: &str, require: Option) -> AssertOutput { 72 | match (*TEST_VERSION, require) { 73 | (Some(toolchain), Some(require)) if require > toolchain => { 74 | return AssertOutput(None); 75 | } 76 | _ => {} 77 | } 78 | let (_test_project, cur_dir) = test_project(test_model); 79 | let output = 80 | self.current_dir(cur_dir).output().context("could not execute process").unwrap(); 81 | AssertOutput(Some(test_helper::cli::AssertOutput { 82 | stdout: String::from_utf8_lossy(&output.stdout).into_owned(), 83 | stderr: String::from_utf8_lossy(&output.stderr) 84 | .lines() 85 | .filter(|l| { 86 | // https://github.com/taiki-e/cargo-hack/issues/239 87 | !(l.starts_with("warning:") 88 | && l.contains(": no edition set: defaulting to the 2015 edition")) 89 | }) 90 | .collect(), 91 | status: output.status, 92 | })) 93 | } 94 | 95 | #[track_caller] 96 | pub(crate) fn assert_success(&mut self, test_model: &str) -> AssertOutput { 97 | self.assert_success2(test_model, None) 98 | } 99 | 100 | #[track_caller] 101 | pub(crate) fn assert_success2( 102 | &mut self, 103 | test_model: &str, 104 | require: Option, 105 | ) -> AssertOutput { 106 | let output = self.assert_output(test_model, require); 107 | if let Some(output) = &output.0 { 108 | if !output.status.success() { 109 | panic!( 110 | "assertion failed: `self.status.success()`:\n\nSTDOUT:\n{0}\n{1}\n{0}\n\nSTDERR:\n{0}\n{2}\n{0}\n", 111 | "-".repeat(60), 112 | output.stdout, 113 | output.stderr, 114 | ); 115 | } 116 | } 117 | output 118 | } 119 | 120 | #[track_caller] 121 | pub(crate) fn assert_failure(&mut self, test_model: &str) -> AssertOutput { 122 | self.assert_failure2(test_model, None) 123 | } 124 | 125 | #[track_caller] 126 | pub(crate) fn assert_failure2( 127 | &mut self, 128 | test_model: &str, 129 | require: Option, 130 | ) -> AssertOutput { 131 | let output = self.assert_output(test_model, require); 132 | if let Some(output) = &output.0 { 133 | if output.status.success() { 134 | panic!( 135 | "assertion failed: `!self.status.success()`:\n\nSTDOUT:\n{0}\n{1}\n{0}\n\nSTDERR:\n{0}\n{2}\n{0}\n", 136 | "-".repeat(60), 137 | output.stdout, 138 | output.stderr, 139 | ); 140 | } 141 | } 142 | output 143 | } 144 | } 145 | 146 | pub(crate) struct AssertOutput(pub(crate) Option); 147 | 148 | fn replace_command(lines: &str) -> String { 149 | if lines.contains("rustup run") { 150 | lines.to_owned() 151 | } else if let Some(minor) = *TEST_VERSION { 152 | lines.replace("cargo ", &format!("rustup run 1.{minor} cargo ")) 153 | } else { 154 | lines.to_owned() 155 | } 156 | } 157 | 158 | impl AssertOutput { 159 | /// Receives a line(`\n`)-separated list of patterns and asserts whether stdout contains each pattern. 160 | #[track_caller] 161 | pub(crate) fn stdout_contains(&self, pats: impl AsRef) -> &Self { 162 | if let Some(output) = &self.0 { 163 | output.stdout_contains(replace_command(pats.as_ref())); 164 | } 165 | self 166 | } 167 | /// Receives a line(`\n`)-separated list of patterns and asserts whether stdout contains each pattern. 168 | #[track_caller] 169 | pub(crate) fn stdout_not_contains(&self, pats: impl AsRef) -> &Self { 170 | if let Some(output) = &self.0 { 171 | output.stdout_not_contains(replace_command(pats.as_ref())); 172 | } 173 | self 174 | } 175 | 176 | /// Receives a line(`\n`)-separated list of patterns and asserts whether stderr contains each pattern. 177 | #[track_caller] 178 | pub(crate) fn stderr_contains(&self, pats: impl AsRef) -> &Self { 179 | if let Some(output) = &self.0 { 180 | output.stderr_contains(replace_command(pats.as_ref())); 181 | } 182 | self 183 | } 184 | /// Receives a line(`\n`)-separated list of patterns and asserts whether stderr contains each pattern. 185 | #[track_caller] 186 | pub(crate) fn stderr_not_contains(&self, pats: impl AsRef) -> &Self { 187 | if let Some(output) = &self.0 { 188 | output.stderr_not_contains(replace_command(pats.as_ref())); 189 | } 190 | self 191 | } 192 | } 193 | 194 | #[track_caller] 195 | fn test_project(model: &str) -> (tempfile::TempDir, PathBuf) { 196 | let tmpdir = tempfile::tempdir().unwrap(); 197 | let tmpdir_path = tmpdir.path(); 198 | 199 | let model_path; 200 | let workspace_root; 201 | if model.contains('/') { 202 | let mut model = model.splitn(2, '/'); 203 | model_path = fixtures_dir().join(model.next().unwrap()); 204 | workspace_root = tmpdir_path.join(model.next().unwrap()); 205 | assert!(model.next().is_none()); 206 | } else { 207 | model_path = fixtures_dir().join(model); 208 | workspace_root = tmpdir_path.to_path_buf(); 209 | } 210 | 211 | test_helper::git::copy_tracked_files(model_path, tmpdir_path); 212 | (tmpdir, workspace_root) 213 | } 214 | -------------------------------------------------------------------------------- /tests/fixtures/default_feature_behavior/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "has_default", 4 | "no_default", 5 | ] 6 | -------------------------------------------------------------------------------- /tests/fixtures/default_feature_behavior/has_default/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "has_default" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [features] 7 | default = [] 8 | -------------------------------------------------------------------------------- /tests/fixtures/default_feature_behavior/has_default/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/default_feature_behavior/has_default/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(feature = "default")] 3 | println!("has default feature!"); 4 | #[cfg(not(feature = "default"))] 5 | println!("no default feature!"); 6 | } 7 | -------------------------------------------------------------------------------- /tests/fixtures/default_feature_behavior/no_default/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "no_default" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [features] 7 | -------------------------------------------------------------------------------- /tests/fixtures/default_feature_behavior/no_default/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/default_feature_behavior/no_default/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(feature = "default")] 3 | println!("has default feature!"); 4 | #[cfg(not(feature = "default"))] 5 | println!("no default feature!"); 6 | } 7 | -------------------------------------------------------------------------------- /tests/fixtures/keep_going/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "keep_going" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [features] 7 | a = [] 8 | 9 | [dependencies] 10 | 11 | [dev-dependencies] 12 | 13 | [workspace] 14 | -------------------------------------------------------------------------------- /tests/fixtures/keep_going/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/keep_going/src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "a")] 2 | compile_error!("`a` feature specified"); 3 | #[cfg(not(feature = "a"))] 4 | compile_error!("`a` feature not specified"); 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /tests/fixtures/namespaced_features/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "namespaced_features" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [features] 8 | explicit = ["dep:explicit"] 9 | combo = ["dep:member2", "dep:member3"] 10 | 11 | [dependencies] 12 | explicit = { path = "explicit", optional = true } # Explicitly defined as a feature with dep: 13 | implicit = { path = "implicit", optional = true } # Implicit feature as an optional dependency 14 | member1 = { path = "member1" } # Regular dependency to be ignored 15 | member2 = { path = "member2", optional = true } # Available only through the combo feature 16 | member3 = { path = "member3", optional = true } # Available only through the combo feature 17 | renamed = { path = "member4", package = "member4", optional = true } # Renamed implicit feature 18 | 19 | [workspace] 20 | resolver = "2" 21 | members = [ 22 | "explicit", 23 | "implicit", 24 | "member1", 25 | "member2", 26 | "member3", 27 | "member4", 28 | ] 29 | -------------------------------------------------------------------------------- /tests/fixtures/namespaced_features/explicit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "explicit" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [dependencies] 7 | 8 | [dev-dependencies] 9 | -------------------------------------------------------------------------------- /tests/fixtures/namespaced_features/explicit/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/namespaced_features/implicit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "implicit" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [dependencies] 7 | 8 | [dev-dependencies] 9 | -------------------------------------------------------------------------------- /tests/fixtures/namespaced_features/implicit/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/namespaced_features/member1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member1" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [dependencies] 7 | 8 | [dev-dependencies] 9 | -------------------------------------------------------------------------------- /tests/fixtures/namespaced_features/member1/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/namespaced_features/member2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member2" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [dependencies] 7 | 8 | [dev-dependencies] 9 | -------------------------------------------------------------------------------- /tests/fixtures/namespaced_features/member2/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/namespaced_features/member3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member3" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [dependencies] 7 | 8 | [dev-dependencies] 9 | -------------------------------------------------------------------------------- /tests/fixtures/namespaced_features/member3/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/namespaced_features/member4/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member4" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [dependencies] 7 | 8 | [dev-dependencies] 9 | -------------------------------------------------------------------------------- /tests/fixtures/namespaced_features/member4/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/namespaced_features/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/namespaced_features/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // Actual features 3 | #[cfg(feature = "explicit")] 4 | println!("explicit"); 5 | #[cfg(feature = "implicit")] 6 | println!("implicit"); 7 | #[cfg(feature = "combo")] 8 | println!("combo"); 9 | #[cfg(feature = "renamed")] 10 | println!("renamed"); 11 | 12 | // Non-existent features 13 | #[cfg(feature = "member1")] 14 | println!("member1"); 15 | #[cfg(feature = "member2")] 16 | println!("member2"); 17 | #[cfg(feature = "member3")] 18 | println!("member3"); 19 | } 20 | -------------------------------------------------------------------------------- /tests/fixtures/optional_deps/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "optional_deps" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [features] 7 | 8 | [dependencies] 9 | real = { path = "real", optional = true } 10 | member2 = { path = "member2" } 11 | renamed = { path = "member3", package = "member3", optional = true } 12 | 13 | [workspace] 14 | members = [ 15 | "real", 16 | "member2", 17 | "member3", 18 | ".", 19 | ] 20 | -------------------------------------------------------------------------------- /tests/fixtures/optional_deps/member2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member2" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [dependencies] 7 | 8 | [dev-dependencies] 9 | -------------------------------------------------------------------------------- /tests/fixtures/optional_deps/member2/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/optional_deps/member2/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/optional_deps/member3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member3" 3 | version = "0.0.0" 4 | publish = true 5 | 6 | [dependencies] 7 | 8 | [dev-dependencies] 9 | -------------------------------------------------------------------------------- /tests/fixtures/optional_deps/member3/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/optional_deps/member3/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/optional_deps/real/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "real" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | 7 | [dev-dependencies] 8 | -------------------------------------------------------------------------------- /tests/fixtures/optional_deps/real/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/optional_deps/real/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/optional_deps/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/optional_deps/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(feature = "member2")] 3 | println!("member2"); 4 | #[cfg(feature = "member3")] 5 | println!("member3"); 6 | #[cfg(feature = "real")] 7 | println!("real"); 8 | #[cfg(feature = "renamed")] 9 | println!("renamed"); 10 | } 11 | -------------------------------------------------------------------------------- /tests/fixtures/package_collision/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "member1", 4 | "member2", 5 | ] 6 | -------------------------------------------------------------------------------- /tests/fixtures/package_collision/member1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member1" 3 | version = "0.0.0" 4 | 5 | [features] 6 | default = ["a"] 7 | a = [] 8 | b = [] 9 | c = [] 10 | 11 | [dependencies] 12 | member2 = { path = "../member2" } 13 | 14 | [dev-dependencies] 15 | -------------------------------------------------------------------------------- /tests/fixtures/package_collision/member1/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/package_collision/member1/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/package_collision/member2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member2" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [features] 7 | default = ["a"] 8 | a = [] 9 | b = [] 10 | c = [] 11 | 12 | [dependencies] 13 | 14 | [dev-dependencies] 15 | -------------------------------------------------------------------------------- /tests/fixtures/package_collision/member2/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/package_collision/member2/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/powerset_deduplication/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deduplication" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [features] 7 | a = [] 8 | b = ["a"] 9 | c = ["b"] 10 | d = ["member1"] 11 | e = ["b", "d"] 12 | 13 | [dependencies] 14 | member1 = { path = "member1", optional = true } 15 | # easytime 0.2.6 requires Rust 1.58 16 | easytime = { version = "=0.2.5", default-features = false } 17 | 18 | [dev-dependencies] 19 | 20 | [workspace] 21 | members = [ 22 | "member1", 23 | ".", 24 | ] 25 | -------------------------------------------------------------------------------- /tests/fixtures/powerset_deduplication/member1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member1" 3 | version = "0.0.0" 4 | 5 | [features] 6 | a = [] 7 | b = ["a"] 8 | c = ["b"] 9 | d = [] 10 | e = ["b", "d"] 11 | 12 | [dependencies] 13 | 14 | [dev-dependencies] 15 | -------------------------------------------------------------------------------- /tests/fixtures/powerset_deduplication/member1/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/powerset_deduplication/member1/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() {} 2 | -------------------------------------------------------------------------------- /tests/fixtures/powerset_deduplication/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/powerset_deduplication/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() {} 2 | -------------------------------------------------------------------------------- /tests/fixtures/real/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "real" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [features] 7 | default = [] 8 | a = [] 9 | b = [] 10 | c = [] 11 | 12 | [dependencies] 13 | member1 = { path = "member1", optional = true } 14 | 15 | [dev-dependencies] 16 | 17 | [workspace] 18 | members = [ 19 | "member1", 20 | "member2", 21 | "member3", 22 | ".", 23 | ] 24 | -------------------------------------------------------------------------------- /tests/fixtures/real/member1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member1" 3 | version = "0.0.0" 4 | 5 | [features] 6 | default = [] 7 | a = [] 8 | b = [] 9 | c = [] 10 | 11 | [dependencies] 12 | 13 | [dev-dependencies] 14 | -------------------------------------------------------------------------------- /tests/fixtures/real/member1/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/real/member1/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/real/member2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member2" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [features] 7 | default = [] 8 | a = [] 9 | b = [] 10 | c = [] 11 | 12 | [dependencies] 13 | 14 | [dev-dependencies] 15 | -------------------------------------------------------------------------------- /tests/fixtures/real/member2/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/real/member2/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/real/member3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member3" 3 | version = "0.0.0" 4 | publish = true 5 | 6 | [features] 7 | default = ["a"] 8 | a = [] 9 | b = [] 10 | c = [] 11 | 12 | [dependencies] 13 | member2 = { path = "../member2" } 14 | 15 | [dev-dependencies] 16 | -------------------------------------------------------------------------------- /tests/fixtures/real/member3/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/real/member3/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/real/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/real/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | 13 | #[cfg(test)] 14 | mod tests { 15 | #[test] 16 | fn test() {} 17 | 18 | #[ignore] 19 | #[test] 20 | fn test_ignored() {} 21 | } 22 | -------------------------------------------------------------------------------- /tests/fixtures/real_root_public/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "real" 3 | version = "0.0.0" 4 | 5 | [features] 6 | default = [] 7 | a = [] 8 | b = [] 9 | c = [] 10 | 11 | [dependencies] 12 | member1 = { path = "member1", optional = true } 13 | 14 | [dev-dependencies] 15 | 16 | [workspace] 17 | members = [ 18 | "member1", 19 | "member2", 20 | "member3", 21 | ".", 22 | ] 23 | -------------------------------------------------------------------------------- /tests/fixtures/real_root_public/member1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member1" 3 | version = "0.0.0" 4 | 5 | [features] 6 | default = [] 7 | a = [] 8 | b = [] 9 | c = [] 10 | 11 | [dependencies] 12 | 13 | [dev-dependencies] 14 | -------------------------------------------------------------------------------- /tests/fixtures/real_root_public/member1/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/real_root_public/member1/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/real_root_public/member2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member2" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [features] 7 | default = [] 8 | a = [] 9 | b = [] 10 | c = [] 11 | 12 | [dependencies] 13 | 14 | [dev-dependencies] 15 | -------------------------------------------------------------------------------- /tests/fixtures/real_root_public/member2/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/real_root_public/member2/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/real_root_public/member3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member3" 3 | version = "0.0.0" 4 | publish = true 5 | 6 | [features] 7 | default = ["a"] 8 | a = [] 9 | b = [] 10 | c = [] 11 | 12 | [dependencies] 13 | member2 = { path = "../member2" } 14 | 15 | [dev-dependencies] 16 | -------------------------------------------------------------------------------- /tests/fixtures/real_root_public/member3/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/real_root_public/member3/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/real_root_public/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/real_root_public/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | 13 | #[cfg(test)] 14 | mod tests { 15 | #[test] 16 | fn test() {} 17 | 18 | #[ignore] 19 | #[test] 20 | fn test_ignored() {} 21 | } 22 | -------------------------------------------------------------------------------- /tests/fixtures/rust-version/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "argfile" 7 | version = "0.1.5" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "265f5108974489a217d5098cd81666b60480c8dd67302acbbe7cbdd8aa09d638" 10 | dependencies = [ 11 | "os_str_bytes", 12 | ] 13 | 14 | [[package]] 15 | name = "member1" 16 | version = "0.0.0" 17 | 18 | [[package]] 19 | name = "member2" 20 | version = "0.0.0" 21 | 22 | [[package]] 23 | name = "member3" 24 | version = "0.0.0" 25 | dependencies = [ 26 | "member2", 27 | ] 28 | 29 | [[package]] 30 | name = "memchr" 31 | version = "2.7.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 34 | 35 | [[package]] 36 | name = "os_str_bytes" 37 | version = "6.6.1" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" 40 | dependencies = [ 41 | "memchr", 42 | ] 43 | 44 | [[package]] 45 | name = "real" 46 | version = "0.0.0" 47 | dependencies = [ 48 | "argfile", 49 | "member1", 50 | ] 51 | -------------------------------------------------------------------------------- /tests/fixtures/rust-version/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "real" 3 | version = "0.0.0" 4 | publish = false 5 | rust-version = "1.76" 6 | 7 | [features] 8 | default = [] 9 | a = [] 10 | b = [] 11 | c = [] 12 | 13 | [dependencies] 14 | member1 = { path = "member1", optional = true } 15 | argfile = "0.1.5" 16 | 17 | [dev-dependencies] 18 | 19 | [workspace] 20 | members = [ 21 | "member1", 22 | "member2", 23 | "member3", 24 | ".", 25 | ] 26 | -------------------------------------------------------------------------------- /tests/fixtures/rust-version/member1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member1" 3 | version = "0.0.0" 4 | rust-version = "1.74" 5 | 6 | [features] 7 | default = [] 8 | a = [] 9 | b = [] 10 | c = [] 11 | 12 | [dependencies] 13 | 14 | [dev-dependencies] 15 | -------------------------------------------------------------------------------- /tests/fixtures/rust-version/member1/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/rust-version/member1/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/rust-version/member2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member2" 3 | version = "0.0.0" 4 | publish = false 5 | rust-version = "1.74" 6 | 7 | [features] 8 | default = [] 9 | a = [] 10 | b = [] 11 | c = [] 12 | 13 | [dependencies] 14 | 15 | [dev-dependencies] 16 | -------------------------------------------------------------------------------- /tests/fixtures/rust-version/member2/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/rust-version/member2/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/rust-version/member3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member3" 3 | version = "0.0.0" 4 | publish = true 5 | rust-version = "1.75" 6 | 7 | [features] 8 | default = ["a"] 9 | a = [] 10 | b = [] 11 | c = [] 12 | 13 | [dependencies] 14 | member2 = { path = "../member2" } 15 | 16 | [dev-dependencies] 17 | -------------------------------------------------------------------------------- /tests/fixtures/rust-version/member3/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/rust-version/member3/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/rust-version/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/rust-version/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | 13 | #[cfg(test)] 14 | mod tests { 15 | #[test] 16 | fn test() {} 17 | 18 | #[ignore] 19 | #[test] 20 | fn test_ignored() {} 21 | } 22 | -------------------------------------------------------------------------------- /tests/fixtures/virtual/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "member1", 4 | "member2", 5 | "dir/not_find_manifest", 6 | ] 7 | -------------------------------------------------------------------------------- /tests/fixtures/virtual/dir/not_find_manifest/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "not_find_manifest" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [features] 7 | default = ["a"] 8 | a = [] 9 | b = [] 10 | c = [] 11 | 12 | [dependencies] 13 | 14 | [dev-dependencies] 15 | -------------------------------------------------------------------------------- /tests/fixtures/virtual/dir/not_find_manifest/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/virtual/dir/not_find_manifest/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/virtual/member1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member1" 3 | version = "0.0.0" 4 | 5 | [features] 6 | default = ["a"] 7 | a = [] 8 | b = [] 9 | c = [] 10 | 11 | [dependencies] 12 | # member2 = { path = "../member2" } 13 | 14 | [dev-dependencies] 15 | -------------------------------------------------------------------------------- /tests/fixtures/virtual/member1/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/virtual/member1/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/virtual/member2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "member2" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [features] 7 | default = ["a"] 8 | a = [] 9 | b = [] 10 | c = [] 11 | f = [] 12 | 13 | [dependencies] 14 | 15 | [dev-dependencies] 16 | -------------------------------------------------------------------------------- /tests/fixtures/virtual/member2/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/virtual/member2/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello!"); 3 | #[cfg(feature = "default")] 4 | println!("default"); 5 | #[cfg(feature = "a")] 6 | println!("a"); 7 | #[cfg(feature = "b")] 8 | println!("b"); 9 | #[cfg(feature = "c")] 10 | println!("c"); 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/weak_dep_features/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "weak_dep_features" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [features] 8 | easytime = ["easytime/std"] 9 | default = ["easytime?/default"] 10 | 11 | [dependencies] 12 | # easytime 0.2.6 requires Rust 1.58 13 | easytime = { version = "=0.2.5", optional = true, default-features = false } 14 | 15 | [dev-dependencies] 16 | 17 | [workspace] 18 | resolver = "2" 19 | -------------------------------------------------------------------------------- /tests/fixtures/weak_dep_features/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/weak_dep_features_implicit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "weak_dep_features_implicit" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [workspace] 8 | resolver = "2" 9 | 10 | [features] 11 | default = ["easytime?/default"] 12 | 13 | [dependencies] 14 | # easytime 0.2.6 requires Rust 1.58 15 | easytime = { version = "=0.2.5", optional = true, default-features = false } 16 | 17 | [dev-dependencies] 18 | -------------------------------------------------------------------------------- /tests/fixtures/weak_dep_features_implicit/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/weak_dep_features_namespaced/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "weak_dep_features_namespaced" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [features] 8 | easytime = ["dep:easytime", "easytime/std"] 9 | default = ["easytime?/default"] 10 | 11 | [dependencies] 12 | # easytime 0.2.6 requires Rust 1.58 13 | easytime = { version = "=0.2.5", optional = true, default-features = false } 14 | 15 | [dev-dependencies] 16 | 17 | [workspace] 18 | resolver = "2" 19 | -------------------------------------------------------------------------------- /tests/fixtures/weak_dep_features_namespaced/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/long-help.txt: -------------------------------------------------------------------------------- 1 | cargo-hack 2 | Cargo subcommand to provide various options useful for testing and continuous integration. 3 | 4 | USAGE: 5 | cargo hack [OPTIONS] [SUBCOMMAND] 6 | 7 | Use -h for short descriptions and --help for more details. 8 | 9 | OPTIONS: 10 | -p, --package ... 11 | Package(s) to check. 12 | 13 | --all 14 | Alias for --workspace. 15 | 16 | --workspace 17 | Perform command for all packages in the workspace. 18 | 19 | --exclude ... 20 | Exclude packages from the check. 21 | 22 | --manifest-path 23 | Path to Cargo.toml. 24 | 25 | --locked 26 | Require Cargo.lock is up to date. 27 | 28 | -F, --features ... 29 | Space or comma separated list of features to activate. 30 | 31 | --each-feature 32 | Perform for each feature of the package. 33 | 34 | This also includes runs with just --no-default-features flag, and default features. 35 | 36 | When this flag is not used together with --exclude-features (--skip) and 37 | --include-features and there are multiple features, this also includes runs with just 38 | --all-features flag. 39 | 40 | --feature-powerset 41 | Perform for the feature powerset of the package. 42 | 43 | This also includes runs with just --no-default-features flag, and default features. 44 | 45 | When this flag is used together with --depth or namespaced features (-Z 46 | namespaced-features) and not used together with --exclude-features (--skip) and 47 | --include-features and there are multiple features, this also includes runs with just 48 | --all-features flag. 49 | 50 | --optional-deps [DEPS]... 51 | Use optional dependencies as features. 52 | 53 | If DEPS are not specified, all optional dependencies are considered as features. 54 | 55 | This flag can only be used together with either --each-feature flag or 56 | --feature-powerset flag. 57 | 58 | --skip ... 59 | Alias for --exclude-features. 60 | 61 | --exclude-features ... 62 | Space or comma separated list of features to exclude. 63 | 64 | To exclude run of default feature, using value `--exclude-features default`. 65 | 66 | To exclude run of just --no-default-features flag, using --exclude-no-default-features 67 | flag. 68 | 69 | To exclude run of just --all-features flag, using --exclude-all-features flag. 70 | 71 | This flag can only be used together with either --each-feature flag or 72 | --feature-powerset flag. 73 | 74 | --exclude-no-default-features 75 | Exclude run of just --no-default-features flag. 76 | 77 | This flag can only be used together with either --each-feature flag or 78 | --feature-powerset flag. 79 | 80 | --exclude-all-features 81 | Exclude run of just --all-features flag. 82 | 83 | This flag can only be used together with either --each-feature flag or 84 | --feature-powerset flag. 85 | 86 | --depth 87 | Specify a max number of simultaneous feature flags of --feature-powerset. 88 | 89 | If NUM is set to 1, --feature-powerset is equivalent to --each-feature. 90 | 91 | This flag can only be used together with --feature-powerset flag. 92 | 93 | --group-features ... 94 | Space or comma separated list of features to group. 95 | 96 | This treats the specified features as if it were a single feature. 97 | 98 | To specify multiple groups, use this option multiple times: `--group-features a,b 99 | --group-features c,d` 100 | 101 | This flag can only be used together with --feature-powerset flag. 102 | 103 | --target 104 | Build for specified target triple. 105 | 106 | Comma-separated lists of targets are not supported, but you can specify the whole 107 | --target option multiple times to do multiple targets. 108 | 109 | This is actually not a cargo-hack option, it is interpreted by Cargo itself. 110 | 111 | --mutually-exclusive-features ... 112 | Space or comma separated list of features to not use together. 113 | 114 | To specify multiple groups, use this option multiple times: 115 | `--mutually-exclusive-features a,b --mutually-exclusive-features c,d` 116 | 117 | This flag can only be used together with --feature-powerset flag. 118 | 119 | --at-least-one-of ... 120 | Space or comma separated list of features. Skips sets of features that don't enable any 121 | of the features listed. 122 | 123 | To specify multiple groups, use this option multiple times: `--at-least-one-of a,b 124 | --at-least-one-of c,d` 125 | 126 | This flag can only be used together with --feature-powerset flag. 127 | 128 | --include-features ... 129 | Include only the specified features in the feature combinations instead of package 130 | features. 131 | 132 | This flag can only be used together with either --each-feature flag or 133 | --feature-powerset flag. 134 | 135 | --no-dev-deps 136 | Perform without dev-dependencies. 137 | 138 | Note that this flag removes dev-dependencies from real `Cargo.toml` while cargo-hack is 139 | running and restores it when finished. 140 | 141 | --remove-dev-deps 142 | Equivalent to --no-dev-deps flag except for does not restore the original `Cargo.toml` 143 | after performed. 144 | 145 | --no-private 146 | Perform without `publish = false` crates. 147 | 148 | --ignore-private 149 | Skip to perform on `publish = false` packages. 150 | 151 | --ignore-unknown-features 152 | Skip passing --features flag to `cargo` if that feature does not exist in the package. 153 | 154 | This flag can be used with --features, --include-features, or --group-features. 155 | 156 | --rust-version 157 | Perform commands on `package.rust-version`. 158 | 159 | This cannot be used with --version-range. 160 | 161 | --version-range [START]..[=END] 162 | Perform commands on a specified (inclusive) range of Rust versions. 163 | 164 | If the upper bound of the range is omitted, the latest stable compiler is used as the 165 | upper bound. 166 | 167 | If the lower bound of the range is omitted, the value of the `rust-version` field in 168 | `Cargo.toml` is used as the lower bound. 169 | 170 | Note that ranges are always inclusive ranges. 171 | 172 | --version-step 173 | Specify the version interval of --version-range (default to `1`). 174 | 175 | This flag can only be used together with --version-range flag. 176 | 177 | --clean-per-run 178 | Remove artifacts for that package before running the command. 179 | 180 | If used this flag with --workspace, --each-feature, or --feature-powerset, artifacts 181 | will be removed before each run. 182 | 183 | Note that dependencies artifacts will be preserved. 184 | 185 | --clean-per-version 186 | Remove artifacts per Rust version. 187 | 188 | Note that dependencies artifacts will also be removed. 189 | 190 | This flag can only be used together with --version-range flag. 191 | 192 | --keep-going 193 | Keep going on failure. 194 | 195 | --partition 196 | Partition runs and execute only its subset according to M/N. 197 | 198 | --log-group 199 | Log grouping: none, github-actions. 200 | 201 | If this option is not used, the environment will be automatically detected. 202 | 203 | --print-command-list 204 | Print commands without run (Unstable). 205 | 206 | --no-manifest-path 207 | Do not pass --manifest-path option to cargo (Unstable). 208 | 209 | -v, --verbose 210 | Use verbose output. 211 | 212 | --color 213 | Coloring: auto, always, never. 214 | 215 | This flag will be propagated to cargo. 216 | 217 | -h, --help 218 | Prints help information. 219 | 220 | -V, --version 221 | Prints version information. 222 | 223 | Some common cargo commands are (see all commands with --list): 224 | build Compile the current package 225 | check Analyze the current package and report errors, but don't build object files 226 | run Run a binary or example of the local package 227 | test Run the tests 228 | -------------------------------------------------------------------------------- /tests/short-help.txt: -------------------------------------------------------------------------------- 1 | cargo-hack 2 | Cargo subcommand to provide various options useful for testing and continuous integration. 3 | 4 | USAGE: 5 | cargo hack [OPTIONS] [SUBCOMMAND] 6 | 7 | Use -h for short descriptions and --help for more details. 8 | 9 | OPTIONS: 10 | -p, --package ... Package(s) to check 11 | --all Alias for --workspace 12 | --workspace Perform command for all packages in the workspace 13 | --exclude ... Exclude packages from the check 14 | --manifest-path Path to Cargo.toml 15 | --locked Require Cargo.lock is up to date 16 | -F, --features ... Space or comma separated list of features to activate 17 | --each-feature Perform for each feature of the package 18 | --feature-powerset Perform for the feature powerset of the package 19 | --optional-deps [DEPS]... Use optional dependencies as features 20 | --skip ... Alias for --exclude-features 21 | --exclude-features ... Space or comma separated list of features to exclude 22 | --exclude-no-default-features Exclude run of just --no-default-features flag 23 | --exclude-all-features Exclude run of just --all-features flag 24 | --depth Specify a max number of simultaneous feature flags of 25 | --feature-powerset 26 | --group-features ... Space or comma separated list of features to group 27 | --target Build for specified target triple 28 | --mutually-exclusive-features ... Space or comma separated list of features to not use 29 | together 30 | --at-least-one-of ... Space or comma separated list of features. Skips sets of 31 | features that don't enable any of the features listed 32 | --include-features ... Include only the specified features in the feature 33 | combinations instead of package features 34 | --no-dev-deps Perform without dev-dependencies 35 | --remove-dev-deps Equivalent to --no-dev-deps flag except for does not 36 | restore the original `Cargo.toml` after performed 37 | --no-private Perform without `publish = false` crates 38 | --ignore-private Skip to perform on `publish = false` packages 39 | --ignore-unknown-features Skip passing --features flag to `cargo` if that feature 40 | does not exist in the package 41 | --rust-version Perform commands on `package.rust-version` 42 | --version-range [START]..[=END] Perform commands on a specified (inclusive) range of Rust 43 | versions 44 | --version-step Specify the version interval of --version-range (default 45 | to `1`) 46 | --clean-per-run Remove artifacts for that package before running the 47 | command 48 | --clean-per-version Remove artifacts per Rust version 49 | --keep-going Keep going on failure 50 | --partition Partition runs and execute only its subset according to 51 | M/N 52 | --log-group Log grouping: none, github-actions 53 | --print-command-list Print commands without run (Unstable) 54 | --no-manifest-path Do not pass --manifest-path option to cargo (Unstable) 55 | -v, --verbose Use verbose output 56 | --color Coloring: auto, always, never 57 | -h, --help Prints help information 58 | -V, --version Prints version information 59 | 60 | Some common cargo commands are (see all commands with --list): 61 | build Compile the current package 62 | check Analyze the current package and report errors, but don't build object files 63 | run Run a binary or example of the local package 64 | test Run the tests 65 | -------------------------------------------------------------------------------- /tools/.tidy-check-license-headers: -------------------------------------------------------------------------------- 1 | git ls-files | grep -Ev '^tests/fixtures/' 2 | -------------------------------------------------------------------------------- /tools/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # SPDX-License-Identifier: Apache-2.0 OR MIT 3 | set -CeEuo pipefail 4 | IFS=$'\n\t' 5 | trap -- 's=$?; printf >&2 "%s\n" "${0##*/}:${LINENO}: \`${BASH_COMMAND}\` exit with ${s}"; exit ${s}' ERR 6 | cd -- "$(dirname -- "$0")"/.. 7 | 8 | # Publish a new release. 9 | # 10 | # USAGE: 11 | # ./tools/publish.sh 12 | # 13 | # Note: This script requires the following tools: 14 | # - parse-changelog 15 | 16 | retry() { 17 | for i in {1..10}; do 18 | if "$@"; then 19 | return 0 20 | else 21 | sleep "${i}" 22 | fi 23 | done 24 | "$@" 25 | } 26 | bail() { 27 | printf >&2 'error: %s\n' "$*" 28 | exit 1 29 | } 30 | 31 | version="${1:?}" 32 | version="${version#v}" 33 | tag_prefix="v" 34 | tag="${tag_prefix}${version}" 35 | changelog="CHANGELOG.md" 36 | if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z\.-]+)?(\+[0-9A-Za-z\.-]+)?$ ]]; then 37 | bail "invalid version format '${version}'" 38 | fi 39 | if [[ $# -gt 1 ]]; then 40 | bail "invalid argument '$2'" 41 | fi 42 | if { sed --help 2>&1 || true; } | grep -Eq -e '-i extension'; then 43 | in_place=(-i '') 44 | else 45 | in_place=(-i) 46 | fi 47 | 48 | # Make sure there is no uncommitted change. 49 | git diff --exit-code 50 | git diff --exit-code --staged 51 | 52 | # Make sure the same release has not been created in the past. 53 | if gh release view "${tag}" &>/dev/null; then 54 | bail "tag '${tag}' has already been created and pushed" 55 | fi 56 | 57 | # Make sure that the release was created from an allowed branch. 58 | if ! git branch | grep -Eq '\* main$'; then 59 | bail "current branch is not 'main'" 60 | fi 61 | if ! git remote -v | grep -F origin | grep -Eq 'github\.com[:/]taiki-e/'; then 62 | bail "cannot publish a new release from fork repository" 63 | fi 64 | 65 | release_date=$(date -u '+%Y-%m-%d') 66 | tags=$(git --no-pager tag | { grep -E "^${tag_prefix}[0-9]+" || true; }) 67 | if [[ -n "${tags}" ]]; then 68 | # Make sure the same release does not exist in changelog. 69 | if grep -Eq "^## \\[${version//./\\.}\\]" "${changelog}"; then 70 | bail "release ${version} already exist in ${changelog}" 71 | fi 72 | if grep -Eq "^\\[${version//./\\.}\\]: " "${changelog}"; then 73 | bail "link to ${version} already exist in ${changelog}" 74 | fi 75 | # Update changelog. 76 | remote_url=$(grep -E '^\[Unreleased\]: https://' "${changelog}" | sed -E 's/^\[Unreleased\]: //; s/\.\.\.HEAD$//') 77 | prev_tag="${remote_url#*/compare/}" 78 | remote_url="${remote_url%/compare/*}" 79 | sed -E "${in_place[@]}" \ 80 | -e "s/^## \\[Unreleased\\]/## [Unreleased]\\n\\n## [${version}] - ${release_date}/" \ 81 | -e "s#^\[Unreleased\]: https://.*#[Unreleased]: ${remote_url}/compare/${tag}...HEAD\\n[${version}]: ${remote_url}/compare/${prev_tag}...${tag}#" "${changelog}" 82 | if ! grep -Eq "^## \\[${version//./\\.}\\] - ${release_date}$" "${changelog}"; then 83 | bail "failed to update ${changelog}" 84 | fi 85 | if ! grep -Eq "^\\[${version//./\\.}\\]: " "${changelog}"; then 86 | bail "failed to update ${changelog}" 87 | fi 88 | else 89 | # Make sure the release exists in changelog. 90 | if ! grep -Eq "^## \\[${version//./\\.}\\] - ${release_date}$" "${changelog}"; then 91 | bail "release ${version} does not exist in ${changelog} or has wrong release date" 92 | fi 93 | if ! grep -Eq "^\\[${version//./\\.}\\]: " "${changelog}"; then 94 | bail "link to ${version} does not exist in ${changelog}" 95 | fi 96 | fi 97 | 98 | # Make sure that a valid release note for this version exists. 99 | # https://github.com/taiki-e/parse-changelog 100 | changes=$(parse-changelog "${changelog}" "${version}") 101 | if [[ -z "${changes}" ]]; then 102 | bail "changelog for ${version} has no body" 103 | fi 104 | printf '============== CHANGELOG ==============\n' 105 | printf '%s\n' "${changes}" 106 | printf '=======================================\n' 107 | 108 | metadata=$(cargo metadata --format-version=1 --no-deps) 109 | prev_version='' 110 | docs=() 111 | for readme in $(git ls-files '*README.md'); do 112 | docs+=("${readme}") 113 | lib="$(dirname -- "${readme}")/src/lib.rs" 114 | if [[ -f "${lib}" ]]; then 115 | docs+=("${lib}") 116 | fi 117 | done 118 | changed_paths=("${changelog}" "${docs[@]}") 119 | # Publishing is unrestricted if null, and forbidden if an empty array. 120 | for pkg in $(jq -c '. as $metadata | .workspace_members[] as $id | $metadata.packages[] | select(.id == $id and .publish != [])' <<<"${metadata}"); do 121 | eval "$(jq -r '@sh "NAME=\(.name) ACTUAL_VERSION=\(.version) manifest_path=\(.manifest_path)"' <<<"${pkg}")" 122 | if [[ -z "${prev_version}" ]]; then 123 | prev_version="${ACTUAL_VERSION}" 124 | fi 125 | # Make sure that the version number of all publishable workspace members matches. 126 | if [[ "${ACTUAL_VERSION}" != "${prev_version}" ]]; then 127 | bail "publishable workspace members must be version '${prev_version}', but package '${NAME}' is version '${ACTUAL_VERSION}'" 128 | fi 129 | 130 | changed_paths+=("${manifest_path}") 131 | # Update version in Cargo.toml. 132 | if ! grep -Eq "^version = \"${prev_version}\" #publish:version" "${manifest_path}"; then 133 | bail "not found '#publish:version' in version in ${manifest_path}" 134 | fi 135 | sed -E "${in_place[@]}" "s/^version = \"${prev_version}\" #publish:version/version = \"${version}\" #publish:version/g" "${manifest_path}" 136 | # Update '=' requirement in Cargo.toml. 137 | for manifest in $(git ls-files '*Cargo.toml'); do 138 | if grep -Eq "^${NAME} = \\{ version = \"=${prev_version}\"" "${manifest}"; then 139 | sed -E "${in_place[@]}" "s/^${NAME} = \\{ version = \"=${prev_version}\"/${NAME} = { version = \"=${version}\"/g" "${manifest}" 140 | fi 141 | done 142 | # Update version in readme and lib.rs. 143 | for path in "${docs[@]}"; do 144 | # TODO: handle pre-release 145 | if [[ "${version}" == "0.0."* ]]; then 146 | # 0.0.x -> 0.0.y 147 | if grep -Eq "^${NAME} = \"${prev_version}\"" "${path}"; then 148 | sed -E "${in_place[@]}" "s/^${NAME} = \"${prev_version}\"/${NAME} = \"${version}\"/g" "${path}" 149 | fi 150 | if grep -Eq "^${NAME} = \\{ version = \"${prev_version}\"" "${path}"; then 151 | sed -E "${in_place[@]}" "s/^${NAME} = \\{ version = \"${prev_version}\"/${NAME} = { version = \"${version}\"/g" "${path}" 152 | fi 153 | elif [[ "${version}" == "0."* ]]; then 154 | prev_major_minor="${prev_version%.*}" 155 | major_minor="${version%.*}" 156 | if [[ "${prev_major_minor}" != "${major_minor}" ]]; then 157 | # 0.x -> 0.y 158 | # 0.x.* -> 0.y 159 | if grep -Eq "^${NAME} = \"${prev_major_minor}(\\.[0-9]+)?\"" "${path}"; then 160 | sed -E "${in_place[@]}" "s/^${NAME} = \"${prev_major_minor}(\\.[0-9]+)?\"/${NAME} = \"${major_minor}\"/g" "${path}" 161 | fi 162 | if grep -Eq "^${NAME} = \\{ version = \"${prev_major_minor}(\\.[0-9]+)?\"" "${path}"; then 163 | sed -E "${in_place[@]}" "s/^${NAME} = \\{ version = \"${prev_major_minor}(\\.[0-9]+)?\"/${NAME} = { version = \"${major_minor}\"/g" "${path}" 164 | fi 165 | fi 166 | else 167 | prev_major="${prev_version%%.*}" 168 | major="${version%%.*}" 169 | if [[ "${prev_major}" != "${major}" ]]; then 170 | # x -> y 171 | # x.* -> y 172 | # x.*.* -> y 173 | if grep -Eq "^${NAME} = \"${prev_major}(\\.[0-9]+(\\.[0-9]+)?)?\"" "${path}"; then 174 | sed -E "${in_place[@]}" "s/^${NAME} = \"${prev_major}(\\.[0-9]+(\\.[0-9]+)?)?\"/${NAME} = \"${major}\"/g" "${path}" 175 | fi 176 | if grep -Eq "^${NAME} = \\{ version = \"${prev_major}(\\.[0-9]+(\\.[0-9]+)?)?\"" "${path}"; then 177 | sed -E "${in_place[@]}" "s/^${NAME} = \\{ version = \"${prev_major}(\\.[0-9]+(\\.[0-9]+)?)?\"/${NAME} = { version = \"${major}\"/g" "${path}" 178 | fi 179 | fi 180 | fi 181 | done 182 | done 183 | 184 | if [[ -n "${tags}" ]]; then 185 | # Create a release commit. 186 | ( 187 | set -x 188 | git add "${changed_paths[@]}" 189 | git commit -m "Release ${version}" 190 | ) 191 | fi 192 | 193 | set -x 194 | 195 | git tag "${tag}" 196 | retry git push origin refs/heads/main 197 | retry git push origin refs/tags/"${tag}" 198 | --------------------------------------------------------------------------------