Add a custom Android CI/release build image and packaging scripts
build / build (push) Failing after 0s
build / build (push) Failing after 0s
- 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)
This commit is contained in:
+16
-37
@@ -1,10 +1,13 @@
|
|||||||
name: build
|
name: build
|
||||||
|
|
||||||
# Builds the Android app (android/) debug APK on every push/PR, plus
|
# Builds the Android app (android/) debug APK on every push/PR, plus
|
||||||
# on-demand via the Gitea "Run workflow" button. Runs inside a public,
|
# on-demand via the Gitea "Run workflow" button. Runs inside build-image
|
||||||
# prebuilt Android CI image (JDK + Android SDK/cmdline-tools bundled -
|
# (see ../../build-image/Dockerfile, built/pushed via ../../build-image.sh
|
||||||
# fuelish's app has no native/NDK code, so it doesn't need a custom
|
# and ../../upload-image.sh), which bundles the JDK, Android SDK, and
|
||||||
# builder image the way a project with native components would).
|
# 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:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -21,52 +24,28 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
container:
|
container:
|
||||||
# JDK + Android SDK cmdline-tools, no platform/build-tools installed -
|
image: cr.ladkau.de/fuelish/android-builder:latest
|
||||||
# the "Install SDK components" step below fetches exactly what
|
credentials:
|
||||||
# compileSdk in android/app/build.gradle.kts needs. Much smaller than
|
username: ${{ secrets.REGISTRY_USER }}
|
||||||
# kitchen-sink alternatives (~0.7GB vs. several GB), which matters
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
# since every job pulls it fresh.
|
|
||||||
image: thyrlian/android-sdk:10.0
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
# APK naming below uses `git rev-parse --short HEAD` - harmless
|
# `make android`'s version comes from `git describe --tags` -
|
||||||
# with the default shallow clone, but fetch-depth: 0 keeps this
|
# needs full history/tags, not actions/checkout's default
|
||||||
# consistent if that ever switches to `git describe --tags`.
|
# shallow single-commit clone.
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Preflight
|
- name: Preflight
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
command -v java >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: java not in PATH" >&2; exit 1; }
|
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; }
|
[ -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
|
- name: Build
|
||||||
working-directory: android
|
run: make 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"
|
|
||||||
|
|
||||||
- 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
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
|
# Build output (see the root Makefile's `android` target, run-image.sh)
|
||||||
|
/dist/
|
||||||
|
|
||||||
# Android app build output/local config and generated resources (launcher
|
# Android app build output/local config and generated resources (launcher
|
||||||
# icon + splash art - see android/app/build.gradle.kts's
|
# icon + splash art - see android/app/build.gradle.kts's
|
||||||
# generateAppIcons task and android/scripts/gen_app_icons.py's own
|
# 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-xhdpi
|
||||||
/android/app/src/main/res/mipmap-xxhdpi
|
/android/app/src/main/res/mipmap-xxhdpi
|
||||||
/android/app/src/main/res/mipmap-xxxhdpi
|
/android/app/src/main/res/mipmap-xxxhdpi
|
||||||
|
|
||||||
|
# Android Studio project files - per-developer, not shared
|
||||||
|
/android/.idea/
|
||||||
|
/android/gradle/gradle-daemon-jvm.properties
|
||||||
|
|||||||
@@ -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+<sha> 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
|
||||||
@@ -1,3 +1,139 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="logo.png" width="280" alt="fuelish logo">
|
||||||
|
</p>
|
||||||
|
|
||||||
# fuelish
|
# fuelish
|
||||||
|
|
||||||
Extracting and organising fuel data from fuel receipts.
|
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-<version>.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/<some-set> # 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://<this-host>: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).
|
||||||
|
|||||||
+27
-14
@@ -1,9 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Builds the fuelish tools image (tooling-container/Dockerfile) locally, tagged with
|
# Builds every image this repo's pipeline/CI needs: the fuelish tools
|
||||||
# tooling-container/VERSION and :latest, and pulls the other image the pipeline
|
# image (tooling-container/Dockerfile) and the Android CI/release build
|
||||||
# needs (ollama/ollama, used by 0_run_ollama.sh) so all images are ready before
|
# image (build-image/Dockerfile), each tagged from its own VERSION file,
|
||||||
# a pipeline run. Does not push - test it with the numbered pipeline scripts
|
# plus pulls ollama/ollama (used by 0_run_ollama.sh). Does not push - test
|
||||||
# first, then publish with ./upload-image.sh.
|
# 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
|
set -euo pipefail
|
||||||
|
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
@@ -14,26 +16,37 @@ fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; }
|
|||||||
command -v docker >/dev/null 2>&1 \
|
command -v docker >/dev/null 2>&1 \
|
||||||
|| fail "docker not found in PATH"
|
|| 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
|
if [ -f "$ROOT/registry.env" ]; then
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
source "$ROOT/registry.env"
|
source "$ROOT/registry.env"
|
||||||
IMAGE="${REGISTRY_IMAGE:-$IMAGE}"
|
TOOLS_IMAGE="${REGISTRY_IMAGE:-$TOOLS_IMAGE}"
|
||||||
|
ANDROID_IMAGE="${REGISTRY_ANDROID_IMAGE:-$ANDROID_IMAGE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
VERSION="$(<"$ROOT/tooling-container/VERSION")"
|
TOOLS_VERSION="$(<"$ROOT/tooling-container/VERSION")"
|
||||||
[ -n "$VERSION" ] || fail "tooling-container/VERSION is empty"
|
[ -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 \
|
docker build \
|
||||||
-f "$ROOT/tooling-container/Dockerfile" \
|
-f "$ROOT/tooling-container/Dockerfile" \
|
||||||
-t "$IMAGE:$VERSION" \
|
-t "$TOOLS_IMAGE:$TOOLS_VERSION" \
|
||||||
-t "$IMAGE:latest" \
|
-t "$TOOLS_IMAGE:latest" \
|
||||||
"$ROOT/tooling-container"
|
"$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) =="
|
echo "== Pulling ollama/ollama (used by 0_run_ollama.sh) =="
|
||||||
docker pull ollama/ollama
|
docker pull ollama/ollama
|
||||||
|
|
||||||
echo "== Done =="
|
echo "== Done =="
|
||||||
echo "Image: $IMAGE:$VERSION (and :latest)"
|
echo "Images: $TOOLS_IMAGE:$TOOLS_VERSION, $ANDROID_IMAGE:$ANDROID_VERSION (both also :latest)"
|
||||||
echo "Test it with the numbered pipeline scripts, then publish with ./upload-image.sh"
|
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"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0.1.0
|
||||||
+16
-6
@@ -1,9 +1,19 @@
|
|||||||
# Copy to registry.env (gitignored, never commit the real values) and fill
|
# Copy to registry.env (gitignored, never commit the real values) and fill
|
||||||
# in your registry credentials. Used by build-image.sh, the numbered
|
# in your registry credentials. Used by build-image.sh, upload-image.sh,
|
||||||
# pipeline scripts, and upload-image.sh. Only REGISTRY_IMAGE is needed for
|
# run-image.sh, and the numbered pipeline scripts.
|
||||||
# local builds/runs; REGISTRY/REGISTRY_USER/REGISTRY_PASSWORD are only
|
#
|
||||||
# needed to push with upload-image.sh.
|
# REGISTRY_IMAGE (the OCR pipeline's tools image, tooling-container/) and
|
||||||
REGISTRY=
|
# REGISTRY_ANDROID_IMAGE (the Android CI/release build image,
|
||||||
REGISTRY_IMAGE=fuelish/tools
|
# 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_USER=
|
||||||
REGISTRY_PASSWORD=
|
REGISTRY_PASSWORD=
|
||||||
|
|||||||
Executable
+69
@@ -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"
|
||||||
+23
-11
@@ -1,8 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Pushes the fuelish tools image - already built locally with
|
# Pushes every image ./build-image.sh built locally - the fuelish tools
|
||||||
# ./build-image.sh, and ideally verified with the numbered pipeline
|
# image and the Android CI/release build image, each already verified
|
||||||
# scripts - to a container registry, so the k3s cluster (or other
|
# (tools image with the numbered pipeline scripts, android-builder image
|
||||||
# machines) can pull it without rebuilding it.
|
# 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
|
set -euo pipefail
|
||||||
|
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
@@ -21,21 +23,31 @@ source "$ROOT/registry.env"
|
|||||||
|
|
||||||
: "${REGISTRY:?registry.env must set REGISTRY}"
|
: "${REGISTRY:?registry.env must set REGISTRY}"
|
||||||
: "${REGISTRY_IMAGE:?registry.env must set REGISTRY_IMAGE}"
|
: "${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_USER:?registry.env must set REGISTRY_USER}"
|
||||||
: "${REGISTRY_PASSWORD:?registry.env must set REGISTRY_PASSWORD}"
|
: "${REGISTRY_PASSWORD:?registry.env must set REGISTRY_PASSWORD}"
|
||||||
|
|
||||||
VERSION="$(<"$ROOT/tooling-container/VERSION")"
|
TOOLS_VERSION="$(<"$ROOT/tooling-container/VERSION")"
|
||||||
[ -n "$VERSION" ] || fail "tooling-container/VERSION is empty"
|
[ -n "$TOOLS_VERSION" ] || fail "tooling-container/VERSION is empty"
|
||||||
|
|
||||||
docker image inspect "$REGISTRY_IMAGE:$VERSION" >/dev/null 2>&1 \
|
ANDROID_VERSION="$(<"$ROOT/build-image/VERSION")"
|
||||||
|| fail "$REGISTRY_IMAGE:$VERSION not found locally - run ./build-image.sh first"
|
[ -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 "== Logging in to $REGISTRY =="
|
||||||
echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
|
echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
|
||||||
|
|
||||||
echo "== Pushing $REGISTRY_IMAGE:$VERSION and :latest =="
|
echo "== Pushing $REGISTRY_IMAGE:$TOOLS_VERSION and :latest =="
|
||||||
docker push "$REGISTRY_IMAGE:$VERSION"
|
docker push "$REGISTRY_IMAGE:$TOOLS_VERSION"
|
||||||
docker push "$REGISTRY_IMAGE:latest"
|
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 "== Done =="
|
||||||
echo "Image: $REGISTRY_IMAGE:$VERSION"
|
echo "Images: $REGISTRY_IMAGE:$TOOLS_VERSION, $REGISTRY_ANDROID_IMAGE:$ANDROID_VERSION"
|
||||||
|
|||||||
Reference in New Issue
Block a user