Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
511 lines (454 loc) · 27.4 KB
/
Copy pathMakefile
File metadata and controls
511 lines (454 loc) · 27.4 KB
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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
BINARY := plumb
CMD := ./cmd/plumb
TESTCACHE := .testcache
# install destination. PREFIX defaults to ~/.local (needs no sudo, is not
# Homebrew-managed, and aligns with the XDG dirs plumb already uses); override
# for a system location, e.g. `make install PREFIX=/usr/local` (needs sudo).
# DESTDIR is honoured for staged/packaged installs.
PREFIX ?= $(HOME)/.local
BINDIR := $(DESTDIR)$(PREFIX)/bin
# Try an exact git tag first (release builds), then fall back to VERSION file,
# then fall back to the short commit hash.
VERSION := $(shell git describe --tags --exact-match 2>/dev/null || cat VERSION 2>/dev/null || git rev-parse --short HEAD 2>/dev/null || echo dev)
# Build-time source provenance. This Makefile always runs with its working
# directory inside the PUBLIC plumb repository, so these git commands describe
# this repository even when the build is invoked from the private plumb-ops
# superproject (`make -C plumb build`), which mounts it as a submodule. That is
# precisely why they are stamped explicitly: Go's own debug.ReadBuildInfo()
# resolves vcs.revision/vcs.modified against the OUTER module there, naming the
# wrong commit and calling a clean tree dirty. Outside a git checkout (a tarball
# build) REVISION is empty, and an unstamped revision is reported as unknown
# rather than as clean — the dirty stamp is only read alongside a revision.
# No build timestamp is injected, deliberately: dev builds stay reproducible.
#
# REVISION_DIRTY emits NOTHING when `git status` itself fails, so the binary
# reports the tree state as unknown rather than as clean. The distinction is
# reachable: `git rev-parse` can succeed while `git status` fails (an unreadable
# .git/index, say), and the obvious `test -n "$(git status --porcelain)"` form
# cannot tell an empty stdout that means "clean" from an empty stdout that means
# "the command died" — it stamps a positive claim of cleanliness about a tree it
# never managed to inspect. The assignment's exit status is git's, so one
# invocation both gates and measures.
REVISION := $(shell git rev-parse HEAD 2>/dev/null)
REVISION_DIRTY := $(shell out=$$(git status --porcelain 2>/dev/null) && { test -n "$$out" && echo true || echo false; })
BUILD_CHANNEL := dev
LDFLAGS := -X github.com/plumbkit/plumb/internal/cli.Version=$(VERSION) \
-X github.com/plumbkit/plumb/internal/cli.Revision=$(REVISION) \
-X github.com/plumbkit/plumb/internal/cli.RevisionDirty=$(REVISION_DIRTY) \
-X github.com/plumbkit/plumb/internal/cli.BuildChannel=$(BUILD_CHANNEL)
# macOS-only codesign settings. CODESIGN_IDENTITY can be:
# - unset/empty: ad-hoc sign (`-s -`). Gives the binary a stable identifier
# but the cdhash changes on every rebuild, so macOS may still re-prompt
# for TCC consent (Documents, Pictures, …) after each rebuild.
# - the name of a self-signed cert in your login keychain (recommended for
# local dev): TCC keys grants to the cert's Designated Requirement, so
# grants survive rebuilds. Create one via Keychain Access:
# Keychain Access → Certificate Assistant → Create a Certificate
# Name: plumb-dev Identity Type: Self Signed Root
# Certificate Type: Code Signing
# Then build with: CODESIGN_IDENTITY=plumb-dev make build
# - a real Apple Developer ID identity (for distribution).
UNAME_S := $(shell uname -s)
CODESIGN_ID := $(if $(CODESIGN_IDENTITY),$(CODESIGN_IDENTITY),-)
CODESIGN_BUNDLE := com.plumbkit.plumb
.PHONY: build tool-sizes web-ui web-ui-audit test test-pure test-race integration-test fuzz build-integration lint lint-tags lint-cross check-size check-brief check-changelog check-site-claims check-verify-disclosure check-changelog-placement check-changelog-placement-test check-changelog-headings-test check-pre-commit cover cover-report vuln tidy-check verify verify-full run clean tidy install install-hooks hooks codesign ts-wasm swift-wasm install-clients clients-test clients-test-auth clients-test-conformance build-clients docker-integration docker-cleanroom verify-full-container site blog demo-gif
$(TESTCACHE):
mkdir -p $(TESTCACHE)
build:
go build -ldflags "$(LDFLAGS)" -o $(BINARY) $(CMD)
ifeq ($(UNAME_S),Darwin)
@$(MAKE) --no-print-directory codesign
endif
# web-ui builds the embedded Svelte SPA into internal/web/ui/dist, which the
# Go binary //go:embed's via internal/web/assets.go. A committed placeholder
# index.html keeps a bare `go build` compiling, so this is only needed to pick
# up frontend changes; run it before `make build` after editing the SPA.
web-ui:
cd internal/web/ui && npm ci && npm run build
# codesign signs the built binary on macOS. Stable identifier (CODESIGN_BUNDLE)
# means TCC associates consent with "this thing called plumb" instead of with
# a raw file path; with a named identity it also survives rebuilds. On
# non-Darwin this is a no-op so the recipe is safe to call unconditionally.
codesign:
ifeq ($(UNAME_S),Darwin)
codesign --force --sign "$(CODESIGN_ID)" \
--identifier "$(CODESIGN_BUNDLE)" \
--preserve-metadata=entitlements,requirements,flags,runtime \
$(BINARY)
@codesign -dv $(BINARY) 2>&1 | sed 's/^/ /' || true
else
@echo "codesign: skipping on $(UNAME_S) (macOS-only)"
endif
# GO_TEST_TIMEOUT replaces go test's 10-minute default, which the cli package
# alone has come within a second of on a loaded machine; a slow CI runner
# (test-race especially) would fail on time, not on a test. integration-test
# shares it: its untagged half runs the same packages. Empty means the default.
GO_TEST_TIMEOUT ?= 20m
ifeq ($(strip $(GO_TEST_TIMEOUT)),)
GO_TEST_TIMEOUT := 20m
endif
export GO_TEST_TIMEOUT
test: $(TESTCACHE)
GOTMPDIR=$(CURDIR)/$(TESTCACHE) go test -timeout=$(GO_TEST_TIMEOUT) ./...
# test-pure builds and tests the tree the way the RELEASE is built rather than
# the way a developer machine is configured: CGO_ENABLED=0. Every other gate
# takes the host toolchain's default, and that default is CGO_ENABLED=1 wherever
# a C compiler is present — ubuntu-latest, this repo's own verify image, a Mac
# with Xcode. So a file behind `linux && !cgo` is in no gate's build at all,
# while being exactly the build that ships: .goreleaser.yml sets CGO_ENABLED=0.
#
# That blind spot is not hypothetical. It hid a blocking defect in
# home_linux_pure.go — PLAN-527's account lookup read only /etc/passwd and would
# have refused every NSS account, so no LDAP/SSSD/AD host could start a daemon,
# shipped on merge. The gate that would have caught it is the gate that compiles
# the file, which is why this target is a build before it is a test.
#
# `go build ./...` covers the whole class: no `!cgo` file anywhere in the tree
# can drift back out of every gate unnoticed. The account run then EXECUTES the
# pure-Go account path — platformHome through /etc/passwd and getent(1) — rather
# than only compiling it, on a real binary and a real name service.
#
# Linux-only, and it says so rather than passing quietly: darwin has no cgo/pure
# split (home_darwin.go carries no `!cgo` counterpart), so a green run here on a
# Mac would be evidence about nothing.
test-pure: $(TESTCACHE)
@[ "$$(go env GOOS)" = linux ] || { \
echo "test-pure: linux only (GOOS=$$(go env GOOS)); the darwin build has no cgo/pure split to cover" >&2; \
exit 1; }
CGO_ENABLED=0 GOTMPDIR=$(CURDIR)/$(TESTCACHE) go build ./...
CGO_ENABLED=0 GOTMPDIR=$(CURDIR)/$(TESTCACHE) go test -timeout=$(GO_TEST_TIMEOUT) ./internal/account/
test-race: $(TESTCACHE)
GOTMPDIR=$(CURDIR)/$(TESTCACHE) go test -race -timeout=$(GO_TEST_TIMEOUT) ./...
integration-test: $(TESTCACHE)
GOTMPDIR=$(CURDIR)/$(TESTCACHE) go test -tags=integration -timeout=$(GO_TEST_TIMEOUT) ./...
# fuzz runs every fuzz target in the tree for FUZZTIME each (default 60s).
#
# Targets are DISCOVERED, not listed. `go test` fuzzes one target per invocation
# (`-fuzz .` over ./... refuses with "matches more than one target"), so this has
# to iterate either way — and a hand-maintained list silently stops covering a
# target the day someone adds one, which is the failure mode a fuzzing target can
# least afford. Discovery also keeps this independent of which branch a given
# target happens to land on.
#
# Not in `verify`: fuzzing is time-boxed exploration, not a pass/fail gate, and
# the edit loop cannot afford it. The retained corpora under <pkg>/testdata/fuzz/
# ARE run by plain `make test` as ordinary cases, so every payload a fuzz run has
# already found stays a regression test whether or not anyone fuzzes again.
FUZZTIME ?= 60s
fuzz: $(TESTCACHE)
@set -e; \
found=0; \
for f in $$(grep -rEl '^func Fuzz[A-Za-z0-9_]*\(' --include='*_test.go' . | sort); do \
pkg=$$(dirname $$f); \
for fn in $$(grep -hoE '^func Fuzz[A-Za-z0-9_]*' $$f | sed 's/^func //' | sort -u); do \
found=1; \
echo "==> fuzzing $$fn in $$pkg for $(FUZZTIME)"; \
GOTMPDIR=$(CURDIR)/$(TESTCACHE) go test $$pkg -run '^$$' -fuzz "^$$fn$$" -fuzztime $(FUZZTIME); \
done; \
done; \
if [ $$found -eq 0 ]; then echo "no fuzz targets found"; exit 1; fi
# build-integration compiles and vets the //go:build integration files, which
# test/lint skip without the tag — catching an integration-only compile error or
# an uncommitted integration helper locally, before CI's integration job. (The
# gap that let 0.8.1 commit a cmd/smoke that did not build under the tag.)
build-integration: $(TESTCACHE)
GOTMPDIR=$(CURDIR)/$(TESTCACHE) go vet -tags=integration ./...
# install-clients installs the MCP client CLIs the clientsmoke harness drives
# (idempotent; never configures API keys). See scripts/install-clients.sh.
install-clients:
./scripts/install-clients.sh
# clients-test is the on-demand CONNECTION tier: it confirms each installed
# client CLI completes the MCP handshake with plumb, non-interactively and
# without API keys. Uninstalled clients (and those lacking an auth-free probe)
# are skipped. See cmd/clientsmoke.
clients-test: $(TESTCACHE)
GOTMPDIR=$(CURDIR)/$(TESTCACHE) go test -tags=clients -timeout=15m -v ./cmd/clientsmoke/...
# clients-test-auth is the LLM AUTH tier: it drives each client headless to force
# a real plumb tool call. Runs only the clients whose API key is exported (e.g.
# OPENAI_API_KEY for most; ANTHROPIC_API_KEY/GEMINI_API_KEY/CURSOR_API_KEY for
# claude/gemini/cursor); the rest skip. Costs money.
clients-test-auth: $(TESTCACHE)
GOTMPDIR=$(CURDIR)/$(TESTCACHE) go test -tags=clients_e2e -timeout=20m -v ./cmd/clientsmoke/...
# clients-test-conformance is the deterministic, API-key-free real-client tier.
# It requires the pinned Codex and OpenCode binaries, drives both through a
# loopback scripted provider, and isolates the Go cache as well as all client and
# plumb state. Run it repeatedly before changing client capability evidence.
clients-test-conformance: $(TESTCACHE)
@cache=$$(mktemp -d); trap 'rm -rf "$$cache"' EXIT; \
GOCACHE="$$cache" GOTMPDIR=$(CURDIR)/$(TESTCACHE) \
go test -tags=clients_conformance -timeout=10m -count=1 -v ./cmd/clientsmoke/...
# build-clients compiles and vets every clientsmoke build tag, which test/lint
# skip — keeping the on-demand harness from bitrotting (mirrors build-integration).
build-clients: $(TESTCACHE)
GOTMPDIR=$(CURDIR)/$(TESTCACHE) go vet -tags=clients ./cmd/clientsmoke/...
GOTMPDIR=$(CURDIR)/$(TESTCACHE) go vet -tags=clients_e2e ./cmd/clientsmoke/...
GOTMPDIR=$(CURDIR)/$(TESTCACHE) go vet -tags=clients_conformance ./cmd/clientsmoke/...
# ── Docker-based Linux testing (opt-in; never part of `make verify`). ─────────
# plumb is developed on macOS; these run the Linux suites in a container so a
# macOS developer can reproduce them locally. arm64-native by default on Apple
# Silicon; set DOCKER_PLATFORM=linux/amd64 for amd64 fidelity (QEMU-emulated).
DOCKER_PLATFORM ?=
DOCKER_PLATFORM_FLAG := $(if $(DOCKER_PLATFORM),--platform $(DOCKER_PLATFORM),)
# docker-integration mirrors the CI `integration` job (real gopls + pyright) on
# Linux, locally. The repo is bind-mounted so the image always reflects the
# working tree; named volumes cache the Go build + module caches across reruns.
docker-integration:
docker build $(DOCKER_PLATFORM_FLAG) -f build/docker/integration.Dockerfile -t plumb-integration build/docker
docker run --rm --init $(DOCKER_PLATFORM_FLAG) \
-v "$(CURDIR)":/src \
-v plumb-gocache:/root/.cache/go-build \
-v plumb-gomod:/go/pkg/mod \
plumb-integration
# verify-full-container runs `make verify-full` in a Linux container, the local
# way to run the whole gate. The gate's daemon-starting suites (cmd/smoke,
# cmd/clientsmoke) refuse on a developer machine, which must run exactly one
# plumb daemon; they run only in CI, a container or a declared VM
# (internal/separateenv). The working tree streams in as git sees it; no plumb
# daemon may outlive the suite. See scripts/verify-full-container.sh.
verify-full-container:
DOCKER_PLATFORM=$(DOCKER_PLATFORM) ./scripts/verify-full-container.sh
# docker-cleanroom proves a fresh Debian with NO toolchain can install and run
# plumb end-to-end: a multi-stage build compiles the binary, then a slim runtime
# (bash + python3 only) drives the two-agents MCP demo. The demo's exit code is
# the verdict — the automatable form of "clean-VM verification" before tagging.
docker-cleanroom:
docker build $(DOCKER_PLATFORM_FLAG) -f build/docker/cleanroom.Dockerfile -t plumb-cleanroom --build-arg VERSION=$(VERSION) .
docker run --rm $(DOCKER_PLATFORM_FLAG) plumb-cleanroom
# lint runs golangci-lint through scripts/lint-with-retry.sh, which retries
# with bounded backoff on the shared-cache lock ("parallel golangci-lint is
# running") so a peer agent's lint does not read as a failure of this one.
lint:
./scripts/lint-with-retry.sh
# lint-tags lints and format-checks the files behind custom build tags, which a
# plain `make lint` never analyses: the integration suite, cmd/smoke, the
# clientsmoke tiers, and the parity and race variants (scripts/build-tags.sh
# finds the tags). They are linted together in one run, because the clientsmoke
# tiers share one harness and linting each tier alone reports the helpers the
# other tiers use as unused. A struct misaligned behind the clients_conformance
# tag sat on main for seven weeks before this existed.
lint-tags:
@tags=$$(./scripts/build-tags.sh) || exit 1; \
echo "lint-tags: linting files behind --build-tags=$$tags"; \
./scripts/lint-with-retry.sh --build-tags="$$tags"
# lint-cross lints the OTHER supported OS's tree. golangci-lint only analyses
# files that match the current GOOS, so a Linux `make lint` never sees
# sandbox_darwin*.go or process_darwin*.go (and vice versa) — a real blind spot:
# a linter change validated on one platform can pass locally and fail CI's other
# matrix leg. Static analysis only; it never runs the other platform's tests.
# It lints the other OS's tagged files too (lint-tags' list): a file behind
# `linux && integration` is otherwise seen by no macOS lint at all.
#
# Run it after changing .golangci.yml or touching platform-constrained code.
# Not in `verify` — it roughly doubles lint time to cover nine files, and CI's
# two-OS matrix is the backstop. Windows is deliberately absent: plumb does not
# support it (internal/session's flock has no Windows implementation), so a
# GOOS=windows pass would report known, intentional breakage.
lint-cross:
@other=$$(if [ "$$(go env GOOS)" = "darwin" ]; then echo linux; else echo darwin; fi); \
echo "lint-cross: linting the $$other tree (current GOOS=$$(go env GOOS))"; \
tags=$$(./scripts/build-tags.sh) || exit 1; \
GOOS=$$other golangci-lint run && GOOS=$$other golangci-lint run --build-tags="$$tags" && \
GOOS=$$other go vet ./... && \
echo "lint-cross: OK ($$other tree clean, tagged files included)"
# check-size fails if any Go file exceeds its line rule — 600 for source, 900 for
# tests (with a grandfather baseline for files still awaiting a split). Keeps the
# standard from regressing — see scripts/check-file-size.sh.
check-size:
./scripts/check-file-size.sh
# tool-sizes prints the per-tool tools/list size report (name, description,
# schema bytes), largest first, pinned tools marked. The test's own exit code
# is kept, so a broken report fails the target rather than printing nothing.
tool-sizes:
@out=$$(go test ./internal/tools/ -run '^TestToolSizeReport$$' -count=1 -v) || { echo "$$out"; exit 1; }; \
echo "$$out" | sed -n 's/^ //p'
# check-brief fails if AGENTS.md grows past its budget (150 lines / 20 KiB) —
# the brief is rules + pointers; reference detail lives in docs/. See
# scripts/check-agents-brief.sh.
check-brief:
./scripts/check-agents-brief.sh
# check-changelog fails if CHANGELOG.md has the same version number in more
# than one '## <version>' heading, or the topmost version repeats a '### '
# subsection or an entry — a rebase can replay an addition as a
# second heading without ever conflicting. See scripts/check-changelog-headings.sh.
check-changelog:
./scripts/check-changelog-headings.sh
# check-changelog-headings-test proves check-changelog rejects a replayed '### '
# subsection and a repeated entry in the topmost version.
check-changelog-headings-test:
./scripts/check-changelog-headings-test.sh
# check-site-claims fails when site copy's claims drift from the code they
# describe — footer stamp vs VERSION, hero counts vs the extractor table,
# tool-grid sums vs docs, adapter tiers vs the status table, and phantom
# subcommands. The same three drifts were fixed twice (June review, PR #429)
# before a guard existed. See scripts/check-site-claims.sh.
check-site-claims:
./scripts/check-site-claims.sh
# check-verify-disclosure fails if the `verify` recipe stops naming the suite it
# compiles but does not run (`make integration-test`) and the target that runs it
# (`verify-full`), or if verify-full stops depending on the suite. It pins those
# tokens, not the wording, so a reworded disclosure still passes. The disclosure
# is the fix for #473, and one nothing checks is one edit from silent removal. See
# scripts/check-verify-disclosure.sh.
check-verify-disclosure:
./scripts/check-verify-disclosure.sh
# check-changelog-placement is check-changelog's diff-shape complement: it fails when
# a change ADDS lines under any heading other than the one that was unreleased at the
# merge-base. Deliberately NOT in `verify` and not in the pre-commit hook — unlike
# every other guard here it needs a base ref, which a working tree does not have, and
# `verify` has to stay hermetic (offline, correct in a fresh or shallow clone). CI's
# verify job runs it as a step with the exact base SHA from the pull_request event;
# this target is for running it by hand against origin/main. See the script header.
check-changelog-placement:
./scripts/check-changelog-placement.sh
# The guard's own regression suite: ten real commits from this repository's history
# (four that shipped a misplaced entry, six that only look misplaced) plus four
# synthetic cases for the rules no real commit exercises. CI-only, for the same
# reason — it replays historical SHAs, so it needs full history.
check-changelog-placement-test:
./scripts/check-changelog-placement-test.sh
# check-pre-commit runs the pre-commit hook against a stub golangci-lint: it must
# pass --allow-parallel-runners (a peer's lint must not fail this commit) and must
# check formatting rather than rewrite files after the commit's content is chosen
# (#545). Hermetic and offline, so it is part of verify.
check-pre-commit:
./scripts/pre-commit-test.sh
# cover measures statement coverage and fails below the floor in
# scripts/check-coverage.sh. Not in `verify` — it re-runs the whole suite with
# instrumentation, so it would roughly double the local edit loop; CI runs it on
# every push. Use `make cover-report` locally to see where the gaps are.
cover:
./scripts/check-coverage.sh
cover-report:
./scripts/check-coverage.sh --report
# vuln scans the module graph against the Go vulnerability database. Pinned to
# @latest deliberately: the value is knowing about a CVE published this morning,
# and the tool's own version is not part of the build.
vuln:
go run golang.org/x/vuln/cmd/govulncheck@latest ./...
# web-ui-audit is vuln's npm counterpart for the embedded SPA. Two tiers, the
# same split CI's npm-audit job enforces: prod dependencies block at high
# (their code ships bundled in dist/), dev/build-time findings print but do
# not fail — advisories against build-time-only dependencies must not turn the
# tree red with no code change; the small prod tree still blocks, by the same
# posture as govulncheck. Reads the lockfile, so no npm ci first.
web-ui-audit:
cd internal/web/ui && npm audit --omit=dev --audit-level=high
-cd internal/web/ui && npm audit --audit-level=high
# tidy-check asserts go.mod/go.sum are already tidy. Note that `go mod tidy`
# mutates in place: on failure the tidied files are left in the working tree —
# deliberate, so the fix is to review and commit them.
tidy-check:
@go mod tidy
@git diff --exit-code -- go.mod go.sum \
|| { echo "go.mod/go.sum not tidy — 'make tidy' changed them; commit the result"; exit 1; }
@echo "tidy-check: OK (go.mod/go.sum unchanged by go mod tidy)"
run:
go run $(CMD)
clean:
rm -f $(BINARY)
tidy:
go mod tidy
# install builds plumb and copies the freshly-codesigned binary onto PATH, so the
# daemon no longer runs out of the build tree (where a rebuild or a stray git
# checkout could swap the live binary). It copies the signed artifact rather than
# using `go install`, so macOS TCC consent stays stable across rebuilds. After
# installing, restart the daemon (`plumb restart --force`) and re-run
# `plumb setup <client>` so client configs point at the installed path.
#
# It also records the installed binary in the account's service launch profile
# (`plumb service install`): an account-authority cold start launches the
# PROFILE's executable and refuses rather than guessing when there is no profile,
# so an install that skipped this would leave the next `plumb serve` unable to
# start a daemon. The command is idempotent and preserving — it updates the
# recorded executable and leaves a user's directories, PATH and XDG settings
# alone — which is the same contract Homebrew's post_install relies on. This is
# what `go install` users do NOT get, so the docs tell them to run
# `plumb service install` once.
install: build
@install -d "$(BINDIR)"
@install -m 0755 $(BINARY) "$(BINDIR)/$(BINARY)"
@echo "installed $$("$(BINDIR)/$(BINARY)" version 2>/dev/null | tail -1) -> $(BINDIR)/$(BINARY)"
@"$(BINDIR)/$(BINARY)" service install
@case ":$$PATH:" in \
*":$(PREFIX)/bin:"*) ;; \
*) printf 'note: %s is not on your PATH — add it (e.g. fish_add_path %s, or export PATH=\"%s:$$PATH\")\n' "$(PREFIX)/bin" "$(PREFIX)/bin" "$(PREFIX)/bin" ;; \
esac
# ts-wasm regenerates the embedded TypeScript/TSX tree-sitter wasm from the
# vendored C sources. Dev-only — requires `zig`; building/running plumb needs
# only Go + wazero. Run after updating the vendored grammar or runtime.
ts-wasm:
bash internal/topology/extractors/wasmts/csrc/build.sh
# swift-wasm regenerates the embedded Swift tree-sitter wasm (canonical
# alex-pinkus grammar + its C external scanner) from the vendored C sources.
# Dev-only — requires `zig`; building/running plumb needs only Go + wazero.
swift-wasm:
bash internal/topology/extractors/wasmts/csrc/build-swift.sh
# demo-gif records docs/demos/daemon-respawn.sh headlessly and renders it to
# docs/assets/daemon-respawn.gif (the README's crash-resilience demo asset).
# Dev-only — requires `asciinema` and `agg` (brew install agg / AUR
# asciinema-agg-bin); the demo itself needs only bash + python3 + the built
# binary. Deterministic size via --window-size; --speed/--last-frame-duration
# tune readability. Idempotent — rerun after changing the demo script.
demo-gif: build
@command -v asciinema >/dev/null || { echo "demo-gif: asciinema not installed"; exit 1; }
@command -v agg >/dev/null || { echo "demo-gif: agg not installed"; exit 1; }
@mkdir -p docs/assets
rm -f .testcache/demo.cast
PLUMB_BIN=$(CURDIR)/$(BINARY) asciinema rec --window-size 96x28 \
--command='./docs/demos/daemon-respawn.sh' .testcache/demo.cast
agg --speed 0.35 --last-frame-duration 6 --font-family "JetBrains Mono" \
.testcache/demo.cast docs/assets/daemon-respawn.gif
@rm -f .testcache/demo.cast
@ls -la docs/assets/daemon-respawn.gif
# site (re)generates the landing-page TUI demo videos (light + dark, webm + mp4)
# from the asciicast at site/plumb_tui.cast into site/. Re-record with `asciinema
# rec site/plumb_tui.cast` (use ~100x26; see docs in the script), then run `make site`.
# Dev-only — requires `agg` (brew install agg), `ffmpeg`, and the Nerd font.
site: blog
python3 scripts/build-tui-video.py
# blog renders the Markdown posts under site/blog/posts/ into styled HTML + the
# blog index (see scripts/build-blog.py). This is the same step CI runs before the
# Pages deploy. Needs Python 3.11+ and the deps in scripts/requirements.txt
# (pip install -r scripts/requirements.txt). Light — no agg/ffmpeg, unlike `site`.
blog:
python3 scripts/build-blog.py
# verify is the fast gate: build + test + lint, of the default and the tagged
# files (lint-tags) + an integration-tag COMPILE pass (build-integration) + the
# file-size, brief, changelog, site-claims and disclosure guards + go.mod
# tidiness. Coverage (`make cover`) and vulnerabilities
# (`make vuln`) are deliberately NOT here — the first doubles the suite runtime,
# the second needs the network; CI runs both on every push.
#
# verify does NOT run the integration suite, and says so on success. It stays fast
# on purpose: the suite needs language servers (gopls, pyright) and would add its
# whole runtime to every local loop, while CI already runs it in the separate
# `integration` job — itself a required check that installs those servers.
# `verify-full` is the same gates plus the suite, run once. A green `make verify`
# does not mean the integration suite passed.
#
# VERIFY_CHECKS is the check list verify and verify-full share. verify-full omits
# `test` because `go test -tags=integration ./...` already runs every non-tagged
# test too, so listing both would run the unit suite twice.
VERIFY_CHECKS := lint lint-tags build-integration build-clients check-size check-brief check-changelog check-site-claims check-verify-disclosure check-pre-commit tidy-check
verify: build test $(VERIFY_CHECKS)
@printf '\n%s\n%s\n%s\n%s\n%s\n\n' \
'verify: PASSED — but the //go:build integration suite was COMPILED, not RUN.' \
' A failing integration test does not fail this target.' \
' Its cmd/smoke suite (make integration-test) starts plumb daemons, so it runs' \
' in CI or a container, never beside the plumb daemon you are using.' \
' Whole gate, in Docker: make verify-full-container'
# verify-full is verify's gates plus the integration suite that verify only
# compiles. It runs the suite once, without verify's separate `test` step, because
# `-tags=integration ./...` already runs every non-tagged test. `integration-test`
# SKIPS tests whose language server is absent, so install gopls and pyright to run
# all of it. Use it as the pre-submit gate.
verify-full: build $(VERIFY_CHECKS) integration-test
# hooks is an alias for install-hooks — the ops-root Makefile uses `hooks-ops`
# for its own hook, and the asymmetry is a recurring stumble.
hooks: install-hooks
install-hooks:
@hooks="$$(git rev-parse --git-path hooks)"; \
mkdir -p "$$hooks"; \
cp scripts/pre-commit "$$hooks/pre-commit"; \
chmod +x "$$hooks/pre-commit"; \
echo "Pre-commit hook installed at $$hooks/pre-commit. Run 'make verify' before every push — and 'make verify-full-container' when the change touches integration-tagged code."