From 8881241fd871ee74a93ba197027b74f16d1ff327 Mon Sep 17 00:00:00 2001 From: ml Date: Sat, 4 Jul 2026 05:21:54 +0200 Subject: [PATCH] Split the monolithic build-image.sh into three scripts: build-image.sh now only builds locally, run-image.sh runs dist.sh inside that local image via a bind mount, andcupload-image.sh pushes the already-built image. The Dockerfile also warms the Gradle dependency cache in a throwaway build stage, and pre-installs the pinned CMake version, so `run-image.sh` doesn't re-download the same Maven dependencies on every run. dist.sh now strips the -pre+meta suffix before writing package.json's version, since Pebble's build tooling parses it strictly as X.Y.Z integers and was rejecting suffixed versions. --- .dockerignore | 32 +++++++++++++++++++++++++ .gitea/workflows/release.yml | 5 ++++ README.md | 6 +++-- build-image.sh | 25 +++++++------------- build-image/Dockerfile | 40 +++++++++++++++++++++++++++---- dist.sh | 6 ++++- docs/publish.md | 26 ++++++++++++++------ run-image.sh | 46 ++++++++++++++++++++++++++++++++++++ upload-image.sh | 41 ++++++++++++++++++++++++++++++++ 9 files changed, 197 insertions(+), 30 deletions(-) create mode 100644 .dockerignore create mode 100755 run-image.sh create mode 100755 upload-image.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..33f3244 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,32 @@ +# Build context for build-image/Dockerfile. The context is the repo root +# (see build-image.sh) so the Gradle-cache-warming stage can COPY in the +# companion app's real project files — everything else is excluded to keep +# the context small and to make sure secrets never reach the Docker daemon. +.git +dist +docs +versions +SchwertUndMagieOnPebbleWatchApp + +# Companion app: only the Gradle project files are needed (see the +# gradle-cache-warm stage) — not generated build output or the VICE/nibtools +# source tarballs (buildVice/buildNibtools are excluded from that stage's +# gradle invocation, so they're never unpacked there). +SchwertUndMagieOnPebbleCompanionApp/app/build +SchwertUndMagieOnPebbleCompanionApp/app/.cxx +SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni/vice-src +SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni/vice-libs +SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni/nibtools-src +SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni/nibtools-libs +SchwertUndMagieOnPebbleCompanionApp/res/*.tar.gz +SchwertUndMagieOnPebbleCompanionApp/.gradle +SchwertUndMagieOnPebbleCompanionApp/.idea +SchwertUndMagieOnPebbleCompanionApp/local.properties +SchwertUndMagieOnPebbleCompanionApp/build +SchwertUndMagieOnPebbleCompanionApp/captures + +# Secrets — must never reach the Docker daemon, even unused. +registry.env +**/keystore.properties +**/*.keystore +**/*.jks diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 624e66f..7966ae9 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -32,6 +32,11 @@ jobs: credentials: username: ${{ secrets.REGISTRY_USER }} password: ${{ secrets.REGISTRY_PASSWORD }} + # Without this, the runner's Docker daemon reuses whatever it already + # has cached locally under the `:latest` tag and never notices a newer + # image was pushed — options: is passed straight through to + # `docker create`, which supports --pull since Docker 20.10. + options: --pull=always steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 11d174d..fec7b08 100644 --- a/README.md +++ b/README.md @@ -150,8 +150,10 @@ Pushing a tag `vX.Y.Z` builds both apps in a containerized runner and publishes a Gitea Release with the versioned artifacts attached — see `docs/publish.md` §4. One-time setup: -1. Build and push the build environment image (`./build-image.sh`, needs - `registry.env` — copy from `registry.env.example`). +1. Build the build environment image locally and push it (`./build-image.sh` + then `./upload-image.sh`, needs `registry.env` — copy from + `registry.env.example`). Use `./run-image.sh` in between to sanity-check + the image before pushing. 2. Register a self-hosted `act_runner` with a Docker executor. 3. Add repo secrets under **Settings → Actions → Secrets**: diff --git a/build-image.sh b/build-image.sh index 083d91b..3415819 100755 --- a/build-image.sh +++ b/build-image.sh @@ -1,8 +1,7 @@ #!/usr/bin/env bash -# Builds and pushes the release build environment image (build-image/Dockerfile) -# to the container registry. The Gitea Actions release workflow pulls this -# image to run dist.sh. Bump build-image/VERSION whenever the Dockerfile -# changes so the workflow can pin a stable tag. +# 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]}")" @@ -19,27 +18,21 @@ command -v docker >/dev/null 2>&1 \ # 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" -echo "== Logging in to $REGISTRY ==" -echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin - echo "== Building $REGISTRY_IMAGE:$VERSION ==" +# Context is the repo root (not build-image/) so the Dockerfile's +# gradle-cache-warm stage can COPY in the companion app's real Gradle +# project files — see .dockerignore for what's excluded from that context. docker build \ -t "$REGISTRY_IMAGE:$VERSION" \ -t "$REGISTRY_IMAGE:latest" \ -f "$ROOT/build-image/Dockerfile" \ - "$ROOT/build-image" - -echo "== Pushing $REGISTRY_IMAGE:$VERSION and :latest ==" -docker push "$REGISTRY_IMAGE:$VERSION" -docker push "$REGISTRY_IMAGE:latest" + "$ROOT" echo "== Done ==" -echo "Image: $REGISTRY_IMAGE:$VERSION" +echo "Image: $REGISTRY_IMAGE:$VERSION (and :latest)" +echo "Test it with ./run-image.sh, then publish with ./upload-image.sh" diff --git a/build-image/Dockerfile b/build-image/Dockerfile index 33da4a0..118fe82 100644 --- a/build-image/Dockerfile +++ b/build-image/Dockerfile @@ -1,17 +1,18 @@ # Build environment for schwert_und_magie_on_pebble release artifacts: # Android SDK/NDK (companion app) + Pebble SDK (watch app), matching the # versions pinned in app/build.gradle.kts and validated on the maintainer's -# dev machine. Rebuild and push with ../build-image.sh whenever a version -# below, or the companion app's ndkVersion/compileSdk, changes. +# dev machine. Rebuild with ../build-image.sh whenever a version below, or +# the companion app's ndkVersion/compileSdk, changes. # # Does NOT contain the release keystore or any secrets — those are injected # at job runtime from Gitea Actions secrets, never baked into this image. -FROM eclipse-temurin:21-jdk-jammy +FROM eclipse-temurin:21-jdk-jammy AS base ARG ANDROID_CMDLINE_TOOLS_VERSION=11076708 ARG ANDROID_PLATFORM=android-36 ARG ANDROID_BUILD_TOOLS=36.1.0 ARG ANDROID_NDK=30.0.14904198 +ARG ANDROID_CMAKE=3.22.1 ARG PEBBLE_TOOL_VERSION=5.0.35 ARG PEBBLE_SDK_CORE_VERSION=4.9.169 ARG NODE_VERSION=24.16.0 @@ -35,7 +36,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ dos2unix autoconf automake pkg-config xa65 build-essential gettext flex \ && rm -rf /var/lib/apt/lists/* -# --- Android SDK: cmdline-tools, platform, build-tools, NDK --- +# --- Android SDK: cmdline-tools, platform, build-tools, NDK, CMake --- +# CMake version must match app/build.gradle.kts's externalNativeBuild.cmake.version +# — baking it in here avoids AGP installing it on first `docker run` instead. RUN mkdir -p "$ANDROID_HOME/cmdline-tools" \ && curl -sSL -o /tmp/cmdline-tools.zip \ "https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDLINE_TOOLS_VERSION}_latest.zip" \ @@ -48,6 +51,7 @@ RUN mkdir -p "$ANDROID_HOME/cmdline-tools" \ "platforms;${ANDROID_PLATFORM}" \ "build-tools;${ANDROID_BUILD_TOOLS}" \ "ndk;${ANDROID_NDK}" \ + "cmake;${ANDROID_CMAKE}" \ >/dev/null # Node.js: `pebble sdk install` below runs `npm install` for the SDK-core's @@ -64,4 +68,32 @@ RUN curl -LsSf https://astral.sh/uv/install.sh | sh \ && /root/.local/bin/uv tool install "pebble-tool==${PEBBLE_TOOL_VERSION}" \ && /root/.local/bin/pebble sdk install "${PEBBLE_SDK_CORE_VERSION}" +# --- Warm the Gradle dependency cache --- +# The companion app's real Gradle project files (not the generated build/ +# output, and not the VICE/nibtools source tarballs — see .dockerignore) are +# COPYed into a throwaway location and built once here, so every actual +# `docker run` of this image (which bind-mounts a fresh checkout over +# /workspace) hits a warm ~/.gradle cache instead of re-downloading the same +# Maven dependencies from dl.google.com/mavenCentral every single run. +# +# buildVice/buildNibtools are excluded (their source tarballs aren't in the +# build context) — CMake already handles that gracefully, falling back to a +# placeholder (see vice_jni.c / build_vice.sh's own header comment) — so this +# stage only ever warms the Gradle/Maven dependency cache, never bakes in +# compiled VICE/nibtools output. +# +# This is a pure optimization: if the app's dependencies change after this +# image was built, Gradle just downloads the delta against the warm cache at +# `docker run` time — same as it would without this stage, just slower for +# that one run, never broken. `|| true` means a transient network failure +# here only costs a slower first `docker run`, never breaks the image build. +FROM base AS gradle-cache-warm +COPY SchwertUndMagieOnPebbleCompanionApp /tmp/warm/SchwertUndMagieOnPebbleCompanionApp +WORKDIR /tmp/warm/SchwertUndMagieOnPebbleCompanionApp +RUN chmod +x gradlew \ + && (./gradlew bundleRelease assembleRelease -x buildVice -x buildNibtools --continue || true) + +FROM base +COPY --from=gradle-cache-warm /root/.gradle /root/.gradle + WORKDIR /workspace diff --git a/dist.sh b/dist.sh index 4630a65..d06d036 100755 --- a/dist.sh +++ b/dist.sh @@ -87,7 +87,11 @@ sed -i \ -e "s/versionCode = [0-9]\+/versionCode = $VERSION_CODE/" \ -e "s/versionName = \"[^\"]*\"/versionName = \"$VERSION\"/" \ "$GRADLE_KTS" -sed -i -e "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" "$WATCH_PKG_JSON" +# Pebble's own build tooling parses package.json's version strictly as +# X.Y.Z integers — it rejects the -pre+meta suffix dist.sh otherwise allows +# (including the default "0.0.0-dev+" placeholder), so strip it here. +PEBBLE_VERSION="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}" +sed -i -e "s/\"version\": \"[^\"]*\"/\"version\": \"$PEBBLE_VERSION\"/" "$WATCH_PKG_JSON" mkdir -p "$DIST" diff --git a/docs/publish.md b/docs/publish.md index 4d133cb..8306ffe 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -249,11 +249,11 @@ be bumped in source files beforehand (`dist.sh` patches `versionCode`/ running the same thing locally). The whole toolchain (Android SDK/NDK, Pebble SDK) lives in -`build-image/Dockerfile`, built and pushed to a container registry by -`build-image.sh`. This keeps the setup portable: the runner just needs Docker -and pulls that image, so it isn't tied to any one machine's local toolchain -install and can be moved or re-registered elsewhere without touching this -repo's build scripts. +`build-image/Dockerfile`, built locally and pushed to a container registry by +three separate scripts (below). This keeps the setup portable: the runner +just needs Docker and pulls that image, so it isn't tied to any one machine's +local toolchain install and can be moved or re-registered elsewhere without +touching this repo's build scripts. ### 4.1 Build environment image @@ -262,9 +262,14 @@ pinned in `app/build.gradle.kts`, matching what's validated for local builds. It does **not** contain the release keystore — that's injected at job runtime from Actions secrets (§4.3), never baked into the image. +Three scripts, kept separate so a Dockerfile change can be built and tested +locally before anything is pushed to the registry: + ```bash cp registry.env.example registry.env # fill in your registry credentials -./build-image.sh # builds + pushes :latest and the pinned VERSION tag +./build-image.sh # builds :latest and the pinned VERSION tag, locally only +./run-image.sh # runs dist.sh inside that local image — sanity-check before publishing +./upload-image.sh # pushes the already-built :latest and VERSION tag to cr.ladkau.de ``` Rebuild and push whenever `build-image/Dockerfile` changes (e.g. a Pebble SDK @@ -329,12 +334,19 @@ Watch the run under the repo's Actions tab. On success, the release appears under the repo's Releases page with the three versioned artifacts attached. To build the same versioned artifacts locally without pushing a tag (e.g. to -test before releasing): +test before releasing), either run `dist.sh` directly with the host toolchain: ```bash VERSION=1.2.3 ./dist.sh ``` +or run it inside the build-image container (same environment the runner +uses — see §4.1): + +```bash +./run-image.sh 1.2.3 +``` + ## 5. Pebble watch app → Rebble app store / direct distribution The official Pebble app store shut down years ago; the community-run diff --git a/run-image.sh b/run-image.sh new file mode 100755 index 0000000..59a29e1 --- /dev/null +++ b/run-image.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Runs dist.sh inside the local build-image container, mirroring what the +# Gitea Actions release workflow does — useful for testing build-image +# changes (or dist.sh/build_vice.sh/build_nibtools.sh changes) locally +# before pushing anything to cr.ladkau.de. +# +# Usage: ./run-image.sh [VERSION] +# VERSION is passed through to dist.sh; omit it for dist.sh's own +# git-tag-based default (see dist.sh's header comment). +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:-}}" + +echo "== Running dist.sh inside $IMAGE ==" +docker run --rm \ + -v "$ROOT:/workspace" \ + -w /workspace \ + -e VERSION="$VERSION" \ + "$IMAGE" \ + bash -c 'git config --global --add safe.directory /workspace && ./dist.sh' + +echo "== Done — artifacts in dist/ ==" +ls -la "$ROOT/dist" diff --git a/upload-image.sh b/upload-image.sh new file mode 100755 index 0000000..551ceee --- /dev/null +++ b/upload-image.sh @@ -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 release workflow pulls this image to +# run dist.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:?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"