aboutsummaryrefslogtreecommitdiff
path: root/Makefile
blob: 6fe82dcdbaffa5e5125403424c8806b05b37b00f (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
COLITUR  := colitur
PREFIX   ?= $(HOME)/.local
BINDIR   := $(PREFIX)/bin
MANDIR   := $(PREFIX)/share/man/man1
# Section 5 is file formats: the overlay format is a thing a user AUTHORS,
# not a command they run, so it belongs beside fstab(5) rather than in man1.
MAN5DIR  := $(PREFIX)/share/man/man5

# The binary locates its calendar data relative to its own path
# (<prefix>/share/colitur/ef), so `dune install` -- not a hand-rolled copy --
# is what places the four .sexp files where a running colitur will look for
# them. See bin/main.ml's own [data_dir] for the full resolution order.
# Task 13 (schema/dune, templates/dune) extends the same `dune install`
# mechanism to <prefix>/share/colitur/schema/ (bin/main.ml's own
# [schema_path], read by `colitur publish`) and <prefix>/share/colitur/
# templates/ (a convenience install -- `--template` takes a plain file path,
# nothing in colitur probes this location the way [data_dir]/[schema_path] do).
#
# dune needs the project-local opam switch on PATH. Every recipe that invokes
# dune goes through this, so `make` works from a plain shell with no
# `eval $(opam env)` first -- which is the whole point of having a Makefile
# over documenting the dune commands.
DUNE := opam exec --

.PHONY: release-tarball help build test check check-schema check-templates check-citations install uninstall reinstall clean fmt man doc release
help: ## show this help
	@grep -hE '^[a-z-]+:.*##' $(MAKEFILE_LIST) | sed -E 's/:.*## /\t/' | sort

build: ## build the binary
	$(DUNE) dune build

test: ## fast suite (~5s): properties sample 200 years
	$(DUNE) dune test

check: ## full gate (~2min): every year 1583-9999, not a sample
	COLITUR_EXHAUSTIVE_SWEEP=1 $(DUNE) dune test --force

check-schema: build ## validate emitted XML against schema/colitur-v1.xsd (needs xmllint; skipped if absent)
	@if command -v xmllint >/dev/null 2>&1; then \
	  opam exec -- dune exec colitur -- emit --format xml --from 2027 --to 2027 > /tmp/colitur-check.xml && \
	  xmllint --noout --schema schema/colitur-v1.xsd /tmp/colitur-check.xml && \
	  echo "xml: valid against schema/colitur-v1.xsd"; \
	else \
	  echo "SKIPPED: xmllint not installed -- XML is emitted but NOT schema-validated"; \
	fi

# Typesets every shipped template through the real tool that would typeset it
# for a reader, not merely through this program's own renderer. The golden
# tests already prove `colitur table` PRODUCES the expected LaTeX/groff/HTML/
# Typst text; only this proves that text actually TYPESETS -- a template can
# render byte-for-byte as pinned and still be malformed LaTeX, groff or
# Typst. Each tool is genuinely optional (none is in colitur's own frozen
# deps), so absence is printed LOUDLY as SKIPPED rather than silently
# treated as a pass -- a silent skip reads as a pass, which is the whole
# failure mode this guards against.
#
# ordo.ms's own -P-pa5 (added alongside grid.ms's existing -P-pa4l, the
# same mechanism, not a new one) and -Kutf8 (both flavours) fix two
# defects this target's own "did it TYPESET" check could never see on its
# own: groff's default PDF device paper is not A5 (so the booklet was
# roughly twice the intended page area -- half a real density problem
# was hiding in the page, not the type) and groff's default input
# encoding is not UTF-8 (so an accented Latin name like Pe\(~nafort
# rendered as visibly wrong characters) -- neither one is a groff
# WARNING, so this target's own "any stderr is a failure" check was
# silently green through both. Rendering to an actual PDF and reading it
# is what caught them; a byte-identical-to-golden pass would not have.
check-templates: build ## typeset every shipped template (needs pdflatex/groff/typst; skipped per-tool if absent)
	@ok=1; \
	if command -v pdflatex >/dev/null 2>&1; then \
	  for t in ordo grid; do \
	    opam exec -- dune exec colitur -- table --year 2027 --template templates/ef/$$t.tex > /tmp/$$t.tex && \
	    (cd /tmp && pdflatex -halt-on-error -interaction=nonstopmode $$t.tex >/dev/null && \
	                pdflatex -halt-on-error -interaction=nonstopmode $$t.tex >/dev/null) && \
	    echo "pdflatex: $$t.tex OK (two passes, so a TOC's own \\pageref settles)" || \
	      { echo "pdflatex: $$t.tex FAILED"; ok=0; }; \
	  done; \
	else echo "SKIPPED: pdflatex not installed -- LaTeX templates render but are NOT typeset"; fi; \
	if command -v groff >/dev/null 2>&1; then \
	  for t in ordo grid; do \
	    gflags=""; \
	    [ "$$t" = grid ] && gflags="-P-pa4l"; \
	    [ "$$t" = ordo ] && gflags="-P-pa5"; \
	    if opam exec -- dune exec colitur -- table --year 2027 --template templates/ef/$$t.ms \
	         > /tmp/$$t.ms 2>/tmp/$$t-render.err; then \
	      groff -ms -t -Tpdf -Kutf8 $$gflags /tmp/$$t.ms > /tmp/$$t-ms.pdf 2>/tmp/$$t-ms.warnings; \
	      if [ -s /tmp/$$t-ms.warnings ]; then \
	        echo "groff: $$t.ms FAILED (groff exits 0 on a tbl/troff warning -- this target"; \
	        echo "  treats ANY stderr output as a failure, since a warning here has meant"; \
	        echo "  silently lost table columns before):"; \
	        cat /tmp/$$t-ms.warnings; ok=0; \
	      else \
	        echo "groff: $$t.ms OK"; \
	      fi; \
	    else \
	      echo "groff: $$t.ms FAILED (colitur table render):"; cat /tmp/$$t-render.err; ok=0; \
	    fi; \
	  done; \
	else echo "SKIPPED: groff not installed -- groff templates render but are NOT typeset"; fi; \
	if command -v typst >/dev/null 2>&1; then \
	  for t in ordo grid; do \
	    if opam exec -- dune exec colitur -- table --year 2027 --template templates/ef/$$t.typ \
	         > /tmp/$$t.typ 2>/tmp/$$t-render.err; then \
	      if typst compile /tmp/$$t.typ /tmp/$$t-typst.pdf 2>/tmp/$$t-typst.err; then \
	        echo "typst: $$t.typ OK (one pass -- typst resolves its own"; \
	        echo "  table-of-contents page numbers inside a single compile, unlike pdflatex above)"; \
	      else \
	        echo "typst: $$t.typ FAILED:"; cat /tmp/$$t-typst.err; ok=0; \
	      fi; \
	    else \
	      echo "typst: $$t.typ FAILED (colitur table render):"; cat /tmp/$$t-render.err; ok=0; \
	    fi; \
	  done; \
	else echo "SKIPPED: typst not installed -- Typst templates render but are NOT typeset"; fi; \
	test $$ok -eq 1

# lang/la.ini's own discipline: every celebration/season/rank name cites the
# docs/research/LT.txt line it was transcribed from, so a claim can be
# checked, not just trusted. tools/check_citations.py re-derives that check
# mechanically: for every "LT.txt:<n>" citation outside a PATTERN-marked
# block, it confirms the EXACT cited line (or an explicit "n-m" range, for
# a heading that genuinely wraps -- no automatic +-2-line tolerance any
# more, see the script's own docstring for why) contains the Latin text the
# citation claims, scoped to that citation's own entry only, never to
# prose quoted elsewhere in the surrounding comment. A citation whose pool
# of candidate words is too thin to discriminate a right line from a wrong
# nearby one (a heuristic, not a proof) is reported CANNOT VERIFY, not
# silently passed, and fails the target exactly like a genuine mismatch --
# a human adjudicates it. The script has its own self-test
# (tools/test_check_citations.py, wired into `dune test`/`make test` as
# well as this target) after an earlier version of this exact check was
# found to be self-poisoning: see either docstring for the full account.
# docs/ is gitignored, so a fresh clone has no docs/research/LT.txt at
# all: the same "SKIPPED, loudly, exit 0" discipline check-schema/
# check-templates already use above -- a silent skip reads as a pass,
# which this project has hit the cost of before.
check-citations: ## verify lang/la.ini's LT.txt:<n> citations (needs docs/research/LT.txt, gitignored; SKIPPED if absent)
	@if command -v python3 >/dev/null 2>&1; then \
	  python3 tools/check_citations.py; \
	else \
	  echo "SKIPPED: python3 not installed -- citations NOT verified this run"; \
	fi

install: build ## install binary, calendar data, language tables, templates, schema and man pages into PREFIX (default ~/.local)
	$(DUNE) dune install --prefix $(PREFIX)
	@mkdir -p $(MANDIR)
	install -m 644 man/colitur.1 $(MANDIR)/colitur.1
	@mkdir -p $(MAN5DIR)
	install -m 644 man/colitur-overlay.5 $(MAN5DIR)/colitur-overlay.5
	install -m 644 man/colitur-templates.5 $(MAN5DIR)/colitur-templates.5
	install -m 644 man/colitur-config.5 $(MAN5DIR)/colitur-config.5
	@echo "installed $(BINDIR)/$(COLITUR), data in $(PREFIX)/share/colitur/{ef,lang,templates,schema}, man pages in $(MANDIR) and $(MAN5DIR)"
	@command -v $(COLITUR) >/dev/null 2>&1 || \
	  echo "note: $(BINDIR) is not on PATH -- add it, or run $(BINDIR)/$(COLITUR) directly"

# --- release -------------------------------------------------------------
# cgit offers per-tag snapshots for EVERY tag or for none, and ten tags meant
# twenty download links, nineteen of them stale. colitur.git therefore has
# cgit.snapshots=0 and this target publishes one current tarball at a stable
# URL instead. Run it from a tagged commit.
RELEASE_HOST ?= vps-mail-i2p
RELEASE_DIR  ?= /usr/local/www/cgit
RELEASE_NAME ?= colitur-latest.tar.gz

release-tarball: ## upload the current tag's tarball as the single published download
	@v=$$(git describe --tags --abbrev=0 2>/dev/null) || \
	  { echo "make release-tarball: no tag to publish" >&2; exit 1; }; \
	test -z "$$(git status --porcelain)" || \
	  { echo "make release-tarball: working tree is dirty" >&2; exit 1; }; \
	git describe --tags --exact-match >/dev/null 2>&1 || \
	  echo "note: HEAD is ahead of $$v; publishing the tag, not HEAD" >&2; \
	echo "packaging $$v"; \
	git archive --format=tar.gz --prefix=colitur-$${v#v}/ -o /tmp/$(RELEASE_NAME) "$$v"; \
	scp -q /tmp/$(RELEASE_NAME) $(RELEASE_HOST):/tmp/$(RELEASE_NAME); \
	ssh $(RELEASE_HOST) "doas install -m 644 /tmp/$(RELEASE_NAME) $(RELEASE_DIR)/$(RELEASE_NAME) && rm -f /tmp/$(RELEASE_NAME)"; \
	rm -f /tmp/$(RELEASE_NAME); \
	echo "published $$v to https://git.labunix.xyz/$(RELEASE_NAME)"

uninstall: ## remove everything install put into PREFIX
	-$(DUNE) dune uninstall --prefix $(PREFIX)
	rm -f $(MANDIR)/colitur.1 $(MAN5DIR)/colitur-overlay.5 $(MAN5DIR)/colitur-templates.5 $(MAN5DIR)/colitur-config.5
	@echo "removed $(COLITUR) from $(PREFIX)"

reinstall: uninstall install ## uninstall then install (the installed copy is a snapshot, not a link)

man: ## preview the man pages
	man -l man/colitur.1
	man -l man/colitur-overlay.5
	man -l man/colitur-templates.5
	man -l man/colitur-config.5

doc: ## lint the man pages (groff warnings; silence means clean)
	groff -man -Tutf8 -ww -z man/colitur.1
	groff -man -Tutf8 -ww -z man/colitur-overlay.5
	groff -man -Tutf8 -ww -z man/colitur-templates.5
	groff -man -Tutf8 -ww -z man/colitur-config.5

fmt: ## format the OCaml sources
	$(DUNE) dune build @fmt --auto-promote

clean: ## remove build artifacts
	$(DUNE) dune clean

# Mirrors lectio's own release target, including its refusals: no release from
# a dirty tree, no release without a CHANGELOG line, and no release whose
# version bump silently failed to apply. The version lives in TWO places
# (dune-project, which feeds colitur.opam, and bin/main.ml's own constant,
# which is what --version prints), so both are rewritten and both are
# re-checked -- a release that bumped one and not the other would ship a
# binary disagreeing with its own package metadata.
release: ## cut a release: make release VERSION=0.1.0  (add its CHANGELOG line first)
	@test -n "$(VERSION)" || { echo "set VERSION=X.Y.Z" >&2; exit 2; }
	@echo "$(VERSION)" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$$' || { echo "VERSION must be X.Y.Z" >&2; exit 2; }
	@test -z "$$(git status --porcelain)" || { echo "working tree not clean; commit or stash first" >&2; exit 2; }
	@grep -q '## \[$(VERSION)\]' CHANGELOG.md || { echo "add a one-line CHANGELOG.md entry under '## [$(VERSION)]' first" >&2; exit 2; }
	@sed -i 's/^(version .*)$$/(version $(VERSION))/' dune-project
	@sed -i 's/^let version = ".*"/let version = "$(VERSION)"/' bin/main.ml
	@grep -q '^(version $(VERSION))$$' dune-project || { echo "dune-project version bump failed" >&2; exit 2; }
	@grep -q '^let version = "$(VERSION)"' bin/main.ml || { echo "bin/main.ml version bump failed" >&2; exit 2; }
	$(MAKE) check
	@test "$$($(DUNE) dune exec --no-build colitur -- --version)" = "$(VERSION)" || \
	  { echo "the built binary does not report $(VERSION)" >&2; exit 2; }
	@git add dune-project colitur.opam bin/main.ml CHANGELOG.md
	@# Only commit if the bump actually changed something. Re-running release
	@# after a failed gate -- or cutting a version whose files are already
	@# correct, which is exactly how the first tag went -- leaves nothing
	@# staged, and `git commit` would abort the whole target on "nothing to
	@# commit" despite everything being in order.
	@git diff --cached --quiet || git commit -m "release: v$(VERSION)"
	@git rev-parse -q --verify "refs/tags/v$(VERSION)" >/dev/null && \
	  { echo "tag v$(VERSION) already exists; delete it first to re-cut" >&2; exit 2; } || true
	git tag -a "v$(VERSION)" -m "colitur $(VERSION)"
	@echo "tagged v$(VERSION). publish with: git push origin main v$(VERSION) && make release-tarball"