Skip to content

ci(android): build every ABI and gate on 16 KB page alignment - #289

Merged
doublegate merged 8 commits into
mainfrom
feat/android-ci
Aug 1, 2026
Merged

ci(android): build every ABI and gate on 16 KB page alignment#289
doublegate merged 8 commits into
mainfrom
feat/android-ci

Conversation

@doublegate

@doublegate doublegate commented Jul 31, 2026

Copy link
Copy Markdown
Owner

First v1.30.0 item.

android/ had Gradle sources and a cargo ndk layout but no workflow at all — nothing built it, so nothing could regress visibly.

What it does

Cross-builds rustysnes-android for all four ABIs, asserts 16 KB PT_LOAD alignment on the 64-bit ones, assembles a debug APK, and checks the APK actually carries each ABI.

The alignment gate

Play requires 16 KB-aligned segments for 16 KB-page devices; a 4 KB-aligned library simply fails to load there. The NDK has defaulted to 16 KB since r27, so this is a regression gate — it catches an NDK downgrade or a stray linker flag, which would otherwise surface as a store rejection rather than a red build.

It fails closed when the .so glob matches nothing, so a build that produces no libraries cannot pass it silently — that is the failure mode a naive loop would have.

Two actions, both already pinned here

I first wrote this with setup-ndk, setup-java and setup-gradle, then checked the committed tree and found none of them in use — and that four of the five SHAs I had written were invented. Rather than introduce three third-party actions to the surface #276 just tightened, the NDK now comes from the runner image's own sdkmanager and the JDK and Gradle are preinstalled. Only checkout and upload-artifact remain, both with pins taken from committed workflows.

No github.event.* interpolation anywhere; pull_request not pull_request_target; contents: read; persist-credentials: false.

Scope

assembleDebug, not release — signing material is maintainer-only and stays out of CI per docs/mobile-readiness.md. This does not flip the STATUS line; it is the engineering half.

🤖 Generated with Claude Code

Adds Android CI coverage for four ABIs, 16 KB PT_LOAD alignment, native-library output, and debug APK packaging.

The claim is false if any ABI fails to build, produces no native library, violates alignment, or is absent from the APK. The workflow does not provide release signing or change mobile readiness status.

No AccuracySNES dossier assertions or coverage denominator changed. No emulator-core behavior changed.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@doublegate, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 22 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8735dc86-6e44-4dab-93ab-3d38c1438389

📥 Commits

Reviewing files that changed from the base of the PR and between 3e0e1d0 and 3aefce4.

