Background
greenlight playscan --apk / --aab checks Google Play's 64-bit requirement: an app that ships native code for a 32-bit ABI must also ship the matching 64-bit ABI.
The check currently works at ABI-directory granularity. It collects the set of ABIs present in the archive and flags armeabi-v7a with no arm64-v8a, or x86 with no x86_64.
internal/playscan/archive.go:157 (check64BitParity):
present := make(map[string]bool)
for _, lib := range libs {
present[lib.ABI] = true
}
for abi32, abi64 := range abi64For {
if present[abi32] && !present[abi64] {
// flag
}
}
The gap
A partial gap passes clean. This archive is reported as fine:
lib/armeabi-v7a/libfoo.so
lib/armeabi-v7a/libbar.so
lib/arm64-v8a/libfoo.so <- libbar.so has no 64-bit build
arm64-v8a exists, so the ABI-level check is satisfied. But on a 64-bit-only device System.loadLibrary("bar") throws UnsatisfiedLinkError at runtime. This is a common real-world shape: one native dependency in the tree gets built for fewer ABIs than the rest.
What to change
Compare the set of library basenames in each 32-bit ABI against its 64-bit counterpart, and report the names that are missing.
Suggested behaviour:
- Whole ABI missing: keep the current CRITICAL. Play rejects this at upload.
- Some libraries missing: report at HIGH, and name them. Play's automated rejection is less certain here, but it is a runtime crash on 64-bit-only devices.
- List the specific missing basenames in
Detail so the fix is obvious. Sort them, since map iteration order is random and findings should be deterministic.
Everything you need is local to check64BitParity. nativeLib already carries Path and ABI, and path.Base gives the basename.
Acceptance criteria
armeabi-v7a/{libfoo,libbar}.so + arm64-v8a/libfoo.so produces a HIGH finding naming libbar.so.
armeabi-v7a/libfoo.so + arm64-v8a/libfoo.so stays clean.
armeabi-v7a with no arm64-v8a at all stays CRITICAL.
- 64-bit-only, and no native code at all, both stay clean.
- Config splits stay exempt.
ScanArchive already skips this rule when the manifest sets android:split, since a split legitimately carries one ABI. Do not remove that guard.
- Findings are deterministic across runs.
Testing
Tests live in internal/playscan/archive_test.go. TestScanArchive64BitParity at line 854 is the existing table and is the natural place to add cases. buildTestAPK (line 311) builds an APK fixture from a map of entry path to bytes, and buildTestELF(align, relro) produces a valid ELF, so no real APK or Android SDK is needed:
apk := buildTestAPK(t, map[string][]byte{
"lib/armeabi-v7a/libfoo.so": buildTestELF(16384, true),
"lib/armeabi-v7a/libbar.so": buildTestELF(16384, true),
"lib/arm64-v8a/libfoo.so": buildTestELF(16384, true),
})
Run with:
go test ./internal/playscan/
gofmt -l internal/ # CI gates on this
Notes for a first-time contributor
This is self-contained: one function, one test file, no new dependencies, and the fixture helpers already exist. internal/playscan/archive.go is worth skimming end to end first, it is short. Comments in this codebase explain why rather than restating the code, so a line on why partial gaps are HIGH rather than CRITICAL is worth more than a line describing the loop.
Happy to answer questions in this issue.
Background
greenlight playscan --apk/--aabchecks Google Play's 64-bit requirement: an app that ships native code for a 32-bit ABI must also ship the matching 64-bit ABI.The check currently works at ABI-directory granularity. It collects the set of ABIs present in the archive and flags
armeabi-v7awith noarm64-v8a, orx86with nox86_64.internal/playscan/archive.go:157(check64BitParity):The gap
A partial gap passes clean. This archive is reported as fine:
arm64-v8aexists, so the ABI-level check is satisfied. But on a 64-bit-only deviceSystem.loadLibrary("bar")throwsUnsatisfiedLinkErrorat runtime. This is a common real-world shape: one native dependency in the tree gets built for fewer ABIs than the rest.What to change
Compare the set of library basenames in each 32-bit ABI against its 64-bit counterpart, and report the names that are missing.
Suggested behaviour:
Detailso the fix is obvious. Sort them, since map iteration order is random and findings should be deterministic.Everything you need is local to
check64BitParity.nativeLibalready carriesPathandABI, andpath.Basegives the basename.Acceptance criteria
armeabi-v7a/{libfoo,libbar}.so+arm64-v8a/libfoo.soproduces a HIGH finding naminglibbar.so.armeabi-v7a/libfoo.so+arm64-v8a/libfoo.sostays clean.armeabi-v7awith noarm64-v8aat all stays CRITICAL.ScanArchivealready skips this rule when the manifest setsandroid:split, since a split legitimately carries one ABI. Do not remove that guard.Testing
Tests live in
internal/playscan/archive_test.go.TestScanArchive64BitParityat line 854 is the existing table and is the natural place to add cases.buildTestAPK(line 311) builds an APK fixture from a map of entry path to bytes, andbuildTestELF(align, relro)produces a valid ELF, so no real APK or Android SDK is needed:Run with:
Notes for a first-time contributor
This is self-contained: one function, one test file, no new dependencies, and the fixture helpers already exist.
internal/playscan/archive.gois worth skimming end to end first, it is short. Comments in this codebase explain why rather than restating the code, so a line on why partial gaps are HIGH rather than CRITICAL is worth more than a line describing the loop.Happy to answer questions in this issue.