import { readFile } from 'node:fs/promises' import { resolve } from 'node:path' const ROOT = resolve(import.meta.dirname, '../..') const BUILD_GRADLE = resolve(ROOT, 'apps/mobile-rn/android/app/build.gradle') const ROOT_BUILD_GRADLE = resolve(ROOT, 'apps/mobile-rn/android/build.gradle') const PRODUCT_VERSION = resolve(ROOT, 'release/product-version.json') const CONFIG_MODULE = resolve( ROOT, 'apps/mobile-rn/android/app/src/main/java/com/d3ro/voice/D3ROConfigModule.kt', ) const GOOGLE_TEST_PUBLISHER_ID = 'ca-app-pub-3940256099942544' const TEST_PUBLISHER_PREFIX = `${GOOGLE_TEST_PUBLISHER_ID}/` const TEST_BANNER_ID = `${TEST_PUBLISHER_PREFIX}6300978111` const TEST_REWARDED_ID = `${TEST_PUBLISHER_PREFIX}5224354917` const VALID_VARIANTS = new Set(['debug', 'e2e', 'release']) function fail(message) { throw new Error(`mobile_build_config_invalid:${message}`) } function requireMatch(source, expression, message) { if (!expression.test(source)) fail(message) } function extractBlock(source, marker, message) { const markerIndex = source.search(marker) if (markerIndex < 0) fail(message) const openIndex = source.indexOf('{', markerIndex) if (openIndex < 0) fail(message) let depth = 0 for (let index = openIndex; index < source.length; index += 1) { if (source[index] === '{') depth += 1 if (source[index] === '}') { depth -= 1 if (depth === 0) return source.slice(openIndex + 1, index) } } fail(`${message}_unclosed`) } function parseBooleanConstant(source, name) { const match = source.match(new RegExp( `public static final boolean ${name} = (?:Boolean\\.parseBoolean\\(\"(true|false)\"\\)|(true|false));`, )) if (!match) fail(`missing_boolean_${name}`) return (match[1] ?? match[2]) === 'true' } function parseStringConstant(source, name) { const match = source.match(new RegExp(`public static final String ${name} = \"([^\"]*)\";`)) if (!match) fail(`missing_string_${name}`) return match[1] } function parseIntegerConstant(source, name) { const match = source.match(new RegExp(`public static final int ${name} = ([0-9]+);`)) if (!match) fail(`missing_integer_${name}`) return Number(match[1]) } async function readSourceContractFiles() { const [gradle, rootGradle, configModule, productVersionSource] = await Promise.all([ readFile(BUILD_GRADLE, 'utf8'), readFile(ROOT_BUILD_GRADLE, 'utf8'), readFile(CONFIG_MODULE, 'utf8'), readFile(PRODUCT_VERSION, 'utf8'), ]) return { gradle, rootGradle, configModule, productVersion: JSON.parse(productVersionSource) } } function verifySourceContractSources({ gradle, rootGradle, configModule, productVersion }) { if (/gradle\.startParameter\.taskNames/.test(gradle)) { fail('variant_gate_must_not_use_requested_task_name_heuristics') } if (/gradle\.taskGraph|whenReady\s*\(/.test(gradle)) { fail('variant_gate_must_not_use_configuration_cache_unsafe_task_graph_listeners') } const defaultConfigBlock = extractBlock(gradle, /\bdefaultConfig\s*\{/, 'missing_default_config') const e2eBlock = extractBlock(gradle, /\be2e\s*\{/, 'missing_e2e_build_type') const debugBlock = extractBlock(gradle, /\bdebug\s*\{/, 'missing_debug_build_type') const releaseBlock = extractBlock(gradle, /\brelease\s*\{/, 'missing_release_build_type') const gateTypeBlock = extractBlock( gradle, /abstract class VerifyD3roMobileVariantConfiguration extends DefaultTask\s*\{/, 'missing_variant_configuration_gate_type', ) const e2eGateBlock = extractBlock( gradle, /tasks\.register\(\s*"verifyE2eBuildConfiguration"/, 'missing_e2e_configuration_gate', ) const releaseGateBlock = extractBlock( gradle, /tasks\.register\(\s*"verifyReleaseBuildConfiguration"/, 'missing_release_configuration_gate', ) const lifecycleWiringBlock = extractBlock( gradle, /tasks\.configureEach\s*\{/, 'missing_variant_lifecycle_gate_wiring', ) requireMatch( defaultConfigBlock, /buildConfigField\s+"boolean",\s*"E2E_TEST_BUILD",\s*"false"/, 'default_e2e_flag_must_be_false', ) requireMatch(e2eBlock, /debuggable\s+false/, 'e2e_must_not_be_debuggable') requireMatch( e2eBlock, /matchingFallbacks\s*=\s*\["release"\]/, 'e2e_native_dependencies_must_match_non_debug_abi', ) requireMatch( e2eBlock, /buildConfigField\s+"boolean",\s*"E2E_TEST_BUILD",\s*"true"/, 'e2e_flag_must_be_true', ) requireMatch(e2eBlock, /abiFilters\.add\("arm64-v8a"\)/, 'e2e_arm64_abi_missing') requireMatch(e2eBlock, /abiFilters\.add\("x86_64"\)/, 'e2e_x86_64_abi_missing') if (/"E2E_TEST_BUILD",\s*"true"/.test(debugBlock)) fail('debug_e2e_flag_must_not_be_true') if (/"E2E_TEST_BUILD",\s*"true"/.test(releaseBlock)) fail('release_e2e_flag_must_not_be_true') const trueAssignments = [...gradle.matchAll(/"E2E_TEST_BUILD",\s*"true"/g)] if (trueAssignments.length !== 1) fail('e2e_true_assignment_must_be_unique') requireMatch( gradle, /debuggableVariants\s*=\s*\["debug"\]/, 'only_debug_may_skip_embedded_bundle', ) requireMatch( gradle, /configuredVersionName\s*=\s*secureSetting\("D3RO_VERSION_NAME"\)/, 'version_name_must_come_from_secure_setting', ) requireMatch( gradle, /configuredVersionCode\s*=\s*secureSetting\("D3RO_VERSION_CODE"\)/, 'version_code_must_come_from_secure_setting', ) if (typeof productVersion?.version !== 'string') fail('product_version_name_invalid') if (!Number.isSafeInteger(productVersion?.androidVersionCode) || productVersion.androidVersionCode <= 0) { fail('product_version_code_invalid') } const fallbackVersionName = gradle.match( /resolvedVersionName\s*=\s*versionSettingsValid\s*\?\s*configuredVersionName\s*:\s*"([^"]+)"/, )?.[1] if (fallbackVersionName !== productVersion.version) { fail('validated_version_name_must_feed_product_version') } const fallbackVersionCode = Number(gradle.match( /resolvedVersionCode\s*=\s*versionSettingsValid\s*\?\s*configuredVersionCodeValue\.toInteger\(\)\s*:\s*(\d+)/, )?.[1]) if (fallbackVersionCode !== productVersion.androidVersionCode) { fail('validated_version_code_must_feed_product_version') } requireMatch( gateTypeBlock, /@org\.gradle\.api\.tasks\.TaskAction\s+void\s+verifyConfiguration\(\)/, 'variant_gate_must_be_an_execution_time_task', ) for (const [pattern, message] of [ [ /private static final String GOOGLE_TEST_ADMOB_PUBLISHER_ID\s*=\s*"ca-app-pub-3940256099942544"/, 'release_gate_google_test_publisher_constant_required', ], [/D3RO_VERSION_NAME is required for release and e2e builds/, 'variant_gate_version_name_required'], [/D3RO_VERSION_NAME must be a strict semantic version/, 'variant_gate_semver_required'], [/D3RO_VERSION_CODE must be a positive decimal integer/, 'variant_gate_version_code_required'], [/D3RO_VERSION_CODE exceeds the Android maximum/, 'variant_gate_version_code_max_required'], [/google-services\.json for com\.d3ro\.voice/, 'release_gate_firebase_required'], [/missingReleaseSettings\.get\(\)/, 'release_gate_missing_settings_required'], [/D3RO_RELEASE_STORE_FILE must point to an existing keystore/, 'release_gate_keystore_required'], [/D3RO_ADMOB_APP_ID has an invalid format/, 'release_gate_admob_app_id_required'], [ /adMobAppId\.startsWith\(\s*GOOGLE_TEST_ADMOB_PUBLISHER_ID\s*\+\s*"~"\s*\)/, 'release_gate_admob_app_test_publisher_rejection_required', ], [/D3RO_ADMOB_BANNER_UNIT_ID:\s*productionBannerUnitId\.get\(\)/, 'release_gate_banner_id_required'], [/D3RO_ADMOB_REWARDED_UNIT_ID:\s*productionRewardedUnitId\.get\(\)/, 'release_gate_rewarded_id_required'], [ /value\.startsWith\(\s*GOOGLE_TEST_ADMOB_PUBLISHER_ID\s*\+\s*"\/"\s*\)/, 'release_gate_admob_unit_test_publisher_rejection_required', ], ]) { requireMatch(gateTypeBlock, pattern, message) } requireMatch(e2eGateBlock, /variantName\.set\("e2e"\)/, 'e2e_gate_variant_identity_missing') requireMatch( e2eGateBlock, /configureVersionGate\(delegate\)/, 'e2e_gate_must_receive_version_contract', ) requireMatch( releaseGateBlock, /variantName\.set\("release"\)/, 'release_gate_variant_identity_missing', ) requireMatch( releaseGateBlock, /configureVersionGate\(delegate\)/, 'release_gate_must_receive_version_contract', ) for (const [pattern, message] of [ [/missingReleaseSettings\.set\(missingReleaseSettingNames\)/, 'release_gate_missing_settings_input_missing'], [/firebaseConfigPath\.set\(firebaseConfigFile\.absolutePath\)/, 'release_gate_firebase_input_missing'], [/releaseStoreFilePath\.set\(releaseStoreAbsolutePath\)/, 'release_gate_keystore_input_missing'], [/productionAdMobAppId\.set\(releaseAdMobAppIdValue\)/, 'release_gate_admob_app_input_missing'], [/productionBannerUnitId\.set\(releaseBannerUnitIdValue\)/, 'release_gate_banner_input_missing'], [/productionRewardedUnitId\.set\(releaseRewardedUnitIdValue\)/, 'release_gate_rewarded_input_missing'], ]) { requireMatch(releaseGateBlock, pattern, message) } requireMatch( lifecycleWiringBlock, /candidate\.name\s*==\s*"preE2eBuild"[\s\S]*candidate\.dependsOn\(verifyE2eBuildConfiguration\)/, 'e2e_gate_must_be_wired_to_pre_e2e_build', ) requireMatch( lifecycleWiringBlock, /candidate\.name\s*==\s*"preReleaseBuild"[\s\S]*candidate\.dependsOn\(verifyReleaseBuildConfiguration\)/, 'release_gate_must_be_wired_to_pre_release_build', ) requireMatch( configModule, /"debug"\s+to\s+\(BuildConfig\.DEBUG\s*\|\|\s*BuildConfig\.E2E_TEST_BUILD\)/, 'native_runtime_must_recognize_non_debuggable_e2e', ) requireMatch( rootGradle, /e2eRequested\s*=\s*requestedBuildTasks\.any\s*\{\s*it\.contains\("e2e"\)\s*\}/, 'root_gradle_must_detect_e2e_tasks', ) requireMatch( rootGradle, /isUniversalNativeBuild\s*=\s*e2eRequested\s*\|\|/, 'root_gradle_e2e_must_be_universal', ) requireMatch( rootGradle, /requestedArchitectures\.toSet\(\)\s*!=\s*\["arm64-v8a",\s*"x86_64"\]\.toSet\(\)/, 'explicit_universal_architectures_must_be_exact', ) } async function verifySourceContract() { verifySourceContractSources(await readSourceContractFiles()) } function expectSourceContractFailure(sources, mutate, expectedMessage) { let failedAsExpected = false try { verifySourceContractSources({ ...sources, gradle: mutate(sources.gradle) }) } catch (error) { if (!(error instanceof Error) || !error.message.includes(expectedMessage)) throw error failedAsExpected = true } if (!failedAsExpected) fail(`self_test_missed_${expectedMessage}`) } function verifyVariant(variant, buildConfig) { const debug = parseBooleanConstant(buildConfig, 'DEBUG') const e2eTestBuild = parseBooleanConstant(buildConfig, 'E2E_TEST_BUILD') const buildType = parseStringConstant(buildConfig, 'BUILD_TYPE') const versionName = parseStringConstant(buildConfig, 'VERSION_NAME') const versionCode = parseIntegerConstant(buildConfig, 'VERSION_CODE') const bannerUnitId = parseStringConstant(buildConfig, 'ADMOB_BANNER_UNIT_ID') const rewardedUnitId = parseStringConstant(buildConfig, 'ADMOB_REWARDED_UNIT_ID') const runtimeDebug = debug || e2eTestBuild if (buildType !== variant) fail(`build_type_${buildType}_expected_${variant}`) if (variant === 'debug') { if (!debug || e2eTestBuild || !runtimeDebug) fail('debug_identity_mismatch') if (bannerUnitId !== TEST_BANNER_ID || rewardedUnitId !== TEST_REWARDED_ID) { fail('debug_must_use_google_test_units') } } if (variant === 'e2e') { if (debug || !e2eTestBuild || !runtimeDebug) fail('e2e_identity_mismatch') if (bannerUnitId !== TEST_BANNER_ID || rewardedUnitId !== TEST_REWARDED_ID) { fail('e2e_must_use_google_test_units') } } if (variant === 'release') { if (debug || e2eTestBuild || runtimeDebug) fail('release_identity_mismatch') for (const [name, value] of Object.entries({ bannerUnitId, rewardedUnitId })) { if (!/^ca-app-pub-\d+\/\d+$/.test(value)) fail(`release_${name}_format`) if (value.startsWith(TEST_PUBLISHER_PREFIX)) fail(`release_${name}_uses_test_publisher`) } const expectedBanner = process.env.D3RO_ADMOB_BANNER_UNIT_ID const expectedRewarded = process.env.D3RO_ADMOB_REWARDED_UNIT_ID if (expectedBanner && bannerUnitId !== expectedBanner) fail('release_banner_env_mismatch') if (expectedRewarded && rewardedUnitId !== expectedRewarded) fail('release_rewarded_env_mismatch') } if (variant === 'e2e' || variant === 'release') { const expectedVersionName = process.env.D3RO_VERSION_NAME const expectedVersionCode = Number(process.env.D3RO_VERSION_CODE) if (!expectedVersionName) fail(`${variant}_expected_version_name_missing`) if (!Number.isSafeInteger(expectedVersionCode) || expectedVersionCode <= 0) { fail(`${variant}_expected_version_code_invalid`) } if (versionName !== expectedVersionName) fail(`${variant}_version_name_mismatch`) if (versionCode !== expectedVersionCode) fail(`${variant}_version_code_mismatch`) } return { variant, buildType, debuggableRuntime: debug, e2eTestBuild, runtimeDebug, versionName, versionCode, adPublisher: bannerUnitId.split('/')[0], } } if (process.argv.includes('--self-test')) { const sources = await readSourceContractFiles() verifySourceContractSources(sources) expectSourceContractFailure( sources, (gradle) => gradle.replace('candidate.name == "preE2eBuild"', 'candidate.name == "preE2eBuildDisabled"'), 'e2e_gate_must_be_wired_to_pre_e2e_build', ) expectSourceContractFailure( sources, (gradle) => gradle.replace('candidate.name == "preReleaseBuild"', 'candidate.name == "preReleaseBuildDisabled"'), 'release_gate_must_be_wired_to_pre_release_build', ) expectSourceContractFailure( sources, (gradle) => `def requested = gradle.startParameter.taskNames\n${gradle}`, 'variant_gate_must_not_use_requested_task_name_heuristics', ) expectSourceContractFailure( sources, (gradle) => gradle.replace( 'firebaseConfigPath.set(firebaseConfigFile.absolutePath)', 'firebaseConfigPath.set("")', ), 'release_gate_firebase_input_missing', ) expectSourceContractFailure( sources, (gradle) => gradle.replace( 'GOOGLE_TEST_ADMOB_PUBLISHER_ID + "~"', '"disabled-google-test-publisher~"', ), 'release_gate_admob_app_test_publisher_rejection_required', ) expectSourceContractFailure( sources, (gradle) => gradle.replace( 'GOOGLE_TEST_ADMOB_PUBLISHER_ID + "/"', '"disabled-google-test-publisher/"', ), 'release_gate_admob_unit_test_publisher_rejection_required', ) expectSourceContractFailure( sources, (gradle) => gradle.replace( 'configureVersionGate(delegate)', 'configureVersionGateDisabled(delegate)', ), 'e2e_gate_must_receive_version_contract', ) expectSourceContractFailure( sources, (gradle) => gradle.replace( `: "${sources.productVersion.version}"`, ': "9.9.9"', ), 'validated_version_name_must_feed_product_version', ) expectSourceContractFailure( sources, (gradle) => gradle.replace( `: ${sources.productVersion.androidVersionCode}`, ': 1', ), 'validated_version_code_must_feed_product_version', ) console.log('Mobile build configuration gate self-test passed.') process.exit(0) } await verifySourceContract() const variant = process.argv[2] if (variant === undefined) { console.log(JSON.stringify({ sourceContract: 'verified' })) process.exit(0) } if (!VALID_VARIANTS.has(variant)) fail(`unknown_variant_${variant}`) const generatedPath = resolve( process.argv[3] ?? `apps/mobile-rn/android/app/build/generated/source/buildConfig/${variant}/com/d3ro/voice/BuildConfig.java`, ) const buildConfig = await readFile(generatedPath, 'utf8') console.log(JSON.stringify({ sourceContract: 'verified', generatedPath, ...verifyVariant(variant, buildConfig), }, null, 2))