Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit d69cf8b

Browse files
committed
refactor Dockerfile and add base image publish workflow
1 parent d5db6fc commit d69cf8b

9 files changed

Lines changed: 175 additions & 96 deletions

File tree

.github/workflows/docker.yml

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ name: Docker CI
33
on:
44
push:
55
branches: ["main"]
6+
pull_request:
7+
paths:
8+
- "**/Dockerfile"
69
workflow_dispatch:
710

811
concurrency:
@@ -13,7 +16,7 @@ env:
1316
BASE_IMAGE_NAME: gem-android-base
1417
BASE_IMAGE_FULL_NAME: ghcr.io/${{ github.repository_owner }}/gem-android-base
1518
DOCKER_BUILDKIT: 1
16-
BUNDLE_TASK: ":app:assembleUniversalRelease"
19+
BUNDLE_TASK: "clean :app:bundleGoogleRelease assembleUniversalRelease"
1720

1821
jobs:
1922
build_base_image:
@@ -94,33 +97,18 @@ jobs:
9497
env:
9598
DOCKER_BUILDKIT: 1
9699
run: |
97-
docker buildx build \
98-
--build-arg TAG=${{ github.ref_name }} \
99-
--build-arg BASE_IMAGE=${{ env.BASE_IMAGE_FULL_NAME }} \
100-
--build-arg BASE_IMAGE_TAG=${{ needs.build_base_image.outputs.image_tag }} \
101-
--build-arg SKIP_SIGN=true \
102-
--build-arg BUNDLE_TASK=${{ env.BUNDLE_TASK }} \
103-
--cache-from type=gha,scope=app-image \
104-
--cache-to type=gha,mode=max,scope=app-image \
105-
--load \
106-
--tag gem-android-app:latest \
107-
--file ./reproducible/Dockerfile \
108-
.
100+
just build-app TAG=${{ github.ref_name }}
109101
110102
- name: Build app inside container
111103
env:
112104
GRADLE_CACHE: ${{ runner.temp }}/gradle-cache
113105
MAVEN_CACHE: ${{ runner.temp }}/m2-cache
106+
TAG: ${{ github.ref_name }}
114107
run: |
115108
mkdir -p "${GRADLE_CACHE}" "${MAVEN_CACHE}"
116-
docker rm -f gem-android-app-build >/dev/null 2>&1 || true
117-
docker run --name gem-android-app-build \
118-
-e SKIP_SIGN=true \
119-
-e BUNDLE_TASK="${{ env.BUNDLE_TASK }}" \
120-
-v "${GRADLE_CACHE}":/root/.gradle \
121-
-v "${MAVEN_CACHE}":/root/.m2 \
122-
gem-android-app:latest \
123-
bash -lc 'cd /root/gem-android && ./gradlew ${BUNDLE_TASK} --no-daemon --build-cache'
109+
BASE_IMAGE_TAG=${{ needs.build_base_image.outputs.image_tag }} \
110+
GRADLE_CACHE="${GRADLE_CACHE}" MAVEN_CACHE="${MAVEN_CACHE}" \
111+
just build-app-inside
124112
125113
- name: Extract Universal APK from Docker container
126114
run: |
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Publish Base Image
2+
3+
on:
4+
workflow_dispatch:
5+
inputs: {}
6+
7+
permissions:
8+
contents: read
9+
packages: write
10+
11+
env:
12+
BASE_IMAGE_NAME: gem-android-base
13+
BASE_IMAGE_FULL_NAME: ghcr.io/${{ github.repository_owner }}/gem-android-base
14+
BASE_IMAGE_TAG_FILE: reproducible/base_image_tag.txt
15+
DOCKER_BUILDKIT: 1
16+
17+
jobs:
18+
build_and_push:
19+
runs-on: ubuntu-latest
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v4
23+
24+
- name: Read base image tag
25+
id: base_tag
26+
run: |
27+
TAG=$(cat "${{ env.BASE_IMAGE_TAG_FILE }}")
28+
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
29+
echo "Base image tag: ${TAG}"
30+
31+
- name: Log in to GitHub Container Registry
32+
uses: docker/login-action@v3
33+
with:
34+
registry: ghcr.io
35+
username: ${{ github.actor }}
36+
password: ${{ secrets.GITHUB_TOKEN }}
37+
38+
- name: Build base Docker image
39+
env:
40+
DOCKER_BUILDKIT: 1
41+
run: docker build --pull --no-cache -t ${BASE_IMAGE_NAME} .
42+
43+
- name: Tag and push base Docker image
44+
env:
45+
BASE_TAG: ${{ steps.base_tag.outputs.tag }}
46+
run: |
47+
docker tag ${BASE_IMAGE_NAME} ${BASE_IMAGE_FULL_NAME}:${BASE_TAG}
48+
docker tag ${BASE_IMAGE_NAME} ${BASE_IMAGE_FULL_NAME}:latest
49+
docker push ${BASE_IMAGE_FULL_NAME}:${BASE_TAG}
50+
docker push ${BASE_IMAGE_FULL_NAME}:latest

