Skip to content

feat(os-image): ReDroid 16 GMS H264 VM image + dual-arch CI to ghcr#131

Merged
CMGS merged 12 commits into
masterfrom
feat/os-image-redroid-16.0-gms-h264
Jul 15, 2026
Merged

feat(os-image): ReDroid 16 GMS H264 VM image + dual-arch CI to ghcr#131
CMGS merged 12 commits into
masterfrom
feat/os-image-redroid-16.0-gms-h264

Conversation

@CMGS

@CMGS CMGS commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Ubuntu 22.04 + Docker + ReDroid 16 GMS VM image where the Android container is baked restart-primed: build.sh creates + arms a --restart container in a privileged DinD via a one-shot bake wrapper, so the VM's dockerd replays it on boot and Android cold-boots there — no first-boot docker create, no launcher service, and the vfs create-copy is off the run/start path.

Image contents (Android 16)

  • Google Play services (GMS), Play Store, Google Services Framework, Chrome + Trichrome — GApps overlay carved from Google's pinned API-36 Play Store system image.
  • Fossify as the sole HOME — a first-boot hook disables Launcher3 once Fossify is the resolved HOME, so Android never shows the "Select Home app" chooser; libndk_translation for ARM-on-x86 (amd64 only).
  • Camera2 and the Chromium WebView test shell (Browser2) are removed; Android System WebView is kept.
  • 720x1280, 320 dpi, 60 fps, 8 Mbit/s scrcpy H.264 over one RFB :5900 (H.264 passthrough + Tight/JPEG fallback), ADB on :5555, public DNS (8.8.8.8/1.1.1.1).

Networking fix

systemd-networkd's 20-wired Name=e* v* uplink match also grabbed Docker's veth* interfaces and raced Docker's bridge enslavement, deterministically leaving the container's veth off docker0 on some builds (docker0 linkdown, no container network, adb offline). A higher-priority 05-docker-unmanaged.network (Unmanaged=yes for veth*/docker0/br-*) keeps networkd off Docker's links so the veth stays enslaved on every boot.

First-boot setup (fossify-home.sh, on sys.boot_completed)

