aboutsummaryrefslogtreecommitdiffstats
path: root/Makefile
blob: 76df1996191d6c34338b21225560bdf8d7561689 (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
# 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)

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

help: ## show this
	@grep -hE '^[a-z-]+:.*##' $(MAKEFILE_LIST) | sed 's/:.*##/\t/' | expand -t20

all: $(BIN) ## build sl0ppty

$(BIN): $(OBJ) $(VT_LIB)
	$(CC) $(OBJ) $(VT_LIB) -o $@ $(LDFLAGS)

build/%.o: src/%.c build/version.h $(VT_LIB) | build
	$(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
	$(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
	$(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
	$(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
	$(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
	$(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
	$(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
	$(CC) $(PLUGIN_CFLAGS) $< -o $@

build/badshader.so: tests/shader_plugin_test.c src/shader_abi.h | build
	$(CC) $(PLUGIN_CFLAGS) -DBAD_ABI $< -o $@

build/exampleshader.so: contrib/shader-plugin/example.c src/shader_abi.h | build
	$(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: 日本語"'

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)
	@$(COV_CC) $(COV_FLAGS) -c $< -o $@

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

$(COV_DIR)/sl0ppty: $(COV_OBJ) $(VT_LIB)
	@$(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)
	@$(COV_CC) $(COV_FLAGS) $^ -o $@
$(COV_DIR)/kdl_test: tests/kdl_test.c $(COV_DIR)/kdl.o
	@$(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)
	@$(COV_CC) $(COV_FLAGS) $^ -o $@
$(COV_DIR)/expr_test: tests/expr_test.c $(COV_DIR)/expr.o
	@$(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)