diff options
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | .markdownlint-cli2.jsonc | 3 | ||||
| -rw-r--r-- | Makefile | 19 | ||||
| -rw-r--r-- | docker/Dockerfile | 2 | ||||
| -rw-r--r-- | docker/docs.Dockerfile | 2 | ||||
| -rw-r--r-- | docker/docs/package.json | 1 | ||||
| -rw-r--r-- | docker/docs/puppeteer.json | 7 | ||||
| -rw-r--r-- | docker/requirements.in | 3 | ||||
| -rw-r--r-- | docker/requirements.txt | 2 | ||||
| -rwxr-xr-x | docker/scripts/app-diag.sh | 36 | ||||
| -rwxr-xr-x | docker/scripts/build-app.sh | 49 | ||||
| -rwxr-xr-x | docker/scripts/ciq-env.sh | 93 | ||||
| -rwxr-xr-x | docker/scripts/gen-key.sh | 3 | ||||
| -rwxr-xr-x | docker/scripts/lint.sh | 33 | ||||
| -rwxr-xr-x | docker/scripts/package.sh | 45 | ||||
| -rwxr-xr-x | docker/scripts/run-unit-tests.sh | 46 | ||||
| -rwxr-xr-x | docker/scripts/screenshots.sh | 48 | ||||
| -rwxr-xr-x | docker/scripts/sdk-setup.sh | 3 | ||||
| -rwxr-xr-x | docker/scripts/sim-vnc.sh | 39 | ||||
| -rw-r--r-- | pyproject.toml | 5 |
20 files changed, 437 insertions, 5 deletions
@@ -1,3 +1,6 @@ +# Author: cirakg & claude ai +# Summary: Files git must never track: local SDK/key cache, build output, signing material, tool caches. +# # 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). diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index bd36a04..00d2fd8 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -1,3 +1,6 @@ +// Author: cirakg & claude ai +// Summary: Markdown lint settings used by `make docs`. +// // Markdown lint settings for `make docs` (markdownlint-cli2 in the docs image). // Default rules, with these deliberate exceptions: { @@ -1,3 +1,6 @@ +# 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 @@ -66,7 +69,7 @@ DOCS_RUN = $(DOCKER) run --rm --init --label project=$(PROJECT) \ --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 \ +.PHONY: help image docs-image sdk key build test test-tools sim screenshots screenshots-approve perf memtest icons \ lint package docs ci shell clean nuke lock products \ check-docker dirs need-image need-docs-image @@ -103,7 +106,10 @@ lock: check-docker ## Regenerate the Python and npm lock files (network) 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' + --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 \ @@ -124,6 +130,10 @@ package: dirs need-image ## Signed .iq for the Store beta + per-device release . products: dirs need-image ## Check manifest products against the downloaded device files $(DEV_OFFLINE) python3 tools/gen_products.py --check +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 @@ -135,9 +145,12 @@ test: test-tools ## Python tool tests + Monkey C unit tests on TEST_DEVICES (hea 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 +screenshots: dirs need-image ## Screenshot the app on TEST_DEVICES and compare with test/golden $(DEV_OFFLINE) screenshots.sh $(TEST_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) + perf: dirs need-image ## Start-up time on TEST_DEVICES against tools/budgets.json $(DEV_OFFLINE) app-diag.sh perf $(TEST_DEVICES) diff --git a/docker/Dockerfile b/docker/Dockerfile index 7d691d5..a43e90f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,6 @@ # syntax=docker/dockerfile:1 +# Author: cirakg & claude ai +# Summary: garmin-otp/dev image: Connect IQ build, test and headless-simulator toolchain (no SDK inside). # # garmin-otp/dev: the toolchain image for this project. # diff --git a/docker/docs.Dockerfile b/docker/docs.Dockerfile index 44dfa93..3f4c9e2 100644 --- a/docker/docs.Dockerfile +++ b/docker/docs.Dockerfile @@ -1,4 +1,6 @@ # syntax=docker/dockerfile:1 +# Author: cirakg & claude ai +# Summary: garmin-otp/docs image: Markdown lint and Mermaid rendering for `make docs`. # # garmin-otp/docs: documentation checks for `make docs`. # * markdownlint-cli2 lints every Markdown file. diff --git a/docker/docs/package.json b/docker/docs/package.json index 1dd8cb4..44482fb 100644 --- a/docker/docs/package.json +++ b/docker/docs/package.json @@ -1,6 +1,7 @@ { "name": "garmin-otp-docs-tools", "private": true, + "author": "cirakg & claude ai", "description": "Documentation checks for the garmin-otp project: Markdown lint and Mermaid rendering. Installed with `npm ci` inside the garmin-otp/docs image; regenerate the lock with `make lock`.", "dependencies": { "@mermaid-js/mermaid-cli": "12.0.0", diff --git a/docker/docs/puppeteer.json b/docker/docs/puppeteer.json index 9ee160d..3c6a13c 100644 --- a/docker/docs/puppeteer.json +++ b/docker/docs/puppeteer.json @@ -1,3 +1,8 @@ { - "args": ["--no-sandbox", "--disable-gpu"] + "_author": "cirakg & claude ai", + "_summary": "Chrome launch options for mermaid-cli in the docs image (no sandbox: cap-dropped container).", + "args": [ + "--no-sandbox", + "--disable-gpu" + ] } diff --git a/docker/requirements.in b/docker/requirements.in index 8c28cb7..69716b7 100644 --- a/docker/requirements.in +++ b/docker/requirements.in @@ -1,3 +1,6 @@ +# Author: cirakg & claude ai +# Summary: Top-level Python tool dependencies of the dev image; `make lock` pins them in requirements.txt. +# # Python tooling baked into the garmin-otp/dev image. # Edit this file, then run `make lock` to regenerate docker/requirements.txt # (exact versions + SHA-256 hashes, resolved only from releases published diff --git a/docker/requirements.txt b/docker/requirements.txt index 3892775..4b1b230 100644 --- a/docker/requirements.txt +++ b/docker/requirements.txt @@ -1,3 +1,5 @@ +# Author: cirakg & claude ai +# Summary: Pinned, hash-checked Python tool dependencies of the dev image (generated by `make lock`). ast-serialize==0.11.2 \ --hash=sha256:00119a8fb8c1dc0f1fab023f4d8071fa49e3b0208ee54d589fd463c16ab0124e \ --hash=sha256:00bbf1f6669f813b48925b759f7ae4591067d456d443924055cab386e7e0a719 \ diff --git a/docker/scripts/app-diag.sh b/docker/scripts/app-diag.sh new file mode 100755 index 0000000..804d68a --- /dev/null +++ b/docker/scripts/app-diag.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Author: cirakg & claude ai +# Summary: Run the debug app on each device in the headless simulator and check start-up time or memory budgets. +# +# Usage: app-diag.sh perf|mem DEVICE [DEVICE ...] +# Output: build/diag/<DEVICE>.log (console with DIAG and HARNESS lines) and +# build/diag/<perf|mem>-summary.json; verdict from tools/check_budgets.py. +set -euo pipefail +# shellcheck source=ciq-env.sh +source "$(dirname "${BASH_SOURCE[0]}")/ciq-env.sh" + +kind="${1:?usage: app-diag.sh perf|mem DEVICE [DEVICE ...]}" +shift +[[ "${kind}" == perf || "${kind}" == mem ]] || die "unknown kind '${kind}' (use perf or mem)" +(($# > 0)) || die "usage: app-diag.sh perf|mem DEVICE [DEVICE ...]" +ciq_require_sdk +ciq_require_key + +for device in "$@"; do + build-app.sh debug "${device}" +done + +ciq_start_display +ciq_start_simulator + +out="${BUILD_DIR}/diag" +mkdir -p "${out}" +logs=() +for device in "$@"; do + log "starting the app on ${device}" + ciq_launch_app "${device}" "${out}/${device}.log" || log "no first frame on ${device} within ${FIRST_FRAME_TIMEOUT_S} s" + ciq_stop_app + logs+=("${out}/${device}.log") +done + +python3 /work/tools/check_budgets.py "${kind}" "${logs[@]}" --summary "${out}/${kind}-summary.json" diff --git a/docker/scripts/build-app.sh b/docker/scripts/build-app.sh new file mode 100755 index 0000000..74dd042 --- /dev/null +++ b/docker/scripts/build-app.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Author: cirakg & claude ai +# Summary: Compile the watch app for one device: debug, release or unit-test build. +# +# Usage: build-app.sh debug|release|test DEVICE +# Output: build/<mode>/<DEVICE>.prg, and the compiler log next to it. +# +# Policy (README section 17.7): strict type checking (-l 3), and any compiler +# warning fails the build. Test builds add test.jungle, so test code never +# reaches debug or release builds; release builds (-r) also drop every +# (:debug) declaration, such as the diagnostics in source/util/Diag.mc. +set -euo pipefail +# shellcheck source=ciq-env.sh +source "$(dirname "${BASH_SOURCE[0]}")/ciq-env.sh" + +mode="${1:?usage: build-app.sh debug|release|test DEVICE}" +device="${2:?usage: build-app.sh debug|release|test DEVICE}" + +ciq_require_sdk +ciq_require_key +ciq_require_device "${device}" + +# icons.jungle (generated by tools/make_icon.py --all) gives every device a +# launcher icon of exactly its size. +jungles="/work/monkey.jungle;/work/icons.jungle" +args=(-d "${device}" -y "${DEV_KEY}" -l 3 -w) +case "${mode}" in + debug) ;; + release) args+=(-r) ;; + test) + jungles="${jungles};/work/test.jungle" + args+=(-t) + ;; + *) die "unknown build mode '${mode}' (use debug, release or test)" ;; +esac + +out_dir="${BUILD_DIR}/${mode}" +mkdir -p "${out_dir}" +compile_log="${out_dir}/${device}.log" + +if ! monkeyc -f "${jungles}" -o "${out_dir}/${device}.prg" "${args[@]}" >"${compile_log}" 2>&1; then + cat "${compile_log}" >&2 + die "compile failed for ${device} (${mode} build)" +fi +if grep -q "^WARNING" "${compile_log}"; then + grep "^WARNING" "${compile_log}" >&2 + die "compiler warnings for ${device} (${mode} build); warnings count as errors" +fi +log "built build/${mode}/${device}.prg" diff --git a/docker/scripts/ciq-env.sh b/docker/scripts/ciq-env.sh index 945d59a..a52db78 100755 --- a/docker/scripts/ciq-env.sh +++ b/docker/scripts/ciq-env.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash -# Shared helpers for the container scripts. Source this file; don't run it. +# Author: cirakg & claude ai +# Summary: Shared helpers for the container scripts (SDK and key lookup, headless display and simulator). +# +# Source this file; don't run it. # # Paths, as seen inside the container: # /work the project folder (bind mount) @@ -12,6 +15,12 @@ KEY_DIR="/work/.cache/keys" DEV_KEY="${KEY_DIR}/developer_key.der" BUILD_DIR="/work/build" +# Virtual display for the headless simulator, and the TCP port (1234, hex +# 04D2 as in /proc/net/tcp) on which the simulator accepts monkeydo. +SIM_DISPLAY=":99" +SIM_PORT_HEX="04D2" +SIM_START_TIMEOUT_S=60 + # 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() { @@ -47,3 +56,85 @@ ciq_require_device() { [[ -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." } + +# Start a virtual X display (Xvfb) with the openbox window manager. The window +# manager matters: screenshots and scripted input find the simulator window +# through it. +ciq_start_display() { + export DISPLAY="${SIM_DISPLAY}" + Xvfb "${DISPLAY}" -screen 0 1600x1200x24 -nolisten tcp >/tmp/xvfb.log 2>&1 & + local _ + for _ in $(seq 1 50); do + xdpyinfo >/dev/null 2>&1 && break + sleep 0.1 + done + xdpyinfo >/dev/null 2>&1 || die "Xvfb did not start (see /tmp/xvfb.log)." + openbox >/tmp/openbox.log 2>&1 & +} + +# True when something listens on the simulator's monkeydo port. +ciq_simulator_listening() { + awk -v port=":${SIM_PORT_HEX}" \ + 'NR > 1 && $4 == "0A" && substr($2, length($2) - 4) == port { found = 1 } END { exit !found }' \ + /proc/net/tcp +} + +# Start the Connect IQ simulator (needs ciq_require_sdk and ciq_start_display) +# and wait until it accepts monkeydo connections. +ciq_start_simulator() { + # The simulator's single-instance lock (~/Sim-<user>) stores its process ID + # and survives in .cache/home after the container is gone. A later container + # often reuses that process ID for something else, and the simulator then + # believes it is already running and never opens its port. Each container + # runs exactly one simulator, so the stale lock is safe to remove. + rm -f "${HOME}"/Sim-* + simulator >/tmp/simulator.log 2>&1 & + local pid=$! _ + for _ in $(seq 1 $((SIM_START_TIMEOUT_S * 5))); do + ciq_simulator_listening && return 0 + kill -0 "${pid}" 2>/dev/null || break + sleep 0.2 + done + # The container (and /tmp) disappears after a failure, so show the log now. + echo "--- last lines of the simulator log ---" >&2 + tail -n 25 /tmp/simulator.log >&2 || true + die "The Connect IQ simulator did not start within ${SIM_START_TIMEOUT_S} s." +} + +# The simulator's main window ID, for screenshots and scripted input. +ciq_simulator_window() { + xdotool search --name '^CIQ Simulator' | head -n 1 +} + +# Longest wait for an app's first frame after launching it. +FIRST_FRAME_TIMEOUT_S=60 + +# Launch the debug build of the app on DEVICE and wait for its first frame, +# which the app reports with a "DIAG first_frame" console line. The console +# goes to LOG, followed by a "HARNESS first_frame wall_ms=N" line: the +# wall-clock time from launch to first frame, including the JVM start of +# monkeydo and the program load the app can't time itself. The app keeps +# running (for screenshots) until ciq_stop_app or the next launch. +# Returns 1 if no first frame appeared in time. +ciq_launch_app() { + local device="$1" app_log="$2" start _ + start="$(date +%s%3N)" + monkeydo "${BUILD_DIR}/debug/${device}.prg" "${device}" >"${app_log}" 2>&1 & + CIQ_MONKEYDO_PID=$! + for _ in $(seq 1 $((FIRST_FRAME_TIMEOUT_S * 20))); do + if grep -q "DIAG first_frame" "${app_log}"; then + echo "HARNESS first_frame wall_ms=$(($(date +%s%3N) - start))" >>"${app_log}" + return 0 + fi + # monkeydo only exits on its own when the app could not run. + kill -0 "${CIQ_MONKEYDO_PID}" 2>/dev/null || break + sleep 0.05 + done + return 1 +} + +# Stop the monkeydo session started by ciq_launch_app. +ciq_stop_app() { + kill "${CIQ_MONKEYDO_PID:-0}" 2>/dev/null || true + wait "${CIQ_MONKEYDO_PID:-0}" 2>/dev/null || true +} diff --git a/docker/scripts/gen-key.sh b/docker/scripts/gen-key.sh index 7b99863..015f9d9 100755 --- a/docker/scripts/gen-key.sh +++ b/docker/scripts/gen-key.sh @@ -1,4 +1,7 @@ #!/usr/bin/env bash +# Author: cirakg & claude ai +# Summary: Generate the RSA-4096 developer key that signs every build, never overwriting an existing one. +# # 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 diff --git a/docker/scripts/lint.sh b/docker/scripts/lint.sh new file mode 100755 index 0000000..09b8410 --- /dev/null +++ b/docker/scripts/lint.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Author: cirakg & claude ai +# Summary: All static checks: project policy, Python lint and types, and strict Monkey C builds without warnings. +# +# Usage: lint.sh DEVICE [DEVICE ...] +# +# 1. tools/lint_project.py: project rules (headers, no logging, no network, ...) +# 2. ruff (lint + formatting) and mypy --strict on the Python tools +# 3. tools/gen_products.py --check: the manifest's products meet the targets +# 4. Debug and release builds with strict type checking (-l 3) on each device; +# build-app.sh fails on any compiler warning. +set -euo pipefail +# shellcheck source=ciq-env.sh +source "$(dirname "${BASH_SOURCE[0]}")/ciq-env.sh" + +(($# > 0)) || die "usage: lint.sh DEVICE [DEVICE ...]" + +log "project policy" +python3 /work/tools/lint_project.py /work + +log "Python tools: ruff and mypy" +ruff check /work/tools +ruff format --check /work/tools +(cd /work && mypy) + +log "product list" +python3 /work/tools/gen_products.py --check + +for device in "$@"; do + build-app.sh debug "${device}" + build-app.sh release "${device}" +done +log "lint passed" diff --git a/docker/scripts/package.sh b/docker/scripts/package.sh new file mode 100755 index 0000000..8d58e39 --- /dev/null +++ b/docker/scripts/package.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Author: cirakg & claude ai +# Summary: Build the signed .iq package for the Connect IQ Store beta, plus release .prg files for sideload tests. +# +# Usage: package.sh [DEVICE ...] (devices for the .prg files; default: TEST_DEVICES) +# Output: build/package/otp-wallet-<version>.iq all 72 products, release, signed +# build/package/prg/<DEVICE>.prg release builds for sideload tests +# +# The .iq is what you upload as a beta app (README section 4). It is built +# with the same strict settings as every other build: type check level 3 and +# no compiler warnings. +set -euo pipefail +# shellcheck source=ciq-env.sh +source "$(dirname "${BASH_SOURCE[0]}")/ciq-env.sh" + +ciq_require_sdk +ciq_require_key + +version="$(sed -n 's/^ *const VERSION = "\([0-9.]*\)";.*/\1/p' /work/source/app/AppInfo.mc)" +[[ -n "${version}" ]] || die "could not read VERSION from source/app/AppInfo.mc" + +out="${BUILD_DIR}/package" +mkdir -p "${out}/prg" +iq="${out}/otp-wallet-${version}.iq" +compile_log="${out}/otp-wallet-${version}.log" + +log "exporting ${iq#/work/} for every product in manifest.xml (this takes a while)" +if ! monkeyc -e -r -f "/work/monkey.jungle;/work/icons.jungle" -o "${iq}" -y "${DEV_KEY}" -l 3 -w \ + >"${compile_log}" 2>&1; then + cat "${compile_log}" >&2 + die "packaging failed" +fi +if grep -q "^WARNING" "${compile_log}"; then + grep "^WARNING" "${compile_log}" >&2 + die "compiler warnings while packaging; warnings count as errors" +fi + +devices=("$@") +((${#devices[@]} > 0)) || read -r -a devices <<<"${TEST_DEVICES:-}" +for device in "${devices[@]}"; do + build-app.sh release "${device}" + cp "${BUILD_DIR}/release/${device}.prg" "${out}/prg/${device}.prg" +done + +log "package ready: ${iq#/work/} ($(du -h "${iq}" | cut -f1)); release .prg files in build/package/prg/" diff --git a/docker/scripts/run-unit-tests.sh b/docker/scripts/run-unit-tests.sh new file mode 100755 index 0000000..60a8955 --- /dev/null +++ b/docker/scripts/run-unit-tests.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Author: cirakg & claude ai +# Summary: Build and run the Monkey C unit tests on each test device in the headless simulator. +# +# Usage: run-unit-tests.sh DEVICE [DEVICE ...] +# Output: build/test/results/<DEVICE>.log (console) and <DEVICE>.xml (JUnit). +# +# Every test build is compiled first, so a compile error stops the run before +# the simulator starts. One simulator then runs every device in turn. +# monkeydo exits with 1 even when all tests pass, so the verdict comes from +# tools/ciq_test_report.py; the script fails if any device fails. +set -euo pipefail +# shellcheck source=ciq-env.sh +source "$(dirname "${BASH_SOURCE[0]}")/ciq-env.sh" + +# Longest a single device's test run may take before it counts as hung. +readonly TEST_TIMEOUT_S=600 + +(($# > 0)) || die "usage: run-unit-tests.sh DEVICE [DEVICE ...]" +ciq_require_sdk +ciq_require_key + +for device in "$@"; do + build-app.sh test "${device}" +done + +ciq_start_display +ciq_start_simulator + +results="${BUILD_DIR}/test/results" +mkdir -p "${results}" +failed=0 +for device in "$@"; do + log "running unit tests on ${device}" + # monkeydo's exit status is ignored on purpose (see above); a timeout leaves + # an incomplete log, which the report then rejects. + timeout "${TEST_TIMEOUT_S}" monkeydo "${BUILD_DIR}/test/${device}.prg" "${device}" -t \ + >"${results}/${device}.log" 2>&1 || true + python3 /work/tools/ciq_test_report.py "${results}/${device}.log" \ + --device "${device}" --junit "${results}/${device}.xml" || failed=1 +done + +if ((failed)); then + die "unit tests failed; console logs are in build/test/results/" +fi +log "unit tests passed on: $*" diff --git a/docker/scripts/screenshots.sh b/docker/scripts/screenshots.sh new file mode 100755 index 0000000..3fc52f5 --- /dev/null +++ b/docker/scripts/screenshots.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Author: cirakg & claude ai +# Summary: Screenshot the app on each device in the headless simulator and compare with the approved golden images. +# +# Usage: screenshots.sh DEVICE [DEVICE ...] (APPROVE=1 saves new goldens) +# Output: build/screenshots/<DEVICE>.png (the watch screen only), plus a +# <DEVICE>-diff.png when it differs from test/golden/<DEVICE>.png. +set -euo pipefail +# shellcheck source=ciq-env.sh +source "$(dirname "${BASH_SOURCE[0]}")/ciq-env.sh" + +# Pause after the first frame, so animations and late redraws settle. +readonly SETTLE_S=1 + +(($# > 0)) || die "usage: screenshots.sh DEVICE [DEVICE ...]" +ciq_require_sdk +ciq_require_key + +for device in "$@"; do + build-app.sh debug "${device}" +done + +ciq_start_display +ciq_start_simulator + +out="${BUILD_DIR}/screenshots" +mkdir -p "${out}" +approve=() +[[ "${APPROVE:-0}" == 1 ]] && approve=(--approve) +failed=0 +for device in "$@"; do + if ! ciq_launch_app "${device}" "${out}/${device}.log"; then + ciq_stop_app + log "no first frame on ${device}; see build/screenshots/${device}.log" + failed=1 + continue + fi + sleep "${SETTLE_S}" + mkdir -p "${out}/raw" + import -window "$(ciq_simulator_window)" "${out}/raw/${device}-window.png" + ciq_stop_app + python3 /work/tools/sim_screenshot.py --raw "${out}/raw/${device}-window.png" \ + --device-dir "${CIQ_HOME}/Devices/${device}" --out "${out}/${device}.png" \ + --golden "/work/test/golden/${device}.png" "${approve[@]}" || failed=1 +done + +((failed == 0)) || die "screenshot checks failed; see build/screenshots/" +log "screenshots checked on: $*" diff --git a/docker/scripts/sdk-setup.sh b/docker/scripts/sdk-setup.sh index 278b060..1f14c58 100755 --- a/docker/scripts/sdk-setup.sh +++ b/docker/scripts/sdk-setup.sh @@ -1,4 +1,7 @@ #!/usr/bin/env bash +# Author: cirakg & claude ai +# Summary: Interactive `make sdk`: accept Garmin's agreements, log in, download the SDK and device files. +# # 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. diff --git a/docker/scripts/sim-vnc.sh b/docker/scripts/sim-vnc.sh new file mode 100755 index 0000000..207db09 --- /dev/null +++ b/docker/scripts/sim-vnc.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Author: cirakg & claude ai +# Summary: Run the app in the simulator and show the simulator in your browser through noVNC. +# +# Usage: sim-vnc.sh DEVICE (the Makefile publishes port 6080 on 127.0.0.1 only) +# View: http://127.0.0.1:6080/vnc.html, then "Connect". +# +# The VNC server listens only inside the container (localhost, no password); +# the only way in is the noVNC web socket that the Makefile publishes on your +# machine's 127.0.0.1. Stop with Ctrl-C. +set -euo pipefail +# shellcheck source=ciq-env.sh +source "$(dirname "${BASH_SOURCE[0]}")/ciq-env.sh" + +readonly VNC_PORT=5900 +readonly NOVNC_PORT=6080 + +device="${1:?usage: sim-vnc.sh DEVICE}" +ciq_require_sdk +ciq_require_key +build-app.sh debug "${device}" + +ciq_start_display +ciq_start_simulator +x11vnc -display "${DISPLAY}" -localhost -nopw -forever -shared -quiet -rfbport "${VNC_PORT}" \ + >/tmp/x11vnc.log 2>&1 & +websockify --web /usr/share/novnc "${NOVNC_PORT}" "localhost:${VNC_PORT}" >/tmp/websockify.log 2>&1 & + +cat >&2 <<EOF + + Simulator running with the app on ${device}. + Open http://127.0.0.1:${NOVNC_PORT}/vnc.html in your browser and press Connect. + The app's console output follows below. Press Ctrl-C to stop. + +EOF +monkeydo "${BUILD_DIR}/debug/${device}.prg" "${device}" +# monkeydo returns when the app exits; keep the simulator up until Ctrl-C. +log "the app exited; the simulator stays up until you press Ctrl-C" +wait diff --git a/pyproject.toml b/pyproject.toml index c92bed4..ba959eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,6 @@ +# Author: cirakg & claude ai +# Summary: Settings for pytest, ruff and mypy, which check the Python tools in tools/. +# # 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. @@ -6,6 +9,8 @@ testpaths = ["tools/tests"] cache_dir = "build/.pytest_cache" addopts = "-p no:cacheprovider" +# Warnings are errors here too (README section 17.7). +filterwarnings = ["error"] [tool.ruff] line-length = 120 |
