Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
666 lines (608 loc) · 38.4 KB
/
Copy pathMakefile
File metadata and controls
666 lines (608 loc) · 38.4 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
.PHONY: help all clean test test-nextest proptest test-documentation-contracts doctest test-default-features lint-default-features test-kani-mutations test-workflow-contracts test-windows-msi-release-rank test-release-admission test-coverage-artifact test-linux-package-metadata test-ci-scripts test-package-verify test-release-publication build release lint lint-clippy lint-whitaker lint-python lint-workflow-scripts github-actions-lint doc-coverage doc-coverage-test validate-coverage-artifact fmt check-fmt typecheck typecheck-python markdownlint spelling nixie install-kani kani-check kani-full kani-ir test-kani-scope-wrapper install-verus verus formal-pr install-build-tools check-build-tools bench-build bench-config-load bench-glob-expansion
RUST_TOOLCHAIN_FILE ?= rust-toolchain.toml
# Export this path before shell probes expand it, so Make does not interpolate
# an override into the shell command line.
export RUST_TOOLCHAIN_FILE
# Threshold and toolchain for the Rustdoc doc-comment coverage gate. The
# threshold mirrors the 80% bar stated in AGENTS.md; the toolchain recollects
# the channel from rust-toolchain.toml the same way the build-tools variables do,
# so overriding either stays independent.
DOC_COVERAGE_THRESHOLD ?= 80
DOC_COVERAGE_TOOLCHAIN ?= $(shell awk -F'"' '/^[[:space:]]*channel[[:space:]]*=/ { print $$2; exit }' "$$RUST_TOOLCHAIN_FILE")
# Exported rather than interpolated into the recipe line. Passing the values
# through the environment means a toolchain or threshold containing a quote
# cannot inject commands into the shell command line; the child script reads
# them from its environment instead.
export DOC_COVERAGE_THRESHOLD DOC_COVERAGE_TOOLCHAIN
APP ?= netsuke
# A bare command name, resolved by the recipe shell. That shell is the only one
# that receives the curated PATH below, which is where `$HOME/.cargo/bin` is
# added; a parse-time `command -v` probe would query the environment Make was
# started with instead, and what that environment exports to a shell function
# is neither documented nor stable across Make releases.
CARGO ?= cargo
# The default must be defined before the export: `export CARGO` on its own
# would define the variable empty and shadow the `?=` default for every recipe.
export CARGO
# Pass `--locked` through only when a caller asks for lockfile verification.
# Keeping the default empty preserves the interactive inner-loop behaviour.
CARGO_LOCKED ?=
# Extra build-parallelism flags for plain Cargo invocations, e.g. `-j 4`.
BUILD_JOBS ?=
# The same concept for cargo-nextest, which spells build parallelism
# `--build-jobs N` and reserves `-j` for test concurrency. Keep the two
# variables separate so a `-j` value is never reinterpreted as a test-thread
# count.
NEXTEST_BUILD_JOBS ?=
# Explicit nextest process concurrency. Keep this distinct from compiler build
# parallelism: `cargo-nextest` uses `-j` for test processes, not Cargo jobs.
NEXTEST_TEST_JOBS ?=
CLIPPY_FLAGS ?= --workspace --all-targets --all-features -- -D warnings
KANI ?= cargo kani
KANI_FLAGS ?=
KANI_INSTALL_FLAGS ?=
KANI_CHECK_FLAGS ?=
KANI_VERSION_FILE ?= tools/kani/VERSION
# The development build standard: the `mold` linker and the parallel `rustc`
# frontend are the defaults for development, test, lint, and typecheck builds.
# `.cargo/config.toml` carries them so a bare `cargo` invocation gets them too;
# release and coverage builds are excluded there and below. The toolchain is not
# pinned separately — the standard uses the repository's own nightly from
# `rust-toolchain.toml`.
MOLD_VERSION_FILE ?= tools/mold/VERSION
MOLD_SHA256SUMS_FILE ?= tools/mold/SHA256SUMS
BUILD_TOOLS_PREFIX ?= $(HOME)/.local
# Exported rather than interpolated into the recipes. Make hands an exported
# variable to the child process directly, so a path containing a quote cannot
# break the command line the shell parses; a `VAR='$(VAR)'` prefix could.
export MOLD_VERSION_FILE MOLD_SHA256SUMS_FILE
export BUILD_TOOLS_PREFIX
# Cargo picks a single `rustflags` source rather than merging them, and an
# externally set `RUSTFLAGS` outranks every `rustflags` table in
# `.cargo/config.toml`. Every gate target below sets `RUSTFLAGS` to deny
# warnings, and CI's `setup-rust` exports the same value for a whole job, so
# without restating the flags here the gates would silently fall back to the
# platform linker and a single-threaded frontend while still reporting success.
# tests/makefile_test_target/rustflags.rs holds these variables equal to the
# configuration file; changing one without the other fails that test.
STANDARD_THREADS_FLAG ?= -Zthreads=8
STANDARD_MOLD_FLAG ?= -Clink-arg=-fuse-ld=mold
# `mold` ships for Linux only. macOS and Windows keep their platform linker,
# matching the `cfg(target_os = "linux")` gate in `.cargo/config.toml`.
BUILD_HOST_OS := $(shell uname -s)
STANDARD_RUSTFLAGS = $(STANDARD_THREADS_FLAG)$(if $(filter Linux,$(BUILD_HOST_OS)), $(STANDARD_MOLD_FLAG))
# Warnings-as-errors plus the standard, appended to whatever the caller set.
GATE_RUSTFLAGS = RUSTFLAGS="$${RUSTFLAGS:+$$RUSTFLAGS }-D warnings $(STANDARD_RUSTFLAGS)"
# A debug build that keeps the caller's warning policy rather than imposing one.
DEBUG_RUSTFLAGS = RUSTFLAGS="$${RUSTFLAGS:+$$RUSTFLAGS }$(STANDARD_RUSTFLAGS)"
# Release builds take neither the parallel frontend nor `mold`: assigning
# `RUSTFLAGS` at all, even to an empty inherited value, displaces the
# configuration file's `rustflags` tables, which is the whole mechanism. The
# configuration names no codegen backend at all, so nothing else is needed.
RELEASE_RUSTFLAGS = RUSTFLAGS="$${RUSTFLAGS-}"
# Kani denies warnings like the gates, but takes none of the standard: it drives
# `rustc` through `kani-compiler` on its own bundled toolchain, so the parallel
# frontend and `mold` would neither apply nor be honoured there. Inherited flags
# still survive, since `cargo kani` appends this value to its own.
KANI_RUSTFLAGS = RUSTFLAGS="$${RUSTFLAGS:+$$RUSTFLAGS }-D warnings"
# Command name, resolved by the recipe shell from the curated PATH, which
# carries `$HOME/.bun/bin` where the global markdownlint install lands.
MDLINT ?= markdownlint-cli2
# `make fmt` and `make check-fmt` call mdtablefix directly. `--git` selects the
# Markdown files Git tracks and `--include-untracked` adds the untracked files
# Git does not ignore, so a new document is formatted before it is staged.
# `--git` skips symbolic links, so CRUSH.md (a link to AGENTS.md) is never
# rewritten or checked twice. Both modes need mdtablefix 0.6.0 or later; CI
# pins the version in MDTABLEFIX_VERSION in .github/workflows/ci.yml.
MDTABLEFIX ?= mdtablefix
MDTABLEFIX_SELECT = --git --include-untracked
MDTABLEFIX_RULES = --wrap --renumber --breaks --ellipsis --fences
NIXIE ?= nixie
YAMLLINT ?= yamllint
# Go separates `GOPATH` entries with the platform list separator: `;` on
# Windows, `:` elsewhere. `go install` writes to the `bin` directory of the
# *first* entry, so split on that separator and keep the first one; appending
# `/bin` to a whole list would curate `$GOPATH` itself and omit the directory Go
# actually installs into.
GOPATH_SEPARATOR := $(if $(filter Windows_NT,$(OS)),;,:)
# `go install` writes to `$GOBIN` when set, otherwise that first `$GOPATH` entry's
# `bin`, otherwise `$HOME/go/bin`. The directory is absent from a minimal caller
# PATH, so name it once here and curate it on PATH below, next to the other user
# tool directories.
GO_BIN ?= $(if $(GOBIN),$(GOBIN),$(if $(GOPATH),$(word 1,$(subst $(GOPATH_SEPARATOR), ,$(GOPATH)))/bin,$(HOME)/go/bin))
# Exported so the `github-actions-lint` preflight can name the directory from
# the recipe shell rather than interpolating a path into the command line.
export GO_BIN
# Command name, resolved by the recipe shell like CARGO and MDLINT above, so an
# absolute `ACTIONLINT=/path/to/actionlint` still overrides it (CI does that).
# The `github-actions-lint` preflight reports the Go tool directory when the
# lookup fails, instead of letting the recipe die with a bare exit 127.
ACTIONLINT ?= actionlint
export ACTIONLINT
# Single source of truth for the shared spelling gate; the Makefile and CI
# both consume it, so the pinned builder cannot drift apart.
TYPOS_CONFIG_BUILDER_VERSION ?= v0.1.3
YAMLLINT_VERSION ?= 1.38.0
UV ?= uv
UV_ENV = UV_CACHE_DIR=.uv-cache UV_TOOL_DIR=.uv-tools
# The CV-005 CodeScene contracts live in shared-actions and run from a full
# commit, so a fix is a pin bump. `.github/cv005.toml` holds this repository's
# parameters and lane pairing.
CV005_CONTRACTS_REF ?= 3754876bc4e94dc65c83af3def37828934f8f65e
CV005_CONTRACTS = $(PUBLIC_GIT_SOURCE_ENV) $(UV_ENV) $(UV) tool run --python $(PYTHON_BASELINE) \
--from 'git+https://github.com/leynos/shared-actions@$(CV005_CONTRACTS_REF)\#subdirectory=packages/cv005-contracts' \
cv005-contracts
# Public GitHub tool sources must not inherit URL rewrites or custom Git
# helper paths from the invoking environment.
PUBLIC_GIT_SOURCE_ENV = env -u GIT_EXEC_PATH GIT_CONFIG_COUNT=0
# The Python baseline every uv-driven helper pins. Bump this alongside the
# `target-version`/`py-version` settings in pyproject.toml and the
# `python-version` inputs in .github/workflows/ci.yml, release.yml, and
# build-and-package.yml; the workflow contract tests hold the Makefile and CI
# in sync.
PYTHON_BASELINE ?= 3.14
COVERAGE_ARTIFACT_DIR ?= coverage-artifact
# Pin Ruff so `make` invokes the same version everywhere; floating the version
# causes version-skew lint failures because rule sets differ between releases.
# CI pins the same value in .github/workflows/ci.yml; a contract test in
# tests/workflow_contracts keeps the two from drifting apart.
RUFF_VERSION ?= 0.16.4
RUFF = $(UV_ENV) $(UV) tool run --from ruff==$(RUFF_VERSION) ruff
# Pin Interrogate so documentation coverage is stable between local and CI
# runs. The repository uses it only as a quality gate; it is not a Python
# distribution and does not need project metadata.
INTERROGATE_VERSION ?= 1.7.0
INTERROGATE = $(UV_ENV) $(UV) tool run --python $(PYTHON_BASELINE) \
--from 'interrogate==$(INTERROGATE_VERSION)' \
interrogate --fail-under 100
# Pin ty so `make` and CI invoke the same typechecker release. ty is pre-1.0
# and diagnostics shift between releases, so an unpinned install breaks the
# typecheck gate without any code change. Bump deliberately and fix new
# diagnostics in the same commit.
TY_VERSION ?= 0.0.74
# The CI helper scripts under scripts/ci declare these in their PEP 723
# blocks; scripts/tests/test_ci_scripts_metadata.py holds the two in step, so
# `uv run --script` in CI and the test and typecheck gates resolve one release.
# cuprum is a PyPI release, not a git ref: the scripts and the older
# `scripts/` helpers share one `run_sync` signature, and a git pin would give
# `make typecheck-python` a different cuprum from the one those helpers
# declare, so no single resolution could satisfy both.
CYCLOPTS_VERSION ?= 4.25.3
CUPRUM_VERSION ?= 0.1.0
CI_SCRIPT_DEPS = --with cyclopts==$(CYCLOPTS_VERSION) --with cuprum==$(CUPRUM_VERSION)
# Every Python source the repository owns. Ruff and Pylint resolve their own
# configuration and exclusions from pyproject.toml, so these paths only bound
# the walk.
PYTHON_SOURCES = .github/scripts scripts tests/workflow_contracts
# Pylint must run on the Python baseline so it parses every repository-owned
# source. `--load-plugins=` clears any configured plugins so this pass runs
# exactly the messages pyproject.toml enables.
PYLINT_PYTHON ?= $(PYTHON_BASELINE)
PYLINT_VERSION ?= 4.0.9
PYLINT_TARGETS ?= $(PYTHON_SOURCES)
PYLINT = $(UV_ENV) $(UV) tool run --managed-python --python $(PYLINT_PYTHON) \
--from 'pylint==$(PYLINT_VERSION)' pylint --load-plugins=
# The df12 house lints need CPython 3.14: they parse syntax PyPy's 3.11
# runtime cannot, and the baseline-gated messages (R9112, C9112) key off the
# `py-version` in pyproject.toml. They run through `uv tool run` rather than
# `uv run` so the repository never needs a project virtual environment for a
# Rust contributor's sake.
DF12_PYTHON_LINTS_REF ?= v0.3.0
DF12_PYTHON_LINTS = git+https://github.com/leynos/df12-python-lints.git@$(DF12_PYTHON_LINTS_REF)
DF12_PYLINT_MESSAGES = R9101,C9102,R9103,R9104,C9105,C9106,C9107,R9108,R9109,R9110,R9111,R9112,C9112
DF12_PYLINT = $(PUBLIC_GIT_SOURCE_ENV) $(UV_ENV) $(UV) tool run \
--python $(PYTHON_BASELINE) \
--from 'pylint' --with '$(DF12_PYTHON_LINTS)' pylint \
--disable=all --load-plugins=df12_python_lints \
--enable=$(DF12_PYLINT_MESSAGES)
AMBRLEAKS = $(PUBLIC_GIT_SOURCE_ENV) $(UV_ENV) $(UV) tool run \
--python $(PYTHON_BASELINE) \
--from '$(DF12_PYTHON_LINTS)' ambrleaks
# The shared en-GB-oxendict spelling gate. It regenerates `typos.toml` from
# the live shared dictionary and the `typos.local.toml` overlay on every run,
# then runs Typos and the prohibited-phrase check.
TYPOS_CONFIG_BUILDER = $(PUBLIC_GIT_SOURCE_ENV) $(UV_ENV) $(UV) tool run \
--python $(PYTHON_BASELINE) \
--from "git+https://github.com/leynos/typos-config-builder.git@$(TYPOS_CONFIG_BUILDER_VERSION)" \
typos-config-builder
PROVER_TOOLS_SOURCE ?= git+https://github.com/leynos/rust-prover-tools@2ba17da6b7e24160c4041e8d41ba1ba6cca87b35
PROVER_TOOLS ?= uv tool run --from $(PROVER_TOOLS_SOURCE) prover-tools
RUSTDOC_FLAGS ?= --cfg docsrs -D warnings
unexport RUSTDOC_FLAGS
export RUSTDOCFLAGS := $(value RUSTDOC_FLAGS)
VERUS_FLAGS ?=
VERUS_INSTALL_FLAGS ?=
WHITAKER ?= whitaker
# The build-tools install prefix leads: `-fuse-ld=mold` resolves by PATH order, so
# the pinned release must outrank any distribution `mold`. Every build target
# now links with it, so this is global rather than target-specific.
# GO_BIN is appended after the three fixed directories so a tool present in
# more than one location keeps its current precedence; CI's explicit
# `ACTIONLINT=` override still wins over all of them.
export PATH := $(BUILD_TOOLS_PREFIX)/bin:$(HOME)/.cargo/bin:$(HOME)/.local/bin:$(HOME)/.bun/bin:$(GO_BIN):$(PATH)
build: target/debug/$(APP) ## Build debug binary
release: target/release/$(APP) ## Build release binary
all: release ## Default target builds release binary
clean: ## Remove build artefacts
$(CARGO) clean
test: test-nextest doctest ## Run every Rust test with warnings treated as errors
test-nextest: check-build-tools ## Run all non-doctest Rust tests through cargo-nextest
$(GATE_RUSTFLAGS) $(CARGO) nextest run --workspace --all-targets --all-features $(NEXTEST_BUILD_JOBS) $(NEXTEST_TEST_JOBS)
.PHONY: test-rfc-stdlib-coverage
test-rfc-stdlib-coverage: check-build-tools ## Check the RFC 0006 split and its parsers
$(GATE_RUSTFLAGS) $(CARGO) nextest run --test rfc_stdlib_coverage_tests --all-features $(NEXTEST_BUILD_JOBS) $(NEXTEST_TEST_JOBS)
# A convenience selector over property-test suites, which `make test` already
# runs: the suites cost about 6 s of CPU against a 45 s budget, so a separate
# tier, a `PROPTEST_HEAVY` variable, and a dedicated CI job would all be
# branches no run ever takes.
#
# The selector lives in the recipe rather than in `.config/nextest.toml`, and
# that is forced rather than preferred. The configuration is held to the
# anchored-selector grammar by
# `tests/workflow_contracts/nextest_child_cargo_group_invariants.py`, whose
# `GROUP_FILTER` captures the name group after a *greedy* `MODULE_PATH`; a
# `src/`-rooted selector such as `test(/^ninja_gen::property_tests($|::)/)`
# therefore captures `property_tests`, and
# `test_filters_match_every_declared_test_they_name` fails it because
# `declared_test_names()` scans only `tests/` and declares no such name.
# Measured, not inferred: appending that override to the configuration and
# running the contract reports
# "filters name tests that no Rust source declares: ['property_tests']".
# The recipe is out of that contract's scope, so the selector is written here.
#
# The shape is a *segment* anchor, `(^|::)…($|::)`, not a leading anchor.
# A leading anchor selects nothing at all: the suites are reached through a
# module path (`ir::cmd_interpolate::property_tests::…`), so
# `test(/^property_tests($|::)/) | test(/^proptests($|::)/)` matches zero of
# the 90 selected tests. Both suffixes appear in the tree (`helper_proptests`
# and `hex::property_tests`), and they diverge at index four, so a single
# substring cannot cover both. Measured: this spelling selects 90 of the 3848
# tests `--workspace` lists, exactly the census of names carrying either
# suffix. Drop `--workspace` and the corpus and the count both shrink (3807
# and 86), which is why the discharge reads the recipe's own flags.
#
# Another agent's tests compiling concurrently are unaffected: this runs the
# same binaries `make test` does and uses the same build cache.
# The `$$` is load-bearing, not decoration. `$|` is Make's automatic variable
# for a target's order-only prerequisites, and it is empty here, so an
# unescaped `($|::)` is silently rewritten to `(::)`: the anchor branch that
# matches a bare suite name disappears, and only tests *inside* a submodule are
# selected. Measured with `make -n proptest` before escaping.
PROPTEST_FILTER := test(/(^|::)[a-z0-9_]*proptests($$|::)/) | test(/(^|::)[a-z0-9_]*property_tests($$|::)/)
proptest: check-build-tools ## Run the property-test suites alone
$(GATE_RUSTFLAGS) $(CARGO) nextest run --workspace --all-targets --all-features --no-tests fail -E '$(PROPTEST_FILTER)' $(NEXTEST_BUILD_JOBS) $(NEXTEST_TEST_JOBS)
test-documentation-contracts: check-build-tools ## Validate marked documentation examples and semantic contracts
$(GATE_RUSTFLAGS) $(CARGO) nextest run --test documentation_examples_loader_tests --test documentation_examples_tests --all-features --no-tests fail -E 'test(malformed_documented_examples_are_rejected) | test(every_documented_fence_has_a_known_unique_identifier) | test(help_targets_edges_example_matches_live_schema) | test(release_wix_extension)' $(NEXTEST_BUILD_JOBS) $(NEXTEST_TEST_JOBS)
doctest: check-build-tools ## Run doctests, which cargo-nextest cannot execute
$(GATE_RUSTFLAGS) $(CARGO) test --workspace --doc --all-features $(BUILD_JOBS)
# Cargo's publish verification: extract the packaged crate and compile it with
# the shipped default features. It runs here rather than in a test because the
# cost is a cold compile of the packaged crate, which no per-test allowance can
# bound; `tests/packaging_smoke_tests.rs` keeps the file-list assertions, which
# compile nothing. Not part of `make test`: CI runs it on the Linux lane, and
# what Cargo packages does not vary by platform. See ADR-0059.
test-package-verify: check-build-tools ## Verify that the packaged crate builds for publication
$(GATE_RUSTFLAGS) $(CARGO) publish --dry-run --allow-dirty -p netsuke-build $(BUILD_JOBS)
# The default-feature lane. Release binaries build the default feature set,
# which excludes `lint` (`netsuke check`) until v0.2.0, so these targets lint
# and test exactly that set; `lint-clippy` and `test` above cover
# `--all-features`. See ADR-056 and "Feature-gated code" in the developers'
# guide.
test-default-features: check-build-tools ## Run every Rust test with only the default features, as release binaries build
$(GATE_RUSTFLAGS) $(CARGO) nextest run --workspace --all-targets $(NEXTEST_BUILD_JOBS) $(NEXTEST_TEST_JOBS)
$(GATE_RUSTFLAGS) $(CARGO) test --workspace --doc $(BUILD_JOBS)
lint-default-features: check-build-tools ## Run rustdoc and Clippy with only the default features and warnings denied
$(GATE_RUSTFLAGS) $(CARGO) doc --workspace --no-deps
$(GATE_RUSTFLAGS) $(CARGO) clippy --workspace --all-targets -- -D warnings
test-kani-mutations: check-build-tools ## Compile each mutation patch's patched tree under the Kani configuration
$(GATE_RUSTFLAGS) $(CARGO) nextest run --test kani_mutation_evidence_tests --all-features --run-ignored ignored-only $(NEXTEST_BUILD_JOBS) $(NEXTEST_TEST_JOBS)
# The first uv run mirrors the exact Cuprum/Cyclopts pins in
# scripts/kani_proof_scope.py's PEP 723 metadata. The second uses compatible
# bounded ranges for those tools.
test-workflow-contracts: ## Validate the CV-005 contracts and the GitHub Actions workflow contracts
$(CV005_CONTRACTS) check --repository .
$(UV_ENV) $(UV) run --no-project --python $(PYTHON_BASELINE) \
--with 'pytest>=8' --with 'pyyaml>=6' --with 'hypothesis>=6' \
--with 'cmd-mox==0.2.0' --with 'cuprum==0.1.0' \
--with 'cyclopts==4.25.3' pytest tests/workflow_contracts -q --doctest-modules
PYTHONPATH=scripts $(UV_ENV) $(UV) run --no-project --python $(PYTHON_BASELINE) \
--with 'pytest>=8' --with 'hypothesis>=6' --with 'cmd-mox==0.2.0' \
--with 'cyclopts>=4.25.3,<5' --with 'cuprum>=0.1.0,<0.2.0' \
pytest scripts/tests/test_ensure_draft_release.py \
scripts/tests/test_install_orthohelp.py \
scripts/tests/test_report_glibc_floor.py \
scripts/tests/test_resolve_wix_extension_version.py \
scripts/ensure_draft_release.py scripts/install_orthohelp.py \
scripts/report_glibc_floor.py scripts/resolve_wix_extension_version.py \
scripts/hoist_binstall_archives.py \
scripts/hoist_binstall_discovery.py -q --doctest-modules
test-windows-msi-release-rank: ## Validate Windows MSI release-rank parsing
@PYTHONPATH=scripts $(UV_ENV) $(UV) run --no-project --python $(PYTHON_BASELINE) \
--with pytest==9.0.2 --with hypothesis==6.151.9 \
python -m pytest scripts/tests/test_windows_msi_release_rank.py
test-release-admission: ## Validate the release-admission runtime contract
@PYTHONPATH=scripts $(UV_ENV) $(UV) run --no-project --python $(PYTHON_BASELINE) \
--with pytest==9.0.2 --with hypothesis==6.151.9 \
--with 'cmd-mox==0.2.0' --with 'cuprum==0.1.0' --with 'cyclopts==4.25.3' \
python -m pytest scripts/tests/test_release_admission_metrics.py \
scripts/tests/test_release_admission_metric_failures.py \
scripts/tests/test_release_admission_metric_boundedness.py \
scripts/tests/test_release_admission_process_bounds.py \
scripts/tests/test_release_admission_environment_contract.py \
scripts/tests/test_release_admission_configuration_resolution.py \
scripts/tests/test_release_admission_artefact_initialization.py \
scripts/tests/test_release_admission_artefact_truncation.py \
scripts/tests/test_release_admission_default_forms.py \
scripts/tests/test_release_admission_adapter_defaults.py \
scripts/tests/test_release_admission_status_translation.py \
scripts/tests/test_release_admission_runner_contract.py \
scripts/tests/test_release_admission_metric_degradation.py \
scripts/tests/test_release_admission_sink_persistence.py -c /dev/null \
--rootdir=. -p no:cacheprovider
test-coverage-artifact: ## Test hostile LCOV artefact validation
@PYTHONPATH=scripts $(UV_ENV) $(UV) run --no-project --python $(PYTHON_BASELINE) \
--with pytest==9.0.2 python -m pytest scripts/tests/test_validate_coverage_artifact.py \
scripts/tests/test_validate_coverage_archive.py -c /dev/null --rootdir=. \
-p no:cacheprovider
test-linux-package-metadata: ## Test Linux package metadata reading and validation
@PYTHONPATH=scripts $(UV_ENV) $(UV) run --no-project --python $(PYTHON_BASELINE) \
--with pytest==9.0.2 --with hypothesis==6.151.9 python -m pytest \
scripts/tests/test_cargo_package_metadata.py \
scripts/tests/test_validate_linux_package_metadata.py -c /dev/null --rootdir=. \
-p no:cacheprovider
test-ci-scripts: ## Test the CI helper scripts under scripts/ci with pytest and cmd-mox
@PYTHONPATH=scripts/ci:scripts $(UV_ENV) $(UV) run --no-project --python $(PYTHON_BASELINE) \
--with pytest==9.0.2 --with cmd-mox==0.2.0 --with hypothesis==6.151.9 \
$(CI_SCRIPT_DEPS) \
python -m pytest -p cmd_mox.pytest_plugin \
scripts/tests/test_ci_archive.py \
scripts/tests/test_ci_containment.py \
scripts/tests/test_ci_discard_instrumented_tree.py \
scripts/tests/test_ci_install_actionlint.py \
scripts/tests/test_ci_install_actionlint_cache.py \
scripts/tests/test_ci_install_kani.py \
scripts/tests/test_ci_report_sccache_stats.py \
scripts/tests/test_ci_scripts_metadata.py \
scripts/tests/test_ci_stage_test_shell.py \
scripts/tests/test_ci_support.py \
-c /dev/null --rootdir=. -p no:cacheprovider
# Release-policy modules receive names, digests, mappings, or typed transport
# observations. CLI and adapter modules own file, JSON, TOML, and provider
# formats; the suites run beside the other script tests without a network job.
#
# The source modules are named here as well as their suites, and the run
# carries --doctest-modules, so the examples in their docstrings execute.
# Without the flag pytest collects test functions only and those examples are
# prose nothing runs: deleting it breaks no import and changes no test, which
# is how they would silently stop running. `test-workflow-contracts` pairs test
# modules with doctested sources the same way.
test-release-publication: ## Test release-candidate identity and staged inventory exactness
@PYTHONPATH=scripts $(UV_ENV) $(UV) run --no-project --python $(PYTHON_BASELINE) \
--with pytest==9.0.2 --with hypothesis==6.151.9 python -m pytest \
scripts/tests/test_release_candidate.py \
scripts/tests/test_release_candidate_cli.py \
scripts/tests/test_package_crate_cli.py \
scripts/tests/test_package_crate_cli_errors.py \
scripts/tests/test_publish_crates_io_cli.py \
scripts/tests/test_verify_release_inventory_cli.py \
scripts/tests/test_verify_release_inventory_command.py \
scripts/tests/test_finalize_github_release_cli.py \
scripts/tests/test_release_inventory.py \
scripts/tests/test_release_identity_record.py \
scripts/tests/test_release_semver.py \
scripts/tests/test_release_digest.py \
scripts/tests/test_release_digest_files.py \
scripts/tests/test_verify_crates_io_bytes.py \
scripts/tests/test_release_crates_io_adapter.py \
scripts/tests/test_release_github_assets_adapter.py \
scripts/tests/test_release_reconcile_crates.py \
scripts/tests/test_release_reconcile_crates_properties.py \
scripts/tests/test_release_reconcile_github.py \
scripts/tests/test_release_declaration.py \
scripts/tests/test_release_declaration_cli.py \
scripts/tests/test_release_declaration_invalid_config.py \
scripts/tests/test_release_digest_record.py \
scripts/tests/test_release_digest_record_files.py \
scripts/tests/test_release_staged_digests.py \
scripts/release_candidate.py scripts/release_candidate_cli.py \
scripts/release_identity_record.py \
scripts/package_crate_cli.py scripts/publish_crates_io_cli.py \
scripts/release_crates_io_adapter.py \
scripts/release_github_assets_adapter.py \
scripts/verify_release_inventory_cli.py \
scripts/finalize_github_release_cli.py \
scripts/release_inventory.py \
scripts/release_semver.py scripts/release_digest.py \
scripts/release_digest_files.py \
scripts/verify_crates_io_bytes.py \
scripts/release_registry_response.py \
scripts/release_reconcile_crates.py \
scripts/release_reconcile_github.py \
scripts/release_asset_names.py scripts/release_package_formats.py \
scripts/release_declaration.py scripts/release_declaration_cli.py \
scripts/release_digest_record.py scripts/release_digest_record_files.py \
scripts/release_staged_digests.py \
-c /dev/null --rootdir=. \
-p no:cacheprovider --doctest-modules
# Split rather than a single `target/%/$(APP)` pattern: the two profiles no
# longer share a command line. The debug build takes the standard; the release
# build is one of the two exclusions.
#
# Both select the whole workspace rather than `--bin $(APP)`. The workspace has
# no `default-members`, so a package-scoped build compiles only `netsuke-build`
# and never `build_slice_fixture`; the fixture is what compiles the production
# build-script module slice, so a package-scoped build would leave that slice
# compiled by nothing but the test commands. See ADR-0051.
target/debug/$(APP): | check-build-tools ## Build the debug binary
$(DEBUG_RUSTFLAGS) $(CARGO) build --workspace $(BUILD_JOBS)
target/release/$(APP): ## Build the release binary on the LLVM backend
$(RELEASE_RUSTFLAGS) $(CARGO) build --workspace $(BUILD_JOBS) --release
lint: lint-clippy lint-whitaker lint-python github-actions-lint ## Run the Rust, Python, and GitHub Actions lint suites with warnings denied
lint-python: lint-workflow-scripts ## Run Ruff, Pylint, Interrogate, the df12 house lints, and ambrleaks over the Python sources
$(RUFF) check $(PYTHON_SOURCES)
$(PYLINT) $(PYLINT_TARGETS)
$(DF12_PYLINT) $(PYLINT_TARGETS)
$(AMBRLEAKS) $(PYTHON_SOURCES)
$(INTERROGATE) $(PYTHON_SOURCES)
lint-workflow-scripts: ## Load every trusted workflow module under the Python baseline
# The trusted coverage workflow runs these through GitHub Actions' `python`
# shell, and tests/workflow_contracts/python_shell_interpreter_test.py
# holds that shell to the baseline. Ruff and ty read the modules; loading
# them catches a definition-time failure -- a name used at module scope, an
# invalid decorator -- which no pull request can fix once it is on main,
# because the workflow runs the default branch's copy.
#
# Loading does NOT catch an annotation naming a TYPE_CHECKING-only import,
# which this comment claimed until the baseline moved to 3.14: PEP 649
# defers annotation evaluation, so such a module loads cleanly and fails
# only when something resolves the annotation. The claim held under 3.12
# and no longer does. Catch it by calling typing.get_type_hints, not by
# loading; see
# docs/adrs/adr-0038-runtime-annotation-introspection-in-workflow-contracts.md.
@for module in .github/scripts/*.py; do \
$(UV_ENV) $(UV) run --no-project --python $(PYTHON_BASELINE) python -c \
'import runpy, sys; runpy.run_path(sys.argv[1], run_name="lint_workflow_scripts")' \
"$$module" || { echo "$$module does not load under Python $(PYTHON_BASELINE)" >&2; exit 1; }; \
done
lint-clippy: check-build-tools ## Run rustdoc and Clippy with warnings denied
$(GATE_RUSTFLAGS) $(CARGO) doc --workspace --no-deps
$(GATE_RUSTFLAGS) $(CARGO) clippy $(CLIPPY_FLAGS)
lint-whitaker: check-build-tools ## Run the Whitaker Dylint suite with warnings denied
DYLINT_TOML="$$(cat dylint.toml)" $(GATE_RUSTFLAGS) $(WHITAKER) --all --no-deps --package netsuke-build -- --all-targets --all-features
# Run from the crate directory as well so Whitaker loads the narrow
# `test_support::fs` exemption from test_support/dylint.toml.
cd test_support && DYLINT_TOML="$$(cat dylint.toml)" $(GATE_RUSTFLAGS) $(WHITAKER) --all --no-deps --package test_support -- --all-targets --all-features
# actionlint is resolved in the recipe shell below, never while Make parses the
# file: only the recipe shell receives the curated PATH, so a parse-time probe
# can report a Go-installed actionlint as missing. Checking in the shell also
# keeps the diagnostic beside the invocation it explains.
github-actions-lint: ## Validate GitHub Actions workflows
$(YAMLLINT) --config-file .yamllint.yml .github/workflows
@command -v "$$ACTIONLINT" >/dev/null 2>&1 || { \
printf '%s\n' \
"actionlint could not be run: ACTIONLINT is '$$ACTIONLINT'." \
"Install it with go (go install github.com/rhysd/actionlint/cmd/actionlint@latest)," \
"which writes $$GO_BIN/actionlint, or set ACTIONLINT=/path/to/actionlint." >&2; \
exit 1; \
}
$(ACTIONLINT)
doc-coverage: doc-coverage-test ## Verify aggregate Rustdoc doc-comment coverage meets the threshold
# Runs under the uv-pinned baseline interpreter, not the system python3:
# the scripts target Python 3.14 syntax and semantics.
@$(UV_ENV) $(UV) run --no-project --python $(PYTHON_BASELINE) \
scripts/doc-coverage.py --toolchain "$$DOC_COVERAGE_TOOLCHAIN" --threshold "$$DOC_COVERAGE_THRESHOLD"
doc-coverage-test: ## Run documentation-coverage pytest modules
@PYTHONPATH=scripts $(UV_ENV) $(UV) run --no-project --python $(PYTHON_BASELINE) \
--with pytest==9.0.2 --with pytest-cov==7.0.0 --with 'hypothesis>=6' \
python -m pytest scripts/tests/test_doc_coverage_model.py \
scripts/tests/test_doc_coverage_cargo.py \
scripts/tests/test_doc_coverage_cargo_payload.py \
scripts/tests/test_doc_coverage_runner.py \
scripts/tests/test_doc_coverage.py -c /dev/null --rootdir=. \
-p no:cacheprovider --cov=doc_coverage_model --cov=doc_coverage_cargo \
--cov=doc_coverage_runner --cov=doc_coverage_module
validate-coverage-artifact: ## Validate downloaded coverage artefact as hostile data
$(UV_ENV) $(UV) run --no-project --python $(PYTHON_BASELINE) \
scripts/validate_coverage_archive.py \
--archive-dir "$(COVERAGE_ARTIFACT_DIR)" \
--output-dir validated-coverage
fmt: ## Format Rust, Python, and Markdown sources
$(CARGO) fmt --all
$(RUFF) format $(PYTHON_SOURCES)
$(RUFF) check --select I --fix $(PYTHON_SOURCES)
$(MDTABLEFIX) --in-place $(MDTABLEFIX_SELECT) $(MDTABLEFIX_RULES)
@unset FORCE_COLOR; $(MDLINT) --fix "**/*.md"
check-fmt: ## Verify formatting
$(CARGO) fmt --all -- --check
$(RUFF) format --check $(PYTHON_SOURCES)
$(MDTABLEFIX) --check $(MDTABLEFIX_SELECT) $(MDTABLEFIX_RULES)
typecheck: check-build-tools typecheck-python ## Typecheck all targets and features
$(GATE_RUSTFLAGS) $(CARGO) check --workspace --all-targets --all-features $(BUILD_JOBS)
typecheck-python: ## Typecheck the Python sources with ty
# `uv tool run` materialises one venv holding ty plus the test-suite
# dependencies, so ty can resolve third-party imports. `uv run --with`
# would layer the extras through `.pth` chaining, which ty cannot follow.
# Standalone scripts declare these runtime dependencies in PEP 723 blocks,
# which ty does not read, so include them in its environment explicitly.
# The bounded ranges name the band each pin sits in, so this one
# resolution typechecks every script and helper together: the CI helper
# scripts pin cyclopts and cuprum exactly, and `scripts/kani_proof_scope.py`
# declares its own pins, which the Kani workflow-contract test holds this
# target to. Both `scripts` and `.github/scripts` are named as search
# roots because the contract tests import from them through a `sys.path`
# insert, which ty does not follow; without the roots those imports read
# as unresolved.
$(UV_ENV) $(UV) tool run --python $(PYTHON_BASELINE) \
--from ty==$(TY_VERSION) --with pytest==9.0.2 --with pytest-cov==7.0.0 \
--with 'pyyaml>=6' --with 'hypothesis>=6' --with 'cmd-mox==0.2.0' \
--with 'cyclopts>=4.25.3,<5' --with 'cuprum>=0.1.0,<0.2.0' \
ty check --python-version $(PYTHON_BASELINE) \
--extra-search-path scripts/ci --extra-search-path scripts \
--extra-search-path .github/scripts \
$(PYTHON_SOURCES)
markdownlint: spelling ## Lint Markdown and enforce en-GB-oxendict spelling
@unset FORCE_COLOR; $(MDLINT) "**/*.md"
spelling: ## Enforce en-GB-oxendict spelling
$(TYPOS_CONFIG_BUILDER) gate --repository .
nixie: ## Validate Mermaid diagrams
nixie --no-sandbox
install-kani: ## Install the pinned Kani verifier
@printf 'prover-tools: source=%s\n' '$(PROVER_TOOLS_SOURCE)' >&2
@printf 'prover-tools: target=install-kani kani-version=%s\n' "$$(cat '$(KANI_VERSION_FILE)')" >&2
@printf 'prover-tools: command=%s\n' '$(PROVER_TOOLS) kani install <redacted-flags>' >&2
@$(PROVER_TOOLS) kani install $(KANI_INSTALL_FLAGS) || { status=$$?; printf 'prover-tools: target=install-kani failed exit=%s\n' "$$status" >&2; exit "$$status"; }
kani-check: ## Check the installed Kani verifier version
@printf 'prover-tools: source=%s\n' '$(PROVER_TOOLS_SOURCE)' >&2
@printf 'prover-tools: target=kani-check kani-command=%s kani-version=%s\n' '$(KANI)' "$$(cat '$(KANI_VERSION_FILE)')" >&2
@printf 'prover-tools: command=%s\n' '$(PROVER_TOOLS) kani check-version --kani-command <redacted-command> <redacted-flags>' >&2
@$(PROVER_TOOLS) kani check-version --kani-command "$(KANI)" $(KANI_CHECK_FLAGS) || { status=$$?; printf 'prover-tools: target=kani-check failed exit=%s\n' "$$status" >&2; exit "$$status"; }
kani-full: ## Run the full Kani verification suite
$(KANI_RUSTFLAGS) $(KANI) $(KANI_FLAGS)
kani-ir: kani-full ## Run the IR Kani verification suite
# The scope wrapper's own end-to-end suite. It drives `systemd-run --user
# --scope` at test-sized limits rather than the documented eight minutes, so it
# needs no Kani install. The strict variable is what makes this lane meaningful:
# without it a host lacking a per-user systemd manager would skip every scenario
# and still report green, which would be coverage in name only. See
# tests/kani_scope_wrapper_e2e_tests.rs for the prerequisites.
#
# It takes `KANI_RUSTFLAGS`, not `GATE_RUSTFLAGS`, and declares no
# `check-build-tools` prerequisite. Both would be wrong here: the lane that runs
# this is `kani-smoke`, which builds on the stable toolchain, so `-Zthreads` is
# rejected outright and the capability check would fail before a single scenario
# ran. The suite is an ordinary test binary with no need for the build standard's
# linker or frontend, so it takes the same flags the Kani targets do. The lane
# does install the build standard, because every Linux job that runs the nextest
# suite must -- but that is `nextest_lane_mold_test.py`'s rule about the lane,
# not a prerequisite of this target, and this target must stay reachable on a
# host that has only `rustup`.
KANI_SCOPE_WRAPPER_STRICT ?= 1
test-kani-scope-wrapper: ## Verify the Kani scope wrapper end to end
NETSUKE_KANI_SCOPE_WRAPPER_STRICT=$(KANI_SCOPE_WRAPPER_STRICT) \
$(KANI_RUSTFLAGS) $(CARGO) nextest run --test kani_scope_wrapper_e2e_tests $(NEXTEST_BUILD_JOBS) $(NEXTEST_TEST_JOBS)
install-verus: ## Install the pinned Verus verifier
@printf 'prover-tools: source=%s\n' '$(PROVER_TOOLS_SOURCE)' >&2
@printf 'prover-tools: target=install-verus\n' >&2
@printf 'prover-tools: command=%s\n' '$(PROVER_TOOLS) verus install <redacted-flags>' >&2
@$(PROVER_TOOLS) verus install $(VERUS_INSTALL_FLAGS) || { status=$$?; printf 'prover-tools: target=install-verus failed exit=%s\n' "$$status" >&2; exit "$$status"; }
verus: ## Run the Verus proof entry point
@printf 'prover-tools: source=%s\n' '$(PROVER_TOOLS_SOURCE)' >&2
@printf 'prover-tools: target=verus\n' >&2
@printf 'prover-tools: command=%s\n' '$(PROVER_TOOLS) verus run <redacted-flags>' >&2
@$(PROVER_TOOLS) verus run $(VERUS_FLAGS) || { status=$$?; printf 'prover-tools: target=verus failed exit=%s\n' "$$status" >&2; exit "$$status"; }
formal-pr: ## Run pull-request formal-verification checks
$(MAKE) kani-check
install-build-tools: ## Install the pinned mold linker and the pinned toolchain
@scripts/install-build-tools.sh
check-build-tools: ## Check the mold linker and toolchain prerequisites
@scripts/check-build-tools.sh
bench-build: check-build-tools ## Time clean and incremental debug builds for all three paths
@CARGO="$(CARGO)" STANDARD_THREADS_FLAG="$(STANDARD_THREADS_FLAG)" \
STANDARD_MOLD_FLAG="$(STANDARD_MOLD_FLAG)" scripts/bench-build.sh
bench-config-load: ## Benchmark cached configuration loading without layer copies
$(CARGO) bench --bench config_load_cached_merge
bench-glob-expansion: ## Benchmark manifest glob expansion with an injected base
$(CARGO) bench --bench glob_expansion
help: ## Show available targets
@grep -E '^[a-zA-Z_-]+:.*?##' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS=":"; printf "Available targets:\n"} {printf " %-20s %s\n", $$1, $$2}'