CC ?= gcc PREFIX ?= /usr/local VERSION := $(or $(shell tr -d '[:space:]' < VERSION 2>/dev/null), 0.0.0) SOVERSION := $(word 1,$(subst ., ,$(VERSION))) PYTHON3 := $(shell command -v python3 2>/dev/null) ifeq ($(PYTHON3),) $(error python3 is required but not found) endif # Base flags CFLAGS_BASE = -std=c11 -Wall -Wextra -Wshadow -Wdouble-promotion -Wnull-dereference -Wimplicit-fallthrough -Wmisleading-indentation -Wstrict-aliasing=2 -pedantic -O2 -I. -Ilib -MMD -MP # Platform-specific UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Linux) CFLAGS_BASE += -D_DEFAULT_SOURCE else ifeq ($(UNAME_S),Darwin) CFLAGS_BASE += -D_DARWIN_C_SOURCE else # BSD and others CFLAGS_BASE += -D_DEFAULT_SOURCE endif # Security hardening (optimized builds only) CFLAGS_HARDENING = -D_FORTIFY_SOURCE=2 -fstack-protector-strong -Wformat-security -fno-common CFLAGS := $(CFLAGS_BASE) $(CFLAGS_HARDENING) LDFLAGS ?= LDLIBS = -lm SRCDIR = lib BUILDDIR = build TESTDIR = test SCRIPTDIR = scripts TEST_BUILDDIR = $(BUILDDIR)/test SRCS = $(filter-out $(SRCDIR)/ts.c $(SRCDIR)/ts_flat_generated.c $(SRCDIR)/ts_registry_generated.c $(SRCDIR)/test.c $(wildcard $(SRCDIR)/ts_*.c),$(wildcard $(SRCDIR)/*.c)) OBJS = $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SRCS)) DEPS = $(OBJS:.o=.d) $(BUILDDIR)/ts.d TS_OBJ = $(BUILDDIR)/ts.o SHARED_OBJS = $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.so.o,$(SRCS)) SHARED_TS_OBJ = $(BUILDDIR)/ts.so.o FLAT_OBJ = $(BUILDDIR)/ts_flat_generated.so.o REG_OBJ = $(BUILDDIR)/ts_registry_generated.so.o TEST_SRCS = $(TESTDIR)/main.c $(filter-out $(TESTDIR)/main.c $(TESTDIR)/main_generated.c $(TESTDIR)/gen_test_data.c $(TESTDIR)/bench.c $(TESTDIR)/fuzz_table_generated.c,$(wildcard $(TESTDIR)/*.c)) TEST_OBJS = $(patsubst $(TESTDIR)/%.c,$(BUILDDIR)/test/%.o,$(TEST_SRCS)) MAIN = $(BUILDDIR)/ts TEST_BIN = $(BUILDDIR)/run_tests BENCH_BIN = $(BUILDDIR)/bench LIB = $(BUILDDIR)/libts.a SHLIB = $(BUILDDIR)/libts.so .PHONY: all test test-one test-python check valgrind clean sanitize sanitize-test lib shared install uninstall distclean dist bench gen-bindings all: $(MAIN) lib: $(LIB) shared: $(SHLIB) gen-bindings $(LIB): $(OBJS) $(TS_OBJ) | $(BUILDDIR) ar rcs $@ $^ ifeq ($(UNAME_S),Linux) SHLIB_SONAME = -Wl,-soname,libts.so.$(SOVERSION) else ifeq ($(UNAME_S),Darwin) SHLIB_SONAME = -Wl,-install_name,@rpath/libts.so.$(SOVERSION) else SHLIB_SONAME = -Wl,-soname,libts.so.$(SOVERSION) endif $(SHLIB): $(SHARED_OBJS) $(SHARED_TS_OBJ) $(FLAT_OBJ) $(REG_OBJ) | $(BUILDDIR) $(CC) $(LDFLAGS) -shared $(SHLIB_SONAME) -Wl,-rpath,'$$ORIGIN' -o $@ $^ $(LDLIBS) $(BUILDDIR): @mkdir -p $@ $(BUILDDIR)/%.o: $(SRCDIR)/%.c ts.h ts_generated.h | $(BUILDDIR) $(CC) $(CFLAGS) -c $< -o $@ $(BUILDDIR)/%.so.o: $(SRCDIR)/%.c ts.h ts_generated.h | $(BUILDDIR) $(CC) $(CFLAGS) -fPIC -c $< -o $@ $(BUILDDIR)/ts.o: ts.c | $(BUILDDIR) $(CC) $(CFLAGS) -c ts.c -o $@ $(BUILDDIR)/ts.so.o: ts.c | $(BUILDDIR) $(CC) $(CFLAGS) -fPIC -c ts.c -o $@ ts_generated.h: $(SCRIPTDIR)/gen_all.py $(filter-out $(SRCDIR)/ts_flat_generated.c $(SRCDIR)/ts_registry_generated.c,$(wildcard $(SRCDIR)/*.c)) $(PYTHON3) $< || (echo "ERROR: gen_all.py failed" && exit 1) test/main_generated.c: ts_generated.h $(PYTHON3) $(SCRIPTDIR)/gen_all.py || (echo "ERROR: gen_all.py failed" && exit 1) $(SRCDIR)/ts_flat_generated.c: $(SCRIPTDIR)/gen_flat.py $(filter-out $(SRCDIR)/ts_flat_generated.c $(SRCDIR)/ts_registry_generated.c,$(wildcard $(SRCDIR)/*.c)) $(PYTHON3) $< || (echo "ERROR: gen_flat.py failed" && exit 1) $(FLAT_OBJ): $(SRCDIR)/ts_flat_generated.c ts.h | $(BUILDDIR) $(CC) $(CFLAGS) -fPIC -c $< -o $@ $(SRCDIR)/ts_registry_generated.c: $(SCRIPTDIR)/gen_registry.py $(filter-out $(SRCDIR)/ts_registry_generated.c $(SRCDIR)/ts_flat_generated.c,$(wildcard $(SRCDIR)/*.c)) $(PYTHON3) $< || (echo "ERROR: gen_registry.py failed" && exit 1) $(REG_OBJ): $(SRCDIR)/ts_registry_generated.c ts.h | $(BUILDDIR) $(CC) $(CFLAGS) -fPIC -c $< -o $@ $(TESTDIR)/fuzz_table_generated.c: $(SCRIPTDIR)/gen_fuzz.py $(filter-out $(SRCDIR)/ts_flat_generated.c $(SRCDIR)/ts_registry_generated.c,$(wildcard $(SRCDIR)/*.c)) $(PYTHON3) $< || (echo "ERROR: gen_fuzz.py failed" && exit 1) $(BUILDDIR)/test/fuzz.o: $(TESTDIR)/fuzz.c $(TESTDIR)/fuzz_table_generated.c ts.h | $(TEST_BUILDDIR) $(CC) $(CFLAGS) -DTS_IMPLEMENTATION -c $< -o $@ $(TEST_BUILDDIR): @mkdir -p $@ $(BUILDDIR)/test/%.o: $(TESTDIR)/%.c ts.h | $(TEST_BUILDDIR) $(CC) $(CFLAGS) -DTS_IMPLEMENTATION -Wno-strict-aliasing -c $< -o $@ $(MAIN): $(OBJS) $(TS_OBJ) main.c | $(BUILDDIR) $(CC) $(CFLAGS) main.c $(OBJS) $(TS_OBJ) $(LDFLAGS) -o $@ $(LDLIBS) $(TEST_BIN): $(OBJS) $(TEST_OBJS) $(TS_OBJ) | $(BUILDDIR) $(CC) $(CFLAGS) $(TEST_OBJS) $(OBJS) $(TS_OBJ) $(LDFLAGS) -o $@ $(LDLIBS) test: $(TEST_BIN) ./$(TEST_BIN) $(BENCH_BIN): $(OBJS) $(TS_OBJ) test/bench.c | $(BUILDDIR) $(CC) $(CFLAGS) test/bench.c $(OBJS) $(TS_OBJ) $(LDFLAGS) -o $@ $(LDLIBS) bench: $(BENCH_BIN) ./$(BENCH_BIN) python/ts/__init__.py: $(SCRIPTDIR)/gen_bindings.py $(SCRIPTDIR)/ts_parser.py $(filter-out $(SRCDIR)/ts_flat_generated.c $(SRCDIR)/ts_registry_generated.c,$(wildcard $(SRCDIR)/*.c)) $(PYTHON3) $(SCRIPTDIR)/gen_bindings.py || (echo "ERROR: gen_bindings.py failed" && exit 1) gen-bindings: python/ts/__init__.py test-python: shared gen-bindings LD_LIBRARY_PATH=$(BUILDDIR):$$LD_LIBRARY_PATH $(PYTHON3) $(TESTDIR)/test_python.py check: $(MAKE) clean $(MAKE) test CFLAGS="$(CFLAGS_BASE) $(CFLAGS_HARDENING) -Werror -Wno-error=strict-aliasing" test-one: $(OBJS) $(TS_OBJ) ifndef TEST $(error TEST is not set. Usage: make test-one TEST=) endif @echo "$(TEST)" | grep -qE '^[a-z0-9_]+$$' || { echo "ERROR: TEST must match [a-z0-9_]+"; exit 1; } @mkdir -p $(BUILDDIR) @printf '#include \nextern bool ts_test_$(TEST)(void);\nint main(void){return ts_test_$(TEST)() ? 0 : 1;}\n' > $(BUILDDIR)/stub.c $(CC) $(CFLAGS) -DTS_IMPLEMENTATION $(BUILDDIR)/stub.c $(TESTDIR)/$(TEST).c $(OBJS) $(TS_OBJ) $(LDFLAGS) -o $(BUILDDIR)/test_$(TEST) $(LDLIBS) @RET=0; ./$(BUILDDIR)/test_$(TEST) || RET=$$?; rm -f $(BUILDDIR)/stub.c $(BUILDDIR)/test_$(TEST); exit $$RET valgrind: $(TEST_BIN) valgrind --leak-check=full --error-exitcode=1 ./$(TEST_BIN) sanitize: $(MAKE) clean $(MAKE) CFLAGS="$(CFLAGS_BASE) -O0 -g -fsanitize=address,undefined -fno-common" sanitize-test: @$(CC) -print-file-name=libasan.so || { echo "ERROR: AddressSanitizer (libasan) not found. Install libasan."; exit 1; } $(MAKE) clean $(MAKE) test CFLAGS="$(CFLAGS_BASE) -O0 -g -fsanitize=address,undefined -fno-common" install: ts_generated.h lib shared install -d $(PREFIX)/include/ts install -m 644 ts.h $(PREFIX)/include/ts/ install -m 644 ts_generated.h $(PREFIX)/include/ts/ install -d $(PREFIX)/lib install -m 644 $(LIB) $(PREFIX)/lib/ install -m 755 $(SHLIB) $(PREFIX)/lib/libts.so.$(VERSION) ln -sf libts.so.$(VERSION) $(PREFIX)/lib/libts.so.$(SOVERSION) ln -sf libts.so.$(SOVERSION) $(PREFIX)/lib/libts.so @if [ "$(shell id -u)" = "0" ]; then ldconfig 2>/dev/null || true; else echo "WARNING: run 'sudo ldconfig' to update linker cache"; fi install -d $(PREFIX)/lib/pkgconfig @test -f ts.pc.in || { echo "ERROR: ts.pc.in not found" && exit 1; } sed 's|@PREFIX@|$(PREFIX)|;s|@VERSION@|$(VERSION)|' ts.pc.in > $(PREFIX)/lib/pkgconfig/ts.pc uninstall: rm -f $(PREFIX)/include/ts/ts.h rm -f $(PREFIX)/include/ts/ts_generated.h rm -rf $(PREFIX)/include/ts rm -f $(PREFIX)/lib/libts.a rm -f $(PREFIX)/lib/libts.so $(PREFIX)/lib/libts.so.$(SOVERSION) $(PREFIX)/lib/libts.so.$(VERSION) rm -f $(PREFIX)/lib/pkgconfig/ts.pc @if [ "$(shell id -u)" = "0" ]; then ldconfig 2>/dev/null || true; fi clean: rm -rf $(BUILDDIR) distclean: clean rm -f $(SRCDIR)/ts_flat_generated.c rm -f $(SRCDIR)/ts_registry_generated.c rm -f $(TESTDIR)/fuzz_table_generated.c rm -f $(TESTDIR)/main_generated.c rm -f ts_generated.h rm -f python/ts/__init__.py DIST_NAME = ts-$(VERSION) DIST_FILES = Makefile VERSION ts.pc.in LICENSE README.md ts.h ts_generated.h ts.c main.c .gitignore DIST_LIB = $(filter-out $(SRCDIR)/ts_flat_generated.c $(SRCDIR)/ts_registry_generated.c $(SRCDIR)/test.c,$(wildcard $(SRCDIR)/*.c)) DIST_SCRIPTS = $(wildcard $(SCRIPTDIR)/*.py) DIST_TEST = $(filter-out $(TESTDIR)/fuzz_table_generated.c,$(wildcard $(TESTDIR)/*.c)) $(wildcard $(TESTDIR)/*.py) dist: $(MAKE) clean $(MAKE) lib @test -s ts_generated.h || { echo "ERROR: ts_generated.h not found or empty" && exit 1; } rm -rf $(DIST_NAME) mkdir -p $(DIST_NAME)/lib $(DIST_NAME)/test $(DIST_NAME)/scripts cp $(DIST_FILES) $(DIST_NAME)/ cp $(DIST_LIB) $(DIST_NAME)/lib/ cp $(DIST_SCRIPTS) $(DIST_NAME)/scripts/ cp $(DIST_TEST) $(DIST_NAME)/test/ tar czf $(DIST_NAME).tar.gz $(DIST_NAME) rm -rf $(DIST_NAME) @echo "Created $(DIST_NAME).tar.gz" -include $(DEPS) -include $(TEST_OBJS:.o=.d)