.github/workflows/verify-apk.yml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ on:
2121
- build
2222
- diff
2323
base_image_tag:
24-
description: gem-android-base tag in GHCR (defaults to latest)
24+
description: gem-android-base tag in GHCR (default: tag from reproducible/base_image_tag.txt if not provided)
2525
required: false
26-
default: latest
26+
default: ""
2727
type: string
2828

2929
concurrency:
@@ -47,6 +47,12 @@ jobs:
4747
with:
4848
submodules: recursive
4949

50+
- name: Read base image tag (default)
51+
id: base_tag
52+
run: |
53+
DEFAULT_TAG=$(cat reproducible/base_image_tag.txt)
54+
echo "tag=${DEFAULT_TAG}" >> "$GITHUB_OUTPUT"
55+
5056
- name: Set up Docker Buildx
5157
uses: docker/setup-buildx-action@v3
5258

@@ -75,7 +81,7 @@ jobs:
7581
7682
- name: Run verify_apk.py
7783
env:
78-
VERIFY_BASE_TAG: ${{ inputs.base_image_tag }}
84+
VERIFY_BASE_TAG: ${{ inputs.base_image_tag != '' && inputs.base_image_tag || steps.base_tag.outputs.tag }}
7985
run: |
8086
python3 reproducible/verify_apk.py "${{ inputs.tag }}" official.apk --stage "${{ inputs.stage }}"
8187

Dockerfile

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,61 @@
11
# syntax=docker/dockerfile:1.4
2-
# This Dockerfile is used to setup the environment (JDK, SDK, NDK, just) for Android app building.
2+
# Pinned Android build environment on top of Gradle.
33

4-
FROM debian:bookworm
4+
ARG GRADLE_IMAGE=gradle:8.13-jdk17
5+
ARG CMDLINE_TOOLS_VERSION=11076708
6+
ARG ANDROID_API_LEVEL=35
7+
ARG ANDROID_BUILD_TOOLS_VERSION=35.0.0
8+
ARG ANDROID_NDK_VERSION=28.1.13356709
9+
ARG JUST_VERSION=1.45.0
10+
ARG JUST_TARGET=x86_64-unknown-linux-musl
11+
12+
FROM ${GRADLE_IMAGE}
13+
14+
USER root
15+
16+
ARG CMDLINE_TOOLS_VERSION
17+
ARG ANDROID_API_LEVEL
18+
ARG ANDROID_BUILD_TOOLS_VERSION
19+
ARG ANDROID_NDK_VERSION
20+
ARG JUST_VERSION
21+
ARG JUST_TARGET
522

