aboutsummaryrefslogtreecommitdiffstats
path: root/Makefile
blob: 50e2fda4664ebf52d746dd22e527b2c79948cb4d (plain) (blame)
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
# sl0ppty — see DESIGN.md
#
# zig is the C compiler: it is already required to build libghostty-vt, and it
# gives us static musl linking and cross-compilation for free (D12).

ZIG      ?= $(HOME)/zig-0.16.0/zig
CC       := $(ZIG) cc
VT       := vendor/libghostty-vt
VT_OUT   := $(VT)/zig-out
VT_LIB   := $(VT_OUT)/lib/libghostty-vt.a
VT_INC   := $(VT_OUT)/include

# -MMD -MP emits a .d per object listing the headers it included, so editing a
# header rebuilds what actually uses it. Without this a header edit rebuilt
# nothing and the only safe move was `make clean`, which is how a 0.1s build
# turns into a habit of throwing the whole thing away.
# The version is baked in, because the first question when a session is
# behaving oddly is "which build is this?" -- and a session keeps the binary it
# started with, so the answer is not "whatever is in build/ now".
VERSION  := 0.1.0
GITDESC  := $(shell git describe --always --dirty 2>/dev/null || echo unknown)

CFLAGS   := -std=c23 -O1 -g -Wall -Wextra -Wno-unused-parameter -MMD -MP \
            -I$(VT_INC) -Isrc -Ibuild
LDFLAGS  :=

