Skip to content

refactor(image): shared fixed-JDK SDK layer, opt-in NDK target, SDK 37 - #32

Closed
azlekov wants to merge 2 commits into
mainfrom
refactor/image-layers
Closed

azlekov wants to merge 2 commits into
mainfrom
refactor/image-layers

Conversation

@azlekov

@azlekov azlekov commented Oct 19, 2026 •

Copy link
Copy Markdown

What

Dockerfile-only restructure (workstream W1). Contract C2 holds: ARG names VERSION, JAVA_VERSION, NODE_VERSION, COMPILE_SDK, BUILD_TOOLS, NDK_VERSION, RUNNER_IMAGE_TAG, RUNNER_IMAGE_REVISION are unchanged; runner is the default and the last stage; runner-ndk is opt-in. Refs #17.

  • sdk stage: cmdline-tools + sdkmanager (retry loop kept, now a small sdk-install helper shared with the ndk stage) on a fixed JDK (ARG SDK_JAVA_VERSION=21, eclipse-temurin:21-jdk-noble). It only sees COMPILE_SDK, BUILD_TOOLS, SDK_TOOLS, so a JDK/Node bump does not touch it.
  • Final image order: COPY --from=sdk straight onto the base image, then one apt layer for tag-independent packages (iproute2 cmake build-essential swig xxd pigz, lists removed), then the Temurin layer, then the NodeSource layer. xxd; pigz is 1 small package.
  • runner-ndk target: ndk;${NDK_VERSION} and cmake;3.22.1 are installed in a separate sdk-ndk stage into their own sdk root, so only the delta (ndk/, cmake/) is copied onto core (no second copy of the SDK). The apt cmake package stays in both.
  • COPY --chmod=755 for hooks/pruner/preflight replaces the extra chmod RUN layers. TAR_OPTIONS, MISE/HK env, hook ENVs, preflight ENTRYPOINT/CMD and the image-identity ARGs are unchanged and still last.
  • JAVA_VERSION/NODE_VERSION/TARGETARCH are declared below the shared layers on purpose: a build arg is part of the cache key of every RUN after its ARG, and with them at the top the common apt layer was rebuilt and re-hashed per JDK (I saw this in the first iteration).

Sizes (linux/amd64, docker image inspect -f '{{.Size}}', uncompressed)

image bytes GiB
old main Dockerfile (SDK 36.1 + NDK + cmake 3.22.1, JDK 21) 6,072,173,606 5.65
new --target runner (JDK 21) 3,434,613,532 3.20
new --target runner (JDK 17) 3,405,603,738 3.17
new --target runner (JDK 27) 3,390,751,783 3.16
new --target runner-ndk (JDK 21) 5,835,228,821 5.43

The SDK without NDK/cmake is only ~430 MB; the NDK + cmake delta is ~2.4 GB, and it is what #17 asked to stop shipping to everyone. A plain docker build . gives runner (same image id as --target runner).

SDK layer is the same for every JDK

RootFS.Layers[9] (the COPY --from=sdk layer) and [10] (shared apt layer) for JDK 17 / 21 / 27 (Node 24):

JDK 17: sha256:95c8248cb6c1aab933dbbb14ae62b393718f674653e5d111ae4f323b2a56d99e  sha256:77b88e64c453...  [11] sha256:6701ae4f5cbd... (JDK)
JDK 21: sha256:95c8248cb6c1aab933dbbb14ae62b393718f674653e5d111ae4f323b2a56d99e  sha256:77b88e64c453...  [11] sha256:4e074173de61... (JDK)
JDK 27: sha256:95c8248cb6c1aab933dbbb14ae62b393718f674653e5d111ae4f323b2a56d99e  sha256:77b88e64c453...  [11] sha256:d58ad4ad2468... (JDK)

Layers 0-10 are identical, only the JDK layer onwards differs. runner-ndk has the same 20 layers as runner + 2 (ndk, cmake); layers 0-17 match runner.

