11#! /usr/bin/env bash
2- # dev-check.sh — full Rust quality gate
2+ # dev-check.sh — full Rust quality gate (macOS)
33# Runs: fmt · fix · clippy (pedantic+nursery) · tests · audit · deny · dupes
44# Produces per-file clustered clippy reports in clippy_reports/
55
@@ -8,10 +8,21 @@ set -Eeuo pipefail
88# ─── Colours ──────────────────────────────────────────────────────────────────
99
1010if [[ -t 1 ]]; then
11- RED=' \033[0;31m' ; YELLOW=' \033[0;33m' ; GREEN=' \033[0;32m'
12- CYAN=' \033[0;36m' ; BOLD=' \033[1m' ; DIM=' \033[2m' ; RESET=' \033[0m'
11+ RED=' \033[0;31m'
12+ YELLOW=' \033[0;33m'
13+ GREEN=' \033[0;32m'
14+ CYAN=' \033[0;36m'
15+ BOLD=' \033[1m'
16+ DIM=' \033[2m'
17+ RESET=' \033[0m'
1318else
14- RED=' ' ; YELLOW=' ' ; GREEN=' ' ; CYAN=' ' ; BOLD=' ' ; DIM=' ' ; RESET=' '
19+ RED=' '
20+ YELLOW=' '
21+ GREEN=' '
22+ CYAN=' '
23+ BOLD=' '
24+ DIM=' '
25+ RESET=' '
1526fi
1627
1728# ─── Globals ──────────────────────────────────────────────────────────────────
@@ -29,7 +40,9 @@ SUMMARY_FILE="$REPORT_DIR/summary.txt"
2940
3041# ─── Helpers ──────────────────────────────────────────────────────────────────
3142
32- command_exists () { command -v " $1 " > /dev/null 2>&1 ; }
43+ has_cmd () {
44+ type -P " $1 " > /dev/null 2>&1
45+ }
3346
3447step () {
3548 echo " "
@@ -38,44 +51,52 @@ step() {
3851
3952pass () {
4053 echo -e " ${GREEN} ✓${RESET} $1 "
41- (( PASS_COUNT++ )) || true
54+ PASS_COUNT= $ (( PASS_COUNT + 1 ))
4255}
4356
4457fail () {
4558 echo -e " ${RED} ✗${RESET} $1 "
46- (( FAIL_COUNT++ )) || true
59+ FAIL_COUNT= $ (( FAIL_COUNT + 1 ))
4760 FAILED_STEPS+=(" $1 " )
4861}
4962
5063skip () {
5164 echo -e " ${DIM} –${RESET} $1 ${DIM} (skipped — tool not installed)${RESET} "
52- (( SKIP_COUNT++ )) || true
65+ SKIP_COUNT= $ (( SKIP_COUNT + 1 ))
5366}
5467
55- warn () { echo -e " ${YELLOW} ⚠${RESET} $1 " ; }
56-
57- require_tool () {
58- local tool=" $1 " install_hint=" $2 "
59- if ! command_exists " $tool " ; then
60- echo -e " ${RED} Error:${RESET} required tool '${BOLD} $tool ${RESET} ' is not installed."
61- echo -e " Install with: ${DIM} $install_hint ${RESET} "
62- exit 1
63- fi
68+ warn () {
69+ echo -e " ${YELLOW} ⚠${RESET} $1 "
6470}
6571
66- optional_tool () {
67- local tool=" $1 " install_hint=" $2 "
68- if ! command_exists " $tool " ; then
69- warn " '$tool ' not installed — step will be skipped."
70- warn " Install with: $install_hint "
71- fi
72+ die () {
73+ echo -e " ${RED} Error:${RESET} $1 "
74+ echo -e " Install with: ${DIM} $2 ${RESET} "
75+ exit 1
7276}
7377
7478elapsed () {
7579 local secs=$(( SECONDS - SCRIPT_START ))
7680 printf ' %dm%02ds' $(( secs / 60 )) $(( secs % 60 ))
7781}
7882
83+ # ─── Strip cargo noise from clippy output ─────────────────────────────────────
84+
85+ filter_clippy () {
86+ grep -Ev \
87+ ' ^[[:space:]]*(Compiling|Checking|Downloading|Updating|Fresh|Finished|Blocking|Locking|Dirty|Scraping|Running|Doctest)[[:space:]]' \
88+ | grep -Ev \
89+ ' ^[[:space:]]*= note: `#\[' \
90+ | grep -Ev \
91+ ' ^warning: [0-9]+ warning(s)? emitted' \
92+ | grep -Ev \
93+ ' ^error: aborting due to' \
94+ | grep -Ev \
95+ ' ^[[:space:]]*= note: for more information' \
96+ | sed ' /^[[:space:]]*$/d' \
97+ || true
98+ }
99+
79100# ─── Header ───────────────────────────────────────────────────────────────────
80101
81102echo -e " ${BOLD} "
@@ -84,30 +105,44 @@ echo "║ Rust Full Quality Gate Check ║"
84105echo " ╚══════════════════════════════════════════════════╝"
85106echo -e " ${RESET} "
86107
87- # ─── CPU cores ────────────── ──────────────────────────────────────────────────
108+ # ─── CPU cores (macOS-aware) ──────────────────────────────────────────────────
88109
89- if command_exists sysctl; then
90- export CARGO_BUILD_JOBS; CARGO_BUILD_JOBS=$( sysctl -n hw.ncpu 2> /dev/null || echo 4)
91- elif command_exists nproc; then
92- export CARGO_BUILD_JOBS; CARGO_BUILD_JOBS=$( nproc)
93- else
94- export CARGO_BUILD_JOBS=4
95- fi
110+ export CARGO_BUILD_JOBS
111+ CARGO_BUILD_JOBS=$( sysctl -n hw.logicalcpu 2> /dev/null || echo 4)
96112echo -e " ${DIM} Using ${BOLD}${CARGO_BUILD_JOBS}${RESET}${DIM} CPU cores${RESET} "
97113
98114# ─── Required tools ───────────────────────────────────────────────────────────
99115
100116step " Verifying required tools"
101117
102- require_tool " cargo" " https://rustup.rs"
103- require_tool " rustfmt" " rustup component add rustfmt"
104- require_tool " clippy-driver" " rustup component add clippy"
118+ if ! has_cmd cargo; then
119+ die " required tool 'cargo' is not installed." " https://rustup.rs"
120+ fi
121+
122+ if ! has_cmd rustfmt; then
123+ die " required tool 'rustfmt' is not installed." " rustup component add rustfmt"
124+ fi
125+
126+ # clippy-driver is not always on PATH on macOS — check via cargo subcommand instead
127+ if ! cargo clippy --version > /dev/null 2>&1 ; then
128+ die " required tool 'clippy' is not installed." " rustup component add clippy"
129+ fi
130+
105131pass " cargo · rustfmt · clippy all present"
106132
107- optional_tool " cargo-audit" " cargo install cargo-audit"
108- optional_tool " cargo-deny" " cargo install cargo-deny"
109- optional_tool " cargo-udeps" " cargo install cargo-udeps"
110- optional_tool " cargo-msrv" " cargo install cargo-msrv"
133+ # Optional tools — warn but do not exit
134+ if ! has_cmd cargo-audit; then
135+ warn " 'cargo-audit' not installed — step will be skipped. cargo install cargo-audit"
136+ fi
137+ if ! has_cmd cargo-deny; then
138+ warn " 'cargo-deny' not installed — step will be skipped. cargo install cargo-deny"
139+ fi
140+ if ! has_cmd cargo-udeps; then
141+ warn " 'cargo-udeps' not installed — step will be skipped. cargo install cargo-udeps"
142+ fi
143+ if ! has_cmd cargo-msrv; then
144+ warn " 'cargo-msrv' not installed — step will be skipped. cargo install cargo-msrv"
145+ fi
111146
112147# ─── Prepare report directory ─────────────────────────────────────────────────
113148
@@ -118,7 +153,11 @@ mkdir -p "$CLUSTER_DIR"
118153
119154if [[ " ${1:- } " == " --update" ]]; then
120155 step " Updating dependency index"
121- if cargo update 2>&1 ; then pass " cargo update" ; else fail " cargo update" ; fi
156+ if cargo update 2>&1 ; then
157+ pass " cargo update"
158+ else
159+ fail " cargo update"
160+ fi
122161fi
123162
124163# ─── 1. Format ────────────────────────────────────────────────────────────────
131170 fail " cargo fmt --all"
132171fi
133172
134- # Verify nothing was left dirty (useful in CI)
135173if git diff --quiet 2> /dev/null; then
136174 pass " No unstaged format changes"
137175else
153191step " 3 · Lint (cargo clippy — pedantic + nursery)"
154192
155193CLIPPY_FLAGS=(
156- # Hard errors
157194 " -D" " warnings"
158-
159- # Pedantic: correctness, performance, style improvements
160195 " -W" " clippy::pedantic"
161-
162- # Nursery: newer lints, some may be noisy — catches subtle bugs early
163196 " -W" " clippy::nursery"
164-
165- # Catch common correctness bugs missed by the default set
166197 " -W" " clippy::correctness"
167198 " -W" " clippy::suspicious"
168199 " -W" " clippy::complexity"
169200 " -W" " clippy::perf"
170-
171- # Panic/unwrap hygiene — forces explicit error handling
172201 " -W" " clippy::unwrap_used"
173202 " -W" " clippy::expect_used"
174203 " -W" " clippy::panic"
175204 " -W" " clippy::todo"
176205 " -W" " clippy::unimplemented"
177206 " -W" " clippy::unreachable"
178-
179- # Index panic risk
180207 " -W" " clippy::indexing_slicing"
181-
182- # Integer overflow in casts
183208 " -W" " clippy::cast_possible_truncation"
184209 " -W" " clippy::cast_possible_wrap"
185210 " -W" " clippy::cast_sign_loss"
186211 " -W" " clippy::cast_precision_loss"
187-
188- # Arithmetic that can panic
189212 " -W" " clippy::arithmetic_side_effects"
190-
191- # Formatting / style discipline
192213 " -W" " clippy::format_collect"
193214 " -W" " clippy::uninlined_format_args"
194215 " -W" " clippy::redundant_closure_for_method_calls"
@@ -209,6 +230,7 @@ CLIPPY_CMD=(
209230 cargo clippy
210231 --all-targets
211232 --all-features
233+ --message-format=short
212234 --
213235 " ${CLIPPY_FLAGS[@]} "
214236)
@@ -217,14 +239,17 @@ echo -e " ${DIM}Running: ${CLIPPY_CMD[*]}${RESET}"
217239echo " "
218240
219241CLIPPY_EXIT=0
220- " ${CLIPPY_CMD[@]} " 2>&1 | tee " $RAW_FILE " || CLIPPY_EXIT=$?
242+ " ${CLIPPY_CMD[@]} " 2>&1 \
243+ | filter_clippy \
244+ | tee " $RAW_FILE " \
245+ || CLIPPY_EXIT=${PIPESTATUS[0]}
221246
222- # ── Cluster clippy output by source file ─────────────────────────────────────
247+ # ── Cluster clippy output by source file ──────────────────────────────────────
223248
224249echo " "
225250echo -e " ${DIM} Clustering clippy output by file...${RESET} "
226251
227- OUTFILE =" "
252+ CURRENT_OUTFILE =" "
228253while IFS= read -r line; do
229254 if [[ $line =~ ([a-zA-Z0-9_/.-]+\. rs):[0-9]+:[0-9]+ ]]; then
230255 FILE=" ${BASH_REMATCH[1]} "
@@ -234,16 +259,16 @@ while IFS= read -r line; do
234259 else
235260 CLUSTER=$( echo " $DIR " | tr ' /' ' _' )
236261 fi
237- OUTFILE =" $CLUSTER_DIR /${CLUSTER} .txt"
262+ CURRENT_OUTFILE =" $CLUSTER_DIR /${CLUSTER} .txt"
238263 {
239264 echo " "
240265 echo " ----------------------------------------"
241266 echo " FILE: $FILE "
242267 echo " ----------------------------------------"
243- } >> " $OUTFILE "
268+ } >> " $CURRENT_OUTFILE "
244269 fi
245- if [[ -n " $OUTFILE " ]]; then
246- echo " $line " >> " $OUTFILE "
270+ if [[ -n " $CURRENT_OUTFILE " ]]; then
271+ echo " $line " >> " $CURRENT_OUTFILE "
247272 fi
248273done < " $RAW_FILE "
249274
@@ -273,7 +298,6 @@ TEST_EXIT=0
273298cargo test --all --all-features 2>&1 || TEST_EXIT=$?
274299
275300if [[ $TEST_EXIT -eq 0 ]]; then
276- PASSED=$( grep -oP ' \d+(?= passed)' <<< " $(cargo test --all --all-features 2>&1)" | tail -1 || echo " ?" )
277301 pass " All tests passed"
278302else
279303 fail " Test suite failed (exit $TEST_EXIT )"
283307
284308step " 5 · Security audit (cargo audit)"
285309
286- if command_exists cargo-audit; then
310+ if has_cmd cargo-audit; then
287311 AUDIT_EXIT=0
288312 cargo audit 2>&1 || AUDIT_EXIT=$?
289313 if [[ $AUDIT_EXIT -eq 0 ]]; then
299323
300324step " 6 · Dependency policy (cargo deny)"
301325
302- if command_exists cargo-deny; then
326+ if has_cmd cargo-deny; then
303327 DENY_EXIT=0
304328 cargo deny check 2>&1 || DENY_EXIT=$?
305329 if [[ $DENY_EXIT -eq 0 ]]; then
315339
316340step " 7 · Unused dependencies (cargo udeps)"
317341
318- if command_exists cargo-udeps; then
342+ if has_cmd cargo-udeps; then
319343 UDEPS_EXIT=0
320344 cargo +nightly udeps --all-targets 2>&1 || UDEPS_EXIT=$?
321345 if [[ $UDEPS_EXIT -eq 0 ]]; then
331355
332356step " 8 · Minimum supported Rust version (cargo msrv)"
333357
334- if command_exists cargo-msrv; then
358+ if has_cmd cargo-msrv; then
335359 MSRV_EXIT=0
336360 cargo msrv verify 2>&1 || MSRV_EXIT=$?
337361 if [[ $MSRV_EXIT -eq 0 ]]; then
@@ -350,8 +374,8 @@ step "9 · Duplicate dependencies (cargo tree -d)"
350374DUPES=$( cargo tree -d 2>&1 || true)
351375if echo " $DUPES " | grep -q ' \[' ; then
352376 warn " Duplicate crate versions detected:"
353- echo " $DUPES " | grep ' ^\[' | sort -u | while read -r line; do
354- echo -e " ${YELLOW} $line ${RESET} "
377+ echo " $DUPES " | grep ' ^\[' | sort -u | while IFS= read -r line; do
378+ echo -e " ${YELLOW}${ line} ${RESET} "
355379 done
356380else
357381 pass " No duplicate crate versions"
@@ -382,7 +406,9 @@ TOTAL_SECS=$(( SECONDS - SCRIPT_START ))
382406 if [[ ${# FAILED_STEPS[@]} -gt 0 ]]; then
383407 echo " "
384408 echo " Failed steps:"
385- for s in " ${FAILED_STEPS[@]} " ; do echo " - $s " ; done
409+ for s in " ${FAILED_STEPS[@]} " ; do
410+ echo " - $s "
411+ done
386412 fi
387413} | tee " $SUMMARY_FILE "
388414
407433 echo " "
408434 echo -e " ${DIM} Reports saved to: $REPORT_DIR /${RESET} "
409435 exit 1
410- fi
436+ fi
0 commit comments