Skip to content

Close four paths that reported the wrong exit code, and fix red CI - #1

Merged
saimskywalker merged 2 commits into
mainfrom
fix/usability-audit
Aug 27, 2026
Merged

saimskywalker merged 2 commits into
mainfrom
fix/usability-audit

Conversation

@saimskywalker

Copy link
Copy Markdown
Owner

What this changes

Four paths returned the wrong exit code, CI has been red on main since the
first push, and the README's install command fails as written. One of the four
reached 0.

Why

I read the README as instructions and ran them literally, then walked the whole
exit-code contract from the outside. Six things came back.

1. PASS and exit 0 on an artifact where nothing was measured

lib/<abi>/ can hold files that are not libraries — wrap.sh is the obvious
one, and Android looks for it there. When the only 64-bit ABI directory held no
.so, the loop set found64=1, the inner glob matched nothing, and the run
fell through to PASS. The ABI was not named anywhere in the report, so the
output looked like a clean 32-bit-plus-64-bit app.

$ ./android-16kb-check /tmp/wrapsh.apk        # lib/arm64-v8a/wrap.sh only
==> ELF LOAD segment alignment (must be >= 2**14)
    armeabi-v7a    [SKIP] 32-bit, exempt (1 libs)

PASS — archive alignment and every 64-bit library meet the 16 KB page requirement.
--> exit 0

Same artifact, after:

==> ELF LOAD segment alignment (must be >= 2**14)
    arm64-v8a      [none] directory present but holds no .so
    armeabi-v7a    [SKIP] 32-bit, exempt (1 libs)

android-16kb-check: CANNOT VERIFY — no 64-bit library was read, so nothing was measured.
--> exit 2

found64 counted directories; there is now a second counter for libraries
actually read, and [none] is printed rather than the ABI being omitted — same
reasoning as the existing [SKIP] line.

2. objdump failing to run was reported as an alignment failure

