├── .gitignore ├── rust-toolchain.toml ├── .github ├── HEADER ├── dependabot.yml ├── workflows │ ├── license-header.yml │ ├── msrv.yml │ ├── toml.yml │ ├── install.yml │ ├── linkcheck.yml │ ├── rust.yml │ ├── downstream_nightly.yml │ ├── release_binaries.yml │ └── downstream_legacy.yml └── header.py ├── tests ├── ui │ ├── root-args │ │ ├── version.yaml │ │ └── help.yaml │ ├── lint │ │ ├── never-enables │ │ │ ├── self.yaml │ │ │ ├── transitive.yaml │ │ │ ├── rename.yaml │ │ │ ├── simple.yaml │ │ │ └── diamond.yaml │ │ ├── never-implies │ │ │ ├── self.yaml │ │ │ └── transitive.yaml │ │ ├── only-enables │ │ │ ├── good.yaml │ │ │ ├── good2.yaml │ │ │ ├── good_re.yaml │ │ │ ├── good_opt.yaml │ │ │ ├── good_opt_re.yaml │ │ │ ├── bad.yaml │ │ │ ├── bad_re.yaml │ │ │ ├── bad_opt.yaml │ │ │ └── bad_opt_re.yaml │ │ ├── propagate-feature │ │ │ ├── feature_enables_dep_non_optional.yaml │ │ │ ├── target_no_feat.yaml │ │ │ ├── feature_enables_dep_no_match.yaml │ │ │ ├── good.yaml │ │ │ ├── rename_good.yaml │ │ │ ├── from_no_feat.yaml │ │ │ ├── from_no_feat_re.yaml │ │ │ ├── feature_enables_dep_same_feature_multi_deps.yaml │ │ │ ├── ignore_missing_re.yaml │ │ │ ├── feature_enables_dep.yaml │ │ │ ├── feature_enables_dep_multiple.yaml │ │ │ ├── dep_kinds_rename.yaml │ │ │ ├── from_select.yaml │ │ │ ├── fix_from_no_feat.yaml │ │ │ ├── fix_from_no_feat_duplicate.yaml │ │ │ ├── args.yaml │ │ │ ├── not_propagated_re.yaml │ │ │ ├── not_propagated.yaml │ │ │ ├── ignore_missing.yaml │ │ │ ├── fix_not_propagated.yaml │ │ │ ├── fix_not_propagated_opt.yaml │ │ │ ├── fix_not_propagates_re.yaml │ │ │ ├── fix_not_propagated_opt_re.yaml │ │ │ ├── dep_kinds.yaml │ │ │ ├── diamond.yaml │ │ │ ├── diamond_re.yaml │ │ │ └── dep_kinds_multiple.yaml │ │ ├── why-enabled │ │ │ ├── multi_from.yaml │ │ │ ├── simple.yaml │ │ │ └── multi_step.yaml │ │ └── duplicate-deps │ │ │ └── dep_kinds_multiple.yaml │ ├── trace │ │ ├── any_path.yaml │ │ ├── simple.yaml │ │ ├── alphabetic.yaml │ │ └── args.yaml │ ├── config │ │ └── v1 │ │ │ ├── version_file.yaml │ │ │ ├── error.yaml │ │ │ ├── version_bin.yaml │ │ │ ├── custom_help.yaml │ │ │ ├── basic.yaml │ │ │ ├── format.yaml │ │ │ └── finds_all.yaml │ └── fmt │ │ ├── args.yaml │ │ ├── dedub.yaml │ │ ├── help.yaml │ │ └── check.yaml ├── integration │ ├── substrate │ │ ├── issue-14044-fix.yaml │ │ ├── duplicate_err.yaml │ │ ├── issue-14044-pre.yaml │ │ ├── issue-14660-fix.yaml │ │ ├── issue-14491.yaml │ │ ├── frame-support.yaml │ │ ├── frame.yaml │ │ └── frame-pallets.yaml │ ├── runtimes │ │ └── transpose.yaml │ ├── alloy │ │ ├── run.yaml │ │ └── run2.yaml │ ├── reth │ │ ├── run.yaml │ │ ├── run2.yaml │ │ └── run3.yaml │ ├── sdk │ │ ├── workflows.yaml │ │ ├── duplicate_fix.yaml │ │ └── duplicate.yaml │ └── polkadot │ │ ├── issue-7261.yaml │ │ └── issue-7537.yaml └── tests.rs ├── src ├── grammar.rs ├── cmd │ ├── run.rs │ ├── debug.rs │ ├── trace.rs │ ├── lint │ │ └── nostd.rs │ ├── transpose │ │ └── mod.rs │ ├── fmt.rs │ └── mod.rs ├── main.rs ├── lib.rs ├── config │ ├── semver.rs │ ├── mod.rs │ └── workflow.rs ├── mock │ └── git.rs └── dag.rs ├── dist-workspace.toml ├── .rustfmt.toml ├── .deny.toml ├── Cargo.toml ├── presets └── polkadot.yaml ├── benches └── dag.rs ├── .cargo └── config.toml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .vscode 3 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.85" 3 | components = [ "rustfmt", "clippy" ] 4 | profile = "minimal" 5 | -------------------------------------------------------------------------------- /.github/HEADER: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Oliver Tale-Yazdi 3 | -------------------------------------------------------------------------------- /tests/ui/root-args/version.yaml: -------------------------------------------------------------------------------- 1 | crates: [] 2 | cases: 3 | - cmd: --version 4 | stdout: | 5 | zepter 1.85.0 6 | - cmd: -V 7 | stdout: | 8 | zepter 1.85.0 9 | -------------------------------------------------------------------------------- /tests/ui/lint/never-enables/self.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | features: 4 | F0: null 5 | cases: 6 | - cmd: lint never-enables --precondition F0 --stays-disabled F0 7 | stdout: '' 8 | -------------------------------------------------------------------------------- /tests/ui/lint/never-implies/self.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | features: 4 | F0: null 5 | cases: 6 | - cmd: lint never-implies --precondition F0 --stays-disabled F0 7 | stdout: '' 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | groups: 8 | known_good_semver: 9 | patterns: 10 | - '*' 11 | -------------------------------------------------------------------------------- /tests/ui/lint/only-enables/good.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | features: 6 | F0: 7 | - - B 8 | - F0 9 | - name: B 10 | features: 11 | F0: null 12 | cases: 13 | - cmd: lint only-enables --precondition F0 --only-enables F0 14 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/feature_enables_dep_non_optional.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B # Non-optional dependency 5 | features: 6 | F0: null 7 | - name: B 8 | cases: 9 | - cmd: lint propagate-feature --feature F0 --feature-enables-dep F0:B -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/target_no_feat.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | features: 6 | F0: 7 | - - B 8 | - F0 9 | - name: B 10 | features: 11 | F0: null 12 | cases: 13 | - cmd: lint propagate-feature --feature F0 14 | -------------------------------------------------------------------------------- /tests/ui/lint/only-enables/good2.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | features: 6 | F0: 7 | - - B 8 | - G0 9 | - name: B 10 | features: 11 | G0: null 12 | cases: 13 | - cmd: lint only-enables --precondition F0 --only-enables G0 14 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/feature_enables_dep_no_match.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | optional: true 6 | features: 7 | F0: null 8 | - name: B 9 | cases: 10 | - cmd: lint propagate-feature --feature F0 --feature-enables-dep F1:B -------------------------------------------------------------------------------- /tests/ui/trace/any_path.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - b 5 | - C 6 | - name: b 7 | deps: 8 | - B 9 | - name: B 10 | deps: 11 | - D 12 | - name: C 13 | deps: 14 | - D 15 | - name: D 16 | cases: 17 | - cmd: trace A D 18 | stdout: | 19 | A -> b -> B -> D 20 | -------------------------------------------------------------------------------- /tests/ui/lint/only-enables/good_re.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | rename: b 6 | features: 7 | F0: 8 | - - b 9 | - F0 10 | - name: B 11 | features: 12 | F0: null 13 | cases: 14 | - cmd: lint only-enables --precondition F0 --only-enables F0 15 | -------------------------------------------------------------------------------- /tests/ui/lint/only-enables/good_opt.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | optional: true 6 | features: 7 | F0: 8 | - - B 9 | - F0 10 | - name: B 11 | features: 12 | F0: null 13 | cases: 14 | - cmd: lint only-enables --precondition F0 --only-enables F0 15 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/good.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | features: 6 | F0: 7 | - - B 8 | - F0 9 | - name: B 10 | features: 11 | F0: null 12 | cases: 13 | - cmd: lint propagate-feature --feature F0 14 | - cmd: lint propagate-feature -p A --feature F0 15 | -------------------------------------------------------------------------------- /tests/integration/substrate/issue-14044-fix.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: paritytech/substrate 3 | ref: de80d0107336a9c7a2efdc0199015e4d67fcbdb5 4 | cases: 5 | - cmd: lint propagate-feature -p frame-support --feature runtime-benchmarks 6 | - cmd: lint propagate-feature -p frame-support --feature runtime-benchmarks --workspace 7 | -------------------------------------------------------------------------------- /tests/integration/runtimes/transpose.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: polkadot-fellows/runtimes 3 | ref: 71ddd5fb0d3bebb90731f921e793b007697d1a6c 4 | cases: 5 | - cmd: transpose dependency lift-to-workspace "regex:bridge-runtime-common" --version-resolver unambiguous 6 | stderr: | 7 | [WARN] Unstable feature - do not rely on this! 8 | -------------------------------------------------------------------------------- /tests/ui/lint/only-enables/good_opt_re.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | rename: b 6 | optional: true 7 | features: 8 | F0: 9 | - - b 10 | - F0 11 | - name: B 12 | features: 13 | F0: null 14 | cases: 15 | - cmd: lint only-enables --precondition F0 --only-enables F0 16 | -------------------------------------------------------------------------------- /tests/ui/lint/only-enables/bad.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | features: 6 | F1: 7 | - - B 8 | - G0 9 | F0: 10 | - - B 11 | - G0 12 | - name: B 13 | features: 14 | G0: null 15 | cases: 16 | - cmd: lint only-enables --precondition F0 --only-enables G0 17 | stdout: | 18 | A/F1 enables B/G0 19 | -------------------------------------------------------------------------------- /tests/ui/lint/only-enables/bad_re.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | rename: b 6 | features: 7 | F1: 8 | - - b 9 | - G0 10 | F0: 11 | - - b 12 | - G0 13 | - name: B 14 | features: 15 | G0: null 16 | cases: 17 | - cmd: lint only-enables --precondition F0 --only-enables G0 18 | stdout: | 19 | A/F1 enables b/G0 20 | -------------------------------------------------------------------------------- /tests/integration/substrate/duplicate_err.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: paritytech/substrate 3 | ref: 033d4e86cc7eff0066cd376b9375f815761d653c 4 | cases: 5 | - cmd: f f 6 | stdout: | 7 | Please fix 1 error in 1 crate manually: 8 | node-template-runtime (bin/node-template/runtime/Cargo.toml) 9 | feature 'std': conflicting ? for 'frame-try-runtime?/std' 10 | code: 1 11 | -------------------------------------------------------------------------------- /tests/ui/lint/only-enables/bad_opt.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | optional: true 6 | features: 7 | F0: 8 | - - B 9 | - G0 10 | F1: 11 | - - B 12 | - G0 13 | - name: B 14 | features: 15 | G0: null 16 | cases: 17 | - cmd: lint only-enables --precondition F0 --only-enables G0 18 | stdout: | 19 | A/F1 enables B/G0 20 | -------------------------------------------------------------------------------- /tests/ui/lint/never-enables/transitive.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | features: 6 | F0: 7 | - - B 8 | - G0 9 | - name: B 10 | deps: 11 | - C 12 | features: 13 | G0: 14 | - - C 15 | - H0 16 | - name: C 17 | features: 18 | H0: null 19 | cases: 20 | - cmd: lint never-enables --precondition F0 --stays-disabled H0 21 | stdout: '' 22 | -------------------------------------------------------------------------------- /tests/integration/alloy/run.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: alloy-rs/alloy 3 | ref: 8ef44f253609a2951846caa6514ee760656b30e2 4 | cases: 5 | - cmd: run default 6 | stderr: | 7 | [INFO] Running workflow 'default' 8 | [INFO] 1/1 lint propagate-feature 9 | - cmd: run check 10 | stderr: | 11 | [INFO] Running workflow 'check' 12 | [INFO] 1/1 lint propagate-feature 13 | no_default_args: true 14 | -------------------------------------------------------------------------------- /tests/integration/alloy/run2.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: alloy-rs/alloy 3 | ref: 04ab8d69d03c7a696e9d88f4c17764051cd00b5e 4 | cases: 5 | - cmd: run default 6 | stderr: | 7 | [INFO] Running workflow 'default' 8 | [INFO] 1/1 lint propagate-feature 9 | - cmd: run check 10 | stderr: | 11 | [INFO] Running workflow 'check' 12 | [INFO] 1/1 lint propagate-feature 13 | no_default_args: true 14 | -------------------------------------------------------------------------------- /tests/integration/reth/run.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: paradigmxyz/reth 3 | ref: f0f32b4a18495ee0829a99bd0edce4ec4009c8cb 4 | cases: 5 | - cmd: run default 6 | stderr: | 7 | [INFO] Running workflow 'default' 8 | [INFO] 1/1 lint propagate-feature 9 | - cmd: run check 10 | stderr: | 11 | [INFO] Running workflow 'check' 12 | [INFO] 1/1 lint propagate-feature 13 | no_default_args: true 14 | -------------------------------------------------------------------------------- /tests/integration/reth/run2.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: paradigmxyz/reth 3 | ref: 8d51c608ce9c27f18411928fc3b1d61252bf9f1a 4 | cases: 5 | - cmd: run default 6 | stderr: | 7 | [INFO] Running workflow 'default' 8 | [INFO] 1/1 lint propagate-feature 9 | - cmd: run check 10 | stderr: | 11 | [INFO] Running workflow 'check' 12 | [INFO] 1/1 lint propagate-feature 13 | no_default_args: true 14 | -------------------------------------------------------------------------------- /tests/integration/reth/run3.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: paradigmxyz/reth 3 | ref: 589fc2a68d6f8974234d92a383ea550ae382f2a0 4 | cases: 5 | - cmd: run default 6 | stderr: | 7 | [INFO] Running workflow 'default' 8 | [INFO] 1/1 lint propagate-feature 9 | - cmd: run check 10 | stderr: | 11 | [INFO] Running workflow 'check' 12 | [INFO] 1/1 lint propagate-feature 13 | no_default_args: true 14 | -------------------------------------------------------------------------------- /tests/ui/lint/only-enables/bad_opt_re.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | rename: b 6 | optional: true 7 | features: 8 | F1: 9 | - - b 10 | - G0 11 | F0: 12 | - - b 13 | - G0 14 | - name: B 15 | features: 16 | G0: null 17 | cases: 18 | - cmd: lint only-enables --precondition F0 --only-enables G0 19 | stdout: | 20 | A/F1 enables b/G0 21 | -------------------------------------------------------------------------------- /tests/ui/trace/simple.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | - name: B 6 | deps: 7 | - C 8 | - name: C 9 | deps: 10 | - D 11 | - name: D 12 | cases: 13 | - cmd: trace A B 14 | stdout: | 15 | A -> B 16 | - cmd: trace A C 17 | stdout: | 18 | A -> B -> C 19 | - cmd: trace B D 20 | stdout: | 21 | B -> C -> D 22 | - cmd: trace A D 23 | stdout: | 24 | A -> B -> C -> D 25 | -------------------------------------------------------------------------------- /tests/ui/config/v1/version_file.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | cases: 4 | - cmd: run default 5 | stderr: | 6 | Can only parse workflow files with version '1' 7 | code: 101 8 | configs: 9 | - to_path: .zepter.yaml 10 | from_path: null 11 | verbatim: | 12 | version: 13 | format: 1.2 14 | binary: 0.0.0 15 | 16 | workflows: 17 | default: 18 | - [ ] 19 | no_default_args: true 20 | -------------------------------------------------------------------------------- /tests/ui/fmt/args.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | cases: 4 | - cmd: format features 5 | - cmd: fmt features 6 | - cmd: f features 7 | - cmd: format f 8 | - cmd: fmt f 9 | - cmd: f f 10 | - cmd: format features --color --check --print-paths --quiet 11 | - cmd: format features --color --print-paths --quiet --mode-per-feature "F:sort" --mode-per-feature "G:none" --mode-per-feature "H:canonicalize" --ignore-feature G,H --ignore-feature Z 12 | -------------------------------------------------------------------------------- /tests/ui/lint/why-enabled/multi_from.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | features: 6 | F0: 7 | - - B 8 | - F0 9 | - name: a 10 | deps: 11 | - B 12 | features: 13 | f0: 14 | - - B 15 | - F0 16 | - name: B 17 | features: 18 | F0: null 19 | cases: 20 | - cmd: lint why-enabled -p B --feature F0 21 | stdout: | 22 | Feature F0/B is enabled by: 23 | A/F0 24 | a/f0 25 | -------------------------------------------------------------------------------- /tests/ui/trace/alphabetic.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - b 5 | - B 6 | - name: b 7 | deps: 8 | - C 9 | - name: B 10 | deps: 11 | - C 12 | - name: C 13 | - name: S 14 | deps: 15 | - r 16 | - R 17 | - name: r 18 | deps: 19 | - R 20 | - name: R 21 | deps: 22 | - T 23 | - name: T 24 | cases: 25 | - cmd: trace A C 26 | stdout: | 27 | A -> b -> C 28 | - cmd: trace S T 29 | stdout: | 30 | S -> r -> R -> T 31 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/rename_good.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | rename: b 6 | features: 7 | F0: 8 | - - b 9 | - F0 10 | - name: B 11 | features: 12 | F0: null 13 | cases: 14 | - cmd: lint propagate-feature --feature F0 15 | - cmd: lint propagate-feature -p A --feature F0 16 | - cmd: lint propagate-feature --feature F0 --workspace 17 | - cmd: lint propagate-feature -p A --feature F0 --workspace 18 | -------------------------------------------------------------------------------- /tests/integration/substrate/issue-14044-pre.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: paritytech/substrate 3 | ref: e94cb0dafd4f30ff29512c1c00ec513ada7d2b5d 4 | cases: 5 | - cmd: lint propagate-feature -p frame-support --feature runtime-benchmarks 6 | stdout: | 7 | crate 'frame-support' 8 | feature 'runtime-benchmarks' 9 | must propagate to: 10 | frame-system 11 | sp-runtime 12 | sp-staking 13 | Found 3 issues (run with `--fix` to fix). 14 | code: 1 15 | -------------------------------------------------------------------------------- /tests/ui/trace/args.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | - name: B 6 | - name: A-dash 7 | deps: 8 | - B_underscore 9 | - name: B_underscore 10 | cases: 11 | - cmd: trace A B --show-version 12 | stdout: | 13 | A v0.1.0 -> B v0.1.0 14 | - cmd: trace A B --path-delimiter=> 15 | stdout: | 16 | A>B 17 | - cmd: trace A B --path-delimiter=> --workspace 18 | stdout: | 19 | A>B 20 | - cmd: trace A-dash B_underscore 21 | stdout: | 22 | A-dash -> B_underscore 23 | -------------------------------------------------------------------------------- /tests/integration/sdk/workflows.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: paritytech/polkadot-sdk 3 | ref: c86b633695299ed27053940d5ea5c5a2392964b3 4 | cases: 5 | - cmd: run check 6 | stderr: | 7 | [INFO] Running workflow 'check' 8 | [INFO] 1/2 lint propagate-feature 9 | [INFO] 2/2 format features 10 | code: 0 11 | - cmd: run default 12 | stderr: | 13 | [INFO] Running workflow 'default' 14 | [INFO] 1/2 lint propagate-feature 15 | [INFO] 2/2 format features 16 | code: 0 17 | no_default_args: true 18 | -------------------------------------------------------------------------------- /src/grammar.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Oliver Tale-Yazdi 3 | 4 | //! Grammar helpers for printing correct English. 5 | 6 | /// Add an plural `s` for English grammar iff `n != 1`. 7 | pub(crate) fn plural(n: usize) -> &'static str { 8 | if n == 1 { 9 | "" 10 | } else { 11 | "s" 12 | } 13 | } 14 | 15 | pub(crate) fn plural_or(n: usize, or: &str) -> String { 16 | if n == 1 { 17 | or.to_string() 18 | } else { 19 | "s".to_string() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /dist-workspace.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["cargo:."] 3 | 4 | # Config for 'dist' 5 | [dist] 6 | # The preferred dist version to use in CI (Cargo.toml SemVer syntax) 7 | cargo-dist-version = "0.30.0" 8 | # CI backends to support 9 | ci = "github" 10 | # The installers to generate for each app 11 | installers = [] 12 | # Target platforms to build apps for (Rust target-triple syntax) 13 | targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] 14 | -------------------------------------------------------------------------------- /.github/workflows/license-header.yml: -------------------------------------------------------------------------------- 1 | name: License 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: [ "master" ] 10 | pull_request: 11 | branches: [ "master" ] 12 | 13 | jobs: 14 | header: 15 | runs-on: self-hosted 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 1 21 | - name: SPDX Header 22 | run: python3 .github/header.py .github/HEADER . 23 | -------------------------------------------------------------------------------- /tests/integration/substrate/issue-14660-fix.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: paritytech/substrate 3 | ref: 2f205d28cddd2f71c229df091bf330951fec8500 4 | cases: 5 | - cmd: lint propagate-feature --feature try-runtime --left-side-feature-missing=ignore --workspace --fix --feature-enables-dep="try-runtime:frame-try-runtime" 6 | - cmd: lint propagate-feature --feature runtime-benchmarks --left-side-feature-missing=ignore --workspace --fix --feature-enables-dep="runtime-benchmarks:frame-benchmarking" 7 | - cmd: lint propagate-feature --feature std --left-side-feature-missing=ignore --workspace --fix 8 | -------------------------------------------------------------------------------- /tests/ui/lint/why-enabled/simple.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | features: 6 | F0: 7 | - - B 8 | - F0 9 | - name: B 10 | features: 11 | F0: null 12 | cases: 13 | - cmd: lint why-enabled -p A --feature F0 14 | code: 1 15 | stdout: | 16 | Did not find package A on the rhs of the dependency tree 17 | - cmd: lint why-enabled --package B --feature F0 18 | stdout: | 19 | Feature F0/B is enabled by: 20 | A/F0 21 | - cmd: lint why-enabled -p B --feature F1 22 | code: 1 23 | stdout: | 24 | Package B does not have feature F1 25 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/from_no_feat.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | - name: B 6 | features: 7 | F0: null 8 | cases: 9 | - cmd: lint propagate-feature --feature F0 10 | stdout: | 11 | crate 'A' 12 | feature 'F0' 13 | is required by 1 dependency: 14 | B 15 | Found 1 issue (run with `--fix` to fix). 16 | code: 1 17 | - cmd: lint propagate-feature --feature F0 --workspace 18 | stdout: | 19 | crate 'A' 20 | feature 'F0' 21 | is required by 1 dependency: 22 | B 23 | Found 1 issue (run with `--fix` to fix). 24 | code: 1 25 | -------------------------------------------------------------------------------- /tests/ui/config/v1/error.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | cases: 4 | - cmd: '' 5 | stdout: | 6 | Error: Command '--wrong' failed with exit code 2 7 | stderr: | 8 | [INFO] Running workflow 'default' 9 | error: unexpected argument '--wrong' found 10 | 11 | Usage: zepter [OPTIONS] [COMMAND] 12 | 13 | For more information, try '--help'. 14 | code: 1 15 | config: 16 | to_path: .zepter.yaml 17 | from_path: null 18 | verbatim: | 19 | version: 20 | format: 1 21 | binary: 0.12.0 22 | workflows: 23 | default: 24 | - [ '--wrong' ] 25 | no_default_args: true 26 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Basic 2 | edition = "2021" 3 | hard_tabs = true 4 | max_width = 100 5 | use_small_heuristics = "Max" 6 | # Imports 7 | imports_granularity = "Crate" 8 | reorder_imports = true 9 | # Consistency 10 | newline_style = "Unix" 11 | # Misc 12 | chain_width = 80 13 | spaces_around_ranges = false 14 | binop_separator = "Back" 15 | reorder_impl_items = false 16 | match_arm_leading_pipes = "Preserve" 17 | match_arm_blocks = false 18 | match_block_trailing_comma = true 19 | trailing_comma = "Vertical" 20 | trailing_semicolon = false 21 | use_field_init_shorthand = true 22 | # Format comments 23 | comment_width = 100 24 | wrap_comments = true 25 | -------------------------------------------------------------------------------- /tests/integration/sdk/duplicate_fix.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: paritytech/polkadot-sdk 3 | ref: 6a951f77bf0cbdb4bbb07783aac8a45bfb38351a 4 | cases: 5 | - cmd: lint duplicate-deps 6 | stdout: | 7 | Found 1 crate with duplicated dependencies between [dependencies] and [dev-dependencies] 8 | crate 'staging-chain-spec-builder' 9 | docify 10 | code: 1 11 | - cmd: lint duplicate-deps --show-paths 12 | stdout: | 13 | Found 1 crate with duplicated dependencies between [dependencies] and [dev-dependencies] 14 | crate 'staging-chain-spec-builder' (substrate/bin/utils/chain-spec-builder/Cargo.toml) 15 | docify 16 | code: 1 17 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/from_no_feat_re.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | rename: b 6 | - name: B 7 | features: 8 | F0: null 9 | cases: 10 | - cmd: lint propagate-feature --feature F0 11 | stdout: | 12 | crate 'A' 13 | feature 'F0' 14 | is required by 1 dependency: 15 | b (renamed from B) 16 | Found 1 issue (run with `--fix` to fix). 17 | code: 1 18 | - cmd: lint propagate-feature --feature F0 --workspace 19 | stdout: | 20 | crate 'A' 21 | feature 'F0' 22 | is required by 1 dependency: 23 | b (renamed from B) 24 | Found 1 issue (run with `--fix` to fix). 25 | code: 1 26 | -------------------------------------------------------------------------------- /tests/ui/lint/never-enables/rename.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | rename: b 6 | features: 7 | F0: 8 | - - b 9 | - G0 10 | - name: B 11 | features: 12 | G0: null 13 | cases: 14 | - cmd: lint never-enables --precondition F0 --stays-disabled G0 15 | stdout: | 16 | crate PackageName("A") 17 | feature "F0" 18 | enables feature "G0" on dependencies: 19 | B (renamed from b) 20 | - cmd: lint never-enables --precondition F0 --stays-disabled G0 --workspace 21 | stdout: | 22 | crate PackageName("A") 23 | feature "F0" 24 | enables feature "G0" on dependencies: 25 | B (renamed from b) 26 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/feature_enables_dep_same_feature_multi_deps.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | optional: true 6 | - name: C 7 | optional: true 8 | features: 9 | F0: null 10 | - name: B 11 | - name: C 12 | cases: 13 | - cmd: lint propagate-feature --feature F0 --feature-enables-dep F0:B,F0:C --fix 14 | stdout: | 15 | crate 'A' 16 | feature 'F0' 17 | must enable dependency as non-optional: 18 | B 19 | C 20 | Found 2 issues and fixed 2 (all fixed). 21 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex 6e53f70e68..6dda058f80 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -16,0 +17,2 @@ F0 = [\n+\t\"B\",\n+\t\"C\"\n" 22 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/ignore_missing_re.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | rename: b 6 | features: 7 | F0: null 8 | - name: B 9 | features: 10 | F0: null 11 | cases: 12 | - cmd: lint propagate-feature --feature F0 --fix --ignore-missing-propagate=A/F0:B/F0 13 | - cmd: lint propagate-feature --feature F0 --fix --ignore-missing-propagate=A/F0:b/F0 14 | stdout: | 15 | crate 'A' 16 | feature 'F0' 17 | must propagate to: 18 | b (renamed from B) 19 | Found 1 issue and fixed 1 (all fixed). 20 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex 4476b86aba..9053bab806 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"b/F0\"\n" 21 | -------------------------------------------------------------------------------- /tests/ui/lint/why-enabled/multi_step.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | features: 6 | F0: 7 | - - B 8 | - F1 9 | - name: B 10 | deps: 11 | - C 12 | features: 13 | F1: 14 | - - C 15 | - F2 16 | - name: C 17 | deps: 18 | - D 19 | features: 20 | F2: 21 | - - D 22 | - F3 23 | - name: D 24 | features: 25 | F3: null 26 | cases: 27 | - cmd: lint why-enabled -p B --feature F1 28 | stdout: | 29 | Feature F1/B is enabled by: 30 | A/F0 31 | - cmd: lint why-enabled -p C --feature F2 32 | stdout: | 33 | Feature F2/C is enabled by: 34 | B/F1 35 | - cmd: lint why-enabled -p D --feature F3 36 | stdout: | 37 | Feature F3/D is enabled by: 38 | C/F2 39 | -------------------------------------------------------------------------------- /.github/workflows/msrv.yml: -------------------------------------------------------------------------------- 1 | name: Rust MSRV 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: [ "master" ] 10 | pull_request: 11 | branches: [ "master" ] 12 | 13 | env: 14 | CARGO_TERM_COLOR: always 15 | 16 | jobs: 17 | build: 18 | runs-on: self-hosted 19 | name: "Check" 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | with: 24 | fetch-depth: 1 25 | - uses: actions-rust-lang/setup-rust-toolchain@v1 26 | - name: Install MSRV 27 | run: cargo install cargo-msrv --locked -q 28 | - name: Verify MSRV 29 | run: cargo msrv verify -- cargo install --locked --path . 30 | -------------------------------------------------------------------------------- /tests/ui/lint/duplicate-deps/dep_kinds_multiple.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: BD 5 | kind: dev 6 | - name: BD 7 | kind: build 8 | features: 9 | F0: null 10 | - name: B 11 | deps: 12 | - name: ND 13 | - name: ND 14 | kind: dev 15 | features: 16 | F0: null 17 | - name: C 18 | deps: 19 | - name: NB 20 | - name: NB 21 | kind: build 22 | features: 23 | F0: null 24 | - name: BD 25 | features: 26 | F0: null 27 | - name: ND 28 | features: 29 | F0: null 30 | - name: NB 31 | features: 32 | F0: null 33 | cases: 34 | - cmd: lint duplicate-deps 35 | stdout: | 36 | Found 1 crate with duplicated dependencies between [dependencies] and [dev-dependencies] 37 | crate 'B' 38 | ND 39 | code: 1 40 | -------------------------------------------------------------------------------- /tests/ui/lint/never-enables/simple.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | features: 6 | F0: 7 | - - B 8 | - G0 9 | F1: 10 | - - B 11 | - G1 12 | - name: B 13 | features: 14 | G0: null 15 | G1: null 16 | cases: 17 | - cmd: lint never-enables --precondition F0 --stays-disabled G0 18 | stdout: | 19 | crate PackageName("A") 20 | feature "F0" 21 | enables feature "G0" on dependencies: 22 | B 23 | - cmd: lint never-enables --precondition F1 --stays-disabled G1 24 | stdout: | 25 | crate PackageName("A") 26 | feature "F1" 27 | enables feature "G1" on dependencies: 28 | B 29 | - cmd: lint never-enables --precondition F0 --stays-disabled G1 30 | - cmd: lint never-enables --precondition F1 --stays-disabled G0 31 | -------------------------------------------------------------------------------- /tests/integration/polkadot/issue-7261.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: paritytech/polkadot 3 | ref: dbff1eb1bda7a70d8eef3e9662b369e2188ca563 4 | cases: 5 | - cmd: lint propagate-feature -p polkadot-test-runtime --feature std --fix 6 | stdout: | 7 | crate 'polkadot-test-runtime' 8 | feature 'std' 9 | must propagate to: 10 | beefy-primitives (renamed from sp-consensus-beefy) 11 | polkadot-runtime-parachains 12 | sp-mmr-primitives 13 | Found 3 issues and fixed 3 (all fixed). 14 | diff: "diff --git runtime/test-runtime/Cargo.toml runtime/test-runtime/Cargo.toml\nindex 6d38a0283d..97253325c5 100644\n--- runtime/test-runtime/Cargo.toml\n+++ runtime/test-runtime/Cargo.toml\n@@ -130,0 +131,3 @@ std = [\n+\t\"polkadot-runtime-parachains/std\",\n+\t\"beefy-primitives/std\",\n+\t\"sp-mmr-primitives/std\"\n" 15 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/feature_enables_dep.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | optional: true 6 | features: 7 | F0: null 8 | - name: B 9 | cases: 10 | - cmd: lint propagate-feature --feature F0 --feature-enables-dep F0:B --fix 11 | stdout: | 12 | crate 'A' 13 | feature 'F0' 14 | must enable dependency as non-optional: 15 | B 16 | Found 1 issue and fixed 1 (all fixed). 17 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex 3332f79684..26dd2b015c 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"B\"\n" 18 | - cmd: lint propagate-feature --feature F0 --feature-enables-dep F0:B 19 | stdout: | 20 | crate 'A' 21 | feature 'F0' 22 | must enable dependency as non-optional: 23 | B 24 | Found 1 issue (run with `--fix` to fix). 25 | code: 1 26 | -------------------------------------------------------------------------------- /tests/ui/lint/never-enables/diamond.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | - b 6 | features: 7 | F0: 8 | - - B 9 | - G0 10 | - - b 11 | - G0 12 | - name: B 13 | deps: 14 | - C 15 | features: 16 | G0: 17 | - - C 18 | - G0 19 | - name: b 20 | deps: 21 | - C 22 | features: 23 | G0: 24 | - - C 25 | - G0 26 | - name: C 27 | features: 28 | G0: null 29 | cases: 30 | - cmd: lint never-enables --precondition F0 --stays-disabled G0 31 | stdout: | 32 | crate PackageName("A") 33 | feature "F0" 34 | enables feature "G0" on dependencies: 35 | B 36 | b 37 | - cmd: lint never-enables --precondition F0 --stays-disabled G0 --workspace 38 | stdout: | 39 | crate PackageName("A") 40 | feature "F0" 41 | enables feature "G0" on dependencies: 42 | B 43 | b 44 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/feature_enables_dep_multiple.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | optional: true 6 | - name: C 7 | optional: true 8 | features: 9 | F0: null 10 | F1: null 11 | - name: B 12 | - name: C 13 | features: 14 | F1: null 15 | cases: 16 | - cmd: lint propagate-feature --feature F0,F1 --feature-enables-dep F0:B,F1:C --fix 17 | stdout: | 18 | crate 'A' 19 | feature 'F0' 20 | must enable dependency as non-optional: 21 | B 22 | Found 1 issue and fixed 1 (all fixed). 23 | crate 'A' 24 | feature 'F1' 25 | must propagate to: 26 | C 27 | Found 1 issue and fixed 1 (all fixed). 28 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex 957fb1d723..0d14f8f860 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -16,0 +17 @@ F0 = [\n+\t\"B\"\n@@ -18,0 +20 @@ F1 = [\n+\t\"C/F1\"\n" 29 | -------------------------------------------------------------------------------- /.github/header.py: -------------------------------------------------------------------------------- 1 | # Check that all rust files in the paths have a license header 2 | 3 | import os 4 | import sys 5 | 6 | print("CWD: " + os.getcwd()) 7 | 8 | def main(): 9 | # args are the paths to check 10 | header = sys.argv[1] 11 | paths = sys.argv[2:] 12 | missing = [] 13 | 14 | with open(header, "r") as f: 15 | header = f.read() 16 | 17 | for path in paths: 18 | for root, dirs, files in os.walk(path): 19 | for file in files: 20 | if file.endswith(".rs"): 21 | print("Checking " + os.path.join(root, file)) 22 | with open(os.path.join(root, file), "r") as f: 23 | if not f.read().startswith(header): 24 | missing.append(os.path.join(root, file)) 25 | 26 | if len(missing) > 0: 27 | print("The following files are missing a license header:") 28 | for file in missing: 29 | print(file) 30 | sys.exit(1) 31 | 32 | if __name__ == "__main__": 33 | main() 34 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/dep_kinds_rename.yaml: -------------------------------------------------------------------------------- 1 | comment: Test that the --dep-kinds argument works with renamed deps. 2 | crates: 3 | - name: A 4 | deps: 5 | - name: RD 6 | rename: rd 7 | kind: dev 8 | - name: RD 9 | rename: rd 10 | - name: RNDB 11 | rename: rndb 12 | - name: RNDB 13 | rename: rndb 14 | kind: build 15 | - name: RNDB 16 | rename: rndb 17 | kind: dev 18 | - name: RD 19 | features: 20 | F0: null 21 | - name: RNDB 22 | features: 23 | F0: null 24 | cases: 25 | - cmd: lint propagate-feature --feature F0 --dep-kinds="normal:ignore,dev:ignore,build:ignore" 26 | - cmd: lint propagate-feature --feature F0 27 | stdout: | 28 | crate 'A' 29 | feature 'F0' 30 | is required by 2 dependencies: 31 | rd (renamed from RD) 32 | rndb (renamed from RNDB) 33 | Found 1 issue (run with `--fix` to fix). 34 | code: 1 35 | -------------------------------------------------------------------------------- /tests/ui/fmt/dedub.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | features: 6 | F0: 7 | - - B 8 | - F0 9 | - - B 10 | - F0 11 | - name: B 12 | features: 13 | F0: null 14 | cases: 15 | - cmd: format f -f 16 | stdout: | 17 | Found 2 crates with unformatted features: 18 | A 19 | B 20 | Formatted 2 crates (all fixed). 21 | diff: | 22 | diff --git A/Cargo.toml A/Cargo.toml 23 | index eba55cd80c..8849621452 100644 24 | --- A/Cargo.toml 25 | +++ A/Cargo.toml 26 | @@ -15,4 +15 @@ B = { version = "*", path = "../B"} 27 | -F0 = [ 28 | -"B/F0", 29 | -"B/F0", 30 | -] 31 | +F0 = [ "B/F0" ] 32 | diff --git B/Cargo.toml B/Cargo.toml 33 | index 195f3af664..4f68eee744 100644 34 | --- B/Cargo.toml 35 | +++ B/Cargo.toml 36 | @@ -14,2 +14 @@ edition = "2024" 37 | -F0 = [ 38 | -] 39 | +F0 = [] 40 | - cmd: format f 41 | stdout: | 42 | Found 2 crates with unformatted features: 43 | A 44 | B 45 | Run again with `--fix` to format them. 46 | code: 1 47 | -------------------------------------------------------------------------------- /tests/ui/lint/never-implies/transitive.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | - b 6 | features: 7 | F0: 8 | - - B 9 | - G0 10 | - - b 11 | - G1 12 | - name: B 13 | deps: 14 | - C 15 | features: 16 | G0: 17 | - - C 18 | - H0 19 | - name: b 20 | deps: 21 | - C 22 | features: 23 | G1: 24 | - - C 25 | - H1 26 | - name: C 27 | features: 28 | H1: null 29 | H0: null 30 | cases: 31 | - cmd: lint never-implies --precondition F0 --stays-disabled H1 32 | stdout: | 33 | Feature 'F0' implies 'H1' via path: 34 | A/F0 -> b/G1 -> C/H1 35 | - cmd: lint never-implies --precondition F0 --stays-disabled H0 36 | stdout: | 37 | Feature 'F0' implies 'H0' via path: 38 | A/F0 -> B/G0 -> C/H0 39 | - cmd: lint never-implies --precondition G0 --stays-disabled H0 40 | stdout: | 41 | Feature 'G0' implies 'H0' via path: 42 | B/G0 -> C/H0 43 | - cmd: lint never-implies --precondition G1 --stays-disabled H1 44 | stdout: | 45 | Feature 'G1' implies 'H1' via path: 46 | b/G1 -> C/H1 47 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/from_select.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: a 3 | deps: 4 | - B 5 | - name: A 6 | deps: 7 | - B 8 | - name: B 9 | features: 10 | F0: null 11 | cases: 12 | - cmd: lint propagate-feature --feature F0 13 | stdout: | 14 | crate 'A' 15 | feature 'F0' 16 | is required by 1 dependency: 17 | B 18 | crate 'a' 19 | feature 'F0' 20 | is required by 1 dependency: 21 | B 22 | Found 2 issues (run with `--fix` to fix). 23 | code: 1 24 | - cmd: lint propagate-feature -p a --feature F0 25 | stdout: | 26 | crate 'a' 27 | feature 'F0' 28 | is required by 1 dependency: 29 | B 30 | Found 1 issue (run with `--fix` to fix). 31 | code: 1 32 | - cmd: lint propagate-feature -p a A --feature F0 33 | stdout: | 34 | crate 'A' 35 | feature 'F0' 36 | is required by 1 dependency: 37 | B 38 | crate 'a' 39 | feature 'F0' 40 | is required by 1 dependency: 41 | B 42 | Found 2 issues (run with `--fix` to fix). 43 | code: 1 44 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/fix_from_no_feat.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | - name: B 6 | features: 7 | F0: null 8 | cases: 9 | - cmd: lint propagate-feature --feature F0 --fix 10 | stdout: | 11 | crate 'A' 12 | feature 'F0' 13 | is required by 1 dependency: 14 | B 15 | Found 1 issue and fixed 1 (all fixed). 16 | code: 0 17 | diff: | 18 | diff --git A/Cargo.toml A/Cargo.toml 19 | index 7f2ba2ef51..197e942470 100644 20 | --- A/Cargo.toml 21 | +++ A/Cargo.toml 22 | @@ -14,0 +15 @@ B = { version = "*", path = "../B"} 23 | +F0 = [] 24 | - cmd: lint propagate-feature --feature F0 --workspace --fix 25 | stdout: | 26 | crate 'A' 27 | feature 'F0' 28 | is required by 1 dependency: 29 | B 30 | Found 1 issue and fixed 1 (all fixed). 31 | code: 0 32 | diff: | 33 | diff --git A/Cargo.toml A/Cargo.toml 34 | index 7f2ba2ef51..197e942470 100644 35 | --- A/Cargo.toml 36 | +++ A/Cargo.toml 37 | @@ -14,0 +15 @@ B = { version = "*", path = "../B"} 38 | +F0 = [] 39 | -------------------------------------------------------------------------------- /.github/workflows/toml.yml: -------------------------------------------------------------------------------- 1 | name: TOML 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: [ "master" ] 10 | pull_request: 11 | branches: [ "master" ] 12 | 13 | env: 14 | CARGO_TERM_COLOR: always 15 | 16 | jobs: 17 | lint: 18 | name: "Lint" 19 | runs-on: self-hosted 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | with: 24 | fetch-depth: 2 25 | 26 | - name: Skip if unchanged 27 | id: changed-files-specific 28 | uses: tj-actions/changed-files@v46 29 | with: 30 | files: | 31 | *.toml 32 | *.tml 33 | 34 | - name: Install Cargo TOML linter 35 | if: steps.changed-files-specific.outputs.only_changed == 'true' 36 | run: cargo install cargo-toml-lint --version 0.1.1 --locked 37 | 38 | - name: Lint Cargo.toml 39 | if: steps.changed-files-specific.outputs.only_changed == 'true' 40 | run: cargo-toml-lint Cargo.toml 41 | -------------------------------------------------------------------------------- /.github/workflows/install.yml: -------------------------------------------------------------------------------- 1 | name: Smokescreen 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: [ "master" ] 10 | pull_request: 11 | branches: [ "master" ] 12 | 13 | env: 14 | CARGO_TERM_COLOR: always 15 | RUST_BACKTRACE: 1 16 | 17 | jobs: 18 | install: 19 | name: "Cargo install" 20 | runs-on: self-hosted 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | with: 25 | fetch-depth: 1 26 | 27 | - uses: actions-rust-lang/setup-rust-toolchain@v1 28 | with: 29 | toolchain: 1.85 30 | 31 | - name: Cargo install 32 | run: cargo install --path . --locked -q 33 | 34 | - name: CLI works 35 | run: zepter --version && zepter --help 36 | 37 | - name: Cargo install (no-default-features) 38 | run: cargo install --path . --locked -q --no-default-features 39 | 40 | - name: CLI works (no-default-features) 41 | run: zepter --version && zepter --help 42 | -------------------------------------------------------------------------------- /tests/ui/config/v1/version_bin.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | cases: 4 | - cmd: run default 5 | stderr: | 6 | Your version of Zepter is too old for this project. 7 | 8 | Required: 2.0.0 9 | Installed: 1.85.0 10 | 11 | Please update Zepter with: 12 | 13 | cargo install zepter --locked 14 | 15 | Or add `--check-cfg-compatibility=off` to the config file. 16 | code: 101 17 | - cmd: run default --check-cfg-compatibility=off 18 | stdout: | 19 | Error: Command '' failed with exit code 101 20 | stderr: | 21 | [INFO] Running workflow 'default' 22 | Your version of Zepter is too old for this project. 23 | 24 | Required: 2.0.0 25 | Installed: 1.85.0 26 | 27 | Please update Zepter with: 28 | 29 | cargo install zepter --locked 30 | 31 | Or add `--check-cfg-compatibility=off` to the config file. 32 | code: 1 33 | configs: 34 | - to_path: .zepter.yaml 35 | from_path: null 36 | verbatim: | 37 | version: 38 | format: 1 39 | binary: 2.0.0 40 | 41 | workflows: 42 | default: 43 | - [ ] 44 | no_default_args: true 45 | -------------------------------------------------------------------------------- /.deny.toml: -------------------------------------------------------------------------------- 1 | 2 | 3 | all-features = false 4 | feature-depth = 1 5 | no-default-features = false 6 | targets = [] 7 | 8 | [advisories] 9 | unsound = "deny" 10 | 11 | db-path = "~/.cargo/advisory-db" 12 | db-urls = ["https://github.com/rustsec/advisory-db"] 13 | ignore = [] 14 | notice = "warn" 15 | unmaintained = "warn" 16 | vulnerability = "deny" 17 | yanked = "warn" 18 | 19 | [licenses] 20 | allow = [ 21 | "Apache-2.0", 22 | "MIT", 23 | "Unicode-DFS-2016", 24 | ] 25 | allow-osi-fsf-free = "neither" 26 | confidence-threshold = 0.8 27 | copyleft = "allow" 28 | default = "deny" 29 | deny = [] 30 | exceptions = [] 31 | unlicensed = "deny" 32 | 33 | [licenses.private] 34 | ignore = false 35 | registries = [] 36 | 37 | [bans] 38 | allow = [] 39 | deny = [] 40 | external-default-features = "allow" 41 | highlight = "all" 42 | multiple-versions = "warn" 43 | wildcards = "allow" 44 | workspace-default-features = "allow" 45 | 46 | skip = [] 47 | skip-tree = [] 48 | 49 | [sources] 50 | allow-git = [] 51 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 52 | unknown-git = "warn" 53 | unknown-registry = "warn" 54 | -------------------------------------------------------------------------------- /.github/workflows/linkcheck.yml: -------------------------------------------------------------------------------- 1 | name: Markdown 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: [ "master" ] 10 | pull_request: 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | 15 | jobs: 16 | linkCheck: 17 | name: "Lint" 18 | runs-on: self-hosted 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 2 24 | 25 | - name: Skip if unchanged 26 | id: changed-files-specific 27 | uses: tj-actions/changed-files@v46 28 | with: 29 | files: | 30 | *.md 31 | *.markdown 32 | 33 | - name: Restore lychee cache 34 | uses: actions/cache@v3 35 | with: 36 | path: .lycheecache 37 | key: cache-lychee-${{ github.sha }} 38 | restore-keys: cache-lychee- 39 | 40 | - name: Link Checker 41 | uses: lycheeverse/lychee-action@v2.0.2 42 | with: 43 | args: "--verbose --cache --max-cache-age 1d . --accept 200,429" 44 | fail: true 45 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/fix_from_no_feat_duplicate.yaml: -------------------------------------------------------------------------------- 1 | # Fix feature issue, but the same feature is listed multiple times. 2 | crates: 3 | - name: A 4 | deps: 5 | - B 6 | - name: B 7 | features: 8 | F0: null 9 | cases: 10 | - cmd: lint propagate-feature --feature F0,F0 --fix 11 | stdout: | 12 | crate 'A' 13 | feature 'F0' 14 | is required by 1 dependency: 15 | B 16 | Found 1 issue and fixed 1 (all fixed). 17 | code: 0 18 | diff: | 19 | diff --git A/Cargo.toml A/Cargo.toml 20 | index 7f2ba2ef51..197e942470 100644 21 | --- A/Cargo.toml 22 | +++ A/Cargo.toml 23 | @@ -14,0 +15 @@ B = { version = "*", path = "../B"} 24 | +F0 = [] 25 | - cmd: lint propagate-feature --feature F0,F0,F0 --workspace --fix 26 | stdout: | 27 | crate 'A' 28 | feature 'F0' 29 | is required by 1 dependency: 30 | B 31 | Found 1 issue and fixed 1 (all fixed). 32 | code: 0 33 | diff: | 34 | diff --git A/Cargo.toml A/Cargo.toml 35 | index 7f2ba2ef51..197e942470 100644 36 | --- A/Cargo.toml 37 | +++ A/Cargo.toml 38 | @@ -14,0 +15 @@ B = { version = "*", path = "../B"} 39 | +F0 = [] 40 | -------------------------------------------------------------------------------- /src/cmd/run.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Oliver Tale-Yazdi 3 | 4 | use super::GlobalArgs; 5 | use crate::{ 6 | config::{workflow::WORKFLOW_DEFAULT_NAME, ConfigArgs}, 7 | log, 8 | }; 9 | 10 | #[derive(Default, Debug, clap::Parser)] 11 | pub struct RunCmd { 12 | #[clap(flatten)] 13 | pub args: RunArgs, 14 | } 15 | 16 | #[derive(Default, Debug, clap::Parser)] 17 | pub struct RunArgs { 18 | #[clap(flatten)] 19 | pub config: ConfigArgs, 20 | 21 | #[clap(name = "WORKFLOW", index = 1)] 22 | pub workflow: Option, 23 | } 24 | 25 | impl RunCmd { 26 | pub fn run(&self, g: &GlobalArgs) { 27 | let config = self.args.config.load_or_panic(); 28 | 29 | let name = self.args.workflow.as_deref().unwrap_or(WORKFLOW_DEFAULT_NAME); 30 | let Some(workflow) = config.workflow(name) else { 31 | panic!("Workflow '{name}' not found"); 32 | }; 33 | 34 | log::info!("Running workflow '{}'", name); 35 | if let Err(err) = workflow.run(g) { 36 | println!("Error: {err}"); 37 | 38 | if let Some(help) = config.fmt_help() { 39 | println!("\n{help}"); 40 | } 41 | 42 | std::process::exit(1); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/ui/config/v1/custom_help.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | features: 6 | F0: 7 | - - B 8 | - F0 9 | - - B 10 | - F0 11 | - name: B 12 | features: 13 | F0: null 14 | cases: 15 | - cmd: '' 16 | stdout: | 17 | Found 2 crates with unformatted features: 18 | A 19 | B 20 | Error: Command 'f f' failed with exit code 1 21 | 22 | 23 | 24 | 25 | 26 | For more information, see: 27 | - 28 | - 29 | - 30 | stderr: | 31 | [INFO] Running workflow 'default' 32 | code: 1 33 | configs: 34 | - to_path: .zepter.yaml 35 | from_path: null 36 | verbatim: | 37 | version: 38 | format: 1 39 | binary: 0.12.0 40 | 41 | workflows: 42 | default: 43 | - [ f, f ] 44 | help: 45 | text: | 46 | 47 | 48 | 49 | links: 50 | - "" 51 | - "" 52 | - "" 53 | no_default_args: true 54 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: [ "master" ] 10 | pull_request: 11 | branches: [ "master" ] 12 | 13 | env: 14 | CARGO_TERM_COLOR: always 15 | 16 | jobs: 17 | build: 18 | runs-on: self-hosted 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 1 24 | - uses: actions-rust-lang/setup-rust-toolchain@v1 25 | with: 26 | toolchain: 1.85 27 | components: rustfmt,clippy 28 | 29 | - name: Build 30 | run: | 31 | cargo build --all-targets --all-features --locked -q 32 | cargo build --all-targets --no-default-features --locked -q 33 | 34 | - name: Run Unit tests 35 | run: cargo test --locked -- --nocapture 36 | 37 | - name: Run UI tests 38 | run: cargo test --locked -- --nocapture --ignored 39 | 40 | - name: Clippy 41 | run: | 42 | cargo clippy --all-targets --all-features --tests -q 43 | cargo clippy --all-targets --no-default-features -q 44 | 45 | - name: Format 46 | run: cargo +nightly fmt --check 47 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Oliver Tale-Yazdi 3 | 4 | //! Entry point of the program. 5 | 6 | use clap::Parser; 7 | use zepter::cmd::Command; 8 | 9 | fn main() -> Result<(), ()> { 10 | setup_logging(); 11 | 12 | // Need to remove this in case `cargo-zepter` is used: 13 | let mut args = std::env::args().collect::>(); 14 | if args.len() > 1 && args[1] == "zepter" { 15 | args.remove(1); 16 | } 17 | 18 | if let Err(err) = Command::parse_from(args).run() { 19 | eprintln!("{err}"); 20 | Err(()) 21 | } else { 22 | Ok(()) 23 | } 24 | } 25 | 26 | #[cfg(not(feature = "logging"))] 27 | fn setup_logging() {} 28 | 29 | #[cfg(feature = "logging")] 30 | fn setup_logging() { 31 | use std::io::Write; 32 | 33 | env_logger::builder() 34 | .parse_env(env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "debug")) 35 | .format_timestamp(None) 36 | .format(|buf, record| { 37 | let level_style = buf.default_level_style(record.level()).bold(); 38 | let begin = level_style.render(); 39 | let reset = level_style.render_reset(); 40 | 41 | writeln!(buf, "[{begin}{}{reset}] {}", record.level(), record.args()) 42 | }) 43 | .init(); 44 | } 45 | -------------------------------------------------------------------------------- /tests/integration/substrate/issue-14491.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: paritytech/substrate 3 | ref: 9705afd8b17d3e0c8557fe220cfccffd8986bd6a 4 | cases: 5 | - cmd: lint propagate-feature -p pallet-name-service --feature try-runtime 6 | stdout: | 7 | crate 'pallet-name-service' 8 | feature 'try-runtime' 9 | is required by 4 dependencies: 10 | frame-support 11 | frame-system 12 | pallet-balances 13 | sp-runtime 14 | Found 1 issue (run with `--fix` to fix). 15 | code: 1 16 | - cmd: lint propagate-feature -p pallet-name-service --feature try-runtime --fix 17 | stdout: | 18 | crate 'pallet-name-service' 19 | feature 'try-runtime' 20 | is required by 4 dependencies: 21 | frame-support 22 | frame-system 23 | pallet-balances 24 | sp-runtime 25 | Found 1 issue and fixed 1 (all fixed). 26 | code: 0 27 | diff: | 28 | diff --git frame/name-service/Cargo.toml frame/name-service/Cargo.toml 29 | index be69eced78..b4bb16690e 100644 30 | --- frame/name-service/Cargo.toml 31 | +++ frame/name-service/Cargo.toml 32 | @@ -45 +45,2 @@ runtime-benchmarks = [ 33 | -] 34 | \ No newline at end of file 35 | +] 36 | +try-runtime = [] 37 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/args.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | features: 6 | F0: null 7 | - name: B 8 | features: 9 | F0: null 10 | cases: 11 | - cmd: lint propagate-feature --show-version --workspace --feature F0 12 | stdout: | 13 | crate 'A' 14 | feature 'F0' 15 | must propagate to: 16 | B 17 | Found 1 issue (run with `--fix` to fix). 18 | code: 1 19 | - cmd: lint propagate-feature --show-version --workspace --feature F0 --color 20 | stdout: "crate 'A'\n feature 'F0'\n must propagate to:\n B\nFound \e[31m1 issue\e[0m (run with `--fix` to fix).\n" 21 | code: 1 22 | - cmd: lint propagate-feature --show-version --workspace --feature F0 --color --quiet 23 | stdout: "crate 'A'\n feature 'F0'\n must propagate to:\n B\nFound \e[31m1 issue\e[0m (run with `--fix` to fix).\n" 24 | code: 1 25 | - cmd: lint propagate-feature --show-version --workspace --feature F0 --color --quiet 26 | stdout: "crate 'A'\n feature 'F0'\n must propagate to:\n B\nFound \e[31m1 issue\e[0m (run with `--fix` to fix).\n" 27 | code: 1 28 | - cmd: lint propagate-feature --show-version --workspace --feature F0 --color --quiet --left-side-feature-missing=ignore --left-side-outside-workspace=ignore 29 | stdout: "crate 'A'\n feature 'F0'\n must propagate to:\n B\nFound \e[31m1 issue\e[0m (run with `--fix` to fix).\n" 30 | code: 1 31 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/not_propagated_re.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | rename: b 6 | features: 7 | F0: null 8 | - name: B 9 | features: 10 | F0: null 11 | cases: 12 | - cmd: lint propagate-feature --feature F0 13 | stdout: | 14 | crate 'A' 15 | feature 'F0' 16 | must propagate to: 17 | b (renamed from B) 18 | Found 1 issue (run with `--fix` to fix). 19 | code: 1 20 | - cmd: lint propagate-feature -p A --feature F0 21 | stdout: | 22 | crate 'A' 23 | feature 'F0' 24 | must propagate to: 25 | b (renamed from B) 26 | Found 1 issue (run with `--fix` to fix). 27 | code: 1 28 | - cmd: lint propagate-feature -p B --feature F0 29 | - cmd: lint propagate-feature -p B --feature F1 30 | - cmd: lint propagate-feature --feature F0 --workspace 31 | stdout: | 32 | crate 'A' 33 | feature 'F0' 34 | must propagate to: 35 | b (renamed from B) 36 | Found 1 issue (run with `--fix` to fix). 37 | code: 1 38 | - cmd: lint propagate-feature -p A --feature F0 --workspace 39 | stdout: | 40 | crate 'A' 41 | feature 'F0' 42 | must propagate to: 43 | b (renamed from B) 44 | Found 1 issue (run with `--fix` to fix). 45 | code: 1 46 | - cmd: lint propagate-feature -p B --feature F0 --workspace 47 | - cmd: lint propagate-feature -p B --feature F1 --workspace 48 | -------------------------------------------------------------------------------- /tests/integration/substrate/frame-support.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: paritytech/substrate 3 | ref: 2cc2e05e78b1e9109669dc959ac7656eb46b3492 4 | cases: 5 | - cmd: lint propagate-feature -p frame-support --feature runtime-benchmarks 6 | - cmd: lint propagate-feature -p frame-support --feature runtime-benchmarks --workspace 7 | - cmd: lint propagate-feature -p frame-support --feature std 8 | stdout: | 9 | crate 'frame-support' 10 | feature 'std' 11 | must propagate to: 12 | once_cell 13 | sp-debug-derive 14 | Found 2 issues (run with `--fix` to fix). 15 | code: 1 16 | - cmd: lint propagate-feature -p frame-support --feature std --workspace 17 | stdout: | 18 | crate 'frame-support' 19 | feature 'std' 20 | must propagate to: 21 | sp-debug-derive 22 | Found 1 issue (run with `--fix` to fix). 23 | code: 1 24 | - cmd: lint propagate-feature -p frame-support --feature try-runtime 25 | stdout: | 26 | crate 'frame-support' 27 | feature 'try-runtime' 28 | must propagate to: 29 | frame-system 30 | sp-runtime 31 | Found 2 issues (run with `--fix` to fix). 32 | code: 1 33 | - cmd: lint propagate-feature -p frame-support --feature try-runtime --workspace 34 | stdout: | 35 | crate 'frame-support' 36 | feature 'try-runtime' 37 | must propagate to: 38 | frame-system 39 | sp-runtime 40 | Found 2 issues (run with `--fix` to fix). 41 | code: 1 42 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/not_propagated.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | features: 6 | F0: null 7 | - name: B 8 | features: 9 | F0: null 10 | cases: 11 | - cmd: lint propagate-feature --feature F0 12 | stdout: | 13 | crate 'A' 14 | feature 'F0' 15 | must propagate to: 16 | B 17 | Found 1 issue (run with `--fix` to fix). 18 | code: 1 19 | - cmd: lint propagate-feature -p A --feature F0 20 | stdout: | 21 | crate 'A' 22 | feature 'F0' 23 | must propagate to: 24 | B 25 | Found 1 issue (run with `--fix` to fix). 26 | code: 1 27 | - cmd: lint propagate-feature -p B --feature F0 28 | - cmd: lint propagate-feature -p B --feature F1 29 | - cmd: lint propagate-feature --feature F0 --workspace 30 | stdout: | 31 | crate 'A' 32 | feature 'F0' 33 | must propagate to: 34 | B 35 | Found 1 issue (run with `--fix` to fix). 36 | code: 1 37 | - cmd: lint propagate-feature -p A --feature F0 --workspace 38 | stdout: | 39 | crate 'A' 40 | feature 'F0' 41 | must propagate to: 42 | B 43 | Found 1 issue (run with `--fix` to fix). 44 | code: 1 45 | - cmd: lint propagate-feature -p B --feature F0 --workspace 46 | - cmd: lint propagate-feature -p B --feature F1 --workspace 47 | - cmd: lint propagate-feature --feature F0 --exit-code-zero 48 | stdout: | 49 | crate 'A' 50 | feature 'F0' 51 | must propagate to: 52 | B 53 | Found 1 issue (run with `--fix` to fix). 54 | -------------------------------------------------------------------------------- /.github/workflows/downstream_nightly.yml: -------------------------------------------------------------------------------- 1 | # Prevents accidential breackage for known downstream projects. 2 | 3 | name: Downstream Integration 4 | 5 | concurrency: 6 | group: ${{ github.workflow }} 7 | cancel-in-progress: true 8 | 9 | on: 10 | push: 11 | branches: [ "master" ] 12 | pull_request: 13 | branches: [ "master" ] 14 | schedule: 15 | - cron: "0 0 * * *" 16 | 17 | env: 18 | CARGO_TERM_COLOR: always 19 | 20 | jobs: 21 | dotsama: 22 | runs-on: self-hosted 23 | name: "Check" 24 | strategy: 25 | matrix: 26 | repo: ["paritytech/polkadot-sdk", "open-web3-stack/open-runtime-module-library", "alloy-rs/alloy", "paradigmxyz/reth"] 27 | version: ["*"] 28 | 29 | steps: 30 | - uses: actions/checkout@master 31 | name: Clone repo 32 | with: 33 | repository: ${{ matrix.repo }} 34 | fetch-depth: 1 35 | 36 | - uses: actions-rust-lang/setup-rust-toolchain@v1 37 | with: 38 | toolchain: 1.85 39 | 40 | - name: Install 41 | run: cargo install zepter --version '${{ matrix.version }}' -f --locked -q --no-default-features 42 | 43 | - if: matrix.repo == 'open-web3-stack/open-runtime-module-library' && matrix.version == '*' 44 | name: Copy cargo toml 45 | run: | 46 | cp Cargo.dev.toml Cargo.toml 47 | cargo generate-lockfile 48 | 49 | - if: matrix.repo != 'open-web3-stack/open-runtime-module-library' || (matrix.repo == 'open-web3-stack/open-runtime-module-library' && matrix.version == '*') 50 | name: Zepter Passes on ${{ matrix.repo }} 51 | run: | 52 | zepter run check 53 | zepter 54 | git diff --exit-code 55 | -------------------------------------------------------------------------------- /.github/workflows/release_binaries.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build_and_upload: 11 | strategy: 12 | matrix: 13 | platform: 14 | - { os: ubuntu-latest, target: x86_64-unknown-linux-musl } 15 | - { os: macos-latest, target: x86_64-apple-darwin } 16 | 17 | runs-on: ${{ matrix.platform.os }} 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup Rust 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | profile: minimal 27 | toolchain: stable 28 | override: true 29 | 30 | - name: Add target 31 | run: rustup target add ${{ matrix.platform.target }} 32 | 33 | - name: Install deps for musl build 34 | if: matrix.platform.os == 'ubuntu-latest' 35 | run: | 36 | sudo apt-get update 37 | sudo apt-get install -y musl-tools clang build-essential curl llvm-dev libclang-dev linux-headers-generic libsnappy-dev liblz4-dev libzstd-dev libgflags-dev zlib1g-dev libbz2-dev 38 | sudo ln -s /usr/bin/g++ /usr/bin/musl-g++ 39 | 40 | - name: Build 41 | run: cargo build --release --target ${{ matrix.platform.target }} 42 | 43 | - name: Upload Binary to Release 44 | uses: actions/upload-release-asset@v1 45 | with: 46 | upload_url: ${{ github.event.release.upload_url }} 47 | asset_path: ./target/${{ matrix.platform.target }}/release/zepter 48 | asset_name: zepter-${{ matrix.platform.target }} 49 | asset_content_type: application/octet-stream 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | -------------------------------------------------------------------------------- /tests/ui/config/v1/basic.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | cases: 4 | - cmd: run default 5 | stdout: | 6 | zepter 1.85.0 7 | stderr: | 8 | [INFO] Running workflow 'default' 9 | [INFO] 1/1 --version 10 | - cmd: run 11 | stdout: | 12 | zepter 1.85.0 13 | stderr: | 14 | [INFO] Running workflow 'default' 15 | [INFO] 1/1 --version 16 | - cmd: '' 17 | stdout: | 18 | zepter 1.85.0 19 | stderr: | 20 | [INFO] Running workflow 'default' 21 | [INFO] 1/1 --version 22 | - cmd: run my_version 23 | stdout: | 24 | zepter 1.85.0 25 | stderr: | 26 | [INFO] Running workflow 'my_version' 27 | [INFO] 1/1 --version 28 | - cmd: run my_debug 29 | stdout: | 30 | Num workspace members: 1 31 | Num dependencies: 1 32 | DAG nodes: 0, links: 0 33 | stderr: | 34 | [INFO] Running workflow 'my_debug' 35 | [WARN] Unstable feature - do not rely on this! 36 | [INFO] 1/1 debug --no-benchmark 37 | - cmd: run both 38 | stdout: | 39 | zepter 1.85.0 40 | Num workspace members: 1 41 | Num dependencies: 1 42 | DAG nodes: 0, links: 0 43 | stderr: | 44 | [INFO] Running workflow 'both' 45 | [INFO] 1/2 --version 46 | [WARN] Unstable feature - do not rely on this! 47 | [INFO] 2/2 debug --no-benchmark 48 | configs: 49 | - to_path: .zepter.yaml 50 | from_path: null 51 | verbatim: | 52 | version: 53 | format: 1 54 | binary: 0.12.0 55 | 56 | workflows: 57 | my_version: 58 | - [ '--version' ] 59 | my_debug: 60 | - [ 'debug', '--no-benchmark', '--no-root' ] 61 | default: 62 | - [ $my_version.0 ] 63 | both: 64 | - [ $my_version.0 ] 65 | - [ $my_debug.0 ] 66 | no_default_args: true 67 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/ignore_missing.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | features: 6 | F0: null 7 | - name: B 8 | deps: 9 | - C 10 | features: 11 | F0: null 12 | - name: C 13 | features: 14 | F0: null 15 | cases: 16 | - cmd: lint propagate-feature --feature F0 --fix --ignore-missing-propagate=A/F0:B/F0,B/F0:C/F0 17 | - cmd: lint propagate-feature --feature F0 --fix --ignore-missing-propagate=A/F0:B/F0 18 | stdout: | 19 | crate 'B' 20 | feature 'F0' 21 | must propagate to: 22 | C 23 | Found 1 issue and fixed 1 (all fixed). 24 | diff: "diff --git B/Cargo.toml B/Cargo.toml\nindex 27e98ccfef..f5bb28facb 100644\n--- B/Cargo.toml\n+++ B/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"C/F0\"\n" 25 | - cmd: lint propagate-feature --feature F0 --fix --ignore-missing-propagate=B/F0:C/F0 26 | stdout: | 27 | crate 'A' 28 | feature 'F0' 29 | must propagate to: 30 | B 31 | Found 1 issue and fixed 1 (all fixed). 32 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex b2b4a461c2..8256aa1ec4 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"B/F0\"\n" 33 | - cmd: lint propagate-feature --feature F0 --fix --ignore-missing-propagate=A/F0:C/F0 34 | stdout: | 35 | crate 'A' 36 | feature 'F0' 37 | must propagate to: 38 | B 39 | crate 'B' 40 | feature 'F0' 41 | must propagate to: 42 | C 43 | Found 2 issues and fixed 2 (all fixed). 44 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex b2b4a461c2..8256aa1ec4 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"B/F0\"\ndiff --git B/Cargo.toml B/Cargo.toml\nindex 27e98ccfef..f5bb28facb 100644\n--- B/Cargo.toml\n+++ B/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"C/F0\"\n" 45 | -------------------------------------------------------------------------------- /.github/workflows/downstream_legacy.yml: -------------------------------------------------------------------------------- 1 | name: Integration test master 2 | 3 | # This tests that Zepter wont panic on Polkadot-SDK, Substrate, Polkadot or Cumulus. 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 7 | cancel-in-progress: true 8 | 9 | on: 10 | push: 11 | branches: [ "master" ] 12 | pull_request: 13 | branches: [ "master" ] 14 | 15 | env: 16 | CARGO_TERM_COLOR: always 17 | 18 | jobs: 19 | dotsama: 20 | runs-on: self-hosted 21 | name: "Check" 22 | strategy: 23 | matrix: 24 | repo: [substrate] 25 | 26 | steps: 27 | - uses: actions/checkout@master 28 | name: Clone ${{ matrix.repo }} 29 | with: 30 | repository: paritytech/${{ matrix.repo }} 31 | fetch-depth: 1 32 | 33 | - uses: actions-rust-lang/setup-rust-toolchain@v1 34 | with: 35 | toolchain: 1.85 36 | 37 | - name: Cargo install 38 | run: cargo install --git ${{ github.server_url }}/${{ github.repository }} zepter --rev $GITHUB_SHA --locked -q 39 | 40 | # Substrate master should be green since its using Zepter in its CI. 41 | - name: Zepter passes 42 | run: | 43 | echo "Checking features #1" 44 | zepter lint propagate-feature --feature try-runtime --left-side-feature-missing=ignore --workspace --fix --feature-enables-dep="try-runtime:frame-try-runtime" 45 | echo "Checking features #2" 46 | zepter lint propagate-feature --feature runtime-benchmarks --left-side-feature-missing=ignore --workspace --feature-enables-dep="runtime-benchmarks:frame-benchmarking" 47 | echo "Checking features #3" 48 | zepter lint propagate-feature --feature std --left-side-feature-missing=ignore --workspace 49 | echo "Checking formatting #1" 50 | zepter format features --check --exit-code-zero 51 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/fix_not_propagated.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | features: 6 | F0: null 7 | - name: B 8 | features: 9 | F0: null 10 | cases: 11 | - cmd: lint propagate-feature --feature F0 --fix 12 | stdout: | 13 | crate 'A' 14 | feature 'F0' 15 | must propagate to: 16 | B 17 | Found 1 issue and fixed 1 (all fixed). 18 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex b2b4a461c2..8256aa1ec4 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"B/F0\"\n" 19 | - cmd: lint propagate-feature -p A --feature F0 --fix 20 | stdout: | 21 | crate 'A' 22 | feature 'F0' 23 | must propagate to: 24 | B 25 | Found 1 issue and fixed 1 (all fixed). 26 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex b2b4a461c2..8256aa1ec4 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"B/F0\"\n" 27 | - cmd: lint propagate-feature -p B --feature F0 --fix 28 | - cmd: lint propagate-feature -p B --feature F1 --fix 29 | - cmd: lint propagate-feature --feature F0 --workspace --fix 30 | stdout: | 31 | crate 'A' 32 | feature 'F0' 33 | must propagate to: 34 | B 35 | Found 1 issue and fixed 1 (all fixed). 36 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex b2b4a461c2..8256aa1ec4 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"B/F0\"\n" 37 | - cmd: lint propagate-feature -p A --feature F0 --workspace --fix 38 | stdout: | 39 | crate 'A' 40 | feature 'F0' 41 | must propagate to: 42 | B 43 | Found 1 issue and fixed 1 (all fixed). 44 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex b2b4a461c2..8256aa1ec4 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"B/F0\"\n" 45 | - cmd: lint propagate-feature -p B --feature F0 --workspace --fix 46 | - cmd: lint propagate-feature -p B --feature F1 --workspace --fix 47 | -------------------------------------------------------------------------------- /tests/integration/polkadot/issue-7537.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: paritytech/polkadot 3 | ref: 30a0be939e09c3ad9c471960091b32a5b3f9c622 4 | cases: 5 | - cmd: lint propagate-feature -p xcm-executor --feature std --fix 6 | stdout: | 7 | crate 'xcm-executor' 8 | feature 'std' 9 | must propagate to: 10 | environmental 11 | frame-benchmarking 12 | Found 2 issues and fixed 2 (all fixed). 13 | diff: "diff --git xcm/xcm-executor/Cargo.toml xcm/xcm-executor/Cargo.toml\nindex 41463bbfbd..8200b8e402 100644\n--- xcm/xcm-executor/Cargo.toml\n+++ xcm/xcm-executor/Cargo.toml\n@@ -38,0 +39,2 @@ std = [\n+\t\"environmental/std\",\n+\t\"frame-benchmarking?/std\"\n" 14 | - cmd: lint propagate-feature -p xcm-executor --feature runtime-benchmarks --fix 15 | stdout: | 16 | crate 'xcm-executor' 17 | feature 'runtime-benchmarks' 18 | must propagate to: 19 | frame-support 20 | sp-runtime 21 | Found 2 issues and fixed 2 (all fixed). 22 | diff: "diff --git xcm/xcm-executor/Cargo.toml xcm/xcm-executor/Cargo.toml\nindex 41463bbfbd..0ebbedf148 100644\n--- xcm/xcm-executor/Cargo.toml\n+++ xcm/xcm-executor/Cargo.toml\n@@ -26,0 +27,2 @@ runtime-benchmarks = [\n+\t\"frame-support/runtime-benchmarks\",\n+\t\"sp-runtime/runtime-benchmarks\"\n" 23 | - cmd: lint propagate-feature -p xcm-executor --feature runtime-benchmarks --fix --feature-enables-dep "runtime-benchmarks:frame-benchmarking" 24 | stdout: | 25 | crate 'xcm-executor' 26 | feature 'runtime-benchmarks' 27 | must propagate to: 28 | frame-support 29 | sp-runtime 30 | Found 2 issues and fixed 2 (all fixed). 31 | diff: "diff --git xcm/xcm-executor/Cargo.toml xcm/xcm-executor/Cargo.toml\nindex 41463bbfbd..0ebbedf148 100644\n--- xcm/xcm-executor/Cargo.toml\n+++ xcm/xcm-executor/Cargo.toml\n@@ -26,0 +27,2 @@ runtime-benchmarks = [\n+\t\"frame-support/runtime-benchmarks\",\n+\t\"sp-runtime/runtime-benchmarks\"\n" 32 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/fix_not_propagated_opt.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | optional: true 6 | features: 7 | F0: null 8 | - name: B 9 | features: 10 | F0: null 11 | cases: 12 | - cmd: lint propagate-feature --feature F0 --fix 13 | stdout: | 14 | crate 'A' 15 | feature 'F0' 16 | must propagate to: 17 | B 18 | Found 1 issue and fixed 1 (all fixed). 19 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex 3332f79684..3dee770310 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"B?/F0\"\n" 20 | - cmd: lint propagate-feature -p A --feature F0 --fix 21 | stdout: | 22 | crate 'A' 23 | feature 'F0' 24 | must propagate to: 25 | B 26 | Found 1 issue and fixed 1 (all fixed). 27 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex 3332f79684..3dee770310 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"B?/F0\"\n" 28 | - cmd: lint propagate-feature -p B --feature F0 --fix 29 | - cmd: lint propagate-feature -p B --feature F1 --fix 30 | - cmd: lint propagate-feature --feature F0 --workspace --fix 31 | stdout: | 32 | crate 'A' 33 | feature 'F0' 34 | must propagate to: 35 | B 36 | Found 1 issue and fixed 1 (all fixed). 37 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex 3332f79684..3dee770310 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"B?/F0\"\n" 38 | - cmd: lint propagate-feature -p A --feature F0 --workspace --fix 39 | stdout: | 40 | crate 'A' 41 | feature 'F0' 42 | must propagate to: 43 | B 44 | Found 1 issue and fixed 1 (all fixed). 45 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex 3332f79684..3dee770310 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"B?/F0\"\n" 46 | - cmd: lint propagate-feature -p B --feature F0 --workspace --fix 47 | - cmd: lint propagate-feature -p B --feature F1 --workspace --fix 48 | -------------------------------------------------------------------------------- /tests/integration/substrate/frame.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: paritytech/substrate 3 | ref: da3c1d6477c0725b2cfd0b17c85763431b855e66 4 | cases: 5 | - cmd: lint propagate-feature -p frame --feature std 6 | - cmd: lint propagate-feature -p frame --feature std --workspace 7 | - cmd: lint propagate-feature -p frame --feature runtime-benchmarks 8 | stdout: | 9 | crate 'frame' 10 | feature 'runtime-benchmarks' 11 | is required by 3 dependencies: 12 | frame-support 13 | frame-system 14 | sp-runtime 15 | Found 1 issue (run with `--fix` to fix). 16 | code: 1 17 | - cmd: lint propagate-feature -p frame --feature runtime-benchmarks --workspace 18 | stdout: | 19 | crate 'frame' 20 | feature 'runtime-benchmarks' 21 | is required by 3 dependencies: 22 | frame-support 23 | frame-system 24 | sp-runtime 25 | Found 1 issue (run with `--fix` to fix). 26 | code: 1 27 | - cmd: lint propagate-feature -p frame --feature try-runtime 28 | stdout: | 29 | crate 'frame' 30 | feature 'try-runtime' 31 | is required by 4 dependencies: 32 | frame-executive 33 | frame-support 34 | frame-system 35 | sp-runtime 36 | Found 1 issue (run with `--fix` to fix). 37 | code: 1 38 | - cmd: lint propagate-feature -p frame --feature try-runtime --workspace 39 | stdout: | 40 | crate 'frame' 41 | feature 'try-runtime' 42 | is required by 4 dependencies: 43 | frame-executive 44 | frame-support 45 | frame-system 46 | sp-runtime 47 | Found 1 issue (run with `--fix` to fix). 48 | code: 1 49 | - cmd: lint never-enables --precondition default --stays-disabled runtime-benchmarks 50 | - cmd: lint never-enables --precondition default --stays-disabled try-runtime 51 | - cmd: lint never-enables --precondition default --stays-disabled runtime-benchmarks --workspace 52 | - cmd: lint never-enables --precondition default --stays-disabled try-runtime --workspace 53 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/fix_not_propagates_re.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | rename: b 6 | features: 7 | F0: null 8 | - name: B 9 | features: 10 | F0: null 11 | cases: 12 | - cmd: lint propagate-feature --feature F0 --fix 13 | stdout: | 14 | crate 'A' 15 | feature 'F0' 16 | must propagate to: 17 | b (renamed from B) 18 | Found 1 issue and fixed 1 (all fixed). 19 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex 4476b86aba..9053bab806 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"b/F0\"\n" 20 | - cmd: lint propagate-feature -p A --feature F0 --fix 21 | stdout: | 22 | crate 'A' 23 | feature 'F0' 24 | must propagate to: 25 | b (renamed from B) 26 | Found 1 issue and fixed 1 (all fixed). 27 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex 4476b86aba..9053bab806 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"b/F0\"\n" 28 | - cmd: lint propagate-feature -p B --feature F0 --fix 29 | - cmd: lint propagate-feature -p B --feature F1 --fix 30 | - cmd: lint propagate-feature --feature F0 --workspace --fix 31 | stdout: | 32 | crate 'A' 33 | feature 'F0' 34 | must propagate to: 35 | b (renamed from B) 36 | Found 1 issue and fixed 1 (all fixed). 37 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex 4476b86aba..9053bab806 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"b/F0\"\n" 38 | - cmd: lint propagate-feature -p A --feature F0 --workspace --fix 39 | stdout: | 40 | crate 'A' 41 | feature 'F0' 42 | must propagate to: 43 | b (renamed from B) 44 | Found 1 issue and fixed 1 (all fixed). 45 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex 4476b86aba..9053bab806 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"b/F0\"\n" 46 | - cmd: lint propagate-feature -p B --feature F0 --workspace --fix 47 | - cmd: lint propagate-feature -p B --feature F1 --workspace --fix 48 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/fix_not_propagated_opt_re.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | rename: b 6 | optional: true 7 | features: 8 | F0: null 9 | - name: B 10 | features: 11 | F0: null 12 | cases: 13 | - cmd: lint propagate-feature --feature F0 --fix 14 | stdout: | 15 | crate 'A' 16 | feature 'F0' 17 | must propagate to: 18 | b (renamed from B) 19 | Found 1 issue and fixed 1 (all fixed). 20 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex e8a14dd805..ded9214270 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"b?/F0\"\n" 21 | - cmd: lint propagate-feature -p A --feature F0 --fix 22 | stdout: | 23 | crate 'A' 24 | feature 'F0' 25 | must propagate to: 26 | b (renamed from B) 27 | Found 1 issue and fixed 1 (all fixed). 28 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex e8a14dd805..ded9214270 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"b?/F0\"\n" 29 | - cmd: lint propagate-feature -p B --feature F0 --fix 30 | - cmd: lint propagate-feature -p B --feature F1 --fix 31 | - cmd: lint propagate-feature --feature F0 --workspace --fix 32 | stdout: | 33 | crate 'A' 34 | feature 'F0' 35 | must propagate to: 36 | b (renamed from B) 37 | Found 1 issue and fixed 1 (all fixed). 38 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex e8a14dd805..ded9214270 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"b?/F0\"\n" 39 | - cmd: lint propagate-feature -p A --feature F0 --workspace --fix 40 | stdout: | 41 | crate 'A' 42 | feature 'F0' 43 | must propagate to: 44 | b (renamed from B) 45 | Found 1 issue and fixed 1 (all fixed). 46 | diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex e8a14dd805..ded9214270 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"b?/F0\"\n" 47 | - cmd: lint propagate-feature -p B --feature F0 --workspace --fix 48 | - cmd: lint propagate-feature -p B --feature F1 --workspace --fix 49 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/dep_kinds.yaml: -------------------------------------------------------------------------------- 1 | comment: Test that the --dep-kinds argument works 2 | crates: 3 | - name: A 4 | deps: 5 | - name: D 6 | kind: dev 7 | - name: B 8 | kind: build 9 | - name: N 10 | features: 11 | F0: null 12 | - name: B 13 | features: 14 | F0: null 15 | - name: D 16 | features: 17 | F0: null 18 | - name: N 19 | features: 20 | F0: null 21 | cases: 22 | - cmd: lint propagate-feature --feature F0 --dep-kinds="normal:ignore,dev:ignore,build:ignore" 23 | - cmd: lint propagate-feature --feature F0 24 | stdout: | 25 | crate 'A' 26 | feature 'F0' 27 | must propagate to: 28 | B 29 | D 30 | N 31 | Found 3 issues (run with `--fix` to fix). 32 | code: 1 33 | - cmd: lint propagate-feature --feature F0 --dep-kinds="normal:ignore" 34 | stdout: | 35 | crate 'A' 36 | feature 'F0' 37 | must propagate to: 38 | B 39 | D 40 | Found 2 issues (run with `--fix` to fix). 41 | code: 1 42 | - cmd: lint propagate-feature --feature F0 --dep-kinds="dev:ignore" 43 | stdout: | 44 | crate 'A' 45 | feature 'F0' 46 | must propagate to: 47 | B 48 | N 49 | Found 2 issues (run with `--fix` to fix). 50 | code: 1 51 | - cmd: lint propagate-feature --feature F0 --dep-kinds="build:ignore" 52 | stdout: | 53 | crate 'A' 54 | feature 'F0' 55 | must propagate to: 56 | D 57 | N 58 | Found 2 issues (run with `--fix` to fix). 59 | code: 1 60 | - cmd: lint propagate-feature --feature F0 --dep-kinds="dev:ignore,build:ignore" 61 | stdout: | 62 | crate 'A' 63 | feature 'F0' 64 | must propagate to: 65 | N 66 | Found 1 issue (run with `--fix` to fix). 67 | code: 1 68 | - cmd: lint propagate-feature --feature F0 --dep-kinds="normal:ignore,build:ignore" 69 | stdout: | 70 | crate 'A' 71 | feature 'F0' 72 | must propagate to: 73 | D 74 | Found 1 issue (run with `--fix` to fix). 75 | code: 1 76 | - cmd: lint propagate-feature --feature F0 --dep-kinds="normal:ignore,dev:ignore" 77 | stdout: | 78 | crate 'A' 79 | feature 'F0' 80 | must propagate to: 81 | B 82 | Found 1 issue (run with `--fix` to fix). 83 | code: 1 84 | -------------------------------------------------------------------------------- /tests/ui/config/v1/format.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | features: 6 | F0: 7 | - - B 8 | - F0 9 | - - B 10 | - F0 11 | - name: B 12 | features: 13 | F0: null 14 | cases: 15 | - cmd: run default 16 | stdout: | 17 | Found 2 crates with unformatted features: 18 | A 19 | B 20 | Formatted 2 crates (all fixed). 21 | stderr: | 22 | [INFO] Running workflow 'default' 23 | [INFO] 1/1 f f 24 | diff: | 25 | diff --git A/Cargo.toml A/Cargo.toml 26 | index eba55cd80c..8849621452 100644 27 | --- A/Cargo.toml 28 | +++ A/Cargo.toml 29 | @@ -15,4 +15 @@ B = { version = "*", path = "../B"} 30 | -F0 = [ 31 | -"B/F0", 32 | -"B/F0", 33 | -] 34 | +F0 = [ "B/F0" ] 35 | diff --git B/Cargo.toml B/Cargo.toml 36 | index 195f3af664..4f68eee744 100644 37 | --- B/Cargo.toml 38 | +++ B/Cargo.toml 39 | @@ -14,2 +14 @@ edition = "2024" 40 | -F0 = [ 41 | -] 42 | +F0 = [] 43 | - cmd: run check 44 | stdout: | 45 | Found 2 crates with unformatted features: 46 | A 47 | B 48 | Error: Command 'f f' failed with exit code 1 49 | stderr: | 50 | [INFO] Running workflow 'check' 51 | code: 1 52 | - cmd: run fix 53 | stdout: | 54 | Found 2 crates with unformatted features: 55 | A 56 | B 57 | Formatted 2 crates (all fixed). 58 | stderr: | 59 | [INFO] Running workflow 'fix' 60 | [INFO] 1/1 f f 61 | diff: | 62 | diff --git A/Cargo.toml A/Cargo.toml 63 | index eba55cd80c..8849621452 100644 64 | --- A/Cargo.toml 65 | +++ A/Cargo.toml 66 | @@ -15,4 +15 @@ B = { version = "*", path = "../B"} 67 | -F0 = [ 68 | -"B/F0", 69 | -"B/F0", 70 | -] 71 | +F0 = [ "B/F0" ] 72 | diff --git B/Cargo.toml B/Cargo.toml 73 | index 195f3af664..4f68eee744 100644 74 | --- B/Cargo.toml 75 | +++ B/Cargo.toml 76 | @@ -14,2 +14 @@ edition = "2024" 77 | -F0 = [ 78 | -] 79 | +F0 = [] 80 | configs: 81 | - to_path: .zepter.yaml 82 | from_path: null 83 | verbatim: | 84 | version: 85 | format: 1 86 | binary: 0.12.0 87 | 88 | workflows: 89 | check: 90 | - [ 'f', 'f' ] 91 | fix: 92 | - [ $check.0, '--fix' ] 93 | default: 94 | - [ $fix.0 ] 95 | no_default_args: true 96 | -------------------------------------------------------------------------------- /src/cmd/debug.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Oliver Tale-Yazdi 3 | 4 | use super::GlobalArgs; 5 | use crate::cmd::lint::build_feature_dag; 6 | #[cfg(feature = "debugging")] 7 | use crate::{cmd::lint::CrateAndFeature, prelude::Dag}; 8 | 9 | use cargo_metadata::Metadata; 10 | use std::time::{Duration, Instant}; 11 | 12 | #[derive(Debug, clap::Parser)] 13 | pub struct DebugCmd { 14 | #[allow(missing_docs)] 15 | #[clap(flatten)] 16 | cargo_args: super::CargoArgs, 17 | 18 | #[clap(long)] 19 | no_benchmark: bool, 20 | 21 | #[clap(long)] 22 | no_root: bool, 23 | } 24 | 25 | impl DebugCmd { 26 | pub fn run(&self, g: &GlobalArgs) { 27 | g.warn_unstable(); 28 | let meta = self.cargo_args.load_metadata().expect("Loads metadata"); 29 | let dag = build_feature_dag(&meta, &meta.packages); 30 | 31 | if !self.no_root { 32 | println!("Root: {}", meta.workspace_root); 33 | } 34 | println!("Num workspace members: {}", meta.workspace_members.len()); 35 | println!("Num dependencies: {}", meta.packages.len()); 36 | println!("DAG nodes: {}, links: {}", dag.num_nodes(), dag.num_edges()); 37 | 38 | #[cfg(feature = "debugging")] 39 | self.connectivity_buckets(&dag); 40 | 41 | if !self.no_benchmark { 42 | let (took, points) = Self::measure(&meta); 43 | println!("DAG setup time: {took:.2?} (avg from {points} runs)"); 44 | } 45 | } 46 | 47 | #[cfg(feature = "debugging")] 48 | pub fn connectivity_buckets(&self, dag: &Dag) { 49 | let mut histogram = histo::Histogram::with_buckets(10); 50 | 51 | for node in dag.lhs_nodes() { 52 | histogram.add(dag.degree(node) as u64); 53 | } 54 | 55 | println!("{histogram}"); 56 | } 57 | 58 | fn measure(meta: &Metadata) -> (Duration, u32) { 59 | // Run at least: 10 times or 5 secs, whatever takes longer. 60 | let mut took = Duration::default(); 61 | let mut count = 0; 62 | 63 | while took < Duration::from_secs(1) || count < 10 { 64 | took += Self::measure_once(meta); 65 | count += 1; 66 | } 67 | 68 | assert!(took >= Duration::from_secs(1) || count >= 10); 69 | (took / count, count) 70 | } 71 | 72 | fn measure_once(meta: &Metadata) -> Duration { 73 | let start = Instant::now(); 74 | let _ = build_feature_dag(meta, &meta.packages); 75 | start.elapsed() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Oliver Tale-Yazdi 3 | 4 | #![doc = include_str!("../README.md")] 5 | 6 | pub mod autofix; 7 | pub mod cmd; 8 | pub mod config; 9 | pub mod dag; 10 | pub mod grammar; 11 | pub mod mock; 12 | mod tests; 13 | 14 | pub mod prelude { 15 | pub use super::{ 16 | dag::{Dag, Path}, 17 | CrateId, 18 | }; 19 | } 20 | 21 | /// Unique Id of a Rust crate. 22 | /// 23 | /// These come in the form of: 24 | /// ` ()` 25 | /// You can get an idea by using `cargo metadata | jq '.packages' | grep '"id"'`. 26 | pub type CrateId = String; 27 | 28 | /// Internal use only. 29 | pub mod log { 30 | pub use crate::{debug, error, info, trace, warn}; 31 | } 32 | 33 | #[macro_export] 34 | macro_rules! info { 35 | ($($arg:tt)*) => { 36 | #[cfg(feature = "logging")] 37 | { 38 | ::log::info!($($arg)*); 39 | } 40 | }; 41 | } 42 | 43 | #[macro_export] 44 | macro_rules! warn { 45 | ($($arg:tt)*) => { 46 | #[cfg(feature = "logging")] 47 | { 48 | ::log::warn!($($arg)*); 49 | } 50 | }; 51 | } 52 | 53 | #[macro_export] 54 | macro_rules! error { 55 | ($($arg:tt)*) => { 56 | #[cfg(feature = "logging")] 57 | { 58 | ::log::error!($($arg)*); 59 | } 60 | }; 61 | } 62 | 63 | #[macro_export] 64 | macro_rules! debug { 65 | ($($arg:tt)*) => { 66 | #[cfg(feature = "logging")] 67 | { 68 | ::log::debug!($($arg)*); 69 | } 70 | }; 71 | } 72 | 73 | #[macro_export] 74 | macro_rules! trace { 75 | ($($arg:tt)*) => { 76 | #[cfg(feature = "logging")] 77 | { 78 | ::log::trace!($($arg)*); 79 | } 80 | }; 81 | } 82 | 83 | /// Convert the error or a `Result` into a `String` error. 84 | pub(crate) trait ErrToStr { 85 | fn err_to_str(self) -> Result; 86 | } 87 | 88 | impl ErrToStr for Result { 89 | fn err_to_str(self) -> Result { 90 | self.map_err(|e| format!("{e}")) 91 | } 92 | } 93 | 94 | use cargo_metadata::DependencyKind; 95 | 96 | pub(crate) fn kind_to_str(kind: &DependencyKind) -> &'static str { 97 | match kind { 98 | DependencyKind::Development => "dev-dependencies", 99 | DependencyKind::Build => "build-dependencies", 100 | DependencyKind::Normal => "dependencies", 101 | _ => unreachable!(), 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zepter" 3 | version = "1.85.0" 4 | edition = "2021" 5 | authors = [ "Oliver Tale-Yazdi" ] 6 | description = "Analyze, Fix and Format features in your Rust workspace." 7 | license = "GPL-3.0-only" 8 | repository = "https://github.com/ggwpez/zepter" 9 | # Update the README if you change this: 10 | rust-version = "1.85" 11 | keywords = ["features", "linting", "formatting"] 12 | categories = ["development-tools"] 13 | 14 | [[bin]] 15 | name = "zepter" 16 | 17 | [[bench]] 18 | name = "dag" 19 | harness = false 20 | required-features = [ "benchmarking" ] 21 | 22 | [dependencies] 23 | anyhow = { version = "1.0.98", optional = true } 24 | assert_cmd = { version = "2.0.17", optional = true } 25 | camino = "1.1.10" 26 | cargo_metadata = "0.20.0" 27 | clap = { version = "4.5.41", features = ["derive", "cargo"] } 28 | colour = { version = "2.1.0", optional = true } 29 | criterion = { version = "0.6", optional = true } 30 | env_logger = { version = "0.11.8", features = [ "auto-color", "humantime" ], optional = true } 31 | histo = { version = "1.0.0", optional = true } 32 | itertools = "0.14.0" 33 | log = { version = "0.4.27", optional = true } 34 | regex = "1.11.1" 35 | semver = "1" 36 | serde = "1.0.219" 37 | serde_json = { version = "1.0.141", optional = true } 38 | serde_yaml_ng = "0.10.0" 39 | tempfile = { version = "3.20.0", optional = true } 40 | toml_edit = "0.23.2" 41 | tracing = { version = "0.1.41", optional = true } 42 | 43 | [dev-dependencies] 44 | glob = "0.3.2" 45 | lazy_static = "1.5.0" 46 | pretty_assertions = "1.4.1" 47 | rand = "0.9.2" 48 | rstest = "0.25.0" 49 | serde = "1.0.219" 50 | zepter = { path = ".", features = ["testing"] } 51 | 52 | [features] 53 | default = [ "logging" ] 54 | logging = [ "dep:env_logger", "dep:log" ] 55 | benchmarking = [ "dep:criterion", "dep:serde_json" ] 56 | testing = [ "dep:anyhow", "dep:assert_cmd", "dep:colour", "dep:tempfile", "dep:serde_json" ] 57 | debugging = [ "dep:histo" ] 58 | 59 | [profile.dev] 60 | opt-level = 3 61 | 62 | [profile.release] 63 | opt-level = 3 64 | debug = true 65 | 66 | # Improves speed of the DAG logic by 4-20%. Normally `cargo metadata` is magnitudes slower, so we 67 | # dont sacrifice (human) compile time for this negligible speedup. 68 | [profile.optimized] 69 | inherits = "release" 70 | lto = true 71 | codegen-units = 1 72 | 73 | # The profile that 'dist' will build with 74 | [profile.dist] 75 | inherits = "optimized" 76 | -------------------------------------------------------------------------------- /presets/polkadot.yaml: -------------------------------------------------------------------------------- 1 | # Configuration for the Zepter CLI to ensure correct feature configuration in the Rust workspace. 2 | # 3 | 4 | version: 5 | # File format for parsing it: 6 | format: 1 7 | # Minimum version of the binary that is expected to work. This is just for printing a nice error 8 | # message when someone tries to use an older version. 9 | binary: 0.13.2 10 | 11 | # The examples in this file assume crate `A` to have a dependency on crate `B`. 12 | workflows: 13 | # Check that everything is good without modifying anything: 14 | check: 15 | - [ 16 | 'lint', 17 | # Check that `A` activates the features of `B`. 18 | 'propagate-feature', 19 | # These are the features to check: 20 | '--features=try-runtime,runtime-benchmarks,std', 21 | # Do not try to add a new section into `[features]` of `A` only because `B` expose that feature. There are edge-cases where this is still needed, but we can add them manually. 22 | '--left-side-feature-missing=ignore', 23 | # Ignore the case that `A` it outside of the workspace. Otherwise it will report errors in external dependencies that we have no influence on. 24 | '--left-side-outside-workspace=ignore', 25 | # Some features imply that they activate a specific dependency as non-optional. Otherwise the default behaviour with a `?` is used. 26 | '--feature-enables-dep=try-runtime:frame-try-runtime,runtime-benchmarks:frame-benchmarking', 27 | # Show the paths of failed crates to have them clickable in the terminal:  28 | '--show-path', 29 | # Aux 30 | '--offline', 31 | '--locked', 32 | '--quiet', 33 | ] 34 | # Format the features into canonical format: 35 | - ['format', 'features', '--offline', '--locked', '--quiet'] 36 | # Same as `check`, but actually fix the issues instead of just reporting them: 37 | default: 38 | - [ $check.0, '--fix' ] 39 | - [ $check.1, '--fix' ] 40 | 41 | # Will be displayed when any workflow fails: 42 | help: 43 | text: | 44 | Polkadot-SDK uses the Zepter CLI to detect abnormalities in the feature configuration. 45 | It looks like one more more checks failed; please check the console output. You can try to automatically address them by running `zepter`. 46 | Otherwise please ask directly in the Merge Request, GitHub Discussions or on Matrix Chat, thank you. 47 | links: 48 | - "https://github.com/paritytech/polkadot-sdk/issues/1831" 49 | - "https://github.com/ggwpez/zepter" 50 | -------------------------------------------------------------------------------- /benches/dag.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Oliver Tale-Yazdi 3 | 4 | use criterion::{criterion_group, criterion_main, Criterion}; 5 | use rand::{Rng, SeedableRng}; 6 | use std::hint::black_box; 7 | use zepter::{cmd::lint::build_feature_dag, prelude::*}; 8 | 9 | fn build_dag(nodes: usize, edges: usize) -> Dag { 10 | let mut rng = rand::rngs::StdRng::seed_from_u64(42); 11 | 12 | let mut dag = Dag::default(); 13 | for i in 0..nodes { 14 | dag.add_node(i); 15 | } 16 | for _ in 0..edges { 17 | let from = rng.random_range(0..nodes); 18 | let to = rng.random_range(0..nodes); 19 | dag.add_edge(from, to); 20 | } 21 | dag 22 | } 23 | 24 | fn any_path(dag: &Dag) { 25 | dag.any_path(&0, &1); 26 | } 27 | 28 | fn dag(c: &mut Criterion) { 29 | let dag = build_dag(1000, 1000); 30 | c.bench_function("DAG 1k/1k", |b| { 31 | b.iter(|| { 32 | any_path(&dag); 33 | black_box(()); 34 | }); 35 | }); 36 | let dag = build_dag(1000, 5000); 37 | c.bench_function("DAG 1k/5k", |b| { 38 | b.iter(|| { 39 | any_path(&dag); 40 | black_box(()); 41 | }); 42 | }); 43 | let dag = build_dag(10000, 1000); 44 | c.bench_function("DAG 10k/1k", |b| { 45 | b.iter(|| { 46 | any_path(&dag); 47 | black_box(()); 48 | }); 49 | }); 50 | let dag = build_dag(10000, 50000); 51 | c.bench_function("DAG 10k/50k", |b| { 52 | b.iter(|| { 53 | any_path(&dag); 54 | black_box(()); 55 | }); 56 | }); 57 | } 58 | 59 | fn polkadot_sdk(c: &mut Criterion) { 60 | let path = std::env::var("META_JSON_PATH").unwrap_or("meta.json".into()); 61 | let path = std::fs::canonicalize(path).unwrap(); 62 | let file = std::fs::read_to_string(path).unwrap(); 63 | let meta = serde_json::from_str::(&file).unwrap(); 64 | 65 | let pkgs = &meta.packages; 66 | let dag = build_feature_dag(&meta, pkgs); 67 | 68 | c.bench_function("Polkadot-SDK / DAG / setup", |b| { 69 | b.iter(|| { 70 | let dag = build_feature_dag(&meta, pkgs); 71 | black_box(dag) 72 | }); 73 | }); 74 | 75 | let from = dag.lhs_iter().find(|c| c.0.starts_with("kitchensink-runtime ")).unwrap(); 76 | let to = dag.rhs_iter().find(|c| c.0.starts_with("sp-io ")).unwrap(); 77 | assert!(dag.lhs_contains(from), "LHS:\n{:?}", dag.lhs_nodes().collect::>()); 78 | assert!(dag.rhs_contains(to)); 79 | 80 | c.bench_function("Polkadot-SDK / DAG / reachability: false", |b| { 81 | b.iter(|| { 82 | let p = dag.any_path(from, to); 83 | black_box(p) 84 | }); 85 | }); 86 | } 87 | 88 | criterion_group!(benches, polkadot_sdk, dag); 89 | criterion_main!(benches); 90 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/diamond.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | - C 6 | features: 7 | F0: null 8 | - name: B 9 | deps: 10 | - D 11 | - name: C 12 | deps: 13 | - D 14 | features: 15 | F0: null 16 | - name: D 17 | features: 18 | F0: null 19 | cases: 20 | - cmd: lint propagate-feature --feature F1 21 | - cmd: lint propagate-feature --feature F0 --fix-hint=off 22 | stdout: | 23 | crate 'A' 24 | feature 'F0' 25 | must propagate to: 26 | C 27 | crate 'B' 28 | feature 'F0' 29 | is required by 1 dependency: 30 | D 31 | crate 'C' 32 | feature 'F0' 33 | must propagate to: 34 | D 35 | Found 3 issues. 36 | code: 1 37 | - cmd: lint propagate-feature -p A --feature F0 --fix-hint=on 38 | stdout: | 39 | crate 'A' 40 | feature 'F0' 41 | must propagate to: 42 | C 43 | Found 1 issue (run with `--fix` to fix). 44 | code: 1 45 | - cmd: lint propagate-feature -p B --feature F0 46 | stdout: | 47 | crate 'B' 48 | feature 'F0' 49 | is required by 1 dependency: 50 | D 51 | Found 1 issue (run with `--fix` to fix). 52 | code: 1 53 | - cmd: lint propagate-feature -p C --feature F0 54 | stdout: | 55 | crate 'C' 56 | feature 'F0' 57 | must propagate to: 58 | D 59 | Found 1 issue (run with `--fix` to fix). 60 | code: 1 61 | - cmd: lint propagate-feature -p D --feature F0 62 | - cmd: lint propagate-feature --feature F1 --workspace 63 | - cmd: lint propagate-feature --feature F0 --workspace 64 | stdout: | 65 | crate 'A' 66 | feature 'F0' 67 | must propagate to: 68 | C 69 | crate 'B' 70 | feature 'F0' 71 | is required by 1 dependency: 72 | D 73 | crate 'C' 74 | feature 'F0' 75 | must propagate to: 76 | D 77 | Found 3 issues (run with `--fix` to fix). 78 | code: 1 79 | - cmd: lint propagate-feature -p A --feature F0 --workspace 80 | stdout: | 81 | crate 'A' 82 | feature 'F0' 83 | must propagate to: 84 | C 85 | Found 1 issue (run with `--fix` to fix). 86 | code: 1 87 | - cmd: lint propagate-feature -p B --feature F0 --workspace 88 | stdout: | 89 | crate 'B' 90 | feature 'F0' 91 | is required by 1 dependency: 92 | D 93 | Found 1 issue (run with `--fix` to fix). 94 | code: 1 95 | - cmd: lint propagate-feature -p C --feature F0 --workspace 96 | stdout: | 97 | crate 'C' 98 | feature 'F0' 99 | must propagate to: 100 | D 101 | Found 1 issue (run with `--fix` to fix). 102 | code: 1 103 | - cmd: lint propagate-feature -p D --feature F0 --workspace 104 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(all())'] 2 | rustflags = [ 3 | # BEGIN - Embark standard lints v6 for Rust 1.55+ 4 | # do not change or add/remove here, but one can add exceptions after this section 5 | # for more info see: 6 | "-Dunsafe_code", 7 | "-Wclippy::all", 8 | "-Wclippy::await_holding_lock", 9 | "-Wclippy::char_lit_as_u8", 10 | "-Wclippy::checked_conversions", 11 | "-Wclippy::dbg_macro", 12 | "-Wclippy::debug_assert_with_mut_call", 13 | "-Wclippy::doc_markdown", 14 | "-Wclippy::empty_enum", 15 | "-Wclippy::enum_glob_use", 16 | "-Wclippy::exit", 17 | "-Wclippy::expl_impl_clone_on_copy", 18 | "-Wclippy::explicit_deref_methods", 19 | "-Wclippy::explicit_into_iter_loop", 20 | "-Wclippy::fallible_impl_from", 21 | "-Wclippy::filter_map_next", 22 | "-Wclippy::flat_map_option", 23 | "-Wclippy::float_cmp_const", 24 | "-Wclippy::fn_params_excessive_bools", 25 | "-Wclippy::from_iter_instead_of_collect", 26 | "-Wclippy::if_let_mutex", 27 | "-Wclippy::implicit_clone", 28 | "-Wclippy::imprecise_flops", 29 | "-Wclippy::inefficient_to_string", 30 | "-Wclippy::invalid_upcast_comparisons", 31 | "-Wclippy::large_digit_groups", 32 | "-Wclippy::large_stack_arrays", 33 | "-Wclippy::large_types_passed_by_value", 34 | "-Wclippy::let_unit_value", 35 | "-Wclippy::linkedlist", 36 | "-Wclippy::lossy_float_literal", 37 | "-Wclippy::macro_use_imports", 38 | "-Wclippy::manual_ok_or", 39 | "-Wclippy::map_err_ignore", 40 | "-Wclippy::map_flatten", 41 | "-Wclippy::map_unwrap_or", 42 | "-Wclippy::match_same_arms", 43 | "-Wclippy::match_wild_err_arm", 44 | "-Wclippy::match_wildcard_for_single_variants", 45 | "-Wclippy::mem_forget", 46 | "-Wclippy::missing_enforced_import_renames", 47 | "-Wclippy::mut_mut", 48 | "-Wclippy::mutex_integer", 49 | "-Wclippy::needless_borrow", 50 | "-Wclippy::needless_continue", 51 | "-Wclippy::needless_for_each", 52 | "-Wclippy::option_option", 53 | "-Wclippy::path_buf_push_overwrite", 54 | "-Wclippy::ptr_as_ptr", 55 | "-Wclippy::rc_mutex", 56 | "-Wclippy::ref_option_ref", 57 | "-Wclippy::rest_pat_in_fully_bound_structs", 58 | "-Wclippy::same_functions_in_if_condition", 59 | "-Wclippy::semicolon_if_nothing_returned", 60 | "-Wclippy::single_match_else", 61 | "-Wclippy::string_add_assign", 62 | "-Wclippy::string_add", 63 | "-Wclippy::string_lit_as_bytes", 64 | "-Wclippy::string_to_string", 65 | "-Wclippy::todo", 66 | "-Wclippy::trait_duplication_in_bounds", 67 | "-Wclippy::unimplemented", 68 | "-Wclippy::unnested_or_patterns", 69 | "-Wclippy::unused_self", 70 | "-Wclippy::useless_transmute", 71 | "-Wclippy::verbose_file_reads", 72 | "-Wclippy::zero_sized_map_values", 73 | "-Wfuture_incompatible", 74 | "-Wnonstandard_style", 75 | "-Wrust_2018_idioms", 76 | # END - Embark standard lints v6 for Rust 1.55+ 77 | ] 78 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/diamond_re.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: B 5 | rename: b 6 | - name: C 7 | rename: c 8 | features: 9 | F0: null 10 | - name: B 11 | deps: 12 | - name: D 13 | rename: d 14 | - name: C 15 | deps: 16 | - name: D 17 | rename: dd 18 | features: 19 | F0: null 20 | - name: D 21 | features: 22 | F0: null 23 | cases: 24 | - cmd: lint propagate-feature --feature F1 25 | - cmd: lint propagate-feature --feature F0 26 | stdout: | 27 | crate 'A' 28 | feature 'F0' 29 | must propagate to: 30 | c (renamed from C) 31 | crate 'B' 32 | feature 'F0' 33 | is required by 1 dependency: 34 | d (renamed from D) 35 | crate 'C' 36 | feature 'F0' 37 | must propagate to: 38 | dd (renamed from D) 39 | Found 3 issues (run with `--fix` to fix). 40 | code: 1 41 | - cmd: lint propagate-feature -p A --feature F0 42 | stdout: | 43 | crate 'A' 44 | feature 'F0' 45 | must propagate to: 46 | c (renamed from C) 47 | Found 1 issue (run with `--fix` to fix). 48 | code: 1 49 | - cmd: lint propagate-feature -p B --feature F0 50 | stdout: | 51 | crate 'B' 52 | feature 'F0' 53 | is required by 1 dependency: 54 | d (renamed from D) 55 | Found 1 issue (run with `--fix` to fix). 56 | code: 1 57 | - cmd: lint propagate-feature -p C --feature F0 58 | stdout: | 59 | crate 'C' 60 | feature 'F0' 61 | must propagate to: 62 | dd (renamed from D) 63 | Found 1 issue (run with `--fix` to fix). 64 | code: 1 65 | - cmd: lint propagate-feature -p D --feature F0 66 | - cmd: lint propagate-feature --feature F1 --workspace 67 | - cmd: lint propagate-feature --feature F0 --workspace 68 | stdout: | 69 | crate 'A' 70 | feature 'F0' 71 | must propagate to: 72 | c (renamed from C) 73 | crate 'B' 74 | feature 'F0' 75 | is required by 1 dependency: 76 | d (renamed from D) 77 | crate 'C' 78 | feature 'F0' 79 | must propagate to: 80 | dd (renamed from D) 81 | Found 3 issues (run with `--fix` to fix). 82 | code: 1 83 | - cmd: lint propagate-feature -p A --feature F0 --workspace 84 | stdout: | 85 | crate 'A' 86 | feature 'F0' 87 | must propagate to: 88 | c (renamed from C) 89 | Found 1 issue (run with `--fix` to fix). 90 | code: 1 91 | - cmd: lint propagate-feature -p B --feature F0 --workspace 92 | stdout: | 93 | crate 'B' 94 | feature 'F0' 95 | is required by 1 dependency: 96 | d (renamed from D) 97 | Found 1 issue (run with `--fix` to fix). 98 | code: 1 99 | - cmd: lint propagate-feature -p C --feature F0 --workspace 100 | stdout: | 101 | crate 'C' 102 | feature 'F0' 103 | must propagate to: 104 | dd (renamed from D) 105 | Found 1 issue (run with `--fix` to fix). 106 | code: 1 107 | - cmd: lint propagate-feature -p D --feature F0 --workspace 108 | -------------------------------------------------------------------------------- /tests/ui/config/v1/finds_all.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | cases: 4 | - cmd: '' 5 | stdout: | 6 | zepter 1.85.0 7 | stderr: | 8 | [INFO] Running workflow 'default' 9 | [INFO] 1/1 --version 10 | config: 11 | to_path: .zepter.yaml 12 | from_path: null 13 | verbatim: | 14 | version: 15 | format: 1 16 | binary: 0.12.0 17 | workflows: 18 | default: 19 | - [ '--version' ] 20 | - cmd: '' 21 | stdout: | 22 | zepter 1.85.0 23 | stderr: | 24 | [INFO] Running workflow 'default' 25 | [INFO] 1/1 --version 26 | config: 27 | to_path: zepter.yaml 28 | from_path: null 29 | verbatim: | 30 | version: 31 | format: 1 32 | binary: 0.12.0 33 | workflows: 34 | default: 35 | - [ '--version' ] 36 | - cmd: '' 37 | stdout: | 38 | zepter 1.85.0 39 | stderr: | 40 | [INFO] Running workflow 'default' 41 | [INFO] 1/1 --version 42 | config: 43 | to_path: .cargo/zepter.yaml 44 | from_path: null 45 | verbatim: | 46 | version: 47 | format: 1 48 | binary: 0.12.0 49 | workflows: 50 | default: 51 | - [ '--version' ] 52 | - cmd: run default 53 | stdout: | 54 | zepter 1.85.0 55 | stderr: | 56 | [INFO] Running workflow 'default' 57 | [INFO] 1/1 --version 58 | config: 59 | to_path: .cargo/.zepter.yaml 60 | from_path: null 61 | verbatim: | 62 | version: 63 | format: 1 64 | binary: 0.12.0 65 | workflows: 66 | default: 67 | - [ '--version' ] 68 | - cmd: run default 69 | stdout: | 70 | zepter 1.85.0 71 | stderr: | 72 | [INFO] Running workflow 'default' 73 | [INFO] 1/1 --version 74 | config: 75 | to_path: .config/.zepter.yaml 76 | from_path: null 77 | verbatim: | 78 | version: 79 | format: 1 80 | binary: 0.12.0 81 | workflows: 82 | default: 83 | - [ '--version' ] 84 | - cmd: run default 85 | stdout: | 86 | zepter 1.85.0 87 | stderr: | 88 | [INFO] Running workflow 'default' 89 | [INFO] 1/1 --version 90 | config: 91 | to_path: .config/.zepter.yaml 92 | from_path: null 93 | verbatim: | 94 | version: 95 | format: 1 96 | binary: 0.12.0 97 | workflows: 98 | default: 99 | - [ '--version' ] 100 | - cmd: run default --config .cargo/polkadot.yaml 101 | stdout: | 102 | zepter 1.85.0 103 | stderr: | 104 | [INFO] Running workflow 'default' 105 | [INFO] 1/1 --version 106 | config: 107 | to_path: .cargo/polkadot.yaml 108 | from_path: null 109 | verbatim: | 110 | version: 111 | format: 1 112 | binary: 0.12.0 113 | workflows: 114 | default: 115 | - [ '--version' ] 116 | - cmd: run default -c .cargo/polkadot.yaml 117 | stdout: | 118 | zepter 1.85.0 119 | stderr: | 120 | [INFO] Running workflow 'default' 121 | [INFO] 1/1 --version 122 | config: 123 | to_path: .cargo/polkadot.yaml 124 | from_path: null 125 | verbatim: | 126 | version: 127 | format: 1 128 | binary: 0.12.0 129 | workflows: 130 | default: 131 | - [ '--version' ] 132 | no_default_args: true 133 | -------------------------------------------------------------------------------- /tests/integration/substrate/frame-pallets.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: paritytech/substrate 3 | ref: 1a1c32a1cc7c56e88485f146977ba0fb32026c0a 4 | cases: 5 | - cmd: lint propagate-feature -p frame-support --feature runtime-benchmarks -p pallet-referenda --fix --feature-enables-dep runtime-benchmarks:frame-benchmarking 6 | stdout: | 7 | crate 'pallet-referenda' 8 | feature 'runtime-benchmarks' 9 | must propagate to: 10 | frame-benchmarking 11 | pallet-balances 12 | pallet-preimage 13 | pallet-scheduler 14 | Found 4 issues and fixed 4 (all fixed). 15 | diff: "diff --git frame/referenda/Cargo.toml frame/referenda/Cargo.toml\nindex a89f641e81..7833ed444a 100644\n--- frame/referenda/Cargo.toml\n+++ frame/referenda/Cargo.toml\n@@ -58,0 +59,4 @@ runtime-benchmarks = [\n+\t\"frame-benchmarking/runtime-benchmarks\",\n+\t\"pallet-balances/runtime-benchmarks\",\n+\t\"pallet-preimage/runtime-benchmarks\",\n+\t\"pallet-scheduler/runtime-benchmarks\"\n" 16 | - cmd: lint propagate-feature -p frame-support --feature runtime-benchmarks -p pallet-referenda --fix --feature-enables-dep "runtime-benchmarks:frame-benchmarking" 17 | stdout: | 18 | crate 'pallet-referenda' 19 | feature 'runtime-benchmarks' 20 | must propagate to: 21 | frame-benchmarking 22 | pallet-balances 23 | pallet-preimage 24 | pallet-scheduler 25 | Found 4 issues and fixed 4 (all fixed). 26 | diff: "diff --git frame/referenda/Cargo.toml frame/referenda/Cargo.toml\nindex a89f641e81..7833ed444a 100644\n--- frame/referenda/Cargo.toml\n+++ frame/referenda/Cargo.toml\n@@ -58,0 +59,4 @@ runtime-benchmarks = [\n+\t\"frame-benchmarking/runtime-benchmarks\",\n+\t\"pallet-balances/runtime-benchmarks\",\n+\t\"pallet-preimage/runtime-benchmarks\",\n+\t\"pallet-scheduler/runtime-benchmarks\"\n" 27 | - cmd: lint propagate-feature -p frame-support --feature runtime-benchmarks -p pallet-referenda --fix 28 | stdout: | 29 | crate 'pallet-referenda' 30 | feature 'runtime-benchmarks' 31 | must propagate to: 32 | frame-benchmarking 33 | pallet-balances 34 | pallet-preimage 35 | pallet-scheduler 36 | Found 4 issues and fixed 4 (all fixed). 37 | diff: "diff --git frame/referenda/Cargo.toml frame/referenda/Cargo.toml\nindex a89f641e81..59f5e84b61 100644\n--- frame/referenda/Cargo.toml\n+++ frame/referenda/Cargo.toml\n@@ -58,0 +59,4 @@ runtime-benchmarks = [\n+\t\"frame-benchmarking?/runtime-benchmarks\",\n+\t\"pallet-balances/runtime-benchmarks\",\n+\t\"pallet-preimage/runtime-benchmarks\",\n+\t\"pallet-scheduler/runtime-benchmarks\"\n" 38 | - cmd: lint propagate-feature --feature try-runtime -p pallet-referenda --fix 39 | stdout: | 40 | crate 'pallet-referenda' 41 | feature 'try-runtime' 42 | must propagate to: 43 | frame-system 44 | pallet-balances 45 | pallet-preimage 46 | pallet-scheduler 47 | sp-runtime 48 | Found 5 issues and fixed 5 (all fixed). 49 | diff: "diff --git frame/referenda/Cargo.toml frame/referenda/Cargo.toml\nindex a89f641e81..405baea477 100644\n--- frame/referenda/Cargo.toml\n+++ frame/referenda/Cargo.toml\n@@ -60 +60,8 @@ runtime-benchmarks = [\n-try-runtime = [\"frame-support/try-runtime\"]\n+try-runtime = [\n+\t\"frame-support/try-runtime\",\n+\t\"frame-system/try-runtime\",\n+\t\"pallet-balances/try-runtime\",\n+\t\"pallet-preimage/try-runtime\",\n+\t\"pallet-scheduler/try-runtime\",\n+\t\"sp-runtime/try-runtime\"\n+]\n" 50 | -------------------------------------------------------------------------------- /tests/ui/fmt/help.yaml: -------------------------------------------------------------------------------- 1 | crates: [] 2 | cases: 3 | - cmd: format --help 4 | stdout: "Format the features in your manifest files\n\nUsage: zepter format [OPTIONS] \n\nCommands:\n features Format the content of each feature in the crate manifest\n help Print this message or the help of the given subcommand(s)\n\nOptions:\n -q, --quiet\n Only print errors. Supersedes `--log`\n\n --log \n Log level to use\n \n [default: info]\n\n --color\n Use ANSI terminal colors\n\n --exit-code-zero\n Try to exit with code zero if the intended check failed.\n \n Will still return != 0 in case of an actual error (eg. failed to find some file) or a panic\n (aka software bug).\n\n --fix-hint \n Dont print any hints on how to fix the error.\n \n This is mostly used internally when dispatching, workflows since they come with their\n hints.\n\n Possible values:\n - on: Prints some hint that is (hopefully) helpful\n - off: Prints no hint at all\n \n [default: on]\n\n -h, --help\n Print help (see a summary with '-h')\n" 5 | - cmd: format features --help 6 | stdout: "Format the content of each feature in the crate manifest\n\nUsage: zepter format features [OPTIONS]\n\nOptions:\n --manifest-path \n Cargo manifest path or directory.\n \n For directories it appends a `Cargo.toml`.\n\n --workspace\n Whether to only consider workspace crates\n\n --offline\n Whether to use offline mode\n\n --locked\n Whether to use all the locked dependencies from the `Cargo.lock`.\n \n Otherwise it may update some dependencies. For CI usage its a good idea to use it.\n\n --all-features\n \n\n --no-workspace\n Include dependencies in the formatting check.\n \n They will not be modified, unless their path is included in `--modify-paths`.\n\n --modify-paths \n Paths that are allowed to be modified by the formatter\n\n -q, --quiet\n Only print errors. Supersedes `--log`\n\n -c, --check\n DEPRECATED AND IGNORED\n\n --log \n Log level to use\n \n [default: info]\n\n --color\n Use ANSI terminal colors\n\n -f, --fix\n Fix the formatting errors automatically\n\n --exit-code-zero\n Try to exit with code zero if the intended check failed.\n \n Will still return != 0 in case of an actual error (eg. failed to find some file) or a panic\n (aka software bug).\n\n --line-width \n The maximal length of a line for a feature\n \n [default: 80]\n\n --fix-hint \n Dont print any hints on how to fix the error.\n \n This is mostly used internally when dispatching, workflows since they come with their\n hints.\n\n Possible values:\n - on: Prints some hint that is (hopefully) helpful\n - off: Prints no hint at all\n \n [default: on]\n\n --mode-per-feature \n Set the formatting mode for a specific feature.\n \n Can be specified multiple times. Example:\n `--mode-per-feature default:sort,default:canonicalize`\n\n --ignore-feature \n Ignore a specific feature across all crates.\n \n This is equivalent to `--mode-per-feature FEATURE:none`.\n\n --print-paths\n Also print the paths of the offending Cargo.toml files\n\n -h, --help\n Print help (see a summary with '-h')\n" 7 | -------------------------------------------------------------------------------- /src/config/semver.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Oliver Tale-Yazdi 3 | 4 | //! Very simple semver implementation. Only supports `major.minor.patch`. 5 | //! 6 | //! Used to lock in config format and binary version. 7 | 8 | use serde::{de, Deserialize, Deserializer}; 9 | use std::fmt::{self, Display, Formatter}; 10 | 11 | /// A semantic version. 12 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 13 | pub struct Semver { 14 | /// The major version. 15 | pub major: u8, 16 | /// The minor version. 17 | pub minor: u8, 18 | /// The patch version. 19 | pub patch: u8, 20 | } 21 | 22 | impl TryFrom<&str> for Semver { 23 | type Error = (); 24 | 25 | #[allow(clippy::map_err_ignore)] 26 | fn try_from(s: &str) -> Result { 27 | let mut parts = s.split('.'); 28 | let major = parts.next().ok_or(())?.parse().map_err(|_| ())?; 29 | let minor = parts.next().unwrap_or("0").parse().map_err(|_| ())?; 30 | let patch = parts.next().unwrap_or("0").parse().map_err(|_| ())?; 31 | 32 | Ok(Self { major, minor, patch }) 33 | } 34 | } 35 | 36 | impl From<(u8, u8, u8)> for Semver { 37 | fn from((major, minor, patch): (u8, u8, u8)) -> Self { 38 | Self { major, minor, patch } 39 | } 40 | } 41 | 42 | impl Display for Semver { 43 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 44 | write!(f, "{}.{}.{}", self.major, self.minor, self.patch) 45 | } 46 | } 47 | 48 | impl Semver { 49 | #[allow(clippy::map_err_ignore)] 50 | pub fn from_serde<'de, D>(deserializer: D) -> Result 51 | where 52 | D: Deserializer<'de>, 53 | { 54 | let s = String::deserialize(deserializer)?; 55 | Self::try_from(s.as_str()).map_err(|_| de::Error::custom("Invalid semver")) 56 | } 57 | 58 | /// Whether `self` is newer or equal to `other`. 59 | /// 60 | /// "Newer" in this case means compatible in the semver sense. 61 | pub fn is_newer_or_equal(&self, other: &Self) -> bool { 62 | self.major > other.major || 63 | (self.major == other.major && 64 | (self.minor > other.minor || 65 | (self.minor == other.minor && self.patch >= other.patch))) 66 | } 67 | } 68 | 69 | #[cfg(test)] 70 | mod test { 71 | use super::*; 72 | 73 | #[test] 74 | fn parser_semver_works() { 75 | assert_eq!(Semver::try_from("1").unwrap(), Semver::from((1, 0, 0))); 76 | 77 | assert_eq!(Semver::try_from("1.2").unwrap(), Semver::from((1, 2, 0))); 78 | 79 | assert_eq!(Semver::try_from("1.2.3").unwrap(), Semver::from((1, 2, 3))); 80 | } 81 | 82 | #[test] 83 | fn semver_display_works() { 84 | assert_eq!(Semver::from((1, 2, 3)).to_string(), "1.2.3"); 85 | } 86 | 87 | #[test] 88 | fn semver_from_serde_works() { 89 | #[derive(Deserialize)] 90 | struct Embedding { 91 | #[serde(deserialize_with = "Semver::from_serde")] 92 | version: Semver, 93 | } 94 | 95 | let s = r#" 96 | { "version": "1.2.3" } 97 | "#; 98 | 99 | let embedding = serde_json::from_str::(s).unwrap(); 100 | assert_eq!(embedding.version, Semver::from((1, 2, 3))); 101 | } 102 | 103 | #[test] 104 | fn semver_is_newer_or_equal_works() { 105 | assert!(Semver::from((1, 2, 3)).is_newer_or_equal(&Semver::from((1, 2, 3)))); 106 | 107 | assert!(Semver::from((1, 2, 3)).is_newer_or_equal(&Semver::from((1, 2, 2)))); 108 | assert!(Semver::from((1, 2, 3)).is_newer_or_equal(&Semver::from((1, 1, 3)))); 109 | assert!(Semver::from((1, 2, 3)).is_newer_or_equal(&Semver::from((0, 2, 3)))); 110 | 111 | assert!(!Semver::from((1, 2, 3)).is_newer_or_equal(&Semver::from((1, 2, 4)))); 112 | assert!(!Semver::from((1, 2, 3)).is_newer_or_equal(&Semver::from((1, 3, 3)))); 113 | assert!(!Semver::from((1, 2, 3)).is_newer_or_equal(&Semver::from((2, 2, 3)))); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/mock/git.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Oliver Tale-Yazdi 3 | 4 | //! Helpers for cloning and checking out git repositories. 5 | 6 | use assert_cmd::Command; 7 | use std::path::{Path, PathBuf}; 8 | 9 | /// Create a mocked git repository. 10 | pub fn git_init(dir: &Path) -> Result<(), anyhow::Error> { 11 | let mut cmd = Command::new("git"); 12 | cmd.current_dir(dir); 13 | cmd.arg("init"); 14 | cmd.arg("--quiet"); 15 | cmd.assert().try_success()?; 16 | 17 | // Do an init commit 18 | let mut cmd = Command::new("git"); 19 | cmd.current_dir(dir); 20 | cmd.arg("add"); 21 | cmd.arg("--all"); 22 | cmd.assert().try_success()?; 23 | 24 | // git config user.email "you@example.com" 25 | // git config user.name "Your Name" 26 | let mut cmd = Command::new("git"); 27 | cmd.current_dir(dir); 28 | cmd.arg("config"); 29 | cmd.arg("user.email"); 30 | cmd.arg("you@example.com"); 31 | cmd.assert().try_success()?; 32 | 33 | let mut cmd = Command::new("git"); 34 | cmd.current_dir(dir); 35 | cmd.arg("config"); 36 | cmd.arg("user.name"); 37 | cmd.arg("Your Name"); 38 | cmd.assert().try_success()?; 39 | 40 | let mut cmd = Command::new("git"); 41 | cmd.current_dir(dir); 42 | cmd.arg("commit"); 43 | cmd.arg("--message"); 44 | cmd.arg("init"); 45 | cmd.arg("--author"); 46 | cmd.arg("test "); 47 | cmd.arg("--no-gpg-sign"); 48 | cmd.arg("--quiet"); 49 | cmd.assert().try_success()?; 50 | 51 | Ok(()) 52 | } 53 | 54 | pub fn git_diff(dir: &Path) -> Result { 55 | let mut cmd = Command::new("git"); 56 | cmd.current_dir(dir); 57 | cmd.arg("diff"); 58 | cmd.arg("--abbrev=10"); // Pick a deterministic commit hash len. 59 | cmd.arg("--patch"); 60 | cmd.arg("--no-color"); 61 | cmd.arg("--minimal"); 62 | cmd.arg("--no-prefix"); 63 | cmd.arg("--unified=0"); 64 | let output = cmd.output()?; 65 | 66 | Ok(String::from_utf8_lossy(&output.stdout).into()) 67 | } 68 | 69 | pub fn git_reset(dir: &Path) -> Result<(), anyhow::Error> { 70 | let mut cmd = Command::new("git"); 71 | cmd.current_dir(dir); 72 | cmd.arg("checkout"); 73 | cmd.arg("--"); 74 | cmd.arg("."); 75 | cmd.assert().try_success()?; 76 | 77 | let mut cmd = Command::new("git"); 78 | cmd.current_dir(dir); 79 | cmd.arg("reset"); 80 | cmd.arg("--hard"); 81 | cmd.arg("--quiet"); 82 | cmd.assert().try_success()?; 83 | 84 | Ok(()) 85 | } 86 | 87 | pub fn clone_repo(repo: &str, rev: &str) -> Result { 88 | let dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".into()); 89 | let repos_dir = std::path::Path::new(&dir).join("test-repos"); 90 | let dir = repos_dir.join(repo); 91 | colour::white_ln!("Checking out '{repo}' at '{}'", &rev[..10]); 92 | 93 | // Check if the repo is already cloned 94 | if Path::new(&dir).exists() { 95 | git_reset(&dir)?; 96 | } else { 97 | std::fs::create_dir_all(&dir)?; 98 | 99 | let mut cmd = Command::new("git"); 100 | cmd.current_dir(&dir); 101 | cmd.arg("init"); 102 | cmd.arg("--quiet"); 103 | cmd.assert().try_success()?; 104 | 105 | // add remote 106 | let mut cmd = Command::new("git"); 107 | cmd.current_dir(&dir); 108 | cmd.arg("remote"); 109 | cmd.arg("add"); 110 | cmd.arg("origin"); 111 | cmd.arg(format!("https://github.com/{repo}")); 112 | cmd.assert().try_success()?; 113 | 114 | fetch(&dir, rev)?; 115 | } 116 | 117 | if checkout(&dir, rev).is_err() { 118 | fetch(&dir, rev)?; 119 | checkout(&dir, rev)?; 120 | } 121 | Ok(dir) 122 | } 123 | 124 | pub fn fetch(dir: &PathBuf, rev: &str) -> Result<(), anyhow::Error> { 125 | let mut cmd = Command::new("git"); 126 | cmd.current_dir(dir); 127 | cmd.arg("fetch"); 128 | cmd.arg("--depth"); 129 | cmd.arg("1"); 130 | cmd.arg("origin"); 131 | cmd.arg(rev); 132 | cmd.assert().try_success()?; 133 | Ok(()) 134 | } 135 | 136 | pub fn checkout(dir: &PathBuf, rev: &str) -> Result<(), anyhow::Error> { 137 | let mut cmd = Command::new("git"); 138 | cmd.current_dir(dir); 139 | cmd.arg("checkout"); 140 | cmd.arg(rev); 141 | cmd.assert().try_success()?; 142 | Ok(()) 143 | } 144 | -------------------------------------------------------------------------------- /tests/ui/fmt/check.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - B 5 | - C 6 | features: 7 | F0: 8 | - - B 9 | - F0 10 | - - C 11 | - F0 12 | G0: 13 | - - C 14 | - G0 15 | - - B 16 | - G0 17 | - name: B 18 | features: 19 | F0: null 20 | G0: null 21 | - name: C 22 | features: 23 | F0: null 24 | G0: null 25 | cases: 26 | - cmd: format features --check 27 | stdout: | 28 | Found 3 crates with unformatted features: 29 | A 30 | B 31 | C 32 | Run again with `--fix` to format them. 33 | stderr: | 34 | [WARN] The `--check` is now implicit and ignored 35 | code: 1 36 | - cmd: f f -c 37 | stdout: | 38 | Found 3 crates with unformatted features: 39 | A 40 | B 41 | C 42 | Run again with `--fix` to format them. 43 | stderr: | 44 | [WARN] The `--check` is now implicit and ignored 45 | code: 1 46 | - cmd: format features --exit-code-zero --fix 47 | stdout: | 48 | Found 3 crates with unformatted features: 49 | A 50 | B 51 | C 52 | Formatted 3 crates (all fixed). 53 | diff: | 54 | diff --git A/Cargo.toml A/Cargo.toml 55 | index c4ef250c25..b4d04dede9 100644 56 | --- A/Cargo.toml 57 | +++ A/Cargo.toml 58 | @@ -16,8 +16,2 @@ C = { version = "*", path = "../C"} 59 | -F0 = [ 60 | -"B/F0", 61 | -"C/F0", 62 | -] 63 | -G0 = [ 64 | -"C/G0", 65 | -"B/G0", 66 | -] 67 | +F0 = [ "B/F0", "C/F0" ] 68 | +G0 = [ "B/G0", "C/G0" ] 69 | diff --git B/Cargo.toml B/Cargo.toml 70 | index 168330edee..62d91ff0e0 100644 71 | --- B/Cargo.toml 72 | +++ B/Cargo.toml 73 | @@ -14,4 +14,2 @@ edition = "2024" 74 | -F0 = [ 75 | -] 76 | -G0 = [ 77 | -] 78 | +F0 = [] 79 | +G0 = [] 80 | diff --git C/Cargo.toml C/Cargo.toml 81 | index 96ff11808e..ee8de9f46c 100644 82 | --- C/Cargo.toml 83 | +++ C/Cargo.toml 84 | @@ -14,4 +14,2 @@ edition = "2024" 85 | -F0 = [ 86 | -] 87 | -G0 = [ 88 | -] 89 | +F0 = [] 90 | +G0 = [] 91 | - cmd: format features -f 92 | stdout: | 93 | Found 3 crates with unformatted features: 94 | A 95 | B 96 | C 97 | Formatted 3 crates (all fixed). 98 | code: 0 99 | diff: | 100 | diff --git A/Cargo.toml A/Cargo.toml 101 | index c4ef250c25..b4d04dede9 100644 102 | --- A/Cargo.toml 103 | +++ A/Cargo.toml 104 | @@ -16,8 +16,2 @@ C = { version = "*", path = "../C"} 105 | -F0 = [ 106 | -"B/F0", 107 | -"C/F0", 108 | -] 109 | -G0 = [ 110 | -"C/G0", 111 | -"B/G0", 112 | -] 113 | +F0 = [ "B/F0", "C/F0" ] 114 | +G0 = [ "B/G0", "C/G0" ] 115 | diff --git B/Cargo.toml B/Cargo.toml 116 | index 168330edee..62d91ff0e0 100644 117 | --- B/Cargo.toml 118 | +++ B/Cargo.toml 119 | @@ -14,4 +14,2 @@ edition = "2024" 120 | -F0 = [ 121 | -] 122 | -G0 = [ 123 | -] 124 | +F0 = [] 125 | +G0 = [] 126 | diff --git C/Cargo.toml C/Cargo.toml 127 | index 96ff11808e..ee8de9f46c 100644 128 | --- C/Cargo.toml 129 | +++ C/Cargo.toml 130 | @@ -14,4 +14,2 @@ edition = "2024" 131 | -F0 = [ 132 | -] 133 | -G0 = [ 134 | -] 135 | +F0 = [] 136 | +G0 = [] 137 | - cmd: format features --check --mode-per-feature "F0:canonicalize" 138 | stdout: | 139 | Found 3 crates with unformatted features: 140 | A 141 | B 142 | C 143 | Run again with `--fix` to format them. 144 | stderr: | 145 | [WARN] The `--check` is now implicit and ignored 146 | code: 1 147 | - cmd: format features --check --mode-per-feature "F0:sort" 148 | stdout: | 149 | Found 3 crates with unformatted features: 150 | A 151 | B 152 | C 153 | Run again with `--fix` to format them. 154 | stderr: | 155 | [WARN] The `--check` is now implicit and ignored 156 | code: 1 157 | - cmd: format features --ignore-feature F0,G0 158 | - cmd: format features --ignore-feature F0,G0 --mode-per-feature "F0:sort" 159 | - cmd: format features --ignore-feature F0,G0 --mode-per-feature "F0:sort,F0:canonicalize" 160 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Oliver Tale-Yazdi 3 | 4 | pub mod semver; 5 | pub mod workflow; 6 | 7 | use crate::{cmd::GlobalArgs, config::workflow::WorkflowFile, log, ErrToStr}; 8 | 9 | use std::{ 10 | fs::canonicalize, 11 | path::{Path, PathBuf}, 12 | }; 13 | 14 | #[derive(Default, Debug, clap::Parser)] 15 | pub struct ConfigArgs { 16 | /// Manually set the location of the manifest file. 17 | /// 18 | /// Must point directly to a file an not a directory. 19 | #[clap(long, global = true)] 20 | pub manifest_path: Option, 21 | 22 | /// The path to the config file to use. 23 | #[clap(long, alias = "cfg", short)] 24 | pub config: Option, 25 | 26 | /// Whether to check if the config file is compatible with the current version of Zepter. 27 | #[clap(long, value_enum, value_name = "TOGGLE", default_value_t = Toggle::On, verbatim_doc_comment)] 28 | pub check_cfg_compatibility: Toggle, 29 | } 30 | 31 | #[derive(Debug, Clone, PartialEq, clap::ValueEnum)] 32 | pub enum Toggle { 33 | On, 34 | Off, 35 | } 36 | 37 | impl Default for Toggle { 38 | fn default() -> Self { 39 | Self::On 40 | } 41 | } 42 | 43 | pub const WELL_KNOWN_CFG_PATHS: &[&str] = &["zepter.yaml", ".zepter.yaml"]; 44 | 45 | /// Search for `zepter.yaml`, `zepter`, `.zepter.yaml` or `.zepter` in the folders: 46 | /// - `./` 47 | /// - `./.cargo/` 48 | /// - `./.config` 49 | pub fn search_config>(workspace: P) -> Result> { 50 | let paths: Vec = vec![ 51 | workspace.as_ref().to_path_buf(), 52 | workspace.as_ref().join(".cargo"), 53 | workspace.as_ref().join(".config"), 54 | ]; 55 | let mut searched = vec![]; 56 | 57 | // Check all combinations of names and paths 58 | for (name, path) in WELL_KNOWN_CFG_PATHS 59 | .iter() 60 | .flat_map(|name| paths.iter().map(move |path| (name, path))) 61 | { 62 | let mut path = path.join(name); 63 | 64 | if path.exists() { 65 | path = canonicalize(path).expect("Failed to canonicalize path"); 66 | return Ok(path) 67 | } 68 | searched.push(path); 69 | } 70 | 71 | Err(searched) 72 | } 73 | 74 | impl ConfigArgs { 75 | pub fn load_or_panic(&self) -> WorkflowFile { 76 | self.load().unwrap_or_else(|e| { 77 | eprintln!("{e}"); 78 | std::process::exit(GlobalArgs::error_code_cfg_parsing()); 79 | }) 80 | } 81 | 82 | pub fn load(&self) -> Result { 83 | let path = self.locate_config()?; 84 | log::debug!("Using config file: {path:?}"); 85 | let cfg = WorkflowFile::from_path(path)?; 86 | 87 | if self.check_cfg_compatibility == Toggle::On { 88 | cfg.check_cfg_compatibility()?; 89 | } 90 | 91 | Ok(cfg) 92 | } 93 | 94 | fn locate_config(&self) -> Result { 95 | if let Some(path) = &self.config { 96 | let path = canonicalize(path).err_to_str()?; 97 | 98 | if path.exists() { 99 | Ok(path) 100 | } else { 101 | Err(format!("Provided config path does not exist: {path:?}")) 102 | } 103 | } else { 104 | let root = self.locate_workspace()?; 105 | 106 | match search_config(root) { 107 | Ok(cfg) => Ok(cfg), 108 | Err(searched) => { 109 | println!("Failed to find config file in any of these locations:"); 110 | for path in searched { 111 | println!(" - {}", path.display()); 112 | } 113 | Err("Could not find a config file".into()) 114 | }, 115 | } 116 | } 117 | } 118 | 119 | fn locate_workspace(&self) -> Result { 120 | let mut cmd = std::process::Command::new("cargo"); 121 | cmd.arg("locate-project").args([ 122 | "--message-format", 123 | "plain", 124 | "--workspace", 125 | "--offline", 126 | "--locked", 127 | ]); 128 | if let Some(path) = &self.manifest_path { 129 | cmd.arg("--manifest-path").arg(path); 130 | } 131 | let output = cmd.output().err_to_str()?; 132 | let path = output.stdout; 133 | let path = String::from_utf8(path).err_to_str()?; 134 | let path = PathBuf::from(path); 135 | let root = path.parent().ok_or("Failed to find workspace root")?; 136 | 137 | Ok(root.into()) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/cmd/trace.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Oliver Tale-Yazdi 3 | 4 | //! Trace the dependency path from one crate to another. 5 | 6 | use super::*; 7 | use crate::{dag::Dag, log, CrateId}; 8 | use cargo_metadata::{Metadata, Package}; 9 | use clap::Parser; 10 | use std::collections::{BTreeMap, BTreeSet}; 11 | 12 | /// Trace the dependency path from one crate to another. 13 | #[derive(Debug, Parser)] 14 | pub struct TraceCmd { 15 | #[allow(missing_docs)] 16 | #[clap(flatten)] 17 | cargo_args: super::CargoArgs, 18 | 19 | /// Show the source location of crates in the output. 20 | #[clap(long)] 21 | show_source: bool, 22 | 23 | /// Show the version of the crates in the output. 24 | #[clap(long)] 25 | show_version: bool, 26 | 27 | /// Delimiter for rendering dependency paths. 28 | #[clap(long, default_value = " -> ")] 29 | path_delimiter: String, 30 | 31 | /// Do not unify versions but treat `(id, version)` as a unique crate in the dependency graph. 32 | /// 33 | /// Unifying the versions would mean that they are factored out and only `id` is used to 34 | /// identify a crate. 35 | #[clap(long)] 36 | unique_versions: bool, 37 | 38 | /// The root crate to start from. 39 | #[clap(index(1))] 40 | from: String, 41 | 42 | /// The dependency crate to end at. 43 | #[clap(index(2))] 44 | to: String, 45 | } 46 | 47 | impl TraceCmd { 48 | pub fn run(&self, _global: &GlobalArgs) { 49 | let meta = self.cargo_args.load_metadata().expect("Loads metadata"); 50 | let (dag, index) = Self::build_dag(meta).expect("Builds dependency graph"); 51 | let lookup = |id: &str| { 52 | index 53 | .get(id) 54 | .unwrap_or_else(|| panic!("Could not find crate {id} in the metadata")) 55 | }; 56 | 57 | let froms = index 58 | .iter() 59 | .filter(|(_id, krate)| krate.name.to_string() == self.from) 60 | .map(|(id, _)| id) 61 | .collect::>(); 62 | if froms.is_empty() { 63 | panic!("Could not find crate {} in the left dependency graph", self.from); 64 | } 65 | 66 | let tos = index 67 | .iter() 68 | .filter(|(_id, krate)| krate.name.to_string() == self.to) 69 | .map(|(id, _)| id) 70 | .collect::>(); 71 | if tos.is_empty() { 72 | panic!("Could not find crate {} in the right dependency graph", self.to); 73 | } 74 | 75 | log::info!( 76 | "No version or features specified: Checking all {} possibly distinct paths", 77 | froms.len() * tos.len() 78 | ); 79 | let mut paths = BTreeSet::new(); 80 | 81 | for from in froms.iter() { 82 | for to in tos.iter() { 83 | if let Some(path) = dag.any_path(from, to) { 84 | paths.insert(path); 85 | } 86 | } 87 | } 88 | if paths.is_empty() { 89 | panic!("No path found"); 90 | } 91 | log::info!("Found {} distinct paths", paths.len()); 92 | // Unescape the delimiter - the ghetto way. 93 | let delimiter = self.path_delimiter.replace("\\n", "\n").replace("\\t", "\t"); 94 | 95 | for path in paths { 96 | let mut out = String::new(); 97 | let mut is_first = true; 98 | 99 | path.for_each(|id| { 100 | let krate = lookup(id); 101 | if !is_first { 102 | out.push_str(&delimiter); 103 | } 104 | is_first = false; 105 | out.push_str(&krate.name); 106 | if self.show_version { 107 | out.push_str(&format!(" v{}", krate.version)); 108 | } 109 | if self.show_source { 110 | if let Some(source) = krate.source.as_ref() { 111 | out.push_str(&format!(" ({})", source.repr)); 112 | } else { 113 | out.push_str(" (local)"); 114 | } 115 | } 116 | }); 117 | 118 | println!("{out}"); 119 | } 120 | } 121 | 122 | /// Build a dependency graph over the crates ids and return an index of all crates. 123 | fn build_dag(meta: Metadata) -> Result<(Dag, BTreeMap), String> { 124 | let mut dag = Dag::new(); 125 | let mut index = BTreeMap::new(); 126 | 127 | for pkg in meta.packages.clone() { 128 | let id = pkg.id.to_string(); 129 | dag.add_node(id.clone()); 130 | index.insert(pkg.id.to_string(), pkg.clone()); 131 | 132 | for dep in pkg.dependencies.iter() { 133 | if let Some(dep) = resolve_dep(&pkg, dep, &meta) { 134 | let dep = dep.pkg; // TODO account for renaming 135 | let did = dep.id.to_string(); 136 | dag.add_edge(id.clone(), did); 137 | } 138 | } 139 | } 140 | 141 | Ok((dag, index)) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/cmd/lint/nostd.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Oliver Tale-Yazdi 3 | 4 | use crate::{ 5 | cmd::{lint::AutoFixer, resolve_dep, CargoArgs, GlobalArgs}, 6 | grammar::plural, 7 | log, 8 | }; 9 | use cargo_metadata::{DependencyKind, Package}; 10 | use std::{ 11 | collections::{btree_map::Entry, BTreeMap}, 12 | fs::canonicalize, 13 | }; 14 | 15 | #[derive(Debug, clap::Parser)] 16 | pub struct NoStdCmd { 17 | #[clap(subcommand)] 18 | sub: NoStdSubCmd, 19 | } 20 | 21 | #[derive(Debug, clap::Subcommand)] 22 | pub enum NoStdSubCmd { 23 | /// Default features of no-std dependencies are disabled if the crate itself supports no-std. 24 | #[clap(name = "default-features-of-nostd-dependencies-disabled")] 25 | DefaultFeaturesDisabled(DefaultFeaturesDisabledCmd), 26 | } 27 | 28 | #[derive(Debug, clap::Parser)] 29 | pub struct DefaultFeaturesDisabledCmd { 30 | #[allow(missing_docs)] 31 | #[clap(flatten)] 32 | cargo_args: CargoArgs, 33 | 34 | /// Whether to fix the issues. 35 | #[clap(long, short)] 36 | fix: bool, 37 | } 38 | 39 | impl NoStdCmd { 40 | pub(crate) fn run(&self, global: &GlobalArgs) -> Result<(), String> { 41 | match &self.sub { 42 | NoStdSubCmd::DefaultFeaturesDisabled(cmd) => cmd.run(global), 43 | } 44 | } 45 | } 46 | 47 | impl DefaultFeaturesDisabledCmd { 48 | pub(crate) fn run(&self, g: &GlobalArgs) -> Result<(), String> { 49 | let meta = self.cargo_args.clone().with_workspace(true).load_metadata()?; 50 | let pkgs = &meta.packages; 51 | let mut cache = BTreeMap::new(); 52 | let mut autofixer = BTreeMap::new(); 53 | let mut issues = 0; 54 | // Dir that we are allowed to write to. 55 | let allowed_dir = canonicalize(meta.workspace_root.as_std_path()).unwrap(); 56 | 57 | for lhs in pkgs.iter() { 58 | // check if lhs supports no-std builds 59 | if !Self::supports_nostd(g, lhs, &mut cache)? { 60 | continue; 61 | } 62 | 63 | for dep in lhs.dependencies.iter() { 64 | if dep.kind != DependencyKind::Normal { 65 | continue; 66 | } 67 | 68 | let Some(rhs) = resolve_dep(lhs, dep, &meta) else { continue }; 69 | 70 | if !Self::supports_nostd(g, &rhs.pkg, &mut cache)? { 71 | continue; 72 | } 73 | 74 | if !dep.uses_default_features { 75 | continue; 76 | } 77 | 78 | println!( 79 | "Default features not disabled for dependency: {} -> {}", 80 | lhs.name, rhs.pkg.name 81 | ); 82 | 83 | let fixer = match autofixer.entry(lhs.manifest_path.clone()) { 84 | Entry::Occupied(e) => e.into_mut(), 85 | Entry::Vacant(e) => { 86 | let krate_path = 87 | canonicalize(lhs.manifest_path.clone().into_std_path_buf()).unwrap(); 88 | 89 | if !krate_path.starts_with(&allowed_dir) { 90 | return Err(format!("Cannot write to path: {}", krate_path.display())) 91 | } 92 | e.insert(AutoFixer::from_manifest(&lhs.manifest_path)?) 93 | }, 94 | }; 95 | 96 | fixer.disable_default_features(&rhs.name())?; 97 | issues += 1; 98 | } 99 | } 100 | 101 | let s = plural(autofixer.len()); 102 | print!("Found {} issue{} in {} crate{s} ", issues, plural(issues), autofixer.len()); 103 | if self.fix { 104 | for (_, fixer) in autofixer.iter_mut() { 105 | fixer.save()?; 106 | } 107 | println!("and fixed all of them."); 108 | Ok(()) 109 | } else { 110 | println!("and fixed none. Re-run with --fix to apply fixes."); 111 | Err("Several issues were not fixed.".to_string()) 112 | } 113 | } 114 | 115 | fn supports_nostd( 116 | g: &GlobalArgs, 117 | krate: &Package, 118 | cache: &mut BTreeMap, 119 | ) -> Result { 120 | log::debug!("Checking if crate supports no-std: {}", krate.name); 121 | if let Some(res) = cache.get(krate.manifest_path.as_str()) { 122 | return Ok(*res) 123 | } 124 | 125 | // try to find the lib.rs 126 | let krate_root = krate 127 | .manifest_path 128 | .parent() 129 | .ok_or_else(|| format!("Could not find parent of manifest: {}", krate.manifest_path))?; 130 | let lib_rs = krate_root.join("src/lib.rs"); 131 | 132 | if !lib_rs.exists() { 133 | return Ok(false) 134 | } 135 | let content = 136 | std::fs::read_to_string(&lib_rs).map_err(|e| format!("Could not read lib.rs: {e}"))?; 137 | 138 | let ret = if content.contains("#![cfg_attr(not(feature = \"std\"), no_std)]") || 139 | content.contains("#![no_std]") 140 | { 141 | if content.contains("\n#![cfg(") { 142 | println!( 143 | "{}: Crate may unexpectedly pull in libstd: {}", 144 | g.yellow("WARN"), 145 | krate.name 146 | ); 147 | } 148 | log::debug!("Crate supports no-std: {} (path={})", krate.name, krate.manifest_path); 149 | true 150 | } else { 151 | false 152 | }; 153 | 154 | cache.insert(krate.manifest_path.as_str().into(), ret); 155 | Ok(ret) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/cmd/transpose/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Oliver Tale-Yazdi 3 | 4 | mod lift_to_workspace; 5 | 6 | use super::GlobalArgs; 7 | use crate::{ 8 | autofix::*, 9 | cmd::{resolve_dep, transpose::lift_to_workspace::LiftToWorkspaceCmd}, 10 | }; 11 | 12 | use cargo_metadata::{Dependency as Dep, DependencyKind}; 13 | use semver::{Op, Version, VersionReq}; 14 | use std::{collections::BTreeMap as Map, fs::canonicalize}; 15 | 16 | #[derive(Debug, Clone, PartialEq, clap::ValueEnum)] 17 | pub enum SourceLocationSelector { 18 | /// The dependency is referenced via a `path`. 19 | Local, 20 | /// Either git or a registry. 21 | Remote, 22 | } 23 | 24 | /// Transpose dependencies in the workspace. 25 | #[derive(Debug, clap::Parser)] 26 | pub struct TransposeCmd { 27 | #[clap(subcommand)] 28 | subcommand: TransposeSubCmd, 29 | } 30 | 31 | impl TransposeCmd { 32 | pub fn run(&self, global: &GlobalArgs) -> Result<(), String> { 33 | match &self.subcommand { 34 | TransposeSubCmd::Dependency(cmd) => cmd.run(global), 35 | TransposeSubCmd::Features(cmd) => { 36 | cmd.run(global); 37 | Ok(()) 38 | }, 39 | } 40 | } 41 | } 42 | 43 | /// Sub-commands of the [Transpose](TransposeCmd) command. 44 | #[derive(Debug, clap::Subcommand)] 45 | pub enum TransposeSubCmd { 46 | #[clap(alias = "dep", alias = "d")] 47 | Dependency(DependencyCmd), 48 | #[clap(alias = "f")] 49 | Features(FeaturesCmd), 50 | } 51 | 52 | #[derive(Debug, clap::Parser)] 53 | pub struct DependencyCmd { 54 | #[clap(subcommand)] 55 | subcommand: DependencySubCmd, 56 | } 57 | 58 | impl DependencyCmd { 59 | pub fn run(&self, global: &GlobalArgs) -> Result<(), String> { 60 | match &self.subcommand { 61 | DependencySubCmd::LiftToWorkspace(cmd) => cmd.run(global), 62 | } 63 | } 64 | } 65 | 66 | #[derive(Debug, clap::Parser)] 67 | pub struct FeaturesCmd { 68 | #[clap(subcommand)] 69 | subcommand: FeaturesSubCmd, 70 | } 71 | 72 | impl FeaturesCmd { 73 | pub fn run(&self, global: &GlobalArgs) { 74 | match &self.subcommand { 75 | FeaturesSubCmd::StripDevOnly(cmd) => cmd.run(global), 76 | } 77 | } 78 | } 79 | 80 | #[derive(Debug, clap::Subcommand)] 81 | pub enum DependencySubCmd { 82 | #[clap(alias = "lift", alias = "l")] 83 | LiftToWorkspace(LiftToWorkspaceCmd), 84 | } 85 | 86 | #[derive(Debug, clap::Subcommand)] 87 | pub enum FeaturesSubCmd { 88 | /// Strip out dev dependencies. 89 | StripDevOnly(StripDevDepsCmd), 90 | } 91 | 92 | #[derive(Debug, clap::Parser)] 93 | pub struct StripDevDepsCmd { 94 | #[allow(missing_docs)] 95 | #[clap(flatten)] 96 | cargo_args: super::CargoArgs, 97 | 98 | /// Only consider these packages. 99 | #[clap(long, short = 'p', value_delimiter = ',', verbatim_doc_comment)] 100 | packages: Option>, 101 | } 102 | 103 | impl StripDevDepsCmd { 104 | pub fn run(&self, g: &GlobalArgs) { 105 | g.warn_unstable(); 106 | let meta = self.cargo_args.load_metadata().expect("Loads metadata"); 107 | 108 | let kind = DependencyKind::Development; 109 | // Allowed dir that we can write to. 110 | let allowed_dir = canonicalize(meta.workspace_root.as_std_path()).unwrap(); 111 | 112 | for name in self.packages.iter().flatten() { 113 | if !meta.packages.iter().any(|p| p.name.to_string() == *name) { 114 | eprintln!("Could not find package named '{}'", g.red(name)); 115 | std::process::exit(1); 116 | } 117 | } 118 | 119 | let mut fixers = Map::new(); 120 | for pkg in meta.packages.iter() { 121 | if let Some(packages) = &self.packages { 122 | if !packages.contains(&pkg.name.to_string()) { 123 | continue 124 | } 125 | } 126 | 127 | // Are we allowed to modify this file path? 128 | let krate_path = canonicalize(pkg.manifest_path.clone().into_std_path_buf()).unwrap(); 129 | if !krate_path.starts_with(&allowed_dir) { 130 | continue 131 | } 132 | let mut fixer = AutoFixer::from_manifest(&krate_path).unwrap(); 133 | 134 | // Find all dependencies that are only used as dev dependencies in this package. 135 | let devs = pkg.dependencies.iter().filter(|d| d.kind == kind); 136 | let only_dev = devs 137 | .filter(|dev| { 138 | pkg.dependencies.iter().filter(|d| d.name == dev.name).all(|d| d.kind == kind) 139 | }) 140 | .collect::>(); 141 | 142 | for dep in only_dev.iter() { 143 | // Account for renamed crates: 144 | let Some(dep) = resolve_dep(pkg, dep, &meta) else { 145 | eprintln!("Could not resolve dependency '{}'", g.red(&dep.name)); 146 | std::process::exit(1); 147 | }; 148 | 149 | fixer.remove_feature(&format!("{}/", dep.name())); 150 | fixer.remove_feature(&format!("{}?/", dep.name())); 151 | } 152 | 153 | if fixer.modified() { 154 | fixers.insert(pkg.name.clone(), fixer); 155 | } 156 | } 157 | 158 | for fixer in fixers.values_mut() { 159 | fixer.save().unwrap(); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Oliver Tale-Yazdi 3 | 4 | use assert_cmd::{assert::OutputAssertExt, Command}; 5 | use std::collections::HashMap; 6 | 7 | use zepter::mock::*; 8 | 9 | #[test] 10 | #[ignore] 11 | fn integration() { 12 | // Set backtrace since it appears in the UI test output 13 | std::env::set_var("RUST_BACKTRACE", "0"); 14 | 15 | let filter = std::env::var("UI_FILTER").unwrap_or_else(|_| "**/*.yaml".into()); 16 | let regex = format!("tests/{filter}"); 17 | // Loop through all files in tests/ recursively 18 | let files = glob::glob(®ex).unwrap(); 19 | let overwrite = std::env::var("OVERWRITE").is_ok(); 20 | let keep_going = std::env::var("KEEP_GOING").is_ok(); 21 | let (mut failed, mut good) = (0, 0); 22 | 23 | if overwrite { 24 | colour::white_ln!("Running tests in OVERWRITE mode\n"); 25 | } 26 | 27 | // Update each time you add a test. 28 | for file in files.filter_map(Result::ok).filter(|f| f.is_file()) { 29 | let mut config = CaseFile::from_file(&file); 30 | let (workspace, ctx) = config.init().unwrap(); 31 | let mut cout_overwrites = HashMap::new(); 32 | let mut cerr_overwrites = HashMap::new(); 33 | let mut diff_overwrites = HashMap::new(); 34 | let m = config.cases().len(); 35 | 36 | for (i, case) in config.cases().iter().enumerate() { 37 | let _init = case.init(workspace.as_path()).unwrap(); 38 | colour::white!("{} {}/{} ", file.display(), i + 1, m); 39 | git_reset(workspace.as_path()).unwrap(); 40 | let mut cmd = Command::cargo_bin("zepter").unwrap(); 41 | for arg in case.cmd.split_whitespace() { 42 | cmd.arg(arg); 43 | } 44 | 45 | if config.default_args() { 46 | let toml_path = workspace.as_path().join("Cargo.toml"); 47 | cmd.args([ 48 | "--manifest-path", 49 | toml_path.as_path().to_str().unwrap(), 50 | "--log", 51 | "warn", 52 | ]); 53 | if i > 0 { 54 | cmd.arg("--offline"); 55 | } 56 | } else { 57 | cmd.current_dir(workspace.as_path()); 58 | } 59 | 60 | // remove empty trailing and suffix lines 61 | let res = cmd.output().unwrap(); 62 | if let Some(code) = case.code { 63 | res.clone().assert().code(code); 64 | } else { 65 | res.clone().assert().success(); 66 | } 67 | 68 | match (res.stdout == case.stdout.as_bytes(), res.stderr == case.stderr.as_bytes()) { 69 | (true, true) => { 70 | colour::white!("cout:"); 71 | colour::green!("OK"); 72 | colour::white!(" "); 73 | good += 1; 74 | }, 75 | (false, _) if !overwrite => { 76 | colour::white!("cerr:"); 77 | colour::red!("FAIL"); 78 | colour::white!(" "); 79 | if !keep_going { 80 | pretty_assertions::assert_eq!( 81 | &String::from_utf8_lossy(&res.stdout), 82 | &normalize(&case.stdout), 83 | ); 84 | unreachable!() 85 | } 86 | }, 87 | (true, false) if !overwrite => { 88 | colour::white!("cerr:"); 89 | colour::red!("FAIL"); 90 | colour::white!(" "); 91 | if !keep_going { 92 | pretty_assertions::assert_eq!( 93 | &String::from_utf8_lossy(&res.stderr), 94 | &normalize(&case.stderr), 95 | ); 96 | unreachable!() 97 | } 98 | }, 99 | (true, false) => { 100 | colour::white!("cerr:"); 101 | colour::yellow!("OVERWRITE"); 102 | colour::white!(" "); 103 | cerr_overwrites.insert(i, String::from_utf8_lossy(&res.stderr).to_string()); 104 | 105 | failed += 1; 106 | }, 107 | (false, _) => { 108 | colour::white!("cout:"); 109 | colour::yellow!("OVERWRITE"); 110 | colour::white!(" "); 111 | cout_overwrites.insert(i, String::from_utf8_lossy(&res.stdout).to_string()); 112 | 113 | failed += 1; 114 | }, 115 | } 116 | 117 | let got = git_diff(workspace.as_path()).unwrap(); 118 | if got != case.diff { 119 | if std::env::var("OVERWRITE").is_ok() { 120 | diff_overwrites.insert(i, got); 121 | colour::white!("diff:"); 122 | colour::yellow_ln!("OVERWRITE"); 123 | colour::white!(""); 124 | } else { 125 | colour::white!("diff:"); 126 | colour::red_ln!("FAILED"); 127 | colour::white!(""); 128 | if !keep_going { 129 | pretty_assertions::assert_eq!(got, case.diff); 130 | } 131 | } 132 | } else { 133 | colour::white!("diff:"); 134 | colour::green_ln!("OK"); 135 | colour::white!(""); 136 | } 137 | git_reset(workspace.as_path()).unwrap(); 138 | } 139 | 140 | if std::env::var("PERSIST").is_ok() { 141 | if let Some(ctx) = ctx { 142 | let path = ctx.persist(); 143 | colour::white_ln!("Persisted to {:?}", path); 144 | } else { 145 | colour::red_ln!("Cannot persist test"); 146 | } 147 | } 148 | 149 | if std::env::var("OVERWRITE").is_ok() { 150 | if cout_overwrites.is_empty() && 151 | cerr_overwrites.is_empty() && 152 | diff_overwrites.is_empty() 153 | { 154 | continue 155 | } 156 | 157 | for (i, stdout) in cout_overwrites { 158 | config.case_mut(i).stdout = stdout; 159 | } 160 | for (i, stderr) in cerr_overwrites { 161 | config.case_mut(i).stderr = stderr; 162 | } 163 | for (i, diff) in diff_overwrites { 164 | config.case_mut(i).diff = diff; 165 | } 166 | 167 | config.to_file(&file).unwrap(); 168 | println!("Updated {}", file.display()); 169 | } 170 | } 171 | 172 | if failed > 0 { 173 | if std::env::var("OVERWRITE").is_ok() { 174 | println!("Updated {failed} test(s)"); 175 | } else { 176 | panic!("{failed} test(s) failed"); 177 | } 178 | } 179 | if failed == 0 && good == 0 { 180 | panic!("No tests found. Try something like '**/never-enables/diamond.yaml'"); 181 | } 182 | println!(); 183 | } 184 | -------------------------------------------------------------------------------- /src/config/workflow.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Oliver Tale-Yazdi 3 | 4 | //! Loads config and workflow files. 5 | 6 | use crate::{cmd::GlobalArgs, config::semver::Semver, log}; 7 | use serde::Deserialize; 8 | use std::{collections::BTreeMap as Map, str::FromStr}; 9 | 10 | pub type WorkflowName = String; 11 | 12 | /// The name of the workflow to run when none is specified. 13 | pub const WORKFLOW_DEFAULT_NAME: &str = "default"; 14 | 15 | #[derive(Deserialize)] 16 | pub struct WorkflowFile { 17 | version: Version, 18 | workflows: Map, 19 | help: Option, 20 | } 21 | 22 | #[derive(Deserialize)] 23 | pub struct Version { 24 | #[serde(deserialize_with = "Semver::from_serde")] 25 | format: Semver, 26 | 27 | #[serde(deserialize_with = "Semver::from_serde")] 28 | binary: Semver, 29 | } 30 | 31 | #[derive(Deserialize, Clone)] 32 | pub struct Workflow(pub Vec); 33 | 34 | #[derive(Deserialize, Clone)] 35 | pub struct WorkflowStep(pub Vec); 36 | 37 | #[derive(Deserialize, Clone)] 38 | pub struct WorkflowHelp { 39 | pub text: String, 40 | pub links: Vec, 41 | } 42 | 43 | impl Workflow { 44 | pub fn run(self, _g: &GlobalArgs) -> Result<(), String> { 45 | for (_i, step) in self.0.iter().enumerate() { 46 | let mut args = step.0.clone(); 47 | // No default hint since the workflows can provide their own. 48 | args.push("--fix-hint=off".into()); 49 | let cmd = std::env::args().next().unwrap_or("zepter".into()); 50 | 51 | log::debug!("Running command '{cmd} {}'", args.join(" ")); 52 | 53 | let status = std::process::Command::new(&cmd) 54 | .args(args.clone()) 55 | .status() 56 | .map_err(|e| format!("Failed to run command '{cmd}': {e}"))?; 57 | 58 | let first_two_args = args 59 | .iter() 60 | .rev() 61 | .skip(1) 62 | .rev() 63 | .take(2) 64 | .map(String::as_str) 65 | .collect::>() 66 | .join(" "); 67 | 68 | if !status.success() { 69 | return Err(format!( 70 | "Command '{first_two_args}' failed with exit code {}", 71 | status.code().unwrap_or(1) 72 | )) 73 | } 74 | 75 | log::info!("{}/{} {:<}", _i + 1, self.0.len(), first_two_args); 76 | } 77 | 78 | Ok(()) 79 | } 80 | } 81 | 82 | impl FromStr for WorkflowFile { 83 | type Err = String; 84 | 85 | fn from_str(content: &str) -> Result { 86 | let parsed = serde_yaml_ng::from_str::(content) 87 | .map_err(|e| format!("yaml parsing: {e}"))?; 88 | 89 | if parsed.version.format != (1, 0, 0).into() { 90 | return Err("Can only parse workflow files with version '1'".into()) 91 | } 92 | 93 | parsed.into_resolved() 94 | } 95 | } 96 | 97 | impl WorkflowFile { 98 | pub fn workflow>(&self, name: S) -> Option { 99 | self.workflows.get(name.as_ref()).cloned() 100 | } 101 | 102 | /// Load a workflow file from the given path. 103 | pub fn from_path>(path: P) -> Result { 104 | let path = path.as_ref(); 105 | let content = std::fs::read_to_string(path) 106 | .map_err(|e| format!("Failed to read config file {path:?}: {e}"))?; 107 | 108 | content.parse() 109 | } 110 | 111 | /// Format the user-provided help message. 112 | pub fn fmt_help(&self) -> Option { 113 | let help = self.help.as_ref()?; 114 | 115 | let links = if !help.links.is_empty() { 116 | format!( 117 | "\n\nFor more information, see:\n{}", 118 | help.links.iter().map(|s| format!(" - {s}")).collect::>().join("\n") 119 | ) 120 | } else { 121 | Default::default() 122 | }; 123 | 124 | let text = help.text.strip_suffix('\n').unwrap_or(""); 125 | format!("{text}{links}").into() 126 | } 127 | 128 | /// Iteratively resolve all references in the workflow file. 129 | pub fn into_resolved(mut self) -> Result { 130 | while self.resolve_once()? {} 131 | Ok(self) 132 | } 133 | 134 | /// Do one iterative resolve step and return whether something changed. 135 | pub fn resolve_once(&mut self) -> Result { 136 | let wfs = self.workflows.clone(); 137 | 138 | for wf in self.workflows.values_mut() { 139 | for step in wf.0.iter_mut() { 140 | for (i, orig_line) in step.0.iter_mut().enumerate() { 141 | if let Some(line) = orig_line.strip_prefix('$') { 142 | let (vname, index) = line.split_once('.').expect("Expecting $name.index"); 143 | let index: u32 = index.parse().map_err(|e| { 144 | format!("Failed to parse index '{index}' in line '{line}': {e}") 145 | })?; 146 | 147 | let value = wfs.get(vname).ok_or_else(|| { 148 | format!("Failed to find workflow '{vname}' in line '{line}'") 149 | })?; 150 | 151 | step.0.remove(i); 152 | for line in value.0[index as usize].0.iter().rev() { 153 | step.0.insert(i, line.clone()); 154 | } 155 | 156 | return Ok(true) 157 | } 158 | } 159 | } 160 | } 161 | 162 | Ok(false) 163 | } 164 | 165 | /// Whether the config file is compatible with the current version of the running binary. 166 | pub fn check_cfg_compatibility(&self) -> Result<(), String> { 167 | let current_version = 168 | Semver::try_from(clap::crate_version!()).expect("Crate version is valid semver"); 169 | let required_version = self.version.binary; 170 | 171 | if current_version.is_newer_or_equal(&required_version) { 172 | Ok(()) 173 | } else { 174 | Err(format!( 175 | "Your version of Zepter is too old for this project.\n\n Required: {required_version}\n Installed: {current_version}\n\nPlease update Zepter with:\n\n cargo install zepter --locked\n\nOr add `--check-cfg-compatibility=off` to the config file." 176 | )) 177 | } 178 | } 179 | } 180 | 181 | #[cfg(test)] 182 | mod tests { 183 | use super::*; 184 | 185 | #[test] 186 | fn workflow_file_from_yaml_works() { 187 | let cfg = WorkflowFile::from_path("presets/polkadot.yaml").unwrap(); 188 | // Sanity checky only 189 | assert_eq!(cfg.workflows.len(), 2); 190 | assert_eq!(cfg.workflow("check").unwrap().0.len(), 2); 191 | assert_eq!(cfg.workflow("default").unwrap().0.len(), 2); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /tests/integration/sdk/duplicate.yaml: -------------------------------------------------------------------------------- 1 | repo: 2 | name: paritytech/polkadot-sdk 3 | ref: db5e645422ccf952018a3c466a33fef477858602 4 | cases: 5 | - cmd: lint duplicate-deps 6 | stdout: | 7 | Found 33 crates with duplicated dependencies between [dependencies] and [dev-dependencies] 8 | crate 'bp-messages' 9 | bp-runtime 10 | crate 'cumulus-client-network' 11 | sc-client-api 12 | sp-consensus 13 | sp-core 14 | crate 'cumulus-client-pov-recovery' 15 | sc-client-api 16 | crate 'cumulus-relay-chain-inprocess-interface' 17 | polkadot-primitives 18 | crate 'cumulus-test-service' 19 | polkadot-test-service 20 | sc-cli 21 | crate 'pallet-bridge-relayers' 22 | bp-runtime 23 | sp-runtime 24 | crate 'pallet-election-provider-multi-block' 25 | sp-core 26 | crate 'pallet-election-provider-multi-phase' 27 | sp-npos-elections 28 | crate 'pallet-revive-eth-rpc' 29 | subxt-signer 30 | crate 'pallet-staking-async-ah-client' 31 | sp-io 32 | crate 'pallet-xcm-bridge-hub' 33 | bp-runtime 34 | crate 'polkadot-availability-distribution' 35 | sc-network 36 | sp-core 37 | crate 'polkadot-availability-recovery' 38 | sc-network 39 | crate 'polkadot-dispute-distribution' 40 | futures-timer 41 | crate 'polkadot-node-core-approval-voting' 42 | async-trait 43 | crate 'polkadot-node-core-approval-voting-parallel' 44 | async-trait 45 | crate 'polkadot-node-core-av-store' 46 | polkadot-node-subsystem-util 47 | crate 'polkadot-node-core-pvf-prepare-worker' 48 | sp-maybe-compressed-blob 49 | crate 'polkadot-statement-distribution' 50 | futures-timer 51 | crate 'polkadot-test-malus' 52 | sp-core 53 | crate 'sc-consensus-grandpa' 54 | sc-network 55 | crate 'sc-consensus-grandpa-rpc' 56 | sp-core 57 | crate 'sc-network' 58 | mockall 59 | rand 60 | crate 'sc-network-sync' 61 | mockall 62 | crate 'sc-offchain' 63 | sc-transaction-pool-api 64 | crate 'sc-rpc' 65 | sc-block-builder 66 | tokio 67 | crate 'sc-transaction-pool' 68 | thiserror 69 | crate 'staging-chain-spec-builder' 70 | docify 71 | crate 'staging-node-cli' 72 | futures 73 | serde_json 74 | crate 'substrate-test-runtime' 75 | serde_json 76 | crate 'test-parachain-adder-collator' 77 | sc-service 78 | crate 'test-parachain-undying-collator' 79 | sc-service 80 | crate 'xcm-runtime-apis' 81 | staging-xcm-executor 82 | code: 1 83 | - cmd: lint duplicate-deps --show-paths 84 | stdout: | 85 | Found 33 crates with duplicated dependencies between [dependencies] and [dev-dependencies] 86 | crate 'bp-messages' (bridges/primitives/messages/Cargo.toml) 87 | bp-runtime 88 | crate 'cumulus-client-network' (cumulus/client/network/Cargo.toml) 89 | sc-client-api 90 | sp-consensus 91 | sp-core 92 | crate 'cumulus-client-pov-recovery' (cumulus/client/pov-recovery/Cargo.toml) 93 | sc-client-api 94 | crate 'cumulus-relay-chain-inprocess-interface' (cumulus/client/relay-chain-inprocess-interface/Cargo.toml) 95 | polkadot-primitives 96 | crate 'cumulus-test-service' (cumulus/test/service/Cargo.toml) 97 | polkadot-test-service 98 | sc-cli 99 | crate 'pallet-bridge-relayers' (bridges/modules/relayers/Cargo.toml) 100 | bp-runtime 101 | sp-runtime 102 | crate 'pallet-election-provider-multi-block' (substrate/frame/election-provider-multi-block/Cargo.toml) 103 | sp-core 104 | crate 'pallet-election-provider-multi-phase' (substrate/frame/election-provider-multi-phase/Cargo.toml) 105 | sp-npos-elections 106 | crate 'pallet-revive-eth-rpc' (substrate/frame/revive/rpc/Cargo.toml) 107 | subxt-signer 108 | crate 'pallet-staking-async-ah-client' (substrate/frame/staking-async/ah-client/Cargo.toml) 109 | sp-io 110 | crate 'pallet-xcm-bridge-hub' (bridges/modules/xcm-bridge-hub/Cargo.toml) 111 | bp-runtime 112 | crate 'polkadot-availability-distribution' (polkadot/node/network/availability-distribution/Cargo.toml) 113 | sc-network 114 | sp-core 115 | crate 'polkadot-availability-recovery' (polkadot/node/network/availability-recovery/Cargo.toml) 116 | sc-network 117 | crate 'polkadot-dispute-distribution' (polkadot/node/network/dispute-distribution/Cargo.toml) 118 | futures-timer 119 | crate 'polkadot-node-core-approval-voting' (polkadot/node/core/approval-voting/Cargo.toml) 120 | async-trait 121 | crate 'polkadot-node-core-approval-voting-parallel' (polkadot/node/core/approval-voting-parallel/Cargo.toml) 122 | async-trait 123 | crate 'polkadot-node-core-av-store' (polkadot/node/core/av-store/Cargo.toml) 124 | polkadot-node-subsystem-util 125 | crate 'polkadot-node-core-pvf-prepare-worker' (polkadot/node/core/pvf/prepare-worker/Cargo.toml) 126 | sp-maybe-compressed-blob 127 | crate 'polkadot-statement-distribution' (polkadot/node/network/statement-distribution/Cargo.toml) 128 | futures-timer 129 | crate 'polkadot-test-malus' (polkadot/node/malus/Cargo.toml) 130 | sp-core 131 | crate 'sc-consensus-grandpa' (substrate/client/consensus/grandpa/Cargo.toml) 132 | sc-network 133 | crate 'sc-consensus-grandpa-rpc' (substrate/client/consensus/grandpa/rpc/Cargo.toml) 134 | sp-core 135 | crate 'sc-network' (substrate/client/network/Cargo.toml) 136 | mockall 137 | rand 138 | crate 'sc-network-sync' (substrate/client/network/sync/Cargo.toml) 139 | mockall 140 | crate 'sc-offchain' (substrate/client/offchain/Cargo.toml) 141 | sc-transaction-pool-api 142 | crate 'sc-rpc' (substrate/client/rpc/Cargo.toml) 143 | sc-block-builder 144 | tokio 145 | crate 'sc-transaction-pool' (substrate/client/transaction-pool/Cargo.toml) 146 | thiserror 147 | crate 'staging-chain-spec-builder' (substrate/bin/utils/chain-spec-builder/Cargo.toml) 148 | docify 149 | crate 'staging-node-cli' (substrate/bin/node/cli/Cargo.toml) 150 | futures 151 | serde_json 152 | crate 'substrate-test-runtime' (substrate/test-utils/runtime/Cargo.toml) 153 | serde_json 154 | crate 'test-parachain-adder-collator' (polkadot/parachain/test-parachains/adder/collator/Cargo.toml) 155 | sc-service 156 | crate 'test-parachain-undying-collator' (polkadot/parachain/test-parachains/undying/collator/Cargo.toml) 157 | sc-service 158 | crate 'xcm-runtime-apis' (polkadot/xcm/xcm-runtime-apis/Cargo.toml) 159 | staging-xcm-executor 160 | code: 1 161 | -------------------------------------------------------------------------------- /tests/ui/root-args/help.yaml: -------------------------------------------------------------------------------- 1 | crates: [] 2 | cases: 3 | - cmd: --help 4 | stdout: "Analyze, Fix and Format features in your Rust workspace.\n\nUsage: zepter [OPTIONS] [COMMAND]\n\nCommands:\n trace Trace the dependency path from one crate to another\n lint Lint your feature usage by analyzing crate metadata\n format Format the features in your manifest files\n run \n debug Arguments for how to load cargo metadata from a workspace\n help Print this message or the help of the given subcommand(s)\n\nOptions:\n -q, --quiet\n Only print errors. Supersedes `--log`\n\n --log \n Log level to use\n \n [default: info]\n\n --color\n Use ANSI terminal colors\n\n --exit-code-zero\n Try to exit with code zero if the intended check failed.\n \n Will still return != 0 in case of an actual error (eg. failed to find some file) or a panic\n (aka software bug).\n\n --fix-hint \n Dont print any hints on how to fix the error.\n \n This is mostly used internally when dispatching, workflows since they come with their\n hints.\n\n Possible values:\n - on: Prints some hint that is (hopefully) helpful\n - off: Prints no hint at all\n \n [default: on]\n\n -h, --help\n Print help (see a summary with '-h')\n\n -V, --version\n Print version\n" 5 | - cmd: lint --help 6 | stdout: "Lint your feature usage by analyzing crate metadata\n\nUsage: zepter lint [OPTIONS] \n\nCommands:\n propagate-feature Check whether features are properly propagated\n never-enables A specific feature never enables a specific other feature\n never-implies A specific feature never implies a specific other feature\n only-enables A specific feature is only implied by a specific set of other features\n why-enabled Arguments for how to load cargo metadata from a workspace\n no-std Check the crates for sane no-std feature configuration\n duplicate-deps Check for duplicated dependencies in `[dependencies]` and `[dev-dependencies]`\n help Print this message or the help of the given subcommand(s)\n\nOptions:\n -q, --quiet\n Only print errors. Supersedes `--log`\n\n --log \n Log level to use\n \n [default: info]\n\n --color\n Use ANSI terminal colors\n\n --exit-code-zero\n Try to exit with code zero if the intended check failed.\n \n Will still return != 0 in case of an actual error (eg. failed to find some file) or a panic\n (aka software bug).\n\n --fix-hint \n Dont print any hints on how to fix the error.\n \n This is mostly used internally when dispatching, workflows since they come with their\n hints.\n\n Possible values:\n - on: Prints some hint that is (hopefully) helpful\n - off: Prints no hint at all\n \n [default: on]\n\n -h, --help\n Print help (see a summary with '-h')\n" 7 | - cmd: lint propagate-feature --help 8 | stdout: "Check whether features are properly propagated\n\nUsage: zepter lint propagate-feature [OPTIONS] --features \n\nOptions:\n --manifest-path \n Cargo manifest path or directory.\n \n For directories it appends a `Cargo.toml`.\n\n --workspace\n Whether to only consider workspace crates\n\n --offline\n Whether to use offline mode\n\n --locked\n Whether to use all the locked dependencies from the `Cargo.lock`.\n \n Otherwise it may update some dependencies. For CI usage its a good idea to use it.\n\n --all-features\n \n\n --features \n Comma separated list of features to check.\n \n Listing the same feature multiple times has the same effect as listing it once.\n\n -p, --packages [...]\n The packages to check. If empty, all packages are checked\n\n -q, --quiet\n Only print errors. Supersedes `--log`\n\n --feature-enables-dep \n Enables the feature of the dependencies as non-optional.\n \n This can be used in case that a dependency should not be enabled like `dep?/feature` but\n like `dep/feature` instead. In this case you would pass `--feature-enables-dep\n feature:dep`. The option can be passed multiple times, or multiple key-value pairs can be\n passed at once by separating them with a comma like: `--feature-enables-dep\n feature:dep,feature2:dep2`. (TODO: Duplicate entries are undefined).\n\n --log \n Log level to use\n \n [default: info]\n\n --color\n Use ANSI terminal colors\n\n --left-side-feature-missing \n Overwrite the behaviour when the left side dependency is missing the feature.\n \n This can be used to ignore missing features, treat them as warning or error. A \"missing\n feature\" here means that if `A` has a dependency `B` which has a feature `F`, and the\n propagation is checked then normally it would error if `A` is not forwarding `F` to `B`.\n Now this option modifies the behaviour if `A` does not have the feature in the first place.\n The default behaviour is to require `A` to also have `F`.\n\n Possible values:\n - ignore: Ignore this behaviour\n - report: Only report but do not fix\n - fix: Fix if `--fix` is passed\n \n [default: fix]\n\n --exit-code-zero\n Try to exit with code zero if the intended check failed.\n \n Will still return != 0 in case of an actual error (eg. failed to find some file) or a panic\n (aka software bug).\n\n --ignore-missing-propagate \n Ignore single missing links in the feature propagation chain.\n\n --fix-hint \n Dont print any hints on how to fix the error.\n \n This is mostly used internally when dispatching, workflows since they come with their\n hints.\n\n Possible values:\n - on: Prints some hint that is (hopefully) helpful\n - off: Prints no hint at all\n \n [default: on]\n\n --left-side-outside-workspace \n How to handle the case that the LHS is outside the workspace.\n\n Possible values:\n - ignore: Ignore this behaviour\n - report: Only report but do not fix\n - fix: Fix if `--fix` is passed\n \n [default: fix]\n\n --dep-kinds \n How to handle dev-dependencies.\n \n [default: normal:check,dev:check,build:check]\n\n --show-version\n Show crate versions in the output\n\n --show-path\n Show crate manifest paths in the output\n\n --fix\n Try to automatically fix the problems\n\n --modify-paths \n \n\n --fix-dependency \n Fix only issues with this package as dependency\n\n --fix-package \n Fix only issues with this package as feature source\n\n -h, --help\n Print help (see a summary with '-h')\n" 9 | -------------------------------------------------------------------------------- /src/cmd/fmt.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Oliver Tale-Yazdi 3 | 4 | //! Format features in the crate manifest. 5 | 6 | use super::GlobalArgs; 7 | use crate::{autofix::*, cmd::parse_key_val, grammar::*, log}; 8 | 9 | use cargo_metadata::Metadata; 10 | use std::{collections::BTreeMap as Map, fs::canonicalize, path::PathBuf, str::FromStr}; 11 | 12 | /// Format the features in your manifest files. 13 | #[derive(Debug, clap::Parser)] 14 | pub struct FormatCmd { 15 | #[clap(subcommand)] 16 | subcommand: SubCommand, 17 | } 18 | 19 | /// Sub-commands of the [Format](FormatCmd) command. 20 | #[derive(Debug, clap::Subcommand)] 21 | pub enum SubCommand { 22 | #[clap(alias = "f")] 23 | Features(FormatFeaturesCmd), 24 | } 25 | 26 | /// Format the content of each feature in the crate manifest. 27 | #[derive(Debug, clap::Parser)] 28 | pub struct FormatFeaturesCmd { 29 | #[allow(missing_docs)] 30 | #[clap(flatten)] 31 | cargo_args: super::CargoArgs, 32 | 33 | /// Include dependencies in the formatting check. 34 | /// 35 | /// They will not be modified, unless their path is included in `--modify-paths`. 36 | #[clap(long)] 37 | no_workspace: bool, 38 | 39 | /// Paths that are allowed to be modified by the formatter. 40 | #[clap(long)] 41 | modify_paths: Vec, 42 | 43 | /// DEPRECATED AND IGNORED 44 | #[clap(long = "check", short = 'c')] 45 | unused_check: bool, 46 | 47 | /// Fix the formatting errors automatically. 48 | #[clap(long, short)] 49 | fix: bool, 50 | 51 | /// The maximal length of a line for a feature. 52 | #[clap(long, default_value_t = 80)] 53 | line_width: u32, 54 | 55 | /// Set the formatting mode for a specific feature. 56 | /// 57 | /// Can be specified multiple times. Example: 58 | /// `--mode-per-feature default:sort,default:canonicalize` 59 | #[clap(long, value_name = "FEATURE:MODE", value_parser = parse_key_val::, value_delimiter = ',', verbatim_doc_comment)] 60 | mode_per_feature: Option>, 61 | 62 | /// Ignore a specific feature across all crates. 63 | /// 64 | /// This is equivalent to `--mode-per-feature FEATURE:none`. 65 | #[clap(long, value_name = "FEATURE", value_delimiter = ',', verbatim_doc_comment)] 66 | ignore_feature: Vec, 67 | 68 | /// Also print the paths of the offending Cargo.toml files. 69 | #[clap(long)] 70 | print_paths: bool, 71 | } 72 | 73 | /// How to format the entries of a feature. 74 | #[derive(Debug, Clone, PartialEq, clap::ValueEnum)] 75 | pub enum Mode { 76 | /// Do nothing. This supersedes all other modes. 77 | None, 78 | /// Alphabetically sort the feature entries. 79 | Sort, 80 | Dedub, 81 | /// Canonicalize the formatting of the feature entries. 82 | /// 83 | /// This means that the order is not changed but that white spaces and newlines are normalized. 84 | /// Comments are kept but also normalized. 85 | Canonicalize, 86 | } 87 | 88 | impl FromStr for Mode { 89 | type Err = std::string::ParseError; 90 | 91 | fn from_str(s: &str) -> Result { 92 | match s.to_ascii_lowercase().as_str() { 93 | "canonicalize" => Ok(Self::Canonicalize), 94 | "sort" => Ok(Self::Sort), 95 | "none" => Ok(Self::None), 96 | _ => panic!("Invalid Mode: {s}. Expected 'canonicalize', 'sort' or 'none'"), // FIXME 97 | } 98 | } 99 | } 100 | 101 | impl FormatCmd { 102 | pub fn run(&self, global: &GlobalArgs) { 103 | match &self.subcommand { 104 | SubCommand::Features(cmd) => cmd.run(global), 105 | } 106 | } 107 | } 108 | 109 | impl FormatFeaturesCmd { 110 | pub fn run(&self, global: &GlobalArgs) { 111 | if self.unused_check { 112 | log::warn!("The `--check` is now implicit and ignored"); 113 | } 114 | 115 | let modes = self.parse_mode_per_feature(); 116 | let meta = self.load_metadata(global); 117 | // Allowed dir that we can write to. 118 | let allowed_dir = canonicalize(meta.workspace_root.as_std_path()).unwrap(); 119 | log::debug!("Allowed dir: {}", allowed_dir.display()); 120 | let mut offenders = Vec::new(); 121 | // (path, crate) -> errors 122 | let mut errors = Map::<(PathBuf, String), Vec>::new(); 123 | 124 | log::debug!("Checking {} crate{}", meta.packages.len(), plural(meta.packages.len())); 125 | 126 | for pkg in meta.packages.iter() { 127 | let path = canonicalize(pkg.manifest_path.clone().into_std_path_buf()).unwrap(); 128 | 129 | let mut fixer = AutoFixer::from_manifest(&path).unwrap(); 130 | if let Err(errs) = fixer.canonicalize_features(&pkg.name, &modes, self.line_width) { 131 | let path = path.strip_prefix(&allowed_dir).unwrap().to_path_buf(); 132 | errors.entry((path.clone(), pkg.name.to_string())).or_default().extend(errs); 133 | } else if fixer.modified() { 134 | offenders.push((path, &pkg.name, fixer)); 135 | } 136 | } 137 | if !errors.is_empty() { 138 | let num_errors = errors.values().map(|errs| errs.len()).sum::(); 139 | println!( 140 | "Please fix {} error{} in {} crate{} manually:", 141 | global.red(&num_errors.to_string()), 142 | plural(num_errors), 143 | global.red(&errors.len().to_string()), 144 | plural(errors.len()) 145 | ); 146 | for ((path, pkg), errs) in errors.iter() { 147 | println!(" {} ({})", global.bold(pkg), path.display()); 148 | for err in errs.iter() { 149 | println!(" {err}"); 150 | } 151 | } 152 | std::process::exit(global.error_code()) 153 | } 154 | 155 | if offenders.is_empty() { 156 | log::debug!( 157 | "Checked {} crate{}: all formatted", 158 | meta.packages.len(), 159 | plural(meta.packages.len()) 160 | ); 161 | return 162 | } 163 | 164 | let mut fixed = 0; 165 | println!( 166 | "Found {} crate{} with unformatted features:", 167 | global.red(&offenders.len().to_string()), 168 | plural(offenders.len()) 169 | ); 170 | for (path, pkg, fixer) in offenders.iter_mut() { 171 | // trim of the allowed_dir, if possible: 172 | let psuffix = 173 | if self.print_paths { format!(" {}", path.display()) } else { Default::default() }; 174 | println!(" {}{}", global.bold(pkg), psuffix); 175 | 176 | if !self.fix { 177 | continue 178 | } 179 | 180 | let can_modify = path.starts_with(&allowed_dir) || 181 | self.modify_paths.iter().any(|p| path.starts_with(p)); 182 | if !can_modify { 183 | log::warn!( 184 | "Not allowed to modify {} outside of: {}", 185 | path.display(), 186 | allowed_dir.display() 187 | ); 188 | continue 189 | } 190 | 191 | fixer.save().unwrap(); 192 | fixed += 1; 193 | } 194 | 195 | if self.fix { 196 | if fixed == offenders.len() { 197 | println!( 198 | "Formatted {} crate{} (all fixed).", 199 | global.green(&fixed.to_string()), 200 | plural(fixed) 201 | ); 202 | } else { 203 | println!( 204 | "Formatted {} crate{} ({} could not be fixed).", 205 | global.green(&fixed.to_string()), 206 | plural(fixed), 207 | global.red(&(offenders.len() - fixed).to_string()) 208 | ); 209 | } 210 | 211 | std::process::exit(0); 212 | } else if global.show_hints() { 213 | println!("Run again with `--fix` to format them."); 214 | } 215 | 216 | std::process::exit(global.error_code()) 217 | } 218 | 219 | fn parse_mode_per_feature(&self) -> Map> { 220 | let mut map = Map::>::new(); 221 | if let Some(modes) = &self.mode_per_feature { 222 | for (feature, mode) in modes.iter() { 223 | map.entry(feature.clone()).or_default().push(mode.clone()); 224 | } 225 | } 226 | 227 | // Respect the ignore_feature flag: 228 | for feature in self.ignore_feature.iter() { 229 | map.insert(feature.clone(), vec![Mode::None]); 230 | } 231 | 232 | map 233 | } 234 | 235 | fn load_metadata(&self, global: &GlobalArgs) -> Metadata { 236 | let mut args = self.cargo_args.clone(); 237 | if args.workspace { 238 | println!("{}", global.yellow("WARNING: --workspace is the default now")); 239 | } 240 | args.workspace = !self.no_workspace; 241 | match args.load_metadata() { 242 | Ok(meta) => meta, 243 | Err(err) => { 244 | println!("{}", global.red(&err)); 245 | std::process::exit(1) 246 | }, 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /tests/ui/lint/propagate-feature/dep_kinds_multiple.yaml: -------------------------------------------------------------------------------- 1 | crates: 2 | - name: A 3 | deps: 4 | - name: BD 5 | kind: dev 6 | - name: BD 7 | kind: build 8 | features: 9 | F0: null 10 | - name: B 11 | deps: 12 | - name: ND 13 | - name: ND 14 | kind: dev 15 | features: 16 | F0: null 17 | - name: C 18 | deps: 19 | - name: NB 20 | - name: NB 21 | kind: build 22 | features: 23 | F0: null 24 | - name: BD 25 | features: 26 | F0: null 27 | - name: ND 28 | features: 29 | F0: null 30 | - name: NB 31 | features: 32 | F0: null 33 | cases: 34 | - cmd: lint propagate-feature -p A --feature F0 --dep-kinds="normal:ignore,dev:ignore,build:ignore" 35 | - cmd: lint propagate-feature -p A --feature F0 --dep-kinds="dev:ignore,build:ignore" 36 | - cmd: lint propagate-feature -p A --feature F0 37 | stdout: | 38 | crate 'A' 39 | feature 'F0' 40 | must propagate to: 41 | BD 42 | Found 1 issue (run with `--fix` to fix). 43 | code: 1 44 | - cmd: lint propagate-feature -p A --feature F0 --dep-kinds="normal:ignore" 45 | stdout: | 46 | crate 'A' 47 | feature 'F0' 48 | must propagate to: 49 | BD 50 | Found 1 issue (run with `--fix` to fix). 51 | code: 1 52 | - cmd: lint propagate-feature -p A --feature F0 --dep-kinds="dev:ignore" 53 | stdout: | 54 | crate 'A' 55 | feature 'F0' 56 | must propagate to: 57 | BD 58 | Found 1 issue (run with `--fix` to fix). 59 | code: 1 60 | - cmd: lint propagate-feature -p A --feature F0 --dep-kinds="build:ignore" 61 | stdout: | 62 | crate 'A' 63 | feature 'F0' 64 | must propagate to: 65 | BD 66 | Found 1 issue (run with `--fix` to fix). 67 | code: 1 68 | - cmd: lint propagate-feature -p A --feature F0 --dep-kinds="normal:ignore,build:ignore" 69 | stdout: | 70 | crate 'A' 71 | feature 'F0' 72 | must propagate to: 73 | BD 74 | Found 1 issue (run with `--fix` to fix). 75 | code: 1 76 | - cmd: lint propagate-feature -p A --feature F0 --dep-kinds="normal:ignore,dev:ignore" 77 | stdout: | 78 | crate 'A' 79 | feature 'F0' 80 | must propagate to: 81 | BD 82 | Found 1 issue (run with `--fix` to fix). 83 | code: 1 84 | - cmd: lint propagate-feature -p B --feature F0 --dep-kinds="normal:ignore,dev:ignore,build:ignore" 85 | - cmd: lint propagate-feature -p B --feature F0 --dep-kinds="normal:ignore,dev:ignore" 86 | - cmd: lint propagate-feature -p B --feature F0 87 | stdout: | 88 | crate 'B' 89 | feature 'F0' 90 | must propagate to: 91 | ND 92 | Found 1 issue (run with `--fix` to fix). 93 | code: 1 94 | - cmd: lint propagate-feature -p B --feature F0 --dep-kinds="normal:ignore" 95 | stdout: | 96 | crate 'B' 97 | feature 'F0' 98 | must propagate to: 99 | ND 100 | Found 1 issue (run with `--fix` to fix). 101 | code: 1 102 | - cmd: lint propagate-feature -p B --feature F0 --dep-kinds="dev:ignore" 103 | stdout: | 104 | crate 'B' 105 | feature 'F0' 106 | must propagate to: 107 | ND 108 | Found 1 issue (run with `--fix` to fix). 109 | code: 1 110 | - cmd: lint propagate-feature -p B --feature F0 --dep-kinds="build:ignore" 111 | stdout: | 112 | crate 'B' 113 | feature 'F0' 114 | must propagate to: 115 | ND 116 | Found 1 issue (run with `--fix` to fix). 117 | code: 1 118 | - cmd: lint propagate-feature -p B --feature F0 --dep-kinds="dev:ignore,build:ignore" 119 | stdout: | 120 | crate 'B' 121 | feature 'F0' 122 | must propagate to: 123 | ND 124 | Found 1 issue (run with `--fix` to fix). 125 | code: 1 126 | - cmd: lint propagate-feature -p B --feature F0 --dep-kinds="normal:ignore,build:ignore" 127 | stdout: | 128 | crate 'B' 129 | feature 'F0' 130 | must propagate to: 131 | ND 132 | Found 1 issue (run with `--fix` to fix). 133 | code: 1 134 | - cmd: lint propagate-feature -p C --feature F0 --dep-kinds="normal:ignore,dev:ignore,build:ignore" 135 | - cmd: lint propagate-feature -p C --feature F0 --dep-kinds="normal:ignore,build:ignore" 136 | - cmd: lint propagate-feature -p C --feature F0 --dep-kinds="normal:ignore,dev:ignore" 137 | stdout: | 138 | crate 'C' 139 | feature 'F0' 140 | must propagate to: 141 | NB 142 | Found 1 issue (run with `--fix` to fix). 143 | code: 1 144 | - cmd: lint propagate-feature -p C --feature F0 145 | stdout: | 146 | crate 'C' 147 | feature 'F0' 148 | must propagate to: 149 | NB 150 | Found 1 issue (run with `--fix` to fix). 151 | code: 1 152 | - cmd: lint propagate-feature -p C --feature F0 --dep-kinds="normal:ignore" 153 | stdout: | 154 | crate 'C' 155 | feature 'F0' 156 | must propagate to: 157 | NB 158 | Found 1 issue (run with `--fix` to fix). 159 | code: 1 160 | - cmd: lint propagate-feature -p C --feature F0 --dep-kinds="dev:ignore" 161 | stdout: | 162 | crate 'C' 163 | feature 'F0' 164 | must propagate to: 165 | NB 166 | Found 1 issue (run with `--fix` to fix). 167 | code: 1 168 | - cmd: lint propagate-feature -p C --feature F0 --dep-kinds="build:ignore" 169 | stdout: | 170 | crate 'C' 171 | feature 'F0' 172 | must propagate to: 173 | NB 174 | Found 1 issue (run with `--fix` to fix). 175 | code: 1 176 | - cmd: lint propagate-feature -p C --feature F0 --dep-kinds="dev:ignore,build:ignore" 177 | stdout: | 178 | crate 'C' 179 | feature 'F0' 180 | must propagate to: 181 | NB 182 | Found 1 issue (run with `--fix` to fix). 183 | code: 1 184 | - cmd: lint propagate-feature --feature F0 --dep-kinds="normal:ignore,dev:ignore,build:ignore" 185 | - cmd: lint propagate-feature --feature F0 --dep-kinds="normal:ignore,build:ignore" 186 | stdout: | 187 | crate 'A' 188 | feature 'F0' 189 | must propagate to: 190 | BD 191 | crate 'B' 192 | feature 'F0' 193 | must propagate to: 194 | ND 195 | Found 2 issues (run with `--fix` to fix). 196 | code: 1 197 | - cmd: lint propagate-feature --feature F0 --dep-kinds="normal:ignore,dev:ignore" 198 | stdout: | 199 | crate 'A' 200 | feature 'F0' 201 | must propagate to: 202 | BD 203 | crate 'C' 204 | feature 'F0' 205 | must propagate to: 206 | NB 207 | Found 2 issues (run with `--fix` to fix). 208 | code: 1 209 | - cmd: lint propagate-feature --feature F0 --dep-kinds="dev:ignore,build:ignore" 210 | stdout: | 211 | crate 'B' 212 | feature 'F0' 213 | must propagate to: 214 | ND 215 | crate 'C' 216 | feature 'F0' 217 | must propagate to: 218 | NB 219 | Found 2 issues (run with `--fix` to fix). 220 | code: 1 221 | - cmd: lint propagate-feature --feature F0 --dep-kinds="build:ignore" 222 | stdout: | 223 | crate 'A' 224 | feature 'F0' 225 | must propagate to: 226 | BD 227 | crate 'B' 228 | feature 'F0' 229 | must propagate to: 230 | ND 231 | crate 'C' 232 | feature 'F0' 233 | must propagate to: 234 | NB 235 | Found 3 issues (run with `--fix` to fix). 236 | code: 1 237 | - cmd: lint propagate-feature --feature F0 --dep-kinds="dev:ignore" 238 | stdout: | 239 | crate 'A' 240 | feature 'F0' 241 | must propagate to: 242 | BD 243 | crate 'B' 244 | feature 'F0' 245 | must propagate to: 246 | ND 247 | crate 'C' 248 | feature 'F0' 249 | must propagate to: 250 | NB 251 | Found 3 issues (run with `--fix` to fix). 252 | code: 1 253 | - cmd: lint propagate-feature --feature F0 --dep-kinds="normal:ignore" 254 | stdout: | 255 | crate 'A' 256 | feature 'F0' 257 | must propagate to: 258 | BD 259 | crate 'B' 260 | feature 'F0' 261 | must propagate to: 262 | ND 263 | crate 'C' 264 | feature 'F0' 265 | must propagate to: 266 | NB 267 | Found 3 issues (run with `--fix` to fix). 268 | code: 1 269 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zepter 2 | 3 | [![Rust](https://github.com/ggwpez/zepter/actions/workflows/rust.yml/badge.svg)](https://github.com/ggwpez/zepter/actions/workflows/rust.yml) 4 | [![crates.io](https://img.shields.io/crates/v/zepter.svg)](https://crates.io/crates/zepter) 5 | ![MSRV](https://img.shields.io/badge/MSRV-1.85-informational) 6 | [![docs.rs](https://img.shields.io/docsrs/zepter)](https://docs.rs/zepter/latest/zepter) 7 | 8 | Analyze, Fix and Format features in your Rust workspace. The goal of this tool is to have this CI ready to prevent common errors with Rust features. 9 | 10 | ## Install 11 | 12 | From source code: 13 | 14 | ```sh 15 | cargo install zepter -f --locked 16 | ``` 17 | 18 | As binary via [`cargo binstall`](https://github.com/cargo-bins/cargo-binstall): 19 | 20 | ```sh 21 | cargo binstall -y zepter 22 | ``` 23 | 24 | ## Commands 25 | 26 | zepter 27 | - *no args*: this is the same as `run`. 28 | - run: Run a workflow from the config file. Uses `default` if none is specified. 29 | - format 30 | - features: Format features layout and remove duplicates. 31 | - trace: Trace dependencies paths. 32 | - lint 33 | - propagate-features: Check that features are passed down. 34 | - never-enables: A feature should never enable another other. 35 | - duplicate-deps: Check that dependencies are not defined in both normal an dev sections. 36 | - never-implies *(unstable)*: A feature should never transitively imply another one. 37 | - only-enables *(unstable)*: A features should exclusively enable another one. 38 | - why-enables *(unstable)*: Find out why a specific feature is enables. 39 | - debug: *(unstable)* just for quick debugging some stuff. 40 | - transpose *(unstable)* 41 | - dependency 42 | - lift-to-workspace: Lifts crate dependencies to the workspace. 43 | 44 | ## Used By 45 | 46 | (Usage does not mean endorsement) 47 | 48 | - [alloy-rs/alloy](https://github.com/alloy-rs/alloy/blob/833f9a22ca7c30079d64554d435561877c15267a/.config/zepter.yaml) 49 | - [paradigmxyz/reth](https://github.com/paradigmxyz/reth/blob/8e7684d49db571891a265239faf8d462e92b23f0/.config/zepter.yaml) 50 | - [flashbots/rbuilder](https://github.com/flashbots/rbuilder/blob/d22c29b894702d0774e64af70d277ae7bde1da22/zepter.yaml) 51 | - [paritytech/polkadot-sdk](https://github.com/paritytech/polkadot-sdk/blob/4b83d24f4bc96a7b17964be94b178dd7b8f873b5/.config/zepter.yaml) 52 | - [chainflip-io/chainflip-backend](https://github.com/chainflip-io/chainflip-backend/blob/a9d15356dc2e41ea953baa4a0ba4c28e4f2c8b40/.zepter.yaml) 53 | 54 | ## Example - Using Workspace dependencies 55 | 56 | Currently this only works for external dependencies and has some cases where it does not work. However, all the changes 57 | that it *does* do, should be correct. 58 | 59 | You can see this in action for example [here](https://github.com/paritytech/polkadot-sdk/pull/3366) or try it out yourself. 60 | For example, pulling up all `serde*` crates to the workspace can look like this: 61 | 62 | ```bash 63 | zepter transpose dependency lift-to-workspace "regex:^serde.*" --ignore-errors 64 | ``` 65 | 66 | It will probably print that some versions are not aligned. Zepter has the default behaviour to be cautious to not accidentally 67 | update some dependencies by pulling them up. To get around this and actually do the changes, you can do: 68 | 69 | ```bash 70 | zepter transpose dependency lift-to-workspace "regex:^serde.*" --ignore-errors --fix --version-resolver=highest 71 | ``` 72 | 73 | This will try to select the "highest" `SemVer` version of each crate. 74 | 75 | ## Example - Feature Formatting 76 | 77 | To ensure that your features are in canonical formatting, just run: 78 | 79 | ```bash 80 | zepter format features 81 | # Or shorter: 82 | zepter f f 83 | ``` 84 | 85 | The output will tell you which features are missing formatting: 86 | 87 | ```pre 88 | Found 37 crates with unformatted features: 89 | polkadot-cli 90 | polkadot-runtime-common 91 | polkadot-runtime-parachains 92 | ... 93 | Run again with `--fix` to format them. 94 | ``` 95 | 96 | Re-running with `--fix`/`-f`: 97 | 98 | ```pre 99 | Found 37 crates with unformatted features: 100 | polkadot-cli 101 | polkadot-parachain 102 | polkadot-core-primitives 103 | polkadot-primitives 104 | ... 105 | Formatted 37 crates (all fixed). 106 | ``` 107 | 108 | Looking at the diff that this command produces; Zepter assumes a default line width of 80. For one-lined features they will just be padded with spaces: 109 | 110 | ```patch 111 | -default = [ 112 | - "static_assertions", 113 | -] 114 | +default = [ "static_assertions" ] 115 | ``` 116 | 117 | Entries are sorted, comments are kept and indentation is one tab for your convenience 😊 118 | 119 | ```patch 120 | - # Hi 121 | - "xcm/std", 122 | "xcm-builder/std", 123 | + # Hi 124 | + "xcm/std", 125 | ``` 126 | 127 | ## Example - Fixing feature propagation 128 | 129 | Let's check that the `runtime-benchmarks` feature is properly passed down to all the dependencies of the `frame-support` crate in the workspace of [Substrate]. You can use commit `395853ac15` to verify it yourself: 130 | 131 | ```bash 132 | zepter lint propagate-feature --feature runtime-benchmarks -p frame-support --workspace 133 | ``` 134 | 135 | The output reveals that some dependencies expose the feature but don't get it passed down: 136 | 137 | ```pre 138 | crate 'frame-support' 139 | feature 'runtime-benchmarks' 140 | must propagate to: 141 | frame-system 142 | sp-runtime 143 | sp-staking 144 | Found 3 issues and fixed 0 issues. 145 | ``` 146 | 147 | Without the `-p` it will detect many more problems. You can verify this for the [frame-support](https://github.com/paritytech/substrate/blob/ce2cee35f8f0fc5968ea6ffaffa6660dcd008804/frame/support/Cargo.toml#L71) which is indeed missing the feature for `sp-runtime` while [sp-runtime](https://github.com/paritytech/substrate/blob/0b6aec52a90870c999856cd37f7d04789cdd8dfc/primitives/runtime/Cargo.toml#L43) clearly supports it 🤔. 148 | 149 | This can be fixed by appending the `--fix` flag, which results in this diff: 150 | 151 | ```patch 152 | -runtime-benchmarks = [] 153 | +runtime-benchmarks = [ 154 | + "frame-system/runtime-benchmarks", 155 | + "sp-runtime/runtime-benchmarks", 156 | + "sp-staking/runtime-benchmarks", 157 | +] 158 | ``` 159 | 160 | The auto-fix can be configured to enable specific optional dependencies as non-optional via `--feature-enables-dep="runtime-benchmarks:frame-benchmarking"` for example. In this case the `frame-benchmarking` dependency would enabled as non-optional if the `runtime-benchmarks` feature is enabled. 161 | 162 | ## Example - Feature tracing 163 | 164 | Let's say you want to ensure that specific features are never enabled by default. For this example, we will use the `try-runtime` feature of [Substrate]. Check out branch `oty-faulty-feature-demo` and try: 165 | 166 | ```bash 167 | zepter lint never-implies --precondition default --stays-disabled try-runtime --offline --workspace 168 | ``` 169 | 170 | The `precondition` defines the feature on the left side of the implication and `stays-disabled` expressing that the precondition never enables this. 171 | 172 | Errors correctly with: 173 | ```pre 174 | Feature 'default' implies 'try-runtime' via path: 175 | frame-benchmarking/default -> frame-benchmarking/std -> frame-system/std -> frame-support/wrong -> frame-support/wrong2 -> frame-support/try-runtime 176 | ``` 177 | 178 | Only the first path is shown in case there are multiple. 179 | 180 | ## Example - Dependency tracing 181 | 182 | Recently there was a build error in the [Substrate](https://github.com/paritytech/substrate) master CI which was caused by a downstream dependency [`snow`](https://github.com/mcginty/snow/issues/146). To investigate this, it is useful to see *how* Substrate depends on it. 183 | 184 | Let's find out how `node-cli` depends on `snow` (example on commit `dd6aedee3b8d5`): 185 | 186 | ```bash 187 | zepter trace node-cli snow 188 | ``` 189 | 190 | It reports that `snow` is pulled in from libp2p - good to know. In this case, all paths are displayed. 191 | 192 | ```pre 193 | node-cli -> try-runtime-cli -> substrate-rpc-client -> sc-rpc-api -> sc-chain-spec -> sc-telemetry -> libp2p -> libp2p-webrtc -> libp2p-noise -> snow 194 | ``` 195 | 196 | ## Config Files 197 | 198 | ⚠️ the syntax for workflows is highly experimental and bound to change. 199 | 200 | The first step is that Zepter checks that it is executed in a rust workspace. Otherwise it fails directly. Then a workflow file is located as follows: 201 | 202 | - `$WORKSPACE/zepter.yaml` 203 | - `$WORKSPACE/.zepter.yaml` 204 | - `$WORKSPACE/.cargo/zepter.yaml` 205 | - `$WORKSPACE/.cargo/.zepter.yaml` 206 | - `$WORKSPACE/.config/zepter.yaml` 207 | - `$WORKSPACE/.config/.zepter.yaml` 208 | 209 | It uses the first file that is found and errors if none is found. Currently it not possible to overwrite the config in a sub-folder. 210 | 211 | ### Workflows 212 | 213 | > [!NOTE] 214 | > A production example can be found in the [Polkadot-SDK](https://github.com/paritytech/polkadot-sdk/blob/8ebb5c3319fa52d68f2d76f90f5787a96de254be/.config/zepter.yaml) or in the [`presets`](presets/polkadot.yaml). 215 | 216 | 217 | It is possible to aggregate the long commands into workflows instead of typing them each time. Zepter tries to locate a config file and run the `default` workflow when it is bare invoked without any arguments. 218 | Alternately, it is possible to use `zepter run default`, or any other workflow name. 219 | 220 | Config files can contain workflows like this: 221 | 222 | ```yaml 223 | workflows: 224 | default: 225 | - [ 'propagate-features', ... ] 226 | - ... 227 | ``` 228 | 229 | It is also possible to extend previous steps: 230 | 231 | ```yaml 232 | workflows: 233 | check: 234 | - ... 235 | default: 236 | - [ $check.0, '--fix' ] 237 | - ... 238 | ``` 239 | 240 | ## CI Usage 241 | 242 | Zepter is currently being used in the [Polkadot-SDK](https://github.com/paritytech/polkadot-sdk/pull/1194) CI to spot missing features. 243 | When these two experiments prove the usefulness and reliability of Zepter for CI application, then a more streamlined process will be introduced (possibly in the form of CI actions). 244 | 245 | ## Testing 246 | 247 | Unit tests: `cargo test` 248 | UI and downstream integration tests: `cargo test -- --ignored --nocapture` 249 | 250 | Environment overwrites exist for the UI tests to: 251 | - `OVERWRITE`: Update the UI diff locks. 252 | - `UI_FILTER`: Regex to selectively run UI test. 253 | - `KEEP_GOING`: Print `FAILED` but don't abort on the first failed UI test. 254 | 255 | ## Development Principles 256 | 257 | - Compile time is human time. Compile time should *always* be substantially below 1 minute. 258 | - Minimal external dependencies. Reduces source of errors and compile time. 259 | - Tests. So far, the tool is used since a year extensively in CI and never got a bug report. It should stay like this. 260 | 261 | 262 | [Cumulus]: https://github.com/paritytech/cumulus 263 | [Substrate]: https://github.com/paritytech/substrate 264 | -------------------------------------------------------------------------------- /src/dag.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Oliver Tale-Yazdi 3 | 4 | //! Directed Acyclic Graphs ([Dag]) and [Path]s through them. 5 | //! 6 | //! Can be used to build and trace dependencies in a rust workspace. 7 | 8 | #![allow(dead_code)] 9 | 10 | use core::fmt::{Display, Formatter}; 11 | use std::{ 12 | borrow::{Cow, ToOwned}, 13 | collections::{BTreeMap, BTreeSet}, 14 | }; 15 | 16 | /// Represents *Directed Acyclic Graph* through its edge relation. 17 | /// 18 | /// A "node" in that sense is anything on the left- or right-hand side of this relation. 19 | #[derive(Clone, serde::Serialize, serde::Deserialize)] 20 | pub struct Dag { 21 | /// Dependant -> Dependency 22 | /// eg: Polkadot -> Substrate or Me -> Rust 23 | pub edges: BTreeMap>, 24 | } 25 | 26 | impl Display for Dag 27 | where 28 | T: Display + Ord, 29 | { 30 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 31 | for (from, tos) in self.edges.iter() { 32 | for to in tos { 33 | writeln!(f, "{from} -> {to}")?; 34 | } 35 | } 36 | Ok(()) 37 | } 38 | } 39 | 40 | impl Default for Dag { 41 | fn default() -> Self { 42 | Self { edges: BTreeMap::new() } 43 | } 44 | } 45 | 46 | /// A path inside a 8Dag]. 47 | /// 48 | /// Tries to use borrowing when possible to mitigate copy overhead. Paths cannot be empty. 49 | #[derive(Ord, PartialOrd, Eq, PartialEq, Hash)] 50 | pub struct Path<'a, T: ToOwned>(pub Vec>); 51 | 52 | impl<'a, T> Path<'a, T> 53 | where 54 | T: ToOwned, 55 | { 56 | /// The number of hops (edges) in the path. 57 | /// 58 | /// This is one less than the number of nodes. 59 | pub fn num_hops(&self) -> usize { 60 | match self.num_nodes() { 61 | 0 => unreachable!("Paths cannot be empty"), 62 | l => l - 1, 63 | } 64 | } 65 | 66 | /// The number of nodes within the path. 67 | /// 68 | /// This is one less than the number of edges and never zero. 69 | pub fn num_nodes(&self) -> usize { 70 | self.0.len() 71 | } 72 | 73 | /// Translate self by applying `f` to all hops and borrowing the returned reference. 74 | pub fn translate_borrowed<'b, F, U>(&'a self, f: F) -> Path<'b, U> 75 | where 76 | F: Fn(&'a T) -> &'b U, 77 | U: ToOwned, 78 | { 79 | Path(self.0.iter().map(|e| Cow::Borrowed(f(e.as_ref()))).collect()) 80 | } 81 | 82 | /// Translate self by applying `f` to all hops and owning the returned value. 83 | pub fn translate_owned<'b, F, U>(self, f: F) -> Path<'b, U> 84 | where 85 | F: Fn(&T) -> U, 86 | U: ToOwned, 87 | { 88 | Path(self.0.into_iter().map(|e| Cow::Owned(f(e.as_ref()))).collect()) 89 | } 90 | 91 | /// Run `f` on all nodes in the path. 92 | pub fn for_each(&self, mut f: F) 93 | where 94 | F: FnMut(&T), 95 | { 96 | for e in self.0.iter() { 97 | f(e.as_ref()); 98 | } 99 | } 100 | } 101 | 102 | impl<'a, T> TryFrom> for Path<'a, T> 103 | where 104 | T: ToOwned, 105 | { 106 | type Error = (); 107 | 108 | fn try_from(v: Vec<&'a T>) -> Result { 109 | if v.is_empty() { 110 | return Err(()) 111 | } 112 | 113 | Ok(Self(v.into_iter().map(Cow::Borrowed).collect())) 114 | } 115 | } 116 | 117 | impl Display for Path<'_, T> 118 | where 119 | T: Display + ToOwned, 120 | { 121 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 122 | self.0.iter().map(|e| e.to_string()).collect::>().join(" -> ").fmt(f) 123 | } 124 | } 125 | 126 | impl Dag 127 | where 128 | T: Ord + PartialEq + Clone, 129 | { 130 | /// Create a new empty [Dag]. 131 | pub fn new() -> Self { 132 | Self::default() 133 | } 134 | 135 | /// Connect two nodes. 136 | pub fn add_edge(&mut self, from: T, to: T) { 137 | self.edges.entry(from).or_default().insert(to); 138 | } 139 | 140 | /// Add a node to the Dag without any edges. 141 | pub fn add_node(&mut self, node: T) { 142 | self.edges.entry(node).or_default(); 143 | } 144 | 145 | pub fn degree(&self, node: &T) -> usize { 146 | self.edges.get(node).map_or(0, |v| v.len()) 147 | } 148 | 149 | /// Whether `from` is directly adjacent to `to`. 150 | /// 151 | /// *Directly* means with via an edge. 152 | pub fn adjacent(&self, from: &T, to: &T) -> bool { 153 | self.edges.get(from).is_some_and(|v| v.contains(to)) 154 | } 155 | 156 | /// Whether `from` is reachable to `to` via. 157 | pub fn reachable(&self, from: &T, to: &T) -> bool { 158 | self.any_path(from, to).is_some() 159 | } 160 | 161 | /// Whether `from` appears on the lhs of the edge relation. 162 | /// 163 | /// Aka: Whether `self` has any dependencies nodes. 164 | pub fn lhs_contains(&self, from: &T) -> bool { 165 | self.edges.contains_key(from) 166 | } 167 | 168 | /// Whether `to` appears on the rhs of the edge relation. 169 | /// 170 | /// Aka: Whether any other node depends on `self`. 171 | pub fn rhs_contains(&self, to: &T) -> bool { 172 | self.edges.values().any(|v| v.contains(to)) 173 | } 174 | 175 | /// The `Dag` only containing the node `from` and its direct dependencies. 176 | /// 177 | /// This can be inflated back to the original `Dag` by calling 178 | /// `from.into_transitive_hull_in(self)`. 179 | pub fn dag_of(&self, from: T) -> Self { 180 | let mut edges = BTreeMap::new(); 181 | let rhs = self.edges.get(&from).cloned().unwrap_or_default(); 182 | edges.insert(from, rhs); 183 | Self { edges } 184 | } 185 | 186 | pub fn sub(&self, pred: impl Fn(&T) -> bool) -> Self { 187 | let mut edges = BTreeMap::new(); 188 | for (k, v) in self.edges.iter() { 189 | if pred(k) { 190 | edges.insert(k.clone(), v.clone()); 191 | } 192 | } 193 | Self { edges } 194 | } 195 | 196 | /// Get get a ref to the a LHS node. 197 | pub fn lhs_node(&self, from: &T) -> Option<&T> { 198 | self.edges.get_key_value(from).map(|(k, _)| k) 199 | } 200 | 201 | pub fn lhs_nodes(&self) -> impl Iterator { 202 | self.edges.keys() 203 | } 204 | 205 | pub fn rhs_nodes(&self) -> impl Iterator { 206 | self.edges.values().flat_map(|v| v.iter()) 207 | } 208 | 209 | pub fn inverse_lookup<'a>(&'a self, to: &'a T) -> impl Iterator { 210 | self.edges 211 | .iter() 212 | .filter_map(move |(k, v)| if v.contains(to) { Some(k) } else { None }) 213 | } 214 | 215 | /// Calculate the transitive hull of `self`. 216 | pub fn transitive_hull(&mut self) { 217 | let topology = self.clone(); 218 | self.transitive_in(&topology); 219 | } 220 | 221 | /// Calculate the transitive hull of `self` while using the connectivity of `topology`. 222 | pub fn transitive_hull_in(&mut self, topology: &Self) { 223 | self.transitive_in(topology); 224 | } 225 | 226 | /// Consume `self` and return the transitive hull. 227 | pub fn into_transitive_hull(mut self) -> Self { 228 | let topology = self.clone(); 229 | while self.transitive_in(&topology) {} 230 | self 231 | } 232 | 233 | /// Consume `self` and return the transitive hull while using the connectivity of `topology`. 234 | pub fn into_transitive_hull_in(mut self, topology: &Self) -> Self { 235 | while self.transitive_in(topology) {} 236 | self 237 | } 238 | 239 | /// Calculate the transitive hull of `self` by using the connectivity of `topology`. 240 | fn transitive_in(&mut self, topology: &Self) -> bool { 241 | let mut changed = false; 242 | // The edges that are added in this stage. 243 | let mut new_edges: BTreeMap> = BTreeMap::new(); 244 | 245 | for (k, vs) in self.edges.iter() { 246 | for v in vs { 247 | if let Some(new_deps) = topology.edges.get(v) { 248 | let edge = self.edges.get(k); 249 | for new_dep in new_deps { 250 | if !edge.unwrap().contains(new_dep) { 251 | new_edges.entry(k.clone()).or_default().insert(new_dep.clone()); 252 | changed = true; 253 | } 254 | } 255 | } 256 | } 257 | } 258 | 259 | for (k, v) in new_edges { 260 | self.edges.entry(k).or_default().extend(v); 261 | } 262 | 263 | changed 264 | } 265 | 266 | /// Find *any* path from `from` to `to`. 267 | /// 268 | /// Note that 1) *the* shortest path does not necessarily exist and 2) this function does not 269 | /// give any guarantee about the returned path. 270 | /// 271 | /// This returns `Some` if (and only if) `to` is *reachable* from `from`. 272 | pub fn any_path<'a>(&'a self, from: &'a T, to: &T) -> Option> { 273 | let mut visited = BTreeSet::new(); 274 | let mut stack = vec![(from, vec![from])]; 275 | 276 | while let Some((node, mut path)) = stack.pop() { 277 | if visited.contains(&node) { 278 | continue 279 | } 280 | visited.insert(node); 281 | if node == to { 282 | return path.try_into().ok() 283 | } 284 | if let Some(neighbors) = self.edges.get(node) { 285 | for neighbor in neighbors.iter() { 286 | path.push(neighbor); 287 | stack.push((neighbor, path.clone())); 288 | path.pop(); 289 | } 290 | } 291 | } 292 | None 293 | } 294 | 295 | /// Check if any node which fulfills `pred` can be reached from `from` and return the first 296 | /// path. 297 | pub fn reachable_predicate<'a>( 298 | &'a self, 299 | from: &'a T, 300 | pred: impl Fn(&T) -> bool, 301 | ) -> Option> { 302 | let mut visited = BTreeSet::new(); 303 | let mut stack = vec![(from, vec![from])]; 304 | 305 | while let Some((node, mut path)) = stack.pop() { 306 | if visited.contains(&node) { 307 | continue 308 | } 309 | visited.insert(node); 310 | if pred(node) { 311 | return path.try_into().ok() 312 | } 313 | if let Some(neighbors) = self.edges.get(node) { 314 | for neighbor in neighbors.iter() { 315 | path.push(neighbor); 316 | stack.push((neighbor, path.clone())); 317 | path.pop(); 318 | } 319 | } 320 | } 321 | None 322 | } 323 | 324 | /// The number of edges in the graph. 325 | pub fn num_edges(&self) -> usize { 326 | self.edges.values().map(|v| v.len()).sum() 327 | } 328 | 329 | /// The number of nodes in the graph. 330 | pub fn num_nodes(&self) -> usize { 331 | self.edges.len() 332 | } 333 | 334 | pub fn lhs_iter(&self) -> impl Iterator { 335 | self.edges.keys() 336 | } 337 | 338 | pub fn rhs_iter(&self) -> impl Iterator { 339 | self.edges.iter().flat_map(|(_, v)| v.iter()) 340 | } 341 | 342 | /// Iterate though all LHS and RHS nodes. 343 | pub fn node_iter(&self) -> impl Iterator { 344 | self.lhs_iter().chain(self.rhs_iter()) 345 | } 346 | } 347 | 348 | #[cfg(test)] 349 | mod tests { 350 | use super::*; 351 | use rstest::*; 352 | 353 | #[rstest] 354 | #[case(vec![("A", "B"), ("B", "C")], vec![("A", vec!["B", "C"]), ("B", vec!["C"])])] 355 | #[case(vec![("A", "B"), ("B", "C"), ("C", "D")], vec![("A", vec!["B", "C", "D"]), ("B", vec!["C", "D"]), ("C", vec!["D"])])] 356 | fn dag_transitive_hull_works( 357 | #[case] edges: Vec<(&str, &str)>, 358 | #[case] expected: Vec<(&str, Vec<&str>)>, 359 | ) { 360 | let mut dag = Dag::::default(); 361 | for (from, to) in edges { 362 | dag.add_edge(from.into(), to.into()); 363 | } 364 | let dag = dag.into_transitive_hull(); 365 | for (k, v) in expected { 366 | assert_eq!( 367 | dag.edges.get(k).unwrap(), 368 | &v.into_iter().map(|s| s.into()).collect::>() 369 | ); 370 | } 371 | let dag2 = dag.clone().into_transitive_hull(); 372 | assert_eq!(dag.num_edges(), dag2.num_edges()); 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Oliver Tale-Yazdi 3 | 4 | //! Sub-command definition and implementation. 5 | 6 | pub mod debug; 7 | pub mod fmt; 8 | pub mod lint; 9 | pub mod run; 10 | pub mod trace; 11 | pub mod transpose; 12 | 13 | use crate::{log, ErrToStr}; 14 | 15 | use cargo_metadata::{Dependency, Metadata, MetadataCommand, Package, Resolve}; 16 | use std::{fs::canonicalize, path::Path}; 17 | 18 | /// See out how Rust dependencies and features are enabled. 19 | #[derive(Debug, clap::Parser)] 20 | #[command(author, version, about, long_about = None)] 21 | pub struct Command { 22 | #[clap(subcommand)] 23 | subcommand: Option, 24 | 25 | #[clap(flatten)] 26 | global: GlobalArgs, 27 | } 28 | 29 | #[derive(Debug, clap::Parser)] 30 | pub struct GlobalArgs { 31 | /// Only print errors. Supersedes `--log`. 32 | #[clap(long, short, global = true)] 33 | quiet: bool, 34 | 35 | /// Log level to use. 36 | #[cfg(feature = "logging")] 37 | #[clap(long = "log", global = true, default_value = "info", ignore_case = true)] 38 | level: ::log::LevelFilter, 39 | 40 | /// LOGGING IS DISABLED IN THIS BUILD; FLAG IGNORED. 41 | #[cfg(not(feature = "logging"))] 42 | #[clap(long = "log", global = true, default_value = "info", ignore_case = true)] 43 | level: String, 44 | 45 | /// Use ANSI terminal colors. 46 | #[clap(long, global = true, default_value_t = false)] 47 | color: bool, 48 | 49 | /// Try to exit with code zero if the intended check failed. 50 | /// 51 | /// Will still return != 0 in case of an actual error (eg. failed to find some file) or a panic 52 | /// (aka software bug). 53 | #[clap(long, global = true, verbatim_doc_comment)] 54 | exit_code_zero: bool, 55 | 56 | /// Dont print any hints on how to fix the error. 57 | /// 58 | /// This is mostly used internally when dispatching, workflows since they come with their 59 | /// hints. 60 | #[clap(long, global = true, value_enum, verbatim_doc_comment, default_value_t = FixHint::On)] 61 | fix_hint: FixHint, 62 | } 63 | 64 | /// Sub-commands of the [Root](Command) command. 65 | #[derive(Debug, clap::Subcommand)] 66 | enum SubCommand { 67 | Trace(trace::TraceCmd), 68 | Lint(lint::LintCmd), 69 | #[clap(alias = "fmt", alias = "f")] 70 | Format(fmt::FormatCmd), 71 | Run(run::RunCmd), 72 | #[clap(hide = true)] 73 | Transpose(transpose::TransposeCmd), 74 | Debug(debug::DebugCmd), 75 | } 76 | 77 | /// A hint on how to fix the error. 78 | #[derive(Debug, Clone, PartialEq, clap::ValueEnum)] 79 | pub enum FixHint { 80 | /// Prints some hint that is (hopefully) helpful. 81 | On, 82 | /// Prints no hint at all. 83 | Off, 84 | // NOTE: We could have a `Custom` here one day 85 | } 86 | 87 | impl Command { 88 | pub fn run(&self) -> Result<(), String> { 89 | self.global.setup_logging(); 90 | 91 | match self.subcommand.as_ref() { 92 | Some(SubCommand::Transpose(cmd)) => cmd.run(&self.global), 93 | 94 | Some(SubCommand::Trace(cmd)) => { 95 | cmd.run(&self.global); 96 | Ok(()) 97 | }, 98 | Some(SubCommand::Lint(cmd)) => cmd.run(&self.global), 99 | Some(SubCommand::Format(cmd)) => { 100 | cmd.run(&self.global); 101 | Ok(()) 102 | }, 103 | Some(SubCommand::Run(cmd)) => { 104 | cmd.run(&self.global); 105 | Ok(()) 106 | }, 107 | Some(SubCommand::Debug(cmd)) => { 108 | cmd.run(&self.global); 109 | Ok(()) 110 | }, 111 | None => { 112 | run::RunCmd::default().run(&self.global); 113 | Ok(()) 114 | }, 115 | } 116 | } 117 | } 118 | 119 | impl GlobalArgs { 120 | pub fn setup_logging(&self) { 121 | #[cfg(feature = "logging")] 122 | if self.quiet { 123 | ::log::set_max_level(::log::LevelFilter::Error); 124 | } else { 125 | ::log::set_max_level(self.level); 126 | } 127 | } 128 | 129 | pub fn warn_unstable(&self) { 130 | log::warn!("Unstable feature - do not rely on this!"); 131 | } 132 | pub fn show_hints(&self) -> bool { 133 | // We use exahausive match to make sure to not forget a new variant in the future. 134 | match self.fix_hint { 135 | FixHint::On => true, 136 | FixHint::Off => false, 137 | } 138 | } 139 | 140 | pub fn error_code(&self) -> i32 { 141 | if self.exit_code_zero { 142 | 0 143 | } else { 144 | 1 145 | } 146 | } 147 | 148 | /// Error code when the config file is invalid or incompatible. 149 | pub fn error_code_cfg_parsing() -> i32 { 150 | 101 151 | } 152 | 153 | pub fn red(&self, s: &str) -> String { 154 | if !self.color { 155 | s.to_string() 156 | } else { 157 | format!("\x1b[31m{s}\x1b[0m") 158 | } 159 | } 160 | 161 | pub fn yellow(&self, s: &str) -> String { 162 | if !self.color { 163 | s.to_string() 164 | } else { 165 | format!("\x1b[33m{s}\x1b[0m") 166 | } 167 | } 168 | 169 | pub fn green(&self, s: &str) -> String { 170 | if !self.color { 171 | s.to_string() 172 | } else { 173 | format!("\x1b[32m{s}\x1b[0m") 174 | } 175 | } 176 | 177 | pub fn bold(&self, s: &str) -> String { 178 | if !self.color { 179 | s.to_string() 180 | } else { 181 | format!("\x1b[1m{s}\x1b[0m") 182 | } 183 | } 184 | } 185 | 186 | /// Arguments for how to load cargo metadata from a workspace. 187 | #[derive(Debug, Clone, clap::Parser, PartialEq)] 188 | pub struct CargoArgs { 189 | /// Cargo manifest path or directory. 190 | /// 191 | /// For directories it appends a `Cargo.toml`. 192 | #[arg(long, global = true)] 193 | pub manifest_path: Option, 194 | 195 | /// Whether to only consider workspace crates. 196 | #[clap(long, global = true)] 197 | pub workspace: bool, 198 | 199 | /// Whether to use offline mode. 200 | #[clap(long, global = true)] 201 | pub offline: bool, 202 | 203 | /// Whether to use all the locked dependencies from the `Cargo.lock`. 204 | /// 205 | /// Otherwise it may update some dependencies. For CI usage its a good idea to use it. 206 | #[clap(long, global = true)] 207 | pub locked: bool, 208 | 209 | #[clap(long, global = true)] 210 | pub all_features: bool, 211 | } 212 | 213 | impl CargoArgs { 214 | pub fn with_workspace(mut self, workspace: bool) -> Self { 215 | self.workspace = workspace; 216 | self 217 | } 218 | 219 | /// Load the metadata of the rust project. 220 | pub fn load_metadata(&self) -> Result { 221 | let err = match self.load_metadata_unsorted() { 222 | Ok(meta) => return Ok(meta), 223 | Err(err) => err, 224 | }; 225 | 226 | if check_for_locked_error(&err) { 227 | Err("\nThe Cargo.lock file needs to be updated first since --locked is present.\n" 228 | .to_string()) 229 | } else { 230 | Err(err) 231 | } 232 | } 233 | 234 | fn load_metadata_unsorted(&self) -> Result { 235 | let mut cmd = MetadataCommand::new(); 236 | 237 | if let Some(ref manifest_path) = self.manifest_path { 238 | let manifest_path = if manifest_path.is_dir() { 239 | manifest_path.join("Cargo.toml") 240 | } else { 241 | manifest_path.clone() 242 | }; 243 | cmd.manifest_path(&manifest_path); 244 | } 245 | 246 | cmd.features(cargo_metadata::CargoOpt::AllFeatures); 247 | 248 | if self.workspace { 249 | cmd.no_deps(); 250 | } 251 | if self.offline { 252 | cmd.other_options(vec!["--offline".to_string()]); 253 | } 254 | if self.locked { 255 | cmd.other_options(vec!["--locked".to_string()]); 256 | } 257 | 258 | cmd.exec().map_err(|e| format!("Failed to load metadata: {e}")) 259 | } 260 | } 261 | 262 | fn check_for_locked_error(err: &str) -> bool { 263 | err.contains("needs to be updated but --locked was passed to prevent this") 264 | } 265 | 266 | /// Resolve the dependency `dep` of `pkg` within the metadata. 267 | /// 268 | /// This checks whether the dependency is a workspace or external crate and resolves it accordingly. 269 | pub(crate) fn resolve_dep( 270 | pkg: &Package, 271 | dep: &Dependency, 272 | meta: &Metadata, 273 | ) -> Option { 274 | match meta.resolve.as_ref() { 275 | Some(resolve) => resolve_dep_from_graph(pkg, dep, (meta, resolve)), 276 | None => resolve_dep_from_workspace(dep, meta), 277 | } 278 | } 279 | 280 | /// Resolve the dependency `dep` within the workspace. 281 | /// 282 | /// Errors if `dep` is not a workspace member. 283 | pub(crate) fn resolve_dep_from_workspace( 284 | dep: &Dependency, 285 | meta: &Metadata, 286 | ) -> Option { 287 | for work in meta.workspace_packages() { 288 | if work.name.to_string() == dep.name { 289 | let pkg = meta.packages.iter().find(|pkg| pkg.id == work.id).cloned(); 290 | return pkg.map(|pkg| RenamedPackage::new(pkg, dep.rename.clone(), dep.optional)) 291 | } 292 | } 293 | None 294 | } 295 | 296 | /// Resolve the dependency `dep` of `pkg` within the resolve graph. 297 | /// 298 | /// The resolve graph should only be used for external crates. I did not try what happens for 299 | /// workspace members - better don't do it. 300 | pub(crate) fn resolve_dep_from_graph( 301 | pkg: &Package, 302 | dep: &Dependency, 303 | (meta, resolve): (&Metadata, &Resolve), 304 | ) -> Option { 305 | let dep_name = dep.rename.clone().unwrap_or(dep.name.clone()).replace('-', "_"); 306 | let resolved_pkg = resolve.nodes.iter().find(|node| node.id == pkg.id)?; 307 | let resolved_dep_id = 308 | resolved_pkg.deps.iter().find(|node| node.name.to_string() == dep_name)?; 309 | let resolve_dep = meta.packages.iter().find(|pkg| pkg.id == resolved_dep_id.pkg)?; 310 | 311 | Some(RenamedPackage::new(resolve_dep.clone(), dep.rename.clone(), dep.optional)) 312 | } 313 | 314 | #[derive(Clone, Debug, PartialEq, Eq)] 315 | pub struct RenamedPackage { 316 | pub pkg: Package, 317 | pub rename: Option, 318 | pub optional: bool, 319 | } 320 | 321 | impl RenamedPackage { 322 | pub fn new(pkg: Package, rename: Option, optional: bool) -> Self { 323 | Self { pkg, rename, optional } 324 | } 325 | 326 | pub fn name(&self) -> String { 327 | self.rename.clone().unwrap_or(self.pkg.name.to_string()) 328 | } 329 | 330 | pub fn original_name(&self) -> String { 331 | self.pkg.name.to_string() 332 | } 333 | 334 | pub fn display_name(&self) -> String { 335 | match &self.rename { 336 | Some(rename) => format!("{} (renamed from {})", rename, self.pkg.name), 337 | None => self.pkg.name.to_string(), 338 | } 339 | } 340 | } 341 | 342 | impl Ord for RenamedPackage { 343 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 344 | // Yikes... dafuq is this?! 345 | //bincode::serialize(self).unwrap().cmp(&bincode::serialize(other).unwrap()) 346 | 347 | (&self.pkg.name, &self.pkg.id).cmp(&(&other.pkg.name, &other.pkg.id)) 348 | } 349 | } 350 | 351 | impl PartialOrd for RenamedPackage { 352 | fn partial_cmp(&self, other: &Self) -> Option { 353 | Some(self.cmp(other)) 354 | } 355 | } 356 | 357 | /// Parse a single key-value pair 358 | /// 359 | /// Copy & paste from 360 | pub(crate) fn parse_key_val( 361 | s: &str, 362 | ) -> Result<(T, U), Box> 363 | where 364 | T: std::str::FromStr, 365 | T::Err: std::error::Error + Send + Sync + 'static, 366 | U: std::str::FromStr, 367 | U::Err: std::error::Error + Send + Sync + 'static, 368 | { 369 | let s = s.trim_matches('"'); 370 | let pos = s.find(':').ok_or_else(|| format!("invalid KEY=value: no `:` found in `{s}`"))?; 371 | Ok((s[..pos].parse()?, s[pos + 1..].parse()?)) 372 | } 373 | 374 | pub(crate) fn check_can_modify>(root: P, modify: P) -> Result { 375 | let root = canonicalize(root).err_to_str()?; 376 | let modify = canonicalize(modify).err_to_str()?; 377 | 378 | if !modify.starts_with(&root) { 379 | println!( 380 | "Path is outside of the workspace: {:?} (not in {:?})", 381 | modify.display(), 382 | root.display() 383 | ); 384 | Ok(false) 385 | } else { 386 | Ok(true) 387 | } 388 | } 389 | --------------------------------------------------------------------------------