← Dev Log
BallOrcs Battle4 min read

BallOrcs Battle: the phone build stops being a button somebody has to remember

No new battlefield this week — this was a week under the floorboards.

In plain words

No new battlefield this week — this was a week under the floorboards. Battle builds for four platforms, and the Android one was the one nobody trusted, so it sat behind a button that a person had to press by hand. It kept dying during packaging with a message that explained nothing: the packaging tool was helping itself to as much of the machine as it liked and getting shut down for it. Once it was told how much it may use, the build finished — and immediately revealed that every Android build had been compiling a second version for emulators and then throwing it away. Both are fixed, and Android now builds automatically on every change.

Under the hood

Four days of build plumbing, all of it on things that were failing quietly or lying about their own state.

  • --jobs never reached Gradle (41304da). Pipeline 402's build:android compiled cleanly at jobs=2, peaking at 2.4 GiB — then the export stage shelled out to Gradle, which started a daemon that grew to 2.7 GB RSS and was cgroup-OOM-killed against BUILD_MEM_LIMIT. Gradle reported only "Gradle build daemon disappeared unexpectedly"; the actual cause appears in dmesg (constraint=CONSTRAINT_MEMCG, task=java) and nowhere in the job log. 43 minutes, no APK. --jobs governs scons and nothing else — Gradle sizes its daemon, worker pool and heap from nproc and available RAM, so on an 8-CPU box it ran about seven ways wide regardless. GRADLE_OPTS is the knob that bounds it, and it joins the -e forwarding list because it is read on the far side of the container boundary.
  • An ABI built four times a week and shipped never (5bd376f). verify:android-package ran for the first time on pipeline 403 and immediately caught what it was written to catch: TARGET_ARCHES builds arm64 and x86_64, but neither game.gdextension nor export_presets.cfg shipped x86_64. Every Android build compiled an x86_64 engine template and an x86_64 GDExtension and threw both away, and the APK went out arm64-only — so the emulator those artifacts existed for could not run the package they were built for. Shipping it (rather than dropping the arch) is the deliberate call: the build already pays the compile cost. Note this escalates the checker — a shipped ABI missing from the APK is an error where built-but-unshipped was a warning.
  • Android goes automatic on main (f4653b6). Both jobs move off the shared .optional_platform_rules onto their own .android_rules — same conditions, minus when: manual. Manual is how the ABI drift survived: a gate nobody triggers is not a gate. allow_failure stays true for now (two green runs is not a track record, and it is the heaviest job on a box with 7.8 GB of guest RAM), and web stays manual because the web artifact is still unproven. Costs ~25 min warm, and the runner is concurrent = 1, so it roughly doubles a main pipeline.
  • Which is why auto-cancel had to start working (a8b4e07). auto_cancel_pending_pipelines only ever cancelled pipelines with no job started — so on 2026-08-11 #407 was 22 minutes into build:linux when #408 superseded it, and #408's ten-second shellcheck was queued to wait about 45 minutes behind a build nobody wanted the answer to. workflow.auto_cancel.on_new_commit: interruptible extends this to running jobs, but GitLab only cancels if every started job opts in, so a per-job sprinkling achieves nothing: default: interruptible: true makes jobs opt out instead. Safe here because the builds are pure functions of the commit. Scheduled pipelines are exempt — the Monday release train tags and writes SHA256SUMS, and killing that halfway is precisely the state that once shipped an empty checksum file.
  • A guard that had stopped covering its own subject (f9b36ea). GRADLE_OPTS was added to the container's -e list and never joined MUST_FORWARD in the test that exists to catch unforwarded knobs — so for a day the check did not cover the fourth instance of the thing it checks. Watched failing, not passing: removing the -e line now reddens the test with the variable named. The general form is worth stating, because two guards here have it — when a check carries a hand-maintained list, adding the thing without adding the list entry is the same defect one level up.
  • The docs that still described the old arrangement (ff582f3, b02caad, 468b2a2). Two files still assumed Android was manual on main, one of them arguing that an emulator job would inherit those rules and therefore stay cheap — now wrong in the expensive direction. The dated CI-facts log was left in place with a SUPERSEDED marker rather than rewritten, because the pipeline timings around it were measured under the old arrangement. The open-items note also had an item asserting two faults and evidencing one: a 401 from the registry establishes dead credentials and says nothing at all about whether the image digest resolves, and those have different fixes.

RTS phase work stands where it did — no commit this week claims progress past what has already landed. The payoff is that the pipeline that will have to carry it now runs the Android build on every push instead of when someone remembers.

#build#ci#android#tooling