@ 261101.7
current
.PHONY: all build syntax fonts run test blackbox bench clean install uninstall deb deb-syntax install-deb uninstall-deb

# WORKING_NAME holds the program's name, provisional until a final one is
# chosen. The binary, the package and the paths below take it.
NAME := $(shell cat WORKING_NAME)

all: build

# build names this checkout in the binary, so its bug button files into
# csi/bugs here; the package is built without it.
build:
	CGO_ENABLED=0 go build -ldflags='-X main.sourceDir=$(CURDIR)' -o build/$(NAME) .

# syntax builds the tree-sitter helper beside the binary, which finds it
# there. It is a module of its own, in syntax/, since tree-sitter needs cgo
# and a C compiler; the binary runs without it and says so on a tree page.
syntax:
	cd syntax && CGO_ENABLED=1 go build -o ../build/$(NAME)-syntax .

# fonts cuts the embedded fonts from their upstream releases into
# assets/fonts and writes assets/fonts.css. Its output is committed, so a
# build needs neither the network nor fontTools.
fonts:
	uv run --no-project --with 'fonttools[woff]==4.68.0' --with brotli==1.2.1 python3 assets/fonts/subset.py

# run serves this directory from build/ with --debug, on port 8080 beside
# an installed server on port 80. A server already there is replaced by
# this build, serving the same trees, so its open pages go on under the
# new code; the browser opens only when no page of this directory was
# open, and www says which happened.
RUNADDR := 127.31.$(shell expr $$(id -u) - 1000).1:8080

run: build syntax
	@build/$(NAME) --restart --debug -a $(RUNADDR) .

test:
	go vet ./...
	go test ./...

# blackbox runs tests/ against the built binary. See doc/testing.md.
blackbox: build
	cd tests && TEST_BIN=$(CURDIR)/build/$(NAME) uv run pytest

# bench runs the benchmarks in bench_test.go. BENCH picks them by pattern
# and COUNT repeats each, eg COUNT=10 for benchstat.
BENCH ?= .
COUNT ?= 1

bench:
	go test -run '^$$' -bench '$(BENCH)' -benchmem -count $(COUNT) .

clean:
	rm -rf build

# install and install-deb end by moving running servers to the new binary,
# so no server keeps old code. install moves only the servers its own
# binary started, so the package keeps its servers. install-deb removes the
# make install link, so it takes every server with --force. A server on
# port 80 stays put under the binary from make install, which cannot bind
# it. install also ships a
# user unit of its own name, disabled, which serves its build's .LOCK trees
# again at login, beside the package's units; it never uses sudo, so the
# system template comes only with the package. An older install named its
# unit as the package's, which hid the package's user unit; install
# disables and removes it.
USERUNIT := $(HOME)/.config/systemd/user/$(NAME)-dev.service
OLDUNIT := $(HOME)/.config/systemd/user/$(NAME).service

install: build
	ln -sf $(CURDIR)/build/$(NAME) $(HOME)/.local/bin/$(NAME)
	mkdir -p $(dir $(USERUNIT))
	sed 's/@NAME@/$(NAME)/g; s|@BIN@|$(HOME)/.local/bin/$(NAME)|' systemd/dev.service.in > $(USERUNIT)
	-grep -qs '^ExecStart=$(HOME)/.local/bin/' $(OLDUNIT) && { systemctl --user disable $(NAME); rm $(OLDUNIT); }
	-systemctl --user daemon-reload
	-build/$(NAME) --restart

uninstall:
	rm -f $(HOME)/.local/bin/$(NAME) $(USERUNIT)
	-systemctl --user daemon-reload

# deb builds a Debian package in build/. The version comes from the last
# commit, its UTC time first so later commits sort higher, until releases
# carry their own; ARCH picks the architecture.
VERSION ?= 0~git$(shell TZ=UTC git log -1 --format=%cd --date=format-local:%Y%m%d%H%M%S).$(shell git rev-parse --short HEAD)
ARCH ?= $(shell go env GOARCH)
STAGE := build/deb
DEB := build/$(NAME)_$(VERSION)_$(ARCH).deb