⛔ Files ignored due to path filters (1)
  • android/gradle/wrapper/gradle-wrapper.jar is excluded by !**/*.jar and included by android/**
📒 Files selected for processing (7)
  • .github/workflows/android.yml
  • CHANGELOG.md
  • android/app/build.gradle.kts
  • android/gradle.properties
  • android/gradle/wrapper/gradle-wrapper.properties
  • android/gradlew
  • android/gradlew.bat

Walkthrough

The pull request adds Android CI and Gradle project configuration. The workflow builds JNI libraries for four ABIs, validates 16 KB alignment and APK contents, assembles a debug APK, and uploads it. The Gradle wrapper is pinned to 8.10.

Changes

Android CI

Layer / File(s) Summary
Workflow and Android toolchain setup
.github/workflows/android.yml
The workflow adds filtered push, pull request, scheduled, and manual triggers. It configures Rust targets, NDK 27.2, cargo-ndk, permissions, concurrency, and build settings.
Native build and APK validation
.github/workflows/android.yml
The workflow builds four JNI libraries. It checks 16 KB PT_LOAD alignment, assembles a debug APK, and validates required ABI and project-library contents.
Gradle wrapper and Android project configuration
android/gradle.properties, android/gradle/wrapper/gradle-wrapper.properties, android/gradlew, android/gradlew.bat
The Android project enables AndroidX and defines Gradle JVM settings. The wrapper uses Gradle 8.10 and supports POSIX and Windows execution.
APK artifact publication and changelog
.github/workflows/android.yml, CHANGELOG.md
The workflow uploads the debug APK with 14-day retention. The changelog records the Android build, alignment, packaging, and Gradle changes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitHubActions as GitHub Actions runner
  participant CargoNdk as cargo-ndk
  participant AndroidGradle as Android Gradle
  participant APKChecks as APK validation
  GitHubActions->>CargoNdk: Build JNI libraries for four ABIs
  CargoNdk-->>GitHubActions: Write native libraries
  GitHubActions->>GitHubActions: Check 16 KB ELF alignment
  GitHubActions->>AndroidGradle: Assemble debug APK
  AndroidGradle-->>GitHubActions: Generate debug APK
  GitHubActions->>APKChecks: Validate ABI contents and APK alignment
  APKChecks-->>GitHubActions: Pass or fail validation
Loading

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 10
✅ Passed checks (10 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title uses the valid ci(android) Conventional Commit format, uses imperative wording, and accurately describes the Android ABI and alignment checks.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Changelog Entry ✅ Passed The full base-to-HEAD diff includes a CHANGELOG.md entry, and the pull request changes CI/build configuration rather than user-visible behavior.
Docs-As-Spec ✅ Passed The base-to-HEAD diff changes only the Android workflow, changelog, and Gradle wrapper files; it changes no crates/rustysnes-/ behavior, so no matching docs edit is required.
Accuracysnes Bookkeeping ✅ Passed The base-to-HEAD diff changes no file under tests/roms/AccuracySNES/gen/src, so the conditional bookkeeping checks do not apply.
No Panic On Untrusted Input ✅ Passed The full PR adds only workflow, changelog, Gradle, and wrapper files; its added lines contain no .unwrap(), .expect(), or panic!() operations.
Safety Comment On New Unsafe ✅ Passed The full PR diff from merge-base changes no Rust files, and added lines contain no unsafe; therefore no new unsafe block or unsafe fn requires a SAFETY comment.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feat/android-ci

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/android.yml:
- Around line 6-18: Add "Cargo.lock" to both the push and pull_request paths
lists in the Android workflow, preserving the existing path filters so Android
CI also runs when only the dependency lockfile changes.
- Around line 76-81: Update the “Build the JNI libraries” step to build all
three required crates, including rustysnes_mobile and rustysnes_monetization,
for every ABI listed in the cargo ndk targets. Also replace any directory-only
validation with assertions that each packaged ABI contains all required .so
files, or remove ABIs that are intentionally unsupported.

In `@CHANGELOG.md`:
- Around line 26-29: Update the prose in the changelog entry to say “two
third-party actions only” instead of “two actions only,” preserving the rest of
the workflow description unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 63c7c316-f62c-45e4-8e42-01b06b25a83b

📥 Commits

Reviewing files that changed from the base of the PR and between ee34709 and 10a26fc.

📒 Files selected for processing (2)
  • .github/workflows/android.yml
  • CHANGELOG.md
📜 Review details
⏰ Context from checks skipped due to timeout. (5)
  • GitHub Check: accuracysnes
  • GitHub Check: test-light
  • GitHub Check: lint
  • GitHub Check: build
  • GitHub Check: review
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{rs,md}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Chip-behavior changes must update both the chip implementation and the corresponding docs/<subsystem>.md documentation.

A chip change must update both the chip implementation and its corresponding docs/<chip>.md documentation in the same change.

Files:

  • CHANGELOG.md
CHANGELOG.md

📄 CodeRabbit inference engine (CONTRIBUTING.md)

User-visible changes must be recorded under the [Unreleased] section.

For the full pull request diff against its base branch, modify CHANGELOG.md when user-visible behavior changes, including emulator output, frontend features, CLI flags, public APIs, or AccuracySNES cartridge contents. Do not require it for purely internal changes, tests, comments, or CI configuration.

Files:

  • CHANGELOG.md
**/*

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*: Do not commit or vendor the generated snesdev_wiki/ mirror; it is gitignored and intended only as a local reference.
Keep commits focused and use Conventional Commits: <type>(<scope>): <subject>, with an imperative subject of at most 72 characters.
Do not use emojis in code, comments, or commit messages.
Before opening a PR, ensure formatting, Clippy, workspace tests, the core embedded build, rustdoc with warnings denied, documentation coverage, and changelog requirements pass.
Ticket completion must be reflected in the relevant to-dos/ sprint file.

**/*: Preserve the one-directional crate graph: chip crates must not depend on one another; rustysnes-core ties them together.
Never commit commercial ROMs; only commit derived screenshots and hashes.
Keep docs/STATUS.md as the authoritative per-subsystem status and update project documentation in the same PR as code changes.
Do not treat RustyNES v2.0 or engine-lineage anchors as project releases.

Files:

  • CHANGELOG.md
**/*.md

⚙️ CodeRabbit configuration file

**/*.md: Docs are the spec, not a changelog. Flag prose that has drifted from the code it describes
rather than style nits. The markdownlint gate is pinned to v0.39.0 via pre-commit —
do not report rules that version does not have (MD060 in particular).

Files:

  • CHANGELOG.md
.github/workflows/**

⚙️ CodeRabbit configuration file

.github/workflows/**: Flag third-party actions pinned to a branch rather than a tag or commit SHA, jobs without
a least-privilege permissions: block, and secrets that could reach the log. The
ci-success job is the required check and must depend on every gate it claims to
aggregate — a new job that is not in its needs: list is a gate nothing enforces.

Files:

  • .github/workflows/android.yml
🪛 zizmor (1.28.0)
.github/workflows/android.yml

[info] 34-34: workflow or action definition without a name (anonymous-definition): this job

(anonymous-definition)

Comment thread .github/workflows/android.yml
Comment thread .github/workflows/android.yml Outdated
Comment thread CHANGELOG.md Outdated
Comment on lines +26 to +29
Deliberately two actions only, both already pinned in this repo: the NDK comes from the runner
image's own `sdkmanager` and the JDK and Gradle are preinstalled, rather than adding three
third-party actions to the supply-chain surface the `v1.26.0` pass tightened. `assembleDebug`, not
release — signing material is maintainer-only and stays out of CI.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

State the number of third-party actions.

The workflow invokes three actions, including ./.github/actions/rust-setup. Change “two actions only” to “two third-party actions only” to match the workflow.

As per path instructions, “Docs are the spec, not a changelog. Flag prose that has drifted from the code it describes rather than style nits.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 26 - 29, Update the prose in the changelog entry
to say “two third-party actions only” instead of “two actions only,” preserving
the rest of the workflow description unchanged.

Source: Path instructions

@doublegate
doublegate force-pushed the feat/android-ci branch 2 times, most recently from 9779f95 to 3ad180a Compare July 31, 2026 23:48
Copilot AI review requested due to automatic review settings August 1, 2026 00:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class Android CI coverage to RustySNES by introducing a dedicated GitHub Actions workflow that cross-builds the Android JNI libraries for all supported ABIs, gates 16 KB PT_LOAD alignment for 64-bit targets, and verifies ABI packaging inside a debug APK. This complements the existing iOS workflow by making Android build regressions visible in CI and by committing the Gradle wrapper/config needed for clean-checkout builds.

Changes:

  • Add .github/workflows/android.yml to build JNI .so artifacts for all four ABIs, enforce 16 KB alignment for 64-bit, assemble a debug APK, and validate ABI presence.
  • Commit Gradle wrapper + android/gradle.properties to make Android builds reproducible and work from a clean checkout.
  • Document the Android CI + alignment gate rationale and findings in CHANGELOG.md.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
CHANGELOG.md Adds a detailed changelog entry describing the new Android CI workflow and the 16 KB alignment gate.
android/gradlew.bat Adds the Windows Gradle wrapper entrypoint.
android/gradlew Adds the POSIX Gradle wrapper entrypoint used by CI (./gradlew).
android/gradle/wrapper/gradle-wrapper.properties Pins the Gradle distribution used by the wrapper (Gradle 8.10).
android/gradle.properties Commits required Gradle/AndroidX settings so Android builds succeed from a clean checkout.
.github/workflows/android.yml Introduces Android CI: NDK install, cargo ndk builds for all ABIs, 16 KB alignment gates, debug APK assembly, and artifact upload.
Suppressed comments (1)

.github/workflows/android.yml:220

  • Same issue as above: find android -name '*-debug.apk' | head -1 can select the wrong APK if multiple debug APKs exist, causing the alignment gate to validate an unintended artifact. Scope the lookup to the known output directory so the gate checks the APK that is actually uploaded.
          set -euo pipefail
          apk=$(find android -name '*-debug.apk' | head -1)
          [ -n "$apk" ] || { echo "::error::no debug APK was produced"; exit 1; }
          # Same divisibility test as the jniLibs gate above -- see its comment for why an exact

Comment on lines +14 to +22
pull_request:
paths:
- "crates/rustysnes-android/**"
- "crates/rustysnes-mobile/**"
- "crates/rustysnes-monetization/**"
- "crates/rustysnes-gfx-shaders/**"
- "android/**"
- "Cargo.lock"
- ".github/workflows/android.yml"
Comment on lines +85 to +86
- name: Install cargo-ndk
run: cargo install cargo-ndk --locked --version ^3
- name: Confirm the APK carries every ABI
run: |
set -euo pipefail
apk=$(find android -name '*-debug.apk' | head -1)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
android/gradle/wrapper/gradle-wrapper.properties (1)

1-8: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add distributionSha256Sum to verify the downloaded Gradle distribution.

This file pins distributionUrl to gradle-8.10-bin.zip but does not set distributionSha256Sum. validateDistributionUrl checks only that the URL is well-formed; it does not verify the downloaded bytes. Gradle's own security guidance states to "Set distributionSha256Sum in gradle-wrapper.properties to verify the integrity of the downloaded Gradle distribution" since this "ensures the gradle-X.X-bin.zip file matches the official SHA-256 checksum published by Gradle, protecting your build from corruption or tampering".

Without it, both CI and any developer machine will accept a tampered or corrupted Gradle 8.10 binary from a compromised mirror or MITM. Add the checksum from gradle.org/release-checksums for Gradle 8.10.

Proposed fix
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
+distributionSha256Sum=<sha256 for gradle-8.10-bin.zip from https://gradle.org/release-checksums/>
 networkTimeout=10000
 validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/gradle/wrapper/gradle-wrapper.properties` around lines 1 - 8, Add the
distributionSha256Sum property to the Gradle wrapper configuration alongside
distributionUrl, using the official SHA-256 checksum published by Gradle for
gradle-8.10-bin.zip.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/android.yml:
- Around line 38-39: Add a descriptive name: field to the build job in the
Android workflow, alongside runs-on, so the job is clearly labeled in the GitHub
Actions UI.
- Around line 38-43: Add a job-level timeout-minutes setting to the Android
workflow’s build job, using a 30-minute limit. Keep the existing runs-on,
environment, and steps unchanged.
- Around line 235-255: Separate the per-library presence check from the
alignment loop in the workflow: keep alignment validation restricted to
arm64-v8a and x86_64, but add a distinct loop over all four packaged ABIs for
librustysnes_android.so, librustysnes_mobile.so, and
librustysnes_monetization.so. Preserve the existing missing-library error and
fail behavior.

In `@CHANGELOG.md`:
- Around line 78-79: Update the CHANGELOG wording describing the assembled APK’s
lib/ validation to remove the claim that exactly six 64-bit libraries are
required or that count changes fail. Keep only the behavior implemented by
.github/workflows/android.yml: each .so is checked and the three required
project libraries are verified for every 64-bit ABI.

---

Outside diff comments:
In `@android/gradle/wrapper/gradle-wrapper.properties`:
- Around line 1-8: Add the distributionSha256Sum property to the Gradle wrapper
configuration alongside distributionUrl, using the official SHA-256 checksum
published by Gradle for gradle-8.10-bin.zip.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ecaf6f16-3587-4516-a167-20b116b45d1f

📥 Commits

Reviewing files that changed from the base of the PR and between 10a26fc and 3e0e1d0.

⛔ Files ignored due to path filters (1)
  • android/gradle/wrapper/gradle-wrapper.jar is excluded by !**/*.jar and included by android/**
📒 Files selected for processing (6)
  • .github/workflows/android.yml
  • CHANGELOG.md
  • android/gradle.properties
  • android/gradle/wrapper/gradle-wrapper.properties
  • android/gradlew
  • android/gradlew.bat
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: copilot-pull-request-reviewer
🧰 Additional context used
📓 Path-based instructions (5)
**/*

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*: Do not commit or vendor the generated snesdev_wiki/ mirror; it is gitignored and intended only as a local reference.
Keep commits focused and use Conventional Commits: <type>(<scope>): <subject>, with an imperative subject of at most 72 characters.
Do not use emojis in code, comments, or commit messages.
Before opening a PR, ensure formatting, Clippy, workspace tests, the core embedded build, rustdoc with warnings denied, documentation coverage, and changelog requirements pass.
Ticket completion must be reflected in the relevant to-dos/ sprint file.

