Self-documented Makefile
in London, GB
This isn’t a Make primer. Checkout https://makefiletutorial.com/ for that.
Here is my take on Make:
- Every engineer needs to learn at least one build / task runner.
makeships with macOS, Linux by default, so no installations, make it great for adoption within a team.
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:
