aboutsummaryrefslogtreecommitdiff
path: root/Makefile
blob: ff279145f0e7e70c191d5b4a694803c636be26e7 (plain) (blame)
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
# Author:  cirakg & claude ai
# Summary: Host entry point: every build, test, packaging and docs step runs inside Docker.
#
# OTP Wallet for Connect IQ: host Makefile.
#
# Every target runs inside Docker; the host needs only Docker and make
# (README section 15). Run `make help` to list the targets.
#
# The project path may contain spaces (".../Garmin OTP"), so paths are always
# quoted in recipes and never used as make prerequisites.

SHELL := /bin/bash
.SHELLFLAGS := -eu -o pipefail -c
.DEFAULT_GOAL := help
MAKEFLAGS += --no-builtin-rules
# Targets run one after another even with `make -j` (a common shell alias):
# `make ci` must stop at the first failing step, and two image builds or two
# simulators must never race.
.NOTPARALLEL:

PROJECT        := garmin-otp
DEV_IMAGE      := $(PROJECT)/dev:local
DOCS_IMAGE     := $(PROJECT)/docs:local
BUILDER        := $(PROJECT)
# Pinned helper images: the BuildKit daemon behind the project's own builder,
# the base images, and the throwaway images `make lock` resolves locks with.
BUILDKIT_IMAGE := moby/buildkit@sha256:cec9f139f45e93c5c69c60f8b07cfad9f43f4ef6b6a6cd917527fea5ff2e3dea
UBUNTU_IMAGE   := ubuntu@sha256:0a9887931b0e38af2e3487ed39e29436b0535c4f3f52e57745baaf79d1898956
NODE_IMAGE     := node@sha256:c3de60bf2f9dd0ac6370e6117950ff62d6e339527e7472301c9c78a017978392
LOCK_PY_IMAGE  := python@sha256:5be5aaecb962a41567bc8041f1e9e3432cb085b037fe057f9a562630b2e4b370
UV_VERSION     := 0.13.1

# Connect IQ SDK installed by `make sdk`.
SDK_VERSION  ?= 9.2.0
# Device for `make build` and `make sim`.
DEVICE       ?= fr255s
# Simulator test devices: weakest round MIP (buttons only), rectangular AMOLED,
# your Forerunner 70, and a large round AMOLED.
TEST_DEVICES ?= fr255s venusq2 fr70 fr965
# Extra screenshot-only devices (README 14): MIP with touch and buttons,
# buttons-only AMOLED, the largest rectangle.
SPOT_DEVICES ?= fenix7 instinct3amoled45mm venux1
# `make lock` only uses package releases published before this moment.
LOCK_CUTOFF  ?= 2026-10-01T00:00:00Z
# Set to `nuke` to skip the confirmation prompt of `make nuke`.
CONFIRM      ?=

HOST_UID := $(shell id -u)
# Your primary group from the user database: `id -g` would report another
# group inside `newgrp docker`, a common way to use a freshly joined group.
HOST_GID := $(shell getent passwd "$$(id -u)" | cut -d: -f4 || id -g)
ROOT     := $(CURDIR)
DOCKER   ?= docker

# Flags shared by every container: removed on exit, labelled for `make nuke`,
# your UID/GID, no Linux capabilities, no privilege escalation, read-only root
# filesystem, and only the project folder mounted. Its .cache/home folder
# becomes $HOME, so the SDK, device files and login token stay in the project.
RUN_FLAGS = --rm --init --label project=$(PROJECT) \
	--user $(HOST_UID):$(HOST_GID) --cap-drop ALL --security-opt no-new-privileges \
	--read-only --tmpfs /tmp:rw,exec,nosuid,size=2g --shm-size 512m \
	-v "$(ROOT)":/work -v "$(ROOT)/.cache/home":/home/dev -w /work \
	-e PYTHONDONTWRITEBYTECODE=1 -e HYPOTHESIS_STORAGE_DIRECTORY=/work/build/.hypothesis \
	-e SDK_VERSION=$(SDK_VERSION) -e DEVICE=$(DEVICE) -e TEST_DEVICES="$(TEST_DEVICES)"