The portable GApps overlay carves only product/system_ext, so GMS Core ships as a system-only APK with no usable Chimera modules (Chrome's Dynamite/certificate lookups then fail and it crashes). The hook stages the identical signed GMS container APK once as a /data/app update so its embedded Chimera modules load, then makes Fossify the sole HOME. The /var/lib/redroid-data /data bind persists, so later boots are only a path check.

Portable, restart-primed bake

The container ENTRYPOINT is a static-BusyBox cocoon-bake-init wrapper. At bake it removes a one-shot marker and sleeps without booting Android, so the CI runner needs no binder/ashmem/native-arch (arm64 bakes with no qemu). In the VM the marker is gone, so the wrapper execs /init and Android boots normally. This keeps use_memfd=0 (the VM's Jammy kernel provides ashmem_linux).

Disk

vfs stores the ReDroid image layer, the container, and its init layer as three near-identical ~1.3 G copies. A hardlink pass over /var/lib/docker/vfs/dir before taring ships them once — safe because in the VM /var/lib/docker is a read-only EROFS lower under overlayfs (every container write copies up, never mutating a shared inode). Image size 4.3 GiB → 1.75 GiB (−59%).

GApps sourcing (prepare-gapps16.sh)

Resolves + downloads the pinned API-36 Play Store system image (per-arch zip + SHA-1, env-overridable), extracts the super partition from system.img's GPT, lpunpacks product.img + system_ext.img, and runs prepare-gapps16-inner.sh to emit gapps16-<abi>.tar. Only the raw Google zip is cached; the derived overlay is never committed.

CI (folded into build-os-images.yml, no dedicated workflow)

The shared build-os-images.yml runs an optional per-image ci-prepare.sh hook (hashFiles-gated, a no-op for every other image) that carves GApps and bakes both arches on one runner — amd64 native, arm64 emulated under QEMU (qemu binfmt registered inside the DinD) — into docker-data-<arch>.tar. A single multi-platform build-push-action then builds the VM image (each platform ADDing its own tar), runs verify-vm-inner.sh in both solves, and pushes a real multi-arch OCI index.

  • A publish-as file remaps the family/tag to android:16.0-gms-h264; the canonical tag publishes only from master, while the always-on -<sha> tag gives a pullable pre-merge ref (android:16.0-gms-h264-<sha>). A feature-branch build (via workflow_dispatch -f only=…) therefore never clobbers the release tag.
  • No .skip-generic-build, no per-arch native runners, no manifest-stitch job, no binder preflight — the sleep-only bake wrapper needs none of them.

Validation (real hardware, from the GHCR-pulled image)

cocoon image pullcocoon vm run (CH dev): container Up (restarts=0), Android 16 boot_completed=1, veth on docker0 / docker0 UP on every boot, ping + DNS OK. :5555 (adb) and :5900 (VNC) reachable ~0–28 s after vm start; four stop/start cycles all clean (not start-once). GMS/Play/Chrome/GSF/Fossify present, Camera2/webview-shell absent, use_memfd=0, 60 fps. Google Play renders its sign-in UI and Finsky reaches play.googleapis.com; GMS checkin provisions android_id — GApps are functional and networked. External adb connect <vm-ip>:5555deviceadb shell works; cocoon vm exec (vsock) reliable. Both arches build + push green.

CMGS added 12 commits July 15, 2026 01:17
…container

The ReDroid container is created and started at build time in a privileged DinD
and baked into /var/lib/docker with --restart unless-stopped, so the VM's dockerd
replays it on boot with no first-boot docker create and no launcher service —
removing the per-VM vfs create-copy from the run/start path. binder/ashmem load
via a docker ExecStartPre drop-in: the exported rootfs keeps /.dockerenv, so
systemd-detect-virt reports "docker" and skips systemd-modules-load. Dual-arch —
amd64 keeps libndk ARM translation, arm64 drops it (native ARM); vfs retained.
Manual/push-triggered: native runner per arch (amd64 ubuntu-24.04, arm64
ubuntu-24.04-arm), binder preflight, privileged DinD bake, ghcr multi-arch
manifest. A push runs the plain-redroid probe; workflow_dispatch does the full
GApps + push build.
…ch push

prepare-gapps16.sh fetches Google's API-36 Play Store system image, extracts the
super partition, lpunpacks product.img + system_ext.img, and runs
prepare-gapps16-inner.sh to emit gapps16-<abi>.tar. binder preflight now checks
the module is loaded (ReDroid mounts binderfs in-container) rather than a
/dev/binder node. A branch push builds the real GApps image and pushes to ghcr.
The 20-wired Name=e* v* uplink match also grabbed docker's veth* and raced
docker's bridge enslavement, deterministically leaving the redroid container's
veth off docker0 on the CI/ghcr build (docker0 linkdown, no container network,
adb offline) while local builds happened to win the race. Add a higher-priority
Unmanaged rule for veth*/docker0/br-* so networkd never touches docker links.

Also drop the hardcoded internal redroid DNS (let redroid default) and align the
ghcr image to android:16.0-gms-h264.
…ublishing

Merge fix/pr-131-ci-portability, reconciled with the networkd fix + android rename:
- bake via a static-busybox one-shot wrapper that sleeps instead of booting Android
  on the runner, so CI needs no binder/native-arch; use_memfd back to 0
- pin the GApps r07 zip + sha1 per arch for reproducible builds
- feature pushes publish immutable dev-<sha> tags; only master/manual publish the
  canonical tag; refuse push with gms=false
- exclude this dir from the generic build-os-images matrix (.skip-generic-build)
- run verify-vm-inner.sh against the built image in CI; also assert the networkd shield
- keep public DNS (8.8.8.8/1.1.1.1) baked, never the internal one
…mage

vfs keeps the ReDroid image layer, the container, and its init layer as three
near-identical ~1.3G full copies. Hardlink the identical files before taring
docker-data, so the image ships them once. Safe because in the VM
/var/lib/docker is a read-only EROFS lower under overlayfs: every container
write copies up and never touches a shared inode.
Keeping Launcher3 enabled left two HOME apps, so Android showed the Select Home
chooser on first boot (with AOSP highlighted). Disable Launcher3 once Fossify is
confirmed as the resolved HOME so Fossify is the only home app and no chooser
appears.
…[skip ci]

The Dockerfile header said binder/ashmem load via modules-load.d and the
container is started at bake; actually the bake wrapper only sleeps and
binder/ashmem load via a docker.service ExecStartPre drop-in. README still
described Launcher3 as a recovery HOME (now disabled) and omitted the vfs
hardlink dedup. Comments/docs only; the built image is unchanged.
…are hook

Replace the dedicated build-redroid-16-gms.yml with a generic mechanism:
- build-os-images.yml gains an optional per-image ci-prepare.sh hook (hashFiles-
  gated, no-op for every other image), a publish-as name remap, and a
  master-gated canonical raw tag (the always-on sha tag gives a pullable
  pre-merge ref without clobbering the release tag). An `only` dispatch input
  scopes a run to one dir.
- ci-prepare.sh bakes both arches on one runner (amd64 native, arm64 under QEMU;
  the sleep-only bake wrapper needs no binder/native arch) into docker-data-<arch>.tar.
- Dockerfile ADDs docker-data-${TARGETARCH}.tar and self-resolves the scrcpy-rfb
  commit (the shared build step passes no build-args). build.sh is now a bake-only
  worker. publish-as -> android:16.0-gms-h264.
- Delete build-redroid-16-gms.yml and .skip-generic-build.
The arm64 bake on an amd64 runner crash-looped: the runner's host binfmt is not
reliably visible to the nested DinD daemon, so the emulated arm64 ReDroid wrapper
could not exec. Install tonistiigi/binfmt (F-flag) inside the DinD before the
container run. Also fail the bake on a non-zero restart count and dump the
container logs, so a bad bake is self-diagnosing.
Disabling Launcher3 to suppress the home-picker also removed the on-screen
back/home/recents buttons: in redroid, Launcher3/Quickstep provides the SystemUI
navigation bar and recents. Instead keep it enabled (as a fallback and the nav
provider) and just set Fossify as the default HOME. Do the set-home-activity
first, before the multi-second GMS staging install, so no chooser appears in that
window.
@CMGS CMGS merged commit ded0c1b into master Jul 15, 2026
6 checks passed
@CMGS CMGS deleted the feat/os-image-redroid-16.0-gms-h264 branch July 15, 2026 15:24
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