-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathMakefile
More file actions
232 lines (188 loc) · 8.6 KB
/
Copy pathMakefile
File metadata and controls
232 lines (188 loc) · 8.6 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
# Author: Hamish M. Blair <hmblair@stanford.edu>
# Conditional so CC from the environment wins over the system cc.
CC ?= cc
OPT ?= -O2
# One directory per variant, none of them inside another, so that an object
# from an ordinary build cannot link into a binary that checks nothing and each
# survives the others being built.
BUILD_ROOT := build
VARIANT := release
# A sanitizer is chosen by name, which stands for a variant of its own and the
# flags that fill it: `make SAN=asan` builds one and `make check SAN=asan` runs
# the tests over it. Thread and address cannot be combined, and neither finds
# leaks on macOS, which has no LeakSanitizer.
SANITIZE_asan := address,undefined
SANITIZE_tsan := thread
# The variable each runtime reads its options from, named so that a target
# needing one does not have to know which sanitizer is in use.
SANOPTS_asan := ASAN_OPTIONS
SANOPTS_tsan := TSAN_OPTIONS
ifdef SAN
ifeq ($(SANITIZE_$(SAN)),)
$(error unknown sanitizer: $(SAN))
endif
VARIANT := $(SAN)
OPT := -O1
endif
BUILD := $(BUILD_ROOT)/$(VARIANT)
# Clean as it stands. Left out: -Wconversion and -Wsign-conversion (three
# sites, two of them htslib's KSEQ_INIT expansion in fasta.c) and
# -Wswitch-enum (one switch in cli.c relying on its default).
WARNINGS := -Wall -Wextra -Wpedantic -Wshadow -Wmissing-prototypes \
-Wstrict-prototypes -Wpointer-arith -Wwrite-strings -Wundef \
-Wvla -Wdouble-promotion -Wcast-qual -Wformat=2 \
-Wredundant-decls -Wnull-dereference
# pkg-config where it works, plain -l otherwise; environment modules often
# ship no .pc file. Overridable on the command line.
# -isystem rather than -I, so the warnings and clang-tidy apply here and not to
# htslib and HDF5.
HTS_CFLAGS := $(subst -I,-isystem ,$(shell pkg-config --cflags htslib 2>/dev/null))
HTS_LIBS := $(shell pkg-config --libs htslib 2>/dev/null || echo -lhts)
HDF5_CFLAGS := $(subst -I,-isystem ,$(shell pkg-config --cflags hdf5 2>/dev/null))
HDF5_LIBS := $(shell pkg-config --libs hdf5 2>/dev/null || echo -lhdf5)
ZLIB_LIBS := -lz
# Static archives under one prefix in place of the pkg-config discovery, so the
# binary carries htslib, HDF5 and the compression libraries. The prefix must
# hold libhts, libdeflate, liblzma, libz, libbz2 and libhdf5, each built
# static. A musl toolchain takes the whole link static; glibc and macOS keep
# their system libraries dynamic.
ifdef STATIC_PREFIX
MACHINE := $(shell $(CC) -dumpmachine)
HTS_CFLAGS := -isystem $(STATIC_PREFIX)/include
HDF5_CFLAGS := -isystem $(STATIC_PREFIX)/include
HTS_LIBS := $(STATIC_PREFIX)/lib/libhts.a $(STATIC_PREFIX)/lib/libdeflate.a \
$(STATIC_PREFIX)/lib/liblzma.a $(STATIC_PREFIX)/lib/libz.a \
$(STATIC_PREFIX)/lib/libbz2.a
HDF5_LIBS := $(STATIC_PREFIX)/lib/libhdf5.a $(STATIC_PREFIX)/lib/libz.a
ZLIB_LIBS := $(STATIC_PREFIX)/lib/libz.a
ifneq (,$(findstring musl,$(MACHINE)))
HTS_LIBS := -static $(HTS_LIBS)
else ifneq (,$(findstring linux,$(MACHINE)))
# libhdf5.a reaches filter plugins through dlopen, which glibc keeps in libdl.
HDF5_LIBS += -ldl
endif
endif
# Where there is no .pc file the headers arrive through CPATH instead, which
# the compiler treats as -I and so warns about. Restated as -isystem, which
# wins for a directory named both ways.
CPATH_CFLAGS := $(addprefix -isystem ,$(subst :, ,$(CPATH)))
CFLAGS := -std=c11 -D_POSIX_C_SOURCE=200809L $(OPT) $(WARNINGS) -pthread -Iinclude $(HTS_CFLAGS) $(HDF5_CFLAGS) $(CPATH_CFLAGS) -MMD -MP
# A sanitizer has to reach both the compiler and the linker; what it adds to
# the link is attached to the binary's libraries, below. Halting on the first
# diagnostic is what makes one visible under the tests, which keep the output of
# a program that exits cleanly; undefined behavior is otherwise reported and
# stepped over.
ifdef SAN
SANFLAGS := -fsanitize=$(SANITIZE_$(SAN)) -fno-sanitize-recover=all \
-fno-omit-frame-pointer -g
CFLAGS += $(SANFLAGS)
endif
# Defaults under the home directory so that installing needs no privileges and
# works on machines where /usr/local is not writable. DESTDIR is prepended only
# at install time, so that a package can be staged somewhere other than where
# it will finally live.
PREFIX ?= $(HOME)/.local
BINDIR ?= $(PREFIX)/bin
INSTALL ?= install
NAME := cmuts
# One directory under apps/ per subcommand, plus one for the dispatcher, all
# linked into the one binary. A subcommand is its own sources, its own private
# headers, and whichever members of the library it refers to; adding one means
# adding a directory, a word here, and an entry point in the dispatcher.
SUBCOMMANDS := hmm gen sub div norm score
APPS := $(NAME) $(SUBCOMMANDS)
# Programs that are scripts rather than sources. One directory each, holding a
# .in that the version is substituted into, so that no copy of the version is
# kept anywhere but the header.
SCRIPTS := cmuts-align
VERSION := $(shell sed -n 's/.*CMUTS_VERSION "\(.*\)".*/\1/p' include/version.h)
LIB := $(BUILD)/lib$(NAME).a
# Sources under src/ are the library and nothing else: no entry point lives
# there, so the binary links the whole of it.
LIB_SRC := $(wildcard src/*.c)
LIB_OBJ := $(LIB_SRC:src/%.c=$(BUILD)/src/%.o)
app_sources = $(wildcard apps/$(1)/*.c)
app_objects = $(patsubst apps/$(1)/%.c,$(BUILD)/apps/$(1)/%.o,$(call app_sources,$(1)))
APP_SRC := $(foreach p,$(APPS),$(call app_sources,$(p)))
APP_OBJ := $(foreach p,$(APPS),$(call app_objects,$(p)))
BINS := $(addprefix $(BUILD)/,$(NAME) $(SCRIPTS))
SRC := $(LIB_SRC) $(APP_SRC)
DEP := $(LIB_OBJ:.o=.d) $(APP_OBJ:.o=.d)
# Additional libraries beyond libcmuts
LIBS := $(HTS_LIBS) $(HDF5_LIBS) $(ZLIB_LIBS) -pthread -lm
ifdef SAN
LIBS += $(SANFLAGS)
endif
all: $(BINS)
# Objects first and the archive last, which is the order a linker resolves in.
$(BUILD)/$(NAME): $(APP_OBJ) $(LIB)
$(CC) $^ -o $@ $(LIBS)
define script_rule
$(BUILD)/$(1): apps/$(1)/$(1).in include/version.h
@mkdir -p $$(@D)
sed 's/@VERSION@/$$(VERSION)/' $$< > $$@
@chmod +x $$@
endef
$(foreach s,$(SCRIPTS),$(eval $(call script_rule,$(s))))
$(LIB): $(LIB_OBJ)
$(AR) rcs $@ $^
# An app's own directory is on its include path, so a header beside its
# source is private to it and one in include/ is shared.
$(BUILD)/apps/%.o: apps/%.c
@mkdir -p $(@D)
$(CC) $(CFLAGS) -I$(<D) -c $< -o $@
$(BUILD)/src/%.o: src/%.c
@mkdir -p $(@D)
$(CC) $(CFLAGS) -c $< -o $@
install: $(BINS)
$(INSTALL) -d $(DESTDIR)$(BINDIR)
$(INSTALL) -m 755 $^ $(DESTDIR)$(BINDIR)
uninstall:
rm -f $(addprefix $(DESTDIR)$(BINDIR)/,$(NAME) $(SCRIPTS))
# Defaults to a virtual environment in the current dir
PYTHON ?= .venv/bin/python
# The tests capture the stderr of every program they run, and a failure reports
# only the exit status. log_path sends each report to a file of its own under
# the variant's build directory instead, which make clean removes along with
# the rest of it. The caller's own options can override this.
ifdef SAN
LOGDIR := $(BUILD)/logs
SANENV := $(SANOPTS_$(SAN))="log_path=$(CURDIR)/$(LOGDIR)/$(SAN) $$$(SANOPTS_$(SAN))"
endif
# The build directory goes first on PATH. The tests refuse anything found
# outside this repository, so they cannot be run without it.
check: $(BINS)
@$(if $(SAN),mkdir -p $(LOGDIR))
PATH=$(CURDIR)/$(BUILD):$$PATH $(SANENV) $(PYTHON) -m pytest
# Ensure the auto-generated regions between markers are current
docs: $(BINS)
@$(PYTHON) scripts/document.py $(BUILD) docs
# -W fails on a broken link or a page missing from the toctree. The doctree
# cache goes under the build directory, leaving site/ holding the published
# tree and nothing besides.
site: docs
@$(PYTHON) -m sphinx -W -q -b html -d $(BUILD_ROOT)/doctrees docs site
# Serves the rendered site locally, rebuilding it as the sources change
serve: site
@$(PYTHON) -m sphinx_autobuild docs site
# A clang that is not the system one needs the SDK spelled out.
CLANG_TIDY ?= $(firstword $(wildcard /opt/homebrew/opt/llvm/bin/clang-tidy) clang-tidy)
SDK := $(if $(filter Darwin,$(shell uname)),-isysroot $(shell xcrun --show-sdk-path),)
DB_FLAGS := $(CFLAGS) $(SDK)
compile_commands.json: Makefile $(SRC)
@{ printf '[\n'; \
first=1; \
for f in $(SRC); do \
[ $$first -eq 1 ] || printf ',\n'; \
first=0; \
printf ' { "directory": "%s", "file": "%s", "command": "%s" }' \
"$(CURDIR)" "$$f" "$(CC) $(DB_FLAGS) -I$$(dirname $$f) -c $$f"; \
done; \
printf '\n]\n'; } > $@
lint: compile_commands.json
@$(CLANG_TIDY) -p . --quiet $(SRC)
# Clears every variant
clean:
rm -rf $(BUILD_ROOT) compile_commands.json
.PHONY: all check clean docs install lint serve site uninstall
-include $(DEP)