# diff.sr.ht — build scaffolding (git.sr.ht / sourcehut-dolt style).
#
# Go binary + Go-template SSR live under cmd/ and web/. The CSS is built from
# the shared sourcehut scss partials with sassc; the frontend diff bundle is
# built once with esbuild and vendored into web/static/. Node is a build-time
# dependency only.
SERVICE=diff.sr.ht
BIN=comparesrht
PREFIX?=/usr/local
BINDIR?=$(PREFIX)/bin
SHAREDIR?=$(PREFIX)/share
ASSETS?=/usr/share/sourcehut
STATICDIR?=$(ASSETS)/$(SERVICE)/static
SASSC?=sassc
SASSC_INCLUDE=-I$(ASSETS)/scss
MINIFY?=minify
# The glob web/templates.go resolves at startup (cssGlob). Named once so that
# `css`, which removes the previous build's file, and `check-css`, which counts
# what is left, cannot drift apart.
CSS=web/static/main.min.*.css
all: build
# Compile the service into ./comparesrht.
build:
go build -o $(BIN) ./cmd/$(BIN)
test:
go test ./...
# CSS pipeline: sassc -> minify -> content-hashed filename. The running
# service globs web/static/main.min.*.css at startup, so the hash in the name
# is the cache-busting version. Requires the shared scss partials to be
# installed at $(ASSETS)/scss (core.sr.ht `make install`, which installs both
# base.scss and bootstrap/scss there) and scss/main.scss to exist (phase 2b).
# Produces exactly ONE web/static/main.min.<sha256[:8]>.css (old ones and the
# intermediate main.css are removed).
#
# The three preflight checks are here because each of their failures is
# otherwise illegible. A missing minifier stops the pipeline after sassc has
# already written main.css, and the shell's "command not found" names a binary
# rather than a package; a missing $(ASSETS)/scss/base.scss is the family's
# standing rake — NO PACKAGE MANAGER SHIPS THAT PARTIAL, it is materialized by
# core.sr.ht's own `make install` locally and by the `scss` task of .build.yml
# in CI, and sassc's report of it is one line about an import.
css:
@command -v $(SASSC) >/dev/null 2>&1 || { \
echo "error: $(SASSC) not found — install sassc (apk add sassc, brew install sassc)"; \
exit 1; }
@command -v $(MINIFY) >/dev/null 2>&1 || { \
echo "error: $(MINIFY) not found — install tdewolff/minify (apk add minify," \
"go install github.com/tdewolff/minify/v2/cmd/minify@latest)"; \
exit 1; }
@[ -f $(ASSETS)/scss/base.scss ] || { \
echo "error: no $(ASSETS)/scss/base.scss — the shared partial is not packaged;" \
"materialize it with core.sr.ht's 'make install' (or the scss task of .build.yml)"; \
exit 1; }
mkdir -p web/static
rm -f web/static/main.css $(CSS)
$(SASSC) $(SASSC_INCLUDE) scss/main.scss web/static/main.css
$(MINIFY) -o web/static/main.min.css web/static/main.css
mv web/static/main.min.css \
web/static/main.min.$$(sha256sum web/static/main.min.css | cut -c1-8).css
rm -f web/static/main.css
# check-css is the packaging gate: no stylesheet, no release. It exists because
# the failure it catches is invisible at build time — `go build` succeeds
# perfectly well with an unstyled web/static (//go:embed takes the directory,
# not the file), and the first sign of trouble is an unstyled page in
# production.
#
# It answers about the DISK, and that is the limit of it: nothing here says the
# file it counted is the file a binary embedded.
#
# It COUNTS the matches rather than merely asking whether there are any, because
# two stylesheets are as wrong as none and quieter: web/templates.go resolves
# cssGlob and takes the first match, so a second file makes the served
# stylesheet depend on readdir order. `css` removes the previous build for that
# reason, which is exactly why this gate must not assume it did — a gate that
# promises "exactly one" and checks "at least one" is a promise the next person
# builds on.
#
# `set --` puts the matches in the positional parameters, so the count is $$#
# and no `wc` output has to be parsed. It is fine that this splits on
# whitespace: every name it can see was produced by the recipe above, out of a
# hex digest.
check-css:
@set -- $$(ls $(CSS) 2>/dev/null); \
if [ $$# -eq 0 ]; then \
echo "error: no $(CSS) — run 'make css' before 'go build' (the CSS is embedded)"; \
exit 1; \
elif [ $$# -gt 1 ]; then \
echo "error: $$# files match $(CSS) — web/ takes the first, so the choice is arbitrary."; \
echo " run 'make css' to get back to one:"; \
for f in "$$@"; do echo " $$f"; done; \
exit 1; \
fi
# Frontend diff/tree bundle: built once, output committed to web/static. The
# filename carries a content hash (like the CSS) so a deploy busts the browser
# cache — a stale bundle.js is otherwise served for up to max-age and can leave
# the file tree rendering blank after an upgrade. Produces exactly ONE
# web/static/bundle.<sha256[:8]>.js (old ones removed).
bundle:
mkdir -p web/static
rm -f web/static/bundle.js web/static/bundle.*.js
cd frontend && npm ci && \
npx esbuild src/app.ts --bundle --minify --format=esm \
--outfile=../web/static/bundle.js
mv web/static/bundle.js \
web/static/bundle.$$(sha256sum web/static/bundle.js | cut -c1-8).js
# Local development run. Requires a ./config.ini in the working directory (or
# ../config.ini, /etc/sr.ht/config.ini): copy config.example.ini and fill in the
# instance's shared [sr.ht]/[webhooks] keys, plus a [git.sr.ht] repos root and an
# api-origin pointing at a real (or stubbed, see contrib/dev-stub) GraphQL API.
# See the "Development" section of README.md for the full recipe.
run-dev: build
./$(BIN) -b localhost:5090
install: check-css build
@$(MAKE) install-files
# The copying half of `install`, with nothing to build in front of it — the
# target a packaging run calls once it has already built and checked the binary,
# so that the files it stages are the very bytes it checked and not a second
# compilation of the same sources.
#
# That distinction is the whole point and it is not theoretical. `build` above is
# .PHONY (Go decides staleness itself, a real file target would never rebuild
# after a source edit), so `install` recompiles; abuild runs package() in a FRESH
# abuild process under fakeroot, which re-sources the APKBUILD and never calls
# build(), so the GOCACHE/GOMODCACHE pins are gone and that recompilation comes
# from a cold cache. The packaged binary would be one nothing had inspected.
#
# It is invoked through a sub-make rather than listed as a third prerequisite of
# `install` on purpose: /usr/share/abuild/default.conf exports
# MAKEFLAGS=-j$(nproc), prerequisites of one target run in parallel under -j, and
# `install: check-css build install-files` would let the copying start beside the
# build it is supposed to follow. A recipe line always runs after the
# prerequisites are done.
#
# There is no check in front of the copies. `install -Dm755 comparesrht` on a
# missing file is already a fatal error naming the file, which is exactly the
# right report for the one way this target can be called too early.
install-files:
install -Dm755 $(BIN) $(DESTDIR)$(BINDIR)/$(BIN)
mkdir -p $(DESTDIR)$(STATICDIR)
install -Dm644 -t $(DESTDIR)$(STATICDIR) web/static/*
.PHONY: all build test css check-css bundle run-dev install install-files