Skip to content

feat!: remove Android from the CLI; android is a provider (cli#405 PR 3) - #441

Merged
apotema merged 2 commits into
developmentfrom
feat/405-remove-android
Sep 27, 2026
Merged

apotema merged 2 commits into
developmentfrom
feat/405-remove-android

Conversation

@apotema

@apotema apotema commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

PR 3 of the #405 plan. The CLI no longer contains any Android code. Android is now the android provider (labelle-android: #12 merged, #13 in review).

  • labelle build|run|bundle --platform=android run that package's hooks.
  • labelle android <cmd> dispatches to its namespace, like any other provider namespace.

The base is development, which already has contract 1.2.0 (#440). Do not merge until labelle-android #13 is proven on Flying Platform. The dev→main merge waits for the web provider (user decision: CLI 2.0.0, one release train).

Deleted

  • src/cli/android.zig
  • src/cli/android/{apk_slim,build,deploy,doctor,launcher_icon,package,run,studio}.zig
  • src/cli/android_sdk.zig
  • docs/workflows/android-release.yml (it now lives in labelle-android, rewritten for bundle + labelle android deploy)

Edited

File Change
src/cli.zig Removed: the android import, its parse branch and value-flag handling, the projectless android doctor fast path, the helpOnlyPrinter .android_cmd entry and its tests, the ApkSlim*Spec and ArgsResolveAndroidBackendSpec re-exports, and the usage text. An unknown first word now goes through reportUnknownCommand (see "Namespace diagnostic" below).
src/cli/args.zig, args_tests.zig .android_cmd removed from Command. resolveAndroidBackend and its spec removed (D9).
src/cli/pipeline.zig The import and the .android_cmd branch removed.
src/cli/pipeline/args_resolve.zig The raylib→sokol Android fallback removed.
src/cli/pipeline/build.zig The packageApk finalisation on labelle build and its import removed.
src/cli/pipeline/run.zig The Android launch branch removed. crossTargetLaunchSkipped drops .android, and its test covers the new shape.
src/cli/pipeline/install.zig New NoRunReplacement gate, plus legacyRunBranch and noRunReplacement with their decision-table tests.
src/cli/project_config.zig AndroidConfig and the android field deleted. Orientation docs made neutral. A test shows that a legacy .android block still parses (the CLI's parse sets ignore_unknown_fields). Platform.android and AssetCompression.android stay until assembler#378 and #407.
src/cli/provider_dispatch.zig "android" removed from reserved, with a test.
src/cli/provider_github{,/registry_cache}.zig New cachedRegistryNamespaceOwner: schema 2 only, cache only, with a test.
help.zig, runner.zig, bundle.zig, app_icon.zig, doctor.zig, add.zig, plugins.zig, check.zig, lockfile.zig, provider_hooks.zig, assembler_proc.zig, pipeline/generate.zig Comments reworded so they no longer name Android.
src/agnostic_guard_test.zig Allowlist shrunk (below). The allowedIndex and stale-entry tests now use cli/serve/*, cli/ios.zig and cli/emsdk_cache.zig, and assert that the removed cli/android* paths are no longer exempt.
docs provider-targets.md: new "labelle run" section, migration notes, verification. provider-hooks.md: limitations. provider-local-dispatch.md: reserved list and namespace diagnostic. tester-onboarding.md: bundle, then labelle android deploy, with settings in providers/android.json. rfc-android-packaging.md: status. provider-contract-v1.md: the provider_config example uses package android.

New generic gate: NoRunReplacement

This sits next to NoBundleReplacement. It is decided with the hook plans after discovery: after the install, before generate, lock or build.

  • Condition: command == .run and target.provider != null and hook_plans.run.replace == null and !legacyRunBranch(target.legacy).
  • legacyRunBranch is true only for {wasm, ios}. It only shrinks: a platform leaves it once its launch moves into a provider.
  • Message:
    labelle: target '<t>' has no run replacement; package '<pkg>' must declare a `.when = .replace` hook on `run`
    
    followed by error: NoRunReplacement, exit 1.

Without this gate, a provider target with no run replacement would fall through to the host launch and run a binary built for another platform. The gate applies to foreign provider targets too, such as probe-target. So the control step in provider_hooks_e2e.py changed: it used to show a replacement-less provider target reaching the host launch. Now it runs the planted sentinel directly, to show the check is real, and then asserts the run is refused before any hook runs.

Namespace diagnostic (generic)

  • labelle <word>, where no built-in, pinned namespace or directory matches, is still unknown command '<word>'.
  • When the cached schema-2 registry names a package that declares <word> as its namespace, the message is instead:
    labelle: no provider for namespace '<word>' in this project; add and pin the package that declares namespace '<word>'
      (registry: <pkg>)
    
    This is the namespace counterpart of the existing no-provider-for-target hint. It reads the cache only and never uses it to dispatch.

Guard allowlist delta: 69 → 54 entries

  • Removed with their files (10): cli/android.zig, cli/android/{apk_slim,build,deploy,doctor,launcher_icon,package,run,studio}.zig, cli/android_sdk.zig.
  • Came clean after the rewording, so the guard reported them stale (5): cli/add.zig, cli/app_icon.zig, cli/check.zig, cli/lockfile.zig, cli/plugins.zig.
  • No entry was added. src/astc/* is untouched (feat(astc): read upload capabilities from backend manifests #428).

Tests

All run locally on macOS arm64 with Zig 0.16.0, and each exit code was checked on its own:

  • zig fmt --check on every touched .zig file: clean.
  • zig build: 0.
  • zig build -Dtarget=x86_64-windows: 0.
  • zig build test: 0; 1024/1029 passed, 5 skipped, guard included.
    • A mutation check confirmed that the four new unit tests are collected: breaking each one made it fail by name. They are legacyRunBranch, the legacy-block parse, the unreserved namespace and cachedRegistryNamespaceOwner.
  • zig build test-provider-dispatch: 0; 81/81.
  • Python e2e: every suite exited 0.
Suite Real CLI invocations
provider_dispatch_e2e 25
provider_github_e2e 70
provider_config_e2e 21
provider_hooks_e2e 50
provider_targets_e2e 40
provider_android_like_e2e (new) 18
plugin_compat_e2e PASS

test/provider_android_like_e2e.py is new and wired into ci.yml next to the other provider suites, on all three OSes. It uses no NDK. A fixture package android (the test/fixtures/provider tool under an android-shaped manifest) declares target and namespace android, the hooks package (after build), deploy (replaces run) and bundle (replaces bundle), and a run command. A fake assembler stands in for the real one. The suite asserts:

  1. Without the pin, run, build and bundle --platform=android each fail with the no-provider message. labelle android run fails with unknown command, or with the namespace diagnostic when a cache exists. With a schema-2 registry cache, the (registry: android) line is present; without one, it is absent. Nothing is generated and no host launch happens.
  2. With the pin:
    • The assembler receives --platform android.
    • The after-build hook sees target_dir, and zig-out/lib/libgame.so exists while it runs: it copies the file, which fails if the file is missing.
    • The run replacement receives run.env built from --scene/--screenshot/--after.
    • The hooks run in the order package, then deploy.
    • The host-launch sentinel does not run. A control run of the same sentinel shows it would leave a marker.
  3. With the run hook removed from the manifest, the run fails with NoRunReplacement after the install and before generate. No hook runs, and build still works.
  4. labelle android run --device X --release reaches the tool verbatim, and labelle android lists the namespace's commands.
  5. bundle --platform=android --build-number=7 passes build_number = "7", and output_dir is <target>/zig-out/bundle/android.
  6. A project.labelle with a legacy .android block goes through the CLI unchanged.

provider_targets_e2e.py: labelle android build with no provider is now an unknown command instead of the target no-provider error.

The real provider with this CLI, without unreserve-android.sh. I used a detached temp worktree of labelle-android origin/feat/provider-packager (e8bfe65), pinned local: in a temp fixture project (bgfx) with a hand-written labelle.lock, and removed both afterwards. Results:

  • labelle help lists:
    labelle android doctor — Check the Android SDK/NDK/JDK
    labelle android run — Install and launch the built APK (--device, --apk)
    labelle android deploy — Publish the bundled APK as a GitHub Release (--tag, --channel, --notes-file, --apk)
    
  • labelle android doctor exited 0 through the provider. The CLI built bin/labelle-android into .labelle/providers/android, and the tool printed its own note: no providers/android.json … using defaults followed by the SDK/NDK/JDK report.
  • strings zig-out/bin/labelle no longer contains the doctor's text: there are 0 matches for All required Android tools, labelle android doctor or android.jar.

No device run was needed in this PR.

Breaking

This change is breaking and ships in CLI 2.0.0. There are no shims.

  • labelle android build|run|studio|deploy|doctor are gone as built-ins. labelle android … exists only when the project pins the android package.
    • build and run have no command form any more: use labelle build|run --platform=android.
    • studio is dropped (D1).
    • doctor, run and deploy are provider commands that act on already-built output.
    • Outside a project, labelle android doctor no longer works (D8, accepted). Run it inside a project that pins the package.
  • labelle build --platform=android no longer packages an APK itself. Without the provider it fails with the no-provider error. With the provider, the provider's after-build hook writes zig-out/apk/game.apk (D10), replacing .labelle/<b>_android/{game.apk,apk-staging,symbols}.
  • labelle run --platform=android no longer deploys by itself. A provider target without a replace run hook is refused with NoRunReplacement.
  • The raylib→sokol backend switch for Android is gone (D9). A project must declare an Android-capable backend. bgfx is the only one verified through the provider packager (D3).
  • project.labelle .android is no longer read by the CLI. It still parses. Packaging keys move to providers/android.json. The assembler's codegen keys (immersive_mode, target_sdk_version, load_assets_from_apk) stay until the assembler shrinks the block in Android: one APK packaging path — retire the assembler's package_apk and slim the Gradle path too #405 PR 4a.
  • The --emulator, --all-abis, --release/--release-small, --keystore*, --tag, --channel and --notes-file flags of labelle android … are gone. Output is arm64 only (D6), --optimize replaces --release, signing moves to the provider settings, and the deploy flags belong to labelle android deploy.
  • labelle-studio, if it shells out to labelle android …, must move to the provider (not verified here).

Not in this PR

  • The assembler's package_apk.txt retirement and the .android shrink (PR 4a).
  • The backend example scripts (PR 4b).
  • FP migration (PR 5, which carries the real-device acceptance).
  • Sokol-Android verification (D3 follow-up).

Refs #405

https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Delete the CLI's Android subcommand, packager, SDK probe and doctor
(src/cli/android.zig, src/cli/android/*, src/cli/android_sdk.zig). The
`android` package (labelle-android) now owns target and namespace
`android`: `labelle build|run|bundle --platform=android` run its hooks and
`labelle android <cmd>` dispatches to it like any provider namespace.

- cli.zig: drop the `android` parse branch, the projectless doctor fast
  path, the helpOnlyPrinter entry and the ApkSlim spec re-exports.
- args.zig: drop `.android_cmd` and `resolveAndroidBackend` (D9: a
  non-Android backend now fails instead of switching to sokol).
- pipeline: drop the sokol fallback, the `packageApk` finalisation on
  `build` and the Android launch branch on `run`.
- project_config.zig: drop `AndroidConfig` and the `.android` field; the
  lenient parse still accepts a legacy block.
- provider_dispatch.zig: `android` leaves `reserved`.
- New generic gate `NoRunReplacement`: `labelle run` of a provider target
  with no `replace run` hook, outside the legacy `{wasm, ios}` run
  branches, is refused after the install and before any build.
- An unknown first word that the cached schema-2 registry names as a
  package namespace gets the no-provider-for-namespace diagnostic with a
  `(registry: <pkg>)` line.
- Agnosticism guard allowlist 69 -> 54: the ten android entries plus five
  files that came clean (add, app_icon, check, lockfile, plugins).
- test/provider_android_like_e2e.py: an android-shaped fixture provider
  through the real CLI, no NDK; wired into CI. Hooks/targets e2e updated.

Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP
…cli#405)

Move docs/workflows/android-release.yml to labelle-android (deleted here).
Update provider-targets (a `labelle run` section, the migration notes,
verification), provider-hooks limitations, provider-local-dispatch's
reserved list, tester onboarding (bundle then `labelle android deploy`,
settings in providers/android.json), the Android packaging RFC status,
and the contract doc's provider_config package name (`android`).

Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP
@coderabbitai

coderabbitai Bot commented Sep 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 1119725a-f4cc-4cc8-8b9d-985b491db030

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-27T01:06:42.338892Z b2ffcfa PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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