Some checks failed
ZaStoGram source guards / guards (push) Failing after 24s
Build three ZaStoGram APKs / build (x86, ZaStoGram-standalone-x86, X86, x86) (push) Failing after 47s
Build three ZaStoGram APKs / build (arm64-v8a, ZaStoGram-standalone-arm64-v8a, Arm64, arm64) (push) Failing after 47s
Build three ZaStoGram APKs / build (armeabi-v7a, ZaStoGram-standalone-armeabi-v7a, Armv7, armv7) (push) Failing after 1m35s
325 lines
13 KiB
Python
325 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
GRADLE_PATH = ROOT / "TMessagesProj_AppStandalone" / "build.gradle"
|
|
LIB_GRADLE_PATH = ROOT / "TMessagesProj" / "build.gradle"
|
|
WORKFLOW_PATH = ROOT / ".forgejo" / "workflows" / "build-apk.yml"
|
|
APK_ABI_CHECK_PATH = ROOT / "Tools" / "check_apk_single_abi.py"
|
|
|
|
|
|
ABI_FLAVORS = {
|
|
"arm64": {
|
|
"gradle_task_flavor": "Arm64",
|
|
"flavor_dir": "arm64",
|
|
"abi": "arm64-v8a",
|
|
"version_code": "11",
|
|
"artifact": "ZaStoGram-standalone-arm64-v8a",
|
|
"ccache_key": "ccache-arm64-v8a-standalone",
|
|
},
|
|
"armv7": {
|
|
"gradle_task_flavor": "Armv7",
|
|
"flavor_dir": "armv7",
|
|
"abi": "armeabi-v7a",
|
|
"version_code": "12",
|
|
"artifact": "ZaStoGram-standalone-armeabi-v7a",
|
|
"ccache_key": "ccache-armeabi-v7a-standalone",
|
|
},
|
|
"x86": {
|
|
"gradle_task_flavor": "X86",
|
|
"flavor_dir": "x86",
|
|
"abi": "x86",
|
|
"version_code": "13",
|
|
"artifact": "ZaStoGram-standalone-x86",
|
|
"ccache_key": "ccache-x86-standalone",
|
|
},
|
|
"x64": {
|
|
"gradle_task_flavor": "X64",
|
|
"flavor_dir": "x64",
|
|
"abi": "x86_64",
|
|
"version_code": "14",
|
|
"artifact": "ZaStoGram-standalone-x86_64",
|
|
"ccache_key": "ccache-x86_64-standalone",
|
|
},
|
|
}
|
|
|
|
WORKFLOW_FLAVORS = ("arm64", "armv7", "x86")
|
|
|
|
|
|
def read_text(path: Path) -> str:
|
|
try:
|
|
return path.read_text(encoding="utf-8")
|
|
except FileNotFoundError:
|
|
fail(f"Missing required file: {path.relative_to(ROOT)}")
|
|
|
|
|
|
def fail(message: str) -> None:
|
|
print(f"FAIL: {message}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def find_named_block(text: str, name: str, *, start: int = 0) -> str | None:
|
|
match = re.search(rf"(?m)^[ \t]*{re.escape(name)}[ \t]*\{{", text[start:])
|
|
if not match:
|
|
return None
|
|
|
|
block_start = start + match.start()
|
|
brace_start = text.find("{", block_start)
|
|
depth = 0
|
|
for index in range(brace_start, len(text)):
|
|
char = text[index]
|
|
if char == "{":
|
|
depth += 1
|
|
elif char == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return text[block_start : index + 1]
|
|
return None
|
|
|
|
|
|
def check_gradle(gradle_text: str) -> list[str]:
|
|
errors: list[str] = []
|
|
product_flavors = find_named_block(gradle_text, "productFlavors")
|
|
if product_flavors is None:
|
|
return ["Gradle file does not define productFlavors"]
|
|
|
|
afat = find_named_block(product_flavors, "afat")
|
|
if afat is None:
|
|
errors.append("Gradle file must keep afat flavor as universal fallback")
|
|
else:
|
|
for abi in ("armeabi-v7a", "arm64-v8a", "x86", "x86_64"):
|
|
if f'"{abi}"' not in afat:
|
|
errors.append(f"afat fallback is missing ABI {abi}")
|
|
|
|
for flavor, expected in ABI_FLAVORS.items():
|
|
block = find_named_block(product_flavors, flavor)
|
|
if block is None:
|
|
errors.append(f"Missing standalone ABI flavor {flavor}")
|
|
continue
|
|
|
|
abi = expected["abi"]
|
|
if f'abiFilters "{abi}"' not in block:
|
|
errors.append(f"Flavor {flavor} must restrict ndk.abiFilters to {abi}")
|
|
|
|
version_code = expected["version_code"]
|
|
if not re.search(rf"abiVersionCode[ \t]*=[ \t]*{re.escape(version_code)}\b", block):
|
|
errors.append(f"Flavor {flavor} must use abiVersionCode = {version_code}")
|
|
|
|
if "AndroidManifest_standalone.xml" not in block:
|
|
errors.append(f"Flavor {flavor} must keep the standalone manifest")
|
|
|
|
if "output.versionCodeOverride = defaultConfig.versionCode * 100000 + zastoBuildNumber * 10 + abiVersionDigit" not in gradle_text:
|
|
errors.append("Gradle version-code derivation must include the Forgejo build number and ABI digit")
|
|
|
|
for literal in ("ZastoBuildNumber", "zastoBuildNumber", "abiVersionDigit"):
|
|
if literal not in gradle_text:
|
|
errors.append(f"Standalone Gradle file is missing Forgejo version-code contract literal: {literal}")
|
|
|
|
for literal in (
|
|
"ZASTO_RELEASE_KEYSTORE",
|
|
"ZASTO_RELEASE_STORE_PASSWORD_FILE",
|
|
"ZASTO_RELEASE_KEY_PASSWORD_FILE",
|
|
"ZASTO_RELEASE_KEY_ALIAS_FILE",
|
|
"zastoExternalSigningValues.any { it } && !zastoExternalSigningValues.every { it }",
|
|
):
|
|
if literal not in gradle_text:
|
|
errors.append(f"Standalone Gradle file is missing secure local signing contract literal: {literal}")
|
|
|
|
if "standaloneBuildFlavors = [\"afat\", \"arm64\", \"armv7\", \"x86\", \"x64\"]" not in gradle_text:
|
|
errors.append("variantFilter must keep an explicit allow-list for afat and the ABI standalone flavors")
|
|
|
|
if "!names.any { standaloneBuildFlavors.contains(it) }" not in gradle_text:
|
|
errors.append("variantFilter must allow ABI standalone flavors through names.any")
|
|
|
|
for literal in (
|
|
"zastoStandaloneAbiApks",
|
|
"zastoValidNativeAbis",
|
|
"java.util.zip.ZipFile",
|
|
"assets/chaquopy/bootstrap-native/${abi}/",
|
|
"foreign ABI payload",
|
|
"finalizedBy verifyTask",
|
|
):
|
|
if literal not in gradle_text:
|
|
errors.append(f"Standalone Gradle file is missing packaged APK ABI guard literal: {literal}")
|
|
|
|
build_types = find_named_block(gradle_text, "buildTypes")
|
|
standalone_build_type = find_named_block(build_types, "standalone") if build_types else None
|
|
if standalone_build_type is None:
|
|
errors.append("Standalone app Gradle file must define the standalone build type")
|
|
else:
|
|
if "minifyEnabled true" not in standalone_build_type:
|
|
errors.append("Standalone app build type must keep app-level R8 minify enabled for release APKs")
|
|
if "../TMessagesProj/proguard-rules.pro" not in standalone_build_type:
|
|
errors.append("Standalone app R8 must keep TMessagesProj proguard rules when library R8 is disabled")
|
|
|
|
return errors
|
|
|
|
|
|
def check_library_gradle(gradle_text: str) -> list[str]:
|
|
errors: list[str] = []
|
|
|
|
required_literals = [
|
|
"zastoAbiFilter",
|
|
"zastoDeprecatedMultiAbiFilters",
|
|
"zastoAllNativeAbis",
|
|
"zastoStandaloneTaskAbis",
|
|
"zastoTaskRequestedAbis.size() > 1",
|
|
'project.findProperty("zastoAbiFilter")',
|
|
'project.findProperty("zastoAbiFilters")',
|
|
'System.getenv("ZASTO_ABI_FILTER")',
|
|
'System.getenv("ZASTO_ABI_FILTERS")',
|
|
"zastoSingleAbiFilter != zastoTaskAbiFilter",
|
|
"abiFilters(*(zastoRequestedAbiFilter ? [zastoRequestedAbiFilter] : zastoAllNativeAbis))",
|
|
"CMAKE_C_COMPILER_LAUNCHER=ccache",
|
|
"CMAKE_CXX_COMPILER_LAUNCHER=ccache",
|
|
]
|
|
for literal in required_literals:
|
|
if literal not in gradle_text:
|
|
errors.append(f"Library Gradle file is missing ABI-cache contract literal: {literal}")
|
|
|
|
for abi in ("armeabi-v7a", "arm64-v8a", "x86", "x86_64"):
|
|
if f'"{abi}"' not in gradle_text:
|
|
errors.append(f"Library Gradle ABI allow-list is missing {abi}")
|
|
|
|
all_abi_filter = 'abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"'
|
|
if all_abi_filter in gradle_text:
|
|
errors.append("Library Gradle file must not force all native ABIs for every matrix job")
|
|
|
|
build_types = find_named_block(gradle_text, "buildTypes")
|
|
standalone_build_type = find_named_block(build_types, "standalone") if build_types else None
|
|
if standalone_build_type is None:
|
|
errors.append("Library Gradle file must define the standalone build type")
|
|
else:
|
|
if "minifyEnabled false" not in standalone_build_type:
|
|
errors.append("Library standalone build type must skip intermediate R8; the app module owns final minify")
|
|
if "minifyEnabled true" in standalone_build_type:
|
|
errors.append("Library standalone build type must not run intermediate R8 before app-level R8")
|
|
|
|
return errors
|
|
|
|
|
|
def check_workflow(workflow_text: str) -> list[str]:
|
|
errors: list[str] = []
|
|
|
|
required_literals = [
|
|
"fail-fast: false",
|
|
'":TMessagesProj_AppStandalone:assemble${{ matrix.flavor }}Standalone"',
|
|
"TMessagesProj_AppStandalone/build/outputs/apk/${{ matrix.flavor_dir }}/standalone",
|
|
"dist/${{ matrix.artifact }}.apk",
|
|
"python3 Tools/check_mtproxy_all.py",
|
|
"python3 Tools/check_plugin_client_utils_contract.py",
|
|
"python3 Tools/check_plugin_android_utils_contract.py",
|
|
"python3 Tools/check_plugin_text_formatting_contract.py",
|
|
"python3 Tools/check_plugin_structured_settings.py",
|
|
"python3 Tools/check_plugin_exteragram_ui_bridge.py",
|
|
"python3 Tools/check_plugin_java_package_shims.py",
|
|
"python3 Tools/check_plugin_python_deps.py",
|
|
"python3 Tools/check_plugin_runtime_requirements.py",
|
|
"python3 Tools/check_plugin_utils_javadoc.py",
|
|
"python3 Tools/check_android_string_format_contract.py",
|
|
"python3 Tools/check_telegram_api_identity.py",
|
|
"python3 Tools/check_logs_activity_compile_contract.py",
|
|
"python3 Tools/check_runtime_resilience.py",
|
|
"python3 Tools/check_zasto_edit_history_contract.py",
|
|
"python3 Tools/check_double_tap_reaction_toggle.py",
|
|
"python3 Tools/check_profile_avatar_blur_toggle.py",
|
|
"python3 Tools/check_forgejo_update_contract.py",
|
|
"-PzastoAbiFilter=${{ matrix.abi }}",
|
|
"ZASTO_UPDATE_CHANNEL: dev",
|
|
"ZASTO_RELEASE_TAG: forgejo-build-${{ forgejo.run_number }}-${{ forgejo.run_attempt }}",
|
|
"ZASTO_BUILD_NUMBER: ${{ forgejo.run_number }}",
|
|
"ZASTO_FORGEJO_REPOSITORY: ${{ forgejo.repository }}",
|
|
"https://data.forgejo.org/forgejo/upload-artifact@v4",
|
|
"forgejo.run_number",
|
|
"forgejo.run_attempt",
|
|
"forgejo.sha",
|
|
"path: ~/.cache/ccache",
|
|
"restore-keys:",
|
|
"CCACHE_MAXSIZE: 10G",
|
|
"ccache --show-stats",
|
|
"Restore Gradle and Python build caches",
|
|
"~/.gradle/caches",
|
|
"~/.gradle/wrapper",
|
|
"TMessagesProj/build/python",
|
|
"--no-configuration-cache",
|
|
"--parallel",
|
|
"TELEGRAM_API_ID: ${{ secrets.TELEGRAM_API_ID }}",
|
|
"TELEGRAM_API_HASH: ${{ secrets.TELEGRAM_API_HASH }}",
|
|
"ZASTO_REQUIRE_TELEGRAM_API_CREDENTIALS: '1'",
|
|
'python3 Tools/check_apk_single_abi.py "$apk" "${{ matrix.abi }}"',
|
|
]
|
|
for literal in required_literals:
|
|
if literal not in workflow_text:
|
|
errors.append(f"Workflow is missing required contract literal: {literal}")
|
|
|
|
if "${{ github." in workflow_text or "GITHUB_" in workflow_text:
|
|
errors.append("Forgejo workflow must not use GitHub compatibility aliases")
|
|
|
|
if "assembleAfatStandalone" in workflow_text:
|
|
errors.append("Workflow must not build the universal assembleAfatStandalone task")
|
|
|
|
if re.search(r"(?m)tag(_name)?\s*:\s*(latest|nightly|prerelease|standalone|apk)\s*$", workflow_text):
|
|
errors.append("Workflow must not publish to a shared rolling release tag")
|
|
|
|
if "forgejo-release" in workflow_text or "direction: upload" in workflow_text:
|
|
errors.append("Background Forgejo builds must not publish releases; verified local APKs remain authoritative")
|
|
|
|
for flavor in WORKFLOW_FLAVORS:
|
|
expected = ABI_FLAVORS[flavor]
|
|
matrix_literals = [
|
|
f"flavor: {expected['gradle_task_flavor']}",
|
|
f"flavor_dir: {expected['flavor_dir']}",
|
|
f"abi: {expected['abi']}",
|
|
f"artifact: {expected['artifact']}",
|
|
f"name: ${{{{ matrix.artifact }}}}",
|
|
f"dist/${{{{ matrix.artifact }}}}.apk",
|
|
]
|
|
for literal in matrix_literals:
|
|
if literal not in workflow_text:
|
|
errors.append(f"Workflow is missing matrix/upload value: {literal}")
|
|
|
|
if "ZaStoGram-standalone-x86_64" in workflow_text or "abi: x86_64" in workflow_text:
|
|
errors.append("Workflow must not spend CI time on the intentionally excluded x86_64 release APK")
|
|
|
|
return errors
|
|
|
|
|
|
def check_apk_abi_checker(checker_text: str) -> list[str]:
|
|
errors: list[str] = []
|
|
for literal in (
|
|
"VALID_ABIS",
|
|
"assets/chaquopy/bootstrap-native/",
|
|
"assets/chaquopy/(?:requirements|stdlib)-",
|
|
"foreign ABI payload is present",
|
|
"no native libraries found for expected ABI",
|
|
"no Chaquopy bootstrap runtime found for expected ABI",
|
|
):
|
|
if literal not in checker_text:
|
|
errors.append(f"APK ABI isolation checker is missing contract literal: {literal}")
|
|
return errors
|
|
|
|
|
|
def main() -> int:
|
|
errors = []
|
|
errors.extend(check_gradle(read_text(GRADLE_PATH)))
|
|
errors.extend(check_library_gradle(read_text(LIB_GRADLE_PATH)))
|
|
errors.extend(check_workflow(read_text(WORKFLOW_PATH)))
|
|
errors.extend(check_apk_abi_checker(read_text(APK_ABI_CHECK_PATH)))
|
|
|
|
if errors:
|
|
print("Build APK workflow guard failed:", file=sys.stderr)
|
|
for error in errors:
|
|
print(f"- {error}", file=sys.stderr)
|
|
return 1
|
|
|
|
print("Build APK workflow guard passed.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|