# Most targets run with no network at all.
DEV_OFFLINE = $(DOCKER) run $(RUN_FLAGS) --network none $(DEV_IMAGE)

# The docs image needs no project cache; Chrome wants a larger /dev/shm.
DOCS_RUN = $(DOCKER) run --rm --init --label project=$(PROJECT) \
	--user $(HOST_UID):$(HOST_GID) --cap-drop ALL --security-opt no-new-privileges \
	--read-only --tmpfs /tmp:rw,exec,nosuid,size=1g --shm-size 512m \
	-v "$(ROOT)":/work -w /work --network none $(DOCS_IMAGE)

.PHONY: help image docs-image sdk key build test test-tools sim screenshots screenshots-approve perf memtest icons vectors corpus codes spike spike-sim \
        lint package docs ci shell clean nuke lock products \
        check-docker dirs need-image need-docs-image

help: ## List the targets
	@echo "OTP Wallet: every target runs in Docker. Variables: DEVICE=$(DEVICE) TEST_DEVICES=\"$(TEST_DEVICES)\""
	@grep -E '^[a-z-]+:.*## ' Makefile | sed -E 's/:.*## /\t/' | awk -F'\t' '{printf "  %-12s %s\n", $$1, $$2}'

# ---------------------------------------------------------------- setup

image: check-docker ## Build the Docker images (network: base images, Ubuntu snapshot, PyPI, GitHub)
	@$(DOCKER) buildx inspect $(BUILDER) >/dev/null 2>&1 || \
	  $(DOCKER) buildx create --name $(BUILDER) --driver docker-container \
	    --driver-opt image=$(BUILDKIT_IMAGE) --bootstrap >/dev/null
	$(DOCKER) buildx build --builder $(BUILDER) --load --label project=$(PROJECT) \
	  --build-arg UID=$(HOST_UID) --build-arg GID=$(HOST_GID) \
	  -t $(DEV_IMAGE) -f docker/Dockerfile docker

docs-image: check-docker
	@$(DOCKER) buildx inspect $(BUILDER) >/dev/null 2>&1 || \
	  $(DOCKER) buildx create --name $(BUILDER) --driver docker-container \
	    --driver-opt image=$(BUILDKIT_IMAGE) --bootstrap >/dev/null
	$(DOCKER) buildx build --builder $(BUILDER) --load --label project=$(PROJECT) \
	  -t $(DOCS_IMAGE) -f docker/docs.Dockerfile docker

sdk: dirs need-image ## Accept Garmin's licence, log in, download SDK + device files (interactive, network)
	$(DOCKER) run -it $(RUN_FLAGS) $(DEV_IMAGE) sdk-setup.sh

key: dirs need-image ## Generate your developer key into .cache/keys (never overwrites one)
	$(DEV_OFFLINE) gen-key.sh

lock: check-docker ## Regenerate the Python and npm lock files (network)
	$(DOCKER) run --rm --label project=$(PROJECT) --user $(HOST_UID):$(HOST_GID) -e HOME=/tmp \
	  -v "$(ROOT)/docker":/w -w /w $(LOCK_PY_IMAGE) sh -c '\
	    pip install --quiet --user uv==$(UV_VERSION) && \
	    /tmp/.local/bin/uv pip compile requirements.in --generate-hashes --no-build \
	      --python-version 3.10 --python-platform x86_64-manylinux_2_35 \
	      --exclude-newer $(LOCK_CUTOFF) --no-header --quiet -o requirements.txt && \
	    { printf "%s\\n" "# Author:  cirakg & claude ai" \
	        "# Summary: Pinned, hash-checked Python tool dependencies of the dev image (generated by \`make lock\`)."; \
	      cat requirements.txt; } > requirements.tmp && mv requirements.tmp requirements.txt'
	$(DOCKER) run --rm --label project=$(PROJECT) --user $(HOST_UID):$(HOST_GID) -e HOME=/tmp \
	  -e npm_config_cache=/tmp/.npm -v "$(ROOT)/docker/docs":/w -w /w $(NODE_IMAGE) sh -c '\
	    npm install --package-lock-only --ignore-scripts --no-audit --no-fund \
	      --before=$(LOCK_CUTOFF) --loglevel=error'
	@echo "Locks regenerated. Review the diff, then run 'make image docs-image'."

