You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 3d6eeb7
Browse filesBrowse the repository at this point in the historyBrowse files
feat(mobile): add the App Store 4.7 self-audit, a release build path, and a UniFFI smoke test (#332)
* feat(mobile): App Store 4.7 self-audit, release build path, UniFFI smoke test
The v1.30.0 rung. An audit first found that four entries in
docs/mobile-readiness.md's "not yet verified" list had gone stale -- android.yml,
the ./gradlew wrapper, the iOS simulator RUN, and much of the CI the plan asked
for all exist. They are now marked DONE in place rather than deleted: a
readiness document that silently drops items cannot be audited backwards.
Three real gaps closed.
The App Store 4.7 self-audit (docs/app-store-4-7-self-audit.md) is the one
store-facing item that is not maintainer-blocked, and it had never been done. It
passes on all five criteria, and the strongest evidence is capability rather
than intent: Android declares NO permissions at all, not even INTERNET, and iOS
has no networking code, so neither shell CAN obtain game software. Every
user-visible string in both shells was enumerated -- the complete set is
RustySNES, Open ROM, Save State, Load State. Two re-audit triggers are recorded:
the peripheral UI when it lands, since Super Scope / Mouse / Multitap names are
a fresh trademark decision the audit does not pre-approve, and
rustysnes-monetization if it is ever activated.
An unsigned assembleRelease path with its own 16 KB gate on the release APK.
Signing material is the maintainer's to provision, so a signed release build
stays out of reach; an unsigned one still runs R8, resource shrinking and the
release manifest merge, which is where release-only breakage lives.
An instrumented UniFFI smoke test, in its own CI job. assembleDebug already
proves the bindings COMPILE, because MainActivity calls MobileCore directly.
What no build can prove is that System.loadLibrary finds the .so for the
device's ABI, that JNA's mapping matches its symbols, and that a call marshals
across and returns -- this project has already shipped one native Android crash
a build could not have caught. Separate job, because an emulator is the flakiest
thing in that workflow and a flaky step inside `build` would put the 16 KB
gates behind an AVD boot.
Mobile Phase 6 stays NOT GREENLIT. Passing the audit removes a prerequisite from
that gate's checklist; it does not move the gate. Distribution signing,
TestFlight and Play's Data Safety form remain maintainer-blocked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ci): the smoke job needs arm64 too — cargoNdkBuild builds every ABI
The job failed on its first run with `can't find crate for core: the
aarch64-linux-android target may not be installed`.
The reasoning in the comment was wrong, not just the target list. "x86_64 only,
the emulator runs the host ABI" confuses what the emulator RUNS with what the
build COMPILES: Gradle's cargoNdkBuild builds every ABI in build.gradle.kts's
`cargoAbis` map -- arm64-v8a AND x86_64 -- before the instrumented test can
install anything, so both Rust targets have to be present regardless of which
one the device is.
Also salvages this project's agent scratch out of /tmp ahead of a reboot: 40
one-off probe scripts and 5 documents into `salvaged/`, which is gitignored, so
the only tracked change is docs/SALVAGE_MANIFEST.md. Dropped ROM-derived
framebuffer captures, regenerable AccuracySNES scene captures, ~1.4 MB of
CHANGELOG working copies, release notes already in CHANGELOG.md, and ~45 PR
bodies and bot-review adjudications for merged PRs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(mobile): resolve the review findings, including a vacuous smoke assertion
Four findings, all real.
CRITICAL -- the release-APK alignment gate could pass on an APK that would fail
on Play. I re-implemented the check instead of reusing the logic the debug gate
already settled, and got all three of its properties wrong:
1. exact string equality (`0x4000|0x10000`) rejects other valid alignments --
the requirement is "at least 16 KB", so it is DIVISIBILITY. The debug
gate's own comment records this exact mistake: an equality test wrongly
flagged JNA's 64 KB-aligned libjnidispatch.so.
2. `awk ... exit` read only the FIRST LOAD segment.
3. no `checked == 0` guard, so a missing ABI directory makes the glob match
nothing and the gate PASSES on an APK with no libraries in it.
The gate now uses the same aligned16k() helper and the same guard as the debug
one. Repeating a bug the file already documents having fixed is the argument for
reusing settled logic rather than writing it again.
VACUOUS TEST -- drain_audio_is_non_destructive_within_a_frame asserted that two
successive drainAudio() calls return the same count, claiming to pin the
documented non-destructive contract. Measured on the host: a no-ROM frame
produces NO audio, `first=0 second=0`, so `0 == 0` passed and proved nothing --
it would have gone on passing had the contract inverted. Replaced with an
assertion the bridge can actually support without a ROM (interleaved stereo, so
an even length); the real contract stays covered host-side by
drain_audio_returns_interleaved_stereo_samples, which loads a ROM first.
Also: the §4.7 audit's monetization grep lacked -E, so its `|` were literal and
it would have reported "nothing" for a reason unrelated to the code -- an audit
whose command cannot be re-run is an assertion, not an audit. And the emulator
step is now bounded by timeout-minutes, since splitting the job contained the
blast radius of a flaky AVD without bounding its runtime.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(mobile): boot a real cart in the smoke test, so its assertions can fail
The audio assertion took two wrong turns of the same kind, and the reviewer was
right both times. It first asserted two successive drainAudio() calls return
equal counts (claiming the non-destructive contract), then that the buffer
length is even (claiming interleaved stereo). A no-ROM frame produces NO audio,
measured on the host as first=0 second=0 -- so the first passed on `0 == 0` and
the second on `0 % 2 == 0`. Neither could fail for the reason it named.
The fix is not a better assertion, it is a cart. AccuracySNES's HiROM image is
64 KB, tracked, and dual-licensed with the repo, so unlike any commercial ROM it
can be packaged into a test APK. Gradle's `copyTestRom` places it as an
androidTest asset (gitignored -- build output, matching how jniLibs is handled)
and the test loads it, asserts it loaded, runs frames, and asserts real audio
crossed the bridge.
That also closes something docs/mobile-readiness.md records as never having
happened: no ROM had ever actually booted on a device or simulator. It has now.
Verified on the host before wiring it, because an assertion nobody has watched
pass is the same gamble as one that cannot fail: a booted cart emits 1066
interleaved samples from the FIRST frame. The eight frames are margin, and the
comment says so rather than repeating my initial guess that the APU needs the
IPL handshake to complete first -- which the measurement disproves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
0 commit comments