A non-zero exit from objdump produced [FAIL] no LOAD segments readable under
Play will reject this upload — a measurement announced about a file the tool
never opened. Reproduced with the real NDK r28 llvm-objdump against an
lib/arm64-v8a/*.so that is not an ELF (objdump exits 1):

before: [FAIL] no LOAD segments readable / FAIL — ... Play will reject   exit 1
after:  CANNOT VERIFY — llvm-objdump could not read arm64-v8a/libgarbage.so   exit 2

The status is now read on its own, before parsing. A library where objdump
succeeds and prints nothing usable is still [FAIL] — that is the
distinction CONTRIBUTING already draws, and t_unreadable_elf_fails_with_one
still asserts 1. Worth knowing: real llvm-objdump exits 0 on a
truncated-but-ELF-magic file and 1 when it cannot open the file at all, so
the two cases really are distinguishable in practice.

3. --ndk "" silently became "no --ndk given"

--ndk "$UNSET_VAR" in a caller's script fell through to auto-detection after
being handed an explicit NDK, and could then PASS using a toolchain nobody
named. The --ndk= form already errored, so the two spellings disagreed. Both
now reject an empty value. Same for --bundletool.

4. Auto-detection chose the wrong NDK

highest_ndk sorted whole paths with sort -t. -k1,1n -k2,2n -k3,3n. That
puts /…/ndk/28 in field 1, where a numeric compare reads it as 0 on both
BSD and GNU sort — so the major version was never compared and only the minor
decided. With 26.3.x, 27.0.x and 28.2.x installed:

highest_ndk picks: /…/ndk/26.3.11579264

Not a BSD-vs-GNU split; it is wrong the same way on both. sort -V would be
GNU-only, so version_gt does the compare in bash and highest_ndk no longer
shells out.

5. CI is red on main

The shellcheck step has failed on every run so far, which is why the tests
have never actually executed in CI. test/run.sh disables SC2329 for its
indirectly-invoked test bodies, but ubuntu-latest still installs shellcheck
0.9.0, which reports that finding as SC2317 — the split into SC2329
landed in 0.10. So it is clean on a current local shellcheck (0.11) and red on
CI. Both codes are disabled now, and CONTRIBUTING has a note about the version
gap so the next person does not rediscover it.

6. The install command in the README fails as written

/usr/local/bin is root-owned on macOS and on a stock Ubuntu:

$ curl -fsSLo /usr/local/bin/android-16kb-check https://raw.githubusercontent.com/...
curl: (56) Failure writing output to destination, passed 823 returned 4294967295   # macOS
curl: (23) Failure writing output to destination                                   # ubuntu:24.04

Now has sudo, plus a ~/.local/bin form for people who would rather not. Both
verified verbatim as a non-root user on Ubuntu and on macOS.

Checklist

  • shellcheck android-16kb-check test/run.sh test/stubs/* is clean
  • test/run.sh passes
  • If this fixes a bug: there is a test that fails without the fix
  • If this changes what an exit code means: CHANGELOG.md says so, and the
    README table matches

Verified on macOS (shellcheck 0.11.0) and in ubuntu:24.04 with the 0.9.0 that
CI installs — clean lint and 40 passed, 0 failed on both. Six new tests, each
confirmed failing against the unfixed script:

FAIL  a 64-bit dir with no .so exits 2, not 0     expected exit 2, got 0
FAIL  a 64-bit dir with no .so is reported        expected output to contain: arm64-v8a
FAIL  an objdump that cannot run exits 2, not 1   expected exit 2, got 1
FAIL  an empty --ndk value is rejected            expected exit 2, got 0
FAIL  an empty --bundletool value is rejected     expected exit 2, got 0
FAIL  the highest installed NDK is chosen         expected output to contain: ndk/28.2.13676358/

If this touches the exit codes

It does, in three places, all in the same direction — away from 0 and away
from a claimed measurement:

case before after
64-bit ABI directory with no .so 0 2
objdump exits non-zero on a library 1 2
--ndk "" / --bundletool "" whatever auto-detection found, incl. 0 2

No new path can reach 0. One path that could is removed. What 0, 1
and 2 mean is unchanged — these were paths landing on the wrong one, so
this is a fix rather than a redefinition, and it sits under [Unreleased]
rather than taking a major. VERSION is left at 1.0.0 for you to bump when
you cut a release.

What you ran it against

A real APK: libc++_shared.so for arm64-v8a, x86_64 and armeabi-v7a
taken from NDK 28.2.13676358, packed with zip -X -0 and aligned with
zipalign -P 16 4. Real detection (no ANDROID_HOME set — it walked
~/Library/Android/sdk), real llvm-objdump, real zipalign 36.1.0:

==> artifact : /tmp/real-aligned.apk
==> objdump  : …/ndk/28.2.13676358/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-objdump
==> archive  : …/build-tools/36.1.0/zipalign (zipalign)

==> Archive alignment
    [ok]   zipalign -c -P 16 4: verification successful

==> ELF LOAD segment alignment (must be >= 2**14)
    arm64-v8a      libc++_shared.so                       [ok]   min align 2**14
    armeabi-v7a    [SKIP] 32-bit, exempt (1 libs)
    x86_64         libc++_shared.so                       [ok]   min align 2**14

PASS — archive alignment and every 64-bit library meet the 16 KB page requirement.
--> exit 0

The same libraries packed without -P 16 give exit 1 at the archive stage,
and --ndk /nonexistent gives exit 2, so all three answers were seen against
a real toolchain.

Two things I found and did not change

  • AAB feature modules are never checked. LIB_GLOB is base/lib/*, so an
    unaligned .so in a dynamic feature module is invisible and the run can
    PASS. Play generates APKs from those modules too. Filed separately rather
    than folded in here, since it changes what "the artifact" means.
  • The [1.0.0] and [Unreleased] links in CHANGELOG.md 404 — there is no
    v1.0.0 tag or release yet. Nothing to change in the file; it resolves when
    you tag.

An outside-in pass over the repo, following the README literally and then
exercising the whole exit-code contract. Six things came back.

The important one first: a 64-bit ABI directory holding no .so printed PASS
and exited 0. lib/<abi>/ can legitimately hold files that are not libraries —
wrap.sh, for one — and such a run measured nothing at all while returning a
green result, without naming the ABI anywhere in the report. That is exactly
the failure the tool exists to refuse. It exits 2 now, and an ABI directory
with no library in it is reported as [none] rather than omitted.

objdump exiting non-zero was reported as [FAIL] no LOAD segments readable,
under "Play will reject this upload" — a measurement announced about a file
the tool never managed to open. That case is 2. A library where objdump
SUCCEEDS and prints nothing usable is still [FAIL], which is the distinction
CONTRIBUTING already draws.

--ndk "" and --bundletool "" were treated as if the flag had not been passed,
so --ndk "$UNSET_VAR" fell through to auto-detection after being handed an
explicit NDK — the silent fallback --ndk exists to prevent. The --ndk= form
already errored; the two forms agree now.

Auto-detection picked the wrong NDK. The versions were sorted as whole paths
on '.', which puts the directory prefix in field 1, where a numeric compare
reads it as 0 on both BSD and GNU sort — so the major version was never
compared. With 26.3.x, 27.0.x and 28.2.x installed it chose 26.3.x. There is
no portable sort spelling of this (sort -V is GNU-only), so version_gt does
it in bash.

CI on main is red, and has been since the first push. test/run.sh disables
SC2329 for its indirectly-invoked test bodies, but ubuntu-latest still ships
shellcheck 0.9, which reports that finding as SC2317. Clean on a current local
shellcheck, red on CI. Both codes are disabled now, and CONTRIBUTING notes the
version gap.

Finally, the install command in the README failed as written: it writes to
root-owned /usr/local/bin without sudo, so a non-root user gets curl: (56) on
macOS and curl: (23) on Ubuntu. It has sudo now, plus a ~/.local/bin form for
people who would rather not.

Six tests, each failing before its fix. Verified on macOS with shellcheck
0.11 and in ubuntu:24.04 with the 0.9 that CI installs: 40 passed, 0 failed
and a clean lint on both.
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 54234988-6916-45f0-b309-db26440fef07


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Two more call sites had the failure-to-measure/measured-failure conflation
this branch already fixed for objdump.

zipalign: any non-zero status was read as "not aligned". Build-tools older
than 35 has no -P flag and exits 2 on the usage error, so a genuinely
16 KB-aligned APK came back as exit 1 under "Play will reject this upload"
from a tool that had never checked it. Verified against a real
build-tools;34.0.0: it prints "illegal option -- P" and exits 2, while
36.1.0 returns 1 for a real misalignment. 1 stays exit 1; anything else is
now exit 2.

unzip: its status was discarded with `|| true`, so an archive it could only
partly extract was measured on whatever came out. An APK with two 64-bit
.so, one of them stored with a compression method unzip cannot decode,
extracted one library and reported PASS with exit 0 — reproduced identically
on macOS and on ubuntu:24.04. 0, 1 and 11 (nothing matched the glob, which
the missing-lib-root check reports properly) still pass through; anything
else is exit 2.

Two tests, both confirmed failing before the change:

  FAIL  a zipalign that cannot run exits 2, not 1   expected exit 2, got 1
  FAIL  a partly-extractable archive exits 2        expected exit 2, got 0

No new path reaches 0, and no path that reached 0 legitimately stopped:
a real aligned APK still exits 0 and a real unaligned one still exits 1
against NDK 28.2.13676358 and build-tools 36.1.0.
@saimskywalker
saimskywalker merged commit c9a1698 into main Aug 27, 2026
2 checks passed
@saimskywalker
saimskywalker deleted the fix/usability-audit branch August 27, 2026 08:21
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