SRC      := $(wildcard src/*.c)
OBJ      := $(SRC:src/%.c=build/%.o)
DEPS     := $(OBJ:.o=.d)
BIN      := build/sl0ppty

# One stamp per python test file. The stamp depends on the file, the harness and
# the binary, so an untouched test whose binary has not changed does not run
# again — and `make -j` runs the rest at once, because they are separate
# processes that share nothing (script mode opens no socket).
# test_session.py drives a real pty and belongs to test-live; it is only named
# test_* by history.
PY_TESTS  := $(filter-out test_session.py,$(notdir $(wildcard tests/test_*.py)))
PY_STAMPS := $(PY_TESTS:%.py=build/.pass-%)
JOBS      ?= $(shell nproc 2>/dev/null || echo 4)

# Quiet by default: `[CC] src/app.c` rather than a screenful of flags, which is
# what you want for the twenty seconds a build takes and never after. `make V=1`
# prints the command lines instead, which is what you want the moment one of
# them is wrong -- so the information is one flag away rather than gone.
V ?= 0
ifeq ($(V),0)
  Q   := @
  say  = @printf '  %-5s %s\n' '$(1)' '$(2)'
else
  Q   :=
  say  = @:
endif

.PHONY: all clean vendor run test retest test-live test-all smoke help coverage docs
.DEFAULT_GOAL := help

help: ## show this
	@grep -hE '^[a-z-]+:.*##' $(MAKEFILE_LIST) | sed 's/:.*##/\t/' | expand -t20
	@printf '\n%-19s %s\n' 'V=1' 'show the command lines rather than [CC] src/app.c'

all: $(BIN) ## build sl0ppty

$(BIN): $(OBJ) $(VT_LIB)
	$(call say,[LD],$@)
	$(Q)$(CC) $(OBJ) $(VT_LIB) -o $@ $(LDFLAGS)

build/%.o: src/%.c build/version.h $(VT_LIB) | build
	$(call say,[CC],$<)
	$(Q)$(CC) $(CFLAGS) -c $< -o $@

# stb_image comes with the vendored libghostty-vt, which is where the include
# path points. It is third-party and does not build clean under our warnings,
# so it gets its own rule rather than every file paying for it.
build/png.o: src/png.c build/version.h $(VT_LIB) | build
	$(call say,[CC],$<)
	$(Q)$(CC) $(CFLAGS) -I$(VT)/src/stb -Wno-unused-function -c $< -o $@

# Rewritten only when the value actually changes, so `make` after a commit
# rebuilds, and `make` twice in a row does not.
.PHONY: FORCE
build/version.h: FORCE | build
	@printf '/* generated by the Makefile */\n#define SL0PPTY_VERSION "%s+%s"\n' \
	  '$(VERSION)' '$(GITDESC)' > $@.tmp
	@cmp -s $@.tmp $@ || mv $@.tmp $@
	@rm -f $@.tmp

build:
	@mkdir -p build

$(VT_LIB):
	@$(MAKE) vendor

vendor: ## build the vendored libghostty-vt (needs zig 0.16)
	cd $(VT) && PATH="$(dir $(ZIG)):$$PATH" zig build -Demit-lib-vt -Doptimize=ReleaseFast

TEST_BIN := build/input_test

$(TEST_BIN): tests/input_test.c src/input.c $(VT_LIB) | build
	$(call say,[LD],$@)
	$(Q)$(CC) $(CFLAGS) tests/input_test.c src/input.c $(VT_LIB) -o $@

KDL_TEST := build/kdl_test

$(KDL_TEST): tests/kdl_test.c src/kdl.c | build
	$(call say,[LD],$@)
	$(Q)$(CC) $(CFLAGS) tests/kdl_test.c src/kdl.c -o $@

EXPR_TEST := build/expr_test

$(EXPR_TEST): tests/expr_test.c src/expr.c | build
	$(call say,[LD],$@)
	$(Q)$(CC) $(CFLAGS) tests/expr_test.c src/expr.c -o $@

# The real expression evaluator as a filter, so test_shadertoy.py can hold
# contrib/shadertoy.html's JavaScript to what the compiler actually does.
EXPR_EVAL := build/expr_eval

$(EXPR_EVAL): tests/expr_eval.c src/expr.c | build
	$(call say,[LD],$@)
	$(Q)$(CC) $(CFLAGS) tests/expr_eval.c src/expr.c -o $@

SHADER_TEST := build/shader_test

$(SHADER_TEST): tests/shader_test.c src/shader.c src/screen.c src/json.c src/expr.c $(VT_LIB) | build
	$(call say,[LD],$@)
	$(Q)$(CC) $(CFLAGS) tests/shader_test.c src/shader.c src/screen.c src/json.c src/expr.c $(VT_LIB) -o $@

# Shader plugins, for test_shader_plugin.py: a good one, one that announces an
# ABI we do not speak, and the example we ship in contrib -- which is built
# here so that a broken example is a failing test rather than a bug report.
PLUGIN_CFLAGS := -std=c23 -O1 -fPIC -shared -Isrc

build/testshader.so: tests/shader_plugin_test.c src/shader_abi.h | build
	$(call say,[SO],$@)
	$(Q)$(CC) $(PLUGIN_CFLAGS) $< -o $@

build/badshader.so: tests/shader_plugin_test.c src/shader_abi.h | build
	$(call say,[SO],$@)
	$(Q)$(CC) $(PLUGIN_CFLAGS) -DBAD_ABI $< -o $@

build/exampleshader.so: contrib/shader-plugin/example.c src/shader_abi.h | build
	$(call say,[SO],$@)
	$(Q)$(CC) $(PLUGIN_CFLAGS) $< -o $@

build/.pass-test_shader_plugin: build/testshader.so build/badshader.so \
                                build/exampleshader.so

build/.pass-test_shadertoy: $(EXPR_EVAL) contrib/shadertoy.html

build/.pass-%: tests/%.py tests/harness.py $(BIN) | build
	@cd tests && timeout 300 python3 $(notdir $<) > ../build/.log-$* 2>&1 \
	  || { echo "FAIL $(notdir $<)"; tail -25 ../build/.log-$*; exit 1; }
	@printf '  ok   %-24s %s\n' "$(notdir $<)" "$$(grep -c '^ok' build/.log-$* 2>/dev/null || echo ?) checks"
	@touch $@

test: $(BIN) $(TEST_BIN) $(KDL_TEST) $(SHADER_TEST) $(EXPR_TEST) $(EXPR_EVAL) ## unit + headless checks (fast)
	@./$(TEST_BIN) >/dev/null && ./$(KDL_TEST) >/dev/null && ./$(SHADER_TEST) >/dev/null \
	  && ./$(EXPR_TEST) >/dev/null \
	  && printf '  ok   %-24s %s\n' "C unit tests" "4 binaries"
	@$(MAKE) --no-print-directory -j$(JOBS) $(PY_STAMPS)
	@printf '\nall green\n'

retest: ## force every python test to run again
	@rm -f build/.pass-*
	@$(MAKE) --no-print-directory test

test-live: $(BIN) ## the checks that need a real tty (slow)
	python3 tests/live_m0.py
	@echo
	python3 tests/live_input.py
	@echo
	python3 tests/test_session.py
	@echo
	python3 tests/live_reload.py

test-all: test test-live ## everything

smoke: $(BIN) ## compose a screen headlessly and print it
	./$(BIN) --headless --cols 40 --rows 12 -- /bin/sh -c 'printf "hello \033[1;32msl0ppty\033[0m\n"; echo "wide: 日本語"'

docs: ## render docs/ into build/docs (no dependencies; open build/docs/index.html)
	$(Q)python3 contrib/gen-docs build/docs

run: $(BIN) ## build and run
	./$(BIN)

# ── coverage ────────────────────────────────────────────────────────────────
#
# A second build, with gcc instead of zig, because the counters need a gcov
# that matches the compiler and gcov is what this machine has. It lives in
# build/cov and touches nothing else; $SL0PPTY_BIN is how the same suite drives
# it. `-O0` because optimised line counts are a work of fiction.
COV_DIR  := build/cov
COV_SRC  := $(wildcard src/*.c)
COV_OBJ  := $(COV_SRC:src/%.c=$(COV_DIR)/%.o)
COV_CC   ?= gcc
COV_FLAGS := -std=c23 -O0 -g --coverage -I$(VT_INC) -Isrc -Ibuild

$(COV_DIR)/%.o: src/%.c build/version.h | $(COV_DIR)
	$(call say,[COV],$<)
	$(Q)$(COV_CC) $(COV_FLAGS) -c $< -o $@

$(COV_DIR):
	@mkdir -p $(COV_DIR)

$(COV_DIR)/sl0ppty: $(COV_OBJ) $(VT_LIB)
	$(call say,[LD],$@)
	$(Q)$(COV_CC) --coverage $(COV_OBJ) $(VT_LIB) -o $@

# The C unit tests link the *same* coverage objects, so their counters land in
# the same place and input.c/kdl.c/shader.c/expr.c are credited for the tests
# that actually exercise them. Without this they read as half-tested, which is
# a lie about where the tests are.
COV_UNITS := $(COV_DIR)/input_test $(COV_DIR)/kdl_test $(COV_DIR)/shader_test \
             $(COV_DIR)/expr_test

$(COV_DIR)/input_test: tests/input_test.c $(COV_DIR)/input.o $(VT_LIB)
	$(call say,[LD],$@)
	$(Q)$(COV_CC) $(COV_FLAGS) $^ -o $@
$(COV_DIR)/kdl_test: tests/kdl_test.c $(COV_DIR)/kdl.o
	$(call say,[LD],$@)
	$(Q)$(COV_CC) $(COV_FLAGS) $^ -o $@
$(COV_DIR)/shader_test: tests/shader_test.c $(COV_DIR)/shader.o $(COV_DIR)/screen.o \
                        $(COV_DIR)/json.o $(COV_DIR)/expr.o $(VT_LIB)
	$(call say,[LD],$@)
	$(Q)$(COV_CC) $(COV_FLAGS) $^ -o $@
$(COV_DIR)/expr_test: tests/expr_test.c $(COV_DIR)/expr.o
	$(call say,[LD],$@)
	$(Q)$(COV_CC) $(COV_FLAGS) $^ -o $@

coverage: $(COV_DIR)/sl0ppty $(COV_UNITS) ## how much of the code the tests run
	@rm -f $(COV_DIR)/*.gcda
	@for u in $(COV_UNITS); do ./$$u >/dev/null || echo "  (failed: $$u)"; done
	@echo "running the suite against the coverage build..."
	@for t in $(PY_TESTS); do \
	  (cd tests && SL0PPTY_BIN=../$(COV_DIR)/sl0ppty timeout 300 python3 $$t >/dev/null 2>&1) \
	    || echo "  (failed: $$t)"; \
	done
	@for t in test_session.py live_m0.py live_input.py live_reload.py; do \
	  (cd tests && SL0PPTY_BIN=../$(COV_DIR)/sl0ppty timeout 300 python3 $$t >/dev/null 2>&1) \
	    || echo "  (failed: $$t)"; \
	done
	@contrib/coverage

clean: ## remove our build output (not the vendor lib)
	rm -rf build

-include $(DEPS)