# 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 watchdog`: one device per watchdog class (120,000 and 240,000 # bytecodes per event). WATCHDOG_DEVICES ?= fr255s fr70 # `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 watchdog 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) watchdog: dirs need-image ## Largest 2D codes at twice the steps per event on WATCHDOG_DEVICES (no watchdog trip) $(DEV_OFFLINE) app-diag.sh watchdog $(WATCHDOG_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 watchdog 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