diff options
| author | cirakg <cirakg@cirakg.xyz> | 2026-10-18 20:28:09 +0200 |
|---|---|---|
| committer | cirakg <cirakg@cirakg.xyz> | 2026-10-18 20:28:09 +0200 |
| commit | a9b1dc8bd9354973eabd72f56d82b2eab6f48d56 (patch) | |
| tree | c77ec27cf2c474ecd07692d6404fa7551afa113c | |
| parent | fe4ba63c0a12d9c914acbb0332d96924810b393b (diff) | |
build: add Docker-only Makefile, SDK setup and product manifest
- Makefile: every target runs in a container as your UID/GID, with no
capabilities, a read-only root filesystem, only the project folder
mounted and no network except for image builds and `make sdk`.
Serial even under `make -j`. `make nuke` removes containers, images,
the dedicated buildx builder and .cache/ after confirmation.
- sdk-setup.sh: agreement acceptance pinned to the hash you read,
password login (never stored), SDK 9.2.0 and device download, and
the login token deleted afterwards.
- gen-key.sh: RSA-4096 developer key into .cache/keys, never
overwritten, with backup instructions.
- manifest.xml: watch-app, minApiLevel 5.0.0, no permissions, and the
72 watches with API 5.0+ and at least 512 KB for apps.
- .gitignore, lint configs and CHANGELOG.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | .gitignore | 26 | ||||
| -rw-r--r-- | .markdownlint-cli2.jsonc | 16 | ||||
| -rw-r--r-- | CHANGELOG.md | 16 | ||||
| -rw-r--r-- | Makefile | 201 | ||||
| -rwxr-xr-x | docker/scripts/ciq-env.sh | 49 | ||||
| -rwxr-xr-x | docker/scripts/gen-key.sh | 39 | ||||
| -rwxr-xr-x | docker/scripts/sdk-setup.sh | 88 | ||||
| -rw-r--r-- | manifest.xml | 96 | ||||
| -rw-r--r-- | pyproject.toml | 27 |
9 files changed, 558 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8da1c0d --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Local tool state: Garmin SDK, device files, SDK-manager login token and the +# developer key. Never commit any of it (Garmin's licence, and the key signs +# every release). +/.cache/ + +# Build outputs. +/build/ +/bin/ + +# Signing keys and signed artifacts, wherever they end up. +*.der +*.pem +*.prg +*.iq +*.prg.debug.xml + +# Python tooling caches. +__pycache__/ +*.py[cod] +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.hypothesis/ + +# Node modules are installed only inside the docs image. +node_modules/ diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc new file mode 100644 index 0000000..bd36a04 --- /dev/null +++ b/.markdownlint-cli2.jsonc @@ -0,0 +1,16 @@ +// Markdown lint settings for `make docs` (markdownlint-cli2 in the docs image). +// Default rules, with these deliberate exceptions: +{ + "config": { + "default": true, + // Prose lines are not hard-wrapped and tables can't wrap. + "MD013": false, + // The same subheading (e.g. "Tests") may repeat under different parents. + "MD024": { "siblings_only": true }, + // Local URLs such as http://127.0.0.1:6080/vnc.html appear as plain text. + "MD034": false + }, + "globs": ["**/*.md"], + // Generated or third-party files, and your original prompt (kept verbatim). + "ignores": ["node_modules/**", ".cache/**", "build/**", "Garmin OTP Prompt.md"] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6d7f149 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to this project are documented here. The format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). Versions stay at +0.x until milestone M8. + +## [Unreleased] + +### Added + +- Project plan (`README.md`) and Phase 0 feasibility findings (`docs/FEASIBILITY.md`). +- M0 scaffolding: Docker development and documentation images with pinned + versions, a host `Makefile` that runs every step in Docker, interactive + Connect IQ SDK setup, developer key generation, headless simulator test + runner, and a minimal watch app with a smoke test. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..29b7702 --- /dev/null +++ b/Makefile @@ -0,0 +1,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 diff --git a/docker/scripts/ciq-env.sh b/docker/scripts/ciq-env.sh new file mode 100755 index 0000000..945d59a --- /dev/null +++ b/docker/scripts/ciq-env.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Shared helpers for the container scripts. Source this file; don't run it. +# +# Paths, as seen inside the container: +# /work the project folder (bind mount) +# /home/dev the project's .cache/home folder (bind mount); the Garmin +# tools keep the SDK and device files under ~/.Garmin +# /work/.cache/keys the developer key (never baked into an image) + +CIQ_HOME="${HOME}/.Garmin/ConnectIQ" +KEY_DIR="/work/.cache/keys" +DEV_KEY="${KEY_DIR}/developer_key.der" +BUILD_DIR="/work/build" + +# Print an error and stop. Used for every user-facing failure, so the message +# says what to do next instead of leaving a bare stack trace. +die() { + echo "error: $*" >&2 + exit 1 +} + +# Progress line on stderr, so stdout stays clean for machine-readable output. +log() { + echo "==> $*" >&2 +} + +# Find the SDK selected by `make sdk` and put its bin/ folder on PATH. +# Sets CIQ_SDK to the SDK root folder. +ciq_require_sdk() { + local cfg="${CIQ_HOME}/current-sdk.cfg" + [[ -f "${cfg}" ]] || die "Connect IQ SDK not installed. Run 'make sdk' first (you accept Garmin's licence and log in there)." + CIQ_SDK="$(tr -d '\r\n' < "${cfg}")" + CIQ_SDK="${CIQ_SDK%/}" + [[ -x "${CIQ_SDK}/bin/monkeyc" ]] || die "SDK folder '${CIQ_SDK}' has no bin/monkeyc. Run 'make sdk' again." + export CIQ_SDK + export PATH="${CIQ_SDK}/bin:${PATH}" +} + +# The developer key signs every build, including test builds. +ciq_require_key() { + [[ -f "${DEV_KEY}" ]] || die "No developer key at .cache/keys/developer_key.der. Run 'make key' first." +} + +# Fail early, with a pointer to `make sdk`, when a device's files are missing. +ciq_require_device() { + local device="$1" + [[ -f "${CIQ_HOME}/Devices/${device}/compiler.json" ]] \ + || die "No device files for '${device}'. Is it in manifest.xml? Then run 'make sdk' to download them." +} diff --git a/docker/scripts/gen-key.sh b/docker/scripts/gen-key.sh new file mode 100755 index 0000000..7b99863 --- /dev/null +++ b/docker/scripts/gen-key.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Generate the developer key that signs every build (README section 15.6). +# +# Output: .cache/keys/developer_key.der (PKCS#8 DER, what monkeyc wants) and +# developer_key.pem, both mode 600 and git-ignored. An existing key is never +# overwritten: the Connect IQ Store rejects updates signed with a different +# key, so replacing it by accident would strand the published app. +set -euo pipefail +# shellcheck source=ciq-env.sh +source "$(dirname "${BASH_SOURCE[0]}")/ciq-env.sh" + +umask 077 +mkdir -p "${KEY_DIR}" + +if [[ -f "${DEV_KEY}" ]]; then + log "Developer key already exists at .cache/keys/developer_key.der; leaving it untouched." + exit 0 +fi + +log "Generating a 4096-bit RSA developer key (this takes a few seconds)..." +openssl genrsa -out "${KEY_DIR}/developer_key.pem" 4096 2>/dev/null +openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt \ + -in "${KEY_DIR}/developer_key.pem" -out "${DEV_KEY}" +chmod 600 "${KEY_DIR}/developer_key.pem" "${DEV_KEY}" + +# A short fingerprint of the public key, so a backup can be recognised later. +fingerprint="$(openssl pkey -in "${KEY_DIR}/developer_key.pem" -pubout -outform DER \ + | sha256sum | cut -c1-16)" + +cat >&2 <<EOF + +Developer key created: .cache/keys/developer_key.der +Public key fingerprint (SHA-256, first 16 hex digits): ${fingerprint} + +BACK IT UP NOW. Copy the folder .cache/keys/ to an encrypted place (a password +manager attachment or an encrypted USB drive). Garmin rejects app updates that +are signed with a different key; without this key you would have to publish a +new app and re-add every entry on the watch. 'make nuke' deletes this folder. +EOF diff --git a/docker/scripts/sdk-setup.sh b/docker/scripts/sdk-setup.sh new file mode 100755 index 0000000..278b060 --- /dev/null +++ b/docker/scripts/sdk-setup.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Interactive first-time setup behind `make sdk` (README section 15.3). +# +# 1. Garmin's agreements: you read them in your browser and accept them here. +# 2. Login with your Garmin account. Email and password are typed here and +# passed only to the SDK manager CLI, which sends them to Garmin's sign-in +# page. Neither is written to disk; the CLI stores an access token. +# 3. Download Connect IQ SDK ${SDK_VERSION} and make it the current SDK. +# 4. Download the device files and simulator fonts for every product listed +# in manifest.xml. +# 5. Log out: delete the access token (set KEEP_LOGIN=1 to keep it). +# +# Why email and password instead of the CLI's browser login: that flow waits +# for a redirect to a random localhost port inside the container, which your +# browser can't reach without host networking (not allowed by the sandbox +# rules). Password login doesn't support Garmin two-step verification; README +# section 15.3 describes the fallback for such accounts. +set -euo pipefail +# shellcheck source=ciq-env.sh +source "$(dirname "${BASH_SOURCE[0]}")/ciq-env.sh" + +readonly SDKMGR=connect-iq-sdk-manager +readonly MANIFEST=/work/manifest.xml +readonly TOKEN="${CIQ_HOME}/token.json" + +[[ -t 0 ]] || die "'make sdk' is interactive; run it from a terminal." +[[ -f "${MANIFEST}" ]] || die "manifest.xml not found in the project folder." +: "${SDK_VERSION:?SDK_VERSION is not set (the Makefile passes it)}" + +echo +echo "Step 1/5: Garmin's agreements" +view="$(${SDKMGR} agreement view 2>&1)" || die "Could not fetch the agreement from Garmin: ${view}" +url="$(sed -n 's/^Agreement: //p' <<<"${view}")" +hash="$(sed -n 's/^Current Hash: //p' <<<"${view}")" +[[ -n "${url}" && -n "${hash}" ]] || die "Unexpected output from 'agreement view': ${view}" +cat <<EOF + Please read the Garmin CONNECT IQ SDK License Agreement and the + CONNECT IQ Application Developer Agreement in your browser: + ${url} + Agreement version (hash): ${hash} +EOF +read -r -p " Type 'accept' to accept them, or anything else to stop: " answer +[[ "${answer}" == accept ]] || die "Agreements not accepted; nothing was downloaded." +# Passing the hash makes the CLI refuse if Garmin changed the text since you read it. +${SDKMGR} agreement accept --agreement-hash "${hash}" >/dev/null +echo " Accepted (version ${hash})." + +echo +echo "Step 2/5: Garmin login" +if [[ -f "${TOKEN}" ]]; then + echo " Reusing the login token kept from an earlier run (KEEP_LOGIN=1)." +else + echo " Email and password go only to Garmin's sign-in page and are not stored." + echo " Accounts with two-step verification can't log in this way (see README 15.3)." + read -r -p " Garmin account email: " GARMIN_USERNAME + read -r -s -p " Password (not shown): " GARMIN_PASSWORD + echo + export GARMIN_USERNAME GARMIN_PASSWORD + if ! ${SDKMGR} login; then + unset GARMIN_USERNAME GARMIN_PASSWORD + die "Login failed. Check email and password. Two-step verification isn't supported here; see README 15.3." + fi + unset GARMIN_USERNAME GARMIN_PASSWORD + echo " Logged in." +fi + +echo +echo "Step 3/5: Connect IQ SDK ${SDK_VERSION}" +${SDKMGR} sdk set "${SDK_VERSION}" +echo " Current SDK: $(${SDKMGR} sdk current-path)" + +echo +echo "Step 4/5: device files and fonts for the products in manifest.xml" +${SDKMGR} device download --manifest "${MANIFEST}" --include-fonts + +echo +echo "Step 5/5: log out" +if [[ "${KEEP_LOGIN:-0}" == 1 ]]; then + echo " KEEP_LOGIN=1: keeping the token at .cache/home/.Garmin/ConnectIQ/token.json" +else + rm -f "${TOKEN}" + echo " Login token deleted. Run 'make sdk' again whenever new device files are needed." +fi + +device_count="$(find "${CIQ_HOME}/Devices" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)" +echo +echo "Done: SDK ${SDK_VERSION}, ${device_count} device folders, $(du -sh "${CIQ_HOME}" | cut -f1) in .cache/home." +echo "Next: 'make key' (if you have no developer key yet), then 'make test'." diff --git a/manifest.xml b/manifest.xml new file mode 100644 index 0000000..cffbe88 --- /dev/null +++ b/manifest.xml @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Connect IQ manifest for OTP Wallet (README sections 4 and 14). + + * type watch-app with minApiLevel 5.0.0: device app, so it never times out + like a widget, and it can provide a glance on API 4.0+ watches. + * No permissions at all, so the app cannot use Toybox.Communications or any + other network API. Keep it that way: it is part of the security design. + * Products: every watch with API level 5.0 or newer and at least 512 KB of + memory for watch apps (72 devices). tools/gen_products.py checks + this list against the downloaded device files (`make products`). +--> +<iq:manifest version="3" xmlns:iq="http://www.garmin.com/xml/connectiq"> + <iq:application id="7e21a3c58041450f913f86cf584c5945" type="watch-app" name="@Strings.AppName" + entry="OtpWalletApp" launcherIcon="@Drawables.LauncherIcon" minApiLevel="5.0.0"> + <iq:products> + <iq:product id="approachs50"/> + <iq:product id="approachs7042mm"/> + <iq:product id="approachs7047mm"/> + <iq:product id="approachs7243mm"/> + <iq:product id="approachs7247mm"/> + <iq:product id="d2airx10"/> + <iq:product id="d2mach1"/> + <iq:product id="d2mach2"/> + <iq:product id="d2mach2pro"/> + <iq:product id="descentg2"/> + <iq:product id="descentmk343mm"/> + <iq:product id="descentmk351mm"/> + <iq:product id="enduro3"/> + <iq:product id="enduro4"/> + <iq:product id="epix2"/> + <iq:product id="epix2pro42mm"/> + <iq:product id="epix2pro47mm"/> + <iq:product id="epix2pro51mm"/> + <iq:product id="fenix7"/> + <iq:product id="fenix7pro"/> + <iq:product id="fenix7pronowifi"/> + <iq:product id="fenix7s"/> + <iq:product id="fenix7spro"/> + <iq:product id="fenix7x"/> + <iq:product id="fenix7xpro"/> + <iq:product id="fenix7xpronowifi"/> + <iq:product id="fenix843mm"/> + <iq:product id="fenix847mm"/> + <iq:product id="fenix8pro47mm"/> + <iq:product id="fenix8solar47mm"/> + <iq:product id="fenix8solar51mm"/> + <iq:product id="fenix943mm"/> + <iq:product id="fenix947mm"/> + <iq:product id="fenix9pro43mm"/> + <iq:product id="fenix9pro47mm"/> + <iq:product id="fenix9pro51mm"/> + <iq:product id="fenix9prosolar47mm"/> + <iq:product id="fenix9prosolar51mm"/> + <iq:product id="fenixe"/> + <iq:product id="fr165"/> + <iq:product id="fr165m"/> + <iq:product id="fr170"/> + <iq:product id="fr170m"/> + <iq:product id="fr255"/> + <iq:product id="fr255m"/> + <iq:product id="fr255s"/> + <iq:product id="fr255sm"/> + <iq:product id="fr265"/> + <iq:product id="fr265s"/> + <iq:product id="fr57042mm"/> + <iq:product id="fr57047mm"/> + <iq:product id="fr70"/> + <iq:product id="fr955"/> + <iq:product id="fr965"/> + <iq:product id="fr970"/> + <iq:product id="instinct3amoled45mm"/> + <iq:product id="instinct3amoled50mm"/> + <iq:product id="instinctcrossoveramoled"/> + <iq:product id="marq2"/> + <iq:product id="marq2aviator"/> + <iq:product id="venu2"/> + <iq:product id="venu2plus"/> + <iq:product id="venu2s"/> + <iq:product id="venu3"/> + <iq:product id="venu3s"/> + <iq:product id="venu441mm"/> + <iq:product id="venu445mm"/> + <iq:product id="venusq2"/> + <iq:product id="venusq2m"/> + <iq:product id="venux1"/> + <iq:product id="vivoactive5"/> + <iq:product id="vivoactive6"/> + </iq:products> + <iq:permissions/> + <iq:languages> + <iq:language>eng</iq:language> + </iq:languages> + <iq:barrels/> + </iq:application> +</iq:manifest> diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c92bed4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +# Configuration for the Python tools in tools/ (they run inside the dev image). +# This is not a Python package; the file only configures pytest, ruff and mypy. +# Caches go to build/ so the source tree stays clean. + +[tool.pytest.ini_options] +testpaths = ["tools/tests"] +cache_dir = "build/.pytest_cache" +addopts = "-p no:cacheprovider" + +[tool.ruff] +line-length = 120 +target-version = "py310" +cache-dir = "build/.ruff_cache" +include = ["tools/**/*.py"] + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF", "PL"] +ignore = [ + "PLR2004", # magic values are fine in tests and small CLI tools + "PLR0912", # branchy parsers read better as one function +] + +[tool.mypy] +python_version = "3.10" +strict = true +files = ["tools"] +cache_dir = "build/.mypy_cache" |