# ---------------------------------------------------------------- build and run

build: dirs need-image ## Debug build for DEVICE (default fr255s) into build/debug
	$(DEV_OFFLINE) build-app.sh debug $(DEVICE)

sim: dirs need-image ## Run the app in the simulator; watch at http://127.0.0.1:6080/vnc.html
	$(DOCKER) run -it $(RUN_FLAGS) -p 127.0.0.1:6080:6080 $(DEV_IMAGE) sim-vnc.sh $(DEVICE)

package: dirs need-image ## Signed .iq for the Store beta + per-device release .prg (build/package)
	$(DEV_OFFLINE) package.sh

products: dirs need-image ## Check manifest products against the downloaded device files
	$(DEV_OFFLINE) python3 tools/gen_products.py --check

vectors: dirs need-image ## Regenerate test/data/Vectors.mc (cross-checked RFC/NIST test vectors)
	$(DEV_OFFLINE) python3 tools/gen_vectors.py

corpus: dirs need-image ## Regenerate test/data/FuzzCorpus.mc (seeded fuzz inputs for the import parsers)
	$(DEV_OFFLINE) python3 tools/gen_fuzz_corpus.py

codes: dirs need-image ## Regenerate the QR and PDF417 tables and test/data/CodeVectors.mc (reference barcodes)
	$(DEV_OFFLINE) python3 tools/gen_qr_tables.py
	$(DEV_OFFLINE) python3 tools/gen_pdf417_tables.py
	$(DEV_OFFLINE) python3 tools/gen_code_vectors.py

spike: dirs need-image ## Build the M1 settings spike (debug .prg + beta .iq) into build/spike
	$(DEV_OFFLINE) build-spike.sh build

spike-sim: dirs need-image ## Run the settings spike headless on DEVICE and screenshot it
	$(DEV_OFFLINE) build-spike.sh sim $(DEVICE)

icons: dirs need-image ## Regenerate the launcher icons for every product (after changing manifest.xml)
	$(DEV_OFFLINE) sh -c 'python3 tools/make_icon.py --all --devices /home/dev/.Garmin/ConnectIQ/Devices && \
	  python3 tools/make_icon.py --size 60 --out resources/drawables/launcher_icon.png'

shell: dirs need-image ## Interactive shell in the dev container (no network)
	$(DOCKER) run -it $(RUN_FLAGS) --network none $(DEV_IMAGE) bash

# ---------------------------------------------------------------- checks

test: test-tools ## Python tool tests + Monkey C unit tests on TEST_DEVICES (headless simulator)
	$(DEV_OFFLINE) run-unit-tests.sh $(TEST_DEVICES)

test-tools: dirs need-image
	$(DEV_OFFLINE) python3 -m pytest -q tools/tests

screenshots: dirs need-image ## Scripted UI flow on TEST_DEVICES + SPOT_DEVICES, every screen compared with test/golden
	$(DEV_OFFLINE) screenshots.sh $(TEST_DEVICES) $(SPOT_DEVICES)

screenshots-approve: dirs need-image ## Save the current screens as the new golden images (review them first!)
	$(DOCKER) run $(RUN_FLAGS) -e APPROVE=1 --network none $(DEV_IMAGE) screenshots.sh $(TEST_DEVICES) $(SPOT_DEVICES)

