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
|
# 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
# `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 perf memtest \
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'
$(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
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 ## Screenshot the app on TEST_DEVICES into build/screenshots
$(DEV_OFFLINE) screenshots.sh $(TEST_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
|