Files
deusdata--codebase-memory-mcp/Makefile.cbm
Martin Vogel 93696aeb55 docs(security): publish the AV false-positive evidence; harden arm64 W^X
SECURITY.md gains an "Antivirus False Positives" section placed directly after
the existing Verification block so it reuses those commands rather than
restating them. It states the detection plainly, shows the measured evidence
(the verdict inverts across architectures and link modes; identical macOS
segment structure splits clean/flagged; entropy is 3.5-4.2 bits/byte, nowhere
near packed), lists the cross-project precedent, and — deliberately — records
that removing the embedded scripts and externalizing the assets did NOT move the
detection. A negative result is still evidence, and publishing it is the point.

It also documents the release policy, invites independent audit through the
provenance/cosign/checksum path we already ship, and adds an `av-analysis` issue
label route for anyone who finds something real. Tone is evidence-first: no
vendor blame, and an explicit note that signing helps Windows but that no
AV-honoured signing scheme exists for Linux ELF, so it is not a full answer.

README carries a short pointer near Quick Start, where someone who just hit a
Defender warning will actually look.

Unrelated to AV, found while dissecting the artifacts: add -Wl,-z,separate-code.
GNU ld enables it by default on x86-64 but not on aarch64, so arm64 emitted ONE
R E PT_LOAD covering the whole image and mapped ~259 MB of tree-sitter parse
tables executable at runtime. Section flags said A, not AX -- but the kernel
applies segment permissions, so the section flags were never the control. The
amd64 build of the same source mapped the same data R only.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-09 16:17:54 +02:00

1243 lines
60 KiB
Makefile