perf: dirs need-image ## Start-up time on TEST_DEVICES against tools/budgets.json
	$(DEV_OFFLINE) app-diag.sh perf $(TEST_DEVICES)

memtest: dirs need-image ## Memory use on TEST_DEVICES against tools/budgets.json
	$(DEV_OFFLINE) app-diag.sh mem $(TEST_DEVICES)

lint: dirs need-image ## Strict type check (warnings fail), project rules, Python lint and types
	$(DEV_OFFLINE) lint.sh $(TEST_DEVICES)

docs: dirs need-image need-docs-image ## Markdown lint, link check, Mermaid rendering into build/docs
	$(DEV_OFFLINE) python3 tools/check_docs.py
	$(DOCS_RUN) sh -c 'markdownlint-cli2 && mkdir -p build/docs && \
	  for f in README.md docs/*.md; do \
	    if grep -q "^\`\`\`mermaid" "$$f"; then \
	      out="build/docs/$$(basename "$$f")"; \
	      mmdc --quiet -p /opt/docs-tools/puppeteer.json -i "$$f" -o "$$out" || exit 1; \
	      echo "rendered Mermaid diagrams of $$f -> $$out"; \
	    fi; \
	  done'

ci: lint test screenshots perf memtest package docs ## Everything above, in order; stops at the first failure
	@echo "make ci: all checks passed"

# ---------------------------------------------------------------- cleanup

clean: ## Delete build outputs
	rm -rf -- "$(ROOT)/build"

nuke: ## Delete everything this project created, including .cache (SDK, login, DEVELOPER KEY)
	@echo "This deletes:"
	@echo "  - containers and images labelled project=$(PROJECT), and the '$(BUILDER)' builder with its cache"
	@echo "  - the pinned base and helper images (skipped if anything else still uses them)"
	@echo "  - .cache/ (Garmin SDK, device files, login token, YOUR DEVELOPER KEY) and build/"
	@echo "Back up .cache/keys/ first if you want to publish updates to the app later."
	@answer="$(CONFIRM)"; \
	  if [[ "$$answer" != nuke ]]; then read -r -p "Type 'nuke' to continue: " answer; fi; \
	  [[ "$$answer" == nuke ]] || { echo "Aborted."; exit 1; }
	-$(DOCKER) ps -aq --filter label=project=$(PROJECT) | xargs -r $(DOCKER) rm -f
	-$(DOCKER) buildx rm $(BUILDER) 2>/dev/null
	-$(DOCKER) image rm $(DEV_IMAGE) $(DOCS_IMAGE) 2>/dev/null
	-$(DOCKER) image ls -q --filter label=project=$(PROJECT) | sort -u | xargs -r $(DOCKER) image rm 2>/dev/null
	-$(DOCKER) image rm $(BUILDKIT_IMAGE) $(UBUNTU_IMAGE) $(NODE_IMAGE) $(LOCK_PY_IMAGE) 2>/dev/null
	rm -rf -- "$(ROOT)/.cache" "$(ROOT)/build"
	@echo "Done. Delete the project folder to remove the rest."

# ---------------------------------------------------------------- helpers

check-docker:
	@$(DOCKER) info >/dev/null 2>&1 || { \
	  echo "Docker isn't reachable. If you were just added to the 'docker' group, log out and" >&2; \
	  echo "back in (or run 'newgrp docker' in this terminal), then try again." >&2; exit 1; }

dirs:
	@mkdir -p "$(ROOT)/.cache/home" "$(ROOT)/.cache/keys" "$(ROOT)/build"
	@chmod 700 "$(ROOT)/.cache/keys"

need-image: check-docker
	@$(DOCKER) image inspect $(DEV_IMAGE) >/dev/null 2>&1 || $(MAKE) --no-print-directory image

need-docs-image: check-docker
	@$(DOCKER) image inspect $(DOCS_IMAGE) >/dev/null 2>&1 || $(MAKE) --no-print-directory docs-image