From d5f4381169021ddec55d660826bc0c87a28905d0 Mon Sep 17 00:00:00 2001 From: ml Date: Tue, 29 Sep 2026 17:48:02 +0200 Subject: [PATCH] Add a custom Android CI/release build image and packaging scripts - build-image/: Dockerfile bundling JDK 17, the Android SDK, and Node.js (needed by Gitea's JS-based actions) - the app has no native code, so no NDK - build-image.sh/upload-image.sh: extended to also build/push this image alongside the existing OCR pipeline tools image - run-image.sh: runs `make android` inside the image against a live checkout, for a CI-equivalent local build - Makefile: `make android` builds and versions a release APK into dist/, from the current git tag - .gitea/workflows/build.yml: now pulls the custom image and just runs `make android`, replacing the ad-hoc Node/SDK install steps - README.md: describes the pipeline, server, app, and build tooling - registry.env.example: fix REGISTRY_IMAGE to be fully-qualified (an unqualified name was silently resolving pushes to Docker Hub instead of the configured registry) --- .gitea/workflows/build.yml | 53 +++++--------- .gitignore | 7 ++ Makefile | 45 ++++++++++++ README.md | 138 ++++++++++++++++++++++++++++++++++++- build-image.sh | 41 +++++++---- build-image/Dockerfile | 73 ++++++++++++++++++++ build-image/VERSION | 1 + registry.env.example | 22 ++++-- run-image.sh | 69 +++++++++++++++++++ upload-image.sh | 34 ++++++--- 10 files changed, 414 insertions(+), 69 deletions(-) create mode 100644 Makefile create mode 100644 build-image/Dockerfile create mode 100644 build-image/VERSION create mode 100755 run-image.sh diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index d18a25e..5fd6cab 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -1,10 +1,13 @@ name: build # Builds the Android app (android/) debug APK on every push/PR, plus -# on-demand via the Gitea "Run workflow" button. Runs inside a public, -# prebuilt Android CI image (JDK + Android SDK/cmdline-tools bundled - -# fuelish's app has no native/NDK code, so it doesn't need a custom -# builder image the way a project with native components would). +# 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 JDK, Android SDK, and +# Node.js (needed by the JS-based actions below - see +# build-image/Dockerfile's own comment) so the runner itself doesn't need +# any of that installed. The Gradle build still needs network access at +# job runtime (Gradle/AGP/androidx dependency resolution). on: push: pull_request: @@ -21,52 +24,28 @@ jobs: runs-on: ubuntu-latest container: - # JDK + Android SDK cmdline-tools, no platform/build-tools installed - - # the "Install SDK components" step below fetches exactly what - # compileSdk in android/app/build.gradle.kts needs. Much smaller than - # kitchen-sink alternatives (~0.7GB vs. several GB), which matters - # since every job pulls it fresh. - image: thyrlian/android-sdk:10.0 + image: cr.ladkau.de/fuelish/android-builder:latest + credentials: + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_PASSWORD }} steps: - uses: actions/checkout@v4 with: - # APK naming below uses `git rev-parse --short HEAD` - harmless - # with the default shallow clone, but fetch-depth: 0 keeps this - # consistent if that ever switches to `git describe --tags`. + # `make android`'s version comes from `git describe --tags` - + # needs full history/tags, not actions/checkout's default + # shallow single-commit clone. fetch-depth: 0 - name: Preflight run: | set -euo pipefail command -v java >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: java not in PATH" >&2; exit 1; } + command -v make >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: make not in PATH" >&2; exit 1; } [ -n "${ANDROID_HOME:-}" ] || { echo "PREFLIGHT FAIL: ANDROID_HOME not set" >&2; exit 1; } - [ -x android/gradlew ] || { echo "PREFLIGHT FAIL: android/gradlew missing or not executable" >&2; exit 1; } - - - name: Install SDK components - run: | - set -euo pipefail - # python3-pil: android/scripts/gen_app_icons.py's Pillow dependency - # (see android/app/build.gradle.kts's generateAppIcons preBuild task) - # - not bundled in this image. - apt-get update -qq - apt-get install -y --no-install-recommends python3-pil - yes | sdkmanager --licenses >/dev/null 2>&1 || true - # Keep in sync with android/app/build.gradle.kts's compileSdk. - sdkmanager "platforms;android-36" "build-tools;36.0.0" - name: Build - working-directory: android - run: ./gradlew :app:assembleDebug --console=plain - - - name: Package - # Renames the debug APK with the commit it was built from, so - # multiple builds in dist/ (and on dl.ladkau.de) don't collide. - run: | - set -euo pipefail - mkdir -p dist - sha="$(git rev-parse --short HEAD)" - cp android/app/build/outputs/apk/debug/app-debug.apk "dist/fuelish-$sha.apk" + run: make android - name: Upload build artifact # v4 uses the newer @actions/artifact backend, which this Gitea diff --git a/.gitignore b/.gitignore index 748785a..5144e48 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ __pycache__/ *.pyc +# Build output (see the root Makefile's `android` target, run-image.sh) +/dist/ + # Android app build output/local config and generated resources (launcher # icon + splash art - see android/app/build.gradle.kts's # generateAppIcons task and android/scripts/gen_app_icons.py's own @@ -32,3 +35,7 @@ __pycache__/ /android/app/src/main/res/mipmap-xhdpi /android/app/src/main/res/mipmap-xxhdpi /android/app/src/main/res/mipmap-xxxhdpi + +# Android Studio project files - per-developer, not shared +/android/.idea/ +/android/gradle/gradle-daemon-jvm.properties diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1e8f082 --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +# Builds the Android app (android/) and packages a versioned release APK. +# The OCR pipeline itself (tooling-container/, 0-3_*.sh, test_batch.sh) is +# container-only and has no host build step, so this Makefile only covers +# the Android app. + +ANDROID_DIR := android +ANDROID_APK := $(ANDROID_DIR)/app/build/outputs/apk/debug/app-debug.apk +DIST_DIR := dist + +.PHONY: all android clean + +all: android + +# Builds the Android app (`./gradlew assembleDebug` in android/, via its +# own Gradle/AGP toolchain) and copies the resulting debug APK into +# dist/, named with the current git tag, or `make android VERSION=1.2.3` +# to override. Untagged builds (including every CI build, unless a +# release tag is pushed) get a 0.0.0-dev+ placeholder version. +# Ships the auto-generated debug keystore, not a release signing config - +# fine for sideloading; a real release signing setup is a separate, +# not-yet-needed step. Requires the Android SDK (ANDROID_HOME set) - see +# build-image/Dockerfile for the exact version this is built/tested +# against, or run this inside it via ./run-image.sh. +android: + cd $(ANDROID_DIR) && ./gradlew assembleDebug + @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; \ + APK_NAME="fuelish-$$V.apk"; \ + cp $(ANDROID_APK) "$(DIST_DIR)/$$APK_NAME"; \ + echo "Wrote $(DIST_DIR)/$$APK_NAME" + +clean: + rm -rf $(DIST_DIR) $(ANDROID_DIR)/build $(ANDROID_DIR)/app/build diff --git a/README.md b/README.md index 9c8c309..2eb5441 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,139 @@ +