# Makefile.cbm — Build system for pure C rewrite
#
# Usage:
# make -f Makefile.cbm test # Build + run all tests (ASan + UBSan)
# make -f Makefile.cbm test-foundation # Foundation tests only (fast)
# make -f Makefile.cbm test-tsan # Thread sanitizer build
# make -f Makefile.cbm cbm # Production binary
# make -f Makefile.cbm clean-c # Remove build artifacts
# Compiler selection — override via: make CC=gcc CXX=g++
# macOS: cc (Apple Clang) — universal binary with ASan support
# Linux: gcc/g++ — system default with full sanitizer support
# CI scripts pass CC/CXX explicitly; don't rely on defaults here
# Target architecture (macOS): build.sh/test.sh export ARCHFLAGS="-arch <arch>"
# (see scripts/env.sh). Fold it into the compiler drivers with `override` so it
# reaches EVERY compile and link recipe — including the vendored objects below
# that use their own *_CFLAGS — and so it survives a command-line `CC=` override.
# ARCHFLAGS is empty on Linux/Windows and for native direct `make` invocations,
# leaving CC/CXX unchanged there.
override CC := $(CC) $(ARCHFLAGS)
override CXX := $(CXX) $(ARCHFLAGS)
# ── Common flags ─────────────────────────────────────────────────
# Include paths for:
# src/ — new foundation headers
# vendored/ — yyjson, xxhash, sqlite3
# internal/cbm — existing extraction headers (cbm.h, helpers.h, etc.)
# internal/cbm/vendored/ts_runtime/include — tree-sitter API
CBM_DIR = internal/cbm
TS_INCLUDE = $(CBM_DIR)/vendored/ts_runtime/include
# Vendored tree-sitter src/ contains unicode/ headers (umachine.h, utf.h, utf8.h).
# This ensures we use our vendored copies instead of requiring system libicu-dev.
TS_SRC = $(CBM_DIR)/vendored/ts_runtime/src
# GCC-only warning suppressions (Clang rejects unknown -Wno-* with -Werror).
# Detect GCC by checking for __GNUC__ without __clang__ — handles all versions.
IS_GCC := $(shell echo | $(CC) -dM -E - 2>/dev/null | grep -q '__GNUC__' && ! echo | $(CC) -dM -E - 2>/dev/null | grep -q '__clang__' && echo yes || echo no)
GCC_ONLY_FLAGS :=
ifeq ($(IS_GCC),yes)
GCC_ONLY_FLAGS := -Wno-format-truncation -Wno-unused-result \
-Wno-stringop-truncation -Wno-alloc-size-larger-than
endif
# -Wdate-time: with -Werror, any use of __DATE__/__TIME__/__TIMESTAMP__ fails the
# build. Build-time entropy is how one identical source tree produced a new hash
# on every compile (see the local patch in vendored/mimalloc/src/options.c) —
# once removed, it must not creep back through a new call site.
CFLAGS_COMMON = -std=c11 -D_DEFAULT_SOURCE -D_GNU_SOURCE -Wall -Wextra -Werror \
-Wno-unused-parameter -Wno-sign-compare -Wdate-time \
$(GCC_ONLY_FLAGS) \
-Isrc -Ivendored -Ivendored/sqlite3 \
-Ivendored/mimalloc/include \
-I$(CBM_DIR) -I$(TS_INCLUDE) -I$(BUILD_DIR)/generated
CXXFLAGS_COMMON = -std=c++14 -Wall -Wextra -Werror \
-Wno-unused-parameter \
-I$(CBM_DIR) -I$(TS_INCLUDE)
# Test seams are OPT-IN, never opt-out. Some suites drive behaviour that only a
# test should be able to ask for (fork an orphan the watchdog must reap, publish
# a lease-ownership marker). That code has no production caller and reads exactly
# like malware to a generic classifier, so it must not be in a shipped binary.
# Opt-IN means the failure mode of forgetting the flag is a CLEAN binary rather
# than a leaky one — the opposite choice would make every future release depend
# on someone remembering. scripts/test.sh passes TEST_SEAMS=1 for the suites that
# need it; the test-runner always has them.
TEST_SEAM_DEFINE :=
ifeq ($(TEST_SEAMS),1)
TEST_SEAM_DEFINE := -DCBM_ENABLE_TEST_SEAMS=1
endif
# Production flags (CFLAGS_EXTRA allows CI to inject -DCBM_VERSION)
# CBM_BIND_TS_ALLOCATOR=1: bind the tree-sitter runtime to mimalloc (#424). Only
# the prod build uses mimalloc (MI_OVERRIDE=1); the test build is CRT+ASan, where
# binding would create an alloc/free mismatch, so the guard is prod-only.
CFLAGS_PROD = $(CFLAGS_COMMON) -O2 -DCBM_BIND_TS_ALLOCATOR=1 $(TEST_SEAM_DEFINE) \
$(GLOBAL_OVERRIDE_DEFINE) $(CFLAGS_EXTRA)
CXXFLAGS_PROD = $(CXXFLAGS_COMMON) -O2
# Test flags: debug + sanitizers (override SANITIZE= to disable on Windows)
SANITIZE = -fsanitize=address,undefined -fno-omit-frame-pointer
EDITOR_TEST_DEFINES = -DCBM_JSON_LIKE_ENABLE_TEST_API=1 \
-DCBM_TOML_EDIT_ENABLE_TEST_API=1 -DCBM_YAML_ENABLE_TEST_API=1 \
-DCBM_TEXT_EDIT_ENABLE_TEST_API=1 -DCBM_CLI_ENABLE_TEST_API=1 \
-DCBM_DIAGNOSTICS_ENABLE_TEST_API=1 -DCBM_ENABLE_TEST_SEAMS=1
# The build system is the single source of truth for "is this binary
# instrumented": compiler-specific probes (__SANITIZE_ADDRESS__) miss
# clang's feature-check spelling and every non-ASan sanitizer, so the
# trap-UBSan leg once ran NATIVE timing budgets on an instrumented
# binary. Any non-empty SANITIZE ⇒ sanitized budgets everywhere.
ifneq ($(strip $(SANITIZE)),)
SANITIZED_DEFINE = -DCBM_SANITIZED_BUILD=1
else
SANITIZED_DEFINE =
endif
KOTLIN_DEDUP_TEST_DEFINE = -DCBM_KOTLIN_DEDUP_TEST_API=1
CALL_REFERENCE_LOOKUP_TEST_DEFINE = -DCBM_CALL_REFERENCE_LOOKUP_TEST_API=1
INCREMENTAL_TEST_DEFINE = -DCBM_INCREMENTAL_TEST_API=1
CFLAGS_TEST = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(SANITIZED_DEFINE) \
$(KOTLIN_DEDUP_TEST_DEFINE) $(CALL_REFERENCE_LOOKUP_TEST_DEFINE) \
$(INCREMENTAL_TEST_DEFINE) -g -O1 $(SANITIZE)
CXXFLAGS_TEST = $(CXXFLAGS_COMMON) $(SANITIZED_DEFINE) -g -O1 $(SANITIZE) $(CXX_STDLIB_FLAGS)
# TSan (can't combine with ASan)
TSAN_SANITIZE = -fsanitize=thread -fno-omit-frame-pointer
CFLAGS_TSAN = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(KOTLIN_DEDUP_TEST_DEFINE) \
$(CALL_REFERENCE_LOOKUP_TEST_DEFINE) $(INCREMENTAL_TEST_DEFINE) \
-g -O1 $(TSAN_SANITIZE)
CXXFLAGS_TSAN = $(CXXFLAGS_COMMON) -g -O1 \
$(TSAN_SANITIZE)
# Windows needs ws2_32 (Winsock), psapi (GetProcessMemoryInfo), shell32
# (CommandLineToArgvW — the UTF-8 argv path in main.c, #423/#20), and
# advapi32 (owner-only activation-candidate ACLs), plus
# --allow-multiple-definition (MinGW CRT symbol clashes).
# Auto-detected via compiler; no manual override needed.
IS_MINGW := $(shell echo | $(CC) -dM -E - 2>/dev/null | grep -q 'define _WIN32 ' && echo yes || echo no)
WIN32_LIBS :=
# Route Windows allocations through mimalloc (#581). mimalloc's own static
# override is gated on _MSC_VER, which clang/MinGW never defines, so it
# compiled out and the CRT won the link: ordinary malloc went to the CRT heap,
# which keeps freed pages committed, leaving every allocator tuning in
# cbm_mem_init POSIX-only. --wrap redirects the same population of allocations
# that link-order override captures on POSIX, so the purge configuration
# finally applies here too. src/foundation/mem_override_win.c supplies the
# wrappers; its deallocating paths check mi_is_in_heap_region so a CRT-owned
# pointer can never reach mi_free (#424).
MIMALLOC_WRAP_SYMS := malloc calloc realloc free strdup strndup _msize \
_aligned_malloc _aligned_free
MIMALLOC_WRAP_FLAGS :=
ifeq ($(IS_MINGW),yes)
MIMALLOC_WRAP_FLAGS := $(foreach sym,$(MIMALLOC_WRAP_SYMS),-Wl,--wrap=$(sym))
endif
# Linux wraps a smaller set for MEASUREMENT only (see mem_override_posix.c):
# allocations already reach mimalloc here, but the profiler needs the same
# observation point as Windows or the platform comparison is not like-for-like.
# macOS ld has no --wrap, so it stays census-only.
IS_LINUX := $(shell uname -s 2>/dev/null | grep -q Linux && echo yes || echo no)
MIMALLOC_WRAP_SYMS_POSIX := malloc calloc realloc free strdup
MIMALLOC_WRAP_FLAGS_POSIX :=
ifeq ($(IS_LINUX),yes)
MIMALLOC_WRAP_FLAGS_POSIX := $(foreach sym,$(MIMALLOC_WRAP_SYMS_POSIX),-Wl,--wrap=$(sym))
endif
ifeq ($(IS_MINGW),yes)
# --no-insert-timestamp: the PE header otherwise carries the link wall clock, so
# two Windows builds of identical source are never byte-identical and every
# release is a brand-new file to a reputation system. Same motivation as the
# mimalloc __DATE__ patch; ELF and Mach-O have no equivalent field to clear.
WIN32_LIBS := -lws2_32 -lpsapi -lshell32 -ladvapi32 -lbcrypt -Wl,--allow-multiple-definition -Wl,--stack,8388608 -Wl,--no-insert-timestamp -static
endif
# STATIC=1 produces a fully static binary (for Alpine/musl portable builds)
ifeq ($(STATIC),1)
STATIC_FLAGS := -static
endif
# W^X: demand a non-executable stack on ELF. This is belt to the braces of the
# .note.GNU-stack annotation in vendored/nomic/code_vectors_blob.S — that note
# fixes the CAUSE (an unannotated object makes ld assume the worst for the whole
# link), this flag fixes the OUTCOME, and the composition gate proves it on the
# shipped artifact. ELF-only: Apple's ld rejects -z noexecstack outright and it
# is meaningless for PE, so it is gated rather than made "common".
#
# -z separate-code additionally keeps read-only DATA out of the executable
# segment. GNU ld enables it by default on x86-64 but NOT on aarch64, so the
# arm64 binaries emitted ONE R E PT_LOAD spanning the whole image: .rodata is
# correctly marked A (not AX), but the kernel applies SEGMENT permissions, so
# ~259 MB of tree-sitter parse tables was mapped executable at runtime while the
# amd64 build of the same source mapped them R only. That is a large ROP gadget
# surface for data that is never executed, and it is invisible to section flags.
ELF_HARDENING_FLAGS :=
ifeq ($(IS_LINUX),yes)
ifneq ($(IS_MINGW),yes)
ELF_HARDENING_FLAGS := -Wl,-z,noexecstack -Wl,-z,separate-code
endif
endif
# The POSIX wrap shim exists only so the profiler can observe allocations. It
# must never reach a sanitized build: the Linux/macOS test builds are CRT+ASan,
# and redirecting malloc into mimalloc underneath ASan's own interception mixes
# two allocators on the same pointers. Windows keeps its wrap flags everywhere,
# because there the shim is what makes mimalloc own the allocations at all.
# C++ stdlib selection: MSan builds must link an MSan-instrumented libc++
# instead of the system libstdc++ (scripts/msan.sh overrides both vars).
CXX_STDLIB ?= -lstdc++
CXX_STDLIB_FLAGS ?=
LDFLAGS = -lm $(CXX_STDLIB) -lpthread -lz $(WIN32_LIBS) $(STATIC_FLAGS) $(MIMALLOC_WRAP_FLAGS) $(ELF_HARDENING_FLAGS)
LDFLAGS_TEST = -lm $(CXX_STDLIB) -lpthread -lz $(SANITIZE) $(WIN32_LIBS) $(MIMALLOC_WRAP_FLAGS) $(ELF_HARDENING_FLAGS)
LDFLAGS_TSAN = -lm $(CXX_STDLIB) -lpthread -lz $(TSAN_SANITIZE) $(WIN32_LIBS) $(MIMALLOC_WRAP_FLAGS) $(ELF_HARDENING_FLAGS)
# ── Source files ─────────────────────────────────────────────────
FOUNDATION_SRCS = \
src/foundation/mem_override_win.c \
src/foundation/arena.c \
src/foundation/hash_table.c \
src/foundation/str_intern.c \
src/foundation/log.c \
src/foundation/str_util.c \
src/foundation/workspace.c \
src/foundation/platform.c \
src/foundation/system_info.c \
src/foundation/slab_alloc.c \
src/foundation/yaml.c \
src/foundation/compat.c \
src/foundation/compat_thread.c \
src/foundation/compat_fs.c \
src/foundation/compat_regex.c \
src/foundation/mem.c \
src/foundation/diagnostics.c \
src/foundation/profile.c \
src/foundation/dump_verify.c \
src/foundation/limits.c \
src/foundation/subprocess.c \
src/foundation/sha256.c \
src/foundation/secure_random.c \
src/foundation/macos_acl.c \
src/foundation/private_file_lock.c \
src/foundation/lock_registry.c
# Existing extraction C code (compiled from current location)
EXTRACTION_SRCS = \
$(CBM_DIR)/cbm.c \
$(CBM_DIR)/extract_defs.c \
$(CBM_DIR)/extract_calls.c \
$(CBM_DIR)/extract_imports.c \
$(CBM_DIR)/extract_usages.c \
$(CBM_DIR)/extract_unified.c \
$(CBM_DIR)/extract_semantic.c \
$(CBM_DIR)/extract_type_refs.c \
$(CBM_DIR)/extract_type_assigns.c \
$(CBM_DIR)/extract_env_accesses.c \
$(CBM_DIR)/extract_channels.c \
$(CBM_DIR)/extract_k8s.c \
$(CBM_DIR)/helpers.c \
$(CBM_DIR)/lang_specs.c \
$(CBM_DIR)/macro_table.c \
$(CBM_DIR)/iris_export_xml.c \
$(CBM_DIR)/service_patterns.c
# LSP resolvers (compiled as one unit via lsp_all.c)
LSP_SRCS = $(CBM_DIR)/lsp_all.c
# Header/source dependencies of the lsp_all unity object. lsp_all.c #includes
# every lsp/*.c resolver, which in turn include cbm.h — and CBMCall is copied
# BY VALUE across the lsp -> pipeline boundary (cbm_calls_push). This object is
# compiled standalone and linked in, NOT recompiled with the rest on every link,
# so it must list the headers/sources it pulls in. Without this, a changed struct
# layout (e.g. a new CBMCall field) leaves a stale lsp_all.o with the old layout —
# an ODR mismatch that under-reads the by-value struct copy. Explicit because this
# Makefile does not use compiler auto-dependency (-MMD) generation.
LSP_UNITY_DEPS = $(CBM_DIR)/lsp_all.c \
$(wildcard $(CBM_DIR)/lsp/*.c $(CBM_DIR)/lsp/*.h \
$(CBM_DIR)/lsp/generated/*.c $(CBM_DIR)/*.h)
# Tree-sitter runtime
TS_RUNTIME_SRC = $(CBM_DIR)/ts_runtime.c
TS_RUNTIME_DEPS = \
$(TS_RUNTIME_SRC) \
$(wildcard $(CBM_DIR)/vendored/ts_runtime/include/tree_sitter/*.h) \
$(wildcard $(CBM_DIR)/vendored/ts_runtime/src/*.c) \
$(wildcard $(CBM_DIR)/vendored/ts_runtime/src/*.h) \
$(wildcard $(CBM_DIR)/vendored/ts_runtime/src/portable/*.h) \
$(wildcard $(CBM_DIR)/vendored/ts_runtime/src/unicode/*.h)
# All 159 grammar shim files
GRAMMAR_SRCS = $(wildcard $(CBM_DIR)/grammar_*.c)
# LZ4 + Aho-Corasick
AC_LZ4_SRCS = $(CBM_DIR)/ac.c $(CBM_DIR)/lz4_store.c
# Zstd compression (for persistent artifacts)
ZSTD_SRCS = $(CBM_DIR)/zstd_store.c
# Preprocessor (C++)
PREPROCESSOR_SRC = $(CBM_DIR)/preprocessor.cpp
# SQLite writer
SQLITE_WRITER_SRC = $(CBM_DIR)/sqlite_writer.c
# Store module (new)
STORE_SRCS = src/store/store.c
# Cypher module (new)
CYPHER_SRCS = src/cypher/cypher.c
# MCP server module (new)
MCP_SRCS = src/mcp/mcp.c src/mcp/index_supervisor.c src/mcp/compact_out.c
# Shared MCP daemon coordination and local transport
DAEMON_SRCS = \
src/daemon/daemon.c \
src/daemon/project_lock.c \
src/daemon/version_cohort.c \
src/daemon/service.c \
src/daemon/runtime.c \
src/daemon/application.c \
src/daemon/frontend.c \
src/daemon/host.c \
src/daemon/bootstrap.c \
src/daemon/ipc.c
# Discover module (new)
DISCOVER_SRCS = \
src/discover/language.c \
src/discover/userconfig.c \
src/discover/gitignore.c \
src/discover/discover.c
# Graph buffer module (new)
GRAPH_BUFFER_SRCS = src/graph_buffer/graph_buffer.c
# Pipeline module (new)
PIPELINE_SRCS = \
src/pipeline/fqn.c \
src/pipeline/lsp_surface.c \
src/pipeline/pipeline_delta.c \
src/pipeline/path_alias.c \
src/pipeline/registry.c \
src/pipeline/pipeline.c \
src/pipeline/pipeline_incremental.c \
src/pipeline/worker_pool.c \
src/pipeline/pass_parallel.c \
src/pipeline/pass_definitions.c \
src/pipeline/pass_calls.c \
src/pipeline/pass_lsp_cross.c \
src/pipeline/pass_usages.c \
src/pipeline/pass_semantic.c \
src/pipeline/pass_tests.c \
src/pipeline/pass_githistory.c \
src/pipeline/pass_gitdiff.c \
src/pipeline/pass_configures.c \
src/pipeline/pass_configlink.c \
src/pipeline/pass_route_nodes.c \
src/pipeline/pass_enrichment.c \
src/pipeline/pass_envscan.c \
src/pipeline/pass_compile_commands.c \
src/pipeline/pass_infrascan.c \
src/pipeline/pass_k8s.c \
src/pipeline/pass_similarity.c \
src/pipeline/pass_semantic_edges.c \
src/pipeline/pass_complexity.c \
src/pipeline/pass_cross_repo.c \
src/pipeline/artifact.c \
src/pipeline/pass_pkgmap.c
# SimHash / MinHash module
SIMHASH_SRCS = src/simhash/minhash.c
# Semantic embedding module
SEMANTIC_SRCS = src/semantic/semantic.c src/semantic/ast_profile.c src/semantic/rotsq.c
# nomic-embed-code pretrained vectors (assembler blob)
UNIXCODER_BLOB_SRC = vendored/nomic/code_vectors_blob.S
# Traces module (new)
TRACES_SRCS = src/traces/traces.c
# Watcher module (new)
WATCHER_SRCS = src/watcher/watcher.c
# Git context module (new)
GIT_SRCS = src/git/git_context.c
# CLI module (new)
CLI_SRCS = src/cli/cli.c src/cli/progress_sink.c src/cli/hook_augment.c src/cli/client_adapter.c \
src/cli/agent_clients.c src/cli/agent_profiles.c \
src/cli/config_json_like.c src/cli/config_toml_edit.c src/cli/config_yaml_edit.c \
src/cli/config_text_edit.c src/cli/activation_transaction.c
# UI module (graph visualization)
UI_SRCS = \
src/ui/config.c \
src/ui/http_server.c \
src/ui/layout3d.c \
src/ui/httpd.c \
src/ui/embedded_stub.c
# mimalloc (vendored, global allocator override)
#
# Override strategy is platform-specific:
# * Unix (macOS/Linux): rely on static-link-order override — the prod mimalloc
# object is linked first so its strong malloc/free symbols win. We do NOT
# define MI_MALLOC_OVERRIDE here: that would compile alloc-override.c's
# forwarding definitions (malloc/free/posix_memalign/...) which, on macOS's
# two-level namespace, makes the binary's free == mi_free while system
# libraries keep allocating via the system allocator — pointers crossing that
# boundary hit "mimalloc: error: mi_free: invalid pointer". (Repro:
# `codebase-memory-mcp index <dir>` aborted on every run.)
# * Windows (MinGW, static CRT): the link-order trick fails (#424's allocator
# mismatch), so we define MI_MALLOC_OVERRIDE=1 to compile the static-CRT
# override added in mimalloc 3.3.0 (the _MSC_VER / _ACRTIMP /
# _CRT_HYBRIDPATCHABLE entry points) which take precedence over the static
# CRT's malloc/free.
# Note: mimalloc's source gates the override body on MI_MALLOC_OVERRIDE (CMake
# derives it from the MI_OVERRIDE option); MI_OVERRIDE alone is not read by the
# source — it is kept here only as this project's prod/test marker.
MIMALLOC_SRC = vendored/mimalloc/src/static.c
# Linux belongs on this list too (#1360). The "Unix relies on static-link-order
# override" strategy described above never actually worked: mimalloc defines
# malloc/free ONLY when MI_MALLOC_OVERRIDE is set (it gates alloc-override.c),
# so with the define off there were no strong symbols for the link order to
# prefer. Measured on the shipped v0.9.1-rc.1 artifacts — linux-arm64 glibc AND
# musl-static both report 0/6 allocator-owned size classes, i.e. ordinary malloc
# was going to libc and every purge/reclaim option set in cbm_mem_init applied
# only to the bound sqlite/tree-sitter populations.
#
# The macOS objection does not transfer. That abort is specific to the two-level
# namespace, where this binary's free becomes mi_free while system libraries keep
# allocating from the system allocator, so a pointer crossing the boundary hits
# "mi_free: invalid pointer". ELF is a flat namespace: interposition is
# process-wide and all-or-nothing, which is the configuration mimalloc is
# normally deployed in. macOS therefore stays off, deliberately and permanently.
#
# TWO defines, switched together in ONE place so they cannot drift:
# MI_MALLOC_OVERRIDE reaches the mimalloc translation unit and turns the
# override on; CBM_MEM_GLOBAL_OVERRIDE reaches OUR sources and tells the startup
# audit what to expect. CBM_MEM_GLOBAL_OVERRIDE goes only into CFLAGS_PROD,
# because MIMALLOC_CFLAGS_TEST compiles mimalloc with -DMI_OVERRIDE=0 and no
# override define at all — a test binary has no global override BY CONSTRUCTION
# and must never be told to expect one, or it warns on every run.
MIMALLOC_OVERRIDE_DEFINE :=
GLOBAL_OVERRIDE_DEFINE :=
ifneq ($(filter yes,$(IS_MINGW) $(IS_LINUX)),)
MIMALLOC_OVERRIDE_DEFINE := -DMI_MALLOC_OVERRIDE=1
GLOBAL_OVERRIDE_DEFINE := -DCBM_MEM_GLOBAL_OVERRIDE=1
endif
MIMALLOC_CFLAGS = -std=c11 -O2 -w \
-Ivendored/mimalloc/include \
-Ivendored/mimalloc/src \
-DMI_OVERRIDE=1 \
$(MIMALLOC_OVERRIDE_DEFINE)
MIMALLOC_CFLAGS_TEST = -std=c11 -g -O1 -w \
-Ivendored/mimalloc/include \
-Ivendored/mimalloc/src \
-DMI_OVERRIDE=0
# sqlite3 (vendored amalgamation — compiled ourselves for ASan instrumentation)
# SQLITE_ENABLE_FTS5: enables the FTS5 full-text search extension used by the
# BM25 search path in search_graph (see nodes_fts virtual table in store.c).
#
# SQLITE_OMIT_LOAD_EXTENSION: no cbm code calls sqlite3_load_extension or
# sqlite3_enable_load_extension, and we never want a graph DB to be able to pull
# a shared library into the process. Loading is off by default at runtime, but
# "disabled by default" is a setting while "not compiled in" is a property —
# this compiles the machinery out, removing the API surface and part of the
# dlopen/dlsym import surface with it. The composition gate asserts the symbols
# are absent from every release artifact.
SQLITE3_OMIT_FLAGS = -DSQLITE_OMIT_LOAD_EXTENSION
SQLITE3_SRC = vendored/sqlite3/sqlite3.c
SQLITE3_CFLAGS = -std=c11 -O2 -w -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=1 -DSQLITE_ENABLE_FTS5 \
$(SQLITE3_OMIT_FLAGS)
SQLITE3_CFLAGS_TEST = -std=c11 -g -O1 -w -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=1 -DSQLITE_ENABLE_FTS5 \
$(SQLITE3_OMIT_FLAGS)
# TRE regex (vendored, Windows only — POSIX uses system <regex.h>)
TRE_SRC = vendored/tre/tre_all.c
TRE_CFLAGS = -std=c11 -g -O1 -w -Ivendored/tre
# yyjson (vendored)
YYJSON_SRC = vendored/yyjson/yyjson.c
# All production sources
PROD_SRCS = $(FOUNDATION_SRCS) $(STORE_SRCS) $(CYPHER_SRCS) $(MCP_SRCS) $(DAEMON_SRCS) $(DISCOVER_SRCS) $(GRAPH_BUFFER_SRCS) $(PIPELINE_SRCS) $(SIMHASH_SRCS) $(SEMANTIC_SRCS) $(TRACES_SRCS) $(WATCHER_SRCS) $(GIT_SRCS) $(CLI_SRCS) $(UI_SRCS) $(YYJSON_SRC)
EXISTING_C_SRCS = $(EXTRACTION_SRCS) $(LSP_SRCS) $(TS_RUNTIME_SRC) \
$(GRAMMAR_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC)
# Project headers, as prerequisites for the targets built in one compiler
# invocation from sources (test-runner, test-repro-runner, test-runner-tsan,
# codebase-memory-mcp, test-foundation). Those rules list only .c files, so
# editing a header changed nothing make could see and the target was NOT
# rebuilt — an incremental build then silently tested the OLD header value.
# Found while revert-checking a constants.h change: the check passed because
# nothing had recompiled.
#
# Vendored trees are deliberately excluded — they are pinned, they carry their
# own explicit dep lists (LSP_UNITY_DEPS, TS_RUNTIME_DEPS), and globbing tens of
# thousands of grammar headers would cost more than it protects.
PROJECT_HDRS = $(wildcard src/*.h src/*/*.h $(CBM_DIR)/*.h $(CBM_DIR)/lsp/*.h tests/*.h \
tests/repro/*.h)
# ── Test sources ─────────────────────────────────────────────────
TEST_FOUNDATION_SRCS = \
tests/test_main.c \
tests/test_arena.c \
tests/test_hash_table.c \
tests/test_dyn_array.c \
tests/test_str_intern.c \
tests/test_log.c \
tests/test_str_util.c \
tests/test_workspace.c \
tests/test_platform.c \
tests/test_diagnostics.c \
tests/test_dump_verify.c \
tests/test_subprocess.c \
tests/test_private_file_lock.c \
tests/test_lock_registry.c
TEST_EXTRACTION_SRCS = \
tests/test_extraction.c \
tests/test_extraction_inheritance.c \
tests/test_extraction_imports.c \
tests/test_parse_coverage.c \
tests/test_grammar_regression.c \
tests/test_grammar_labels.c \
tests/test_grammar_imports.c \
tests/test_ac.c
TEST_STORE_SRCS = \
tests/test_store_nodes.c \
tests/test_store_edges.c \
tests/test_store_search.c \
tests/test_store_arch.c \
tests/test_store_bulk.c \
tests/test_store_pragmas.c \
tests/test_store_checkpoint.c \
tests/test_dump_verify_io.c
TEST_CYPHER_SRCS = \
tests/test_cypher.c
TEST_MCP_SRCS = \
tests/test_mcp.c \
tests/test_index_supervisor.c
TEST_DAEMON_SRCS = \
tests/test_daemon.c \
tests/test_project_lock.c \
tests/test_version_cohort.c \
tests/test_daemon_version.c \
tests/test_daemon_runtime.c \
tests/test_daemon_application.c \
tests/test_daemon_frontend.c \
tests/test_daemon_bootstrap.c \
tests/test_daemon_ipc.c
TEST_DISCOVER_SRCS = \
tests/test_language.c \
tests/test_userconfig.c \
tests/test_gitignore.c \
tests/test_git_context.c \
tests/test_discover.c
TEST_GRAPH_BUFFER_SRCS = tests/test_graph_buffer.c
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_cross_repo.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c tests/test_call_reference_contract.c tests/repro/repro_call_scope_usages.c tests/repro/repro_call_argument_usages.c tests/repro/repro_reference_precision.c tests/repro/repro_lexical_binding_precision.c tests/repro/repro_call_argument_matrix_a.c tests/repro/repro_call_argument_matrix_b.c tests/repro/repro_call_node_behaviors.c tests/repro/repro_language_registry.c tests/repro/repro_call_node_manifest.c tests/repro/repro_lsp_ordered_signatures.c tests/repro/repro_lsp_ordered_local.c tests/repro/repro_ts_overload_return_chains.c tests/repro/repro_harness_cleanup.c tests/repro/repro_runner_filter.c
TEST_WATCHER_SRCS = tests/test_watcher.c
TEST_LZ4_SRCS = tests/test_lz4.c
TEST_ZSTD_SRCS = tests/test_zstd.c
TEST_ARTIFACT_SRCS = tests/test_artifact.c
TEST_SQLITE_WRITER_SRCS = tests/test_sqlite_writer.c
TEST_GO_LSP_SRCS = tests/test_go_lsp.c
TEST_C_LSP_SRCS = tests/test_c_lsp.c
TEST_PHP_LSP_SRCS = tests/test_php_lsp.c
TEST_CS_LSP_SRCS = tests/test_cs_lsp.c
TEST_CS_LSP_BENCH_SRCS = tests/test_cs_lsp_bench.c
TEST_PERL_LSP_SRCS = tests/test_perl_lsp.c
TEST_SCOPE_SRCS = tests/test_scope.c
TEST_TYPE_REP_SRCS = tests/test_type_rep.c
TEST_PY_LSP_SRCS = tests/test_py_lsp.c
TEST_PY_LSP_BENCH_SRCS = tests/test_py_lsp_bench.c
TEST_PY_LSP_STRESS_SRCS = tests/test_py_lsp_stress.c
TEST_PY_LSP_SCALE_SRCS = tests/test_py_lsp_scale.c
TEST_TS_LSP_SRCS = tests/test_ts_lsp.c
TEST_JAVA_LSP_SRCS = tests/test_java_lsp.c tests/test_java_lsp_coverage.c
TEST_KOTLIN_LSP_SRCS = tests/test_kotlin_lsp.c
TEST_RUST_LSP_SRCS = tests/test_rust_lsp.c
TEST_INTEGRATION_SRCS = tests/test_integration.c tests/test_incremental.c tests/test_lang_contract.c tests/test_edge_imports.c tests/test_edge_structural.c tests/test_lsp_resolution_probe.c tests/test_node_creation_probe.c tests/test_edge_types_probe.c tests/test_convergence_probe.c tests/test_matrix_known_classes.c tests/test_matrix_new_constructs.c tests/test_grammar_probe_a.c tests/test_grammar_probe_b.c tests/test_grammar_probe_c.c tests/test_grammar_probe_d.c tests/test_grammar_probe_e.c tests/test_grammar_probe_f.c tests/test_grammar_probe_g.c
TEST_TRACES_SRCS = tests/test_traces.c
TEST_CLI_SRCS = tests/test_cli.c tests/test_agent_clients.c tests/test_agent_profiles.c \
tests/test_config_json_like.c \
tests/test_config_toml_edit.c tests/test_config_yaml_edit.c tests/test_config_text_edit.c \
tests/test_activation_transaction.c
TEST_MEM_SRCS = tests/test_mem.c
TEST_UI_SRCS = tests/test_ui.c
TEST_HTTPD_SRCS = tests/test_httpd.c
TEST_SECURITY_SRCS = tests/test_security.c
TEST_YAML_SRCS = tests/test_yaml.c
TEST_SEMANTIC_SRCS = tests/test_semantic.c
TEST_AST_PROFILE_SRCS = tests/test_ast_profile.c
TEST_SLAB_ALLOC_SRCS = tests/test_slab_alloc.c
TEST_SIMHASH_SRCS = tests/test_simhash.c
TEST_STACK_OVERFLOW_SRCS = tests/test_stack_overflow.c
# Cumulative BUG-REPRODUCTION suite (separate runner, NOT in ALL_TEST_SRCS).
# Contains RED reproductions plus GREEN controls — see tests/repro/repro_main.c.
# Kept out of the gating `make test` so `ci-ok` stays green; run via `make test-repro`.
TEST_REPRO_SRCS = \
tests/repro/repro_main.c \
tests/repro/repro_parallel_determinism.c \
tests/repro/repro_extraction.c \
tests/repro/repro_runner_filter.c \
tests/repro/repro_harness_cleanup.c \
tests/repro/repro_language_registry.c \
tests/repro/repro_call_node_manifest.c \
tests/repro/repro_call_scope_usages.c \
tests/repro/repro_call_argument_usages.c \
tests/repro/repro_lsp_ordered_signatures.c \
tests/repro/repro_lsp_ordered_local.c \
tests/repro/repro_ts_overload_return_chains.c \
tests/repro/repro_reference_precision.c \
tests/repro/repro_lexical_binding_precision.c \
tests/repro/repro_call_argument_matrix_a.c \
tests/repro/repro_call_argument_matrix_b.c \
tests/repro/repro_call_node_behaviors.c \
tests/repro/repro_issue495.c \
tests/repro/repro_issue521.c \
tests/repro/repro_issue382.c \
tests/repro/repro_issue408.c \
tests/repro/repro_issue56.c \
tests/repro/repro_issue480.c \
tests/repro/repro_issue571.c \
tests/repro/repro_issue523.c \
tests/repro/repro_issue546.c \
tests/repro/repro_issue627.c \
tests/repro/repro_issue514.c \
tests/repro/repro_issue510.c \
tests/repro/repro_issue557.c \
tests/repro/repro_issue520.c \
tests/repro/repro_issue333.c \
tests/repro/repro_issue570.c \
tests/repro/repro_issue409.c \
tests/repro/repro_issue431.c \
tests/repro/repro_issue607.c \
tests/repro/repro_issue403.c \
tests/repro/repro_issue434.c \
tests/repro/repro_issue471.c \
tests/repro/repro_issue221.c \
tests/repro/repro_issue548.c \
tests/repro/repro_new_ts_class_field_arrow.c \
tests/repro/repro_new_py_tuple_unpack.c \
tests/repro/repro_new_cypher_limit_zero.c \
tests/repro/repro_issue363.c \
tests/repro/repro_issue581.c \
tests/repro/repro_issue787.c \
tests/repro/repro_issue842.c \
tests/repro/repro_issue964.c \
tests/repro/repro_invariant_calls.c \
tests/repro/repro_invariant_graph.c \
tests/repro/repro_invariant_breadth.c \
tests/repro/repro_invariant_enclosing_parity.c \
tests/repro/repro_invariant_lsp_rescue.c \
tests/repro/repro_invariant_discovery_fqn.c \
tests/repro/repro_grammar_core.c \
tests/repro/repro_grammar_scripting.c \
tests/repro/repro_grammar_functional.c \
tests/repro/repro_grammar_systems.c \
tests/repro/repro_grammar_web.c \
tests/repro/repro_grammar_config.c \
tests/repro/repro_grammar_build.c \
tests/repro/repro_grammar_shells.c \
tests/repro/repro_grammar_scientific.c \
tests/repro/repro_grammar_markup.c \
tests/repro/repro_grammar_misc.c \
tests/repro/repro_lsp_c_cpp.c \
tests/repro/repro_lsp_go_py.c \
tests/repro/repro_lsp_ts.c \
tests/repro/repro_ts_inherited_method.c \
tests/repro/repro_lsp_java_cs.c \
tests/repro/repro_lsp_kt_php_rust.c
ALL_TEST_SRCS =$(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DAEMON_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_ZSTD_SRCS) $(TEST_ARTIFACT_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_PHP_LSP_SRCS) $(TEST_CS_LSP_SRCS) $(TEST_CS_LSP_BENCH_SRCS) $(TEST_PERL_LSP_SRCS) $(TEST_SCOPE_SRCS) $(TEST_TYPE_REP_SRCS) $(TEST_PY_LSP_SRCS) $(TEST_PY_LSP_BENCH_SRCS) $(TEST_PY_LSP_STRESS_SRCS) $(TEST_PY_LSP_SCALE_SRCS) $(TEST_TS_LSP_SRCS) $(TEST_JAVA_LSP_SRCS) $(TEST_KOTLIN_LSP_SRCS) $(TEST_RUST_LSP_SRCS) $(TEST_TRACES_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_HTTPD_SRCS) $(TEST_SECURITY_SRCS) $(TEST_YAML_SRCS) $(TEST_SEMANTIC_SRCS) $(TEST_AST_PROFILE_SRCS) $(TEST_SLAB_ALLOC_SRCS) $(TEST_SIMHASH_SRCS) $(TEST_STACK_OVERFLOW_SRCS) $(TEST_INTEGRATION_SRCS)
# ── Build directories ────────────────────────────────────────────
BUILD_DIR = build/c
# ── Object file compilation (grammars need relaxed warnings) ─────
# Grammar + tree-sitter runtime: compiled without -Werror (upstream code has warnings)
GRAMMAR_CFLAGS = -std=c11 -D_DEFAULT_SOURCE -O2 -w -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC)
GRAMMAR_CFLAGS_TEST = -std=c11 -D_DEFAULT_SOURCE -g -O1 -w -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC) \
$(SANITIZE)
GRAMMAR_CFLAGS_TSAN = -std=c11 -D_DEFAULT_SOURCE -g -O1 -w -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC) \
$(TSAN_SANITIZE)
# Object files for grammars + ts_runtime + lsp_all + preprocessor
GRAMMAR_OBJS_TEST = $(patsubst $(CBM_DIR)/%.c,$(BUILD_DIR)/%.o,$(GRAMMAR_SRCS))
TS_RUNTIME_OBJ_TEST = $(BUILD_DIR)/ts_runtime.o
LSP_OBJ_TEST = $(BUILD_DIR)/lsp_all.o
PP_OBJ_TEST = $(BUILD_DIR)/preprocessor.o
GRAMMAR_OBJS_TSAN = $(patsubst $(CBM_DIR)/%.c,$(BUILD_DIR)/tsan_%.o,$(GRAMMAR_SRCS))
TS_RUNTIME_OBJ_TSAN = $(BUILD_DIR)/tsan_ts_runtime.o
LSP_OBJ_TSAN = $(BUILD_DIR)/tsan_lsp_all.o
PP_OBJ_TSAN = $(BUILD_DIR)/tsan_preprocessor.o
# ── Targets ──────────────────────────────────────────────────────
.PHONY: test test-par test-repro test-foundation test-tsan test-daemon-smoke cbm cbm-with-ui frontend embed clean-c lint lint-tidy lint-cppcheck lint-format security
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
# ── Foundation-only test (fast, no extraction) ───────────────────
$(BUILD_DIR)/test-foundation: $(TEST_FOUNDATION_SRCS) $(FOUNDATION_SRCS) $(PROJECT_HDRS) | $(BUILD_DIR)
$(CC) $(CFLAGS_TEST) -o $@ $(TEST_FOUNDATION_SRCS) $(FOUNDATION_SRCS) $(LDFLAGS_TEST)
test-foundation: $(BUILD_DIR)/test-foundation
cd $(CURDIR) && $(BUILD_DIR)/test-foundation
# ── Grammar/TS/LSP object files (compiled with relaxed warnings) ─
$(BUILD_DIR)/%.o: $(CBM_DIR)/%.c | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS_TEST) -c -o $@ $<
$(BUILD_DIR)/ts_runtime.o: $(TS_RUNTIME_DEPS) | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS_TEST) -c -o $@ $<
$(BUILD_DIR)/lsp_all.o: $(LSP_UNITY_DEPS) | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS_TEST) $(SANITIZE) -c -o $@ $<
$(BUILD_DIR)/preprocessor.o: $(CBM_DIR)/preprocessor.cpp | $(BUILD_DIR)
$(CXX) $(CXXFLAGS_TEST) -w -I$(CBM_DIR)/vendored -c -o $@ $<
$(BUILD_DIR)/tsan_%.o: $(CBM_DIR)/%.c | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS_TSAN) -c -o $@ $<
$(BUILD_DIR)/tsan_ts_runtime.o: $(TS_RUNTIME_DEPS) | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS_TSAN) -c -o $@ $<
$(BUILD_DIR)/tsan_lsp_all.o: $(LSP_UNITY_DEPS) | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS_TSAN) -c -o $@ $<
$(BUILD_DIR)/tsan_preprocessor.o: $(CBM_DIR)/preprocessor.cpp | $(BUILD_DIR)
$(CXX) $(CXXFLAGS_TSAN) -w -I$(CBM_DIR)/vendored -c -o $@ $<
# ── Full test binary ─────────────────────────────────────────────
# mimalloc object files
MIMALLOC_OBJ_TEST = $(BUILD_DIR)/mimalloc.o
MIMALLOC_OBJ_TSAN = $(BUILD_DIR)/tsan_mimalloc.o
MIMALLOC_OBJ_PROD = $(BUILD_DIR)/prod_mimalloc.o
$(BUILD_DIR)/mimalloc.o: $(MIMALLOC_SRC) | $(BUILD_DIR)
$(CC) $(MIMALLOC_CFLAGS_TEST) -c -o $@ $<
$(BUILD_DIR)/tsan_mimalloc.o: $(MIMALLOC_SRC) | $(BUILD_DIR)
$(CC) $(MIMALLOC_CFLAGS_TEST) $(TSAN_SANITIZE) -c -o $@ $<
# static.c is an AMALGAMATION: it #includes options.c and friends, none of which
# make can see through the single named prerequisite, and the compile flags live
# in this Makefile. Both gaps bit for real — the __DATE__ removal in options.c and
# SQLITE_OMIT_LOAD_EXTENSION below each silently did nothing on an incremental
# build because the object was considered up to date. Depend on the included
# sources AND on this Makefile so a flag change rebuilds too.
$(BUILD_DIR)/prod_mimalloc.o: $(MIMALLOC_SRC) $(wildcard vendored/mimalloc/src/*.c) \
$(wildcard vendored/mimalloc/include/*.h) Makefile.cbm | $(BUILD_DIR)
$(CC) $(MIMALLOC_CFLAGS) -c -o $@ $<
# sqlite3 object files (vendored amalgamation)
SQLITE3_OBJ_TEST = $(BUILD_DIR)/sqlite3.o
SQLITE3_OBJ_TSAN = $(BUILD_DIR)/tsan_sqlite3.o
SQLITE3_OBJ_PROD = $(BUILD_DIR)/prod_sqlite3.o
$(BUILD_DIR)/sqlite3.o: $(SQLITE3_SRC) | $(BUILD_DIR)
$(CC) $(SQLITE3_CFLAGS_TEST) $(SANITIZE) -c -o $@ $<
$(BUILD_DIR)/tsan_sqlite3.o: $(SQLITE3_SRC) | $(BUILD_DIR)
$(CC) $(SQLITE3_CFLAGS_TEST) $(TSAN_SANITIZE) -c -o $@ $<
$(BUILD_DIR)/prod_sqlite3.o: $(SQLITE3_SRC) Makefile.cbm | $(BUILD_DIR)
$(CC) $(SQLITE3_CFLAGS) -c -o $@ $<
# TRE regex (only compiled on Windows — POSIX uses system <regex.h>)
TRE_OBJ_TEST :=
TRE_OBJ_TSAN :=
TRE_OBJ_PROD :=
ifeq ($(IS_MINGW),yes)
TRE_OBJ_TEST = $(BUILD_DIR)/tre.o
TRE_OBJ_TSAN = $(BUILD_DIR)/tsan_tre.o
TRE_OBJ_PROD = $(BUILD_DIR)/prod_tre.o
# tre.o keeps UBSan's alignment check OFF, and that is not a coverage
# preference -- it is a portability fact. Arming it here (attempted, then
# reverted after this bisect) makes the vendored TRE regex engine trap on
# Windows/ARM64, where the CLANGARM64 leg builds with -fsanitize-trap and a
# trap is an illegal instruction with no diagnostic: every fixture-indexing
# suite died with STATUS_ILLEGAL_INSTRUCTION and no message. macOS and Linux
# stay clean under the same check because Windows is LLP64 -- 32-bit long --
# so TRE's struct layouts and access widths differ there and nowhere else.
#
# TRE is third-party code we vendor and do not modify, so the alignment
# behaviour is upstream's to change; suppressing the check for this one
# object is the honest scope. Everything else keeps alignment armed.
$(BUILD_DIR)/tre.o: $(TRE_SRC) | $(BUILD_DIR)
$(CC) $(TRE_CFLAGS) $(SANITIZE) -fno-sanitize=alignment -c -o $@ $<
$(BUILD_DIR)/tsan_tre.o: $(TRE_SRC) | $(BUILD_DIR)
$(CC) $(TRE_CFLAGS) $(TSAN_SANITIZE) -c -o $@ $<
$(BUILD_DIR)/prod_tre.o: $(TRE_SRC) | $(BUILD_DIR)
$(CC) $(TRE_CFLAGS) -O2 -c -o $@ $<
endif
# Vendored LZ4 (test build)
LZ4_OBJ_TEST = $(BUILD_DIR)/test_lz4.o $(BUILD_DIR)/test_lz4hc.o
LZ4_OBJ_TSAN = $(BUILD_DIR)/tsan_lz4.o $(BUILD_DIR)/tsan_lz4hc.o
$(BUILD_DIR)/test_lz4.o: $(CBM_DIR)/vendored/lz4/lz4.c | $(BUILD_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 $(SANITIZE) -w -I$(CBM_DIR) -c -o $@ $<
$(BUILD_DIR)/test_lz4hc.o: $(CBM_DIR)/vendored/lz4/lz4hc.c | $(BUILD_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 $(SANITIZE) -w -I$(CBM_DIR)/vendored/lz4 -c -o $@ $<
$(BUILD_DIR)/tsan_lz4.o: $(CBM_DIR)/vendored/lz4/lz4.c | $(BUILD_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 $(TSAN_SANITIZE) -w -I$(CBM_DIR) -c -o $@ $<
$(BUILD_DIR)/tsan_lz4hc.o: $(CBM_DIR)/vendored/lz4/lz4hc.c | $(BUILD_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 $(TSAN_SANITIZE) -w -I$(CBM_DIR)/vendored/lz4 -c -o $@ $<
# Vendored zstd (test build)
# ZSTD_EXTRA_CFLAGS: hook for sanitizer lanes. The MSan lane needs
# `-include stdint.h` for THIS object only: zstd's MEMORY_SANITIZER-guarded
# block declares __msan_test_shadow returning intptr_t, but the amalgamator
# collapsed the `#define ZSTD_DEPS_NEED_STDINT` re-include that should supply
# it. The flag cannot go on the global SANITIZE line: force-including a libc
# header ahead of a source file freezes glibc's feature-test macros before
# files like sqlite3.c set _GNU_SOURCE themselves, which strips
# MREMAP_MAYMOVE/nanosleep out of their view of libc.
ZSTD_OBJ_TEST = $(BUILD_DIR)/test_zstd.o
ZSTD_OBJ_TSAN = $(BUILD_DIR)/tsan_zstd.o
$(BUILD_DIR)/test_zstd.o: $(CBM_DIR)/vendored/zstd/zstd.c | $(BUILD_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 $(SANITIZE) $(ZSTD_EXTRA_CFLAGS) -w -I$(CBM_DIR)/vendored/zstd -c -o $@ $<
$(BUILD_DIR)/tsan_zstd.o: $(CBM_DIR)/vendored/zstd/zstd.c | $(BUILD_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 $(TSAN_SANITIZE) -w -I$(CBM_DIR)/vendored/zstd -c -o $@ $<
# nomic-embed-code pretrained vector blob
UNIXCODER_OBJ = $(BUILD_DIR)/unixcoder_blob.o
$(UNIXCODER_OBJ): $(UNIXCODER_BLOB_SRC) vendored/nomic/code_vectors.bin | $(BUILD_DIR)
$(CC) -c -o $@ $<
OBJS_VENDORED_TEST = $(MIMALLOC_OBJ_TEST) $(SQLITE3_OBJ_TEST) $(TRE_OBJ_TEST) $(GRAMMAR_OBJS_TEST) $(TS_RUNTIME_OBJ_TEST) $(LSP_OBJ_TEST) $(PP_OBJ_TEST) $(LZ4_OBJ_TEST) $(ZSTD_OBJ_TEST) $(UNIXCODER_OBJ)
OBJS_VENDORED_TSAN = $(MIMALLOC_OBJ_TSAN) $(SQLITE3_OBJ_TSAN) $(TRE_OBJ_TSAN) $(GRAMMAR_OBJS_TSAN) $(TS_RUNTIME_OBJ_TSAN) $(LSP_OBJ_TSAN) $(PP_OBJ_TSAN) $(LZ4_OBJ_TSAN) $(ZSTD_OBJ_TSAN) $(UNIXCODER_OBJ)
$(BUILD_DIR)/test-runner: $(ALL_TEST_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) $(PROJECT_HDRS) | $(BUILD_DIR)
$(CC) $(CFLAGS_TEST) -Itests -Itests/repro -o $@ \
$(ALL_TEST_SRCS) $(PROD_SRCS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
$(OBJS_VENDORED_TEST) \
$(LDFLAGS_TEST)
test: $(BUILD_DIR)/test-runner
cd $(CURDIR) && $(BUILD_DIR)/test-runner
# Parallel variant: every suite as its own process; identical gate quality
# (see the ZERO-LOSS CONTRACT in scripts/run-tests-parallel.sh — union guard
# against --list-suites + aggregated pass/fail/skip totals).
test-par: $(BUILD_DIR)/test-runner
cd $(CURDIR) && bash scripts/run-tests-parallel.sh $(BUILD_DIR)/test-runner
# Focused native development is intentionally a separate, explicit target so
# an inherited TEST_SUITES value cannot silently narrow the gating test target.
TEST_SUITES ?=
test-focused: $(BUILD_DIR)/test-runner
@test -n "$(strip $(TEST_SUITES))" || \
(echo "TEST_SUITES is required for test-focused"; exit 2)
cd $(CURDIR) && $(BUILD_DIR)/test-runner $(TEST_SUITES)
# ── Cumulative bug-reproduction runner (RED board by design, non-gating) ──
# Mirrors test-runner's link line but uses repro_main.c (own main + counters)
# and TEST_REPRO_SRCS instead of ALL_TEST_SRCS. Exits non-zero while any bug is
# still reproduced (the expected state); bug-repro.yml surfaces it as a board.
$(BUILD_DIR)/test-repro-runner: $(TEST_REPRO_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) $(PROJECT_HDRS) | $(BUILD_DIR)
$(CC) $(CFLAGS_TEST) -Itests -o $@ \
$(TEST_REPRO_SRCS) $(PROD_SRCS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
$(OBJS_VENDORED_TEST) \
$(LDFLAGS_TEST)
test-repro: $(BUILD_DIR)/test-repro-runner
cd $(CURDIR) && bash tests/repro/repro_script_summary.sh
cd $(CURDIR) && bash tests/repro/repro_script_zero_selection.sh
cd $(CURDIR) && bash tests/repro/repro_script_missing_summary.sh
cd $(CURDIR) && bash tests/repro/repro_script_skipped_summary.sh
cd $(CURDIR) && bash tests/repro/repro_runner_zero_selection.sh
cd $(CURDIR) && bash tests/repro/repro_runner_passing_semantics.sh
cd $(CURDIR) && bash tests/repro/repro_script_no_workspace_artifact.sh
cd $(CURDIR) && bash tests/repro/repro_script_stderr_summary.sh
cd $(CURDIR) && bash tests/repro/repro_make_tracks_headers.sh
cd $(CURDIR) && $(BUILD_DIR)/test-repro-runner
# ── TSan full test ───────────────────────────────────────────────
# Every threaded production surface that runs clean AND stable under TSan:
# allocator concurrency (mem, slab_alloc), the parallel extraction worker pool
# (parallel, worker_pool, pipeline), the filesystem watcher (watcher), the
# embedded HTTP server (httpd), diagnostics sampling (diagnostics), the MCP
# server + mutation guard (mcp, mcp_mutation_guard), subprocess supervision
# (subprocess), and the runnable daemon-coordination paths (daemon,
# daemon_application). Was `mem slab_alloc parallel` — a keyhole that ran TSan
# over no real threaded production code.
#
# NO EXCLUSIONS. A sanitizer covers the complete surface or it is asserting
# coverage it does not have. The three suites once excluded here
# (daemon_runtime, daemon_ipc, daemon_frontend) are back:
# daemon_ipc, daemon_frontend — the documented test-harness race and thread
# leaks no longer reproduce; both run clean under TSan.
# daemon_runtime — did NOT deadlock as documented. It reported a REAL
# production data race on the log sink (plain global function pointer,
# written by the configuring thread, read AND CALLED by connection
# workers), now fixed with atomics in log.c. Excluding the suite is what
# had hidden it.
TEST_TSAN_SUITES ?= mem slab_alloc parallel worker_pool watcher httpd pipeline \
diagnostics mcp mcp_mutation_guard subprocess daemon daemon_application \
daemon_runtime daemon_ipc daemon_frontend
# halt_on_error: a race is a bug, stop at the first one.
#
# report_thread_leaks=0: this disables TSan's thread-HYGIENE check only —
# RACE detection, the reason the lane exists, is completely unaffected. Some
# daemon fixtures fork after the process has gone multi-threaded, and in the
# forked child TSan sees the parent's finished threads as never-joined even
# though the fixture joins them (test_daemon_frontend.c:726 joins the thread
# reported at :698). It fires on macOS and not Linux, i.e. it tracks fork
# semantics rather than anything about our code. The alternative was dropping
# whole suites from the lane, which costs real race coverage; this costs none.
TSAN_OPTIONS ?= halt_on_error=1:report_thread_leaks=0
# A fixed concurrent envelope keeps TSan deterministic across developer hosts
# and GitHub runners. Four workers still exercise real races without turning
# sanitizer-instrumented allocator bookkeeping into a high-core lock convoy.
# Normal ASan/native/soak paths remain uncapped. High-worker TSan diagnostics
# invoke test-runner-tsan directly so release gates cannot drift accidentally.
$(BUILD_DIR)/test-runner-tsan: $(ALL_TEST_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TSAN) $(PROJECT_HDRS) | $(BUILD_DIR)
$(CC) $(CFLAGS_TSAN) -Itests -Itests/repro -o $@ \
$(ALL_TEST_SRCS) $(PROD_SRCS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
$(OBJS_VENDORED_TSAN) \
$(LDFLAGS_TSAN)
test-tsan: $(BUILD_DIR)/test-runner-tsan
@echo "ThreadSanitizer workers: 4"
cd $(CURDIR) && CBM_WORKERS=4 TSAN_OPTIONS="$(TSAN_OPTIONS)" \
$(BUILD_DIR)/test-runner-tsan $(TEST_TSAN_SUITES)
# Real-binary POSIX lifecycle smoke. The endpoint is deliberately account-wide;
# an explicit Make invocation requires a clean rendezvous and fails rather than
# silently skipping an occupied one. Deterministic C tests cover Windows IPC.
test-daemon-smoke: $(BUILD_DIR)/codebase-memory-mcp
cd $(CURDIR) && CBM_DAEMON_SMOKE_REQUIRE_RUN=1 python3 tests/test_daemon_smoke.py $(BUILD_DIR)/codebase-memory-mcp
# ── Production binary ────────────────────────────────────────────
# Grammar/TS/LSP objects for production (compiled with relaxed warnings, -O2)
GRAMMAR_OBJS_PROD = $(patsubst $(CBM_DIR)/%.c,$(BUILD_DIR)/prod_%.o,$(GRAMMAR_SRCS))
TS_RUNTIME_OBJ_PROD = $(BUILD_DIR)/prod_ts_runtime.o
LSP_OBJ_PROD = $(BUILD_DIR)/prod_lsp_all.o
PP_OBJ_PROD = $(BUILD_DIR)/prod_preprocessor.o
$(BUILD_DIR)/prod_%.o: $(CBM_DIR)/%.c | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS) -c -o $@ $<
$(BUILD_DIR)/prod_ts_runtime.o: $(TS_RUNTIME_DEPS) | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS) -c -o $@ $<
$(BUILD_DIR)/prod_lsp_all.o: $(LSP_UNITY_DEPS) | $(BUILD_DIR)
$(CC) $(GRAMMAR_CFLAGS) -c -o $@ $<
$(BUILD_DIR)/prod_preprocessor.o: $(CBM_DIR)/preprocessor.cpp | $(BUILD_DIR)
$(CXX) $(CXXFLAGS_PROD) -w -I$(CBM_DIR)/vendored -c -o $@ $<
# Vendored LZ4 (compiled separately, not unity-built via lz4_store.c)
LZ4_OBJ_PROD = $(BUILD_DIR)/prod_lz4.o $(BUILD_DIR)/prod_lz4hc.o
$(BUILD_DIR)/prod_lz4.o: $(CBM_DIR)/vendored/lz4/lz4.c | $(BUILD_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -O2 -w -I$(CBM_DIR) -c -o $@ $<
$(BUILD_DIR)/prod_lz4hc.o: $(CBM_DIR)/vendored/lz4/lz4hc.c | $(BUILD_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -O2 -w -I$(CBM_DIR)/vendored/lz4 -c -o $@ $<
# Vendored zstd (compiled separately, not unity-built via zstd_store.c)
ZSTD_OBJ_PROD = $(BUILD_DIR)/prod_zstd.o
$(BUILD_DIR)/prod_zstd.o: $(CBM_DIR)/vendored/zstd/zstd.c | $(BUILD_DIR)
$(CC) -std=c11 -D_DEFAULT_SOURCE -O2 -w -I$(CBM_DIR)/vendored/zstd -c -o $@ $<
OBJS_VENDORED_PROD = $(MIMALLOC_OBJ_PROD) $(SQLITE3_OBJ_PROD) $(TRE_OBJ_PROD) $(GRAMMAR_OBJS_PROD) $(TS_RUNTIME_OBJ_PROD) $(LSP_OBJ_PROD) $(PP_OBJ_PROD) $(LZ4_OBJ_PROD) $(ZSTD_OBJ_PROD) $(UNIXCODER_OBJ)
MAIN_SRC = src/main.c
# Rebuild when the build CONFIGURATION changes, not only when sources do. The
# product binary is compiled in ONE shot from sources, so after a TEST_SEAMS or
# version flip make finds the binary newer than every source and skips the recipe
# entirely, handing back a binary built with the PREVIOUS configuration. The
# dangerous direction is a release build silently keeping test seams, which is a
# property no reviewer can see by reading the diff. The stamp's CONTENT is the
# configuration and it is rewritten only when that content changes, so ordinary
# incremental builds are unaffected. (Single-quoted: the signature may contain
# the escaped quotes of -DCBM_VERSION, which are literal inside '', but must not
# contain a literal single quote.)
# SANITIZE belongs here: it changes the instrumentation compiled into every
# object, so a build that only changes it MUST rebuild. Its absence made
# `scripts/test.sh SANITIZE= ...` -- the documented way to get a plain build
# for trap debugging -- silently re-run the previously instrumented binary,
# which during this bisect produced a "crashes without sanitizers too" result
# that was pure artifact and sent the investigation down the wrong path.
BUILD_CONFIG_SIG := TEST_SEAMS=$(TEST_SEAMS)|CFLAGS_EXTRA=$(CFLAGS_EXTRA)|SANITIZE=$(SANITIZE)
.PHONY: build-config-check
build-config-check:
$(BUILD_DIR)/.build-config: build-config-check | $(BUILD_DIR)
@printf '%s\n' '$(BUILD_CONFIG_SIG)' > $@.tmp
@if cmp -s $@.tmp $@; then rm -f $@.tmp; \
else mv -f $@.tmp $@; rm -f $(BUILD_DIR)/codebase-memory-mcp; \
echo "=== build config changed -> full rebuild: $(BUILD_CONFIG_SIG) ==="; fi
$(BUILD_DIR)/codebase-memory-mcp: $(MAIN_SRC) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_PROD) $(BUILD_DIR)/.build-config $(PROJECT_HDRS) | $(BUILD_DIR)
$(CC) $(CFLAGS_PROD) -o $@ \
$(MAIN_SRC) $(PROD_SRCS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
$(OBJS_VENDORED_PROD) \
$(LDFLAGS)
cbm: $(BUILD_DIR)/codebase-memory-mcp
@echo "Built: $(BUILD_DIR)/codebase-memory-mcp"
# ── Build with embedded UI (requires Node.js) ───────────────────
# Swap embedded_stub.c for the generated embedded_assets.c
UI_SRCS_WITH_ASSETS = $(subst src/ui/embedded_stub.c,src/ui/embedded_assets.c,$(UI_SRCS))
PROD_SRCS_WITH_ASSETS = $(subst src/ui/embedded_stub.c,src/ui/embedded_assets.c,$(PROD_SRCS))
# Embedded asset object files (generated by embed script)
EMBED_OBJS = $(wildcard $(BUILD_DIR)/embedded/embed_*.o)
frontend:
cd graph-ui && npm ci && npm run build
embed: frontend
scripts/embed-frontend.sh graph-ui/dist $(BUILD_DIR)/embedded
cbm-with-ui: embed $(OBJS_VENDORED_PROD)
$(CC) $(CFLAGS_PROD) -o $(BUILD_DIR)/codebase-memory-mcp \
$(MAIN_SRC) $(PROD_SRCS_WITH_ASSETS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
$(OBJS_VENDORED_PROD) \
$(wildcard $(BUILD_DIR)/embedded/embed_*.o) \
$(LDFLAGS)
@echo "Built with UI: $(BUILD_DIR)/codebase-memory-mcp"
clean-c: export BUILD_DIR := $(BUILD_DIR)
clean-c:
@bash -c 'source scripts/path-safety.sh && cbm_remove_build_dir "$$PWD" "$$BUILD_DIR"'
# ── Linting ─────────────────────────────────────────────────────
# clang-tidy and clang-format: use Homebrew LLVM on macOS, overridable via make args
# e.g. make lint-format CLANG_FORMAT=clang-format-18
LLVM_BIN := $(shell brew --prefix llvm 2>/dev/null)/bin
CLANG_TIDY ?= $(shell [ -x "$(LLVM_BIN)/clang-tidy" ] && echo "$(LLVM_BIN)/clang-tidy" || echo clang-tidy)
CLANG_FORMAT ?= $(shell [ -x "$(LLVM_BIN)/clang-format" ] && echo "$(LLVM_BIN)/clang-format" || echo clang-format)
CPPCHECK ?= cppcheck
# macOS SDK sysroot (needed for Homebrew LLVM to find system headers)
SYSROOT = $(shell xcrun --show-sdk-path 2>/dev/null)
SYSROOT_FLAG = $(if $(SYSROOT),-isysroot $(SYSROOT),)
# Our source files (excluding vendored, grammars, tree-sitter runtime)
LINT_SRCS = $(FOUNDATION_SRCS) $(STORE_SRCS) $(CYPHER_SRCS) $(MCP_SRCS) $(DAEMON_SRCS) \
$(DISCOVER_SRCS) $(GRAPH_BUFFER_SRCS) $(PIPELINE_SRCS) $(SIMHASH_SRCS) $(SEMANTIC_SRCS) \
$(TRACES_SRCS) $(WATCHER_SRCS) $(CLI_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) \
$(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(MAIN_SRC)
LINT_HDRS = $(wildcard src/**/*.h src/*.h $(CBM_DIR)/*.h)
LINT_TEST_SRCS = $(ALL_TEST_SRCS)
# clang-tidy: deep static analysis (config in .clang-tidy)
lint-tidy:
@echo "=== clang-tidy ==="
@$(CLANG_TIDY) --quiet $(LINT_SRCS) -- $(CFLAGS_COMMON) $(SYSROOT_FLAG)
# cppcheck: complementary analysis (config in .cppcheck)
lint-cppcheck:
@echo "=== cppcheck ==="
@$(CPPCHECK) --enable=warning,style,performance,portability \
--std=c11 --language=c \
--suppressions-list=.cppcheck \
--error-exitcode=1 \
--inline-suppr \
--quiet \
--suppress=varFuncNullUB \
--suppress=intToPointerCast \
--suppress=unusedStructMember \
--suppress=nullPointerOutOfMemory \
--suppress=nullPointerArithmeticOutOfMemory \
--suppress=ctunullpointerOutOfMemory \
--suppress='nullPointer:internal/cbm/sqlite_writer.c' \
-Isrc -Ivendored -Ivendored/sqlite3 \
-I$(CBM_DIR) -I$(TS_INCLUDE) -I$(BUILD_DIR)/generated \
$(LINT_SRCS)
# clang-format: formatting check (config in .clang-format, dry-run = no changes)
lint-format:
@echo "=== clang-format ==="
@$(CLANG_FORMAT) --dry-run --Werror $(LINT_SRCS) $(LINT_HDRS)
# Ban ALL NOLINT variants EXCEPT whitelisted NOLINT(misc-no-recursion).
# The whitelist is in src/foundation/recursion_whitelist.h with documented reasoning.
# Rule: NOLINT(misc-no-recursion) is allowed ONLY on function definitions that are
# listed in recursion_whitelist.h. All other NOLINT forms are banned.
lint-no-suppress:
@echo "=== NOLINT check ==="
@if grep -rn 'NOLINT' src/ internal/cbm/*.c internal/cbm/*.h 2>/dev/null \
| grep -v vendored \
| grep -v 'NOLINT(misc-no-recursion)' \
| grep -v 'recursion_whitelist.h'; then \
echo "ERROR: Banned NOLINT comment found in source code."; \
echo "Only NOLINT(misc-no-recursion) is allowed, and only for whitelisted functions."; \
echo "See src/foundation/recursion_whitelist.h for the whitelist."; \
exit 1; \
fi
@echo " Checking NOLINT(misc-no-recursion) against whitelist..."
@scripts/check-nolint-whitelist.sh
# All linters (run with make -j3 lint for parallel execution)
lint: lint-tidy lint-cppcheck lint-format lint-no-suppress
@echo "=== All linters passed ==="
# CI linters (no clang-tidy — platform-dependent, enforced locally via pre-commit)
lint-ci: lint-cppcheck lint-format lint-no-suppress
@echo "=== CI linters passed ==="
# ── Local memory-diagnostic lanes (not PR-CI gates by decision: the diag
# build doubles a runner's bill for a marginal delta, and a gating analyzer
# lane needs a suppression story the NOLINT ban deliberately forbids.
# Run these on the local ladder; both found real bugs on first use.) ──
# Newest pinned toolchain + ASan/UBSan + straighter stacks. Separate BUILD_DIR
# so the shipping-toolchain build stays untouched.
#
# This lane also turns on the ASan checks that are OFF by default everywhere
# else, which is the difference between running ASan and running all of it:
# detect_stack_use_after_return a returned-to stack frame reused later
# detect_stack_use_after_scope a pointer outliving its enclosing block
# strict_string_checks str* reads validated over the whole buffer
#
# detect_invalid_pointer_pairs is deliberately NOT here. It fires during
# static initialisation inside vendored simplecpp -- a std::string global at
# simplecpp.cpp:101 -- reporting a pair whose second "pointer" is the
# sentinel 0xfffffffffffffff3, i.e. libstdc++ string internals rather than
# anything this codebase wrote. The option is a process-wide runtime flag
# with no per-file scoping, so it cannot be aimed away from vendored code the
# way the analyzer's path filter can. Enabling it would mean a permanently
# red lane reporting a non-defect, which teaches people to ignore the lane.
# The instrumentation it requires (-fsanitize=pointer-compare,pointer-
# subtract) comes out with it, since it is dead weight without the check.
# They stay on the diagnostic lane rather than the gating ones until they have
# a clean history here; promoting them is a separate, deliberate step.
DIAG_LLVM_BIN ?= /opt/homebrew/opt/llvm/bin
diag:
@echo "=== diagnostic lane: $(shell $(DIAG_LLVM_BIN)/clang --version | head -1) ==="
$(MAKE) -f Makefile.cbm build/diag/test-runner \
CC=$(DIAG_LLVM_BIN)/clang CXX=$(DIAG_LLVM_BIN)/clang++ BUILD_DIR=build/diag \
SANITIZE="-fsanitize=address,undefined -fno-omit-frame-pointer -fno-optimize-sibling-calls"
ASAN_OPTIONS="detect_stack_use_after_return=1:strict_string_checks=1:detect_stack_use_after_scope=1" \
./build/diag/test-runner
# ── macOS leak detection ─────────────────────────────────────────
# LeakSanitizer is ON by default under ASan on Linux, so the Linux test leg
# has had leak coverage all along and the Linux-only leaks this branch fixed
# were found there. On macOS it is OFF by default and Apple's clang refuses
# detect_leaks=1 outright ("Exiting: detect_leaks is not supported on this
# platform"), which is why this repo had NO macOS leak coverage — a whole
# platform's worth of the sanitizer matrix missing.
#
# Apple's refusal is not a darwin limitation. Upstream clang supports LSan on
# darwin/arm64: this lane runs the full suite clean and was verified to still
# catch a deliberately leaked allocation. So the lane is simply "the normal
# ASan test build, compiled by Homebrew LLVM instead of Apple clang, run with
# leak detection turned on".
LSAN_LLVM_BIN ?= $(shell brew --prefix llvm 2>/dev/null)/bin
LSAN_SUITES ?=
test-lsan:
@echo "=== macOS leak lane: $(shell $(LSAN_LLVM_BIN)/clang --version | head -1) ==="
$(MAKE) -f Makefile.cbm build/lsan/test-runner \
CC=$(LSAN_LLVM_BIN)/clang CXX=$(LSAN_LLVM_BIN)/clang++ BUILD_DIR=build/lsan
cd $(CURDIR) && ASAN_OPTIONS="detect_leaks=1:halt_on_error=1" \
./build/lsan/test-runner $(LSAN_SUITES)
# Path-sensitive memory analysis: leak paths, null derefs, uninitialized reads.
# Non-gating: triage findings against the code (this lane's first run produced
# 9 real fixes and 6 recorded false positives — see the analyzer batch commit).
LINT_MEM_CHECKS = -*,clang-analyzer-unix.Malloc,clang-analyzer-unix.MallocSizeof,clang-analyzer-core.NullDereference,clang-analyzer-core.CallAndMessage,clang-analyzer-core.UndefinedBinaryOperatorResult,clang-analyzer-core.uninitialized.*,clang-analyzer-cplusplus.NewDelete,clang-analyzer-cplusplus.NewDeleteLeaks
lint-mem:
@echo "=== clang-analyzer memory lane (non-gating; triage, don't suppress) ==="
@printf '%s\n' $(LINT_SRCS) | xargs -P 10 -I{} $(CLANG_TIDY) --quiet \
--checks='$(LINT_MEM_CHECKS)' {} -- $(CFLAGS_COMMON) $(SYSROOT_FLAG) 2>/dev/null; true
# Gating variant: identical run, but vendored-tree diagnostics are excluded
# (analyzer diagnostics bypass --header-filter by design; this mirrors the
# .cppcheck vendored suppression) and any finding the gate cannot account for
# fails the target. A finding here is a leak path, null deref, or uninitialized
# read the analyzer can prove — triage it against the code.
#
# A finding is accounted for by being FIXED, or — only where the analyzer is
# genuinely wrong — by an argued entry in scripts/lint-mem-whitelist.txt. Each
# entry is pinned to the sha256 of the function it argues about, so editing
# that function invalidates it and the finding must be argued again against
# the new code. A suppression is never allowed to outlive its reasoning, and
# NOLINT is never honoured here.
lint-mem-ci:
@echo "=== clang-analyzer memory gate ==="
@printf '%s\n' $(LINT_SRCS) | xargs -P 4 -I{} $(CLANG_TIDY) --quiet \
--checks='$(LINT_MEM_CHECKS)' {} -- $(CFLAGS_COMMON) $(SYSROOT_FLAG) 2>/dev/null \
| python3 scripts/lint-mem-gate.py
# ── Security audit (6 layers) ────────────────────────────────────
# Run all security checks: static audit, binary strings, UI, install, network
# Requires: production binary already built (make cbm)
security: cbm
@echo "=== Running security audit suite ==="
scripts/security-audit.sh
scripts/security-strings.sh $(BUILD_DIR)/codebase-memory-mcp
scripts/security-ui.sh
scripts/security-install.sh $(BUILD_DIR)/codebase-memory-mcp
scripts/security-network.sh $(BUILD_DIR)/codebase-memory-mcp
scripts/security-fuzz.sh $(BUILD_DIR)/codebase-memory-mcp
scripts/security-vendored.sh
@echo "=== All security checks passed ==="