Add check-flavor.py: verify an artifact's flavour without installing it
Reads the Dart AOT snapshot out of an .apk or a Linux/Windows bundle .zip and asserts exactly one AMBER_FLAVOR:: marker matching the expected variant. See the amber-app commit for why nothing cheaper distinguishes the two builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
1d50e5a680
commit
4bf6523ab3
1 changed files with 80 additions and 0 deletions
80
scripts/check-flavor.py
Executable file
80
scripts/check-flavor.py
Executable file
|
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Verify a built artifact's flavour without installing or launching it.
|
||||
|
||||
check-flavor.py <artifact> <expected: adult|clean>
|
||||
|
||||
Exits non-zero unless the artifact contains exactly one `AMBER_FLAVOR::` marker
|
||||
and it matches. Handles an `.apk`, a Linux bundle `.zip` and a Windows bundle
|
||||
`.zip` — it finds the Dart AOT snapshot inside each.
|
||||
|
||||
**Why this exists.** The clean flavour is what anonymous downloads and every
|
||||
child profile receive, so publishing an adult build into a clean slot is the
|
||||
worst mistake this release process can make — and until now the only way to tell
|
||||
the two apart offline was to install the APK on a television and look at the tab
|
||||
row. That made a content-safety gate depend on a TV being awake and on adb still
|
||||
being authorised, which is exactly how it gets skipped "just this once".
|
||||
|
||||
Nothing cheaper worked. Class names such as `AdultBrowse` survive in **both**
|
||||
snapshots whatever the dart-define, and the `'adult'`/`'clean'` strings the
|
||||
updater compares sit inside a function body, so both literals ship in both
|
||||
builds. The two APKs are also routinely **byte-identical in size** (zip
|
||||
alignment absorbs the difference), so size proves nothing either. `config.dart`
|
||||
therefore compiles in a deliberate const-folded marker; see [kFlavorMarker].
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
MARKER = re.compile(rb'AMBER_FLAVOR::(adult|clean)')
|
||||
|
||||
# Where the Dart AOT snapshot lives, per artifact kind.
|
||||
SNAPSHOTS = (
|
||||
'lib/arm64-v8a/libapp.so', # apk
|
||||
'lib/armeabi-v7a/libapp.so', # apk, 32-bit
|
||||
'lib/libapp.so', # linux bundle zip
|
||||
'data/app.so', # windows bundle zip
|
||||
)
|
||||
|
||||
|
||||
def markers_in(blob: bytes) -> set:
|
||||
return {m.group(1).decode() for m in MARKER.finditer(blob)}
|
||||
|
||||
|
||||
def main(path: str, expected: str) -> int:
|
||||
if expected not in ('adult', 'clean'):
|
||||
print(f'error: expected must be adult|clean, got {expected!r}')
|
||||
return 2
|
||||
|
||||
found = set()
|
||||
checked = []
|
||||
with zipfile.ZipFile(path) as z:
|
||||
names = set(z.namelist())
|
||||
for candidate in SNAPSHOTS:
|
||||
if candidate in names:
|
||||
checked.append(candidate)
|
||||
found |= markers_in(z.read(candidate))
|
||||
|
||||
if not checked:
|
||||
# Better to fail loudly than to pass an artifact nothing was read from.
|
||||
print(f'FAIL {path}: no Dart snapshot found (looked for {SNAPSHOTS})')
|
||||
return 1
|
||||
if not found:
|
||||
print(f'FAIL {path}: no AMBER_FLAVOR marker in {checked} — '
|
||||
'built before the marker existed?')
|
||||
return 1
|
||||
if len(found) > 1:
|
||||
print(f'FAIL {path}: ambiguous, contains {sorted(found)}')
|
||||
return 1
|
||||
|
||||
actual = found.pop()
|
||||
if actual != expected:
|
||||
print(f'FAIL {path}: is {actual!r}, expected {expected!r}')
|
||||
return 1
|
||||
print(f'ok {path}: {actual} (from {", ".join(checked)})')
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 3:
|
||||
raise SystemExit(__doc__)
|
||||
raise SystemExit(main(sys.argv[1], sys.argv[2]))
|
||||
Loading…
Reference in a new issue