Caveat for W2 (CI): that identity comes from one BuildKit cache. Two independent builds on different runners would re-run the sdk stage and produce different bytes (sdkmanager writes build-time mtimes), so the SDK layer is only shared if the matrix builds share the cache (cache-from/cache-to on the registry or gha) or build/push the sdk stage once first. Clamping mtimes with SOURCE_DATE_EPOCH + rewrite-timestamp=true should make it reproducible without a shared cache, but I did not test that.

Smoke tests (JDK 21, JDK 27 and runner-ndk)

All passed: java -version (Temurin 21.0.12.1 / 27), javac, node -v (v24.22.0), JAVA_HOME exists and $JAVA_HOME/bin/java is executable, xxd -v (2023-10-25), flock -V (util-linux 2.39.3), pigz 2.8, ip -V, cmake 3.28.3 (apt), sdkmanager --list_installed:

  • runner: build-tools;37.0.0, platform-tools 37.0.1, platforms;android-37.0
  • runner-ndk: the above + ndk;30.0.16248370 + cmake;3.22.1

Hooks are -rwxr-xr-x, TAR_OPTIONS, MISE_*, HK_*, ACTIONS_RUNNER_HOOK_* and RUNNER_IMAGE_* env are present, workdir is /actions-runner, ENTRYPOINT/CMD unchanged. The embedded sdk-install script passes shellcheck -s sh, and I ran it against a fake sdkmanager that fails once: it retried and succeeded on attempt 2.

Platform 37: stable, and why check-sdk-updates.yml never proposed it (note for W2)

From repository2-3.xml (checked today): platforms;android-37.0 (rev 2), android-37.1 and android-37.2 are all stable: channel-0, uses-license ref="android-sdk-license" (not the preview license), not obsolete, and the codename element is empty (<codename />). Only the -beta1/2/3 packages carry a real codename (CinnamonBun / DEV). So I set COMPILE_SDK=37.0 (matches the consumers' compileSdk 37). 37.1/37.2 also exist if consumers later need the minor releases.

Why the workflow skipped it: is_preview() in the "Check for Android SDK updates" step returns True when any descendant element has the local tag codename:

if local(e.tag) == 'codename':
    return True

It tests for the element's presence, not for non-empty text. Google writes an empty <codename /> on stable platforms from 37.0 on (older ones such as 36 / 36.1 have no such element at all), so 37.0, 37.1 and 37.2 are all classed as preview and dropped; latest() then settled on 36.1, which equals the current value, so no PR was ever opened. Fix for W2: if local(e.tag) == 'codename' and (e.text or '').strip(): return True. Also worth a check on the latest() regex: platforms;android-(\d+(?:\.\d+)?) accepts 37.2 but ignores -extNN/-betaN, which is what we want.

Heads-up: the stable platform dir is platforms/android-37.0, not android-37. I did not run a Gradle build against it; consumers with compileSdk = 37 on a current AGP should resolve it, but the first real consumer build is the check. Also, once the workflow is fixed it will propose 37.2 next.

Other notes

  • New external base: eclipse-temurin:21-jdk-noble (sdk stages only, never in the final image). Renovate can track it.
  • --target runner never builds sdk-ndk (BuildKit skips unreferenced stages).
  • Build-time pulls of dl.google.com, packages.adoptium.net, deb.nodesource.com are unchanged.

🤖 Generated with Claude Code

azlekov and others added 2 commits October 19, 2026 20:13
… NDK opt-in

- sdk stage builds the Android SDK on a fixed JDK (SDK_JAVA_VERSION) and is
  copied in right on top of the base image, before the shared apt layer, the
  JDK and Node, so the SDK layer is the same for every JDK/Node tag
- one apt layer for the tag-independent packages (iproute2, cmake,
  build-essential, swig, xxd, pigz) with the apt lists removed
- ndk and cmake;3.22.1 move to the opt-in runner-ndk target; runner stays the
  last stage and is the default
- COMPILE_SDK 36.1 -> 37.0 (stable)
- COPY --chmod instead of a separate chmod layer

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… upstream sync

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@azlekov

azlekov commented Oct 19, 2026

Copy link
Copy Markdown
Author

folded into the combined refactor PR

@azlekov azlekov closed this Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant