feat(os-image): ReDroid 16 GMS H264 VM image + dual-arch CI to ghcr#131
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ubuntu 22.04 + Docker + ReDroid 16 GMS VM image where the Android container is baked restart-primed:
build.shcreates + arms a--restartcontainer 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-bootdocker create, no launcher service, and the vfs create-copy is off the run/start path.Image contents (Android 16)
libndk_translationfor ARM-on-x86 (amd64 only).:5900(H.264 passthrough + Tight/JPEG fallback), ADB on:5555, public DNS (8.8.8.8/1.1.1.1).Networking fix
systemd-networkd's20-wiredName=e* v*uplink match also grabbed Docker'sveth*interfaces and raced Docker's bridge enslavement, deterministically leaving the container's veth offdocker0on some builds (docker0linkdown, no container network, adb offline). A higher-priority05-docker-unmanaged.network(Unmanaged=yesforveth*/docker0/br-*) keeps networkd off Docker's links so the veth stays enslaved on every boot.First-boot setup (
fossify-home.sh, onsys.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/appupdate so its embedded Chimera modules load, then makes Fossify the sole HOME. The/var/lib/redroid-data/databind persists, so later boots are only a path check.Portable, restart-primed bake
The container ENTRYPOINT is a static-BusyBox
cocoon-bake-initwrapper. At bake it removes a one-shot marker andsleeps without booting Android, so the CI runner needs nobinder/ashmem/native-arch (arm64 bakes with no qemu). In the VM the marker is gone, so the wrapperexecs/initand Android boots normally. This keepsuse_memfd=0(the VM's Jammy kernel providesashmem_linux).Disk
vfs stores the ReDroid image layer, the container, and its init layer as three near-identical ~1.3 G copies. A
hardlinkpass over/var/lib/docker/vfs/dirbefore taring ships them once — safe because in the VM/var/lib/dockeris 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
superpartition fromsystem.img's GPT,lpunpacksproduct.img+system_ext.img, and runsprepare-gapps16-inner.shto emitgapps16-<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.ymlruns an optional per-imageci-prepare.shhook (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) — intodocker-data-<arch>.tar. A single multi-platformbuild-push-actionthen builds the VM image (each platformADDing its own tar), runsverify-vm-inner.shin both solves, and pushes a real multi-arch OCI index.publish-asfile remaps the family/tag toandroid: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 (viaworkflow_dispatch -f only=…) therefore never clobbers the release tag..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 pull→cocoon vm run(CH dev): containerUp(restarts=0), Android 16boot_completed=1, veth ondocker0/docker0UP on every boot, ping + DNS OK.:5555(adb) and:5900(VNC) reachable ~0–28 s aftervm 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 reachesplay.googleapis.com; GMS checkin provisionsandroid_id— GApps are functional and networked. Externaladb connect <vm-ip>:5555→device→adb shellworks;cocoon vm exec(vsock) reliable. Both arches build + push green.