**/*: Preserve the one-directional crate graph: chip crates must not depend on one another; rustysnes-core ties them together.
Never commit commercial ROMs; only commit derived screenshots and hashes.
Keep docs/STATUS.md as the authoritative per-subsystem status and update project documentation in the same PR as code changes.
Do not treat RustyNES v2.0 or engine-lineage anchors as project releases.

Files:

  • android/gradle.properties
  • android/gradle/wrapper/gradle-wrapper.properties
  • android/gradlew.bat
  • CHANGELOG.md
  • android/gradlew
**/*.{rs,md}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Chip-behavior changes must update both the chip implementation and the corresponding docs/<subsystem>.md documentation.

A chip change must update both the chip implementation and its corresponding docs/<chip>.md documentation in the same change.

Files:

  • CHANGELOG.md
CHANGELOG.md

📄 CodeRabbit inference engine (CONTRIBUTING.md)

User-visible changes must be recorded under the [Unreleased] section.

For the full pull request diff against its base branch, modify CHANGELOG.md when user-visible behavior changes, including emulator output, frontend features, CLI flags, public APIs, or AccuracySNES cartridge contents. Do not require it for purely internal changes, tests, comments, or CI configuration.

Files:

  • CHANGELOG.md
**/*.md

⚙️ CodeRabbit configuration file

**/*.md: Docs are the spec, not a changelog. Flag prose that has drifted from the code it describes
rather than style nits. The markdownlint gate is pinned to v0.39.0 via pre-commit —
do not report rules that version does not have (MD060 in particular).

