-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMakefile
More file actions
485 lines (415 loc) · 19.5 KB
/
Copy pathMakefile
File metadata and controls
485 lines (415 loc) · 19.5 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
# ============================================================================
# CONFIG
# ============================================================================
.PHONY: help version checkCleanGit open-github \
clean clean-build clean-artifacts clean-test \
bump-patch bump-minor bump-major \
check-uv install-uv list-uv \
uv-bootstrap-pythons uv-bootstrap uv-sync uv-sync-headless uv-sync-dev uv-sync-release uv-sync-local uv-editable uv-refresh \
uv-lint uv-format uv-typecheck uv-fullCheck \
uv-test uv-test-all uv-test-matrix \
uv-flush-cache uv-flush-envs uv-flush-pythons uv-flush-everything uv-nuke \
uv-lifecycle-test \
dev setup \
installDev e refresh \
test cleanRoomCleanup cleanRoomBootstrap cleanRoomPytest testInEnv \
build validateBuild release-test release \
nuke list
.DEFAULT_GOAL := help
# Load .env file if it exists
ifneq (,$(wildcard .env))
include .env
export
endif
# Defaults (overridable via .env — the user-editable surface). Keep in sync with .env.
PYTHONS ?= 3.10 3.11 3.12 3.13
DEFAULT_PYTHON ?= 3.13
PYTHON ?= python3
VENV ?= .cleanroom-venv
# Quality-target paths. ROOT half has NO src/ — its Python lives in hooks/ + tests/
# (see GAPS.md §6). The template half overrides these to src/. Overridable via .env.
PY_SRC ?= hooks
PY_TESTS ?= tests
PY_EXAMPLES ?= examples
PY_ALL ?= $(PY_SRC) $(PY_TESTS) $(PY_EXAMPLES)
# Derived
# Tool runner for uv- quality/test recipes. `--extra dev` ensures ruff/mypy/pytest are
# resolved (and installed if missing) from the "[dev]" extra even on a FRESH checkout —
# no reliance on a pre-existing .venv, rather than the ambient PATH.
UV := uv run --no-project
PIP := $(PYTHON) -m pip
BUMPVERSION := bumpversion --allow-dirty
REPO := $(notdir $(CURDIR))
UNAME_S := $(shell uname -s)
HR := ========================================
# Guard: DEFAULT_PYTHON must be one of the versions we test against.
ifeq ($(filter $(DEFAULT_PYTHON),$(PYTHONS)),)
$(error DEFAULT_PYTHON ($(DEFAULT_PYTHON)) is not in PYTHONS ($(PYTHONS)) — fix .env)
endif
# ============================================================================
# MARK: - Helpers ·
# ============================================================================
define uninstall_package_list
@$(1) | while read pkg; do \
[ -n "$$pkg" ] || continue; \
$(PIP) uninstall -y "$$pkg" 2>&1 \
|| echo "SKIPPED (system-managed): $$pkg"; \
done
endef
define print_packages
@echo "========================================"
@echo "$(1)"
@echo "========================================"
@$(2) list 2>/dev/null || echo "No packages or pip not available"
@echo
endef
# Roll HISTORY.md on a version bump: open a fresh dated section under
# [Unreleased] (folding the accumulated notes into the just-bumped version) and
# amend it into bump2version's commit so version + changelog move together.
# Keep-a-Changelog convention: the `## [Unreleased]` header is the anchor.
define roll_changelog
@ver=$$($(MAKE) -s version); day=$$(date +%F); \
awk -v v="$$ver" -v d="$$day" '\
{ print } \
/^## \[Unreleased\]/ && !seen { print ""; print "## [" v "] - " d; seen=1 }' \
HISTORY.md > HISTORY.md.tmp && mv HISTORY.md.tmp HISTORY.md; \
git add HISTORY.md; \
case "$$(git log -1 --pretty=%s)" in \
"Bump version:"*) git commit --amend --no-edit ;; \
*) git commit -m "Roll HISTORY.md for v$$ver" ;; \
esac
endef
# ============================================================================
# MARK: - HELP
# ============================================================================
help: ## Show this help
@echo "$(REPO) — make targets (bare = pip · uv-… = uv path)"
@echo "config: DEFAULT_PYTHON=$(DEFAULT_PYTHON) PYTHONS=$(PYTHONS)"
@echo ""
@awk 'BEGIN {FS = ":.*?## "} \
/^##@ / {printf "\n\033[1m%s\033[0m\n", substr($$0, 5); next} \
/^[a-zA-Z0-9_%-]+:.*?## / {printf " \033[36m%-26s\033[0m %s\n", $$1, $$2}' \
$(MAKEFILE_LIST)
# ============================================================================
# MARK: - COMMON · VERSION & GIT
# ============================================================================
##@ Common · Version & Git
version: ## Display the current project version
@$(PYTHON) -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])" 2>/dev/null \
|| grep -m1 '^version' pyproject.toml | cut -d'"' -f2
checkCleanGit: ## Guard: fail if the git working tree is dirty
@[ -z "$$(git status --porcelain)" ] || \
(echo "Working tree is dirty. Commit or stash changes first."; exit 1)
# Static pattern rule: all three documented parts share one recipe (`$*` = the
# part). Bump the version, then roll the changelog into the same commit.
bump-patch bump-minor bump-major: bump-%: ## Bump version (patch|minor|major) + roll HISTORY.md
$(BUMPVERSION) $*
$(call roll_changelog)
open-github: ## Open the GitHub repository in the default browser (macOS/Linux)
@remote=$$(git remote | head -1); \
[ -n "$$remote" ] || { echo "No git remote configured."; exit 1; }; \
url=$$(git remote get-url "$$remote" | sed -e 's|git@github.com:|https://github.com/|' -e 's|\.git$$||'); \
echo "Opening $$url"; \
if [ "$(UNAME_S)" = "Darwin" ]; then open "$$url"; \
elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$$url"; \
else echo "No browser opener found; visit: $$url"; fi
# ============================================================================
# MARK: - COMMON · CLEAN
# Base cleanup targets used by install, test, and CI workflows.
# ============================================================================
##@ Common · Clean
clean: clean-build clean-artifacts clean-test ## Remove all build, cache, and test artifacts
clean-build: ## Remove packaging and distribution artifacts
rm -rf build/ dist/ .eggs/
find . \( -name '*.egg-info' -o -name '*.egg' \) -exec rm -rf {} +
rm -f uv.lock
clean-artifacts: ## Remove Python bytecode and cache files
find . \( \
-name '*.pyc' -o \
-name '*.pyo' -o \
-name '*~' -o \
-name '__pycache__' \
\) -exec rm -rf {} +
clean-test: ## Remove test, coverage, and lint caches
rm -f .coverage
rm -rf \
htmlcov/ \
.pytest_cache/ \
.mypy_cache/ \
.ruff_cache/ \
.tox/ \
.nox/
# ============================================================================
# MARK: - UV · TOOLING
# ============================================================================
##@ UV · Tooling
check-uv: ## Check if uv is installed (guard for all uv- targets)
@command -v uv >/dev/null 2>&1 || { \
echo "ERROR: uv not found."; \
echo " Install it with: make install-uv"; \
echo " Or see: https://docs.astral.sh/uv/getting-started/installation/"; \
exit 1; }
install-uv: ## Install uv (brew on macOS, installer script on Linux)
ifeq ($(UNAME_S),Darwin)
@echo "Detected macOS - installing via Homebrew..."
@command -v brew >/dev/null 2>&1 || { echo "ERROR: Homebrew not found. Install from https://brew.sh"; exit 1; }
brew install uv
else ifeq ($(UNAME_S),Linux)
@echo "Detected Linux - installing via official installer..."
curl -LsSf https://astral.sh/uv/install.sh | sh
@echo ""
@echo "NOTE: You may need to add ~/.local/bin to your PATH:"
@echo ' export PATH="$$HOME/.local/bin:$$PATH"'
else
@echo "Unsupported OS: $(UNAME_S)"
@echo "Install manually: https://docs.astral.sh/uv/getting-started/installation/"
@exit 1
endif
@echo ""
@echo "uv installed successfully:"
@uv --version
list-uv: check-uv ## List uv envs, installed Pythons, packages, and cache info
@echo "$(HR)"; echo "UV VERSION"; echo "$(HR)"
@uv --version
@echo ""; echo "$(HR)"; echo "INSTALLED PYTHON VERSIONS"; echo "$(HR)"
@uv python list --only-installed
@echo ""; echo "$(HR)"; echo "PROJECT VIRTUAL ENVIRONMENTS"; echo "$(HR)"
@ls -d .venv 2>/dev/null || echo "No .venv found"
@ls -d .venvs/*/ 2>/dev/null || echo "No .venvs/ matrix environments found"
@echo ""; echo "$(HR)"; echo "INSTALLED PACKAGES (.venv)"; echo "$(HR)"
@uv pip list 2>/dev/null || echo "No packages or .venv not found"
@echo ""; echo "$(HR)"; echo "UV CACHE INFO"; echo "$(HR)"
@uv cache dir
@du -sh $$(uv cache dir) 2>/dev/null || echo "Cache empty or not accessible"
# ============================================================================
# MARK: - UV · BOOTSTRAP & SYNC
# ============================================================================
##@ UV · Bootstrap & Sync
uv-bootstrap-pythons: check-uv ## Install all configured Python versions via uv
uv python install $(PYTHONS)
# Dependency model (BKM; see GAPS §5 / spec §6): pyproject.toml declares dependency
# NAMES ONLY — never version-pinned (only the application layer pins; module-level pins
# cause conflicts). The requirements*.txt files carry pins and git-based pointers, and
# every install path — pip AND uv — leans on them: `-r requirements.txt` then the
# editable self-install. No `uv.lock`, no `lock`/compile target.
uv-bootstrap: check-uv uv-bootstrap-pythons ## Full bootstrap: pythons + venv + deps
uv venv --python $(DEFAULT_PYTHON)
uv pip install -r requirements.txt
uv pip install -e ".[dev]"
@echo ""
@echo "Bootstrap complete. Run 'make uv-test-all' to validate."
uv-sync: check-uv ## Sync all dependencies including dev (default dev workflow)
@[ -d ".venv" ] || uv venv --python $(DEFAULT_PYTHON)
uv pip install -r requirements.txt
uv pip install -e ".[dev]"
uv-sync-headless: check-uv ## Sync dependencies without dev/UI extras (headless deploy)
@[ -d ".venv" ] || uv venv --python $(DEFAULT_PYTHON)
uv pip install -r requirements.txt
uv pip install -e "."
uv-sync-dev: check-uv ## Sync dependencies with dev extras
@[ -d ".venv" ] || uv venv --python $(DEFAULT_PYTHON)
uv pip install -r requirements.txt
uv pip install -e ".[dev]"
uv-sync-release: check-uv ## Sync using tag-pinned release requirements (requirements-release.txt)
@[ -d ".venv" ] || uv venv --python $(DEFAULT_PYTHON)
uv pip install -r requirements-release.txt
uv pip install -e "."
uv-sync-local: check-uv ## Sync using local editable path overrides (requirements-local.txt)
@[ -d ".venv" ] || uv venv --python $(DEFAULT_PYTHON)
uv pip install -r requirements-local.txt
uv pip install -e ".[dev]"
dev: uv-sync ## One-command dev setup entrypoint (alias → uv-sync)
setup: dev ## One-command dev setup entrypoint (alias → uv-sync)
uv-editable: check-uv ## Install this package editable via uv (uv pip install -e .)
uv pip install -e .
uv-refresh: check-uv ## Clean cache + reinstall from requirements + upgrade editable dev
uv cache clean
uv pip install -r requirements.txt
uv pip install --upgrade -e ".[dev]"
# ============================================================================
# MARK: - UV · QUALITY
# ============================================================================
##@ UV · Quality
uv-lint: check-uv ## Run ruff linter (read-only; non-zero exit for CI)
$(UV) ruff check $(PY_ALL)
uv-format: check-uv ## Format code with ruff
$(UV) ruff format $(PY_ALL)
# KEEP --unsafe-fixes: intentional, added after findings on 2026-06-25.
# Do NOT strip in cleanup/refactor passes — the unsafe autofixes here are relied
# upon deliberately. Removing it has regressed this target before.
$(UV) ruff check --fix --unsafe-fixes $(PY_ALL)
uv-typecheck: check-uv ## Strict type check with mypy
$(UV) mypy $(PY_SRC) $(PY_TESTS) $(PY_EXAMPLES)
# ty (Astral's preview type-checker) is intentionally OUT for now (decision D1):
# it's pre-release and not wired into uv-fullCheck. Revisit when it stabilizes.
uv-fullCheck: check-uv uv-lint uv-typecheck uv-test ## lint + typecheck + tests
# ============================================================================
# MARK: - UV · TEST
# ============================================================================
##@ UV · Test
# Depends on uv-sync so a fresh checkout never tests an empty/stale .venv (no
# false-green no-op): the [dev] extra is installed from pyproject before pytest runs.
uv-test: check-uv uv-sync ## Run tests on DEFAULT_PYTHON (ensures a synced env first)
$(UV) pytest
uv-test-all: check-uv ## Run tests across all configured Python versions (.venvs/<ver>)
@failed=""; \
for py in $(PYTHONS); do \
echo ""; \
echo "========================================"; \
echo "Testing Python $$py"; \
echo "========================================"; \
venv=".venvs/$$py"; \
[ -d "$$venv" ] || uv venv --python $$py "$$venv"; \
if ( . "$$venv/bin/activate" && \
uv pip install -q -e ".[dev]" && \
python -m pytest ); then \
echo "PASS: Python $$py"; \
else \
echo "FAIL: Python $$py"; \
failed="$$failed $$py"; \
fi; \
done; \
echo ""; \
echo "========================================"; \
if [ -n "$$failed" ]; then \
echo "FAILED VERSIONS:$$failed"; \
echo "========================================"; \
exit 1; \
else \
echo "ALL PYTHON VERSIONS PASSED"; \
echo "========================================"; \
fi
uv-test-matrix: uv-bootstrap-pythons uv-test-all ## Ensure Pythons installed, then run all tests
# ============================================================================
# MARK: - UV · FLUSH / NUKE
# ============================================================================
##@ UV · Flush / Nuke
uv-flush-envs: ## Remove all virtual environments (.venv + .venvs/<ver>)
@echo ">> Removing virtual environments..."
rm -rf .venv
rm -rf .venvs
rm -rf .venv-py*
@echo "Virtual environments removed."
uv-flush-cache: check-uv ## Clean uv cache
@echo ">> Cleaning uv cache..."
uv cache clean
@echo "uv cache cleaned."
uv-flush-pythons: ## Remove uv-managed Python installs (NUCLEAR)
@echo "WARNING: This removes ALL uv-managed Python installations!"
@echo "Location: ~/.local/share/uv/python"
rm -rf ~/.local/share/uv/python
@echo "uv-managed Pythons removed."
uv-flush-everything: clean uv-flush-envs uv-flush-cache ## Full cleanup (keeps pythons)
@echo "Environment flushed. Run 'make uv-flush-pythons' separately for global Pythons."
uv-nuke: uv-flush-everything ## NUCLEAR: everything then prompt for Python removal
@echo ""
@echo ">> Running uv-nuke..."
@$(MAKE) uv-flush-pythons
@echo ""
@echo "Environment nuked. Run 'make uv-bootstrap' to rebuild from scratch."
uv-lifecycle-test: uv-flush-everything uv-bootstrap uv-test-all ## flush -> bootstrap -> test-all
@echo ">> Lifecycle test complete"
# ============================================================================
# MARK: - PIP · INSTALL
# ============================================================================
##@ PIP · Install
# Ambient-pip fallback (prefer the uv- path). Both pip and uv lean on the requirements
# file (BKM rule 4): install -r requirements.txt, then self-install the editable
# package. No --break-system-packages / --force-reinstall: use a venv (make uv-sync)
# rather than fighting an externally-managed interpreter.
installDev: clean ## Install dev dependencies with pip (-r requirements.txt + editable [dev])
$(PIP) install -r requirements.txt
$(PIP) install -e ".[dev]"
e: ## Install this package in editable mode (pip install -e .)
$(PIP) install -e .
refresh: ## Refresh pip packages: reinstall from requirements + upgrade editable dev
$(PIP) install -r requirements.txt
$(PIP) install --upgrade -e ".[dev]"
# ============================================================================
# MARK: - PIP · TEST
# ============================================================================
##@ PIP · Test
test: ## Run tests using the current Python environment
pytest
cleanRoomCleanup: ## Delete the clean-room venv ($(VENV))
rm -rf $(VENV) || true
# The clean room is an install path, so it obeys the same BKM rule as every other
# one (see the dependency-model comment above ##@ UV · Bootstrap): pyproject.toml
# declares dependency NAMES ONLY, and requirements.txt carries the pins and the
# git/path pointers. Installing ".[dev]" alone makes pip resolve those bare names
# against PyPI, which fails outright for any unpublished sibling dependency
# (`No matching distribution found`). Install the requirements file FIRST, then the
# package. Keep ".[dev]" NON-editable here — validating the real packaging path is
# this target's entire purpose.
cleanRoomBootstrap: cleanRoomCleanup ## Bootstrap the clean-room venv + deps (runs NO tests)
$(PYTHON) -m venv $(VENV)
. $(VENV)/bin/activate && \
which python3 && \
$(VENV)/bin/pip install -r requirements.txt && \
$(VENV)/bin/pip install ".[dev]"
@echo "Virtual env can be activated with 'source $(VENV)/bin/activate'"
cleanRoomPytest: ## Run pytest inside the clean-room venv
. $(VENV)/bin/activate && \
which $(PYTHON) && \
$(PYTHON) -m pytest
testInEnv: clean cleanRoomBootstrap cleanRoomPytest cleanRoomCleanup ## Full clean-room test
@echo ">> testInEnv completed"
# ============================================================================
# MARK: - PIP · BUILD & RELEASE
# ============================================================================
##@ PIP · Build & Release
build: clean-build ## Build sdist + wheel ($(PYTHON) -m build)
@echo "Building package..."
$(PYTHON) -m build
validateBuild: build ## Validate build artifacts with twine
@echo "Validating dist/ with twine..."
$(PYTHON) -m twine check dist/*
release-test: checkCleanGit validateBuild ## Dry-run publish to TestPyPI (clean tree only)
@echo "Uploading $(REPO) v$$($(MAKE) -s version) to TestPyPI..."
@$(PYTHON) -m twine upload --repository testpypi dist/*
# No CI-driven publish path exists: `.github/workflows/ci-cd.yml` runs quality
# checks and the Python compatibility matrix on PRs to dev/prod only — it does not
# tag or upload anything. Release is therefore a manual, human-run procedure;
# `make release` refuses to upload from an unreviewed working tree and prints that
# procedure instead of performing it.
release: validateBuild ## Refuse local upload; print the manual release procedure
@echo "No CI-driven publish path exists — release is a manual procedure."
@echo ""
@echo "To release $(REPO) v$$($(MAKE) -s version):"
@echo " 1. Bump the version (make bump-patch|bump-minor|bump-major) and merge to prod."
@echo " 2. Build + validate: make validateBuild."
@echo " 3. Upload by hand: python3 -m twine upload dist/*"
@echo ""
@echo "For a local pre-flight, use: make release-test (TestPyPI)."
@exit 1
# ============================================================================
# MARK: - PIP · FLUSH / LIST
# ============================================================================
##@ PIP · Flush / List
# `nuke` is the INFERIOR pip fallback (Lesson 2): it per-package-uninstalls from
# the AMBIENT interpreter ($(PIP)). Prefer `make uv-flush-envs` — deleting the
# venv dir is the reliable flush primitive. Use this only when you're stuck in a
# non-deletable (e.g. system) env. Non-editable URL/VCS installs are skipped.
nuke: ## Per-package uninstall from ambient env (inferior — prefer uv-flush-envs)
@echo "Uninstalling regular packages (skipping system-managed)..."
$(call uninstall_package_list,$(PIP) freeze --exclude-editable | grep -v ' @ ')
@echo "Uninstalling editable packages by name..."
$(call uninstall_package_list,$(PIP) list --editable --format=freeze | cut -d= -f1)
@echo "pip-nuke complete."
list: ## List pip packages in available environments
$(call print_packages,SYSTEM PYTHON PACKAGES,$(PIP))
@if [ -d ".venv" ]; then \
echo "$(HR)"; \
echo "VENV PACKAGES (.venv)"; \
echo "$(HR)"; \
.venv/bin/pip list 2>/dev/null || echo "No packages or pip not available"; \
echo; \
fi
@for venv in .venvs/*; do \
[ -d "$$venv" ] || continue; \
echo "$(HR)"; \
echo "VENV PACKAGES ($$venv)"; \
echo "$(HR)"; \
$$venv/bin/pip list 2>/dev/null || echo "No packages or pip not available"; \
echo; \
done