-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathMakefile
More file actions
1229 lines (1089 loc) · 56.9 KB
/
Copy pathMakefile
File metadata and controls
1229 lines (1089 loc) · 56.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
MAKE_MAJOR_VER := $(shell echo $(MAKE_VERSION) | cut -d'.' -f1)
ifneq ($(shell test $(MAKE_MAJOR_VER) -gt 3; echo $$?),0)
$(error Make version $(MAKE_VERSION) is not supported, please install GNU Make 4.x)
endif
# Strict shell and Make settings for robust recipes
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
.DEFAULT_GOAL := help
# Directory path of Makefile
BASE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
# Path to the Python bindings crate. Absolute path so the recipes
# below resolve correctly even when Make is invoked from a nested
# directory. BASE_DIR ends in a trailing slash (from $(dir ...))
# which absorbs into the concatenation; BCA_PY_DIR has no trailing
# slash and that's intended (matches how cargo / maturin invocations
# read it).
BCA_PY_DIR := $(BASE_DIR)big-code-analysis-py
# Directories excluded from linting and file-search operations.
# `tests/repositories` holds vendored fixtures (incl. the
# big-code-analysis-output submodule); `tree-sitter-*` are vendored
# grammar crates that follow upstream conventions; `enums/` is excluded
# from the workspace and owns its own files. None of these may be
# reformatted by this project's tooling. Glob entries (e.g. `tree-sitter-*`)
# are quoted at the use site below — keep this list plain.
EXCLUDE_DIRS := .claude .git target tests/repositories \
big-code-analysis-book/book \
tree-sitter-* enums
# File finder: prefer fd/fdfind (fast, .gitignore-aware), fall back to find
FD := $(shell command -v fdfind 2>/dev/null || command -v fd 2>/dev/null)
# Precomputed exclusion flags for fd and find. Single-quote each entry so
# the recipe shell does not glob-expand patterns like `tree-sitter-*` into
# absolute paths before fd/find see them (see #160). `find -path` uses its
# own glob engine and accepts unquoted patterns the same way fd does.
FD_EXCLUDE := $(foreach dir,$(EXCLUDE_DIRS),--exclude '$(dir)')
FIND_EXCLUDE := $(foreach dir,$(EXCLUDE_DIRS),! -path './$(dir)/*')
# Find files by extension with fd (preferred) or find (fallback).
# Usage: $(call find-by-ext,EXTENSION,EXTRA_FD_ARGS). Always pass the
# second arg (empty if unused) to avoid --warn-undefined-variables
# warnings on `$(2)`, e.g. $(call find-by-ext,md,).
find-by-ext = $(if $(FD),$(FD) --extension $(1) $(FD_EXCLUDE) $(2),find . -name "*.$(1)" -type f $(FIND_EXCLUDE))
.PHONY: help check-tools build build-release check test test-doc fmt fmt-check markdown-fmt markdown-lint shellcheck sh-fmt sh-fmt-check toml-fmt toml-fmt-check toml-lint makefile-check actionlint snapshot-anchors grammar-marker-sync grammar-marker-sync-test check-versions check-manpage-assets enums-check enums-codegen-drift enums-codegen-drift-test self-scan self-scan-headroom self-scan-write-baseline self-scan-write-baseline-headroom vcs lint clippy udeps insta-review insta-accept clean distclean install install-cli install-web doc doc-open doc-check book book-serve book-deploy all pre-commit ci release-check verify-changelog pkg-deb-local pkg-rpm-local py-bootstrap py-sync py-relock py-clean py-fmt py-fmt-check py-lint py-typecheck py-test py-stubtest _check-find _pc-fmt _pc-clippy _pc-test _pc-doc-check _pc-udeps _pc-shellcheck _pc-markdown-lint _pc-toml-lint _pc-makefile-check _pc-actionlint _pc-snapshot-anchors _pc-grammar-marker-sync _pc-grammar-marker-sync-test _pc-check-versions _pc-check-versions-test _pc-check-grammar-crate-test _pc-check-manpage-assets _pc-enums-check _pc-enums-codegen-drift _pc-enums-codegen-drift-test _pc-self-scan _pc-self-scan-headroom _pc-py-fmt _pc-py-typecheck _pc-py-test _pc-py-stubtest _ci-fmt-check _ci-clippy _ci-test _ci-doc-check _ci-build _ci-udeps _ci-shellcheck _ci-markdown-lint _ci-toml-lint _ci-makefile-check _ci-actionlint _ci-snapshot-anchors _ci-grammar-marker-sync _ci-grammar-marker-sync-test _ci-check-versions _ci-check-versions-test _ci-check-grammar-crate-test _ci-check-manpage-assets _ci-enums-check _ci-enums-codegen-drift _ci-enums-codegen-drift-test _ci-enums-codegen-drift-test _ci-self-scan _ci-self-scan-headroom _ci-cargo-pipeline _ci-py-fmt-check _ci-py-lint _ci-py-typecheck _ci-py-test _ci-py-stubtest
# Default target
help:
@echo "Build and test commands for big-code-analysis"
@echo ""
@echo "Usage: make <target>"
@echo ""
@echo "Prerequisites:"
@echo " check-tools Verify required tools are present"
@echo ""
@echo "Build targets:"
@echo " build Build debug binaries"
@echo " build-release Build optimized release binaries"
@echo " check Run cargo check"
@echo ""
@echo "Test targets:"
@echo " test Run unit and integration tests"
@echo " test-doc Run cargo doc tests"
@echo " insta-review Review pending insta snapshot diffs"
@echo " insta-accept Accept all pending insta snapshots"
@echo ""
@echo "Code quality:"
@echo " fmt Format Rust + Markdown + TOML + Bash"
@echo " fmt-check Verify formatting without modifying files"
@echo " clippy Run clippy with -D warnings"
@echo " udeps Detect unused deps (requires nightly)"
@echo " markdown-fmt Auto-fix Markdown with rumdl"
@echo " markdown-lint Lint Markdown with rumdl"
@echo " shellcheck Lint bash scripts"
@echo " sh-fmt Format bash scripts with shfmt"
@echo " sh-fmt-check Check bash formatting without modifying"
@echo " toml-fmt Format TOML files with taplo"
@echo " toml-fmt-check Check TOML formatting without modifying"
@echo " toml-lint Lint TOML files with taplo"
@echo " makefile-check Lint Makefile with checkmake"
@echo " actionlint Lint GitHub Actions workflows with actionlint"
@echo " snapshot-anchors Block new bare insta snapshots"
@echo " grammar-marker-sync Block grammar-marker bumps without source regen"
@echo " grammar-marker-sync-test Self-tests for the grammar-marker-sync gate"
@echo " check-versions Enforce lockstep version invariant across owned crates"
@echo " check-versions-test Self-tests for the check-versions gate"
@echo " check-grammar-crate-test Sync-test EXTENSIONS table vs src/langs.rs"
@echo " check-manpage-assets Assert every bca-*.1 man page is in deb+rpm asset lists"
@echo " enums-check cargo clippy + cargo test on workspace-excluded enums crate"
@echo " enums-codegen-drift Block enums codegen output drifting from checked-in files"
@echo " enums-codegen-drift-test Self-tests for the enums-codegen-drift gate"
@echo " self-scan bca threshold gate against this repo (hard: 100%)"
@echo " self-scan-headroom bca threshold gate (soft: BCA_HEADROOM, default 0.95)"
@echo " self-scan-write-baseline Refresh .bca-baseline.toml at the hard thresholds"
@echo " self-scan-write-baseline-headroom Refresh .bca-baseline.toml at the soft thresholds"
@echo " vcs Change-history (VCS) risk report for this repo"
@echo " lint Run all linters"
@echo ""
@echo "Python bindings (big-code-analysis-py):"
@echo " py-bootstrap Create .venv from uv.lock (alias: py-sync) — requires uv"
@echo " py-relock Regenerate uv.lock from pyproject.toml (run after editing deps)"
@echo " py-clean Remove .venv, _native*.so, *_cache, __pycache__"
@echo " py-fmt Format Python sources with ruff"
@echo " py-fmt-check Verify Python formatting"
@echo " py-lint Lint Python sources with ruff"
@echo " py-typecheck Type-check with mypy --strict + pyright"
@echo " py-test maturin develop + pytest (needs active venv)"
@echo " py-stubtest maturin develop + mypy stubtest of _native.pyi (needs venv)"
@echo " (first-time setup: 'make py-bootstrap' — installs uv-managed venv from uv.lock)"
@echo ""
@echo "Maintenance:"
@echo " clean Remove cargo build artifacts (target/) only"
@echo " distclean Remove cargo + Python artifacts (full wipe — chains py-clean and clean)"
@echo " install Install both CLI and web binaries"
@echo " install-cli Install bca"
@echo " install-web Install bca-web"
@echo ""
@echo "Documentation:"
@echo " doc Generate rustdoc (warning-tolerant viewer)"
@echo " doc-open Generate and open rustdoc (warning-tolerant viewer)"
@echo " doc-check Strict rustdoc gate (RUSTDOCFLAGS appends -D warnings)"
@echo " book Build the mdBook"
@echo " book-serve Serve the mdBook with live reload"
@echo " book-deploy Publish the mdBook to the gh-pages branch (manual fallback)"
@echo ""
@echo "Combined targets:"
@echo " all Check, test, build release"
@echo " pre-commit Verify formatting, lint, test (recommended before commit)"
@echo " ci Validate formatting, lint, test (no auto-fix)"
@echo ""
@echo "Release engineering:"
@echo " release-check Pre-tag gate: deny + about + CHANGELOG (VERSION=x.y.z)"
@echo " verify-changelog Verify CHANGELOG.md has section for VERSION=x.y.z"
@echo " pkg-deb-local Build .deb locally (host target, no CI matrix)"
@echo " pkg-rpm-local Build .rpm locally (host target, no CI matrix)"
# ---------------------------------------------------------------------------
# Prerequisites
# ---------------------------------------------------------------------------
check-tools:
@bash $(BASE_DIR)utils/check-tools.sh
# ---------------------------------------------------------------------------
# Build
# ---------------------------------------------------------------------------
build:
cargo build --workspace --all-targets
build-release:
cargo build --workspace --release
check:
cargo check --workspace --all-targets
# ---------------------------------------------------------------------------
# Test
# ---------------------------------------------------------------------------
test:
cargo test --workspace --all-features --lib --bins --tests
test-doc:
cargo test --workspace --all-features --doc
insta-review:
cargo insta test --review
insta-accept:
cargo insta test --accept
# ---------------------------------------------------------------------------
# Formatting
# ---------------------------------------------------------------------------
fmt:
@echo "Formatting Rust code..."
@cargo fmt --all
@echo "Formatting Markdown files..."
@$(MAKE) --no-print-directory markdown-fmt
@echo "Formatting bash scripts..."
@$(MAKE) --no-print-directory sh-fmt
@echo "Formatting TOML files..."
@$(MAKE) --no-print-directory toml-fmt
fmt-check:
@echo "Checking Rust code formatting..."
@cargo fmt --all --check || { echo "Rust code is not formatted (run 'make fmt')"; exit 1; }
@echo "Checking Markdown formatting..."
@$(MAKE) --no-print-directory markdown-lint
@echo "Checking bash script formatting..."
@$(MAKE) --no-print-directory sh-fmt-check
@echo "Checking TOML formatting..."
@$(MAKE) --no-print-directory toml-fmt-check
@echo "All formatting checks passed"
# Sanity guard for the find-by-ext helper. If EXCLUDE_DIRS over-matches
# (as it did in #160 when `tree-sitter-*` was unquoted and the recipe
# shell expanded the glob into absolute paths), every lint that pipes
# through `xargs -r` silently no-ops. Run as a prerequisite of every
# recipe that consumes find-by-ext.
_check-find:
@N=$$($(call find-by-ext,md,) | wc -l); \
[ "$$N" -ge 5 ] || { echo "ERROR: find-by-ext returned $$N .md files (expected >=5); EXCLUDE_DIRS is over-matching — see #160"; exit 1; }
# `rumdl check --fix` is preferred over `rumdl fmt` here: `fmt`
# exits 0 even when unfixable findings remain (formatter semantics),
# whereas `check --fix` applies every auto-fix it can and then exits
# non-zero if any finding is still outstanding. The latter is what
# we want for a Makefile gate — auto-fix what you can, then fail
# loudly on the rest so the contributor knows to edit manually.
markdown-fmt: _check-find
@echo "Auto-fixing Markdown files..."
@$(call find-by-ext,md,) | xargs -r rumdl check --fix || { echo "rumdl could not auto-fix all issues"; exit 1; }
markdown-lint: _check-find
@echo "Linting Markdown files..."
@$(call find-by-ext,md,) | xargs -r rumdl check || { echo "rumdl found issues"; exit 1; }
sh-fmt: _check-find
@$(call find-by-ext,sh,) | xargs -r shfmt -w -i 0 -ci -bn
sh-fmt-check: _check-find
@$(call find-by-ext,sh,) | xargs -r shfmt -d -i 0 -ci -bn || { echo "Bash scripts are not formatted (run 'make sh-fmt')"; exit 1; }
shellcheck: _check-find
@echo "Linting bash scripts with shellcheck..."
@$(call find-by-ext,sh,) | xargs -r shellcheck || { echo "Shellcheck found issues"; exit 1; }
toml-fmt:
@taplo fmt
toml-fmt-check:
@taplo fmt --check || { echo "TOML files are not formatted (run 'make toml-fmt')"; exit 1; }
toml-lint:
@echo "Linting TOML files..."
@taplo lint || { echo "TOML lint found issues"; exit 1; }
makefile-check:
@echo "Linting Makefile with checkmake..."
@checkmake --config $(BASE_DIR).checkmake.ini $(BASE_DIR)Makefile || { echo "checkmake found issues"; exit 1; }
# actionlint scans every workflow under .github/workflows/ and shells
# out to shellcheck (when present on PATH) for the `run:` blocks. It
# takes no file arguments here: invoked at the repo root it discovers
# .github/workflows/ automatically, which matches the canonical
# upstream invocation and keeps the recipe robust against new
# workflows being added.
actionlint:
@echo "Linting GitHub Actions workflows with actionlint..."
@(cd $(BASE_DIR) && actionlint -no-color) || { echo "actionlint found issues"; exit 1; }
snapshot-anchors:
@echo "Checking insta snapshot anchors..."
@python3 $(BASE_DIR)check-snapshot-anchors.py
# Grammar-marker-sync gate. Blocks the failure mode from #400:
# bumping the notification-only `tree-sitter-{javascript,cpp}`
# marker in `tree-sitter-{mozjs,mozcpp}/Cargo.toml` without
# re-running the matching `./generate-grammars/generate-*.sh`.
# Static lint — no network, no cargo, runs in milliseconds.
grammar-marker-sync:
@echo "Checking grammar-marker sync against baseline..."
@python3 $(BASE_DIR)check-grammar-marker-sync.py
# Enums-codegen drift gate. Closes #405: running any
# `recreate-grammars.sh` invocation silently regenerated
# `src/c_langs_macros/{c_macros,c_specials}.rs` to a pre-
# optimization form. This gate runs the codegen into a tempdir
# and diffs against the checked-in files; drift fails.
enums-codegen-drift:
@echo "Checking enums codegen drift..."
@bash $(BASE_DIR)check-enums-codegen-drift.sh
# Self-tests for the enums-codegen-drift gate. Kept separate from
# the gate target so the gate stays a one-line invariant check
# (matching the grammar-marker-sync pattern) and test failures
# get their own clean parallel-arm output in the pre-commit/CI DAG.
enums-codegen-drift-test:
@echo "Running enums-codegen-drift self-tests..."
@(cd $(BASE_DIR) && python3 -m unittest -q check-enums-codegen-drift-test.py)
# Self-tests for the grammar-marker-sync gate. Kept separate from
# the gate target so the gate stays a one-line invariant check
# (matching the snapshot-anchors / check-versions pattern) and
# the test failures get their own clean parallel-arm output in
# the pre-commit/CI DAG.
grammar-marker-sync-test:
@echo "Running grammar-marker-sync self-tests..."
@(cd $(BASE_DIR) && python3 -m unittest -q check-grammar-marker-sync-test.py)
# Regenerate the man pages under `man/` from the live clap schema.
# Auto-fix flavour: `cargo xtask` rewrites every `.1` file so a
# subsequent `git diff --exit-code -- man/` is clean. Used by
# `_pc-manpages` so contributors can stage the regenerated output.
manpages:
@echo "Regenerating man pages from clap schema..."
@cargo xtask
# Regenerate-then-assert-clean drift gate. Like the CI `manpage`
# job in `.github/workflows/ci.yml`, this recipe runs `cargo xtask`
# (which rewrites `man/*.1` in place — the same side effect CI
# accepts on its ephemeral runners) and then fails if the resulting
# tree differs from the index. Used by `_ci-manpages` and
# `_pc-manpages`. Locally, contributors with hand-edited `.1` files
# will see those edits overwritten; man pages are generated
# artifacts and should not be hand-edited.
manpages-check:
@echo "Checking man pages match clap schema..."
@cargo xtask
@if ! git diff --exit-code -- man/; then \
echo "ERROR: man pages drift from the clap schema. The regenerated files are already in your working tree — run 'git add man/' and commit alongside the clap change."; \
exit 1; \
fi
# Lockstep-version invariant: every owned crate and every internal
# `=<v>` dep pin must equal `[workspace.package].version`. See
# `RELEASING.md` "Lockstep version policy" and `check-versions.py`.
check-versions:
@echo "Checking lockstep version invariant..."
@python3 $(BASE_DIR)check-versions.py
# Self-tests for the lockstep-version gate. Kept separate from the
# gate target (matching the grammar-marker-sync-test pattern) so the
# gate stays a one-line invariant check and the test failures get
# their own clean parallel-arm output in the pre-commit/CI DAG.
check-versions-test:
@echo "Running check-versions self-tests..."
@(cd $(BASE_DIR) && python3 -m unittest -q check-versions-test.py)
# Man-page packaging gate. Blocks the failure mode from #444:
# a bca subcommand man page that drops out of the hand-maintained
# deb/rpm asset lists in the CLI / web crate Cargo.toml. Static
# lint — no network, no cargo, runs in milliseconds. See #446.
check-manpage-assets:
@echo "Checking man-page packaging asset lists..."
@python3 $(BASE_DIR)check-manpage-assets.py
# Sync gate for check-grammar-crate.py's EXTENSIONS table. Re-derives
# the grammar -> extension mapping from src/langs.rs `mk_langs!` and
# fails if the hand-maintained table has drifted (#869). Static — no
# network, no cargo — so it rides the cheap test DAG alongside the
# other helper-script self-tests.
check-grammar-crate-test:
@echo "Running check-grammar-crate self-tests..."
@(cd $(BASE_DIR) && python3 -m unittest -q check-grammar-crate-test.py)
# The `enums/` crate is listed in `[workspace].exclude` (it ships a
# non-published codegen binary used only by `recreate-grammars.sh`), so
# it is invisible to `cargo {check,clippy,test} --workspace` and to the
# `lint` / `test` CI jobs. Without an explicit gate, lint regressions in
# `enums/src/*.rs` drift silently — the `unused_imports` warning fixed
# in #162 went unnoticed for that exact reason (see #164).
#
# RUSTFLAGS is set per-recipe (defensive): CI exports the same value at
# workflow scope, so the recipe-local export is redundant in CI but
# necessary locally to keep `make enums-check` behave identically
# everywhere.
#
# Uses `cargo clippy` (not `cargo check`) so the gate enforces the same
# lint floor as the workspace `clippy` job: rustc-level warnings plus the
# clippy default group. The three `manual_is_ascii_check` sites that
# previously blocked this (tracked as #166) have been fixed.
#
# Also runs `cargo test` on the same manifest because the workspace
# `test` job (`cargo test --workspace`) skips this crate by exclusion,
# leaving `enums/tests/dispatch.rs` and any other integration tests
# unexecuted in CI / pre-commit. The dispatch test pins each `Lang`
# variant to its expected backing grammar crate (issue #350); without
# this runtime gate, an arm in `mk_get_language!` pointing at the wrong
# grammar would compile cleanly and only fail when a developer
# manually invoked `cargo test` against the enums manifest.
enums-check:
@echo "Linting workspace-excluded enums crate..."
@RUSTFLAGS="-D warnings" cargo clippy \
--manifest-path $(BASE_DIR)enums/Cargo.toml \
--all-targets --locked -- -D warnings
@echo "Running tests for workspace-excluded enums crate..."
@cargo test \
--manifest-path $(BASE_DIR)enums/Cargo.toml \
--locked
# ---------------------------------------------------------------------------
# bca self-scan threshold gate
#
# Re-runs the CI threshold gate against the in-tree bca binary.
# Mirrors the `Threshold gate (baseline-ratcheted)` step in
# .github/workflows/pages.yml. We build from source on purpose:
# any in-progress change to metric computation is reflected in the
# values we gate on. When pages.yml bumps the pinned release
# binary, the two invocations re-sync.
#
# Path selection (`paths = ["."]`), the ignore file
# (`exclude_from = ".bcaignore"`), the baseline
# (`baseline = ".bca-baseline.toml"`), the per-function thresholds,
# and the cyclomatic `?` policy (`cyclomatic_count_try = false`) all
# live in the auto-discovered `bca.toml` manifest, so a bare
# `bca check` reproduces the full hard-tier gate with no flags.
#
# Two tiers:
#
# self-scan hard gate (limits as configured in bca.toml;
# absorbed by .bca-baseline.toml). Mirrors CI.
#
# self-scan-headroom soft gate (`--tier=soft=BCA_HEADROOM`).
# Scales every limit by BCA_HEADROOM (default
# 0.95) and runs the same baseline-ratcheted
# check, so new functions encroaching into the
# 95-100% band fail before they would trip the
# hard gate. Set BCA_HEADROOM=0.90 to widen the
# band, 0.99 to tighten it. The ratio rides on
# the soft tier itself (#688): `--tier=soft=R`
# replaces the retired `--headroom R` dial.
#
# Refresh the baseline (after an intentional regression or after
# raising a limit):
#
# self-scan-write-baseline refreshes .bca-baseline.toml in place.
# ---------------------------------------------------------------------------
SELF_SCAN_BCA := cargo run --quiet --release -p big-code-analysis-cli --
# `--jobs` defaults to `auto` (effective CPU count, cgroup-/cpuset-
# aware on Linux via Rust's std lib), so no `$(nproc)` plumbing is
# needed for the self-scan recipes (issue #383).
#
# The walk root is left to the manifest's `paths = ["."]` key: a bare
# `bca check` resolves it to an absolute root, but the walker now
# re-anchors that root to the `./`-prefixed relative form before
# matching (#488), so the `./`-anchored `.bcaignore` deny-set matches
# the same files whether the seed was `.`, `$PWD`, or a manifest-
# resolved absolute path. The previous `--paths .` workaround is no
# longer needed.
#
# Diff-aware footer partitioning in the self-scan gate (#356/#387). Set
# BCA_SINCE=<ref> to append `--since <ref>`, which splits the per-file
# rollup footer into "Files in this range:" (offenders the diff touched)
# vs "Other offenders:". CI (`.github/workflows/pages.yml`) sets it to
# `origin/${GITHUB_BASE_REF:-main}` so PR runs highlight the offenders a
# PR introduced; local runs leave it unset, so the gate is unchanged and
# the local/CI invocation stays identical by construction. `--since` is
# presentational only — it never changes pass/fail (that would be
# `--changed-only`, deliberately not adopted here, see #387). The
# `--github-annotations` / `$GITHUB_STEP_SUMMARY` digest / remediation
# block all auto-enable from GitHub Actions env vars, so no flag is
# needed for them.
#
# `--since` is a `check` *subcommand* argument, so it is spliced in
# after `check` in each gate target below.
#
# Default to empty so the `:=` expansion does not trip
# `--warn-undefined-variables` when the var is unset (the common
# case); CI's environment value still overrides, since `?=` only
# assigns when undefined.
BCA_SINCE ?=
SELF_SCAN_SINCE_ARGS := $(if $(BCA_SINCE),--since $(BCA_SINCE),)
self-scan:
@echo "bca self-scan (hard gate)..."
@$(SELF_SCAN_BCA) check $(SELF_SCAN_SINCE_ARGS)
self-scan-headroom:
@echo "bca self-scan (soft gate, BCA_HEADROOM=$${BCA_HEADROOM:-0.95})..."
@$(SELF_SCAN_BCA) check $(SELF_SCAN_SINCE_ARGS) \
--tier=soft=$${BCA_HEADROOM:-0.95}
self-scan-write-baseline:
@echo "Refreshing .bca-baseline.toml from current offenders..."
@$(SELF_SCAN_BCA) check --write-baseline
# Refresh `.bca-baseline.toml` against the SOFT thresholds
# (the `bca.toml` limits scaled by BCA_HEADROOM, default 0.95).
# Records every current offender at the soft tier — strictly a
# superset of the hard-tier offenders. Use this when launching the
# soft gate or after raising BCA_HEADROOM so the baseline absorbs
# the new headroom band rather than firing on every commit.
self-scan-write-baseline-headroom:
@echo "Refreshing .bca-baseline.toml from current soft-tier offenders (BCA_HEADROOM=$${BCA_HEADROOM:-0.95})..."
@$(SELF_SCAN_BCA) check \
--tier=soft=$${BCA_HEADROOM:-0.95} \
--write-baseline
# ---------------------------------------------------------------------------
# Change-history (VCS) risk report (issue #328). Dogfoods `bca vcs` on
# this repo: ranks tracked files by composite change-history risk (churn,
# commit/author counts, ownership dilution, bug-/security-fix history)
# over the default 12mo / 90d windows. Informational only — it never
# gates (always exits 0). Path selection and the `.bcaignore` deny-set
# come from the auto-discovered `bca.toml` manifest, exactly like
# `make self-scan`, so the file universe matches the threshold gate.
# `BCA_VCS_TOP` overrides the row cap (default 40; 0 = all). This prints
# the quick terminal table; for the styled, sortable page (published to
# Pages by the `pages.yml` workflow, issue #573) run
# `bca vcs --format html --output vcs.html`.
# ---------------------------------------------------------------------------
vcs:
@$(SELF_SCAN_BCA) vcs --top $${BCA_VCS_TOP:-40}
# ---------------------------------------------------------------------------
# Python tooling (big-code-analysis-py)
#
# Most targets in this section gracefully no-op when the corresponding
# tool is absent — matching how the markdown / TOML lint families
# behave on a barebones host. CI installs all tools, so the skip path
# never fires there. Exception: `py-bootstrap` is the documented
# setup entry point and hard-fails when `uv` is missing, so a
# contributor sees the install instruction immediately rather than
# silently skipping.
#
# Tools used:
# uv — Python env / lockfile manager (required for py-bootstrap)
# ruff — lint + format
# mypy — type check (strict mode, invoked from the bindings dir)
# pyright — type check (strict mode, second opinion)
# maturin — build the compiled extension into the active venv
#
# `py-test` requires an active venv that maturin can write the .so
# into. The recipe does NOT create one — if `VIRTUAL_ENV` is not set,
# maturin will fail with a clear error. CI explicitly creates one per
# matrix leg (see `.github/workflows/ci.yml`). Locally, run
# `make py-bootstrap` once to create big-code-analysis-py/.venv from
# the checked-in uv.lock.
# ---------------------------------------------------------------------------
# Bootstrap the bindings dev environment from uv.lock. Hard-fails if
# uv is missing — there is no pip fallback because uv.lock is the
# project's source of truth for the resolved dev set, and consuming
# it requires uv. `uv sync --locked` creates big-code-analysis-py/.venv,
# installs the locked `dev` extra into it, and is idempotent. The
# `--locked` flag refuses to mutate uv.lock — if pyproject.toml has
# drifted from the lockfile, this target fails loudly and points at
# `make py-relock` instead of silently rewriting the lockfile on
# every contributor's machine.
py-bootstrap:
@command -v uv >/dev/null 2>&1 || { \
echo "ERROR: uv missing — install via 'curl -LsSf https://astral.sh/uv/install.sh | sh' (or 'brew install uv', 'pipx install uv')"; \
exit 1; }
@cd "$(BCA_PY_DIR)" && uv sync --locked --extra dev
# Alias so contributors familiar with `uv sync` find the right target.
py-sync: py-bootstrap
# Regenerate uv.lock after editing pyproject.toml (e.g. bumping a
# dev-extra floor). Separated from py-bootstrap so lockfile churn is
# always a deliberate, reviewable act — `make py-bootstrap` will not
# silently update the lockfile on contributors' machines.
py-relock:
@command -v uv >/dev/null 2>&1 || { \
echo "ERROR: uv missing — install via 'curl -LsSf https://astral.sh/uv/install.sh | sh' (or 'brew install uv', 'pipx install uv')"; \
exit 1; }
@cd "$(BCA_PY_DIR)" && uv lock
py-fmt:
@if command -v ruff >/dev/null 2>&1; then \
echo "Formatting Python sources..."; \
ruff format "$(BCA_PY_DIR)"; \
else echo "ruff not found; skipping py-fmt"; fi
py-fmt-check:
@if command -v ruff >/dev/null 2>&1; then \
echo "Checking Python formatting..."; \
ruff format --check "$(BCA_PY_DIR)" || \
{ echo "Python files not formatted (run 'make py-fmt')"; exit 1; }; \
else echo "ruff not found; skipping py-fmt-check"; fi
py-lint:
@if command -v ruff >/dev/null 2>&1; then \
echo "Linting Python sources..."; \
ruff check "$(BCA_PY_DIR)" || { echo "ruff lint found issues"; exit 1; }; \
else echo "ruff not found; skipping py-lint"; fi
py-typecheck:
@# Prefer the bindings dir's `.venv/bin/{mypy,pyright}` when
@# present so the type checker resolves dev-dependencies
@# (pytest, etc.) declared in `big-code-analysis-py/pyproject.toml`
@# from the project's documented venv layout. Fall back to the
@# host's PATH when the venv hasn't been provisioned (CI sets
@# `VIRTUAL_ENV` and uses PATH-resolved binaries). A pipx-isolated
@# system `mypy` can't see the bindings dir's pytest stubs.
@if [ -x "$(BCA_PY_DIR)/.venv/bin/mypy" ]; then \
echo "Type-checking with mypy --strict (venv)..."; \
(cd "$(BCA_PY_DIR)" && .venv/bin/mypy --strict python tests examples) || \
{ echo "mypy --strict found issues"; exit 1; }; \
elif command -v mypy >/dev/null 2>&1; then \
echo "Type-checking with mypy --strict..."; \
(cd "$(BCA_PY_DIR)" && mypy --strict python tests examples) || \
{ echo "mypy --strict found issues"; exit 1; }; \
else echo "mypy not found; skipping mypy stage of py-typecheck"; fi
@if [ -x "$(BCA_PY_DIR)/.venv/bin/pyright" ]; then \
echo "Type-checking with pyright (strict, venv)..."; \
(cd "$(BCA_PY_DIR)" && .venv/bin/pyright) || \
{ echo "pyright found issues"; exit 1; }; \
elif command -v pyright >/dev/null 2>&1; then \
echo "Type-checking with pyright (strict)..."; \
(cd "$(BCA_PY_DIR)" && pyright) || \
{ echo "pyright found issues"; exit 1; }; \
else echo "pyright not found; skipping pyright stage of py-typecheck"; fi
# Two pre-build cleanups defend against distinct failure modes:
#
# 1. `find $(BASE_DIR)target -name 'libbig_code_analysis_py*' -delete`
# Defeats a maturin 1.13 + cargo-incremental-cache interaction that
# reliably emits a 0-byte .so on the second back-to-back invocation
# when neither sources nor deps changed (the wheel-build step
# truncates target/maturin/libbig_code_analysis_py.so before cargo
# decides "no rebuild needed" and skips the relink). Forcing a
# relink each time is roughly free (~50ms).
#
# 2. `rm -f $(BCA_PY_DIR)/python/big_code_analysis/_native*.so`
# Removes the previous editable-install extension before
# `maturin develop` writes a fresh one. A contributor who has
# switched build modes (non-abi3 → abi3, or vice versa) ends up
# with both `_native.cpython-3XX-<plat>.so` AND `_native.abi3.so`
# in the source tree — Python's loader prefers the more-specific
# cpython-tagged filename, shadowing the fresh build. Symptom:
# ImportError for any symbol added since the last non-abi3 build,
# even though maturin reports a successful rebuild. Glob covers
# every tag variant in one shot.
#
# CI does NOT need these guards: each CI job starts from a fresh
# checkout (target/ is restored from cache but the .so is rebuilt on
# every job invocation, not repeated within a single job), and the
# editable install dir is never populated before the build step.
py-test:
@# Pre-build cleanups (see header comment) are hoisted before the
@# if/elif so a future fix to either guard lands in one place; the
@# rm/find are safe no-ops on missing files.
@find "$(BASE_DIR)target" -name 'libbig_code_analysis_py*' -delete 2>/dev/null || true
@rm -f "$(BCA_PY_DIR)/python/big_code_analysis/"_native*.so
@# Prefer the bindings dir's `.venv/bin/{maturin,python}` over the
@# host's PATH for the same reason `py-typecheck` does: the venv
@# has pytest (declared as a dev-dependency in
@# `big-code-analysis-py/pyproject.toml`), the host's bare Python
@# typically does not. CI activates the venv explicitly via
@# `VIRTUAL_ENV` and uses PATH-resolved binaries — both paths
@# reach the same wheel because `maturin develop` installs into
@# whichever venv it finds.
@if [ -x "$(BCA_PY_DIR)/.venv/bin/maturin" ] && [ -x "$(BCA_PY_DIR)/.venv/bin/python" ]; then \
echo "Building extension + running pytest (venv)..."; \
(cd "$(BCA_PY_DIR)" && .venv/bin/maturin develop --quiet && .venv/bin/python -m pytest) || \
{ echo "py-test failed"; exit 1; }; \
elif command -v maturin >/dev/null 2>&1; then \
echo "Building extension + running pytest..."; \
(cd "$(BCA_PY_DIR)" && maturin develop --quiet && python -m pytest) || \
{ echo "py-test failed"; exit 1; }; \
else echo "maturin not found; skipping py-test"; fi
# `mypy stubtest` diffs the hand-written `_native.pyi` stub against the
# compiled PyO3 extension — names, signatures, and *defaults* — catching
# the `#[pyo3(signature = …)]`-vs-stub drift that the usage-only
# `py-typecheck` mypy/pyright passes cannot see (#583 shipped one such
# drift; #673 closes the gap). Unlike `py-typecheck`, stubtest must
# import the built extension, so it carries the same `maturin develop`
# build + pre-build cleanups as `py-test` and skips cleanly when the
# venv / maturin / stubtest are absent. The allowlist covers the
# deliberate facade differences (the `vcs` submodule, runtime
# `__all__`); see `big-code-analysis-py/stubtest-allowlist.txt`.
py-stubtest:
@find "$(BASE_DIR)target" -name 'libbig_code_analysis_py*' -delete 2>/dev/null || true
@rm -f "$(BCA_PY_DIR)/python/big_code_analysis/"_native*.so
@if [ -x "$(BCA_PY_DIR)/.venv/bin/maturin" ] && [ -x "$(BCA_PY_DIR)/.venv/bin/python" ]; then \
if "$(BCA_PY_DIR)/.venv/bin/python" -c "import mypy.stubtest" >/dev/null 2>&1; then \
echo "Building extension + running mypy stubtest (venv)..."; \
(cd "$(BCA_PY_DIR)" && .venv/bin/maturin develop --quiet && \
.venv/bin/python -m mypy.stubtest big_code_analysis._native big_code_analysis.vcs \
--allowlist stubtest-allowlist.txt) || \
{ echo "stubtest found stub/runtime drift"; exit 1; }; \
else echo "mypy stubtest not found in venv; skipping py-stubtest"; fi; \
elif command -v maturin >/dev/null 2>&1 && python -c "import mypy.stubtest" >/dev/null 2>&1; then \
echo "Building extension + running mypy stubtest..."; \
(cd "$(BCA_PY_DIR)" && maturin develop --quiet && \
python -m mypy.stubtest big_code_analysis._native big_code_analysis.vcs \
--allowlist stubtest-allowlist.txt) || \
{ echo "stubtest found stub/runtime drift"; exit 1; }; \
else echo "maturin or mypy stubtest not found; skipping py-stubtest"; fi
# ---------------------------------------------------------------------------
# Lint aggregate
# ---------------------------------------------------------------------------
clippy:
@echo "Running Rust lints..."
@cargo clippy --workspace --all-targets -- -D warnings
@cargo clippy --workspace --all-targets --all-features -- -D warnings
udeps:
@echo "Detecting unused dependencies..."
@cargo +nightly udeps --workspace --all-targets
# Reuse the _ci-* family so `make lint` runs the same set of gates as
# `make ci`'s non-cargo-pipeline branch, in parallel. _ci-clippy holds
# the workspace `target/` lock; the other lints don't use cargo at all
# (or use a separate `target/`, in _ci-enums-check's case), so they
# fan out safely.
lint:
$(MAKE) -j --output-sync=target \
_ci-clippy \
_ci-shellcheck _ci-markdown-lint _ci-toml-lint _ci-makefile-check \
_ci-actionlint _ci-snapshot-anchors _ci-grammar-marker-sync _ci-grammar-marker-sync-test _ci-check-versions _ci-check-versions-test _ci-check-grammar-crate-test _ci-check-manpage-assets _ci-enums-check _ci-enums-codegen-drift _ci-enums-codegen-drift-test
# ---------------------------------------------------------------------------
# Maintenance
# ---------------------------------------------------------------------------
# Python-only clean. Removes everything maturin / pytest / ruff / mypy
# can recreate from sources: the uv-managed venv, the editable
# install's compiled extension (matches both abi3 and per-version
# tagged variants — switching build modes leaves stale `.so` files
# that Python's loader prefers over the fresh one), the per-tool
# caches, and `__pycache__` trees. Does NOT remove `uv.lock` (a
# checked-in artifact) or `target/maturin/` (handled by `cargo
# clean`).
#
# Best-effort: `rm -rf` and `rm -f` are no-ops on missing paths, and
# `find` swallows its own missing-root errors via `2>/dev/null || true`
# (matching the idiom used by py-test on `find $(BASE_DIR)target`).
# A missing BCA_PY_DIR (sparse checkout, experimental removal) leaves
# every line as a silent no-op and the recipe still exits 0 under
# `.SHELLFLAGS := -eu -o pipefail -c`.
py-clean:
@echo "Removing Python build artifacts..."
@rm -rf "$(BCA_PY_DIR)/.venv"
@rm -rf "$(BCA_PY_DIR)/.pytest_cache"
@rm -rf "$(BCA_PY_DIR)/.mypy_cache"
@rm -rf "$(BCA_PY_DIR)/.ruff_cache"
@rm -f "$(BCA_PY_DIR)/python/big_code_analysis/"_native*.so
@find "$(BCA_PY_DIR)" -type d -name '__pycache__' -prune -exec rm -rf {} + 2>/dev/null || true
# `clean` stays cargo-only by default — a Rust-only contributor running
# `make clean` to reset incremental state should not lose their
# uv-managed venv as collateral (forcing a multi-step rebootstrap on
# the next Python invocation). Use `make distclean` for the full wipe.
clean:
cargo clean
# Full wipe: cargo target/ AND every Python artifact (.venv, caches,
# editable-install .so, __pycache__). Useful before a fresh-checkout
# bootstrap or when reproducing CI from scratch.
distclean: py-clean clean
install: install-cli install-web
# `--locked` makes `cargo install` honour the committed Cargo.lock rather than
# re-resolving to the newest semver-compatible deps. The lock pins `time` to
# 0.3.46: 0.3.47+ trips a coherence error (E0119) in the blanket impl of
# cookie 0.16.2 (pulled transitively by actix-web 4) under rustc 1.95.
install-cli:
RUSTFLAGS="-C target-cpu=native" cargo install --locked --path big-code-analysis-cli
install-web:
RUSTFLAGS="-C target-cpu=native" cargo install --locked --path big-code-analysis-web
# ---------------------------------------------------------------------------
# Documentation
#
# `doc` and `doc-open` are warning-tolerant interactive viewers — they build
# whatever they can so a developer mid-refactor can still scroll the rendered
# output even when an unrelated doc-comment has drifted. `doc-check` is the
# strict gate invoked by the pre-commit and CI pipelines (`_pc-doc-check` /
# `_ci-doc-check`); it appends `-D warnings` to any caller-set `RUSTDOCFLAGS`
# so docs.rs-style invocations (e.g. `RUSTDOCFLAGS="--cfg docsrs"`) still
# compose correctly instead of being clobbered.
# ---------------------------------------------------------------------------
doc:
cargo doc --no-deps --workspace --all-features
doc-open:
cargo doc --no-deps --workspace --all-features --open
doc-check:
@echo "Building rustdoc with -D warnings..."
@RUSTDOCFLAGS="$${RUSTDOCFLAGS:-} -D warnings" \
cargo doc --no-deps --workspace --all-features
book:
mdbook build big-code-analysis-book
book-serve:
mdbook serve big-code-analysis-book
# Manual fallback for publishing the book to GitHub Pages. The
# canonical publish path is .github/workflows/book.yml, which fires
# on every push to main; this target exists so contributors can
# republish from a checkout when CI is unavailable.
book-deploy:
./utils/deploy-book-to-gh-pages.sh
# ---------------------------------------------------------------------------
# Combined workflows
# ---------------------------------------------------------------------------
all: check test build-release
pre-commit:
$(MAKE) -j --output-sync=target \
_pc-test \
_pc-shellcheck _pc-markdown-lint _pc-toml-lint _pc-makefile-check \
_pc-actionlint _pc-snapshot-anchors _pc-grammar-marker-sync _pc-grammar-marker-sync-test _pc-check-versions _pc-check-versions-test _pc-check-grammar-crate-test _pc-check-manpage-assets _pc-enums-check _pc-enums-codegen-drift _pc-enums-codegen-drift-test \
_pc-manpages \
_pc-self-scan _pc-self-scan-headroom \
_pc-py-fmt _pc-py-typecheck _pc-py-test _pc-py-stubtest
@echo "Pre-commit checks passed"
ci:
$(MAKE) _ci-fmt-check
$(MAKE) -j --output-sync=target \
_ci-cargo-pipeline \
_ci-shellcheck _ci-markdown-lint _ci-toml-lint _ci-makefile-check \
_ci-actionlint _ci-snapshot-anchors _ci-grammar-marker-sync _ci-grammar-marker-sync-test _ci-check-versions _ci-check-versions-test _ci-check-grammar-crate-test _ci-check-manpage-assets _ci-enums-check _ci-enums-codegen-drift _ci-enums-codegen-drift-test \
_ci-py-fmt-check _ci-py-lint _ci-py-typecheck _ci-py-test _ci-py-stubtest
@echo "CI checks passed"
# ---------------------------------------------------------------------------
# Parallel pre-commit DAG
#
# These _pc-* targets express the dependency graph so `make -j` runs
# independent stages concurrently. The `pre-commit` target invokes them
# with `-j --output-sync=target`.
#
# All cargo invocations against the workspace `target/` (clippy, test,
# udeps, py-test's maturin develop) share the package cache and the
# target/.cargo-lock mutex, so they are serialized into one chain.
# Non-cargo checks (lint families, py-fmt's ruff, py-typecheck's
# mypy + pyright) run in parallel with the cargo pipeline, gated only
# on _pc-fmt.
#
# Dependency graph:
#
# _pc-fmt
# ├── _pc-clippy → _pc-test → _pc-doc-check → _pc-udeps → _pc-manpages
# │ → _pc-self-scan → _pc-self-scan-headroom → _pc-py-test
# ├── _pc-shellcheck
# ├── _pc-markdown-lint
# ├── _pc-toml-lint
# ├── _pc-makefile-check
# ├── _pc-actionlint
# ├── _pc-snapshot-anchors
# ├── _pc-grammar-marker-sync
# ├── _pc-grammar-marker-sync-test
# ├── _pc-check-versions
# ├── _pc-enums-check
# ├── _pc-enums-codegen-drift (chained after _pc-enums-check)
# ├── _pc-py-fmt
# └── _pc-py-typecheck
#
# _pc-self-scan and _pc-self-scan-headroom both invoke
# `cargo run --release -p big-code-analysis-cli`, which holds the
# workspace `target/` lock for the release-profile build, so they
# serialize into the cargo chain after _pc-manpages rather than
# fanning out in parallel. The hard tier runs first so a regression
# is named before the soft tier reports near-limit headroom.
#
# _pc-enums-check runs cargo on `enums/Cargo.toml`, which has its own
# `target/` (the crate is workspace-excluded), so it does NOT share the
# `target/` lock with the workspace cargo chain and is safe to run in
# parallel with _pc-clippy/_pc-test/_pc-udeps.
#
# _pc-py-fmt and _pc-py-typecheck do NOT touch cargo — they invoke
# ruff and mypy/pyright respectively against pre-built sources/stubs
# — so they run in parallel with the cargo pipeline.
#
# _pc-py-test runs `maturin develop` against the workspace target/, so
# it MUST chain after the tail of the cargo lock-holding pipeline
# (currently _pc-self-scan-headroom) rather than fanning out in
# parallel. Fanning out caused implicit serialization via cargo's
# lock anyway and obscured the true wall-clock cost. When a new
# cargo-lock-holding stage is added, extend this chain at the tail
# (and update the dependency graph comment above) — do not parallelise.
#
# Do not invoke _pc-* targets directly; use `make pre-commit`.
# ---------------------------------------------------------------------------
_pc-fmt:
$(MAKE) fmt-check
_pc-clippy: _pc-fmt
cargo clippy --workspace --all-targets -- -D warnings
cargo clippy --workspace --all-targets --all-features -- -D warnings
_pc-test: _pc-clippy
cargo test --workspace --all-features --lib --bins --tests
cargo test --workspace --all-features --doc
_pc-doc-check: _pc-test
$(MAKE) doc-check
_pc-udeps: _pc-doc-check
cargo +nightly udeps --workspace --all-targets
_pc-shellcheck: _pc-fmt
$(MAKE) shellcheck
_pc-markdown-lint: _pc-fmt
$(MAKE) markdown-lint
_pc-toml-lint: _pc-fmt
$(MAKE) toml-lint
_pc-makefile-check: _pc-fmt
$(MAKE) makefile-check
_pc-actionlint: _pc-fmt
$(MAKE) actionlint
_pc-snapshot-anchors: _pc-fmt
$(MAKE) snapshot-anchors
_pc-grammar-marker-sync: _pc-fmt
$(MAKE) grammar-marker-sync
_pc-grammar-marker-sync-test: _pc-fmt
$(MAKE) grammar-marker-sync-test
_pc-check-versions: _pc-fmt
$(MAKE) check-versions
_pc-check-versions-test: _pc-fmt
$(MAKE) check-versions-test
_pc-check-grammar-crate-test: _pc-fmt
$(MAKE) check-grammar-crate-test
_pc-check-manpage-assets: _pc-fmt
$(MAKE) check-manpage-assets
_pc-enums-check: _pc-fmt
$(MAKE) enums-check
# Chain after enums-check so the cargo invocations share warm
# dependency fingerprints. The two stages target different
# build profiles (clippy/test vs run), so the leaf binary still
# needs its own build pass; the win is dependency-fingerprint
# reuse, not zero-cost. Note this ordering applies only inside
# the Makefile pipeline — the pre-commit framework's own hook
# scheduling does NOT honor it, so direct hook invocations pay
# a cold cargo build.
_pc-enums-codegen-drift: _pc-enums-check
$(MAKE) enums-codegen-drift
# Self-tests run after the gate. The tests stand up a tempdir
# fixture (symlinked enums + copied target dirs) and exercise
# every documented failure mode of the gate. Cheap once the
# enums binary build is warm from _pc-enums-codegen-drift.
_pc-enums-codegen-drift-test: _pc-enums-codegen-drift
$(MAKE) enums-codegen-drift-test
# Man-page drift gate. Uses the verify flavour (`manpages-check`,
# not `manpages`) so `make pre-commit` exits non-zero when man
# pages drift — matching CI semantics. Chains after `_pc-udeps`
# rather than `_pc-fmt` because `cargo xtask` shares the workspace
# `target/` lock with the rest of the cargo pipeline; explicit
# serialization is clearer (and faster) than letting cargo's lock
# implicitly serialize parallel arms.
_pc-manpages: _pc-udeps
$(MAKE) manpages-check
# bca self-scan tiers. Both build with `cargo run --release` against
# the workspace target/, so they chain after _pc-manpages (the
# tail of the workspace cargo chain) rather than fanning out in
# parallel. The hard gate runs before the soft gate so a regression
# beyond the configured limit surfaces first.
_pc-self-scan: _pc-manpages
$(MAKE) self-scan
_pc-self-scan-headroom: _pc-self-scan
$(MAKE) self-scan-headroom
# Python pre-commit stages. _pc-py-fmt auto-fixes (ruff format +
# ruff check --fix); the typecheck and test stages are check-only
# (they cannot reasonably auto-fix). _pc-py-fmt and _pc-py-typecheck
# gate on _pc-fmt — they do not touch cargo so they parallelise
# safely with the clippy/test chain. _pc-py-test runs `maturin
# develop` against the workspace target/, so it must chain after
# _pc-udeps to avoid lock contention with the cargo pipeline.
_pc-py-fmt: _pc-fmt
@if command -v ruff >/dev/null 2>&1; then \
ruff format "$(BCA_PY_DIR)" && ruff check --fix "$(BCA_PY_DIR)"; \
else echo "ruff not found; skipping _pc-py-fmt"; fi
_pc-py-typecheck: _pc-fmt
$(MAKE) py-typecheck
_pc-py-test: _pc-self-scan-headroom
$(MAKE) py-test
# Chain after _pc-py-test, never concurrently: both run `maturin
# develop` against the shared workspace target/, so a parallel run
# would race the editable `_native` .so the other just wrote.
_pc-py-stubtest: _pc-py-test
$(MAKE) py-stubtest
# ---------------------------------------------------------------------------
# CI validation targets (no auto-formatting)
#
# These _ci-* targets have NO prerequisites — they can be invoked
# individually from GitHub Actions workflow steps or composed via `make ci`
# for local use.
#
# Execution order (enforced by `ci` target + _ci-cargo-pipeline):