Fix GMS gate APK mode false positive
Some checks are pending
data_transfer_flutter tests / test (push) Waiting to run
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
sync_settings_ui tests / test (push) Waiting to run

Decide by AGP library marker files instead of dex strings:
play-services-base/basement reference GoogleSignInAccount without
linking play-services-auth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWVQE5pYiASpRGgA3Kg
This commit is contained in:
Krzysztof Rudnicki 2026-11-10 18:00:16 +01:00
parent eef7af4c86
commit ae56037e41
2 changed files with 75 additions and 25 deletions

View File

@ -14,8 +14,8 @@
# (google_sign_in_web, google_sign_in_platform_interface and their transitive
# google_identity_services_web) are allowed while google_sign_in_android is
# absent: kartka's web build uses them for the GIS button and they never
# reach an APK. APK mode greps the built dex for the GMS auth classes, the
# ground truth when a dependency arrives some other way.
# reach an APK. APK mode looks for the GMS auth library marker files in the
# built APK, the ground truth when a dependency arrives some other way.
#
# Usage: check_no_gms_signin.sh [pubspec.lock | dir ...]
# check_no_gms_signin.sh --apk app.apk
@ -35,7 +35,19 @@ readonly WEB_ONLY=(google_sign_in_web google_sign_in_platform_interface
# Anchored so com.google.gms:google-services (Firebase) and
# play-services-auth-api-phone do not trip it.
readonly GRADLE_PATTERN='com\.google\.android\.gms:play-services-(auth|identity-credentials)([^-[:alnum:]]|$)|com\.google\.android\.libraries\.identity\.googleid|androidx\.credentials:credentials-play-services-auth|name[[:space:]]*=[[:space:]]*"(play-services-auth|play-services-identity-credentials|credentials-play-services-auth|googleid)"'
readonly DEX_PATTERN='Lcom/google/android/(gms/auth|libraries/identity/googleid)/'
# APK mode: library marker files AGP copies to the APK root (or META-INF),
# one per linked artifact. Dex strings are not used: play-services-base and
# -basement reference GoogleSignInAccount without linking play-services-auth.
# play-services-fido (passkeys) is deliberately allowed.
readonly APK_MARKERS=(
googleid.properties
play-services-auth.properties
play-services-auth-base.properties
play-services-auth-api-phone.properties
play-services-auth-blockstore.properties
play-services-identity-credentials.properties
META-INF/androidx.credentials_credentials-play-services-auth.version
)
readonly PRUNE=(-name build -o -name .dart_tool -o -name .git -o -name .gradle -o -name .claude
-o -name node_modules)
@ -122,17 +134,18 @@ check_lock() {
}
check_apk() {
local apk="$1" dex count
local apk="$1" entries found=()
local marker
[[ -f "$apk" ]] || { echo "Error: no such APK: $apk" >&2; exit 2; }
count="$(for dex in $(unzip -Z1 "$apk" 'classes*.dex'); do
unzip -p "$apk" "$dex"
done | strings | grep -cE "$DEX_PATTERN" || true)"
if [[ "$count" -gt 0 ]]; then
echo "Error: $apk has $count GMS sign-in class references" >&2
echo " (Lcom/google/android/gms/auth or .../identity/googleid)." >&2
fix_hint
return 1
fi
entries="$(unzip -Z1 "$apk")"
for marker in "${APK_MARKERS[@]}"; do
grep -qxF -- "$marker" <<<"$entries" && found+=("$marker")
done
[[ "${#found[@]}" -eq 0 ]] && return 0
echo "Error: $apk bundles Google's non-free sign-in libraries:" >&2
printf ' %s\n' "${found[@]}" >&2
fix_hint
return 1
}
# Expands the arguments into pubspec.lock paths, NUL-separated.

View File

@ -136,23 +136,60 @@ def test_missing_explicit_file_is_a_usage_error(tmp_path: Path) -> None:
assert _run(tmp_path / "nope.lock").returncode == 2
def _apk(path: Path, dex: bytes) -> Path:
CREDENTIALS_MARKER = "META-INF/androidx.credentials_credentials-play-services-auth.version"
DEX_REF = b"\x00Lcom/google/android/gms/auth/api/signin/GoogleSignInAccount;\x00" * 2
def _apk(path: Path, dex: bytes = b"", markers: tuple[str, ...] = ()) -> Path:
"""Fake APK: dex strings plus library marker files AGP copies in."""
with zipfile.ZipFile(path, "w") as apk:
apk.writestr("AndroidManifest.xml", b"\x03\x00")
apk.writestr("classes.dex", b"dex\n035\x00" + dex)
apk.writestr("classes2.dex", b"dex\n035\x00Lkotlin/Unit;\x00")
for name in markers:
apk.writestr(name, b"version=1")
return path
def test_apk_with_gms_auth_classes_fails(tmp_path: Path) -> None:
dex = b"\x00Lcom/google/android/gms/auth/api/signin/GoogleSignIn;\x00" * 2
proc = _run("--apk", _apk(tmp_path / "bad.apk", dex))
assert proc.returncode == 1
assert "2" in proc.stderr
assert "google_account_token" in proc.stderr
def test_apk_without_gms_auth_classes_passes(tmp_path: Path) -> None:
dex = b"\x00Lcom/google/android/gms/common/api/Status;\x00"
proc = _run("--apk", _apk(tmp_path / "good.apk", dex))
def test_apk_with_only_dex_references_passes(tmp_path: Path) -> None:
"""play-services-base references GoogleSignInAccount without linking auth."""
markers = (
"play-services-base.properties",
"play-services-basement.properties",
"play-services-tasks.properties",
"play-services-fido.properties",
)
proc = _run("--apk", _apk(tmp_path / "ok.apk", DEX_REF, markers))
assert proc.returncode == 0, proc.stderr
def test_apk_without_markers_passes(tmp_path: Path) -> None:
assert _run("--apk", _apk(tmp_path / "clean.apk")).returncode == 0
def test_apk_banned_markers_fail_and_are_named(tmp_path: Path) -> None:
banned = (
"play-services-auth.properties",
"googleid.properties",
"play-services-auth-base.properties",
"play-services-auth-api-phone.properties",
"play-services-auth-blockstore.properties",
"play-services-identity-credentials.properties",
CREDENTIALS_MARKER,
)
for marker in banned:
apk = _apk(tmp_path / "bad.apk", DEX_REF, ("play-services-base.properties", marker))
proc = _run("--apk", apk)
assert proc.returncode == 1, marker
assert marker in proc.stderr
assert "google_account_token" in proc.stderr
def test_apk_nested_marker_name_is_not_a_root_marker(tmp_path: Path) -> None:
"""Only APK-root files count; a same-named file in a subdir is not AGP's."""
apk = _apk(tmp_path / "nested.apk", markers=("assets/play-services-auth.properties",))
assert _run("--apk", apk).returncode == 0
def test_apk_missing_file_is_a_usage_error(tmp_path: Path) -> None:
assert _run("--apk", tmp_path / "nope.apk").returncode == 2