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.
This commit is contained in:
ml
2026-07-04 05:21:54 +02:00
parent ad887c8bc8
commit 8881241fd8
9 changed files with 197 additions and 30 deletions
+32
View File
@@ -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
+5
View File
@@ -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
+4 -2
View File
@@ -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**:
+9 -16
View File
@@ -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"
+36 -4
View File
@@ -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
+5 -1
View File
@@ -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+<sha>" 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"
+19 -7
View File
@@ -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
Executable
+46
View File
@@ -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"
+41
View File
@@ -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"