+ fuelish logo +

+ # fuelish -Extracting and organising fuel data from fuel receipts. \ No newline at end of file +Extracts structured data from photos of German fuel/petrol receipts — +petrol station, transaction number, date, time, price per litre, litres, +discount, and total price — and lets you review, correct, save, and +export it from an Android app. + +The extraction pipeline is self-hosted (no cloud AI APIs): Tesseract OCR +plus a small local LLM (`qwen2.5:3b`, served by Ollama), all running in +containers. It's built to eventually run on a Raspberry Pi 4 cluster +under k3s; see [`AGENT.md`](AGENT.md) for the pipeline's architecture, +preprocessing rationale, and current extraction-accuracy notes. + +## How it fits together + +- **Pipeline** (`tooling-container/`): preprocess → OCR → LLM-structure a + receipt photo into JSON. Runs either as three independently-inspectable + host scripts (for development/testing) or as an HTTP job API (for the + app). +- **Server** (`docker-compose.yml`): the same pipeline, served over HTTP + (`POST /jobs`, `GET /jobs/{id}`) so the Android app can submit a photo + and poll real per-stage progress, backed by an Ollama container. +- **Android app** (`android/`): take a photo, watch it get processed, + correct any misread fields, save it locally, browse everything saved + so far, and export to CSV or `.ods` (matching the layout of the + ground-truth spreadsheets under `testdata/`). +- **CI** (`.gitea/workflows/build.yml`): builds the Android debug APK on + every push and publishes it to dl.ladkau.de, using a custom build image + (`build-image/`) so the runner needs nothing preinstalled. + +## Repository layout + +``` +tooling-container/ Pipeline source (preprocess.py, ocr.py, extract.py, + server.py, compare_ground_truth.py) and the + Dockerfile/VERSION for the fuelish/tools image + both the host scripts and docker-compose.yml run. +0-3_*.sh Numbered host scripts driving one pipeline stage + each, for independent inspection during + development (0_run_ollama.sh starts Ollama). +test_batch.sh Runs the full pipeline over a directory of test + receipts and compares against its ground-truth + .ods - see AGENT.md's "Current state". +docker-compose.yml Runs the pipeline as a long-lived HTTP job API + (api + ollama services) for the Android app. +android/ The Android app: Kotlin + Jetpack Compose, Room + for local storage, OkHttp against the server API, + CSV/.ods export. +build-image/ Dockerfile for the Android CI/release build + environment (JDK + Android SDK, no NDK needed - + the app has no native code), used by CI and by + the build-image.sh/run-image.sh/upload-image.sh + scripts at the repo root (see "Building" below). +.gitea/workflows/ CI: builds the Android APK and publishes it to + dl.ladkau.de. +Makefile Root Makefile - packages a versioned release APK + (`make android`). +testdata/ Real receipt photos + ground-truth .ods files + (gitignored, not checked in - personal data). +``` + +See [`AGENT.md`](AGENT.md) for the pipeline's architecture, why the +current preprocessing approach was chosen over heavier alternatives, and +the latest batch-accuracy numbers and known failure modes. + +## Building + +**The pipeline image** (`tooling-container/`), used by the numbered host +scripts, `test_batch.sh`, and `docker-compose.yml`: + +```bash +./build-image.sh # builds fuelish/tools and the Android build-image + # (below), and pulls ollama/ollama +``` + +**The server**, once the tools image is built: + +```bash +docker compose up -d # starts ollama + api; api reachable at :8080 +``` + +**The Android app** (`android/`) needs the Android SDK +(`ANDROID_HOME` set). Locally, without installing it yourself: + +```bash +cp registry.env.example registry.env # fill in your registry credentials +./build-image.sh # builds the android-builder image (JDK + Android SDK) +./run-image.sh # runs `make android` inside it, against this + # checkout -> dist/fuelish-.apk +``` + +With the SDK already installed, `make android` (or +`cd android && ./gradlew assembleDebug`) works directly on the host. + +**CI** builds the same way — `.gitea/workflows/build.yml` runs +`make android` inside the `build-image/` image (pulled from the +registry, published via `./upload-image.sh` after `./build-image.sh`) +and publishes the APK to dl.ladkau.de on every push to the default +branch. + +## Running + +**Inspect the pipeline stage by stage**, for development/debugging one +receipt at a time: + +```bash +./0_run_ollama.sh # once, starts Ollama +./1_preprocess_image.sh -in photo.jpg -out scratch/ +./2_ocr_image.sh -in scratch/03_final.png -out scratch/ +./3_extract_data.sh -in scratch/ocr.txt -out scratch/ # -> extracted.json +``` + +**Batch-test against ground truth**: + +```bash +./test_batch.sh -in testdata/ # needs a *.ods ground-truth + # file in that directory +``` + +**Serve it for the app**: + +```bash +docker compose up -d +``` + +Then, in the Android app's Settings tab, point it at +`http://:8080` (both HTTP-only - see the app's +`network_security_config.xml` - and LAN-only by design, no auth/TLS). +Take a photo, watch the 4-stage progress (upload → preprocess → OCR → +extract), correct anything the model got wrong, and save. The Receipts +tab lists everything saved so far and exports to CSV or `.ods`. + +## License + +MIT — see [`LICENSE`](LICENSE). diff --git a/build-image.sh b/build-image.sh index f3b63c0..a3c1e06 100755 --- a/build-image.sh +++ b/build-image.sh @@ -1,9 +1,11 @@ #!/usr/bin/env bash -# Builds the fuelish tools image (tooling-container/Dockerfile) locally, tagged with -# tooling-container/VERSION and :latest, and pulls the other image the pipeline -# needs (ollama/ollama, used by 0_run_ollama.sh) so all images are ready before -# a pipeline run. Does not push - test it with the numbered pipeline scripts -# first, then publish with ./upload-image.sh. +# Builds every image this repo's pipeline/CI needs: the fuelish tools +# image (tooling-container/Dockerfile) and the Android CI/release build +# image (build-image/Dockerfile), each tagged from its own VERSION file, +# plus pulls ollama/ollama (used by 0_run_ollama.sh). Does not push - test +# the tools image with the numbered pipeline scripts and the +# android-builder image with ./run-image.sh, then publish both with +# ./upload-image.sh. set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")" @@ -14,26 +16,37 @@ fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; } command -v docker >/dev/null 2>&1 \ || fail "docker not found in PATH" -IMAGE="${REGISTRY_IMAGE:-fuelish/tools}" +TOOLS_IMAGE="${REGISTRY_IMAGE:-fuelish/tools}" +ANDROID_IMAGE="${REGISTRY_ANDROID_IMAGE:-cr.ladkau.de/fuelish/android-builder}" if [ -f "$ROOT/registry.env" ]; then # shellcheck disable=SC1091 source "$ROOT/registry.env" - IMAGE="${REGISTRY_IMAGE:-$IMAGE}" + TOOLS_IMAGE="${REGISTRY_IMAGE:-$TOOLS_IMAGE}" + ANDROID_IMAGE="${REGISTRY_ANDROID_IMAGE:-$ANDROID_IMAGE}" fi -VERSION="$(<"$ROOT/tooling-container/VERSION")" -[ -n "$VERSION" ] || fail "tooling-container/VERSION is empty" +TOOLS_VERSION="$(<"$ROOT/tooling-container/VERSION")" +[ -n "$TOOLS_VERSION" ] || fail "tooling-container/VERSION is empty" -echo "== Building $IMAGE:$VERSION ==" +ANDROID_VERSION="$(<"$ROOT/build-image/VERSION")" +[ -n "$ANDROID_VERSION" ] || fail "build-image/VERSION is empty" + +echo "== Building $TOOLS_IMAGE:$TOOLS_VERSION ==" docker build \ -f "$ROOT/tooling-container/Dockerfile" \ - -t "$IMAGE:$VERSION" \ - -t "$IMAGE:latest" \ + -t "$TOOLS_IMAGE:$TOOLS_VERSION" \ + -t "$TOOLS_IMAGE:latest" \ "$ROOT/tooling-container" +echo "== Building $ANDROID_IMAGE:$ANDROID_VERSION ==" +docker build \ + -t "$ANDROID_IMAGE:$ANDROID_VERSION" \ + -t "$ANDROID_IMAGE:latest" \ + "$ROOT/build-image" + echo "== Pulling ollama/ollama (used by 0_run_ollama.sh) ==" docker pull ollama/ollama echo "== Done ==" -echo "Image: $IMAGE:$VERSION (and :latest)" -echo "Test it with the numbered pipeline scripts, then publish with ./upload-image.sh" +echo "Images: $TOOLS_IMAGE:$TOOLS_VERSION, $ANDROID_IMAGE:$ANDROID_VERSION (both also :latest)" +echo "Test the tools image with the numbered pipeline scripts, the android-builder image with ./run-image.sh, then publish both with ./upload-image.sh" diff --git a/build-image/Dockerfile b/build-image/Dockerfile new file mode 100644 index 0000000..80f302e --- /dev/null +++ b/build-image/Dockerfile @@ -0,0 +1,73 @@ +# Build environment for the Android app (android/): JDK + Android SDK +# cmdline-tools, pinned to the versions validated on the maintainer's dev +# machine (matching android/app/build.gradle.kts's compileSdk). Used both +# by .gitea/workflows/build.yml's CI container and by ../run-image.sh for +# a local, CI-equivalent build. Rebuild with ../build-image.sh whenever a +# version below changes. +# +# fuelish's Android app has no native/NDK code, so unlike a build image +# that also needs an NDK/CMake toolchain, this is just a JDK + the SDK's +# own cmdline-tools/platform/build-tools. +# +# Does NOT contain any secrets - registry/SFTP credentials are injected +# at job runtime from Gitea Actions secrets, never baked into this image. +FROM ubuntu:24.04 + +ARG NODE_VERSION=24.16.0 +ARG ANDROID_CMDLINE_TOOLS_VERSION=9862592 +ARG ANDROID_PLATFORM_VERSION=36 +ARG ANDROID_BUILD_TOOLS_VERSION=36.0.0 + +ENV DEBIAN_FRONTEND=noninteractive + +# openjdk-17-jdk-headless: Gradle/AGP's own minimum JDK (see +# android/app/build.gradle.kts's compileOptions). +# make: the root Makefile's `android` target (run by both run-image.sh +# and .gitea/workflows/build.yml). +# python3/python3-pil: android/scripts/gen_app_icons.py's Pillow +# dependency (see android/app/build.gradle.kts's generateAppIcons +# preBuild task) - apt's python3-pil is used instead of pip/venv since +# nothing else in this image needs a Python environment. +# git/curl/ca-certificates/unzip/xz-utils: checkout and SDK/Node tool +# downloads (xz-utils: Node's release tarball below is .tar.xz). +# openssh-client: the build workflow's `sftp` publish to dl.ladkau.de. +RUN apt-get update && apt-get install -y --no-install-recommends \ + openjdk-17-jdk-headless make python3 python3-pil \ + git curl ca-certificates unzip xz-utils openssh-client \ + && rm -rf /var/lib/apt/lists/* + +# Node.js: actions/checkout@v4 and actions/upload-artifact@v3 (used by +# .gitea/workflows/build.yml) are both JS actions - Gitea's act_runner +# execs them with whatever `node` is on this image's PATH rather than +# injecting one itself (https://github.com/nektos/act/issues/107), and +# this base image doesn't bring one. +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 + +# Android SDK: the cmdline-tools' own sdkmanager installs exactly the +# platform/build-tools version android/app/build.gradle.kts pins - +# resolved here so no `docker run` of this image needs network access to +# install anything before `./gradlew assembleDebug` works. The Gradle +# build itself still needs network access at build time (Gradle/AGP/ +# androidx dependency resolution against Google's Maven + Maven Central) - +# android/gradlew resolves its own pinned Gradle version, no Gradle +# binary is baked into this image. +ENV ANDROID_HOME=/opt/android-sdk \ + ANDROID_SDK_ROOT=/opt/android-sdk +ENV PATH=${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${PATH} + +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" \ + && unzip -q /tmp/cmdline-tools.zip -d "${ANDROID_HOME}/cmdline-tools" \ + && mv "${ANDROID_HOME}/cmdline-tools/cmdline-tools" "${ANDROID_HOME}/cmdline-tools/latest" \ + && rm /tmp/cmdline-tools.zip \ + && yes | sdkmanager --licenses >/dev/null \ + && sdkmanager --install \ + "platform-tools" \ + "platforms;android-${ANDROID_PLATFORM_VERSION}" \ + "build-tools;${ANDROID_BUILD_TOOLS_VERSION}" + +WORKDIR /workspace diff --git a/build-image/VERSION b/build-image/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/build-image/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/registry.env.example b/registry.env.example index 30c8f47..e71d54f 100644 --- a/registry.env.example +++ b/registry.env.example @@ -1,9 +1,19 @@ # Copy to registry.env (gitignored, never commit the real values) and fill -# in your registry credentials. Used by build-image.sh, the numbered -# pipeline scripts, and upload-image.sh. Only REGISTRY_IMAGE is needed for -# local builds/runs; REGISTRY/REGISTRY_USER/REGISTRY_PASSWORD are only -# needed to push with upload-image.sh. -REGISTRY= -REGISTRY_IMAGE=fuelish/tools +# in your registry credentials. Used by build-image.sh, upload-image.sh, +# run-image.sh, and the numbered pipeline scripts. +# +# REGISTRY_IMAGE (the OCR pipeline's tools image, tooling-container/) and +# REGISTRY_ANDROID_IMAGE (the Android CI/release build image, +# build-image/) must both be fully-qualified with the registry host - +# upload-image.sh does `docker push "$REGISTRY_IMAGE:..."` directly, so +# an unqualified name (e.g. "fuelish/tools") silently resolves to Docker +# Hub instead of $REGISTRY and fails to push there ("insufficient_scope: +# authorization failed") even though `docker login "$REGISTRY"` +# succeeded. Adjust the host below if your registry isn't cr.ladkau.de. +# REGISTRY/REGISTRY_USER/REGISTRY_PASSWORD are only needed to push with +# upload-image.sh. +REGISTRY=cr.ladkau.de +REGISTRY_IMAGE=cr.ladkau.de/fuelish/tools +REGISTRY_ANDROID_IMAGE=cr.ladkau.de/fuelish/android-builder REGISTRY_USER= REGISTRY_PASSWORD= diff --git a/run-image.sh b/run-image.sh new file mode 100755 index 0000000..5717ac6 --- /dev/null +++ b/run-image.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Runs `make android` inside the local android-builder image, mirroring +# what the Gitea Actions build workflow does - useful for testing +# build-image/ changes locally before pushing anything to a registry. +# +# Usage: ./run-image.sh [VERSION] +# VERSION is passed through to `make android` (see the Makefile's +# `android` target); omit it for its 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" + +IMAGE="${REGISTRY_ANDROID_IMAGE:-cr.ladkau.de/fuelish/android-builder}" +if [ -f "$ROOT/registry.env" ]; then + # shellcheck disable=SC1091 + source "$ROOT/registry.env" + IMAGE="${REGISTRY_ANDROID_IMAGE:-$IMAGE}" +fi + +IMAGE_TAG="$(<"$ROOT/build-image/VERSION")" +[ -n "$IMAGE_TAG" ] || fail "build-image/VERSION is empty" +IMAGE="$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 +# (dist/, android/build, the generated app icon/splash resources) 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/dist" "$ROOT/android/build" "$ROOT/android/app/build" \ + "$ROOT/android/app/src/main/res/drawable-nodpi" \ + "$ROOT/android/app/src/main/res"/mipmap-mdpi "$ROOT/android/app/src/main/res"/mipmap-hdpi \ + "$ROOT/android/app/src/main/res"/mipmap-xhdpi "$ROOT/android/app/src/main/res"/mipmap-xxhdpi \ + "$ROOT/android/app/src/main/res"/mipmap-xxxhdpi + +echo "== Running make android inside $IMAGE ==" +# The container runs as root (needed for the SDK/Gradle caches baked into +# the image), so anything it writes into this bind mount - dist/, +# android/build, android/.gradle, 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 android + ' + +echo "== Done - artifact in dist/ ==" +ls -la "$ROOT/dist" diff --git a/upload-image.sh b/upload-image.sh index ca6e1fd..52f34b3 100755 --- a/upload-image.sh +++ b/upload-image.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash -# Pushes the fuelish tools image - already built locally with -# ./build-image.sh, and ideally verified with the numbered pipeline -# scripts - to a container registry, so the k3s cluster (or other -# machines) can pull it without rebuilding it. +# Pushes every image ./build-image.sh built locally - the fuelish tools +# image and the Android CI/release build image, each already verified +# (tools image with the numbered pipeline scripts, android-builder image +# with ./run-image.sh) - to a container registry, so the k3s cluster can +# pull the tools image and Gitea Actions can pull the android-builder +# image without rebuilding either. set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")" @@ -21,21 +23,31 @@ source "$ROOT/registry.env" : "${REGISTRY:?registry.env must set REGISTRY}" : "${REGISTRY_IMAGE:?registry.env must set REGISTRY_IMAGE}" +: "${REGISTRY_ANDROID_IMAGE:?registry.env must set REGISTRY_ANDROID_IMAGE}" : "${REGISTRY_USER:?registry.env must set REGISTRY_USER}" : "${REGISTRY_PASSWORD:?registry.env must set REGISTRY_PASSWORD}" -VERSION="$(<"$ROOT/tooling-container/VERSION")" -[ -n "$VERSION" ] || fail "tooling-container/VERSION is empty" +TOOLS_VERSION="$(<"$ROOT/tooling-container/VERSION")" +[ -n "$TOOLS_VERSION" ] || fail "tooling-container/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" +ANDROID_VERSION="$(<"$ROOT/build-image/VERSION")" +[ -n "$ANDROID_VERSION" ] || fail "build-image/VERSION is empty" + +docker image inspect "$REGISTRY_IMAGE:$TOOLS_VERSION" >/dev/null 2>&1 \ + || fail "$REGISTRY_IMAGE:$TOOLS_VERSION not found locally - run ./build-image.sh first" +docker image inspect "$REGISTRY_ANDROID_IMAGE:$ANDROID_VERSION" >/dev/null 2>&1 \ + || fail "$REGISTRY_ANDROID_IMAGE:$ANDROID_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" +echo "== Pushing $REGISTRY_IMAGE:$TOOLS_VERSION and :latest ==" +docker push "$REGISTRY_IMAGE:$TOOLS_VERSION" docker push "$REGISTRY_IMAGE:latest" +echo "== Pushing $REGISTRY_ANDROID_IMAGE:$ANDROID_VERSION and :latest ==" +docker push "$REGISTRY_ANDROID_IMAGE:$ANDROID_VERSION" +docker push "$REGISTRY_ANDROID_IMAGE:latest" + echo "== Done ==" -echo "Image: $REGISTRY_IMAGE:$VERSION" +echo "Images: $REGISTRY_IMAGE:$TOOLS_VERSION, $REGISTRY_ANDROID_IMAGE:$ANDROID_VERSION"