Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
481 lines (416 loc) · 20.6 KB
/
Copy pathMakefile
File metadata and controls
481 lines (416 loc) · 20.6 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
# =============================================
# Generic Android build/run workflow — no Android Studio required.
# Nothing here is project-specific: the app module, application id, launcher
# activity and ABI are all detected from the project it runs in.
#
# Canonical copy: ~/AndroidStudioProjects/android.mk
# Run from any project root: make -f ~/AndroidStudioProjects/android.mk refresh
# (the `make` shell function in ~/.bashrc does this for you)
# =============================================
# One per line: `A := B := C` would assign A the literal text "B := C".
ifdef NO_COLOR
GREEN :=
YELLOW :=
RED :=
BLUE :=
CYAN :=
DIM :=
BOLD :=
NC :=
else
GREEN := \033[0;32m
YELLOW := \033[1;33m
RED := \033[0;31m
BLUE := \033[0;34m
CYAN := \033[0;36m
DIM := \033[2m
BOLD := \033[1m
NC := \033[0m
endif
# Build flags come from each project's gradle.properties. Nothing is overridden
# here: user-home gradle.properties wins over the project one anyway, and on this
# box its -XX:MaxMetaspaceSize=1g matters — 512m GC-thrashes release builds.
GRADLE := ./gradlew
ADB := adb
# --- Project detection ----------------------------------------------------
# Immediate (:=) so each is resolved once per invocation; these are local file
# reads, unlike the adb calls below.
# The application module is the one declaring an applicationId; library modules
# never do. Falls back to "app".
APP_MODULE := $(shell find . -maxdepth 2 -name 'build.gradle*' -not -path './build/*' \
-exec grep -l 'applicationId' {} + 2>/dev/null | head -1 | xargs -r dirname | sed 's|^\./||')
APP_MODULE := $(if $(APP_MODULE),$(APP_MODULE),app)
APP_BUILD_FILE := $(firstword $(wildcard $(APP_MODULE)/build.gradle.kts $(APP_MODULE)/build.gradle))
MANIFEST := $(APP_MODULE)/src/main/AndroidManifest.xml
# Kotlin DSL uses `namespace = "x"`, Groovy `namespace "x"`.
NAMESPACE := $(shell grep -hoE '^[[:space:]]*namespace[[:space:]]*=?[[:space:]]*"[^"]+"' $(APP_BUILD_FILE) 2>/dev/null \
| head -1 | grep -oE '"[^"]+"' | tr -d '"')
# AGP writes the fully-resolved id (suffixes, flavors and all) next to the APK,
# so prefer it. The gradle read is only for a tree that has never been built.
DEBUG_META := $(APP_MODULE)/build/outputs/apk/debug/output-metadata.json
APP_ID := $(shell \
if [ -f "$(DEBUG_META)" ]; then \
grep -oE '"applicationId"[[:space:]]*:[[:space:]]*"[^"]+"' "$(DEBUG_META)" \
| head -1 | grep -oE '"[^"]+"$$' | tr -d '"'; \
elif [ -n "$(APP_BUILD_FILE)" ]; then \
base=$$(grep -hoE 'applicationId[[:space:]]*=?[[:space:]]*"[^"]+"' "$(APP_BUILD_FILE)" \
| head -1 | grep -oE '"[^"]+"' | tr -d '"'); \
sfx=$$(awk '/^[[:space:]]*debug[[:space:]]*\{/{d=1} d&&/applicationIdSuffix/{print;exit} d&&/^[[:space:]]*\}[[:space:]]*$$/{d=0}' \
"$(APP_BUILD_FILE)" | grep -oE '"[^"]+"' | tr -d '"'); \
echo "$$base$$sfx"; \
fi)
# The SOURCE manifest, deliberately: the merged one also carries LAUNCHER
# activities from libraries (LeakCanary registers one in debug), and those win
# over the real app if you launch by category instead of by component.
LAUNCH_CLASS := $(shell [ -f "$(MANIFEST)" ] && awk '\
/<activity/ {inact=1; name=""; main=0; lau=0} \
inact && name=="" && match($$0, /android:name="[^"]+"/) {name=substr($$0, RSTART+14, RLENGTH-15)} \
inact && /android.intent.action.MAIN/ {main=1} \
inact && /android.intent.category.LAUNCHER/ {lau=1} \
/<\/activity>/ {if (inact && main && lau && name!="") {print name; exit} inact=0} \
' "$(MANIFEST)")
# A leading-dot class is relative to the namespace, NOT to the application id —
# they differ whenever a debug build sets applicationIdSuffix.
LAUNCH_ACTIVITY := $(APP_ID)/$(if $(filter .%,$(LAUNCH_CLASS)),$(NAMESPACE)$(LAUNCH_CLASS),$(LAUNCH_CLASS))
# Only pass the ABI properties to projects that actually read them; an unused
# -P is harmless but needlessly perturbs the configuration cache.
HAS_DEBUG_ABI := $(shell grep -rlq 'debugAbi' --include='*.gradle.kts' --include='*.gradle' --include='gradle.properties' . 2>/dev/null && echo 1)
HAS_RELEASE_ABI := $(shell grep -rlq 'releaseAbi' --include='*.gradle.kts' --include='*.gradle' --include='gradle.properties' . 2>/dev/null && echo 1)
# Lazily expanded: only costs an adb call on targets that use it. Blank with no
# device, which leaves the project's own default in charge.
DEVICE_ABI = $(shell $(ADB) shell getprop ro.product.cpu.abi 2>/dev/null | tr -d '\r')
DEBUG_ABI = $(if $(HAS_DEBUG_ABI),-PdebugAbi=$(DEVICE_ABI),)
RELEASE_ABI_ONE = $(if $(HAS_RELEASE_ABI),-PreleaseAbi=$(DEVICE_ABI),)
RELEASE_ABI_ALL = $(if $(HAS_RELEASE_ABI),-PreleaseAbi=all,)
APK_DEBUG_DIR := $(APP_MODULE)/build/outputs/apk/debug
APK_RELEASE_DIR := $(APP_MODULE)/build/outputs/apk/release
AAB_DIR := $(APP_MODULE)/build/outputs/bundle/release
APKSIGNER = $(shell ls $(HOME)/Android/Sdk/build-tools/*/apksigner 2>/dev/null | sort -V | tail -1)
PROJECT := $(notdir $(CURDIR))
default: help
.PHONY: help
help:
@printf "$(BLUE)=============================================$(NC)\n"
@printf "$(BOLD)$(BLUE) $(PROJECT)$(NC) $(DIM)— no-Studio build workflow$(NC)\n"
@printf "$(BLUE)=============================================$(NC)\n\n"
@printf "$(BOLD)Every day$(NC)\n"
@printf " $(GREEN)make$(NC) $(BOLD)refresh$(NC) Rebuild changed code, install, relaunch $(DIM)(the main loop)$(NC)\n"
@printf " $(GREEN)make$(NC) $(BOLD)all$(NC) Alias for refresh\n"
@printf " $(GREEN)make$(NC) $(BOLD)restart$(NC) Relaunch on device, no build\n"
@printf " $(GREEN)make$(NC) $(BOLD)logs$(NC) Live logcat for this app only\n"
@printf " $(GREEN)make$(NC) $(BOLD)logs-error$(NC) Errors and crashes only\n\n"
@printf "$(BOLD)Building$(NC)\n"
@printf " $(GREEN)make$(NC) $(BOLD)assemble$(NC) Debug APK, no install\n"
@printf " $(GREEN)make$(NC) $(BOLD)release$(NC) $(BOLD)Signed$(NC) APK for this device's ABI $(DIM)(fast)$(NC)\n"
@printf " $(GREEN)make$(NC) $(BOLD)release-all$(NC) $(BOLD)Signed$(NC) APKs, every ABI $(DIM)(for publishing)$(NC)\n"
@printf " $(GREEN)make$(NC) $(BOLD)bundle$(NC) $(BOLD)Signed$(NC) .aab for Play\n"
@printf " $(GREEN)make$(NC) $(BOLD)rebuild$(NC) Clean + install + launch $(DIM)(cache gone weird)$(NC)\n\n"
@printf "$(BOLD)Install$(NC)\n"
@printf " $(GREEN)make$(NC) $(BOLD)install$(NC) Build + install debug\n"
@printf " $(GREEN)make$(NC) $(BOLD)reinstall$(NC) Uninstall first $(DIM)(fixes signature conflicts)$(NC)\n"
@printf " $(GREEN)make$(NC) $(BOLD)install-release$(NC) Install the signed APK\n"
@printf " $(GREEN)make$(NC) $(BOLD)uninstall$(NC) Remove this build\n\n"
@printf "$(BOLD)Tests$(NC)\n"
@printf " $(GREEN)make$(NC) $(BOLD)test$(NC) Unit tests $(DIM)(no device)$(NC)\n"
@printf " $(GREEN)make$(NC) $(BOLD)test-device$(NC) Instrumented tests $(DIM)(T=SomeTest to filter)$(NC)\n"
@printf " $(DIM)leaves the app uninstalled — run 'make refresh' after$(NC)\n"
@printf " $(GREEN)make$(NC) $(BOLD)lint$(NC) Android lint\n\n"
@printf "$(BOLD)CI (GitHub Actions)$(NC)\n"
@printf " $(GREEN)make$(NC) $(BOLD)ci$(NC) Recent workflow runs\n"
@printf " $(GREEN)make$(NC) $(BOLD)ci-watch$(NC) Watch the latest branch run\n"
@printf " $(GREEN)make$(NC) $(BOLD)ci-quality$(NC) Run the quality gate\n"
@printf " $(GREEN)make$(NC) $(BOLD)ci-release-dry$(NC) Build a draft GitHub Release $(DIM)(TAG=v3.4.0-rc1)$(NC)\n"
@printf " $(GREEN)make$(NC) $(BOLD)ci-release$(NC) Publish a public GitHub Release $(DIM)(CONFIRM=yes)$(NC)\n"
@printf " $(GREEN)make$(NC) $(BOLD)ci-log$(NC) Failed-step log $(DIM)(R=run-id)$(NC)\n"
@printf " $(GREEN)make$(NC) $(BOLD)ci-artifacts$(NC) Download run artifacts $(DIM)(R=run-id)$(NC)\n\n"
@printf "$(BOLD)RAM / cache$(NC)\n"
@printf " $(GREEN)make$(NC) $(BOLD)kill$(NC) Stop Gradle+Kotlin daemons $(DIM)(frees several GB)$(NC)\n"
@printf " $(GREEN)make$(NC) $(BOLD)ram$(NC) What is eating memory right now\n"
@printf " $(GREEN)make$(NC) $(BOLD)clean$(NC) Delete build outputs\n"
@printf " $(GREEN)make$(NC) $(BOLD)clean-cache$(NC) Drop the configuration cache only\n"
@printf " $(GREEN)make$(NC) $(BOLD)devices$(NC) List connected devices\n"
@printf " $(GREEN)make$(NC) $(BOLD)info$(NC) What this Makefile detected here\n\n"
@printf "$(DIM)Tip: close Android Studio first — it holds its own Gradle daemon.$(NC)\n"
@printf "$(BLUE)=============================================$(NC)\n"
# Detection is invisible until it is wrong; this makes it inspectable.
.PHONY: info
info:
@printf "$(BOLD)Project$(NC) %s\n" "$(PROJECT)"
@printf "$(BOLD)App module$(NC) %s\n" "$(APP_MODULE)"
@printf "$(BOLD)Namespace$(NC) %s\n" "$(NAMESPACE)"
@printf "$(BOLD)Application id$(NC) %s %s\n" "$(APP_ID)" \
"$$([ -f '$(DEBUG_META)' ] && printf '$(DIM)(from AGP metadata)$(NC)' || printf '$(DIM)(from build.gradle — not built yet)$(NC)')"
@printf "$(BOLD)Launch$(NC) %s\n" "$(LAUNCH_ACTIVITY)"
@printf "$(BOLD)Device ABI$(NC) %s\n" "$(DEVICE_ABI)"
@$(if $(HAS_DEBUG_ABI)$(HAS_RELEASE_ABI),\
printf "$(BOLD)ABI flags$(NC) %s\n" "$(strip $(DEBUG_ABI) $(RELEASE_ABI_ONE))",\
printf "$(BOLD)ABI flags$(NC) $(DIM)project declares no debugAbi/releaseAbi$(NC)\n")
# --- Guards ---------------------------------------------------------------
.PHONY: check-project
check-project:
@if [ ! -x "$(GRADLE)" ]; then \
printf "$(RED)No ./gradlew here — run this from an Android project root.$(NC)\n"; exit 1; fi
@if [ -z "$(APP_ID)" ]; then \
printf "$(RED)Could not work out the application id.$(NC)\n"; \
printf " -> Looked for applicationId in $(APP_BUILD_FILE)\n"; \
printf " -> Override it: $(BOLD)make refresh APP_ID=com.example.app$(NC)\n"; exit 1; fi
@if [ -z "$(LAUNCH_CLASS)" ]; then \
printf "$(RED)No MAIN/LAUNCHER activity found in $(MANIFEST).$(NC)\n"; \
printf " -> Override it: $(BOLD)make refresh LAUNCH_ACTIVITY=pkg/pkg.MainActivity$(NC)\n"; exit 1; fi
.PHONY: check-adb
check-adb:
@devices=$$($(ADB) devices 2>/dev/null | tr -d '\r' | sed '1d' | awk '$$2 == "device" {print $$1}'); \
if [ -z "$$devices" ]; then \
printf "$(YELLOW)No device — restarting adb...$(NC)\n"; \
$(ADB) kill-server >/dev/null 2>&1; $(ADB) start-server >/dev/null 2>&1; sleep 2; \
devices=$$($(ADB) devices 2>/dev/null | tr -d '\r' | sed '1d' | awk '$$2 == "device" {print $$1}'); \
if [ -z "$$devices" ]; then \
printf "$(RED)No Android device connected.$(NC)\n"; \
printf " -> Plug in the phone and enable $(BOLD)USB debugging$(NC).\n"; \
exit 1; \
fi; \
fi
# Fails in a second rather than after a long build. Only projects that keep
# signing in local.properties can be checked this way; for anything else the
# apksigner verify after the build is the real guarantee.
.PHONY: check-signing
check-signing:
@if grep -q '^storeFile=' local.properties 2>/dev/null; then \
missing=""; \
for k in storeFile storePassword keyAlias keyPassword; do \
grep -q "^$$k=" local.properties || missing="$$missing $$k"; \
done; \
if [ -n "$$missing" ]; then \
printf "$(RED)Signing incomplete in local.properties:$(NC)$$missing\n"; exit 1; fi; \
store=$$(grep '^storeFile=' local.properties | cut -d= -f2-); \
if [ ! -f "$$store" ] && [ ! -f "$(APP_MODULE)/$$store" ]; then \
printf "$(RED)Keystore not found: $$store$(NC)\n"; exit 1; fi; \
fi
# --- The main loop --------------------------------------------------------
.PHONY: refresh
refresh: check-project check-adb
@printf "$(YELLOW)$(PROJECT): building + installing$(if $(DEBUG_ABI), (ABI: $(DEVICE_ABI)))...$(NC)\n"
@$(GRADLE) :$(APP_MODULE):installDebug $(DEBUG_ABI) || { \
printf "$(RED)Install failed.$(NC)\n"; \
printf " -> A different signature, or a newer build already on the device\n"; \
printf " (VERSION_DOWNGRADE), both need it gone first: $(BOLD)make reinstall$(NC)\n"; \
printf " -> Currently installed: %s\n" \
"$$($(ADB) shell dumpsys package $(APP_ID) 2>/dev/null | grep -m1 versionCode | tr -d '\r' | sed 's/^ *//' || echo 'not installed')"; \
exit 1; \
}
@$(MAKE) --no-print-directory -f $(firstword $(MAKEFILE_LIST)) restart
.PHONY: all
all: refresh
.PHONY: run
run: check-project check-adb
@$(ADB) shell am start -n $(LAUNCH_ACTIVITY) >/dev/null 2>&1 \
&& printf "$(GREEN)Launched$(NC) $(DIM)$(LAUNCH_ACTIVITY)$(NC)\n" \
|| printf "$(RED)Launch failed — installed? (make install)$(NC)\n"
.PHONY: stop
stop: check-adb
@$(ADB) shell am force-stop $(APP_ID)
.PHONY: restart
restart: stop run
# --- Building -------------------------------------------------------------
.PHONY: assemble
assemble: check-project
@printf "$(YELLOW)Assembling debug APK...$(NC)\n"
@$(GRADLE) :$(APP_MODULE):assembleDebug $(DEBUG_ABI)
@printf "$(GREEN)APK:$(NC) $$(ls -1 $(APK_DEBUG_DIR)/*.apk 2>/dev/null | head -1)\n"
.PHONY: release
release: check-project check-signing
@printf "$(YELLOW)Building signed release$(if $(RELEASE_ABI_ONE), ($(DEVICE_ABI) only))...$(NC)\n"
@$(GRADLE) :$(APP_MODULE):assembleRelease $(RELEASE_ABI_ONE)
@$(MAKE) --no-print-directory -f $(firstword $(MAKEFILE_LIST)) verify-signature
.PHONY: release-all
release-all: check-project check-signing
@printf "$(YELLOW)Building signed release (all ABIs)...$(NC)\n"
@$(GRADLE) :$(APP_MODULE):assembleRelease $(RELEASE_ABI_ALL)
@$(MAKE) --no-print-directory -f $(firstword $(MAKEFILE_LIST)) verify-signature
.PHONY: bundle
bundle: check-project check-signing
@printf "$(YELLOW)Building signed App Bundle...$(NC)\n"
@$(GRADLE) :$(APP_MODULE):bundleRelease
@printf "$(GREEN)AAB:$(NC) $$(ls -1 $(AAB_DIR)/*.aab 2>/dev/null | head -1)\n"
# A "signed APK" target that never checks the signature is how unsigned builds ship.
.PHONY: verify-signature
verify-signature:
@found=0; \
for apk in $$(ls -1 $(APK_RELEASE_DIR)/*.apk 2>/dev/null); do \
found=1; size=$$(du -h "$$apk" | cut -f1); \
if [ -n "$(APKSIGNER)" ]; then \
if $(APKSIGNER) verify "$$apk" >/dev/null 2>&1; then \
printf "$(GREEN)signed$(NC) %s $(DIM)(%s)$(NC)\n" "$$apk" "$$size"; \
else \
printf "$(RED)UNSIGNED$(NC) %s\n" "$$apk"; exit 1; \
fi; \
else \
printf "$(YELLOW)? $(NC)%s $(DIM)(apksigner not found, unverified)$(NC)\n" "$$apk"; \
fi; \
done; \
[ "$$found" = 1 ] || printf "$(RED)No release APK was produced.$(NC)\n"
.PHONY: rebuild
rebuild: clean-all refresh
# --- Install --------------------------------------------------------------
.PHONY: install
install: check-project check-adb
@$(GRADLE) :$(APP_MODULE):installDebug $(DEBUG_ABI)
.PHONY: uninstall
uninstall: check-adb
-@$(ADB) uninstall $(APP_ID) 2>/dev/null || true
.PHONY: reinstall
reinstall: uninstall install run
.PHONY: install-release
install-release: check-adb
@apk=$$(ls -1 $(APK_RELEASE_DIR)/*.apk 2>/dev/null | grep -v universal | head -1); \
if [ -z "$$apk" ]; then printf "$(RED)No release APK — run: make release$(NC)\n"; exit 1; fi; \
printf "$(YELLOW)Installing $$apk$(NC)\n"; \
$(ADB) install -r "$$apk"
# --- Tests ----------------------------------------------------------------
.PHONY: test
test: check-project
@$(GRADLE) :$(APP_MODULE):testDebugUnitTest
# connectedAndroidTest rejects --tests; the runner argument is the supported
# filter. AGP uninstalls the app when it finishes — `make refresh` puts it back.
.PHONY: test-device
test-device: check-project check-adb
@if [ -n "$(T)" ]; then \
printf "$(YELLOW)Instrumented tests: $(T)$(NC)\n"; \
$(GRADLE) :$(APP_MODULE):connectedDebugAndroidTest $(DEBUG_ABI) \
-Pandroid.testInstrumentationRunnerArguments.class=$(T); \
else \
printf "$(YELLOW)All instrumented tests (T=Class to filter)$(NC)\n"; \
$(GRADLE) :$(APP_MODULE):connectedDebugAndroidTest $(DEBUG_ABI); \
fi
.PHONY: lint
lint: check-project
@$(GRADLE) :$(APP_MODULE):lintDebug
# --- CI (GitHub Actions) ---
B ?= $(shell git rev-parse --abbrev-ref HEAD)
R ?=
RESOLVE_RUN_ID = run_id="$(R)"; \
if [ -z "$$run_id" ]; then \
run_id=$$(gh run list --branch "$(B)" --limit 1 --json databaseId --jq '.[0].databaseId'); \
fi; \
if [ -z "$$run_id" ]; then \
printf "$(RED)No GitHub Actions run found for $(B).$(NC)\n"; exit 1; \
fi;
.PHONY: check-gh
check-gh:
@if ! command -v gh >/dev/null 2>&1; then \
printf "$(RED)GitHub CLI (gh) is not installed or not on PATH.$(NC)\n"; exit 1; fi
@if ! gh auth status >/dev/null 2>&1; then \
printf "$(RED)GitHub CLI is not authenticated — run: gh auth login$(NC)\n"; exit 1; fi
.PHONY: ci
ci: check-gh
@gh run list --limit 10
.PHONY: ci-watch
ci-watch: check-gh
@$(RESOLVE_RUN_ID) \
gh run watch "$$run_id" --exit-status
.PHONY: ci-quality
ci-quality: check-gh
@gh workflow run quality.yml --ref "$(B)"; \
sleep 4; \
run_id=$$(gh run list --workflow quality.yml --branch "$(B)" --limit 1 --json databaseId --jq '.[0].databaseId'); \
url=$$(gh run view "$$run_id" --json url --jq .url); \
printf "$(GREEN)Run:$(NC) %s\n" "$$url"; \
printf "$(CYAN)Watch it: make ci-watch$(NC)\n"
.PHONY: ci-codeql
ci-codeql: check-gh
@printf "$(YELLOW)CodeQL runs on push/PR/schedule only.$(NC)\n"
@gh run list --workflow codeql.yml --limit 1
.PHONY: ci-release-dry
ci-release-dry: check-gh
@if [ -n "$(TAG)" ]; then \
gh workflow run release.yml --ref "$(B)" -f dry_run=true -f tag="$(TAG)"; \
else \
printf "$(YELLOW)A blank tag resolves to the latest existing tag, whose release is probably already published.$(NC)\n"; \
printf "Usage: $(BOLD)make ci-release-dry TAG=v3.4.0-rc1$(NC)\n"; \
exit 1; \
fi; \
sleep 4; \
run_id=$$(gh run list --workflow release.yml --branch "$(B)" --limit 1 --json databaseId --jq '.[0].databaseId'); \
url=$$(gh run view "$$run_id" --json url --jq .url); \
printf "$(GREEN)Run:$(NC) %s\n" "$$url"; \
printf "$(CYAN)Watch it: make ci-watch$(NC)\n"
.PHONY: ci-release
ci-release: check-gh
@if [ "$(CONFIRM)" != "yes" ]; then \
printf "$(RED)Refusing: this publishes a public GitHub Release. Re-run with CONFIRM=yes.$(NC)\n"; exit 1; fi
@if [ -n "$(TAG)" ]; then \
gh workflow run release.yml --ref "$(B)" -f dry_run=false -f tag="$(TAG)"; \
else \
gh workflow run release.yml --ref "$(B)" -f dry_run=false; \
fi; \
sleep 4; \
run_id=$$(gh run list --workflow release.yml --branch "$(B)" --limit 1 --json databaseId --jq '.[0].databaseId'); \
url=$$(gh run view "$$run_id" --json url --jq .url); \
printf "$(GREEN)Run:$(NC) %s\n" "$$url"; \
printf "$(CYAN)Watch it: make ci-watch$(NC)\n"
.PHONY: ci-log
ci-log: check-gh
@$(RESOLVE_RUN_ID) \
gh run view "$$run_id" --log-failed
.PHONY: ci-rerun
ci-rerun: check-gh
@$(RESOLVE_RUN_ID) \
gh run rerun "$$run_id" --failed
.PHONY: ci-artifacts
ci-artifacts: check-gh
@$(RESOLVE_RUN_ID) \
mkdir -p build/ci-artifacts; \
gh run download "$$run_id" --dir build/ci-artifacts; \
printf "$(GREEN)Artifacts:$(NC) build/ci-artifacts/\n"
.PHONY: ci-open
ci-open: check-gh
@$(RESOLVE_RUN_ID) \
gh run view "$$run_id" --web
# --- Logs -----------------------------------------------------------------
.PHONY: logs
logs: check-adb
@pid=$$($(ADB) shell pidof $(APP_ID) | tr -d '\r'); \
if [ -z "$$pid" ]; then \
printf "$(RED)$(APP_ID) is not running — start it with: make run$(NC)\n"; exit 1; \
fi; \
printf "$(CYAN)pid $$pid — Ctrl+C to stop$(NC)\n"; \
$(ADB) logcat --pid=$$pid
.PHONY: logs-error
logs-error: check-adb
@pid=$$($(ADB) shell pidof $(APP_ID) | tr -d '\r'); \
if [ -n "$$pid" ]; then $(ADB) logcat *:E --pid=$$pid; \
else $(ADB) logcat *:E | grep -E "$(APP_ID)|AndroidRuntime"; fi
.PHONY: crash
crash: check-adb
@$(ADB) logcat -d -b crash | grep -A 30 -iE "$(APP_ID)" | tail -40 \
|| printf "$(GREEN)No crashes logged.$(NC)\n"
# --- RAM / cache ----------------------------------------------------------
.PHONY: kill
kill:
@printf "$(YELLOW)Stopping Gradle + Kotlin daemons...$(NC)\n"
@before=$$(free -m | awk '/^Mem:/ {print $$7}'); \
[ -x "$(GRADLE)" ] && $(GRADLE) --stop >/dev/null 2>&1 || true; \
pkill -f "[K]otlinCompileDaemon" 2>/dev/null || true; \
sleep 2; \
after=$$(free -m | awk '/^Mem:/ {print $$7}'); \
printf "$(GREEN)Freed %s MB.$(NC) Available: %s MB\n" "$$((after - before))" "$$after"
.PHONY: ram
ram:
@free -h | awk '/^Mem:/ {printf "$(BOLD)Memory:$(NC) %s available of %s\n\n", $$7, $$2}'
@printf "$(BOLD)%8s %-7s %s$(NC)\n" "RSS" "PID" "PROCESS"
@ps -eo rss,pid,comm --sort=-rss | grep -iE "studio|java|kotlin|gradle" | grep -v grep \
| awk '$$1 > 204800 {printf "%7.1fG %-7s %s\n", $$1/1048576, $$2, $$3}' | head -8
@pgrep -f "studio.sh|android-studio" >/dev/null 2>&1 \
&& printf "\n$(YELLOW)Android Studio is running — closing it frees the most.$(NC)\n" || true
.PHONY: clean
clean: check-project
@$(GRADLE) clean
.PHONY: clean-cache
clean-cache:
@rm -rf .gradle/configuration-cache
@printf "$(GREEN)Configuration cache dropped.$(NC)\n"
.PHONY: clean-all
clean-all: clean clean-cache
.PHONY: devices
devices:
@$(ADB) devices -l