-
Notifications
You must be signed in to change notification settings - Fork 94
Expand file tree
/
Copy pathMakefile
More file actions
311 lines (267 loc) · 14.9 KB
/
Copy pathMakefile
File metadata and controls
311 lines (267 loc) · 14.9 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
# Common GitX developer commands. Run `make help` for the list.
#
# Targets deliberately mirror the steps in .github/workflows/BuildPR.yml so the
# two stay in sync; the CI step each one matches is named below.
# Changing a command here means changing it there too, and the other way round.
#
# deps "pre build"
# unit-test "Run unit tests"
# all-tests "Run tests"
# archive "Build project"
# package-signed "Prepare artifact"
#
# `pre-build` is `deps` plus the submodule checkout that CI gets from its own
# checkout step, so a fresh local clone wants pre-build and CI wants deps.
#
# `ui-test` runs the UI tests on their own. CI's "Run tests" runs the whole
# scheme, repeating the unit tests it has already run in its own step, so
# `all-tests` is the one that matches CI today and `ui-test` is the narrower
# target to reach for otherwise.
#
# Signing: without a Dev.xcconfig the build is signed ad-hoc, and the hardened
# runtime rejects that, leaving the app unable to load its own frameworks. So
# `dmg` builds with the hardened runtime off and stays runnable either way,
# while `archive` and `dmg-signed` keep it and want a real identity, which the
# README explains how to set up; `make Dev.xcconfig` writes one for you. The
# tests need no identity at all.
#
# Override the architecture on an Intel Mac or for a cross build:
# make build ARCH=x86_64
ARCH ?= $(shell uname -m)
WORKSPACE := GitX.xcworkspace
SCHEME := GitX
DESTINATION := platform=macOS,arch=$(ARCH)
BUILD_DIR := build
ARCHIVE := $(BUILD_DIR)/GitX.xcarchive
APP := $(BUILD_DIR)/GitX.app
DMG := $(BUILD_DIR)/GitX-$(ARCH).dmg
# Repo the UI screenshot tests open. CI points this at a fixed snapshot so the
# screenshots stay comparable; locally this checkout is good enough.
GITX_SCREENSHOT_REPO ?= $(CURDIR)
# Signing options for `dmg-signed`, in the format `xcodebuild -exportArchive`
# expects. Not in the repo: create your own, or export one from Xcode.
EXPORT_OPTIONS ?= ExportOptions.plist
# Extra build settings for the archive, as `xcodebuild` NAME=VALUE arguments.
ARCHIVE_SETTINGS ?=
# Where `dmg-signed` exports the signed app, and the zip it packs alongside the
# disk image. CI overrides both, since it notarizes the app where it lands.
EXPORT_DIR ?= $(BUILD_DIR)/export
ZIP ?= $(BUILD_DIR)/GitX-$(ARCH).zip
# Set to anything to have the packaging commands name every file they pack.
VERBOSE ?=
ZIP_QUIET := $(if $(VERBOSE),,-q)
# Set to a path to have xcodebuild write an .xcresult bundle, which is where CI
# reads the screenshots back out of a test run.
RESULT_BUNDLE ?=
RESULT_BUNDLE_ARG := $(if $(RESULT_BUNDLE),-resultBundlePath $(RESULT_BUNDLE))
XCODEBUILD := xcodebuild -workspace $(WORKSPACE) -scheme $(SCHEME) ARCHS="$(ARCH)"
MAKEFILE := $(firstword $(MAKEFILE_LIST))
# Column the map target lines its descriptions up in.
MAP_WIDTH := 24
# The tests sign ad-hoc, so they drop the hardened runtime too: a Dev.xcconfig
# turns it on, and it refuses to map an ad-hoc signed framework into the host.
TEST_SETTINGS := CODE_SIGN_IDENTITY="-" ENABLE_HARDENED_RUNTIME=NO
# Asked of xcodebuild: DerivedData holds a GitX-* directory per checkout path.
BUILD_PRODUCTS_ROOT = $(XCODEBUILD) -showBuildSettings 2>/dev/null \
| awk -F' = ' '/ BUILD_DIR /{ print $$2; exit }'
FRAMEWORK_GIT2_VERSION := ObjectiveGit.framework/Headers/git2/version.h
LIBGIT2_VERSION_HEADER := External/objective-git/External/libgit2/include/git2/version.h
# xcodebuild's output runs to tens of thousands of lines, so a warning from
# before the build has long scrolled past by the time anything reads it. Report
# it again at the end, keeping the status the build itself returned.
CHECK_AGAIN = status=$$?; \
$(MAKE) --no-print-directory git-submodule-check framework-check; \
exit $$status
.PHONY: help git-submodule-sync git-submodule-check framework-check \
framework-clean deps pre-build bootstrap \
build unit-test test \
ui-test all-tests archive build-project app smoke-test run dmg map \
export-signed \
package-signed \
dmg-signed clean git-clean-dry-run
help: ## Show this help
@grep -hE '^[A-Za-z][A-Za-z0-9_.-]*:.*## ' $(MAKEFILE_LIST) \
| awk -F':.*## ' '{printf " %-20s %s\n", $$1, $$2}'
# Reads the edges out of make's own rule database, so a target that gains a
# prerequisite appears here without anyone maintaining a second copy of the
# graph. The descriptions alongside are the help text above, in a column.
map: ## Show which targets pull in which, with the help text
@{ make -pnr -f $(MAKEFILE) 2>/dev/null \
| sed -n 's/^\([a-zA-Z][A-Za-z0-9_.-]*\):\([^=]*\)$$/EDGE \1\2/p' \
| grep -v '^EDGE $(MAKEFILE)' | sort -u; \
sed -n 's/^\([a-zA-Z][A-Za-z0-9_.-]*\):.*## \(.*\)$$/DESC \1 \2/p' $(MAKEFILE_LIST); } \
| awk -v width=$(MAP_WIDTH) '$$1 == "EDGE" { target = $$2; $$1 = ""; $$2 = ""; sub(/^ +/, ""); prerequisites[target] = $$0; order[++found] = target; next } \
$$1 == "DESC" { target = $$2; $$1 = ""; $$2 = ""; sub(/^ +/, ""); description[target] = $$0; next } \
function label(indent, name, left) { \
left = indent name; \
return (name in description ? sprintf("%-" width "s - %s", left, description[name]) : left) \
} \
function walk(name, indent, i, count, needs) { \
count = split(prerequisites[name], needs, " "); \
for (i = 1; i <= count; i++) { print label(indent "\\_ ", needs[i]); walk(needs[i], indent " ") } \
} \
END { \
if (found == 0) exit 1; \
print "Make Targets Map _______________________________________________________________"; print ""; \
print "Targets that pull something in _________"; print ""; \
for (i = 1; i <= found; i++) if (prerequisites[order[i]] != "") { print label("", order[i]); walk(order[i], ""); print "" } \
print "Targets that stand alone _______________"; print ""; \
for (i = 1; i <= found; i++) if (prerequisites[order[i]] == "") print label("", order[i]) \
}' \
|| { echo "map: no targets found in $(MAKEFILE)" >&2; exit 1; }
# A real file, not a phony target, so that make leaves an existing config
# alone rather than writing over settings you may have edited by hand.
Dev.xcconfig: ## Write the local signing settings from the keychain certificate
scripts/make-dev-xcconfig.sh $@
git-submodule-sync: ## Check out the submodules at the revisions this tree wants
git submodule sync
git submodule update --init --recursive
# Locally this warns and carries on: parking a submodule on a commit of your
# own is a normal thing to be doing, and the build that follows may well be
# testing exactly that. On CI it is a defect rather than a choice, since the
# checkout step is the only thing that puts submodules in place, so there it
# fails the build rather than whispering into a log nobody reads. Recursive,
# since libgit2 sits under objective-git and its pin is the one that leaves a
# stale framework behind in a build directory.
#
# GitHub reads its annotations from stdout, and %0A is how one carries a
# newline.
git-submodule-check: ## Report a submodule that is not at the revision this tree wants
@drifted=$$(git submodule status --recursive 2>/dev/null | sed -n 's/^[+-]//p'); \
test -n "$$drifted" || exit 0; \
list=$$(echo "$$drifted" | awk '{ print $$2 " is at " substr($$1, 1, 8) }'); \
if [ -n "$$GITHUB_ACTIONS" ]; then \
summary="the checkout left submodules that are not at the recorded revisions"; \
echo "::error title=Submodule drift::$$(printf '%s\n%s\n' "$$summary" "$$list" \
| awk '{ printf "%s%s", separator, $$0; separator = "%0A" }')"; \
{ echo "error: $$summary:"; echo "$$list" | sed 's/^/ /'; } >&2; \
exit 1; \
fi; \
{ echo "warning: submodules are not at the revisions this tree wants:"; \
echo "$$list" | sed 's/^/ /'; \
echo 'run `make git-submodule-sync` to check them out'; } >&2
# The compile meets both sets of git2 headers and dies before the phase that
# refreshes the copy runs, so a build directory in this state cannot recover.
framework-check: ## Report a built framework whose libgit2 headers the tree has moved past
@test -f $(LIBGIT2_VERSION_HEADER) || exit 0; \
level=warning; test -z "$$GITHUB_ACTIONS" || level=error; \
for built in $$($(BUILD_PRODUCTS_ROOT))/*/$(FRAMEWORK_GIT2_VERSION); do \
test -f "$$built" && ! cmp -s "$$built" $(LIBGIT2_VERSION_HEADER) || continue; \
echo "$$level: stale libgit2 headers in $$built" >&2; \
stale=1; \
done; \
test -n "$$stale" || exit 0; \
echo 'run `make framework-clean` before building' >&2; \
test -z "$$GITHUB_ACTIONS" || { echo "::error::stale libgit2 headers"; exit 1; }
framework-clean: ## Drop built ObjectiveGit.frameworks so the next build recopies their headers
@for framework in $$($(BUILD_PRODUCTS_ROOT))/*/ObjectiveGit.framework; do \
test -d "$$framework" || continue; \
echo "Removing $$framework"; \
rm -rf "$$framework"; \
done
deps: ## Build the objective-git and libgit2 dependencies
cd External/objective-git && script/bootstrap && script/update_libgit2
# CI gets the submodules from actions/checkout and so calls `deps` on its own;
# a fresh local clone needs both halves.
pre-build: git-submodule-sync deps ## Check out the submodules, then build the dependencies
bootstrap: pre-build ## (alias)
build: git-submodule-check framework-check ## Build the app for local use
@start_time=$$(date +%s); \
$(XCODEBUILD) -destination "$(DESTINATION)" build; status=$$?; \
$(MAKE) --no-print-directory git-submodule-check framework-check; \
elapsed=$$(($$(date +%s) - start_time)); \
printf '\n⏱ make build finished in %dm %02ds (exit %d)\n' $$((elapsed/60)) $$((elapsed%60)) $$status; \
exit $$status
unit-test: git-submodule-check framework-check ## Run the unit tests, needing no signing, repo or network
$(XCODEBUILD) -destination "$(DESTINATION)" \
-only-testing:GitXTests $(TEST_SETTINGS) $(RESULT_BUNDLE_ARG) test; \
$(CHECK_AGAIN)
test: unit-test ## (alias)
ui-test: git-submodule-check framework-check ## Run the UI tests that drive the app and take the screenshots
$(XCODEBUILD) -destination "$(DESTINATION)" \
-only-testing:GitXUITests $(TEST_SETTINGS) \
GITX_SCREENSHOT_REPO="$(GITX_SCREENSHOT_REPO)" $(RESULT_BUNDLE_ARG) test; \
$(CHECK_AGAIN)
# Runs the unit tests a second time, since the scheme tests every target. That
# is what CI's "Run tests" step does today, and this target exists to match it.
all-tests: git-submodule-check framework-check ## Run every test target in the scheme, screenshots included
$(XCODEBUILD) -destination "$(DESTINATION)" \
$(TEST_SETTINGS) \
GITX_SCREENSHOT_REPO="$(GITX_SCREENSHOT_REPO)" $(RESULT_BUNDLE_ARG) test; \
$(CHECK_AGAIN)
# Only for the goals that need the real identity: CI and `dmg` sign ad-hoc.
ifneq (,$(filter smoke-test dmg-signed,$(MAKECMDGOALS)))
archive: Dev.xcconfig
endif
archive: git-submodule-check framework-check ## Build a release GitX.xcarchive, which the dmg targets export from
$(XCODEBUILD) -archivePath $(ARCHIVE) $(ARCHIVE_SETTINGS) archive; $(CHECK_AGAIN)
build-project: archive ## (alias)
app: archive ## Copy the app out of the archive to build/GitX.app
rm -rf $(APP)
cp -R $(ARCHIVE)/Products/Applications/GitX.app $(APP)
# Covers what no test does: the Release build turns the hardened runtime on,
# and library validation then refuses to map a framework whose team differs
# from the tool loading it. Reads the output rather than the exit status,
# since gitx exits 1 after printing its version, while a bundle it cannot
# load dies in dyld before main and prints nothing. Wants the real identity
# `archive` builds with, so an ad-hoc archive proves nothing here.
#
# The guard keeps that from passing silently: asking for `dmg` in the same
# invocation turns the hardened runtime off for the archive they share, and
# without it library validation never runs and the check proves nothing.
smoke-test: app ## Check the packaged gitx tool can load the app frameworks
@codesign -dv --verbose=2 "$(APP)" 2>&1 | grep -q "flags=.*runtime" \
|| { echo "$(APP) carries no hardened runtime; run smoke-test on its own"; exit 1; }
"$(APP)/Contents/Resources/gitx" --version | grep -q "GitX version"
# Runs the Debug build, not the archive: Release turns on the hardened runtime,
# and an ad-hoc signature plus the hardened runtime leaves the app unable to
# load its own frameworks. -n forces a new instance, since an installed GitX
# claims the same bundle id and `open` would just bring that one to the front.
run: build ## Open the app that was just built
open -n "$$($(XCODEBUILD) -showBuildSettings \
| awk -F' = ' '/ BUILT_PRODUCTS_DIR /{print $$2}')/GitX.app"
# Drops the hardened runtime so that an ad-hoc signed image still runs; with a
# real identity to hand, `make dmg ARCHIVE_SETTINGS=` keeps it instead.
dmg: ARCHIVE_SETTINGS = ENABLE_HARDENED_RUNTIME=NO
dmg: app ## Package build/GitX.app into an unsigned disk image that runs locally
rm -rf $(BUILD_DIR)/dist $(DMG)
mkdir $(BUILD_DIR)/dist
cp -R $(APP) $(BUILD_DIR)/dist/
ln -s /Applications $(BUILD_DIR)/dist/
hdiutil create -fs HFS+ -srcfolder $(BUILD_DIR)/dist -volname GitX $(DMG)
rm -rf $(BUILD_DIR)/dist
# Clears the exported app rather than the directory holding it, since CI
# exports into the checkout itself and that is not ours to delete.
export-signed: ## Export the signed app from an archive that already exists
@test -f $(EXPORT_OPTIONS) \
|| { echo "No $(EXPORT_OPTIONS); see EXPORT_OPTIONS in the Makefile"; exit 1; }
rm -rf $(EXPORT_DIR)/GitX.app $(BUILD_DIR)/dist $(DMG) $(ZIP)
mkdir -p $(EXPORT_DIR)
xcodebuild -exportArchive -archivePath $(ARCHIVE) \
-exportPath $(EXPORT_DIR) -exportOptionsPlist $(EXPORT_OPTIONS)
# Kept apart from the export so that notarization can staple the exported app
# before it is sealed into anything: a dmg or zip made ahead of the stapler
# carries no ticket, whatever is done to the app afterwards.
package-signed: ## Package the exported app (run export-signed first)
rm -rf $(BUILD_DIR)/dist $(DMG) $(ZIP)
mkdir -p $(BUILD_DIR)/dist
cp -R $(EXPORT_DIR)/GitX.app $(BUILD_DIR)/dist/
ln -s /Applications $(BUILD_DIR)/dist/
hdiutil create -fs HFS+ -srcfolder $(BUILD_DIR)/dist -volname GitX $(DMG)
rm -rf $(BUILD_DIR)/dist
# -y stores the symlinks rather than following them, which is what keeps
# the frameworks' Versions/Current a link and the signature verifiable.
cd $(EXPORT_DIR) && zip -r -y $(ZIP_QUIET) $(abspath $(ZIP)) GitX.app
# Packaging runs as its own make so that it cannot start before the archive
# has finished. CI archives in a step of its own and calls package-signed.
dmg-signed: archive ## Build and package a signed disk image and zip
$(MAKE) export-signed
$(MAKE) package-signed
clean: ## Remove the build directory and Xcode's build products
rm -rf $(BUILD_DIR)
$(XCODEBUILD) clean
# Lists only, and nothing depends on it: the real `git clean -Xdf` throws away
# every ignored file in the tree, not just the ones a build made, so deciding
# to run it is left to you.
git-clean-dry-run: ## List the ignored files a `git clean -Xdf` would remove
git clean -Xdn --exclude='!/Dev.xcconfig'