Files:

  • CHANGELOG.md
.github/workflows/**

⚙️ CodeRabbit configuration file

.github/workflows/**: Flag third-party actions pinned to a branch rather than a tag or commit SHA, jobs without
a least-privilege permissions: block, and secrets that could reach the log. The
ci-success job is the required check and must depend on every gate it claims to
aggregate — a new job that is not in its needs: list is a gate nothing enforces.

Files:

  • .github/workflows/android.yml
🪛 zizmor (1.28.0)
.github/workflows/android.yml

[info] 38-38: workflow or action definition without a name (anonymous-definition): this job

(anonymous-definition)

🔇 Additional comments (5)
CHANGELOG.md (1)

96-98: Duplicate of the existing action-count wording comment.

The sentence still says “two actions only”. The previous review already requested “two third-party actions only” because the workflow also invokes ./.github/actions/rust-setup.

.github/workflows/android.yml (1)

44-46: LGTM!

Also applies to: 258-264

android/gradle.properties (1)

1-23: LGTM!

android/gradlew (1)

1-253: LGTM!

android/gradlew.bat (1)

1-95: LGTM!

Comment on lines +38 to +39
build:
runs-on: ubuntu-latest

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: name the job.

zizmor flags build as an anonymous job definition. Adding a name: key improves readability in the Actions UI but has no functional effect.

🧰 Tools
🪛 zizmor (1.28.0)

[info] 38-38: workflow or action definition without a name (anonymous-definition): this job

(anonymous-definition)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/android.yml around lines 38 - 39, Add a descriptive name:
field to the build job in the Android workflow, alongside runs-on, so the job is
clearly labeled in the GitHub Actions UI.

Source: Linters/SAST tools

Comment on lines +38 to +43
build:
runs-on: ubuntu-latest
env:
CARGO_NET_RETRY: "10"
CARGO_TERM_COLOR: always
steps:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a job-level timeout-minutes.

The build job has no timeout-minutes. It installs an NDK via sdkmanager, and it runs a Rust and Gradle build. If any step hangs — for example, sdkmanager blocking on an unexpected license prompt — the job runs up to the default GitHub Actions limit before failing. Set an explicit bound, for example timeout-minutes: 30, to fail fast and free the runner sooner.

🧰 Tools
🪛 zizmor (1.28.0)

[info] 38-38: workflow or action definition without a name (anonymous-definition): this job

(anonymous-definition)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/android.yml around lines 38 - 43, Add a job-level
timeout-minutes setting to the Android workflow’s build job, using a 30-minute
limit. Keep the existing runs-on, environment, and steps unchanged.

Comment thread .github/workflows/android.yml
Comment thread CHANGELOG.md
doublegate and others added 8 commits July 31, 2026 20:45
android/ had Gradle sources and a cargo ndk layout but no workflow at
all, so nothing built it and nothing could regress visibly.

The new workflow cross-builds rustysnes-android for arm64-v8a,
armeabi-v7a, x86_64 and x86; asserts every 64-bit .so's PT_LOAD segments
are 16 KB aligned; assembles a debug APK; and checks the APK actually
carries each ABI.

The alignment gate is the store-facing one. Play requires 16 KB-aligned
segments for 16 KB-page devices and a 4 KB-aligned library simply fails
to load there. The NDK has defaulted to 16 KB since r27, so this is a
REGRESSION gate against an NDK downgrade or a stray linker flag -- the
kind of thing otherwise found by a store rejection. It fails closed when
the .so glob matches nothing, so a build producing no libraries cannot
pass it silently.

Two actions only, both already pinned in the committed tree: the NDK
comes from the runner image's own sdkmanager, and the JDK and Gradle are
preinstalled. Adding setup-ndk/setup-java/setup-gradle would have put
three more third-party actions on the surface the v1.26.0 hardening pass
just tightened.

assembleDebug, not release: signing material is maintainer-only and
stays out of CI, per docs/mobile-readiness.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The project had no wrapper, so every build -- CI or local -- used
whatever Gradle happened to be installed, and this environment only ever
had a cached 8.10 distribution. That is the drift a wrapper removes.

gradlew, gradlew.bat and gradle/wrapper/ (jar + properties) are the
official artefacts of the 8.10 distribution, generated by 'gradle wrapper
--gradle-version 8.10 --distribution-type bin'. The workflow now runs
./gradlew rather than the runner's preinstalled copy, so CI and a
developer's machine build with the same Gradle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
First CI run failed with 'sdkmanager: command not found'. ubuntu-latest
sets $ANDROID_HOME but does not put the SDK's cmdline-tools on PATH.

Uses the full path, asserts $ANDROID_HOME is set, and fails with a
directory listing if the binary is not where expected -- so the next
runner-image change reports what it found rather than exit 127.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The alignment gate failed on its first run with 4 KB-aligned arm64-v8a
and x86_64 libraries -- what Play rejects. The gate was not broken; the
libraries were really misaligned.

Its own comment claimed 16 KB was the NDK default from r27 onward, which
made the check look like the faulty part. Bisected locally instead: with
the same NDK r27c, cargo-ndk 3.5.4 emits 0x1000 and 4.1.2 emits 0x4000.
The linker invocation decides the alignment, not the NDK version, and
the workflow's `--version ^3` pin resolved to 3.5.4.

Pass -C link-arg=-Wl,-z,max-page-size=16384 explicitly for the 64-bit
ABIs, verified to produce 0x4000 under the failing 3.5.4, so the result
does not depend on a tool default a version range cannot promise. The
32-bit ABIs move to their own step and stay 4 KB by design -- listing
them together would relink the 64-bit ones without the flag.

Both comments that asserted the NDK default are corrected to say what
was measured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The app could not be built from a clean checkout. AGP fails
:app:checkDebugAarMetadata outright with "contains AndroidX
dependencies, but the `android.useAndroidX` property is not enabled",
and android/gradle.properties had simply never been committed.

Every Android build this project has done -- including the real APK
installed on an AVD in v1.15.0 -- ran on a machine that already had
those settings in a user-level ~/.gradle/gradle.properties, so nothing
surfaced the gap until CI built from a bare checkout.

enableJetifier is explicitly off: it rewrites legacy support-library
artifacts and there are none here, so leaving it on would cost build
time on every dependency for no effect. The Kotlin daemon heap is
raised because Compose otherwise OOMs on a runner rather than failing
legibly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The APK carries three libraries per ABI -- librustysnes_android.so,
librustysnes_mobile.so and librustysnes_monetization.so -- all built by
Gradle's own cargoNdkBuild task. The workflow's standalone pre-build
produced only rustysnes-android, and Gradle's task did not inherit the
max-page-size flag, so the alignment gate could pass on artifacts that
never ship while the ones that do were 4 KB aligned.

Found by CodeRabbit on this PR; worse than reported, since the effect
is a false pass on the store-facing requirement rather than only
missing coverage.

Build all three crates in both pre-build steps, set RUSTFLAGS on the
Gradle step, and add a second gate over the assembled APK's lib/ --
pinned at exactly six 64-bit libraries so a packaging change that drops
one fails instead of passing more quietly.

Also add Cargo.lock and crates/rustysnes-monetization/** to the path
filters, so a lockfile-only dependency bump still runs this workflow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The APK-level gate failed on `libjnidispatch.so`, and the gate was
wrong, not the library. It compared each PT_LOAD Align to 0x4000 for
equality, but the requirement is "aligned to at least 16 KB" -- a
divisibility property. JNA ships that library at 0x10000 (64 KB), which
is valid and stricter.

Measured before acting rather than bumping the dependency on a guess:
the JNA AARs for 5.15.0/5.16.0/5.17.0/5.19.1 are 0x10000, 0x4000,
0x4000, 0x4000 -- every one of them fine. A version bump would have
"fixed" nothing.

The test is now `align % 16384 == 0`, checked against real files to
accept 64 KB and 16 KB and reject 4 KB.

Also replace the hardcoded "expect exactly 6 libraries" assertion,
which was simply wrong -- the APK carries 10, two of them third-party
-- with a by-name presence check for the three this project builds. A
count would need revising whenever a dependency adds a native library.
Every library in the APK is alignment-checked, not only ours, because
Play's requirement is a property of the shipped package.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JNA 5.15.0 ships libjnidispatch.so 4 KB aligned on x86_64, armeabi-v7a
and x86, and 64 KB aligned only on arm64-v8a. The APK therefore carried
libraries that violate Play's 16 KB page-size requirement.

This corrects the previous commit's message, which claimed a version
bump would fix nothing. That was measured on arm64-v8a alone and
generalised -- the one ABI where 5.15.0 happens to be fine. Reading all
four ABIs of the published AARs:

  5.15.0   arm64 0x10000  x86_64 0x1000  armv7 0x1000  x86 0x1000
  5.16.0+  arm64 0x4000   x86_64 0x4000  armv7 0x4000  x86 0x4000

5.16.0 is the first release that fixes it; 5.17.0 is the nearest
settled patch line after that change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR adds a GitHub Actions workflow for building Android JNI libraries across four ABIs (arm64-v8a, x86_64, armeabi-v7a, x86), gates 64-bit targets on 16 KB ELF page alignment, updates JNA to 5.17.0, and commits the missing Gradle wrapper and project properties.

Blocking issues

None found.

Suggestions

  • .github/workflows/android.yml:L99-L189: The workflow compiles all Rust crates twice in --release mode (first via standalone cargo ndk to populate jniLibs, and again inside ./gradlew assembleDebug via Gradle's cargoNdkBuild task). Compiling release artifacts twice without shared target caching inflates CI execution time. Consider leveraging Cargo cache actions or triggering cargoNdkBuild only once if Gradle already manages jniLibs.
  • .github/workflows/android.yml:L85: cargo install cargo-ndk --locked --version ^3 pins the tool to the legacy 3.x release line. Although RUSTFLAGS explicitly enforces -Wl,-z,max-page-size=16384, updating the version constraint to support cargo-ndk v4+ will ensure compatibility with future NDK and Cargo releases.

Nitpicks

  • .github/workflows/android.yml:L133-L138 and .github/workflows/android.yml:L219-L224: The aligned16k shell helper function is defined identically in two adjacent step blocks. Any future change to the readelf output filter must be manually synchronized between both locations.

Automated first-pass review by agy on a self-hosted runner -- not a human review.

@doublegate
doublegate merged commit 10841d8 into main Aug 1, 2026
15 checks passed
@doublegate
doublegate deleted the feat/android-ci branch August 1, 2026 01:09
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.

2 participants