NPROC != nproc
MAKEFLAGS += -j$(NPROC)
MAKEFLAGS += -r -R
DEFAULT_OPTLEVEL := g
DEFAULT_PREFIX := /usr/local
DEFAULT_TYPE := default
DEFAULT_USE_CCACHE := y
DEFAULT_EMIT_LLVM := n
DEFAULT_BUILD_FROM_ASM := n
.PHONY: $(shell grep -ho "^[0-9a-z-]\\+:" $(MAKEFILE_LIST) | sed -e "s/://")
define ERROR_INVALID_VALUE
$(error invalid value for $$($1): expected one of $(or $2,[yn]) but got $($1))
endef
CLANG21 != command -v clang-21
CLANG20 != command -v clang-20
CLANG19 != command -v clang-19
CLANG != command -v clang
CC := $(or $(CLANG21),$(CLANG20),$(CLANG19),$(CLANG),$(error CC not found: install clang (>=19)))
USE_CCACHE ?= $(DEFAULT_USE_CCACHE) ## use ccache [yn] (default: y)
ifeq ($(strip $(USE_CCACHE)),y)
CCACHE != command -v ccache
CC := $(CCACHE) $(CC)
else ifeq ($(strip $(USE_CCACHE)),n)
else
$(call ERROR_INVALID_VALUE,USE_CCACHE)
endif
PROJECT_NAME := $(notdir $(CURDIR))
CDIR := src
INCDIR := include
BUILDDIR := .build
PREFIX ?= $(DEFAULT_PREFIX) ## install prefix (default: /usr/local)
OPTLEVEL ?= $(or $(OL), $(DEFAULT_OPTLEVEL)) ## optimization level [0-3|g] (default: g)
LOGLEVEL ?= $(or $(LL), $(DEFAULT_LOGLEVEL))
TYPE ?= $(or $(T), $(DEFAULT_TYPE)) ## build type [test|bench|default]
# compiler flags
CFLAGS != cat build/cflags
OPTFLAGS != cat build/optflags
# linker flags
LDFLAGS != cat build/ldflags
OPTLDFLAGS != cat build/optldflags
DEBUGFLAGS != cat build/debugflags
ASMFLAGS != cat build/asmflags
# Enables macro in the source
CFLAGS += -DLOGLEVEL=$(LOGLEVEL)
ifdef ASAN
CFLAGS += -fsanitize=$(ASAN)
LDFLAGS += -fsanitize=$(ASAN)
endif
ifeq ($(MAKECMDGOALS),coverage)
CFLAGS += -fprofile-instr-generate -fcoverage-mapping
LDFLAGS += -fprofile-instr-generate
TYPE := test
OPTLEVEL := 0
endif
ifeq ($(strip $(TYPE)),test)
CFLAGS += -DTEST_MODE
else ifeq ($(strip $(TYPE)),bench)
CFLAGS += -DBENCHMARK_MODE
else ifeq ($(strip $(TYPE)),default)
else
$(call ERROR_INVALID_VALUE,TYPE,[test|bench|default])
endif
ifeq ($(strip $(OPTLEVEL)),g)
CFLAGS += $(DEBUGFLAGS)
LDFLAGS += $(DEBUGFLAGS)
RUNNER ?= gdb ## runner (default in debug run: gdb)
else ifneq ($(filter 1 2 3,$(OPTLEVEL)),)
CFLAGS += $(OPTFLAGS) -O$(OPTLEVEL)
LDFLAGS += $(OPTLDFLAGS) -O$(OPTLEVEL)
else ifeq ($(strip $(OPTLEVEL)),0)
else
$(call ERROR_INVALID_VALUE,OPTLEVEL,[0-3|g])
endif
ifeq ($(MAKECMDGOALS),profile)
CFLAGS += -pg
LDFLAGS += -pg
LDFLAGS := $(filter-out -s,$(LDFLAGS))
endif
ifdef TEST_FILTER
CFLAGS += -DTEST_FILTER="\"$(TEST_FILTER)\""
endif
# generate output path
GITBRANCH != git branch --show-current 2>/dev/null
SEED = $(CC)$(CFLAGS)$(LDFLAGS)$(GITBRANCH)$(IN_NIX_SHELL)
HASH != echo '$(SEED)' | md5sum | cut -d' ' -f1
OUTDIR := $(BUILDDIR)/$(HASH)
TARGET := $(OUTDIR)/$(PROJECT_NAME)
EMIT_LLVM ?= $(DEFAULT_EMIT_LLVM) ## use llvmIR instead of asm [yn] (default: n)
ifeq ($(strip $(EMIT_LLVM)),y)
ASMFLAGS += -emit-llvm
ASMEXT := ll
else ifeq ($(strip $(EMIT_LLVM)),n)
ASMEXT := s
else
$(call ERROR_INVALID_VALUE,EMIT_LLVM)
endif
# source files
SRCS := $(wildcard $(CDIR)/*.c)
OBJS := $(patsubst $(CDIR)/%.c,$(OUTDIR)/%.o,$(SRCS))
DEPS := $(OBJS:.o=.d)
ASMS := $(OBJS:.o=.$(ASMEXT))
# e.g.)
# $ make asm OL=3
# $ # edit asm files...
# $ make BUILD_FROM_ASM=y OL=3
BUILD_FROM_ASM ?= $(DEFAULT_BUILD_FROM_ASM) ## use asm instead of c files [yn] (default: n)
ifeq ($(strip $(BUILD_FROM_ASM)),y)
SRCDIR := $(OUTDIR)
SRCEXT := $(ASMEXT)
CFLAGS =
else ifeq ($(strip $(BUILD_FROM_ASM)),n)
SRCDIR := $(CDIR)
SRCEXT := c
else
$(call ERROR_INVALID_VALUE,BUILD_FROM_ASM)
endif
include $(realpath $(DEPS))
### build rules
.DEFAULT_GOAL := $(TARGET)
# link
$(TARGET): $(OBJS)
$(CC) $(LDFLAGS) $^ -o $@
# compile
$(OBJS): $(OUTDIR)/%.o: $(SRCDIR)/%.$(SRCEXT) $(OUTDIR)/%.d | $(OUTDIR)/
$(CC) $< $(CFLAGS) -c -o $@
define DEPFLAGS
-MM -MP -MT $1.o -MF $1.d
endef
$(DEPS): $(OUTDIR)/%.d: $(CDIR)/%.c | $(OUTDIR)/
$(CC) $< $(CFLAGS) $(call DEPFLAGS,$(@:.d=))
# e.g.) run with valgrind
# make run RUNNER=valgrind
# e.g.) don't use gdb (default debug RUNNER) in debug run
# make run RUNNER=
run: $(TARGET) ## run target
$(RUNNER) $<
# `make run-foo` is same as `make run RUNNER=foo`
run-%: $(TARGET)
$* $<
test: ; $(MAKE) run TYPE=test RUNNER= ## run test
asm: $(ASMS) ## generate asm files
$(ASMS): $(OUTDIR)/%.$(ASMEXT): $(CDIR)/%.c | $(OUTDIR)/
$(CC) $< $(ASMFLAGS) $(CFLAGS) -o $@
clean-all: ; rm -rf $(BUILDDIR)
# e.g.) remove test build for opt level 3
# make clean OPTLEVEL=3 TYPE=test
clean:
ifneq ($(realpath $(OUTDIR)),)
rm -rf $(OUTDIR)
endif
$(PREFIX)/bin/$(PROJECT_NAME): $(TARGET) | $(PREFIX)/bin/
cp $< $@
install: $(PREFIX)/bin/$(PROJECT_NAME)
uninstall: ; rm $(PREFIX)/bin/$(PROJECT_NAME)
doc: doc/Doxyfile ## generate doc
doxygen $<
doc/Doxyfile: doc/
doxygen -g $@
fmt: ; clang-format -Werror --dry-run $(SRCS) $(INCDIR)/*.h
lint: $(SRCS)
clang-tidy $^ -- $(CFLAGS)
flawfinder $^
check: $(SRCS)
cppcheck $^ --enable=all --suppress=missingIncludeSystem -I$(INCDIR)
pre-commit: fmt test ## .git/hooks/pre-commit
FP ?= /dev/stdout
log: ## show build flags
@echo "Compiler: $(CC)" > $(FP)
@echo "CFLAGS: $(CFLAGS)" >> $(FP)
@echo "LDFLAGS: $(LDFLAGS)" >> $(FP)
@echo "TARGET: $(TARGET)" >> $(FP)
@echo "SRCS: $(SRCS)" >> $(FP)
@echo "OBJS: $(OBJS)" >> $(FP)
@echo "DEPS: $(DEPS)" >> $(FP)
info: $(TARGET) ## show target info
@echo "target file size:"
@size $(TARGET)
help: ## show help
@echo "$$ make # debug build"
@echo "$$ make test # run test"
@echo "$$ make run OL=3 # run release build"
@echo
@echo "build files: .build/HASH/*"
@echo
@echo "Variables:"
@grep -h "^[^\t].* ## " $(MAKEFILE_LIST) \
| sed -En "s/^ *([0-9A-Z_]+) .?= .*## (.*)$$/\\1 = \\2/p"
@echo
@echo "Targets:"
@grep -h "^[^\t].* ## " $(MAKEFILE_LIST) \
| sed -En "s/^([0-9a-z-]+): .*## (.*)$$/\\1: \\2/p"
### llmfile
LLMFILE ?= llmfile.txt
FILES ?= makefile build.zig doc/t.typ doc/readme.typ doc/manual.typ
DIRS ?= build include src
FILES_IN_DIRS := $(wildcard $(addsuffix /*, $(DIRS)))
SORTED_FILES_IN_DIRS := $(sort $(notdir $(basename $(FILES_IN_DIRS))))
REAL_PATH_FILES_IN_DIRS := $(foreach f,$(SORTED_FILES_IN_DIRS),$(wildcard */$f.[ch]))
LIST_FILES ?= $(FILES) $(REAL_PATH_FILES_IN_DIRS)
$(LLMFILE): $(LIST_FILES)
echo $^ | sed 's/ /\n/g' > $@
echo >> $@ # newline
# `head` automatically inserts the file name at the start of the file
head -n 9999 $^ >> $@
llmfile: $(LLMFILE) ## for the llm to read
### compiledb
, = ,
LOCAL_CCJ = $(OUTDIR)/compile_commands.json
define STRINGIFY
$(foreach f,$1,"$f"$,)
endef
$(LOCAL_CCJ).%: | $(dir $(LOCAL_CCJ))
echo '{"arguments":[' > $@
echo '"$(lastword $(CC))",' >> $@
echo '$(call STRINGIFY,$(CFLAGS))' >> $@
echo '$(call STRINGIFY,$(call COMPILEFLAGS,$(OUTDIR)/$*.o))' >> $@
echo '"$(SRCDIR)/$*.c"],' >> $@
echo '"directory":"$(CURDIR)",' >> $@
echo '"file":"$(CURDIR)/$(SRCDIR)/$*.c",' >> $@
echo '"output":"$(CURDIR)/$(OUTDIR)/$*.o"},' >> $@
$(LOCAL_CCJ): $(addprefix $(LOCAL_CCJ)., $(basename $(notdir $(SRCS))))
echo "[" > $@
cat $^ >> $@
sed -i "\$$s/,$$/]/" $@
compile_commands.json: $(LOCAL_CCJ)
cp $< $@
compiledb: compile_commands.json ## for lsp
### coverage
COVDIR ?= $(OUTDIR)/coverage-report
PROFRAW := $(OUTDIR)/default.profraw
PROFDATA := $(OUTDIR)/coverage.profdata
$(PROFRAW): $(TARGET)
LLVM_PROFILE_FILE=$@ ./$<
$(PROFDATA): $(PROFRAW)
llvm-profdata merge --sparse $< -o $@
$(COVDIR): $(PROFDATA)
llvm-cov show $(TARGET) --format=html --instr-profile=$< --output-dir=$@
BROWSER ?= cha
coverage: $(COVDIR) ## report test coverage
$(BROWSER) $</index.html
### perf
$(OUTDIR)/perf.data: $(TARGET)
perf record -o $@ -g ./$<
perf: $(OUTDIR)/perf.data ## run perf analysis
perf report -i $<
### profile
gmon.out: $(TARGET) ; ./$<
$(OUTDIR)/gmon.out: gmon.out | $(OUTDIR)/
mv $< $@
$(OUTDIR)/profile.txt: $(OUTDIR)/gmon.out
gprof $(TARGET) $< > $@
profile: $(OUTDIR)/profile.txt
less $<
### last resort
%/: ; mkdir -p $@