Compare commits
3 Commits
7a77c4a07d
..
v0.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
| a3e437277f | |||
| ff2b43f1e0 | |||
| 4e83ebbf50 |
@@ -1,7 +1,11 @@
|
|||||||
name: build
|
name: build
|
||||||
|
|
||||||
# Build a versioned Linux CLI release tarball (see `make package`) on
|
# Build a versioned Linux CLI release tarball (see `make package`) and the
|
||||||
# every push/PR, plus on-demand via the Gitea "Run workflow" button.
|
# Pebble watchapp (see `make watchapp`) on every push/PR, plus on-demand
|
||||||
|
# via the Gitea "Run workflow" button. Runs inside build-image (see
|
||||||
|
# ../../build-image/Dockerfile, built/pushed via ../../build-image.sh and
|
||||||
|
# ../../upload-image.sh), which bundles the Pebble SDK alongside the C
|
||||||
|
# toolchain.
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -17,12 +21,18 @@ jobs:
|
|||||||
# Must match a label your act_runner is registered with.
|
# Must match a label your act_runner is registered with.
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: cr.ladkau.de/deck-in-a-dash/builder:latest
|
||||||
|
credentials:
|
||||||
|
username: ${{ secrets.REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
# `make package`'s version comes from `git describe --tags` (see
|
# `make package`/`make watchapp`'s version comes from `git
|
||||||
# the Makefile's `package` target) - needs full history/tags,
|
# describe --tags` - needs full history/tags, not
|
||||||
# not actions/checkout's default shallow single-commit clone.
|
# actions/checkout's default shallow single-commit clone.
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Preflight
|
- name: Preflight
|
||||||
@@ -30,12 +40,14 @@ jobs:
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
command -v cc >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: no C compiler (cc) in PATH" >&2; exit 1; }
|
command -v cc >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: no C compiler (cc) in PATH" >&2; exit 1; }
|
||||||
command -v make >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: make not in PATH" >&2; exit 1; }
|
command -v make >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: make not in PATH" >&2; exit 1; }
|
||||||
|
command -v pebble >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: pebble not in PATH" >&2; exit 1; }
|
||||||
|
|
||||||
- name: Build and test
|
- name: Build and test
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
make test
|
make test
|
||||||
make package
|
make package
|
||||||
|
make watchapp
|
||||||
|
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
# v4 uses the newer @actions/artifact backend, which this Gitea
|
# v4 uses the newer @actions/artifact backend, which this Gitea
|
||||||
@@ -45,14 +57,21 @@ jobs:
|
|||||||
name: deck-in-a-dash
|
name: deck-in-a-dash
|
||||||
path: dist/deck-in-a-dash-*-linux-*.tar.gz
|
path: dist/deck-in-a-dash-*-linux-*.tar.gz
|
||||||
|
|
||||||
|
- name: Upload watchapp artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: deck-in-a-dash-watchapp
|
||||||
|
path: dist/deck-in-a-dash-*.pbw
|
||||||
|
|
||||||
- name: Publish to dl.ladkau.de
|
- name: Publish to dl.ladkau.de
|
||||||
# Uploads the tarball over SFTP instead of using
|
# Uploads the tarball and .pbw over SFTP instead of using
|
||||||
# actions/upload-artifact (whose zip wrapping can't be disabled).
|
# actions/upload-artifact (whose zip wrapping can't be disabled).
|
||||||
# Only runs on push so PR builds don't publish.
|
# Only runs on push so PR builds don't publish.
|
||||||
if: gitea.event_name == 'push'
|
if: gitea.event_name == 'push'
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
FILE="$(ls dist/deck-in-a-dash-*-linux-*.tar.gz)"
|
TARBALL="$(ls dist/deck-in-a-dash-*-linux-*.tar.gz)"
|
||||||
|
PBW="$(ls dist/deck-in-a-dash-*.pbw)"
|
||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
echo "${{ secrets.DL_SFTP_KEY }}" > ~/.ssh/dl_sftp_key
|
echo "${{ secrets.DL_SFTP_KEY }}" > ~/.ssh/dl_sftp_key
|
||||||
chmod 600 ~/.ssh/dl_sftp_key
|
chmod 600 ~/.ssh/dl_sftp_key
|
||||||
@@ -60,5 +79,6 @@ jobs:
|
|||||||
-o StrictHostKeyChecking=accept-new \
|
-o StrictHostKeyChecking=accept-new \
|
||||||
uploader@dl.ladkau.de <<EOF
|
uploader@dl.ladkau.de <<EOF
|
||||||
-mkdir files/deck-in-a-dash
|
-mkdir files/deck-in-a-dash
|
||||||
put $FILE files/deck-in-a-dash/$(basename "$FILE")
|
put $TARBALL files/deck-in-a-dash/$(basename "$TARBALL")
|
||||||
|
put $PBW files/deck-in-a-dash/$(basename "$PBW")
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
+10
@@ -7,6 +7,12 @@
|
|||||||
/watch/resources/img
|
/watch/resources/img
|
||||||
/watch/resources/app_icon.png
|
/watch/resources/app_icon.png
|
||||||
|
|
||||||
|
# waf's own build lock file - regenerated every `pebble build`, holds
|
||||||
|
# absolute machine paths and a full dump of the invoking shell's
|
||||||
|
# environment variables. Was tracked accidentally; see the commit that
|
||||||
|
# added this .gitignore line for the git rm --cached that untracked it.
|
||||||
|
/watch/.lock-waf_linux_build
|
||||||
|
|
||||||
# Durable backup of the same real birth data, outside dist/ so a dist/
|
# Durable backup of the same real birth data, outside dist/ so a dist/
|
||||||
# wipe doesn't lose it — see the Makefile's `scripts` target. Never
|
# wipe doesn't lose it — see the Makefile's `scripts` target. Never
|
||||||
# commit this either.
|
# commit this either.
|
||||||
@@ -17,3 +23,7 @@ core
|
|||||||
core.*
|
core.*
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Container registry credentials for build-image.sh/run-image.sh/
|
||||||
|
# upload-image.sh — never commit; copy registry.env.example instead.
|
||||||
|
/registry.env
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ make # builds dist/{deck-engine,interpreter-cli}, dist/img/, dist/i18n/
|
|||||||
make test # builds and runs engine/tests/smoke_test.c and interpreter/tests/*_test.c
|
make test # builds and runs engine/tests/smoke_test.c and interpreter/tests/*_test.c
|
||||||
make clean # removes build/ and the generated binaries/img/i18n (never touches dist/user.properties)
|
make clean # removes build/ and the generated binaries/img/i18n (never touches dist/user.properties)
|
||||||
make package # builds a versioned Linux CLI tarball at dist/ - see "Packaging" below
|
make package # builds a versioned Linux CLI tarball at dist/ - see "Packaging" below
|
||||||
|
make watchapp # builds the Pebble watchapp and copies a versioned .pbw to
|
||||||
|
# dist/ - see "Packaging" below. Requires the Pebble SDK
|
||||||
|
# (not needed for anything else above); not run by CI.
|
||||||
```
|
```
|
||||||
|
|
||||||
A single `Makefile` at the repo root builds both `engine/` and
|
A single `Makefile` at the repo root builds both `engine/` and
|
||||||
@@ -101,6 +104,50 @@ published over SFTP to `dl.ladkau.de` under `files/deck-in-a-dash/` -
|
|||||||
requires a `DL_SFTP_KEY` secret configured on this repo (or inherited
|
requires a `DL_SFTP_KEY` secret configured on this repo (or inherited
|
||||||
from the org/instance level).
|
from the org/instance level).
|
||||||
|
|
||||||
|
### Watch app packaging (`make watchapp`)
|
||||||
|
|
||||||
|
A separate target, deliberately **not** a prerequisite of `all` - it
|
||||||
|
shells out to `pebble build` inside `watch/` (via `watch/wscript`'s own
|
||||||
|
Pebble SDK/waf tooling, not this Makefile's plain-`cc` rules) and copies
|
||||||
|
the resulting `watch/build/watch.pbw` to
|
||||||
|
`dist/deck-in-a-dash-<version>.pbw`, using the exact same version
|
||||||
|
resolution as `make package` above (current git tag, or `make watchapp
|
||||||
|
VERSION=1.2.3` to override - see that section, not duplicated here).
|
||||||
|
Requires the Pebble SDK (`pebble` on `PATH`, plus the Python 3 + Pillow
|
||||||
|
`watch/wscript` needs for its resource-generation scripts) - a machine
|
||||||
|
without it can still build/test/package the CLI via every other target
|
||||||
|
in this file.
|
||||||
|
|
||||||
|
`.gitea/workflows/build.yml` **does** run `make watchapp` on every
|
||||||
|
push/PR, alongside `make test`/`make package` - the whole job runs
|
||||||
|
inside a container image (`build-image/`, built and pushed with
|
||||||
|
`build-image.sh`/`upload-image.sh`, see "Build image" below) that has
|
||||||
|
the Pebble SDK baked in, so the CI runner itself needs no toolchain
|
||||||
|
beyond Docker access to pull that image. Both the CLI tarball and the
|
||||||
|
`.pbw` are uploaded as build artifacts and published to
|
||||||
|
`dl.ladkau.de/files/deck-in-a-dash/` on push.
|
||||||
|
|
||||||
|
### Build image
|
||||||
|
|
||||||
|
`build-image/Dockerfile` pins the exact toolchain versions (Pebble
|
||||||
|
Tool, Pebble SDK core, Node.js) needed for `make test`, `make package`,
|
||||||
|
and `make watchapp`, so builds are reproducible independent of whatever
|
||||||
|
happens to be installed on a given machine. Three root-level scripts
|
||||||
|
drive it, each reading registry settings from a gitignored
|
||||||
|
`registry.env` (copy `registry.env.example` to create it - never commit
|
||||||
|
the real file, since it holds registry credentials) and the image tag
|
||||||
|
from `build-image/VERSION`:
|
||||||
|
|
||||||
|
- `build-image.sh` - builds and locally tags the image (`:VERSION` and
|
||||||
|
`:latest`), no push.
|
||||||
|
- `run-image.sh [VERSION]` - runs `make test package watchapp` inside
|
||||||
|
the locally-built image against a live bind-mount of the repo, wiping
|
||||||
|
generated build artifacts first for CI parity. Use this to validate a
|
||||||
|
`build-image/Dockerfile` change before pushing the image anywhere.
|
||||||
|
- `upload-image.sh` - logs in and pushes the image (`:VERSION` and
|
||||||
|
`:latest`) to the registry; `.gitea/workflows/build.yml` pulls
|
||||||
|
`:latest` to run its build job (see "Watch app packaging" above).
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -87,7 +87,12 @@ GUIDANCE_TEST_BIN := $(BUILD_DIR)/guidance_test
|
|||||||
ARCH := $(shell uname -m)
|
ARCH := $(shell uname -m)
|
||||||
PACKAGE_DIR := $(BUILD_DIR)/package
|
PACKAGE_DIR := $(BUILD_DIR)/package
|
||||||
|
|
||||||
.PHONY: all test clean images i18n scripts package
|
# ---- watch app (Pebble) ----
|
||||||
|
|
||||||
|
WATCH_DIR := watch
|
||||||
|
WATCH_PBW := $(WATCH_DIR)/build/watch.pbw
|
||||||
|
|
||||||
|
.PHONY: all test clean images i18n scripts package watchapp
|
||||||
|
|
||||||
all: $(ENGINE_BINARY) $(INTERP_BINARY) images i18n scripts
|
all: $(ENGINE_BINARY) $(INTERP_BINARY) images i18n scripts
|
||||||
|
|
||||||
@@ -208,3 +213,32 @@ clean:
|
|||||||
rm -rf $(BUILD_DIR) $(ENGINE_BINARY) $(INTERP_BINARY) $(DIST_IMG_DIR) $(DIST_I18N_DIR) \
|
rm -rf $(BUILD_DIR) $(ENGINE_BINARY) $(INTERP_BINARY) $(DIST_IMG_DIR) $(DIST_I18N_DIR) \
|
||||||
$(DIST_DIR)/run-engine.sh $(DIST_DIR)/run-interpreter.sh
|
$(DIST_DIR)/run-engine.sh $(DIST_DIR)/run-interpreter.sh
|
||||||
# user.properties holds the user's own data - never removed by clean.
|
# user.properties holds the user's own data - never removed by clean.
|
||||||
|
|
||||||
|
# Builds the Pebble watchapp (`pebble build` in watch/, via its own
|
||||||
|
# wscript/pebble_sdk waf tooling - not a plain C toolchain, so this is a
|
||||||
|
# separate target from `all` rather than one of its prerequisites) and
|
||||||
|
# copies the resulting .pbw into dist/, versioned the same way `package`
|
||||||
|
# above versions the CLI tarball (current git tag, or
|
||||||
|
# `make watchapp VERSION=1.2.3` to override). Requires the Pebble SDK
|
||||||
|
# (`pebble` on PATH, plus Python 3 + Pillow for watch/scripts/*.py - see
|
||||||
|
# watch/wscript) - this is NOT checked by the root `make`/`make test`,
|
||||||
|
# so a machine without the Pebble SDK can still build/test the CLI.
|
||||||
|
watchapp:
|
||||||
|
cd $(WATCH_DIR) && pebble build
|
||||||
|
@mkdir -p $(DIST_DIR)
|
||||||
|
@V="$(VERSION)"; \
|
||||||
|
if [ -z "$$V" ]; then \
|
||||||
|
if TAG=$$(git describe --tags --exact-match --match 'v[0-9]*.[0-9]*.[0-9]*' 2>/dev/null); then \
|
||||||
|
V=$${TAG#v}; \
|
||||||
|
else \
|
||||||
|
V="0.0.0-dev+$$(git rev-parse --short HEAD)"; \
|
||||||
|
echo "WARNING: HEAD is not on a vX.Y.Z tag - building placeholder version $$V (push a tag to drive a real release version)" >&2; \
|
||||||
|
fi; \
|
||||||
|
fi; \
|
||||||
|
case "$$V" in \
|
||||||
|
[0-9]*.[0-9]*.[0-9]*) ;; \
|
||||||
|
*) echo "PREFLIGHT FAIL: VERSION '$$V' is not a semantic version (expected X.Y.Z, optionally with a -pre+meta suffix)" >&2; exit 1;; \
|
||||||
|
esac; \
|
||||||
|
PBW_NAME="deck-in-a-dash-$$V.pbw"; \
|
||||||
|
cp $(WATCH_PBW) "$(DIST_DIR)/$$PBW_NAME"; \
|
||||||
|
echo "Wrote $(DIST_DIR)/$$PBW_NAME"
|
||||||
|
|||||||
@@ -52,7 +52,18 @@ interpreter/ Separate module + binary (dist/interpreter-cli)
|
|||||||
deck-engine's --format json output. Supports
|
deck-engine's --format json output. Supports
|
||||||
--format text|html|json and --lang en|de for
|
--format text|html|json and --lang en|de for
|
||||||
its own output too (see CLAUDE.md).
|
its own output too (see CLAUDE.md).
|
||||||
|
watch/ The actual Pebble watchapp - links engine/src/
|
||||||
|
and interpreter/src/ in directly (see CLAUDE.md's
|
||||||
|
"Portability to the watch"), plus its own
|
||||||
|
persistence, reading recompute, UI, daily
|
||||||
|
notification, and config page. Built separately
|
||||||
|
from the rest of this repo (`make watchapp`,
|
||||||
|
below) - requires the Pebble SDK.
|
||||||
docs/ Architecture/dataflow diagrams, input/output format reference.
|
docs/ Architecture/dataflow diagrams, input/output format reference.
|
||||||
|
build-image/ Dockerfile for the reproducible build environment
|
||||||
|
(C toolchain + Pebble SDK) used by CI and by the
|
||||||
|
build-image.sh/run-image.sh/upload-image.sh scripts
|
||||||
|
at the repo root (see "Building" below).
|
||||||
Makefile Single root Makefile, builds both engine/ and
|
Makefile Single root Makefile, builds both engine/ and
|
||||||
interpreter/, and packages a release (`make package`).
|
interpreter/, and packages a release (`make package`).
|
||||||
```
|
```
|
||||||
@@ -78,6 +89,29 @@ make package # -> dist/deck-in-a-dash-<version>-linux-<arch>.tar.gz, a
|
|||||||
|
|
||||||
Requires only a C99 compiler and `make` — no other dependencies.
|
Requires only a C99 compiler and `make` — no other dependencies.
|
||||||
|
|
||||||
|
**Building the watch app itself** (`watch/`, the actual Pebble app)
|
||||||
|
needs the separate Pebble SDK (`pebble` on `PATH`), so it's a separate
|
||||||
|
target, not part of `make`/`make test`/`make package` above:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make watchapp # -> dist/deck-in-a-dash-<version>.pbw, same version
|
||||||
|
# resolution as `make package`
|
||||||
|
```
|
||||||
|
|
||||||
|
**CI runs `make watchapp` too** — `.gitea/workflows/build.yml` builds
|
||||||
|
and tests everything inside a container built from `build-image/`,
|
||||||
|
which bundles the Pebble SDK alongside the C toolchain so the runner
|
||||||
|
itself doesn't need either installed. To build/test that image
|
||||||
|
yourself, copy `registry.env.example` to `registry.env` and fill in
|
||||||
|
your registry credentials, then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build-image.sh # build and locally tag the image
|
||||||
|
./run-image.sh # run `make test package watchapp` inside it, against
|
||||||
|
# this checkout, for a CI-equivalent local build
|
||||||
|
./upload-image.sh # push the image so CI can pull it
|
||||||
|
```
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
**Daily use**, via `dist/run-engine.sh`: pulls today's date and the
|
**Daily use**, via `dist/run-engine.sh`: pulls today's date and the
|
||||||
|
|||||||
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Builds the release build environment image (build-image/Dockerfile)
|
||||||
|
# locally, tagged with build-image/VERSION and :latest. Does not push -
|
||||||
|
# test it with ./run-image.sh first, then publish with ./upload-image.sh.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
ROOT="$(pwd)"
|
||||||
|
|
||||||
|
fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
command -v docker >/dev/null 2>&1 \
|
||||||
|
|| fail "docker not found in PATH"
|
||||||
|
|
||||||
|
[ -f "$ROOT/registry.env" ] \
|
||||||
|
|| fail "registry.env not found — copy registry.env.example to registry.env and fill in your cr.ladkau.de credentials"
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$ROOT/registry.env"
|
||||||
|
|
||||||
|
: "${REGISTRY_IMAGE:?registry.env must set REGISTRY_IMAGE}"
|
||||||
|
|
||||||
|
VERSION="$(<"$ROOT/build-image/VERSION")"
|
||||||
|
[ -n "$VERSION" ] || fail "build-image/VERSION is empty"
|
||||||
|
|
||||||
|
echo "== Building $REGISTRY_IMAGE:$VERSION =="
|
||||||
|
docker build \
|
||||||
|
-t "$REGISTRY_IMAGE:$VERSION" \
|
||||||
|
-t "$REGISTRY_IMAGE:latest" \
|
||||||
|
"$ROOT/build-image"
|
||||||
|
|
||||||
|
echo "== Done =="
|
||||||
|
echo "Image: $REGISTRY_IMAGE:$VERSION (and :latest)"
|
||||||
|
echo "Test it with ./run-image.sh, then publish with ./upload-image.sh"
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Build environment for deck_in_a_dash release artifacts: a plain C
|
||||||
|
# toolchain for the desktop deck-engine/interpreter-cli binaries (`make
|
||||||
|
# test`, `make package`) plus the Pebble SDK for the watch app (`make
|
||||||
|
# watchapp`), pinned to the versions validated on the maintainer's dev
|
||||||
|
# machine. Rebuild with ../build-image.sh whenever a version below
|
||||||
|
# changes.
|
||||||
|
#
|
||||||
|
# Does NOT contain any secrets - registry/SFTP credentials are injected
|
||||||
|
# at job runtime from Gitea Actions secrets, never baked into this image.
|
||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
ARG PEBBLE_TOOL_VERSION=5.0.39
|
||||||
|
ARG PEBBLE_SDK_CORE_VERSION=4.17
|
||||||
|
ARG NODE_VERSION=24.16.0
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
|
PATH=/root/.local/bin:${PATH}
|
||||||
|
|
||||||
|
# build-essential: gcc/make for the desktop deck-engine/interpreter-cli
|
||||||
|
# binaries (`make test`, `make package`).
|
||||||
|
# python3/python3-venv/python3-pip: `pebble sdk install` below creates a
|
||||||
|
# venv per SDK version; Pillow (installed below) is needed at `pebble
|
||||||
|
# build` time by watch/scripts/gen_card_images.py and gen_app_icon.py.
|
||||||
|
# git/curl/ca-certificates/unzip/tar/xz-utils: checkout, tool downloads,
|
||||||
|
# and `make package`/`make watchapp`'s `git describe --tags` versioning.
|
||||||
|
# openssh-client: the build workflow's `sftp` publish to dl.ladkau.de.
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential python3 python3-venv python3-pip \
|
||||||
|
git curl ca-certificates unzip tar xz-utils openssh-client \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN pip3 install --no-cache-dir Pillow
|
||||||
|
|
||||||
|
# Node.js: `pebble sdk install` below runs `npm install` for the SDK
|
||||||
|
# core's bundled webpack tooling - it does not bring its own node/npm.
|
||||||
|
RUN curl -sSL -o /tmp/node.tar.xz \
|
||||||
|
"https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" \
|
||||||
|
&& tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1 \
|
||||||
|
&& rm /tmp/node.tar.xz
|
||||||
|
|
||||||
|
# Pebble SDK: pebble-tool (via uv, pipx-style isolated install) + the
|
||||||
|
# sdk-core toolchain/webpack deps for PEBBLE_SDK_CORE_VERSION, both
|
||||||
|
# fully resolved here so no `docker run` of this image needs network
|
||||||
|
# access to install anything before `pebble build` works.
|
||||||
|
RUN curl -LsSf https://astral.sh/uv/install.sh | sh \
|
||||||
|
&& uv tool install "pebble-tool==${PEBBLE_TOOL_VERSION}" \
|
||||||
|
&& pebble sdk install "${PEBBLE_SDK_CORE_VERSION}"
|
||||||
|
|
||||||
|
WORKDIR /workspace
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Copy to registry.env (gitignored, never commit the real values) and
|
||||||
|
# fill in your cr.ladkau.de credentials. Used by build-image.sh,
|
||||||
|
# run-image.sh, and upload-image.sh.
|
||||||
|
REGISTRY=cr.ladkau.de
|
||||||
|
REGISTRY_IMAGE=cr.ladkau.de/deck-in-a-dash/builder
|
||||||
|
REGISTRY_USER=
|
||||||
|
REGISTRY_PASSWORD=
|
||||||
Executable
+69
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Runs `make test package watchapp` inside the local build-image
|
||||||
|
# container, mirroring what the Gitea Actions build workflow does -
|
||||||
|
# useful for testing build-image changes locally before pushing
|
||||||
|
# anything to cr.ladkau.de.
|
||||||
|
#
|
||||||
|
# Usage: ./run-image.sh [VERSION]
|
||||||
|
# VERSION is passed through to `make` (see the Makefile's `package`/
|
||||||
|
# `watchapp` targets); omit it for their own git-tag-based default.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
ROOT="$(pwd)"
|
||||||
|
|
||||||
|
fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
command -v docker >/dev/null 2>&1 \
|
||||||
|
|| fail "docker not found in PATH"
|
||||||
|
|
||||||
|
[ -f "$ROOT/registry.env" ] \
|
||||||
|
|| fail "registry.env not found — copy registry.env.example to registry.env and fill in your cr.ladkau.de credentials"
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$ROOT/registry.env"
|
||||||
|
|
||||||
|
: "${REGISTRY_IMAGE:?registry.env must set REGISTRY_IMAGE}"
|
||||||
|
|
||||||
|
IMAGE_TAG="$(<"$ROOT/build-image/VERSION")"
|
||||||
|
[ -n "$IMAGE_TAG" ] || fail "build-image/VERSION is empty"
|
||||||
|
IMAGE="$REGISTRY_IMAGE:$IMAGE_TAG"
|
||||||
|
|
||||||
|
docker image inspect "$IMAGE" >/dev/null 2>&1 \
|
||||||
|
|| fail "$IMAGE not found locally — run ./build-image.sh first"
|
||||||
|
|
||||||
|
VERSION="${1:-${VERSION:-}}"
|
||||||
|
|
||||||
|
# CI always starts from a fresh checkout, but this script bind-mounts the
|
||||||
|
# live host repo — so build output left over from a previous local run
|
||||||
|
# (build/, dist/, watch/build, and the generated watch resources below)
|
||||||
|
# would make this run skip work a real fresh checkout wouldn't, silently
|
||||||
|
# hiding bugs that only show up in CI. Wipe them first so every run
|
||||||
|
# exercises a true from-scratch build, same as CI.
|
||||||
|
echo "== Cleaning generated build artifacts for a fresh build =="
|
||||||
|
rm -rf \
|
||||||
|
"$ROOT/build" "$ROOT/dist" "$ROOT/watch/build" \
|
||||||
|
"$ROOT/watch/resources/img" "$ROOT/watch/resources/app_icon.png" \
|
||||||
|
"$ROOT/watch/src/c/i18n_tables.auto.c"
|
||||||
|
|
||||||
|
echo "== Running make test package watchapp inside $IMAGE =="
|
||||||
|
# The container runs as root (needed for the toolchain baked into the
|
||||||
|
# image), so anything it writes into this bind mount — dist/, build/,
|
||||||
|
# watch/build, etc. — would otherwise come back owned by root, leaving
|
||||||
|
# the host repo unusable without sudo. Chown everything back to the host
|
||||||
|
# user on exit, whether the build succeeds or fails.
|
||||||
|
docker run --rm \
|
||||||
|
-v "$ROOT:/workspace" \
|
||||||
|
-w /workspace \
|
||||||
|
-e VERSION="$VERSION" \
|
||||||
|
-e HOST_UID="$(id -u)" \
|
||||||
|
-e HOST_GID="$(id -g)" \
|
||||||
|
"$IMAGE" \
|
||||||
|
bash -c '
|
||||||
|
git config --global --add safe.directory /workspace
|
||||||
|
trap "chown -R \"$HOST_UID:$HOST_GID\" /workspace" EXIT
|
||||||
|
make test package watchapp
|
||||||
|
'
|
||||||
|
|
||||||
|
echo "== Done — artifacts in dist/ =="
|
||||||
|
ls -la "$ROOT/dist"
|
||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Pushes the build environment image — already built locally with
|
||||||
|
# ./build-image.sh, and ideally verified with ./run-image.sh — to
|
||||||
|
# cr.ladkau.de. The Gitea Actions build workflow pulls this image to run
|
||||||
|
# `make test package watchapp`.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
ROOT="$(pwd)"
|
||||||
|
|
||||||
|
fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
command -v docker >/dev/null 2>&1 \
|
||||||
|
|| fail "docker not found in PATH"
|
||||||
|
|
||||||
|
[ -f "$ROOT/registry.env" ] \
|
||||||
|
|| fail "registry.env not found — copy registry.env.example to registry.env and fill in your cr.ladkau.de credentials"
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$ROOT/registry.env"
|
||||||
|
|
||||||
|
: "${REGISTRY:?registry.env must set REGISTRY}"
|
||||||
|
: "${REGISTRY_IMAGE:?registry.env must set REGISTRY_IMAGE}"
|
||||||
|
: "${REGISTRY_USER:?registry.env must set REGISTRY_USER}"
|
||||||
|
: "${REGISTRY_PASSWORD:?registry.env must set REGISTRY_PASSWORD}"
|
||||||
|
|
||||||
|
VERSION="$(<"$ROOT/build-image/VERSION")"
|
||||||
|
[ -n "$VERSION" ] || fail "build-image/VERSION is empty"
|
||||||
|
|
||||||
|
docker image inspect "$REGISTRY_IMAGE:$VERSION" >/dev/null 2>&1 \
|
||||||
|
|| fail "$REGISTRY_IMAGE:$VERSION not found locally — run ./build-image.sh first"
|
||||||
|
|
||||||
|
echo "== Logging in to $REGISTRY =="
|
||||||
|
echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
|
||||||
|
|
||||||
|
echo "== Pushing $REGISTRY_IMAGE:$VERSION and :latest =="
|
||||||
|
docker push "$REGISTRY_IMAGE:$VERSION"
|
||||||
|
docker push "$REGISTRY_IMAGE:latest"
|
||||||
|
|
||||||
|
echo "== Done =="
|
||||||
|
echo "Image: $REGISTRY_IMAGE:$VERSION"
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "app_message.h"
|
#include "app_message.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "i18n_tables.h"
|
#include "i18n_tables.h"
|
||||||
|
#include "notify.h"
|
||||||
#include "reading_state.h"
|
#include "reading_state.h"
|
||||||
#include "ui_report_window.h"
|
#include "ui_report_window.h"
|
||||||
#include "ui_text_window.h"
|
#include "ui_text_window.h"
|
||||||
@@ -26,6 +27,7 @@ static void on_config_updated(const WatchConfig *cfg) {
|
|||||||
|
|
||||||
config_log(&s_config); /* the values this newly-recomputed reading is based on */
|
config_log(&s_config); /* the values this newly-recomputed reading is based on */
|
||||||
reading_state_recompute(&s_config.birth, s_config.gender);
|
reading_state_recompute(&s_config.birth, s_config.gender);
|
||||||
|
notify_reschedule(&s_config); /* settings may have changed notify_enabled/hour/minute */
|
||||||
if (was_configured) {
|
if (was_configured) {
|
||||||
report_window_reload();
|
report_window_reload();
|
||||||
} else {
|
} else {
|
||||||
@@ -33,16 +35,36 @@ static void on_config_updated(const WatchConfig *cfg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fires only if the scheduled wakeup goes off while this app is already
|
||||||
|
* open (notify_was_wakeup_launch() below covers the far more common
|
||||||
|
* case: the wakeup launching the app fresh). Refreshes the same way a
|
||||||
|
* new day's config submission would, since the report window is already
|
||||||
|
* on screen and the reading may now be for a new day. */
|
||||||
|
static void on_wakeup(WakeupId wakeup_id, int32_t cookie) {
|
||||||
|
(void)wakeup_id;
|
||||||
|
(void)cookie;
|
||||||
|
vibes_double_pulse();
|
||||||
|
if (!s_config.configured) return;
|
||||||
|
reading_state_recompute(&s_config.birth, s_config.gender);
|
||||||
|
report_window_reload();
|
||||||
|
notify_reschedule(&s_config); /* wakeup_schedule() is one-shot - re-arm for tomorrow */
|
||||||
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
config_load(&s_config);
|
config_load(&s_config);
|
||||||
watch_i18n_load(s_config.lang);
|
watch_i18n_load(s_config.lang);
|
||||||
app_message_init(on_config_updated);
|
app_message_init(on_config_updated);
|
||||||
|
notify_subscribe(on_wakeup);
|
||||||
|
|
||||||
config_log(&s_config); /* the values this session's reading (if any) is based on */
|
config_log(&s_config); /* the values this session's reading (if any) is based on */
|
||||||
|
|
||||||
if (s_config.configured) {
|
if (s_config.configured) {
|
||||||
reading_state_recompute(&s_config.birth, s_config.gender);
|
reading_state_recompute(&s_config.birth, s_config.gender);
|
||||||
report_window_push();
|
report_window_push();
|
||||||
|
notify_reschedule(&s_config); /* wakeup_schedule() is one-shot - re-arm on every launch */
|
||||||
|
if (notify_was_wakeup_launch()) {
|
||||||
|
vibes_double_pulse();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
show_setup_required();
|
show_setup_required();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#include "notify.h"
|
||||||
|
|
||||||
|
/* Only one kind of wakeup event exists in this app, so the cookie value
|
||||||
|
* itself carries no information - it exists only because
|
||||||
|
* wakeup_schedule()/wakeup_get_launch_event() require one. */
|
||||||
|
#define NOTIFY_WAKEUP_COOKIE 0
|
||||||
|
|
||||||
|
void notify_reschedule(const WatchConfig *cfg) {
|
||||||
|
wakeup_cancel_all();
|
||||||
|
if (!cfg->notify_enabled) return;
|
||||||
|
|
||||||
|
time_t now = time(NULL);
|
||||||
|
struct tm target_tm = *localtime(&now);
|
||||||
|
target_tm.tm_hour = cfg->notify_hour;
|
||||||
|
target_tm.tm_min = cfg->notify_minute;
|
||||||
|
target_tm.tm_sec = 0;
|
||||||
|
|
||||||
|
time_t target = mktime(&target_tm);
|
||||||
|
if (target <= now) target += SECONDS_PER_DAY; /* today's time already passed */
|
||||||
|
|
||||||
|
WakeupId id = wakeup_schedule(target, NOTIFY_WAKEUP_COOKIE, true);
|
||||||
|
if (id < 0) {
|
||||||
|
APP_LOG(APP_LOG_LEVEL_ERROR, "notify: wakeup_schedule failed: %d", (int)id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool notify_was_wakeup_launch(void) {
|
||||||
|
return launch_reason() == APP_LAUNCH_WAKEUP;
|
||||||
|
}
|
||||||
|
|
||||||
|
void notify_subscribe(WakeupHandler handler) {
|
||||||
|
wakeup_service_subscribe(handler);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#ifndef WATCH_NOTIFY_H
|
||||||
|
#define WATCH_NOTIFY_H
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
/* (Re)schedules the daily notification wakeup from `cfg`'s notify_*
|
||||||
|
* fields: cancels any wakeup this app previously scheduled, then - if
|
||||||
|
* cfg->notify_enabled - schedules a new one-shot wakeup_schedule() for
|
||||||
|
* the next occurrence of notify_hour:notify_minute in the watch's own
|
||||||
|
* local time (today if that time hasn't passed yet, tomorrow otherwise).
|
||||||
|
* If !cfg->notify_enabled, this only cancels - no new wakeup is
|
||||||
|
* scheduled.
|
||||||
|
*
|
||||||
|
* Safe to call any time, and needs to be: once per app launch (Pebble's
|
||||||
|
* wakeup_schedule() is one-shot - each firing consumes itself, so the
|
||||||
|
* next day's has to be scheduled again after it fires or after a wakeup
|
||||||
|
* that arrives while the app happens to already be open - see
|
||||||
|
* notify_subscribe()) and on every config submission (settings may have
|
||||||
|
* changed notify_enabled/notify_hour/notify_minute). */
|
||||||
|
void notify_reschedule(const WatchConfig *cfg);
|
||||||
|
|
||||||
|
/* True if this app launch was triggered by the daily notification wakeup
|
||||||
|
* firing rather than the user/phone opening it normally - main.c uses
|
||||||
|
* this to vibrate on launch, since the report window it pushes either
|
||||||
|
* way already leads with day significance and the top transit (the
|
||||||
|
* "quick digest" this notification promises), so no separate screen is
|
||||||
|
* needed for it. */
|
||||||
|
bool notify_was_wakeup_launch(void);
|
||||||
|
|
||||||
|
/* Subscribes `handler` to fire if the scheduled wakeup goes off while
|
||||||
|
* this app is already running in the foreground - the only case
|
||||||
|
* notify_was_wakeup_launch() can't see, since no fresh launch happens
|
||||||
|
* then (see wakeup_service_subscribe()'s own doc comment in pebble.h).
|
||||||
|
* Call once, early in main(). */
|
||||||
|
void notify_subscribe(WakeupHandler handler);
|
||||||
|
|
||||||
|
#endif
|
||||||
+10
-7
@@ -61,21 +61,24 @@ def configure(ctx):
|
|||||||
|
|
||||||
|
|
||||||
def build(ctx):
|
def build(ctx):
|
||||||
ctx.load('pebble_sdk')
|
|
||||||
|
|
||||||
# Regenerate src/c/i18n_tables.auto.c from engine/i18n/*.lang,
|
# Regenerate src/c/i18n_tables.auto.c from engine/i18n/*.lang,
|
||||||
# resources/img/*.png from res/img/*.jpeg, and resources/app_icon.png
|
# resources/img/*.png from res/img/*.jpeg, and resources/app_icon.png
|
||||||
# from res/app_icon_50x50_64_color.png, before compiling/bundling
|
# from res/app_icon_50x50_64_color.png, before compiling/bundling
|
||||||
# anything - see each script's own doc comment. All three run eagerly
|
# anything - see each script's own doc comment. All three run eagerly
|
||||||
# (not as waf Tasks) since the bundle step below depends on their
|
# (not as waf Tasks) and must run *before* ctx.load('pebble_sdk')
|
||||||
# output existing; gen_card_images.py/gen_app_icon.py both skip
|
# below, since loading that tool in a build context immediately packs
|
||||||
# regenerating anything already up to date, so this stays cheap on
|
# resources/ into the app bundle - on a fresh checkout with no
|
||||||
# repeat builds. Requires Pillow (`pip install Pillow`) - see
|
# generated resources/img/ yet, that packing step would crash before
|
||||||
# watch/README.md.
|
# these scripts ever got a chance to create it. gen_card_images.py/
|
||||||
|
# gen_app_icon.py both skip regenerating anything already up to date,
|
||||||
|
# so this stays cheap on repeat builds. Requires Pillow (`pip install
|
||||||
|
# Pillow`) - see watch/README.md.
|
||||||
subprocess.check_call([sys.executable, 'scripts/gen_i18n_tables.py'])
|
subprocess.check_call([sys.executable, 'scripts/gen_i18n_tables.py'])
|
||||||
subprocess.check_call([sys.executable, 'scripts/gen_card_images.py'])
|
subprocess.check_call([sys.executable, 'scripts/gen_card_images.py'])
|
||||||
subprocess.check_call([sys.executable, 'scripts/gen_app_icon.py'])
|
subprocess.check_call([sys.executable, 'scripts/gen_app_icon.py'])
|
||||||
|
|
||||||
|
ctx.load('pebble_sdk')
|
||||||
|
|
||||||
engine_dir = ctx.path.parent.find_dir('engine/src')
|
engine_dir = ctx.path.parent.find_dir('engine/src')
|
||||||
engine_nodes = [engine_dir.find_node(name) for name in ENGINE_SRCS]
|
engine_nodes = [engine_dir.find_node(name) for name in ENGINE_SRCS]
|
||||||
interp_dir = ctx.path.parent.find_dir('interpreter/src')
|
interp_dir = ctx.path.parent.find_dir('interpreter/src')
|
||||||
|
|||||||
Reference in New Issue
Block a user