6-
ENV DEBIAN_FRONTEND=noninteractive
723
ENV HOME=/root
824
ENV ANDROID_HOME=/opt/android-sdk
925
ENV ANDROID_SDK_ROOT=/opt/android-sdk
10-
ENV PATH=${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${PATH}
11-
12-
RUN dpkg --add-architecture amd64
13-
14-
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
15-
--mount=type=cache,target=/var/lib/apt,sharing=locked \
16-
apt-get update && apt-get install -y \
17-
curl \
18-
unzip \
19-
zip \
20-
git \
21-
make \
22-
build-essential \
23-
pkg-config \
24-
openjdk-17-jdk-headless \
25-
libc6:amd64 \
26-
zlib1g:amd64
27-
28-
RUN curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
29-
30-
RUN --mount=type=cache,target=/tmp/android-dl \
31-
mkdir -p ${ANDROID_HOME}/cmdline-tools/latest && \
32-
if [ ! -f /tmp/android-dl/commandlinetools-linux-11076708_latest.zip ]; then \
33-
curl -o /tmp/android-dl/commandlinetools-linux-11076708_latest.zip \
34-
https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip; \
35-
fi && \
36-
unzip -q /tmp/android-dl/commandlinetools-linux-11076708_latest.zip -d /tmp/cmdline-tools && \
37-
mv /tmp/cmdline-tools/cmdline-tools/* ${ANDROID_HOME}/cmdline-tools/latest/ && \
38-
rm -rf /tmp/cmdline-tools
26+
ENV ANDROID_SDK_URL=https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip
27+
ENV PATH=${ANDROID_HOME}/cmdline-tools/bin:${ANDROID_HOME}/platform-tools:${PATH}
28+
29+
# Runtime deps for build-tools/aapt2.
30+
RUN apt-get update && \
31+
apt-get install -y --no-install-recommends \
32+
libc6 \
33+
libstdc++6 \
34+
zlib1g \
35+
libtinfo6 \
36+
ca-certificates \
37+
&& rm -rf /var/lib/apt/lists/*
38+
39+
# Install just from a pinned release.
40+
RUN curl -fL \
41+
"https://github.com/casey/just/releases/download/${JUST_VERSION}/just-${JUST_VERSION}-${JUST_TARGET}.tar.gz" \
42+
-o /tmp/just.tar.gz && \
43+
tar -xzf /tmp/just.tar.gz -C /tmp && \
44+
mv /tmp/just /usr/local/bin/just && \
45+
rm -rf /tmp/just*
46+
47+
RUN mkdir -p "${ANDROID_HOME}" /root/.android && \
48+
cd "${ANDROID_HOME}" && \
49+
curl -o sdk.zip "${ANDROID_SDK_URL}" && \
50+
unzip -q sdk.zip && \
51+
rm sdk.zip
3952

4053
RUN --mount=type=cache,target=/root/.android \
41-
yes | ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --licenses && \
42-
${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager \
54+
yes | ${ANDROID_HOME}/cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} --licenses && \
55+
${ANDROID_HOME}/cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} \
4356
"platform-tools" \
44-
"platforms;android-35" \
45-
"build-tools;35.0.0" \
46-
"ndk;28.1.13356709"
57+
"platforms;android-${ANDROID_API_LEVEL}" \
58+
"build-tools;${ANDROID_BUILD_TOOLS_VERSION}" \
59+
"ndk;${ANDROID_NDK_VERSION}"
4760

4861
CMD ["bash"]

justfile

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,46 @@ generate-models: install-typeshare
4343
@cd core && cargo run --package generate --bin generate android ../gemcore/src/main/kotlin/com/wallet/core
4444

4545
build-base-image:
46-
just --justfile reproducible/justfile build-base
46+
DOCKER_BUILDKIT=1 docker build --no-cache -t gem-android-base ..
4747

4848
TAG := env("TAG", "main")
4949
BUILD_MODE := env("BUILD_MODE", "")
5050

5151
build-app:
52-
just --justfile reproducible/justfile build-app TAG={{TAG}} BUNDLE_TASK="clean :app:bundleGoogleRelease assembleUniversalRelease"
52+
#!/usr/bin/env bash
53+
set -euo pipefail
54+
base_tag=$(cat reproducible/base_image_tag.txt)
55+
tag="{{TAG}}"
56+
if ! docker pull ghcr.io/gemwalletcom/gem-android-base:${base_tag} >/dev/null 2>&1; then
57+
echo "Base image ghcr.io/gemwalletcom/gem-android-base:${base_tag} not found; building locally..." >&2
58+
DOCKER_BUILDKIT=1 DOCKER_DEFAULT_PLATFORM=linux/amd64 docker build --platform linux/amd64 -t ghcr.io/gemwalletcom/gem-android-base:${base_tag} .
59+
fi
60+
DOCKER_BUILDKIT=1 DOCKER_DEFAULT_PLATFORM=linux/amd64 docker build --platform linux/amd64 \
61+
--build-arg TAG="${tag}" \
62+
--build-arg SKIP_SIGN=true \
63+
--build-arg BUNDLE_TASK="clean :app:bundleGoogleRelease assembleUniversalRelease" \
64+
--build-arg BASE_IMAGE=ghcr.io/gemwalletcom/gem-android-base \
65+
--build-arg BASE_IMAGE_TAG="${base_tag}" \
66+
-t gem-android-app-verify \
67+
-f ./reproducible/Dockerfile \
68+
.
69+
70+
build-app-inside:
71+
#!/usr/bin/env bash
72+
set -euo pipefail
73+
TAG="{{TAG}}" just build-app
74+
container_name="gem-android-app-build"
75+
gradle_cache=$(mktemp -d)
76+
maven_cache=$(mktemp -d)
77+
trap 'rm -rf "${gradle_cache}" "${maven_cache}"; docker rm -f ${container_name} >/dev/null 2>&1 || true' EXIT
78+
docker rm -f ${container_name} >/dev/null 2>&1 || true
79+
DOCKER_DEFAULT_PLATFORM=linux/amd64 docker run --platform linux/amd64 --name ${container_name} \
80+
-e SKIP_SIGN=true \
81+
-e BUNDLE_TASK="clean :app:bundleGoogleRelease assembleUniversalRelease" \
82+
-v "${gradle_cache}":/root/.gradle \
83+
-v "${maven_cache}":/root/.m2 \
84+
gem-android-app-verify \
85+
bash -lc 'cd /root/gem-android && ./gradlew ${BUNDLE_TASK} --no-daemon --build-cache'
5386
5487
core-upgrade:
5588
@git submodule update --recursive --remote
@@ -84,4 +117,7 @@ add-verification-dependency dependency:
84117
gem-android-base \
85118
bash -lc './scripts/add_verification_dependency.sh "$ADDITIONAL_DEPENDENCY"'
86119
120+
verify TAG APK:
121+
python3 ./reproducible/verify_apk.py {{TAG}} {{APK}}
122+
87123
mod core

reproducible/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
This folder contains the tooling to rebuild tagged releases inside Docker and compare them to published APKs.
44

55
## Principle
6-
- Build inside Docker using the repo-root `Dockerfile` and `reproducible/Dockerfile`, running the release task sequence (`clean :app:bundleGoogleRelease assembleUniversalRelease`) with Gradle/Maven caches cleared each run.
6+
- Build inside Docker using the repo-root base image `Dockerfile` and `reproducible/Dockerfile`, running the release task sequence (`clean :app:bundleGoogleRelease assembleUniversalRelease`) with Gradle/Maven caches cleared each run.
7+
- Base image publishing is done by the `Publish Base Image` workflow to build/push `ghcr.io/gemwalletcom/gem-android-base:<base-tag>` (where `<base-tag>` lives in `reproducible/base_image_tag.txt`).
78
- Require `local.properties` for GitHub packages; use the same base image used for releases.
89
- Strip signing artifacts, patch map-id when present, then copy the official signing block onto the rebuilt APK with [apksigcopier](https://github.com/obfusk/apksigcopier) to confirm payload identity without exposing keys.
910
- Keep outputs under `artifacts/reproducible/<tag>/` and only run diffoscope if hashes still differ after signature copy.
@@ -24,11 +25,10 @@ This folder contains the tooling to rebuild tagged releases inside Docker and co
2425
```
2526
Or, with an exported token: `echo <github-token> | docker login ghcr.io -u <github-username> --password-stdin` (token needs `read:packages`).
2627
2) Run: `./verify_apk.py <git-tag-or-branch> <path-to-official-apk> [--stage all|build|diff]`. Outputs: `official.apk`, `rebuilt.apk`, `r8_patched.apk` (when needed), `rebuilt_signed.apk`, `diffoscope.html` under `artifacts/reproducible/<tag>/`.
27-
3) CI: trigger the `Verify APK` workflow dispatch (`.github/workflows/verify-apk.yml`) with `tag`, `official_apk_url`, optional `stage`, and optional `base_image_tag`; artifacts upload mirrors local outputs.
28-
4) The release pipeline publishes `gem-android-base` to GHCR as `ghcr.io/gemwalletcom/gem-android-base:<tag>` (and `:latest`). Verification defaults `base_image_tag`/`VERIFY_BASE_TAG` to the tag being verified, so it will use the matching base image unless you override it.
29-
5) `verify_apk.py` will `docker pull` the base image by default (set `VERIFY_PULL_BASE=false` to skip). It then reuses a local image or builds if the pull fails.
30-
6) Optional manual map-id patch: `./fix_pg_map_id.py <apk-in> <apk-out> <pg-map-id>`.
31-
7) Optional dexdump diff: `./diff_dexdump.py <official-apk> <rebuilt-apk> [--out-dir DIR] [--dexdump PATH] [--tag TAG]` to write per-dex dumps/diffs (defaults to `artifacts/reproducible/<tag>/dexdump` when `--tag` is provided).
28+
3) CI: trigger the `Verify APK` workflow dispatch (`.github/workflows/verify-apk.yml`) with `tag`, `official_apk_url`, optional `stage`, and optional `base_image_tag`; artifacts upload mirrors local outputs. If `base_image_tag` is empty, the workflow reads `reproducible/base_image_tag.txt`.
29+
4) `verify_apk.py` will `docker pull` the base image by default (set `VERIFY_PULL_BASE=false` to skip). It then reuses a local image or builds if the pull fails.
30+
5) Optional manual map-id patch: `./fix_pg_map_id.py <apk-in> <apk-out> <pg-map-id>`.
31+
6) Optional dexdump diff: `./diff_dexdump.py <official-apk> <rebuilt-apk> [--out-dir DIR] [--dexdump PATH] [--tag TAG]` to write per-dex dumps/diffs (defaults to `artifacts/reproducible/<tag>/dexdump` when `--tag` is provided).
3232

3333
## Known issues
3434
- AGP 8.13.1 (bundled R8) randomizes map-id; we patch via `fix_pg_map_id.py` and confirm payload identity by copying the official signing block (apksigcopier). Deterministic map-id support is still required for strict reproducibility.

reproducible/base_image_tag.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
base-2025-12-18

reproducible/justfile

Lines changed: 0 additions & 23 deletions
This file was deleted.

0 commit comments

Comments
 (0)