Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
1278 lines (1150 loc) · 67.9 KB
/
Copy pathMakefile
File metadata and controls
1278 lines (1150 loc) · 67.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
.PHONY: all build fixtures release install test-install test test-cxx test-voicematch test-slow test-golden test-optional-fixtures test-librosa-live clean rebuild format format-check lint wasm coverage \
coverage-build coverage-clean build-shared build-node build-wasm-binding \
test-python test-python-slow test-node test-wasm parity conformance test-gm-cross-surface test-mix-assistant-cross-surface abi-layout abi-layout-check check-abi-version \
capability-catalog capability-catalog-check processor-types processor-types-check ci-local \
build-bank-shared bank-versions bank-versions-check \
surface-coverage surface-coverage-check \
gs-census gs-census-header gs-census-check gs-program-census gs-address-table-json gs-unit-archive-set gs-unit-diff gs-unit-diff-check \
gs-efx-archive-set gs-efx-tables gs-efx-tables-check gs-efx-coverage \
gs-classic-models gs-classic-check gs-classic-overlays-check gs-efx-soundings-drift \
gs-efx-bindings gs-efx-bindings-check gs-efx-join gs-efx-join-check \
test-hardening test-hardening-asan test-hardening-tsan test-hardening-host test-hardening-wasm \
build-feature-matrix accuracy-report voice-gate voice-status voice-status-all \
voice-readiness voice-status-refresh voice-status-check spec-check \
voicematch-substitution voicematch-substitution-all voicematch-determinism \
voicematch-reextract-check voicematch-sf2-corpus voicematch-sustain-check \
voicematch-loss-sensitivity voicematch-loss-cells \
spec-liveness spec-liveness-census spec-liveness-census-check \
excerpts excerpts-check test-voicematch \
check-c-api-out-param-init check-c-api-pointer-contracts check-c-api-header-self-contained \
check-c-api-type-home check-binding-warning-flags
BUILD_DIR ?= build
OPTIONAL_FIXTURE_BUILD_DIR := build-optional-fixtures
GOLDEN_BUILD_DIR := build-golden
ACCURACY_REPORT_JSON ?= $(CURDIR)/build-optional-fixtures/accuracy-report.json
INSTALL_PREFIX_DIR := $(CURDIR)/build-install-prefix
RYE ?= rye
CMAKE ?= cmake
# Compile workers for every target that builds into a private -B directory.
# An explicit --parallel outranks CMAKE_BUILD_PARALLEL_LEVEL, so honour that
# variable here or the documented way to cap a shared machine silently loses.
HARDENING_JOBS ?= $(if $(CMAKE_BUILD_PARALLEL_LEVEL),$(CMAKE_BUILD_PARALLEL_LEVEL),2)
# Compile workers for the ordinary build targets. A bare `-j` outranks
# CMAKE_BUILD_PARALLEL_LEVEL the same way an explicit --parallel does, and it
# means "unbounded" to the underlying make, so pass the level through when one
# is set and fall back to the native default when it is not.
BUILD_PARALLEL := $(if $(CMAKE_BUILD_PARALLEL_LEVEL),--parallel $(CMAKE_BUILD_PARALLEL_LEVEL),-j)
UV_CACHE_DIR ?= $(CURDIR)/.uv-cache
PYTHON_PKG_DIR := bindings/python/src/libsonare
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
SHARED_LIB := $(BUILD_DIR)/lib/libsonare.dylib
PYTHON_SHARED_LIB := $(PYTHON_PKG_DIR)/libsonare.dylib
HARDENING_ASAN_OPTIONS := strict_string_checks=1
else
SHARED_LIB := $(BUILD_DIR)/lib/libsonare.so
PYTHON_SHARED_LIB := $(PYTHON_PKG_DIR)/libsonare.so
HARDENING_ASAN_OPTIONS := detect_leaks=1:strict_string_checks=1
endif
all: build
build:
$(CMAKE) -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=Debug
$(CMAKE) --build $(BUILD_DIR) $(BUILD_PARALLEL)
release:
$(CMAKE) -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=Release
$(CMAKE) --build $(BUILD_DIR) $(BUILD_PARALLEL)
# Install the C++ library, its headers, the CMake package files and the native
# CLI under CMAKE_INSTALL_PREFIX (/usr/local by default; override with
# `make install CMAKE_INSTALL_PREFIX=~/.local`). Retargets build/ to Release,
# same as `make release`.
install: release
$(CMAKE) --install $(BUILD_DIR)
# Gate for the installed package. Installs into a scratch prefix, then
# configures a consumer project that knows nothing but find_package(sonare) and
# builds it. This is the only check that can see the defects an in-tree build
# hides: a source-tree path leaking into an exported target, a header the
# install rules miss, an archive whose declared link interface is incomplete.
# BUILD_SHARED is on so the run also covers the shared artifact and sonare.pc.
# The build tree is reused between runs; the prefix and the consumer tree are
# rebuilt from scratch so a removed file cannot survive as a stale copy.
test-install:
rm -rf $(INSTALL_PREFIX_DIR) build-install-consumer
$(CMAKE) -B build-install -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF \
-DBUILD_SHARED=ON -DCMAKE_INSTALL_PREFIX=$(INSTALL_PREFIX_DIR)
$(CMAKE) --build build-install $(BUILD_PARALLEL)
$(CMAKE) --install build-install
$(CMAKE) -S tests/cmake/consumer -B build-install-consumer \
-DCMAKE_PREFIX_PATH=$(INSTALL_PREFIX_DIR)
$(CMAKE) --build build-install-consumer $(BUILD_PARALLEL)
ctest --test-dir build-install-consumer --output-on-failure --no-tests=error
# Delegates instead of configuring a tree of its own, because the feature
# flags, the output name and the analysis-only switch that decide WHICH module
# gets built live in bindings/wasm/package.json, while the post-build step
# copies whatever was built over the dist/ every surface's tests import. It
# also covers both bundles: the freshness guard checks each, so building one
# leaves the other stale and the suite refuses to start.
wasm:
cd bindings/wasm && yarn build
# The K-weighting cases compare against a reference this script computes, and
# the file it writes is gitignored, so a fresh checkout has no copy of it. The
# cases fail rather than skip when it is absent, so every target that runs them
# generates it first and CI invokes this same target rather than repeating the
# command.
fixtures:
python3 tools/scripts/k_weighting_reference.py
# The C++ suite on its own: cmake, a C++17 compiler and python3, with no Node
# and no rye. A change confined to the core can be verified with this, which is
# what CONTRIBUTING points a contributor at.
test-cxx: build fixtures
ctest --test-dir $(BUILD_DIR) --output-on-failure --parallel
# The full local run. The calibration harness rides on it because `tools/` is
# outside ctest and outside the drift gates, so a harness left to be remembered
# is a harness that rots -- but it needs rye, which a contributor touching only
# C++ has no other reason to install, so its absence reports itself and does not
# fail a green C++ run. CI covers the harness in a build-free job of its own, so
# nothing goes unchecked when this branch is taken.
test: test-cxx
@if command -v $(RYE) >/dev/null 2>&1; then \
$(MAKE) test-voicematch; \
else \
echo "note: skipping the calibration harness -- $(RYE) is not installed."; \
echo " run 'make test-voicematch' once it is, or let CI cover it."; \
fi
# The cases read only the tracked capture definitions and reference profiles --
# no rendered corpus, no plugin, no built library -- so they pass on a fresh
# clone in about half a minute. Invoked directly, this reports a missing rye as
# the error it is; `test` is the target that treats it as a skip.
test-voicematch:
$(RYE) run --pyproject bindings/python/pyproject.toml python -m pytest tools/voicematch tools/audition -q
# Heavy cases (>~2 s each) are tagged [.][slow] and hidden from the default
# ctest run; this runs just those. Must run from the repo root (librosa
# fixtures load by relative path).
test-slow: build fixtures
./$(BUILD_DIR)/bin/sonare_tests "[slow]"
# Golden regressions are hidden from the default Catch2 run so they can be
# invoked explicitly in local development and CI.
#
# Release, in its own directory, for two independent reasons. The recorded
# digests come from a Release build, and comparing them against a Debug one asks
# whether two optimization levels agree rather than whether the code changed.
# And $(BUILD_DIR) is shared: depending on `build` would reconfigure whatever
# another session has in it, mid-run, as a side effect of running a test.
test-golden: fixtures
$(CMAKE) -B $(GOLDEN_BUILD_DIR) -DCMAKE_BUILD_TYPE=Release
$(CMAKE) --build $(GOLDEN_BUILD_DIR) $(BUILD_PARALLEL) --target sonare_tests
./$(GOLDEN_BUILD_DIR)/bin/sonare_tests "[golden]"
test-optional-fixtures:
$(CMAKE) -B $(OPTIONAL_FIXTURE_BUILD_DIR) -DCMAKE_BUILD_TYPE=Debug -DSONARE_ENABLE_OPTIONAL_FIXTURE_TESTS=ON
$(CMAKE) --build $(OPTIONAL_FIXTURE_BUILD_DIR) $(BUILD_PARALLEL)
ctest --test-dir $(OPTIONAL_FIXTURE_BUILD_DIR) --output-on-failure -R "optional|fixture|EBU R128" --parallel
# Measures musical accuracy against whatever corpus the music_eval manifests
# point at and rolls it up into a publishable table. Distinct from
# test-optional-fixtures, which gates: only report_only manifest rows produce
# the observations this reads. Reports "unmeasured" rather than a score for a
# dimension with no rows. See tools/eval/README.md.
accuracy-report:
$(CMAKE) -B $(OPTIONAL_FIXTURE_BUILD_DIR) -DCMAKE_BUILD_TYPE=Release -DSONARE_ENABLE_OPTIONAL_FIXTURE_TESTS=ON
$(CMAKE) --build $(OPTIONAL_FIXTURE_BUILD_DIR) $(BUILD_PARALLEL)
python3 tests/fixtures/run_optional_fixture_report.py --suite music \
--sonare-tests $(OPTIONAL_FIXTURE_BUILD_DIR)/bin/sonare_tests \
--output $(ACCURACY_REPORT_JSON)
python3 tools/eval/summarize_accuracy.py $(ACCURACY_REPORT_JSON) --markdown
test-librosa-live: build
$(RYE) sync --pyproject tests/librosa/pyproject.toml
$(RYE) run --pyproject tests/librosa/pyproject.toml python -m ensurepip --upgrade
$(RYE) run --pyproject tests/librosa/pyproject.toml python -m pip install --no-build-isolation ../librosa
$(RYE) run --pyproject tests/librosa/pyproject.toml python tests/librosa/run_live_reference_check.py --build-dir $(BUILD_DIR)
clean:
rm -rf build build-*/ cmake-build-*/ build-*.log
rm -rf bindings/node/build
rm -rf bindings/wasm/build-wasm bindings/wasm/build-wasm-bench
rebuild: clean build
# `format` applies every auto-fixable change the CI lint gate checks, then runs
# `lint` to verify. The binding steps use `lint:fix` (biome check --write), not
# `yarn format` (biome format --write): the former also applies import
# organization and the safe lint fixes that `yarn lint` (biome check) enforces in
# CI, so `make format` can no longer succeed while CI lint would fail. Anything
# left (e.g. unused imports, an unsafe fix biome will not auto-apply) surfaces in
# the final `lint` step for manual resolution.
#
# Every writing step is fed a tracked file list from git, never a directory. A
# directory argument cannot tell a file you just wrote from one another worktree
# is still writing, and a file that exists in no commit is the one write class
# with nothing to diff against. Format a brand-new file by naming it, or stage it
# first -- `ls-files` reads the index, so `git add` opts it in. The lists are
# generated, so a new source tree is picked up without editing this file, and the
# two binding scripts own their own so `yarn lint:fix` is safe to run directly.
# `format-check` covers the same files and only reads; point CI and any caller
# that does not own the worktree at that instead.
RUFF_FORMAT_PATHS = 'bindings/python/src/*.py' 'bindings/python/src/*.pyi' 'bindings/python/tests/*.py' 'tools/*.py' 'tools/*.pyi'
LS_EXISTING = python3 -c 'import os, sys; paths = [p for p in sys.stdin.buffer.read().split(b"\0") if p and os.path.exists(os.fsdecode(p))]; sys.stdout.buffer.write(b"\0".join(paths) + (b"\0" if paths else b""))'
format:
git ls-files -z -- '*.h' '*.hpp' '*.c' '*.cpp' '*.mm' ':!:third_party/**' | $(LS_EXISTING) | xargs -0 clang-format -i
cd bindings/wasm && yarn lint:fix
cd bindings/node && yarn lint:fix
UV_CACHE_DIR=$(UV_CACHE_DIR) $(RYE) sync --pyproject bindings/python/pyproject.toml
git ls-files -z -- $(RUFF_FORMAT_PATHS) | $(LS_EXISTING) | xargs -0 env UV_CACHE_DIR=$(UV_CACHE_DIR) $(RYE) run --pyproject bindings/python/pyproject.toml ruff format
git ls-files -z -- '*.py' '*.pyi' '*pyproject.toml' | $(LS_EXISTING) | xargs -0 env UV_CACHE_DIR=$(UV_CACHE_DIR) $(RYE) run --pyproject bindings/python/pyproject.toml ruff check --fix
$(MAKE) lint
# `test:types` type-checks the Node binding's tests against src (biome does not
# type-check, and the build tsconfig excludes tests). It reads sources only —
# no built addon or dist/ needed — so it belongs with the other static gates.
#
# Ruff lints the repo rather than a path list. Python lives in ten trees here —
# the binding, `tools/`, `benchmarks/`, `examples/python/` and four under
# `tests/` — and a list of them is a hand-maintained index that a new tree drops
# out of silently. This target only reads, so it takes `.`; the auto-fix side
# derives the same population from `git ls-files` because it writes. `ruff
# format` covers the binding and `tools/` (`RUFF_FORMAT_PATHS`, shared with
# `format-check`); the remaining trees keep hand-set line breaks.
#
# The sample-rate rule, stated in src/midi/synth/docs/gs.md and CONTRIBUTING.md: a
# quantity measured at the machine's 32 kHz internal clock must be stored in
# physical units (Hz/ms/s/ratio/dB), never as a sample-rate-dependent
# coefficient. This is not a style preference -- it is the exact shape of a
# defect this tree already shipped once (a waveguide loop filter's brightness
# mapping with no `sr` term at all, correct only at 48 kHz). No test can
# enforce it (a coefficient and the physical quantity it came from both
# compile and both run), so this is a grep over src/effects/**,
# src/mastering/** and src/playback/**: no `*Config` struct field named for a coefficient shape
# (coeff/b1/a1/alpha). (A second rule that flagged `std::exp(` combined with
# `sample_rate` was tried and dropped -- that combination is the CORRECT
# pattern, not the defective one, so the rule could only ever fire on code
# already doing the right thing. The two-sample-rate behavioural test,
# tests/effects/insert_sample_rate_test.cpp, is what actually catches this
# defect class.) One field predates this rule and is not a sample-rate
# coefficient despite the name match; allowlisted by exact field name with a
# reason, retired the day the field is renamed or becomes rate-derived.
define GS_EFX_SR_COEFFICIENT_LINT_PY
import pathlib, re, sys
ROOTS = [pathlib.Path("src/effects"), pathlib.Path("src/mastering"), pathlib.Path("src/playback")]
FILES = sorted(p for r in ROOTS for p in r.rglob("*") if p.suffix in (".h", ".hpp", ".cpp"))
FIELD_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(coeff|Coeff|b1|a1|alpha|Alpha)[A-Za-z0-9_]*\s*[;=]")
STRUCT_RE = re.compile(r"\bstruct\s+\w*Config\b")
ALLOWED_FIELDS = {
("src/mastering/repair/denoise_classical.h", "dd_alpha"):
"Ephraim-Malah frame-to-frame smoothing factor, a literature constant "
"with no sample_rate term -- not a filter coefficient.",
}
violations = []
scanned = 0
for path in FILES:
scanned += 1
posix = path.as_posix()
depth = 0
config_depth = None
pending_config = False
for lineno, line in enumerate(path.read_text().splitlines(), start=1):
if config_depth is None and not pending_config and STRUCT_RE.search(line):
pending_config = True
for ch in line:
if ch == "{":
depth += 1
if pending_config and config_depth is None:
config_depth = depth
pending_config = False
elif ch == "}":
depth -= 1
if config_depth is not None and depth < config_depth:
config_depth = None
in_config = config_depth is not None
if in_config:
fm = FIELD_RE.search(line)
if fm:
field_name = fm.group(0).rstrip(";= ").strip()
if (posix, field_name) not in ALLOWED_FIELDS:
violations.append((path, lineno, line.strip(),
"field name carries a sample-rate-dependent coefficient shape "
"(coeff/b1/a1/alpha) inside a *Config struct"))
if scanned == 0:
sys.exit("sample-rate coefficient rule: found no files under src/effects, "
"src/mastering or src/playback -- the glob is broken, this is not a clean result")
if violations:
for path, lineno, code, why in violations:
print(f"{path}:{lineno}: {why}")
print(f" {code}")
sys.exit(f"sample-rate coefficient rule: {len(violations)} violation(s) across {scanned} files scanned")
print(f"sample-rate coefficient rule: clean ({scanned} files scanned)")
endef
export GS_EFX_SR_COEFFICIENT_LINT_PY
# A flat enum parameter is read through `read_enum`, which refuses an undeclared
# value and publishes the declared ones as the catalog's choices. A bare
# `static_cast<Enum>(i(...))` does neither, so it is refused here, scoped to the
# enum namespaces so an integer cast such as a `size_t` count stays legal.
define MASTERING_ENUM_CAST_LINT_PY
import pathlib, re, sys
FILES = sorted(p for p in pathlib.Path("src/mastering/api").glob("*") if p.suffix in (".h", ".cpp"))
CAST_RE = re.compile(
r"static_cast<(sonare::)?(mastering::)?(eq|multiband|saturation|dynamics|final|rt|effects)::[^>]*>\(\s*i\(")
violations = []
for path in FILES:
text = path.read_text()
for match in CAST_RE.finditer(text):
violations.append((path, text.count("\n", 0, match.start()) + 1, match.group(0)))
if not FILES:
sys.exit("mastering enum cast rule: found no files under src/mastering/api -- the glob is "
"broken, this is not a clean result")
if violations:
for path, lineno, code in violations:
print(f"{path}:{lineno}: enum parameter cast from i(); read it with read_enum")
print(f" {code}")
sys.exit(f"mastering enum cast rule: {len(violations)} violation(s) across {len(FILES)} files scanned")
print(f"mastering enum cast rule: clean ({len(FILES)} files scanned)")
endef
export MASTERING_ENUM_CAST_LINT_PY
# The insertion-effect chain skeleton holds one fixed quantity the archive
# never measured -- a mix ratio -- and a binding row may not carry a constant at
# all (tools/gs/efx-bindings/SCHEMA.md). What is pinned is the count rather than
# the names: the rule being defended is that the measured table stays the place
# a new quantity lands, so a second is a law hand-written into the skeleton. Moving one the other way (into efx-tables.json,
# where its provenance becomes checkable) fails this too, on purpose -- the pin
# comes down in the same commit that earns it.
GS_EFX_SKELETON_CONSTANTS := 1
GS_EFX_SKELETON_CONSTANT_RE := constexpr float k[A-Za-z]*(Hz|Db|DryWet) =
# The insertion-effect coverage tally is described in three documents, each for
# a different reader, and a tally described three ways drifts three ways. So the
# line they quote is checked against the tool that prints it rather than against
# a copy of itself: rename a form in coverage.py and every document that did not
# follow goes red. The counts are deliberately NOT in the documents -- they move
# with every parameter anyone adjudicates, and a hand-maintained number in three
# files is stale the day after it is written.
define GS_EFX_COVERAGE_LINE_LINT_PY
import pathlib
import re
import sys
DOCS = ["CONTRIBUTING.md", "src/midi/synth/docs/gs.md", "tools/gs/docs/efx-tables.md"]
LINE = "GS EFX coverage: printed=<n> translated=<n> designed=<n> enables=<n>"
source = pathlib.Path("tools/gs/coverage.py").read_text()
try:
start = source.index('"GS EFX coverage: ')
end = source.index(".format(", start)
except ValueError:
sys.exit("coverage.py no longer prints a line beginning 'GS EFX coverage: '")
# The adjacent string literals the tally is spelled across, with each field
# placeholder collapsed to the one the documents carry.
printed = re.sub(r"\{\w+\}", "<n>", "".join(re.findall(r'"([^"]*)"', source[start:end])))
if printed != LINE:
sys.exit(f"coverage.py prints\n {printed}\nand the documents carry\n {LINE}")
missing = [doc for doc in DOCS if LINE not in pathlib.Path(doc).read_text()]
if missing:
sys.exit("the GS EFX coverage line is missing from: " + ", ".join(missing))
print(f"GS EFX coverage line: current with coverage.py, in {len(DOCS)} documents")
endef
export GS_EFX_COVERAGE_LINE_LINT_PY
lint:
cd bindings/wasm && yarn lint
cd bindings/node && yarn lint
cd bindings/node && yarn test:types
UV_CACHE_DIR=$(UV_CACHE_DIR) $(RYE) sync --pyproject bindings/python/pyproject.toml
UV_CACHE_DIR=$(UV_CACHE_DIR) $(RYE) run --pyproject bindings/python/pyproject.toml ruff check .
# The Python half of the type check that `yarn test:types` above already does for
# Node. CI has run it all along and nothing local did, so a stub that stopped
# describing its module -- invisible to ruff, to every test, and to the runtime
# that answers anyway -- reached develop and was found by a push that had not
# happened yet. Second invocation is the non-vacuity guard, as in CI: a file that
# must NOT type-check, so a configuration that silently checks nothing fails here
# instead of certifying everything.
cd bindings/python && UV_CACHE_DIR=$(UV_CACHE_DIR) $(RYE) run --pyproject pyproject.toml \
mypy --strict src/libsonare ../../tests/typing/python_smoke.py
@cd bindings/python && if UV_CACHE_DIR=$(UV_CACHE_DIR) $(RYE) run --pyproject pyproject.toml \
mypy --strict ../../tests/typing/python_catalog_invalid.py; then \
echo "python_catalog_invalid.py unexpectedly passed mypy" >&2; exit 1; \
fi
python3 -c "$$GS_EFX_SR_COEFFICIENT_LINT_PY"
python3 -c "$$MASTERING_ENUM_CAST_LINT_PY"
# The test-independence rule, stated in CONTRIBUTING.md: the byte-to-physical-unit
# conversion functions are tested against the archive's raw measured
# readings, hand-transcribed, never against gs_efx_tables.h -- the table the
# same derivation script generates from the same archive. A test that
# imported the table instead would be comparing the derivation to itself and
# could never go red. `-n` prints the offending line so a future false
# positive (a comment mentioning the header, say) is visible immediately
# rather than needing a second run to see what matched; a bare mention
# outside an #include is deliberately not this rule's concern.
@test -f tests/midi/gs_efx_convert_test.cpp || { \
echo "lint: tests/midi/gs_efx_convert_test.cpp is missing -- the include-scope check has nothing to read" >&2; \
exit 1; }
@if grep -n '#include.*gs_efx_tables\.h' tests/midi/gs_efx_convert_test.cpp; then \
echo "tests/midi/gs_efx_convert_test.cpp includes gs_efx_tables.h -- its expectations"; \
echo "would come from the same derivation that built the table it is meant to check"; \
exit 1; \
fi
@found=$$(grep -cE '$(GS_EFX_SKELETON_CONSTANT_RE)' src/midi/synth/gs_layer.cpp); \
if [ "$$found" != "$(GS_EFX_SKELETON_CONSTANTS)" ]; then \
echo "gs_layer.cpp holds $$found hand-placed EFX constants, pinned at $(GS_EFX_SKELETON_CONSTANTS):" >&2; \
grep -nE '$(GS_EFX_SKELETON_CONSTANT_RE)' src/midi/synth/gs_layer.cpp >&2 || true; \
echo " a new one is a measured law written into the skeleton by hand; one fewer means the" >&2; \
echo " pin moves in the same commit. Neither is decided here." >&2; \
exit 1; \
fi; \
echo "gs_layer.cpp hand-placed EFX constants: $$found (pinned)"
python3 -c "$$GS_EFX_COVERAGE_LINE_LINT_PY"
# Both headers are rendered from committed inputs alone, so a clone can re-run
# them and a drifted one is a static fact rather than something a build reports.
$(MAKE) gs-efx-bindings-check
$(MAKE) gs-efx-join-check
$(MAKE) gs-classic-overlays-check
format-check:
git ls-files -z -- '*.h' '*.hpp' '*.c' '*.cpp' '*.mm' ':!:third_party/**' | xargs -0 clang-format --dry-run --Werror
$(MAKE) lint
git ls-files -z -- $(RUFF_FORMAT_PATHS) | $(LS_EXISTING) | xargs -0 env UV_CACHE_DIR=$(UV_CACHE_DIR) $(RYE) run --pyproject bindings/python/pyproject.toml ruff format --check
# Binding targets
build-shared:
$(CMAKE) -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED=ON
$(CMAKE) --build $(BUILD_DIR) --target sonare_shared $(BUILD_PARALLEL)
cp -L $(SHARED_LIB) $(PYTHON_SHARED_LIB)
ifeq ($(UNAME_S),Darwin)
-install_name_tool -id @loader_path/libsonare.dylib $(PYTHON_SHARED_LIB)
endif
# Regenerate the checked-in runtime processor and preset catalog from the C
# ABI. The check variant leaves the worktree untouched and fails on drift.
capability-catalog: build-shared
python3 tools/generate_capability_catalog.py --library $(SHARED_LIB)
capability-catalog-check: build-shared
python3 tools/generate_capability_catalog.py --library $(SHARED_LIB) --check
# The instrument bank's own version registry: one generation per voice, per drum
# note and per group of shared calibration constants (the engines, the GS effect
# scales, the fallback send weights). Read from the library's own knob dump, so
# it cannot drift from what the render uses -- which needs a BUILD_TUNING build,
# in its own directory, since it neither retargets the Debug `build/` that ctest
# reads nor disturbs the shared `build-python-shared/`.
BANK_BUILD_DIR ?= build-tuning
ifeq ($(UNAME_S),Darwin)
BANK_SHARED_LIB := $(CURDIR)/$(BANK_BUILD_DIR)/lib/libsonare.dylib
else
BANK_SHARED_LIB := $(CURDIR)/$(BANK_BUILD_DIR)/lib/libsonare.so
endif
FIELD_COVERAGE := $(CURDIR)/tools/voicematch/field-coverage.json
build-bank-shared:
$(CMAKE) -S . -B $(BANK_BUILD_DIR) -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED=ON -DBUILD_TUNING=ON
$(CMAKE) --build $(BANK_BUILD_DIR) --target sonare_shared $(BUILD_PARALLEL)
# NOTE is what the bump is recorded as; a run without one records "unrecorded",
# which the version can never recover.
bank-versions: build-bank-shared
$(RYE) run --pyproject bindings/python/pyproject.toml python tools/generate_bank_versions.py \
--library $(BANK_SHARED_LIB) --note "$(NOTE)"
bank-versions-check: build-bank-shared
$(RYE) run --pyproject bindings/python/pyproject.toml python tools/generate_bank_versions.py \
--library $(BANK_SHARED_LIB) --check
# Render the per-binding processor-name declarations from the tracked catalog,
# so no surface carries a hand-maintained copy of the shipped name set. Reads
# only the committed catalog, so neither target needs a build.
processor-types:
python3 tools/generate_processor_types.py
processor-types-check:
python3 tools/generate_processor_types.py --check
build-node:
cd bindings/node && yarn install && yarn build
build-wasm-binding:
cd bindings/wasm && yarn install --immutable && yarn build
test-python: build-shared
$(RYE) sync --pyproject bindings/python/pyproject.toml
$(RYE) run --pyproject bindings/python/pyproject.toml python -m pytest bindings/python/tests/ -v
# Tests marked @pytest.mark.slow are excluded by the default addopts
# (-m "not slow"); the explicit -m here overrides that and runs just them.
test-python-slow: build-shared
$(RYE) sync --pyproject bindings/python/pyproject.toml
$(RYE) run --pyproject bindings/python/pyproject.toml python -m pytest bindings/python/tests/ -v -m slow
test-node: build-node
cd bindings/node && yarn test
# The WASM tests type-check against dist/, so unlike the Node one this gate
# cannot live in `lint` — it needs the build prerequisite this target already
# carries. Left unwired it would decay into a script nobody runs.
test-wasm: build-wasm-binding
cd bindings/wasm && yarn test
cd bindings/wasm && yarn test:types
# Focused security-hardening gates. Each test command writes its complete log
# under the matching build directory, and --no-tests=error prevents a renamed
# or accidentally undiscovered regression test from passing silently.
# `halt_on_error=1` is load-bearing -- without it UndefinedBehaviorSanitizer
# diagnoses and continues, the process exits 0, and the target passes with
# undefined behaviour present.
test-hardening-asan:
CC=clang CXX=clang++ $(CMAKE) -B build-hardening-asan -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON -DBUILD_CLI=OFF -DSONARE_WITH_FFMPEG=OFF -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" -DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address,undefined"
$(CMAKE) --build build-hardening-asan --target sonare_tests --parallel $(HARDENING_JOBS)
ASAN_OPTIONS=$(HARDENING_ASAN_OPTIONS) UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ctest --test-dir build-hardening-asan --output-on-failure --no-tests=error --output-log build-hardening-asan/test-hardening.log -R "public input corpus|set_markers rejects an invalid list|duplicate parameter rejection|offline results reject shapes|default Audio exposes a valid empty iterator"
# The TSan filter selects every test that starts a second thread, matched on the
# naming vocabulary those tests share. Three threaded cases stay out of reach
# because they are `[.]`-hidden and ctest never discovers them: the GS EFX render
# race and the two seqlock tearing soaks.
test-hardening-tsan:
CC=clang CXX=clang++ $(CMAKE) -B build-hardening-tsan -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON -DBUILD_CLI=OFF -DSONARE_WITH_FFMPEG=OFF -DCMAKE_C_FLAGS="-fsanitize=thread -fno-omit-frame-pointer" -DCMAKE_CXX_FLAGS="-fsanitize=thread -fno-omit-frame-pointer" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread" -DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=thread"
$(CMAKE) --build build-hardening-tsan --target sonare_tests --parallel $(HARDENING_JOBS)
TSAN_OPTIONS=halt_on_error=1 ctest --test-dir build-hardening-tsan --output-on-failure --no-tests=error --output-log build-hardening-tsan/test-hardening.log -R "concurrent|producer consumer stress|control/audio threads|captured samples before captured_frames|polls safely during processing|reclaims retired pages|cannot lap an audio-held snapshot|race with process_block"
# This is the only configuration that compiles the AU adapters at all, so the
# filter below decides whether any of their tests ever run. It matches the whole
# `AU ` family rather than naming cases: an enumerated list silently stops
# covering the next probe added beside them.
test-hardening-host:
ifeq ($(UNAME_S),Darwin)
$(CMAKE) -B build-hardening-host -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON -DBUILD_CLI=OFF -DSONARE_WITH_FFMPEG=OFF -DBUILD_COREAUDIO=ON -DBUILD_COREMIDI=ON -DBUILD_AU_HOST=ON
$(CMAKE) --build build-hardening-host --target sonare_tests --parallel $(HARDENING_JOBS)
ctest --test-dir build-hardening-host --output-on-failure --no-tests=error --output-log build-hardening-host/test-hardening.log -R "CoreAudio oversize callback|CoreMIDI (input|output|scripted)|AU "
else
@echo "test-hardening-host: skipped (Darwin only)"
endif
test-hardening-wasm: build-wasm-binding
cd bindings/wasm && yarn vitest run tests/basic.test.ts tests/public-input-conformance.test.ts -t "processes realtime engine clips|keeps marker transactions conformant" --reporter=verbose > build-wasm/test-hardening.log
test-hardening: test-hardening-asan test-hardening-tsan test-hardening-host test-hardening-wasm
# Feature-gate build matrix. Configure + compile only, no ctest: what it catches
# is the defect class where an always-compiled translation unit references a
# symbol that only exists inside a feature gate, which shows up as a compile or
# link failure and never as a test failure. It catches no behaviour at all -- a
# stub returning wrong values, or a feature-off build answering SONARE_OK with
# zeroes, passes this gate.
#
# The all-off entry is not redundant with the single-option rows. A gated symbol
# can be reachable through a second enabled feature, so a break that needs two
# options off together is invisible to a matrix that only turns one off at a
# time. Tests are excluded so a failure points at the shipped library and the
# CLI rather than at test code that assumes a full-feature build.
#
# Each row checks that its option actually went off before spending a build on
# it. A dependency rule in CMakeLists.txt may force an option back ON -- which
# is a legitimate thing for it to do -- and the row then configures, compiles
# and passes while being a byte-for-byte duplicate of the default build. Green,
# expensive, and evidence of nothing. Reading the resolved value back out of the
# cache is what separates the two, and it costs one `cmake -L` rather than a
# full core build: a forced row is reported and skipped, and the target exits
# non-zero at the end naming every option that is not currently switchable.
#
# A dependency rule that REFUSES to configure is the same fact stated the other
# way round, so it takes the same reported-and-skipped path rather than ending
# the run. Aborting there costs every row after it, including the all-off one
# that no single-option row can stand in for -- which is how a matrix meant to
# cover ten options covered six.
#
# Every failure is therefore carried to the summary instead of exiting where it
# happens, so one kind of failure can never hide the report of another.
FEATURE_MATRIX_OPTIONS := BUILD_MASTERING BUILD_MIXING BUILD_MIXING_ASSISTANT BUILD_GRAPH \
BUILD_FX BUILD_ACOUSTIC_SIM BUILD_PITCH_EDITOR BUILD_VOICE_CHANGER BUILD_ARRANGEMENT \
BUILD_ASSIST BUILD_PLAYBACK
FEATURE_MATRIX_ALL_OFF := $(foreach opt,$(FEATURE_MATRIX_OPTIONS),-D$(opt)=OFF)
build-feature-matrix:
@set -e; \
forced=""; \
blocked=""; \
broken=""; \
for opt in $(FEATURE_MATRIX_OPTIONS); do \
dir="build-feature-$$(echo $$opt | tr 'A-Z_' 'a-z-')-off"; \
echo "=== $$opt=OFF ($$dir) ==="; \
if ! $(CMAKE) -B "$$dir" -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=OFF -DSONARE_WITH_FFMPEG=OFF -D$$opt=OFF > "$$dir.log" 2>&1; then \
echo " SKIPPED: configuring with $$opt=OFF failed, so this row has nothing to build"; \
sed 's/^/ /' "$$dir.log"; \
blocked="$$blocked $$opt"; \
continue; \
fi; \
resolved=$$($(CMAKE) -L -N "$$dir" 2>/dev/null | sed -n "s/^$$opt:BOOL=//p"); \
case "$$resolved" in \
OFF|0|FALSE|NO|N|IGNORE|NOTFOUND|"") ;; \
*) echo " SKIPPED: a dependency rule forced $$opt back to $$resolved, so this row would rebuild the default configuration"; \
grep -i "enabling $$opt" "$$dir.log" | sed 's/^/ /' || true; \
forced="$$forced $$opt"; \
continue;; \
esac; \
if ! $(CMAKE) --build "$$dir" --parallel $(HARDENING_JOBS) >> "$$dir.log" 2>&1; then \
cat "$$dir.log"; \
broken="$$broken $$opt"; \
fi; \
done; \
echo "=== all features OFF (build-feature-all-off) ==="; \
if ! $(CMAKE) -B build-feature-all-off -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=OFF -DSONARE_WITH_FFMPEG=OFF $(FEATURE_MATRIX_ALL_OFF) > build-feature-all-off.log 2>&1 \
|| ! $(CMAKE) --build build-feature-all-off --parallel $(HARDENING_JOBS) >> build-feature-all-off.log 2>&1; then \
cat build-feature-all-off.log; \
broken="$$broken ALL_OFF"; \
fi; \
rc=0; \
if test -n "$$broken"; then \
echo; \
echo "feature-off builds that failed:$$broken"; \
rc=1; \
fi; \
if test -n "$$blocked"; then \
echo; \
echo "cannot be switched off alone, so the matrix cannot cover them:$$blocked"; \
echo "Each one is required by another option that is ON by default, so its row"; \
echo "never configures. Express the requirement as a forced value rather than a"; \
echo "refusal, so the row reports as forced and the default stays buildable, or"; \
echo "pair the two options in FEATURE_MATRIX_OPTIONS and vary them together."; \
rc=1; \
fi; \
if test -n "$$forced"; then \
echo; \
echo "not switchable, so the matrix cannot cover them:$$forced"; \
echo "Either gate the subsystem that forces each one (the NOT_SUPPORTED stub"; \
echo "pattern in src/c_api/sonare_c_daw.cpp is how the tree already does it),"; \
echo "or drop the option and make the subsystem unconditional. Leaving it in"; \
echo "FEATURE_MATRIX_OPTIONS buys a row that can never differ from default."; \
rc=1; \
fi; \
exit $$rc
# Cross-binding parity gate (C API is canonical). Stdlib-only, no build needed:
# it reads the binding sources directly and exits non-zero on active drift.
parity: conformance
python3 tools/parity/check_parity.py
# Tracked per-runtime capability matrix, derived from the same reachability
# decision the parity checker makes. The check variant leaves the worktree
# untouched and fails when the table no longer matches the surfaces.
surface-coverage:
python3 tools/parity/surface_coverage.py
surface-coverage-check:
python3 tools/parity/surface_coverage.py --check
# The GS address census: which addresses real Standard MIDI Files reach, and how
# many files reach each. The corpus is not in the repository (see
# tools/gs/docs/census.md), so `gs-census` needs one fetched first and
# `gs-census-header` rerenders the committed test input from the committed JSON.
# The coverage gate itself is a C++ case, gs_address_census_test.cpp.
# The whole corpus root, not the loose `mid/` half: the committed census was
# taken over both that and the unpacked archives, and pointing this at one of
# them regenerates a census a third smaller — which reads as coverage
# regressing and lowers the gate's ceilings, the one direction a refresh must
# not be able to move them by accident.
GS_CORPUS ?= .cache/gs-corpus
GS_CENSUS_SOURCE ?= regenerated locally
gs-census:
python3 tools/gs/extract_addresses.py --corpus $(GS_CORPUS) \
--out tools/gs/address-census.json --source "$(GS_CENSUS_SOURCE)"
$(MAKE) gs-census-header
# Which programs and which variation banks real files select, as a sibling
# histogram. Nothing gates on it: it is evidence for the bank's working order
# (tools/voicematch/policy.json), which was a reasonable guess until it ran.
gs-program-census:
python3 tools/gs/extract_programs.py --corpus $(GS_CORPUS) \
--out tools/gs/program-census.json --source "$(GS_CENSUS_SOURCE)" --top 20
gs-census-header:
python3 tools/gs/gen_census_header.py --census tools/gs/address-census.json \
--out tests/midi/gs_address_census.inc
gs-census-check:
python3 tools/gs/gen_census_header.py --census tools/gs/address-census.json \
--out /tmp/gs_address_census_check.inc
diff -u tests/midi/gs_address_census.inc /tmp/gs_address_census_check.inc
# The address table against a measured unit. The census above says what real
# files reach and is blind to any address they never send; this says what the
# machine itself answers, which is the other half and the one that closes the
# provenance gap gs.md declares. The archive is external and its licence is its
# own, so the diff is committed and a fresh clone reads the work list without
# fetching it -- and GS_UNIT_ARCHIVE has no default for the same reason, a path
# into a tree a clone does not have being a dead pointer rather than a
# convenience. See tools/gs/docs/unit-diff.md.
GS_UNIT_ARCHIVE ?=
GS_TABLE_JSON := .cache/gs-address-table.json
gs-address-table-json:
@mkdir -p .cache
$(CXX) -std=c++17 -I src -o .cache/gs_dump_address_table tools/gs/dump_address_table.cpp
.cache/gs_dump_address_table > $(GS_TABLE_JSON)
gs-unit-archive-set:
@test -n "$(GS_UNIT_ARCHIVE)" || { \
echo "GS_UNIT_ARCHIVE is unset: point it at a unit directory of an external"; \
echo "measurement archive (tools/gs/docs/unit-diff.md)."; exit 1; }
gs-unit-diff: gs-unit-archive-set gs-address-table-json
python3 tools/gs/check_unit.py --table $(GS_TABLE_JSON) \
--unit $(GS_UNIT_ARCHIVE) --out tools/gs/unit-diff.json
gs-unit-diff-check: gs-unit-archive-set gs-address-table-json
python3 tools/gs/check_unit.py --table $(GS_TABLE_JSON) \
--unit $(GS_UNIT_ARCHIVE) --out /tmp/gs_unit_diff_check.json
diff -u tools/gs/unit-diff.json /tmp/gs_unit_diff_check.json
# The GS insertion-effect byte-to-physical-unit conversion tables, derived
# from the same external archive as gs-unit-diff but read differently: that
# script's --unit wants a unit DIRECTORY (GS_UNIT_ARCHIVE, documented as such
# above), while derive_efx_tables.py's --archive wants the archive ROOT and
# resolves the unit itself. The two are not interchangeable, so this gets its
# own variable rather than reading GS_UNIT_ARCHIVE -- doing so would silently
# break whichever of the two callers is already relying on its documented
# shape. No default, for the reason GS_UNIT_ARCHIVE has none: a path into a
# tree a clone does not have is a dead pointer rather than a convenience. See
# tools/gs/docs/efx-tables.md.
GS_EFX_ARCHIVE ?=
gs-efx-archive-set:
@test -n "$(GS_EFX_ARCHIVE)" || { \
echo "GS_EFX_ARCHIVE is unset: point it at the root of an external"; \
echo "measurement archive of what an individual SC-8850 answered"; \
echo "(tools/gs/docs/efx-tables.md)."; exit 1; }
gs-efx-tables: gs-efx-archive-set
python3 tools/gs/derive_efx_tables.py --archive $(GS_EFX_ARCHIVE) \
--out tools/gs/efx-tables.json --header src/midi/synth/gs_efx_tables.h
# Regenerates into a scratch directory and diffs against the committed pair.
# The scratch directory is a fresh mktemp -d rather than a fixed /tmp path,
# because two sessions running this target at once would otherwise clobber
# each other's output. A revision mismatch is reported on its own line
# before the diff runs: "the archive moved on" and "the script changed" both
# show up as a diff, and folding the two into one report sends whoever reads
# it to the wrong place.
gs-efx-tables-check: gs-efx-archive-set
@scratch=$$(mktemp -d) || exit 1; \
trap 'rm -rf "$$scratch"' EXIT; \
set -e; \
python3 tools/gs/derive_efx_tables.py --archive $(GS_EFX_ARCHIVE) \
--out "$$scratch/gs_efx_tables_check.json" --header "$$scratch/gs_efx_tables_check.h"; \
committed=$$(python3 -c "import json; print(json.load(open('tools/gs/efx-tables.json'))['archive_revision'])"); \
regenerated=$$(python3 -c "import json; print(json.load(open('$$scratch/gs_efx_tables_check.json'))['archive_revision'])"); \
if [ "$$committed" != "$$regenerated" ]; then \
echo "archive_revision differs: committed $$committed, regenerated from \$$GS_EFX_ARCHIVE $$regenerated"; \
echo " (this is the archive moving on, not necessarily the derivation changing --"; \
echo " the diff below says whether the tables themselves moved too)"; \
fi; \
diff -u tools/gs/efx-tables.json "$$scratch/gs_efx_tables_check.json"; \
diff -u src/midi/synth/gs_efx_tables.h "$$scratch/gs_efx_tables_check.h"
# Writes the classic-realisation models, reference digests and classic.md.
gs-classic-models: gs-efx-archive-set
python3 tools/gs/classic_models.py --archive $(GS_EFX_ARCHIVE)
# Archive-free: the overlay set against the digest recorded in the committed
# .inc, so an overlay edited without regenerating is a static fact. Run by lint.
gs-classic-overlays-check:
python3 tools/gs/classic_models.py --check-overlays
# Regenerates the classic-realisation models and the reference digests into a
# scratch directory and diffs them against the committed files; the script
# reports an archive_revision difference on one line ahead of the diff, as
# gs-efx-tables-check does. Needs the archive (and its venv), so like that
# target it is not part of `make lint`.
gs-classic-check: gs-efx-archive-set
python3 tools/gs/classic_models.py --archive $(GS_EFX_ARCHIVE) --check
# One line per recorded key (designed row, enables row, classic overlay entry,
# classic model hash) the archive has moved past; exit 3 when any is found.
# Archive required, so not part of `make lint`.
gs-efx-soundings-drift: gs-efx-archive-set
python3 tools/gs/soundings_drift.py --archive $(GS_EFX_ARCHIVE)
# The binding table the insert chain walks, rendered from the hand-written
# tools/gs/efx-bindings/*.json. Both inputs are committed, so this one needs no
# archive and a clone can regenerate it -- which is why it is a script of its
# own rather than another output of derive_efx_tables.py.
gs-efx-bindings:
python3 tools/gs/bindings_header.py
gs-efx-bindings-check:
python3 tools/gs/bindings_header.py --check
# The same rows as gs-efx-bindings, but all five forms rather than the
# assigned ones alone: what the tests need in order to tell a documented
# state from an unmapped type, which a table of reached controls cannot say.
gs-efx-join:
python3 tools/gs/join_header.py
gs-efx-join-check:
python3 tools/gs/join_header.py --check
# The GS EFX coverage equation: how many of the 770 printed (type, slot)
# parameters are translated, documented as a state, unmapped, unreadable or
# built by hand, and how many nobody has adjudicated yet. See
# tools/gs/coverage.py.
gs-efx-coverage: gs-efx-archive-set
python3 tools/gs/coverage.py --archive $(GS_EFX_ARCHIVE)
# Shared public-input schema plus public streaming field/flag/default snapshot.
# Also gates request-object coverage: every one-shot facade export keeps a
# *Request overload, and every *Request a public function accepts stays exported
# from the package entry (both are invisible to the C-ABI parity checker).
#
# `check_lint_scope` gates the static gates themselves. CI does not call this
# Makefile for lint, so the ruff target, the clang-format pathspec and every
# pinned tool version each live in three files at once; widening one and not the
# others leaves the gate that runs on a push as narrow as it was, and green.
conformance:
python3 tools/conformance/check_public_contracts.py
python3 tools/api/check_request_object_coverage.py
python3 tools/conformance/check_cli_contract.py --schema
python3 tools/conformance/check_repair_cli_param_keys.py
python3 tools/conformance/check_lint_scope.py
python3 -m unittest tests/conformance/test_cli_contract.py
python3 -m unittest tests/conformance/test_playback_cli.py
python3 -m unittest tests/conformance/test_wasm_exception_scope.py
python3 -m unittest tests/conformance/test_wasm_feature_gate_scope.py
python3 -m unittest tests/conformance/test_binding_warning_flags.py
python3 tests/conformance/check_wasm_narrowing_scope.py
python3 -m unittest tests/conformance/test_wasm_narrowing_scope.py
python3 tests/conformance/check_python_narrowing_scope.py
python3 -m unittest tests/conformance/test_python_narrowing_scope.py
python3 tests/conformance/check_public_integer_domains.py
python3 -m unittest tests/conformance/test_public_integer_domains.py
python3 tests/conformance/check_zero_sentinel_filter.py
python3 -m unittest tests/conformance/test_zero_sentinel_filter.py
python3 tests/conformance/check_goniometer_capacity_mirrors.py
python3 -m unittest tests/conformance/test_goniometer_capacity_mirrors.py
python3 tests/conformance/check_chord_quality_tables.py
python3 -m unittest tests/conformance/test_chord_quality_tables.py
python3 tests/conformance/check_documented_bound_mirrors.py
python3 -m unittest tests/conformance/test_documented_bound_mirrors.py
python3 tests/conformance/check_rir_diagnostic_codes.py
python3 -m unittest tests/conformance/test_rir_diagnostic_codes.py
python3 tests/conformance/check_synth_param_surface.py
python3 -m unittest tests/conformance/test_synth_param_surface.py
python3 -m unittest tests/conformance/test_ts_surface_walk.py
python3 tests/conformance/check_mastering_param_surfaces.py
python3 -m unittest tests/conformance/test_mastering_param_surfaces.py
python3 tests/conformance/check_result_schema_surfaces.py
python3 -m unittest tests/conformance/test_result_schema_surfaces.py
python3 -m unittest tests/conformance/test_bank_versions.py
python3 tests/conformance/check_bank_policy.py
python3 -m unittest tests/conformance/test_bank_policy.py
python3 -m unittest tests/conformance/test_gs_program_census.py
python3 -m unittest tests/conformance/test_gs_efx_coverage.py
python3 -m unittest tests/conformance/test_gs_classic_models.py
python3 -m unittest tests/conformance/test_gs_classic_overlays.py
python3 -m unittest tests/conformance/test_c_api_out_param_init.py
python3 -m unittest tests/conformance/test_c_api_pointer_contracts.py
python3 -m unittest tests/conformance/test_c_api_header_self_contained.py
python3 -m unittest tests/conformance/test_c_api_feature_off_compile.py
python3 -m unittest tests/conformance/test_c_api_type_home.py
python3 tests/conformance/check_c_api_out_param_init.py --floor 250
python3 tests/conformance/check_c_api_pointer_contracts.py --floor 250
python3 tests/conformance/check_c_api_header_self_contained.py --floor 20
python3 tests/conformance/check_c_api_type_home.py --floor 150
python3 -m unittest tests/conformance/test_error_code_mapping.py
python3 tools/conformance/test_lint_scope.py
python3 tools/parity/test_handle_gating.py
python3 tools/parity/test_record_shape.py
python3 tools/parity/test_ts_reexport.py
python3 tools/parity/test_surface_coverage.py
python3 tools/parity/test_allowlist_audit.py
python3 tools/parity/test_comparison_reach.py
python3 tools/parity/test_c_return_type_coverage.py
python3 tools/eval/test_summarize_accuracy.py
python3 tools/audition/test_serve.py
python3 tools/audition/test_page.py
python3 tools/parity/surface_coverage.py --check
python3 tools/parity/check_parity.py --audit-allowlist
@if test -x "$(BUILD_DIR)/bin/sonare-cli" && test -x "bindings/python/.venv/bin/python"; then \
python3 tools/conformance/check_cli_contract.py \
--native "$(BUILD_DIR)/bin/sonare-cli" \
--python "bindings/python/.venv/bin/python"; \
else \
echo "conformance: live CLI check skipped (build/bin/sonare-cli or bindings/python/.venv/bin/python is unavailable)"; \
fi
# C-ABI contract scans, kept as named targets for running one on its own;
# `conformance` runs all four, plus the self-tests that carry the non-vacuity
# break each is calibrated against.
#
# The floors are the point of the invocation: each scan reports nothing when its
# pattern stops matching, so the count of declarations it resolves at all is
# pinned separately from the count of findings, and falling under it exits 2.
check-c-api-out-param-init:
python3 tests/conformance/check_c_api_out_param_init.py --floor 250
check-c-api-pointer-contracts:
python3 tests/conformance/check_c_api_pointer_contracts.py --floor 250
# Compiles each public header as a TU's only include, in C and in C++. No build
# tree: it drives the compiler on a probe file, so it costs about a second.
check-c-api-header-self-contained:
python3 tests/conformance/check_c_api_header_self_contained.py --floor 20
# Reports a type a surface header defines but none of its own declarations
# take, while a sibling's do. Text-only, no build tree.
check-c-api-type-home:
python3 tests/conformance/check_c_api_type_home.py --floor 150
# Holds both hand-written binding layers to the core's warning bar, read off the
# compilation database each layer's own build wrote. It therefore needs the WASM
# module and the Node addon built, and refuses an absent database rather than
# passing on it, so it stays out of `conformance`: the gate that runs on a push
# builds neither. The unittest beside it does stay in `conformance` — it works
# on synthetic databases and skips a layer this tree has not built.
check-binding-warning-flags:
python3 tests/conformance/check_binding_warning_flags.py
# Opt-in GM-program project bounce acceptance across the C, Python, Node, and
# WASM public surfaces. The check deliberately does not build the bindings: it
# is an acceptance run over already-built artifacts, with an explicit preflight
# so a clean tree fails with the exact build targets to run first.
test-gm-cross-surface:
@command -v "$(RYE)" >/dev/null 2>&1 || { \
echo "test-gm-cross-surface: required command not found: $(RYE)" >&2; \
exit 1; \
}
@set -eu; \
for artifact in \
"$(PYTHON_SHARED_LIB)" \
"bindings/node/dist/index.js" \
"bindings/node/build/Release/sonare-node.node" \
"bindings/wasm/dist/index.js" \
"bindings/wasm/dist/sonare.js" \
"bindings/wasm/dist/sonare.wasm"; do \
if test ! -f "$$artifact"; then \
echo "test-gm-cross-surface: missing required artifact: $$artifact" >&2; \
echo "test-gm-cross-surface: run 'make build-shared build-node build-wasm-binding' first" >&2; \
exit 1; \
fi; \
done
SONARE_LIB_PATH=$(PYTHON_SHARED_LIB) PYTHONPATH=$(CURDIR)/bindings/python/src \
$(RYE) run --pyproject bindings/python/pyproject.toml python tests/conformance/check_gm_project_surfaces.py
# Opt-in mixing-assistant scene acceptance across the C, Python, Node, and WASM
# public surfaces: one synthetic multi-track fixture in, one scene JSON out of
# each facade. Request-object shapes and per-binding option-name tables are
# invisible to the parity checker, so this runs the surfaces instead of reading
# them. Like the GM check, it builds nothing and preflights the artifacts.
test-mix-assistant-cross-surface:
@command -v "$(RYE)" >/dev/null 2>&1 || { \
echo "test-mix-assistant-cross-surface: required command not found: $(RYE)" >&2; \
exit 1; \
}
@set -eu; \
for artifact in \
"$(PYTHON_SHARED_LIB)" \
"bindings/node/dist/index.js" \
"bindings/node/build/Release/sonare-node.node" \
"bindings/wasm/dist/index.js" \
"bindings/wasm/dist/sonare.js" \
"bindings/wasm/dist/sonare.wasm"; do \
if test ! -f "$$artifact"; then \
echo "test-mix-assistant-cross-surface: missing required artifact: $$artifact" >&2; \
echo "test-mix-assistant-cross-surface: run 'make build-shared build-node build-wasm-binding' first" >&2; \
exit 1; \
fi; \
done
SONARE_LIB_PATH=$(PYTHON_SHARED_LIB) PYTHONPATH=$(CURDIR)/bindings/python/src \
$(RYE) run --pyproject bindings/python/pyproject.toml python tests/conformance/check_mix_assistant_surfaces.py
# Regenerate the authoritative C-ABI struct layout snapshot. Compiles a tiny
# probe (needs a C++ compiler, not a full build) that reports sizeof/alignof/
# offsetof straight from the headers. The JSON is tracked; the Python guard
# (tests/test_abi_layout.py) and the WASM abi-layout vitest compare against it.
abi-layout:
$(RYE) run --pyproject bindings/python/pyproject.toml python tools/abi/gen_abi_layout.py
# Fail if the committed snapshot is stale (regenerate + git-diff style check).
abi-layout-check:
$(RYE) run --pyproject bindings/python/pyproject.toml python tools/abi/gen_abi_layout.py --check
# Verify the ABI-version mirror literals in every binding match the C source of
# truth (per-subsystem + packed aggregate). Stdlib-only, read-only.
check-abi-version: