-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathMakefile
More file actions
278 lines (252 loc) · 14 KB
/
Copy pathMakefile
File metadata and controls
278 lines (252 loc) · 14 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
# withful — thin build/test tooling. Self-contained (no scripts/ dir needed).
# Mirrors gaurava's lane names so muscle memory transfers: gen / build / test /
# ui-screenshots / run.
# Maintainer-specific signing values (team ID, provisioning profile UUIDs) live
# in an untracked Makefile.local — see README "Build it yourself". Forks don't
# need one: simulator lanes (gen/build/test/test-unit) require no signing.
-include Makefile.local
APP_NAME := withful
APP_SCHEME ?= withful
SANDBOX_SCHEME ?= withfulsandbox
CONFIGURATION ?= Debug
APPLE_TEAM_ID ?=
CLOUDKIT_CONTAINER ?= iCloud.com.nags.withful
APP_GROUP ?= group.com.nags.withful
BUNDLE_ID ?= com.nags.withful
SANDBOX_BUNDLE_ID ?= com.nags.withful.sandbox
PROJECT := $(APP_NAME).xcodeproj
DERIVED ?= build/DerivedData
ASC_APP_ID ?= 6782300577
ASC_TESTFLIGHT_GROUP ?=
TESTFLIGHT_INTERNAL_TESTING_ONLY ?= false
RELEASE_PROFILE_UUID ?=
RELEASE_PROFILE_SPECIFIER ?= withful App Store
RELEASE_PROFILE_PATH ?= $(HOME)/Library/MobileDevice/Provisioning Profiles/$(RELEASE_PROFILE_UUID).mobileprovision
WIDGET_BUNDLE_ID ?= com.nags.withful.withfulWidgets
WIDGET_RELEASE_PROFILE_UUID ?=
WIDGET_RELEASE_PROFILE_SPECIFIER ?= withful Widgets App Store
WIDGET_RELEASE_PROFILE_PATH ?= $(HOME)/Library/MobileDevice/Provisioning Profiles/$(WIDGET_RELEASE_PROFILE_UUID).mobileprovision
IOS_ARCHIVE_PATH ?= build/withful.xcarchive
IOS_EXPORT_PATH ?= build/export
IOS_EXPORT_OPTIONS ?= build/ExportOptions.generated.plist
IOS_IPA_PATH ?= $(IOS_EXPORT_PATH)/withful.ipa
ASC_OUTPUT ?= json
# Pick a destination: prefer a Booted simulator, else newest iPhone available.
SIM_NAME ?= auto
DESTINATION = $(shell \
booted=$$(xcrun simctl list devices booted | grep -Eo '\([0-9A-F-]{36}\)' | head -1 | tr -d '()'); \
if [ -n "$$booted" ]; then echo "platform=iOS Simulator,id=$$booted"; \
else echo "platform=iOS Simulator,name=iPhone 17 Pro"; fi)
# Strict warning/concurrency settings live in project.yml so Swift packages do
# not inherit app-only compiler flags.
STRICT =
.PHONY: help gen build test test-unit ui-screenshots run install-device install-device-sandbox install-device-onboarding localization-check release-readiness testflight bump-build implement-testflight clean cloudkit-checklist diagnose
.DEFAULT_GOAL := help
help:
@printf "%s\n" \
"withful targets:" \
" make gen Regenerate $(PROJECT) from project.yml (XcodeGen)" \
" make build Build the app for an iOS Simulator (strict flags)" \
" make test Build + run unit and UI tests (the gate)" \
" make test-unit Run only withfulTests (fast inner loop)" \
" make ui-screenshots Run the UI test screenshot lane (withfulUITests)" \
" make run Build, install, and launch on a Simulator" \
" make install-device Install the durable withful app on your iPhone (same bundle as TestFlight/App Store)" \
" make install-device-sandbox Install/reset withfulsandbox on your iPhone (CloudKit OFF)" \
" make install-device-onboarding Alias for install-device-sandbox" \
" make localization-check Validate catalogs and runtime-language guardrails" \
" make testflight Archive and upload the real withful app to TestFlight" \
" make implement-testflight Test, bump build, and upload the real withful app to TestFlight" \
" make clean Remove build/ derived data" \
" make cloudkit-checklist Print the one-time Apple portal + CloudKit steps"
gen:
@command -v xcodegen >/dev/null 2>&1 || { echo "Install XcodeGen: brew install xcodegen"; exit 1; }
@xcodegen generate
@printf "Generated %s\n" "$(PROJECT)"
build: gen
@xcodebuild \
-project $(PROJECT) \
-scheme $(APP_SCHEME) \
-configuration $(CONFIGURATION) \
-destination "$(DESTINATION)" \
-derivedDataPath $(DERIVED) \
$(STRICT) \
build
test: gen
@xcodebuild \
-project $(PROJECT) \
-scheme $(APP_SCHEME) \
-configuration $(CONFIGURATION) \
-destination "$(DESTINATION)" \
-derivedDataPath $(DERIVED) \
-collect-test-diagnostics on-failure \
$(STRICT) \
test
test-unit: gen
@xcodebuild \
-project $(PROJECT) \
-scheme $(APP_SCHEME) \
-configuration $(CONFIGURATION) \
-destination "$(DESTINATION)" \
-derivedDataPath $(DERIVED) \
-only-testing:withfulTests \
$(STRICT) \
test
# Screenshot/marketing lane: runs the UI-test target, which captures attachments
# via XCTAttachment. Export images from the .xcresult after the run.
ui-screenshots: gen
@xcodebuild \
-project $(PROJECT) \
-scheme $(APP_SCHEME) \
-configuration $(CONFIGURATION) \
-destination "$(DESTINATION)" \
-derivedDataPath $(DERIVED) \
-resultBundlePath build/ui-screenshots.xcresult \
-only-testing:withfulUITests \
$(STRICT) \
test
@printf "Screenshots captured in build/ui-screenshots.xcresult — export with:\n"
@printf " xcrun xcresulttool export attachments --path build/ui-screenshots.xcresult --output-path build/screenshots\n"
run: build
@APP=$$(find $(DERIVED)/Build/Products/$(CONFIGURATION)-iphonesimulator -maxdepth 1 -name '$(APP_NAME).app' | head -1); \
if [ -z "$$APP" ]; then echo "App not found — did the build succeed?"; exit 1; fi; \
UDID=$$(xcrun simctl list devices booted | grep -Eo '\([0-9A-F-]{36}\)' | head -1 | tr -d '()'); \
if [ -z "$$UDID" ]; then UDID=$$(xcrun simctl list devices available | grep -E 'iPhone' | grep -Eo '\([0-9A-F-]{36}\)' | head -1 | tr -d '()'); xcrun simctl boot "$$UDID"; fi; \
open -a Simulator; \
xcrun simctl install "$$UDID" "$$APP"; \
xcrun simctl launch "$$UDID" $(BUNDLE_ID)
# Build (Debug) + install + launch the durable withful app on connected device(s)
# via automatic signing (-allowProvisioningUpdates). Defaults to your iPhone so a
# paired iPad is left alone; set DEVICE_FILTER= (all) or DEVICE_FILTER=iPad.
# Data is preserved unless DEVICE_UNINSTALL_FIRST=1 is explicitly passed.
# Needs an Apple ID in Xcode > Settings > Apple Accounts. See
# scripts/device_install.sh.
DEVICE_FILTER ?= iPhone
DEVICE_UNINSTALL_FIRST ?= 0
ASC_KEY_ID ?=
ASC_ISSUER_ID ?=
ASC_KEY_PATH ?=
install-device:
@APP_SCHEME="$(APP_SCHEME)" APP_BUNDLE_ID="$(BUNDLE_ID)" CONFIGURATION=Debug APPLE_TEAM_ID="$(APPLE_TEAM_ID)" DEVICE_DERIVED="$(DERIVED)" DEVICE_INSTALL_ONLY="$(DEVICE_FILTER)" DEVICE_UNINSTALL_FIRST="$(DEVICE_UNINSTALL_FIRST)" ASC_KEY_ID="$(ASC_KEY_ID)" ASC_ISSUER_ID="$(ASC_ISSUER_ID)" ASC_KEY_PATH="$(ASC_KEY_PATH)" bash scripts/device_install.sh
# Side-by-side sandbox lane (CloudKit OFF, no App Group). This is intentionally
# disposable and resets by default; override with SANDBOX_UNINSTALL_FIRST=0 if a
# later sandbox test needs to keep local sandbox data. DEVICE_FILTER as above.
SANDBOX_UNINSTALL_FIRST ?= 1
install-device-sandbox:
@APP_SCHEME="$(SANDBOX_SCHEME)" APP_BUNDLE_ID="$(SANDBOX_BUNDLE_ID)" CONFIGURATION=Debug APPLE_TEAM_ID="$(APPLE_TEAM_ID)" DEVICE_DERIVED="$(DERIVED)-sandbox" DEVICE_INSTALL_ONLY="$(DEVICE_FILTER)" DEVICE_UNINSTALL_FIRST="$(SANDBOX_UNINSTALL_FIRST)" ASC_KEY_ID="$(ASC_KEY_ID)" ASC_ISSUER_ID="$(ASC_ISSUER_ID)" ASC_KEY_PATH="$(ASC_KEY_PATH)" bash scripts/device_install.sh
install-device-onboarding: install-device-sandbox
localization-check:
@python3 scripts/check_localization.py
release-readiness:
@set -eu; \
printf "Checking %s release readiness...\n" "$(APP_NAME)"; \
printf "ASC app ID: %s\n" "$(ASC_APP_ID)"; \
printf "Internal TestFlight group: %s\n" "$(ASC_TESTFLIGHT_GROUP)"; \
if [ -z "$(ASC_APP_ID)" ]; then printf "BLOCKED: ASC_APP_ID is missing.\n"; exit 1; fi; \
if [ ! -f "$(RELEASE_PROFILE_PATH)" ]; then printf "BLOCKED: profile not installed at %s\n" "$(RELEASE_PROFILE_PATH)"; exit 1; fi; \
if [ ! -f "$(WIDGET_RELEASE_PROFILE_PATH)" ]; then printf "BLOCKED: widget profile not installed at %s\n" "$(WIDGET_RELEASE_PROFILE_PATH)"; exit 1; fi; \
for profile_path in "$(RELEASE_PROFILE_PATH)" "$(WIDGET_RELEASE_PROFILE_PATH)"; do \
profile_plist="$$(mktemp)"; \
security cms -D -i "$$profile_path" > "$$profile_plist"; \
profile_name="$$(/usr/libexec/PlistBuddy -c 'Print :Name' "$$profile_plist")"; \
app_id="$$(/usr/libexec/PlistBuddy -c 'Print :Entitlements:application-identifier' "$$profile_plist" 2>/dev/null || true)"; \
groups="$$(/usr/libexec/PlistBuddy -c 'Print :Entitlements:com.apple.security.application-groups' "$$profile_plist" 2>/dev/null || true)"; \
printf "Profile: %s (%s)\n" "$$profile_name" "$$app_id"; \
if ! printf "%s\n" "$$groups" | grep -F "$(APP_GROUP)" >/dev/null; then \
printf "BLOCKED: profile %s does not include %s.\n" "$$profile_name" "$(APP_GROUP)"; \
rm -f "$$profile_plist"; \
exit 2; \
fi; \
rm -f "$$profile_plist"; \
done; \
app_profile_plist="$$(mktemp)"; \
trap 'rm -f "$$app_profile_plist"' EXIT; \
security cms -D -i "$(RELEASE_PROFILE_PATH)" > "$$app_profile_plist"; \
containers="$$(/usr/libexec/PlistBuddy -c 'Print :Entitlements:com.apple.developer.icloud-container-identifiers' "$$app_profile_plist" 2>/dev/null || true)"; \
dev_containers="$$(/usr/libexec/PlistBuddy -c 'Print :Entitlements:com.apple.developer.icloud-container-development-container-identifiers' "$$app_profile_plist" 2>/dev/null || true)"; \
aps="$$(/usr/libexec/PlistBuddy -c 'Print :Entitlements:aps-environment' "$$app_profile_plist" 2>/dev/null || true)"; \
if ! printf "%s\n" "$$containers" | grep -F "$(CLOUDKIT_CONTAINER)" >/dev/null; then printf "BLOCKED: app profile missing production CloudKit container.\n"; exit 2; fi; \
if ! printf "%s\n" "$$dev_containers" | grep -F "$(CLOUDKIT_CONTAINER)" >/dev/null; then printf "BLOCKED: app profile missing development CloudKit container.\n"; exit 2; fi; \
if [ "$$aps" != "production" ]; then printf "BLOCKED: app profile aps-environment is %s, expected production.\n" "$$aps"; exit 2; fi; \
printf "Release profiles include App Group, CloudKit, and production push.\n"
bump-build:
@CURRENT="$$(awk -F': ' '/CURRENT_PROJECT_VERSION:/ {print $$2; exit}' project.yml | tr -d '\"')"; \
case "$$CURRENT" in ''|*[!0-9]*) echo "Could not read numeric CURRENT_PROJECT_VERSION from project.yml" >&2; exit 2;; esac; \
NEXT=$$((CURRENT + 1)); \
perl -0pi -e "s/CURRENT_PROJECT_VERSION: $$CURRENT/CURRENT_PROJECT_VERSION: $$NEXT/g" project.yml; \
xcodegen generate >/dev/null; \
echo "Bumped iOS build $$CURRENT -> $$NEXT"
testflight: gen release-readiness
@if ! command -v asc >/dev/null 2>&1; then echo "Missing required command: asc" >&2; exit 2; fi
@mkdir -p "$(dir $(IOS_ARCHIVE_PATH))" "$(IOS_EXPORT_PATH)"
@xcodebuild \
-project "$(PROJECT)" \
-scheme "$(APP_SCHEME)" \
-configuration Release \
-destination "generic/platform=iOS" \
-archivePath "$(IOS_ARCHIVE_PATH)" \
DEVELOPMENT_TEAM="$(APPLE_TEAM_ID)" \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="Apple Distribution" \
clean archive
@/usr/libexec/PlistBuddy -c "Clear dict" "$(IOS_EXPORT_OPTIONS)" 2>/dev/null || true
@/usr/libexec/PlistBuddy -c "Add :destination string export" "$(IOS_EXPORT_OPTIONS)"
@/usr/libexec/PlistBuddy -c "Add :method string app-store-connect" "$(IOS_EXPORT_OPTIONS)"
@/usr/libexec/PlistBuddy -c "Add :signingStyle string manual" "$(IOS_EXPORT_OPTIONS)"
@/usr/libexec/PlistBuddy -c "Add :teamID string $(APPLE_TEAM_ID)" "$(IOS_EXPORT_OPTIONS)"
@/usr/libexec/PlistBuddy -c "Add :provisioningProfiles dict" "$(IOS_EXPORT_OPTIONS)"
@/usr/libexec/PlistBuddy -c "Add :provisioningProfiles:$(BUNDLE_ID) string $(RELEASE_PROFILE_SPECIFIER)" "$(IOS_EXPORT_OPTIONS)"
@/usr/libexec/PlistBuddy -c "Add :provisioningProfiles:$(WIDGET_BUNDLE_ID) string $(WIDGET_RELEASE_PROFILE_SPECIFIER)" "$(IOS_EXPORT_OPTIONS)"
@/usr/libexec/PlistBuddy -c "Add :stripSwiftSymbols bool true" "$(IOS_EXPORT_OPTIONS)"
@/usr/libexec/PlistBuddy -c "Add :testFlightInternalTestingOnly bool $(TESTFLIGHT_INTERNAL_TESTING_ONLY)" "$(IOS_EXPORT_OPTIONS)"
@/usr/libexec/PlistBuddy -c "Add :uploadSymbols bool true" "$(IOS_EXPORT_OPTIONS)"
@xcodebuild \
-exportArchive \
-archivePath "$(IOS_ARCHIVE_PATH)" \
-exportPath "$(IOS_EXPORT_PATH)" \
-exportOptionsPlist "$(IOS_EXPORT_OPTIONS)"
@if [ ! -f "$(IOS_IPA_PATH)" ]; then echo "IPA not found at $(IOS_IPA_PATH)" >&2; exit 2; fi
@asc publish testflight \
--app "$(ASC_APP_ID)" \
--ipa "$(IOS_IPA_PATH)" \
--group "$(ASC_TESTFLIGHT_GROUP)" \
--wait \
--output "$(ASC_OUTPUT)"
implement-testflight:
@$(MAKE) --no-print-directory test
@$(MAKE) --no-print-directory bump-build
@$(MAKE) --no-print-directory testflight
clean:
@rm -rf build
@printf "Cleaned build/\n"
diagnose:
@printf "Scheme: %s\n" "$(APP_SCHEME)"
@printf "Config: %s\n" "$(CONFIGURATION)"
@printf "Dest: %s\n" "$(DESTINATION)"
@printf "Container: %s\n" "$(CLOUDKIT_CONTAINER)"
@printf "AppGroup: %s\n" "$(APP_GROUP)"
@xcodegen --version 2>/dev/null || echo "xcodegen: NOT INSTALLED (brew install xcodegen)"
@xcodebuild -version | head -1
cloudkit-checklist:
@printf "%s\n" \
"withful — one-time capability setup (do this ONCE, by a human):" \
"" \
"Apple Developer portal (developer.apple.com -> Certificates, IDs & Profiles):" \
" 1. Identifiers -> + -> App IDs -> App. Bundle ID: $(BUNDLE_ID)." \
" 2. Enable capabilities: iCloud (CloudKit), Push Notifications, App Groups." \
" 3. iCloud Containers: + -> create '$(CLOUDKIT_CONTAINER)'. Assign it to the App ID." \
" (This container id is IMMUTABLE once data syncs — lock the app name first.)" \
" 4. App Groups: + -> create '$(APP_GROUP)'. Assign it to the App ID." \
" 5. Create an App Store distribution provisioning profile named" \
" 'withful App Store' that includes the iCloud + App Group entitlements," \
" then set PROVISIONING_PROFILE_SPECIFIER in project.yml Release config." \
"" \
"CloudKit Console (icloud.developer.apple.com/dashboard):" \
" 6. Select container '$(CLOUDKIT_CONTAINER)'." \
" 7. Run the app once in DEBUG so initializeCloudKitSchema() creates the" \
" Development schema (record types CD_Person, CD_Moment, CD_Reminder," \
" CD_AppSettings + the CD_subjects join). Confirm they appear under" \
" Development -> Schema -> Record Types." \
" 8. Before TestFlight: Schema -> Deploy Schema Changes... -> promote" \
" Development to PRODUCTION. (Production is additive-only forever after.)" \
" 9. The withfulsandbox target needs NONE of the above — it is local-only."