| 1 | APP := contra |
| 2 | |
| 3 | # What `carlos ship -target` defaults to, and therefore what a release |
| 4 | # must be built for. Building for your own machine and shipping that is a |
| 5 | # silent architecture mismatch: the binary uploads fine and fails to exec |
| 6 | # on the instance. Override for a one-off: |
| 7 | # make release RELEASE_GOARCH=amd64 |
| 8 | RELEASE_GOOS ?= linux |
| 9 | RELEASE_GOARCH ?= arm64 |
| 10 | RELEASE_BIN := releases/$(APP)-$(RELEASE_GOOS)-$(RELEASE_GOARCH) |
| 11 | |
| 12 | # What the running binary reports from GET /api/version. The platform's |
| 13 | # deploy verification polls that endpoint on every instance socket and |
| 14 | # compares what it finds against the release it believes it adopted; an |
| 15 | # unstamped binary answers "dev" from every build you have ever made, so |
| 16 | # those two facts can never disagree and a process that was never |
| 17 | # recycled onto the new release verifies green. Stamping this is what |
| 18 | # gives that check the ability to fail. |
| 19 | # |
| 20 | # git describe, because the tag is what you ship against. Outside a |
| 21 | # repository, or before the first commit, name it yourself: |
| 22 | # make release VERSION=v0.1.0 |
| 23 | VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null) |
| 24 | |
| 25 | .PHONY: build release version-check test test-js test-oracles test-webkit oracles vet fmt-check migration-check ci |
| 26 | |
| 27 | # build is the compile check: with more than one package matched, go |
| 28 | # build discards the output, so this catches a broken package without |
| 29 | # producing an artifact. |
| 30 | build: |
| 31 | CGO_ENABLED=0 go build ./... |
| 32 | |
| 33 | # release is what ships. -s -w drops the symbol table and DWARF, which on |
| 34 | # this family's apps is 23-31% off the binary and 45-53% off the |
| 35 | # compressed artifact — and compressed size is what actually gets |
| 36 | # transferred. carlos is built the same way (see platform's Makefile). |
| 37 | # |
| 38 | # What survives: panic traces keep their function names AND line numbers |
| 39 | # (Go's pclntab is untouched by these flags), debug.ReadBuildInfo works, |
| 40 | # and go version -m still reports module metadata. What you lose is |
| 41 | # source-level debugging with delve or gdb — build without the flags for |
| 42 | # that. rastrillo dev never strips, for the same reason. |
| 43 | release: version-check |
| 44 | @mkdir -p releases |
| 45 | CGO_ENABLED=0 GOOS=$(RELEASE_GOOS) GOARCH=$(RELEASE_GOARCH) \ |
| 46 | go build -ldflags="-s -w -X 'github.com/carlosframework/rastrillo.BuildVersion=$(VERSION)'" \ |
| 47 | -o $(RELEASE_BIN) ./cmd/$(APP) |
| 48 | @echo |
| 49 | @echo "built $(RELEASE_BIN) reporting version $(VERSION)" |
| 50 | @echo "ship it with:" |
| 51 | @echo " carlos ship -app $(APP) -target $(RELEASE_GOOS)-$(RELEASE_GOARCH) $(RELEASE_BIN)" |
| 52 | |
| 53 | # version-check refuses rather than letting release ship a binary whose |
| 54 | # version is untrue. Both refusals are about the same thing: /api/version |
| 55 | # is only evidence if the answer identifies one artifact. |
| 56 | # |
| 57 | # An empty stamp is worse than the "dev" default, because "dev" is |
| 58 | # saying something true about itself and "" looks like a value that |
| 59 | # arrived from somewhere. |
| 60 | # |
| 61 | # A dirty tree is refused rather than suffixed. A -dirty stamp names a |
| 62 | # commit plus changes nobody can name, so two different binaries can |
| 63 | # carry the identical string — which does not fix the check, it makes |
| 64 | # it look reliable while it is not, and that is the more expensive |
| 65 | # failure. Refusing costs one `git stash`; a wrong stamp ships. If you |
| 66 | # mean to ship uncommitted work, say so out loud: |
| 67 | # make release VERSION=v0.1.0-wip |
| 68 | version-check: |
| 69 | @if [ -z "$(VERSION)" ]; then \ |
| 70 | echo "make release: no version to stamp."; \ |
| 71 | echo " git describe found nothing here: no repository, or no commit yet."; \ |
| 72 | echo " Commit, or name it: make release VERSION=v0.1.0"; \ |
| 73 | exit 1; \ |
| 74 | fi |
| 75 | @case "$(VERSION)" in *-dirty) \ |
| 76 | echo "make release: the tree is dirty, so $(VERSION) does not name what you are about to ship."; \ |
| 77 | echo " Commit or stash first, or name it: make release VERSION=v0.1.0-wip"; \ |
| 78 | exit 1;; \ |
| 79 | esac |
| 80 | |
| 81 | test: |
| 82 | go test ./... |
| 83 | |
| 84 | # The crypto twin. AGENTS.md: "Crypto is byte-identical in Go and JS, pinned |
| 85 | # by shared golden vectors." This runs the browser modules against the same |
| 86 | # vectors/contra-v1.json the Go suite uses, so a change to one side that is |
| 87 | # not made to the other fails here rather than in somebody's vault. |
| 88 | # |
| 89 | # A HARD gate, and it does not soften when node is missing. A security product |
| 90 | # must not silently stop verifying that the browser's crypto matches the |
| 91 | # server's: a skip would be indistinguishable from a pass on the one check |
| 92 | # that stops a Mac opening a vault the browser cannot. |
| 93 | test-js: |
| 94 | @command -v node >/dev/null 2>&1 || { \ |
| 95 | echo "make test-js: node is not installed."; \ |
| 96 | echo " This gate is not optional -- it is what proves the browser's"; \ |
| 97 | echo " crypto matches the server's. Install node, do not skip it."; \ |
| 98 | exit 1; } |
| 99 | node --test test/crypto.test.mjs test/ecies.test.mjs test/items.test.mjs \ |
| 100 | test/sharing.test.mjs |
| 101 | |
| 102 | # The third-party oracles: the QR symbols decoded by zxing-cpp and drawn |
| 103 | # again by python-qrcode for a module-by-module comparison, and the two kit |
| 104 | # PDFs read back by PyMuPDF and pypdf. |
| 105 | # |
| 106 | # These matter. Comparing matrices rather than only decoding is how the |
| 107 | # reversed format-information bits were found — twelve wrong modules that a |
| 108 | # decode-only check reports as "does not scan" and leaves you guessing. |
| 109 | # |
| 110 | # NOT part of `make ci`, and that is a deliberate, reluctant split made on |
| 111 | # 2026-09-02 for the same reason test-webkit is out: the shared CI runner's |
| 112 | # python3 is a bare 3.12.3 with no pip module and no ensurepip, so `make |
| 113 | # oracles` cannot install anything there and the box is immutable-boot. A |
| 114 | # gate that cannot run on the runner is a gate that is permanently red, and |
| 115 | # permanently red CI teaches people to ignore red — which costs more than |
| 116 | # this check is worth defending in the wrong place. |
| 117 | # |
| 118 | # It does NOT skip when the packages are missing; it fails with the command |
| 119 | # that fixes it. Run it before trusting a change to qr.js, qrtables.js, |
| 120 | # qrgf.js, pdf.js or kit.js: |
| 121 | # |
| 122 | # make oracles && make test-oracles |
| 123 | # |
| 124 | # Getting these back into CI is one line of python3-pip in the runner's |
| 125 | # user-data. Filed rather than worked around. |
| 126 | test-oracles: |
| 127 | @command -v node >/dev/null 2>&1 || { \ |
| 128 | echo "make test-oracles: node is not installed."; exit 1; } |
| 129 | node --test test/qr.test.mjs test/kit.test.mjs |
| 130 | |
| 131 | # The third-party oracles' Python packages, for `make test-oracles`. |
| 132 | # |
| 133 | # Opt-in rather than a dependency of anything: a machine whose system python3 |
| 134 | # already has them needs nothing, and test/oracle.mjs reaches for what this |
| 135 | # builds only when it exists. Pinned in test/requirements.txt, because an |
| 136 | # oracle that drifts is an oracle that stops meaning what it said last week. |
| 137 | # |
| 138 | # Two ways in, because a Debian python3 without the python3-venv package |
| 139 | # cannot create a venv at all (no ensurepip) — which is exactly what the |
| 140 | # shared runner turned out to be. A venv is preferred where it works; where |
| 141 | # it does not, the packages go into .oracles/ and test/oracle.mjs puts that |
| 142 | # on PYTHONPATH. Both give the oracles their imports without touching the |
| 143 | # box, which is the requirement: its user-data is fixed at launch. |
| 144 | oracles: |
| 145 | @command -v python3 >/dev/null 2>&1 || { \ |
| 146 | echo "make oracles: python3 is not installed."; exit 1; } |
| 147 | @set -e; \ |
| 148 | if python3 -m venv .venv >/dev/null 2>&1; then \ |
| 149 | .venv/bin/pip install --quiet --upgrade pip; \ |
| 150 | .venv/bin/pip install --quiet -r test/requirements.txt; \ |
| 151 | echo "oracles ready: .venv/bin/python3"; \ |
| 152 | elif python3 -m pip --version >/dev/null 2>&1; then \ |
| 153 | rm -rf .venv; \ |
| 154 | python3 -m pip install --quiet --target .oracles \ |
| 155 | -r test/requirements.txt; \ |
| 156 | echo "oracles ready: python3 with PYTHONPATH=.oracles"; \ |
| 157 | else \ |
| 158 | rm -rf .venv; \ |
| 159 | echo "make oracles: this python3 can neither create a venv nor run pip."; \ |
| 160 | echo " python3: $$(python3 --version 2>&1)"; \ |
| 161 | echo " venv : $$(python3 -m venv /tmp/contra-venv-probe 2>&1 | tail -1)"; \ |
| 162 | echo " pip : $$(python3 -m pip --version 2>&1 | tail -1)"; \ |
| 163 | rm -rf /tmp/contra-venv-probe; \ |
| 164 | echo " Install python3-venv or python3-pip, or set CONTRA_PYTHON to"; \ |
| 165 | echo " an interpreter that already has test/requirements.txt."; \ |
| 166 | exit 1; \ |
| 167 | fi |
| 168 | |
| 169 | # The browser crypto in WebKit, which is the engine this product can least |
| 170 | # afford to be wrong about and the one the Chromium rig never touches. |
| 171 | # |
| 172 | # NOT part of `make ci`, on purpose. It needs Playwright, which not every |
| 173 | # machine will have, and a gate that cannot run everywhere is a gate that |
| 174 | # teaches people to ignore red. It skips visibly rather than silently, and it |
| 175 | # is what you run before trusting a change to the crypto modules. |
| 176 | # |
| 177 | # npm install playwright && make test-webkit |
| 178 | test-webkit: |
| 179 | node --test test/webkit.test.mjs |
| 180 | |
| 181 | vet: |
| 182 | go vet ./... |
| 183 | |
| 184 | fmt-check: |
| 185 | @out=$$(gofmt -l .); if [ -n "$$out" ]; then echo "gofmt needed:"; echo "$$out"; exit 1; fi |
| 186 | |
| 187 | # A step of its own, not folded into another target's recipe: ci.d/ |
| 188 | # steps each exec one make target, so migration-check needs its own |
| 189 | # name to get its own reported step on a runner with step support. |
| 190 | # |
| 191 | # go run, not a bare "rastrillo": go.mod already requires rastrillo, so |
| 192 | # this needs nothing on PATH but the Go toolchain, and works offline |
| 193 | # after go mod download. A bare "rastrillo migration check" failed a |
| 194 | # fresh clone's very first CI run with "make: rastrillo: Command not |
| 195 | # found", and nothing in the scaffold said what to install. |
| 196 | migration-check: |
| 197 | go run github.com/carlosframework/rastrillo/cmd/rastrillo migration check |
| 198 | |
| 199 | # ci is the one gate: what a runner executes and what you run before |
| 200 | # pushing are the same definition. amadan's runner falls back to this |
| 201 | # target when .amadan/ci is absent; ci.d/ carries the same targets, |
| 202 | # reported one by one, for a runner with step support. migration-check |
| 203 | # fails the build the moment models.go and migrations/ disagree, |
| 204 | # instead of at boot on whatever machine notices next. If the app |
| 205 | # declares manifest resources, also add: rastrillo generate --check |
| 206 | ci: vet fmt-check test test-js migration-check |
| 207 | |