-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMakefile
More file actions
453 lines (392 loc) · 24.1 KB
/
Copy pathMakefile
File metadata and controls
453 lines (392 loc) · 24.1 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
# BMK MAKEFILE 3.14.0
# do not alter this file - it might be overwritten on new versions of BMK
# if You want to alter it, remove the first line # BMK MAKEFILE 1.0 - then it is a custom makefile and will not be overwritten
# bmk Makefile - thin wrapper using `uv tool upgrade` for persistent bmk
#
# Usage:
# make test # run test suite
# make test ARGS="--verbose" # forward extra FLAGS (bare --verbose would be parsed
# # by make itself, not forwarded: "unknown option")
# make bump-patch # bump patch version
# make push fix login bug # push with a plain one-line commit message
# make push MSG="fix(cli): x" # push with ANY message: punctuation, newlines, $ - all safe
# make custom deploy # run custom command
# make custom deploy --dry-run
#
# bmk is installed ONCE per machine, in uv's own tool dir, and holds NOTHING of this
# project: only bmk and its toolchain. Your project's dependencies live in the project's
# own venv (.venv), which bmk provisions and syncs from ./pyproject.toml, and that is the
# env your tests, pyright and pip-audit all run against.
#
# That separation is the point. bmk used to be installed together with the project's
# dependencies in one env that resolved them TOGETHER, and every consequence of that was
# a bug: a project dependency capping one of bmk's silently backtracked bmk to an ancient
# release; a yanked transitive dependency made bmk itself uninstallable, bricking `make`
# fleet-wide; and the tests ran in that co-resolved env while pyright and pip-audit
# inspected the project's real venv - so the suite and the audit described different
# environments. With no project dependencies in bmk's env, none of that can happen, and
# one shared env serves every repo (it is identical for all of them).
#
# The env is disposable - delete it and the next make rebuilds it. It is re-resolved on
# every invocation, so a new bmk release is picked up automatically.
#
# Arguments after the target name are forwarded automatically.
# You can also use ARGS="..." explicitly if preferred.
#
# For a COMMIT MESSAGE prefer MSG="..." over ARGS="..." - ARGS is re-parsed by bash, so
# punctuation like ( ) ; ` $ * breaks or executes, and a newline is impossible. See the
# "Commit messages" section below.
SHELL := /bin/bash
.DEFAULT_GOAL := help
# uv names the executable bmk.exe on Windows, and lays a tool env out like a venv, so its
# interpreter is under Scripts/ there and bin/ everywhere else.
ifeq ($(OS),Windows_NT)
BMK_EXE := .exe
BMK_ENV_BIN := Scripts
else
BMK_EXE :=
BMK_ENV_BIN := bin
endif
# ONE bmk per machine, in uv's own tool dir - not a copy per project.
#
# A per-project env was necessary only while the env also carried the PROJECT's
# dependencies: two projects then fought over one env, whichever ran `make` last winning
# (measured - a `six` project and a `chardet` project overwrote each other). bmk's env now
# holds bmk alone, so it is byte-identical for every repo and there is nothing left to
# collide. Sharing it costs nothing and stops ~300MB (mostly pyright's bundled Node) from
# being duplicated into all ~46 repos.
#
# Ask uv where it puts entry points rather than trusting PATH: `uv tool install` warns
# (and does nothing about it) when its bin dir is not on PATH, which is the default state
# on a fresh machine, and a bare `bmk` would then fail with "command not found" from a
# Makefile that had just installed it. Falling back to the bare name keeps the old
# behaviour if uv is too old to answer.
BMK_BIN_DIR := $(shell uv tool dir --bin 2>/dev/null)
BMK := $(if $(BMK_BIN_DIR),$(BMK_BIN_DIR)/bmk$(BMK_EXE),bmk$(BMK_EXE))
# The interpreter INSIDE bmk's env, which is a different directory from the entry-point bin
# dir above: `uv tool dir` is the tools root, `uv tool dir --bin` is where the shims go.
# Used only by the integrity check in _ensure_bmk.
BMK_TOOLS_DIR := $(shell uv tool dir 2>/dev/null)
BMK_PY := $(if $(BMK_TOOLS_DIR),$(BMK_TOOLS_DIR)/bmk/$(BMK_ENV_BIN)/python$(BMK_EXE),)
ARGS ?=
# ──────────────────────────────────────────────────────────────
# Commit messages: MSG= is the safe channel, ARGS= is not
# ──────────────────────────────────────────────────────────────
# A commit message is free-form PROSE, and prose does not survive a shell command line.
# make expands $(ARGS) into the recipe text and hands the RESULT to /bin/bash, which then
# applies its full grammar to words that were never escaped for it. The quotes you typed
# were eaten by your own shell long before make saw them. So a bare ARGS= message means
# bash parses your prose as code: "fix(cli): x" is a syntax error, "a; b" runs b, and a
# backtick or $(...) EXECUTES. A newline is the worst case - it ends the recipe LINE, so
# make commits a truncated subject on line 1 and then runs the rest as a command.
#
# MSG= avoids all of it by never touching a command line. make's `export` puts the value
# straight into the child process environment, where nothing is word-split or re-parsed,
# and bmk already prefers args -> BMK_COMMIT_MESSAGE -> prompt (git_ops.resolve_message).
# git commit -m accepts embedded newlines, so a MSG= body becomes a real commit body.
#
# make push MSG="fix(cli): subject line
#
# Body with (parens), a ; and a $HOME, all safe."
#
# $(value MSG) is deliberate: it yields the UNEXPANDED value, so a literal $ in the message
# survives. Plain $(MSG) would make-expand it first and silently turn $HOME into OME.
ifdef MSG
export BMK_COMMIT_MESSAGE := $(value MSG)
endif
# A newline in ARGS cannot be made safe (see above), so refuse it up front rather than
# commit half of it. $(error) fires during parsing, before any recipe runs, so nothing is
# staged, committed or pushed. This guard exists because the truncate-then-push failure is
# silent and has already shipped wrong commit messages more than once.
define _BMK_NEWLINE
endef
ifneq (,$(findstring $(_BMK_NEWLINE),$(ARGS)))
$(error ARGS contains a newline, which make cannot pass to a recipe safely. Use MSG="..." for a multi-line commit message)
endif
# ──────────────────────────────────────────────────────────────
# Ensure bmk (and ONLY bmk) is installed, once per machine
# ──────────────────────────────────────────────────────────────
# This runs before EVERY target, on purpose, so a new bmk release is picked up with
# nothing to remember and no version marker to go stale.
#
# It UPGRADES; it does not reinstall. That distinction is the whole point. `uv tool install
# --reinstall --force` tears the env down and rebuilds it on every single make, and that env
# is shared by every repo on the machine - so a make in repo B was deleting the
# site-packages out from under a bmk still RUNNING in repo A, minutes into its test suite.
# The symptom is an ImportError deep inside bmk's OWN dependencies (a vanished
# `pip_api._hash`, a half-written pyright typeshed) that clears by itself on a re-run, which
# is what makes it read as a flake rather than a bug. `uv tool upgrade` leaves the
# environment untouched when bmk is already current (measured: "Nothing to upgrade" in
# ~0.9s, directory mtimes unchanged), so the destructive window shrinks from every make to
# only an actual version change.
#
# It needs no --refresh flag, and REJECTS one (exit 2). `uv tool install` re-resolves
# against uv's CACHED index, which is why this recipe used to carry `--refresh-package bmk`
# (measured: with 3.8.0 on PyPI, `uv tool install "bmk>=3.7.1"` still installed 3.7.1).
# `uv tool upgrade` revalidates `pypi.org/simple/bmk/` on EVERY run with no freshness
# window, so a release published a minute ago is seen immediately. That also retires the
# `UV_OFFLINE` carve-out this block used to need: uv refused `--refresh` when offline, while
# `UV_OFFLINE=1 uv tool upgrade bmk` simply answers "Nothing to upgrade".
#
# The integrity check in front of it replaces a repair that used to be accidental: when
# every make rebuilt the env, a corrupted env silently fixed itself. Upgrading does not, so
# `python -m bmk_selfcheck` (shipped by bmk, stdlib-only, ~0.23s for ~14k files) compares
# every installed distribution's RECORD against the filesystem and fails if a package was
# only partially written. uv's copy fallback on a filesystem it cannot hardlink across has
# left `pip_api._hash` and `pydantic.functional_serializers` missing exactly this way, and
# that flavour does NOT clear on a re-run. On failure the recipe falls through to the full
# reinstall, which does rebuild. Net cost is still lower than before: ~1.15s here against
# ~2s of unconditional teardown.
#
# The check runs the interpreter directly rather than `bmk --version`: an import probe costs
# 1.3s (more than the upgrade it guards) and would have missed the real failure anyway,
# because bmk's startup never imports pip_api.
#
# NOTE what is NOT here: the project. bmk is installed on its own, so its env holds bmk's
# toolchain and nothing else. The project's dependencies live in the project's own venv
# (.venv), which bmk provisions from ./pyproject.toml and runs the tests, pyright and
# pip-audit against. Do NOT "restore" a `--with .` / `--with-editable ".[dev]"`: that is
# what made bmk and the project resolve TOGETHER, and it caused, in order - a project
# dependency capping one of bmk's silently backtracking bmk to an ancient release
# (codecov-cli capped click<8.3.0 against bmk's click>=8.4.2, pinning bmk at 3.1.7 with no
# error); a yanked transitive dependency making bmk itself uninstallable and bricking
# `make` in every repo; and the suite running in that co-resolved env while pyright and
# pip-audit inspected the project's real venv, so the tests and the audit described
# different environments. It also forced an env per project, duplicating ~300MB per repo.
#
# The rest of the recipe is load-bearing. Do not:
#
# * turn the upgrade back into an unconditional `uv tool install --reinstall`. That is the
# race described above, and it is not theoretical: it has produced failing gates in
# unrelated repos that looked like real test failures.
# * drop `--reinstall` from the FALLBACK attempts. `uv tool install` without it NO-OPS
# when the tool is already present, so the repair path would silently repair nothing.
# * drop `--force`. The entry point already exists in uv's bin dir on every rebuild.
# * add `2>/dev/null` to the upgrade or the installs. A real failure must reach the
# terminal; a suppressed one surfaces later, somewhere unrelated. (The integrity check
# is the one exception: its non-zero exit IS the signal, it prints its own diagnosis,
# and it must stay quiet about an env that is merely absent - a fresh machine has no
# tool env yet, and that is not an error, it is the first install.)
# * collapse the retry. It covers the transient __pycache__ removal race ("Directory not
# empty", os error 39). If BOTH attempts fail, make fails loudly - correct, because
# there is no safe degraded state to continue from.
# * add UV_TOOL_DIR/UV_TOOL_BIN_DIR back. uv's default location is shared by every repo,
# which is now correct: with no project dependencies in it, the env is identical for
# all of them and there is nothing left to collide.
#
# BMK_MIN floors the FALLBACK install. The upgrade path does not consult it: uv re-resolves
# the requirement recorded at install time, which is itself a `bmk>=...`, so it always lands
# on the newest release and clears the floor anyway.
BMK_MIN := 3.14.0
# Absent env, missing check, or damaged env - all three mean "do not upgrade, rebuild".
# `test -x` keeps a fresh machine quiet: there is no interpreter to run yet.
BMK_INTACT := $(if $(BMK_PY),test -x "$(BMK_PY)" && "$(BMK_PY)" -m bmk_selfcheck,false)
.PHONY: _ensure_bmk
_ensure_bmk:
@$(BMK_INTACT) && uv tool upgrade bmk \
|| uv tool install --reinstall --force "bmk>=$(BMK_MIN)" \
|| uv tool install --reinstall --force "bmk>=$(BMK_MIN)"
# ──────────────────────────────────────────────────────────────
# Argument forwarding via MAKECMDGOALS
# ──────────────────────────────────────────────────────────────
# Allows natural argument passing: make push fix login bug
# instead of: make push ARGS="fix login bug"
# All targets that accept trailing arguments
_BMK_TARGETS := test t test-all test-human th testintegration testi ti testintegration-human tih \
codecov coverage cov \
build bld clean cln cl clean-all run ensure \
bump-major bump-minor bump-patch bump \
commit c push psh p release rel r ship sh \
dependencies deps d dependencies-update \
config config-deploy config-generate-examples \
send-email send-notification custom \
info logdemo
ifneq (,$(filter $(_BMK_TARGETS),$(firstword $(MAKECMDGOALS))))
# Capture everything after the first word as extra arguments
_EXTRA := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
# Append to ARGS (so explicit ARGS="..." still works alongside)
override ARGS += $(_EXTRA)
endif
# ──────────────────────────────────────────────────────────────
# Test & Quality
# ──────────────────────────────────────────────────────────────
.PHONY: test t
test: _ensure_bmk ## Run test suite [alias: t]
$(BMK) test $(ARGS)
t: _ensure_bmk
$(BMK) test $(ARGS)
.PHONY: test-all
test-all: _ensure_bmk ## Run pytest + pyright on every declared Python version (matrix)
$(BMK) test-all $(ARGS)
.PHONY: test-human th
test-human: _ensure_bmk ## Run test suite with human-readable output [alias: th]
$(BMK) test --human $(ARGS)
th: _ensure_bmk
$(BMK) test --human $(ARGS)
.PHONY: testintegration testi ti
testintegration: _ensure_bmk ## Run integration tests only [aliases: testi, ti]
$(BMK) testintegration $(ARGS)
testi ti: _ensure_bmk
$(BMK) testintegration $(ARGS)
.PHONY: testintegration-human tih
testintegration-human: _ensure_bmk ## Run integration tests with human-readable output [alias: tih]
$(BMK) testintegration --human $(ARGS)
tih: _ensure_bmk
$(BMK) testintegration --human $(ARGS)
.PHONY: codecov coverage cov
codecov: _ensure_bmk ## Upload coverage report to Codecov [aliases: coverage, cov]
$(BMK) codecov $(ARGS)
coverage cov: _ensure_bmk
$(BMK) codecov $(ARGS)
# ──────────────────────────────────────────────────────────────
# Build & Clean
# ──────────────────────────────────────────────────────────────
.PHONY: build bld
build: _ensure_bmk ## Build wheel and sdist artifacts [alias: bld]
$(BMK) build $(ARGS)
bld: _ensure_bmk
$(BMK) build $(ARGS)
.PHONY: clean cln cl
clean: _ensure_bmk ## Remove build artifacts and caches [aliases: cln, cl]
$(BMK) clean $(ARGS)
cln cl: _ensure_bmk
$(BMK) clean $(ARGS)
.PHONY: clean-all
clean-all: _ensure_bmk ## Remove build artifacts, caches AND every virtual environment (.venv*)
$(BMK) clean-all $(ARGS)
# ──────────────────────────────────────────────────────────────
# Run
# ──────────────────────────────────────────────────────────────
.PHONY: run
run: _ensure_bmk ## Run the project CLI
$(BMK) run $(ARGS)
.PHONY: ensure
ensure: _ensure_bmk ## Install missing external tools for this OS
$(BMK) ensure $(ARGS)
# ──────────────────────────────────────────────────────────────
# Version Bumping
# ──────────────────────────────────────────────────────────────
.PHONY: bump-major
bump-major: _ensure_bmk ## Bump major version (X+1).0.0
$(BMK) bump major $(ARGS)
.PHONY: bump-minor
bump-minor: _ensure_bmk ## Bump minor version X.(Y+1).0
$(BMK) bump minor $(ARGS)
.PHONY: bump-patch
bump-patch: _ensure_bmk ## Bump patch version X.Y.(Z+1)
$(BMK) bump patch $(ARGS)
.PHONY: bump
bump: bump-patch ## Bump patch version (default for bump)
# ──────────────────────────────────────────────────────────────
# Git Operations
# ──────────────────────────────────────────────────────────────
# The "$(ARGS)" quoting on commit/push is LOAD-BEARING - do not "tidy" it back to a bare
# $(ARGS) to match the other targets. These two take a MESSAGE and nothing else (both
# CLIs declare it as nargs=-1 with no options), so passing it as one quoted word costs
# nothing: bmk does
# " ".join(args).strip(), which round-trips a single arg unchanged, and empty ARGS still
# yields "" and falls through to BMK_COMMIT_MESSAGE / the prompt. What it buys is that
# bash stops parsing the message as code, so "fix(cli): x", "a; b" and *globs* survive.
# Flag-taking targets (test, run, custom, ...) must stay UNQUOTED - quoting them would
# collapse "--human -k foo" into a single argv element and break them.
.PHONY: commit c
commit: _ensure_bmk ## Create a git commit with timestamped message [alias: c]
$(BMK) commit "$(ARGS)"
c: _ensure_bmk
$(BMK) commit "$(ARGS)"
.PHONY: push psh p
push: _ensure_bmk ## Run tests, commit, and push to remote [aliases: psh, p]
$(BMK) push "$(ARGS)"
psh p: _ensure_bmk
$(BMK) push "$(ARGS)"
.PHONY: release rel r
release: _ensure_bmk ## Create a versioned release (tag + GitHub release) [aliases: rel, r]
$(BMK) release $(ARGS)
rel r: _ensure_bmk
$(BMK) release $(ARGS)
# ship stays UNQUOTED although it does take a commit message, because unlike commit/push it
# also takes options (--ci-workflow, --release-workflow); one quoted word would swallow them.
# So give ship its message via MSG="..." (the env channel) and keep ARGS for its flags.
.PHONY: ship sh
ship: _ensure_bmk ## Push, wait for CI, release, wait for release CI (CI-gated) [alias: sh]
$(BMK) ship $(ARGS)
sh: _ensure_bmk
$(BMK) ship $(ARGS)
# ──────────────────────────────────────────────────────────────
# Dependencies
# ──────────────────────────────────────────────────────────────
.PHONY: dependencies deps d
dependencies: _ensure_bmk ## Check and list project dependencies [aliases: deps, d]
$(BMK) dependencies $(ARGS)
deps d: _ensure_bmk
$(BMK) dependencies $(ARGS)
.PHONY: dependencies-update
dependencies-update: _ensure_bmk ## Update dependencies to latest versions
$(BMK) dependencies update $(ARGS)
# ──────────────────────────────────────────────────────────────
# Configuration
# ──────────────────────────────────────────────────────────────
.PHONY: config
config: _ensure_bmk ## Show current merged configuration
$(BMK) config $(ARGS)
.PHONY: config-deploy
config-deploy: _ensure_bmk ## Deploy configuration to system/user directories
$(BMK) config-deploy $(ARGS)
.PHONY: config-generate-examples
config-generate-examples: _ensure_bmk ## Generate example configuration files
$(BMK) config-generate-examples $(ARGS)
# ──────────────────────────────────────────────────────────────
# Email
# ──────────────────────────────────────────────────────────────
.PHONY: send-email
send-email: _ensure_bmk ## Send an email via configured SMTP
$(BMK) send-email $(ARGS)
.PHONY: send-notification
send-notification: _ensure_bmk ## Send a plain-text notification email
$(BMK) send-notification $(ARGS)
# ──────────────────────────────────────────────────────────────
# Custom Commands
# ──────────────────────────────────────────────────────────────
.PHONY: custom
custom: _ensure_bmk ## Run a custom command (make custom <name> [args...])
$(BMK) custom $(ARGS)
# ──────────────────────────────────────────────────────────────
# Info & Demos
# ──────────────────────────────────────────────────────────────
.PHONY: info
info: _ensure_bmk ## Print resolved package metadata
$(BMK) info $(ARGS)
.PHONY: logdemo
logdemo: _ensure_bmk ## Run logging demonstration
$(BMK) logdemo $(ARGS)
.PHONY: version-current
version-current: _ensure_bmk ## Print current version
$(BMK) --version
# ──────────────────────────────────────────────────────────────
# Development
# ──────────────────────────────────────────────────────────────
.PHONY: dev
dev: ## Install package with dev extras (editable)
uv pip install -e ".[dev]"
.PHONY: install
install: ## Editable install (no dev extras)
uv pip install -e .
# ──────────────────────────────────────────────────────────────
# Help
# ──────────────────────────────────────────────────────────────
.PHONY: help
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-26s\033[0m %s\n", $$1, $$2}' | \
sort
# ──────────────────────────────────────────────────────────────
# No-op overrides for trailing argument words (MUST be last)
# ──────────────────────────────────────────────────────────────
# Placed after all real target definitions so the no-op recipes
# override them. This prevents "make push codecov fix" from
# executing the real codecov target - "codecov" is an argument
# to push, not a separate command.
ifneq (,$(_EXTRA))
$(_EXTRA):
@:
endif