deb:
	rm -rf $(STAGE)
	CGO_ENABLED=0 GOARCH=$(ARCH) go build -trimpath -ldflags='-s -w' -o $(STAGE)/usr/bin/$(NAME) .
	install -D -m 0644 README.md $(STAGE)/usr/share/doc/$(NAME)/README.md
	{ sed '/^@EXPAT@$$/,$$d; s/@NAME@/$(NAME)/g' deb/copyright.in; sed '1,4d; s/^$$/./; s/^/ /' LICENSE; \
	  sed '1,/^@EXPAT@$$/d' deb/copyright.in; sed 's/^$$/./; s/^/ /' assets/fonts/STIXTwoText-OFL.txt; } > $(STAGE)/copyright
	install -D -m 0644 $(STAGE)/copyright $(STAGE)/usr/share/doc/$(NAME)/copyright
	rm $(STAGE)/copyright
	mkdir -p $(STAGE)/usr/share/lintian/overrides
	sed 's/@NAME@/$(NAME)/g' deb/lintian-overrides.in > $(STAGE)/usr/share/lintian/overrides/$(NAME)
	mkdir -p $(STAGE)/usr/lib/systemd/user $(STAGE)/usr/lib/systemd/system
	sed 's/@NAME@/$(NAME)/g; s|@BIN@|/usr/bin/$(NAME)|' systemd/user.service.in > $(STAGE)/usr/lib/systemd/user/$(NAME).service
	sed 's/@NAME@/$(NAME)/g; s|@BIN@|/usr/bin/$(NAME)|' systemd/system@.service.in > $(STAGE)/usr/lib/systemd/system/$(NAME)@.service
	printf '$(NAME) (%s) unstable; urgency=low\n\n  * Build of commit %s.\n\n -- %s  %s\n' \
		'$(VERSION)' "$$(git rev-parse --short HEAD)" "$$(sed -n 's/^Maintainer: //p' deb/control.in)" \
		"$$(git log -1 --format=%cD)" | gzip -9n > $(STAGE)/changelog.gz
	install -D -m 0644 $(STAGE)/changelog.gz $(STAGE)/usr/share/doc/$(NAME)/changelog.gz
	rm $(STAGE)/changelog.gz
	mkdir -p $(STAGE)/DEBIAN
	sed 's/@NAME@/$(NAME)/g' deb/postinst.in > $(STAGE)/DEBIAN/postinst
	sed -e 's/@NAME@/$(NAME)/g' -e 's/@VERSION@/$(VERSION)/' -e 's/@ARCH@/$(ARCH)/' \
		-e "s/@SIZE@/$$(du -sk $(STAGE)/usr | cut -f1)/" deb/control.in > $(STAGE)/DEBIAN/control
	chmod -R u=rwX,go=rX $(STAGE)
	chmod 0755 $(STAGE)/DEBIAN/postinst
	dpkg-deb --root-owner-group --build $(STAGE) $(DEB)

# deb-syntax packages the tree-sitter helper as NAME-syntax. It is a
# package of its own because it needs cgo and libc, while the main binary
# is static. The main package recommends it and it depends on the main
# package, each at the same version, since the main binary reads the
# helper's output. Its libc dependency is the newest glibc symbol version
# the helper uses. A build for another ARCH needs a C cross compiler, eg
# CC=aarch64-linux-gnu-gcc.
SYNSTAGE := build/deb-syntax
SYNDEB := build/$(NAME)-syntax_$(VERSION)_$(ARCH).deb
SYNDOC := $(SYNSTAGE)/usr/share/doc/$(NAME)-syntax

deb-syntax:
	rm -rf $(SYNSTAGE)
	cd syntax && CGO_ENABLED=1 GOARCH=$(ARCH) go build -trimpath -buildmode=pie -ldflags='-s -w' -o ../$(SYNSTAGE)/usr/bin/$(NAME)-syntax .
	mkdir -p $(SYNDOC) $(SYNSTAGE)/DEBIAN
	{ sed '/^@EXPAT@$$/,$$d; s/@NAME@/$(NAME)/g' deb/syntax-copyright.in; sed '1,4d; s/^$$/./; s/^/ /' LICENSE; } > $(SYNDOC)/copyright
	printf '$(NAME)-syntax (%s) unstable; urgency=low\n\n  * Build of commit %s.\n\n -- %s  %s\n' \
		'$(VERSION)' "$$(git rev-parse --short HEAD)" "$$(sed -n 's/^Maintainer: //p' deb/control.in)" \
		"$$(git log -1 --format=%cD)" | gzip -9n > $(SYNDOC)/changelog.gz
	sed -e 's/@NAME@/$(NAME)/g' -e 's/@VERSION@/$(VERSION)/g' -e 's/@ARCH@/$(ARCH)/' \
		-e "s/@LIBC@/$$(objdump -T $(SYNSTAGE)/usr/bin/$(NAME)-syntax | grep -o 'GLIBC_[0-9.]*' | sed 's/GLIBC_//' | sort -V | tail -1)/" \
		-e "s/@SIZE@/$$(du -sk $(SYNSTAGE)/usr | cut -f1)/" deb/syntax-control.in > $(SYNSTAGE)/DEBIAN/control
	chmod -R u=rwX,go=rX $(SYNSTAGE)
	dpkg-deb --root-owner-group --build $(SYNSTAGE) $(SYNDEB)

# install-deb installs both packages through apt, which pulls in git.
# --reinstall replaces a build of the same commit, which apt would skip as
# already installed. The link from make install goes first: ~/.local/bin
# usually leads PATH, and its binary lacks the capability for port 80.
# NEEDRESTART_MODE=l makes Debian's needrestart list the services that use
# old libraries instead of asking which to restart; env sets it past sudo's
# environment reset.
# The package builds in the background while sudo asks for the password,
# and its output is hidden so it does not break into the prompt. Plain make,
# not $(MAKE), so make -n prints the line instead of running sudo.
install-deb: uninstall
	@make -s deb deb-syntax >/dev/null & build=$$!; \
	sudo -v; auth=$$?; \
	wait $$build && [ $$auth -eq 0 ]
	sudo env NEEDRESTART_MODE=l apt install --reinstall ./$(DEB) ./$(SYNDEB)
	-/usr/bin/$(NAME) --restart --force

uninstall-deb:
	sudo apt remove $(NAME) $(NAME)-syntax