if tool_call: msgs.append(exc(res))

Self-documented Makefile

in London, GB

This isn’t a Make primer. Checkout https://makefiletutorial.com/ for that.

Here is my take on Make:

The problem with make is you have to read the Makefile to understand what are the possible tasks. This template fixes that.

For example, the actual Makefile that I used for supacode:

  1# Sensible defaults
  2.ONESHELL:
  3SHELL := bash
  4.SHELLFLAGS := -e -u -c -o pipefail
  5.DELETE_ON_ERROR:
  6MAKEFLAGS += --warn-undefined-variables
  7MAKEFLAGS += --no-builtin-rules
  8
  9# Derived values (DO NOT TOUCH).
 10CURRENT_MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST)))
 11CURRENT_MAKEFILE_DIR := $(patsubst %/,%,$(dir $(CURRENT_MAKEFILE_PATH)))
 12.DEFAULT_GOAL := help
 13
 14# VARIABLES
 15GHOSTTY_XCFRAMEWORK_PATH := $(CURRENT_MAKEFILE_DIR)/Frameworks/GhosttyKit.xcframework
 16GHOSTTY_RESOURCE_PATH := $(CURRENT_MAKEFILE_DIR)/Resources/ghostty
 17GHOSTTY_TERMINFO_PATH := $(CURRENT_MAKEFILE_DIR)/Resources/terminfo
 18GHOSTTY_BUILD_OUTPUTS := $(GHOSTTY_XCFRAMEWORK_PATH) $(GHOSTTY_RESOURCE_PATH) $(GHOSTTY_TERMINFO_PATH)
 19VERSION ?=
 20BUILD ?=
 21
 22.PHONY: serve build-ghostty-xcframework build-app run-app install-dev-build sync-ghostty-resources lint test update-wt bump-version bump-and-release install-git-hooks
 23
 24help:  # Display this help.
 25	@-+echo "Run make with one of the following targets:"
 26	@-+echo
 27	@-+grep -Eh "^[a-z-]+:.*#" $(CURRENT_MAKEFILE_PATH) | sed -E 's/^(.*:)(.*#+)(.*)/  \1 @@@ \3 /' | column -t -s "@@@"
 28
 29build-ghostty-xcframework: $(GHOSTTY_BUILD_OUTPUTS) # Build ghostty framework
 30
 31$(GHOSTTY_BUILD_OUTPUTS):
 32	@cd $(CURRENT_MAKEFILE_DIR)/ThirdParty/ghostty && mise exec -- zig build -Doptimize=ReleaseFast -Demit-xcframework=true -Dsentry=false
 33	rsync -a ThirdParty/ghostty/macos/GhosttyKit.xcframework Frameworks
 34	@src="$(CURRENT_MAKEFILE_DIR)/ThirdParty/ghostty/zig-out/share/ghostty"; \
 35	dst="$(GHOSTTY_RESOURCE_PATH)"; \
 36	terminfo_src="$(CURRENT_MAKEFILE_DIR)/ThirdParty/ghostty/zig-out/share/terminfo"; \
 37	terminfo_dst="$(GHOSTTY_TERMINFO_PATH)"; \
 38	mkdir -p "$$dst"; \
 39	rsync -a --delete "$$src/" "$$dst/"; \
 40	mkdir -p "$$terminfo_dst"; \
 41	rsync -a --delete "$$terminfo_src/" "$$terminfo_dst/"
 42
 43build-app: build-ghostty-xcframework # Build the macOS app (Debug)
 44	bash -o pipefail -c 'xcodebuild -project supacode.xcodeproj -scheme supacode -configuration Debug build CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" -skipMacroValidation 2>&1 | mise exec -- xcsift -qw --format toon'
 45
 46run-app: build-app # Build then launch (Debug) with log streaming
 47	@settings="$$(xcodebuild -project supacode.xcodeproj -scheme supacode -configuration Debug -showBuildSettings -json 2>/dev/null)"; \
 48	build_dir="$$(echo "$$settings" | jq -r '.[0].buildSettings.BUILT_PRODUCTS_DIR')"; \
 49	product="$$(echo "$$settings" | jq -r '.[0].buildSettings.FULL_PRODUCT_NAME')"; \
 50	exec_name="$$(echo "$$settings" | jq -r '.[0].buildSettings.EXECUTABLE_NAME')"; \
 51	"$$build_dir/$$product/Contents/MacOS/$$exec_name"
 52
 53install-dev-build: build-app # install dev build to /Applications
 54	@settings="$$(xcodebuild -project supacode.xcodeproj -scheme supacode -configuration Debug -showBuildSettings -json 2>/dev/null)"; \
 55	build_dir="$$(echo "$$settings" | jq -r '.[0].buildSettings.BUILT_PRODUCTS_DIR')"; \
 56	product="$$(echo "$$settings" | jq -r '.[0].buildSettings.FULL_PRODUCT_NAME')"; \
 57	src="$$build_dir/$$product"; \
 58	dst="/Applications/$$product"; \
 59	if [ ! -d "$$src" ]; then \
 60		echo "app not found: $$src"; \
 61		exit 1; \
 62	fi; \
 63	echo "copying $$src -> $$dst"; \
 64	rm -rf "$$dst"; \
 65	ditto "$$src" "$$dst"; \
 66	echo "installed $$dst"
 67
 68lint: # Run swiftlint
 69	mise exec -- swiftlint --quiet
 70
 71test: build-ghostty-xcframework
 72	xcodebuild test -project supacode.xcodeproj -scheme supacode -destination "platform=macOS" CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" -skipMacroValidation 2>&1
 73
 74format: # Swift format
 75	swift-format -p --in-place --recursive --configuration ./.swift-format.json supacode supacodeTests
 76	mise exec -- swiftlint --fix --quiet
 77
 78update-wt: # Download git-wt binary to Resources
 79	@mkdir -p "$(CURRENT_MAKEFILE_DIR)/Resources/git-wt"
 80	@curl -fsSL "https://raw.githubusercontent.com/khoi/git-wt/refs/heads/main/wt" -o "$(CURRENT_MAKEFILE_DIR)/Resources/git-wt/wt"
 81	@chmod +x "$(CURRENT_MAKEFILE_DIR)/Resources/git-wt/wt"
 82	@git add "$(CURRENT_MAKEFILE_DIR)/Resources/git-wt/wt"
 83	@git commit -m "update git-wt"
 84
 85bump-version: # Bump app version (usage: make bump-version [VERSION=x.x.x] [BUILD=123])
 86	@if [ -z "$(VERSION)" ]; then \
 87		current="$$(/usr/bin/awk -F' = ' '/MARKETING_VERSION = [0-9.]+;/{gsub(/;/,"",$$2);print $$2; exit}' "$(CURRENT_MAKEFILE_DIR)/supacode.xcodeproj/project.pbxproj")"; \
 88		if [ -z "$$current" ]; then \
 89			echo "error: MARKETING_VERSION not found"; \
 90			exit 1; \
 91		fi; \
 92		major="$$(echo "$$current" | cut -d. -f1)"; \
 93		minor="$$(echo "$$current" | cut -d. -f2)"; \
 94		patch="$$(echo "$$current" | cut -d. -f3)"; \
 95		version="$$major.$$minor.$$((patch + 1))"; \
 96	else \
 97		if ! echo "$(VERSION)" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$$'; then \
 98			echo "error: VERSION must be in x.x.x format"; \
 99			exit 1; \
100		fi; \
101		version="$(VERSION)"; \
102	fi; \
103	if [ -z "$(BUILD)" ]; then \
104		build="$$(/usr/bin/awk -F' = ' '/CURRENT_PROJECT_VERSION = [0-9]+;/{gsub(/;/,"",$$2);print $$2; exit}' "$(CURRENT_MAKEFILE_DIR)/supacode.xcodeproj/project.pbxproj")"; \
105		if [ -z "$$build" ]; then \
106			echo "error: CURRENT_PROJECT_VERSION not found"; \
107			exit 1; \
108		fi; \
109		build="$$((build + 1))"; \
110	else \
111		if ! echo "$(BUILD)" | grep -qE '^[0-9]+$$'; then \
112			echo "error: BUILD must be an integer"; \
113			exit 1; \
114		fi; \
115		build="$(BUILD)"; \
116	fi; \
117	sed -i '' "s/MARKETING_VERSION = [0-9.]*;/MARKETING_VERSION = $$version;/g" \
118		"$(CURRENT_MAKEFILE_DIR)/supacode.xcodeproj/project.pbxproj"; \
119	sed -i '' "s/CURRENT_PROJECT_VERSION = [0-9]*;/CURRENT_PROJECT_VERSION = $$build;/g" \
120		"$(CURRENT_MAKEFILE_DIR)/supacode.xcodeproj/project.pbxproj"; \
121	git add "$(CURRENT_MAKEFILE_DIR)/supacode.xcodeproj/project.pbxproj"; \
122	git commit -m "bump v$$version"; \
123	git tag -a "v$$version" -m "v$$version"; \
124	echo "version bumped to $$version (build $$build), tagged v$$version"
125
126bump-and-release: bump-version # Bump version and push tags to trigger release
127	git push --follow-tags

The usage is simple: add # comments to public targets, so when you run make you’ll see this self-documented doc.

1❯❯ make
2Run make with one of the following targets:
3
4  help:          Display this help.
5  serve:         Serve the server
6  build:         Build the site
7  new:           make new "your post title"

Having something like that in the team allows you to have straight to the point answers when people ask you on how to run things: