plugins { id "com.jetbrains.python.envs" version "0.0.26" } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' apply plugin: 'jacoco' apply from: "$project.rootDir/automation/gradle/versionCode.gradle" apply plugin: 'androidx.navigation.safeargs.kotlin' apply plugin: 'com.google.android.gms.oss-licenses-plugin' import com.android.build.OutputFile import org.gradle.internal.logging.text.StyledTextOutput.Style import org.gradle.internal.logging.text.StyledTextOutputFactory import org.mozilla.fenix.gradle.tasks.LintUnitTestRunner import static org.gradle.api.tasks.testing.TestResult.ResultType android { compileSdkVersion 28 defaultConfig { applicationId "org.mozilla" minSdkVersion Config.minSdkVersion targetSdkVersion Config.targetSdkVersion versionCode 1 versionName Config.generateDebugVersionName() vectorDrawables.useSupportLibrary = true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArguments clearPackageData: 'true' resValue "bool", "IS_DEBUG", "false" buildConfigField "boolean", "USE_RELEASE_VERSIONING", "false" buildConfigField "String", "AMO_COLLECTION", "\"7e8d6dc651b54ab385fb8791bf9dac\"" manifestPlaceholders = [ "isRaptorEnabled": "false", "deepLinkScheme": "fenix-dev" ] } def releaseTemplate = { shrinkResources true minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' matchingFallbacks = ['release'] // Use on the "release" build type in dependencies (AARs) } buildTypes { debug { shrinkResources false minifyEnabled false applicationIdSuffix ".fenix.debug" buildConfigField "String", "AMO_COLLECTION", "\"16f6e5d9a40448b8955db57ced6d75\"" manifestPlaceholders.isRaptorEnabled = "true" resValue "bool", "IS_DEBUG", "true" pseudoLocalesEnabled true } forPerformanceTest releaseTemplate >> { // the ">>" concatenates the raptor-specific options with the template manifestPlaceholders.isRaptorEnabled = "true" applicationIdSuffix ".fenix.performancetest" debuggable true manifestPlaceholders = [ // Since we configure this build to behave like a "fennec" flavored build, we need // to set a shared user id for the manifest. The actual values does not matter. // However we pick a unique value to not "clash" with other Fenix/Fennec builds // installed on the device. "sharedUserId": "org.mozilla.fenix.performancetest.sharedID" ] } fenixNightly releaseTemplate >> { applicationIdSuffix ".fenix.nightly" buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" buildConfigField "String", "AMO_COLLECTION", "\"16f6e5d9a40448b8955db57ced6d75\"" manifestPlaceholders = ["deepLinkScheme": "fenix-nightly"] } fenixBeta releaseTemplate >> { applicationIdSuffix ".fenix.beta" buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" manifestPlaceholders = ["deepLinkScheme": "fenix-beta"] } fenixProduction releaseTemplate >> { applicationIdSuffix ".fenix" buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" manifestPlaceholders = ["deepLinkScheme": "fenix"] } fennecProduction releaseTemplate >> { buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" applicationIdSuffix ".firefox" manifestPlaceholders = [ // This release type is meant to replace Firefox (Release channel) and therefore needs to inherit // its sharedUserId for all eternity. See: // https://searchfox.org/mozilla-central/search?q=moz_android_shared_id&case=false®exp=false&path= // Shipping an app update without sharedUserId can have // fatal consequences. For example see: // - https://issuetracker.google.com/issues/36924841 // - https://issuetracker.google.com/issues/36905922 "sharedUserId": "org.mozilla.firefox.sharedID", "deepLinkScheme": "fenix" ] } fennecBeta releaseTemplate >> { buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" applicationIdSuffix ".firefox_beta" manifestPlaceholders = [ // This release type is meant to replace Firefox (Release channel) and therefore needs to inherit // its sharedUserId for all eternity. See: // https://searchfox.org/mozilla-central/search?q=moz_android_shared_id&case=false®exp=false&path= // Shipping an app update without sharedUserId can have // fatal consequences. For example see: // - https://issuetracker.google.com/issues/36924841 // - https://issuetracker.google.com/issues/36905922 "sharedUserId": "org.mozilla.firefox.sharedID", "deepLinkScheme": "fenix-beta" ] } fennecNightly releaseTemplate >> { buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" buildConfigField "String", "AMO_COLLECTION", "\"16f6e5d9a40448b8955db57ced6d75\"" applicationIdSuffix ".fennec_aurora" manifestPlaceholders = [ // This release type is meant to replace Firefox (Release channel) and therefore needs to inherit // its sharedUserId for all eternity. See: // https://searchfox.org/mozilla-central/search?q=moz_android_shared_id&case=false®exp=false&path= // Shipping an app update without sharedUserId can have // fatal consequences. For example see: // - https://issuetracker.google.com/issues/36924841 // - https://issuetracker.google.com/issues/36905922 "sharedUserId": "org.mozilla.fennec.sharedID", "deepLinkScheme": "fenix-nightly" ] } } variantFilter { // There's a "release" build type that exists by default that we don't use (it's replaced by "nightly" and "beta") if (buildType.name == 'release') { setIgnore true } // Current build variant setup: // // | geckoNightly | geckoBeta | // |--------------------|---------------|-----------| // | debug | ✅ | ✅ | Both variants for testing and development. // | forPerformanceTest | ✅ | ✅ | Both variants unless the perf team only cares about Nightly (TBD). // | fenixNightly | ✅ | ✅ | Built with both, but only the "geckoNightly" one is published to Google Play // | fenixBeta | ❌ | ✅ | Fenix Beta ships with GV Beta // | fenixProduction | ❌ | ✅ | Fenix Production ships with GV Beta // | fennecProduction | ❌ | ✅ | Fenix build to replace production Firefox builds // | fennecBeta | ❌ | ✅ | Fenix build to replace beta Firefox builds // | fennecNightly | ✅ | ❌ | Fenix build to replace Nightly Firefox builds def flavors = flavors*.name.toString().toLowerCase() if (buildType.name == 'fenixBeta' && flavors.contains("geckonightly")) { setIgnore true } if (buildType.name == 'fenixProduction' && flavors.contains("geckonightly")) { setIgnore true } if ((buildType.name == 'fennecProduction' || buildType.name == 'fennecBeta') && flavors.contains("geckonightly")) { setIgnore true } if (buildType.name == 'fennecNightly' && flavors.contains("geckobeta")) { setIgnore true } } aaptOptions { // All JavaScript code used internally by GeckoView is packaged in a // file called omni.ja. If this file is compressed in the APK, // GeckoView must uncompress it before it can do anything else which // causes a significant delay on startup. noCompress 'ja' } testOptions { execution 'ANDROIDX_TEST_ORCHESTRATOR' unitTests.includeAndroidResources = true animationsDisabled = true } flavorDimensions "engine" sourceSets { androidTest { resources.srcDirs += ['src/androidTest/resources'] } fennecNightly { java.srcDirs = ['src/migration/java'] manifest.srcFile "src/migration/AndroidManifest.xml" } fennecBeta { java.srcDirs = ['src/migration/java'] manifest.srcFile "src/migration/AndroidManifest.xml" } fennecProduction { java.srcDirs = ['src/migration/java'] manifest.srcFile "src/migration/AndroidManifest.xml" } forPerformanceTest { // We want our performance test builds to use the same code as our "fennec" flavor builds // since those builds will ship to our large user base. java.srcDirs = ['src/migration/java'] manifest.srcFile "src/migration/AndroidManifest.xml" } } productFlavors { geckoNightly { dimension "engine" } geckoBeta { dimension "engine" } } splits { abi { enable true reset() include "x86", "armeabi-v7a", "arm64-v8a", "x86_64" } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } lintOptions { lintConfig file("lint.xml") } packagingOptions { exclude 'META-INF/atomicfu.kotlin_module' } testOptions { unitTests.returnDefaultValues = true } } def baseVersionCode = generatedVersionCode android.applicationVariants.all { variant -> // ------------------------------------------------------------------------------------------------- // Set up kotlin-allopen plugin for writing tests // ------------------------------------------------------------------------------------------------- boolean hasTest = gradle.startParameter.taskNames.find { it.contains("test") || it.contains("Test") } != null if (hasTest) { apply plugin: 'kotlin-allopen' allOpen { annotation("org.mozilla.fenix.utils.OpenClass") } } // ------------------------------------------------------------------------------------------------- // Generate version codes for builds // ------------------------------------------------------------------------------------------------- def isDebug = variant.buildType.resValues['IS_DEBUG']?.value ?: false def useReleaseVersioning = variant.buildType.buildConfigFields['USE_RELEASE_VERSIONING']?.value ?: false def versionName = Config.releaseVersionName(project) println("----------------------------------------------") println("Variant name: " + variant.name) println("Application ID: " + [variant.mergedFlavor.applicationId, variant.buildType.applicationIdSuffix].findAll().join()) println("Build type: " + variant.buildType.name) println("Flavor: " + variant.flavorName) println("Telemetry enabled: " + !isDebug) if (useReleaseVersioning) { // The Google Play Store does not allow multiple APKs for the same app that all have the // same version code. Therefore we need to have different version codes for our ARM and x86 // builds. // Our generated version code now has a length of 9 (See automation/gradle/versionCode.gradle). // Our x86 builds need a higher version code to avoid installing ARM builds on an x86 device // with ARM compatibility mode. variant.outputs.each { output -> def abi = output.getFilter(OutputFile.ABI) def versionCodeOverride if (variant.name.contains("Fennec")) { versionCodeOverride = Config.generateFennecVersionCode(abi) } else if (abi == "x86_64") { versionCodeOverride = baseVersionCode + 3 } else if (abi == "x86") { versionCodeOverride = baseVersionCode + 2 } else if (abi == "arm64-v8a") { versionCodeOverride = baseVersionCode + 1 } else if (abi == "armeabi-v7a") { versionCodeOverride = baseVersionCode } else { throw RuntimeException("Unknown ABI: $abi") } println("versionCode for $abi = $versionCodeOverride") output.versionNameOverride = versionName output.versionCodeOverride = versionCodeOverride } } // ------------------------------------------------------------------------------------------------- // BuildConfig: Set variables for Sentry, Crash Reporting, and Telemetry // ------------------------------------------------------------------------------------------------- buildConfigField 'String', 'SENTRY_TOKEN', 'null' if (!isDebug) { buildConfigField 'boolean', 'CRASH_REPORTING', 'true' // Reading sentry token from local file (if it exists). In a release task on taskcluster it will be available. try { def token = new File("${rootDir}/.sentry_token").text.trim() buildConfigField 'String', 'SENTRY_TOKEN', '"' + token + '"' } catch (FileNotFoundException ignored) {} } else { buildConfigField 'boolean', 'CRASH_REPORTING', 'false' } if (!isDebug) { buildConfigField 'boolean', 'TELEMETRY', 'true' } else { buildConfigField 'boolean', 'TELEMETRY', 'false' } def buildDate = Config.generateBuildDate() buildConfigField 'String', 'BUILD_DATE', '"' + buildDate + '"' def variantName = variant.getName() // ------------------------------------------------------------------------------------------------- // Adjust: Read token from local file if it exists (Only release builds) // ------------------------------------------------------------------------------------------------- print("Adjust token: ") if (!isDebug) { try { def token = new File("${rootDir}/.adjust_token").text.trim() buildConfigField 'String', 'ADJUST_TOKEN', '"' + token + '"' println "(Added from .adjust_token file)" } catch (FileNotFoundException ignored) { buildConfigField 'String', 'ADJUST_TOKEN', 'null' println("X_X") } } else { buildConfigField 'String', 'ADJUST_TOKEN', 'null' println("--") } // ------------------------------------------------------------------------------------------------- // Leanplum: Read token from local file if it exists // ------------------------------------------------------------------------------------------------- print("Leanplum token: ") try { def parts = new File("${rootDir}/.leanplum_token").text.trim().split(":") def id = parts[0] def key = parts[1] buildConfigField 'String', 'LEANPLUM_ID', '"' + id + '"' buildConfigField 'String', 'LEANPLUM_TOKEN', '"' + key + '"' println "(Added from .leanplum_token file)" } catch (FileNotFoundException ignored) { buildConfigField 'String', 'LEANPLUM_ID', 'null' buildConfigField 'String', 'LEANPLUM_TOKEN', 'null' println("X_X") } // ------------------------------------------------------------------------------------------------- // Digital Asset Links: Read token from local file if it exists // ------------------------------------------------------------------------------------------------- print("Digital Asset Links token: ") try { def token = new File("${rootDir}/.digital_asset_links_token").text.trim() buildConfigField 'String', 'DIGITAL_ASSET_LINKS_TOKEN', '"' + token + '"' println "(Added from .digital_asset_links_token file)" } catch (FileNotFoundException ignored) { buildConfigField 'String', 'DIGITAL_ASSET_LINKS_TOKEN', 'null' println("X_X") } // ------------------------------------------------------------------------------------------------- // MLS: Read token from local file if it exists // ------------------------------------------------------------------------------------------------- print("MLS token: ") try { def token = new File("${rootDir}/.mls_token").text.trim() buildConfigField 'String', 'MLS_TOKEN', '"' + token + '"' println "(Added from .mls_token file)" } catch (FileNotFoundException ignored) { buildConfigField 'String', 'MLS_TOKEN', '""' println("X_X") } } androidExtensions { experimental = true } // Generate Kotlin code and markdown docs for the Fenix Glean metrics. ext.gleanGenerateMarkdownDocs = true ext.gleanDocsDirectory = "$rootDir/docs" apply plugin: "org.mozilla.telemetry.glean-gradle-plugin" dependencies { geckoNightlyImplementation Deps.mozilla_browser_engine_gecko_nightly geckoBetaImplementation Deps.mozilla_browser_engine_gecko_beta implementation Deps.kotlin_stdlib implementation Deps.kotlin_coroutines implementation Deps.kotlin_coroutines_android testImplementation Deps.kotlin_coroutines_test implementation Deps.androidx_appcompat implementation Deps.androidx_constraintlayout implementation Deps.androidx_coordinatorlayout implementation Deps.sentry implementation Deps.osslicenses_library implementation Deps.leanplum_core implementation Deps.leanplum_fcm implementation Deps.mozilla_concept_engine implementation Deps.mozilla_concept_push implementation Deps.mozilla_concept_storage implementation Deps.mozilla_concept_sync implementation Deps.mozilla_concept_toolbar implementation Deps.mozilla_browser_awesomebar implementation Deps.mozilla_feature_downloads implementation Deps.mozilla_browser_domains implementation Deps.mozilla_browser_icons implementation Deps.mozilla_browser_menu implementation Deps.mozilla_browser_search implementation Deps.mozilla_browser_session implementation Deps.mozilla_browser_state implementation Deps.mozilla_browser_storage_sync implementation Deps.mozilla_browser_toolbar implementation Deps.mozilla_support_extensions implementation Deps.mozilla_feature_addons implementation Deps.mozilla_feature_accounts implementation Deps.mozilla_feature_app_links implementation Deps.mozilla_feature_awesomebar implementation Deps.mozilla_feature_contextmenu implementation Deps.mozilla_feature_customtabs implementation Deps.mozilla_feature_downloads implementation Deps.mozilla_feature_intent implementation Deps.mozilla_feature_media implementation Deps.mozilla_feature_prompts implementation Deps.mozilla_feature_push implementation Deps.mozilla_feature_pwa implementation Deps.mozilla_feature_qr implementation Deps.mozilla_feature_search implementation Deps.mozilla_feature_session implementation Deps.mozilla_feature_toolbar implementation Deps.mozilla_feature_tabs implementation Deps.mozilla_feature_findinpage implementation Deps.mozilla_feature_site_permissions implementation Deps.mozilla_feature_readerview implementation Deps.mozilla_feature_tab_collections implementation Deps.mozilla_feature_top_sites implementation Deps.mozilla_feature_share implementation Deps.mozilla_feature_accounts_push implementation Deps.mozilla_feature_webcompat implementation Deps.mozilla_feature_webnotifications implementation Deps.mozilla_service_sync_logins implementation Deps.mozilla_service_firefox_accounts implementation Deps.mozilla_service_glean implementation Deps.mozilla_service_experiments implementation Deps.mozilla_service_location implementation Deps.mozilla_support_base implementation Deps.mozilla_support_ktx implementation Deps.mozilla_support_rustlog implementation Deps.mozilla_support_utils implementation Deps.mozilla_support_locale implementation Deps.mozilla_support_migration implementation Deps.mozilla_ui_colors implementation Deps.mozilla_ui_icons implementation Deps.mozilla_ui_publicsuffixlist implementation Deps.mozilla_lib_crash implementation Deps.mozilla_lib_push_firebase implementation Deps.mozilla_lib_dataprotect debugImplementation Deps.leakcanary implementation Deps.androidx_legacy implementation Deps.androidx_biometric implementation Deps.androidx_paging implementation Deps.androidx_preference implementation Deps.androidx_fragment implementation Deps.androidx_navigation_fragment implementation Deps.androidx_navigation_ui implementation Deps.androidx_recyclerview implementation Deps.androidx_lifecycle_livedata implementation Deps.androidx_lifecycle_runtime implementation Deps.androidx_lifecycle_viewmodel implementation Deps.androidx_core implementation Deps.androidx_core_ktx implementation Deps.androidx_transition implementation Deps.androidx_work_ktx implementation Deps.google_material implementation Deps.google_flexbox implementation Deps.lottie implementation Deps.adjust implementation Deps.installreferrer // Required by Adjust implementation Deps.google_ads_id // Required for the Google Advertising ID androidTestImplementation Deps.uiautomator // Removed pending AndroidX fixes androidTestImplementation "tools.fastlane:screengrab:2.0.0" // androidTestImplementation "br.com.concretesolutions:kappuccino:1.2.1" androidTestImplementation Deps.espresso_core, { exclude group: 'com.android.support', module: 'support-annotations' } androidTestImplementation(Deps.espresso_contrib) { exclude module: 'appcompat-v7' exclude module: 'support-v4' exclude module: 'support-annotations' exclude module: 'recyclerview-v7' exclude module: 'design' exclude module: 'espresso-core' } androidTestImplementation Deps.androidx_test_core androidTestImplementation Deps.espresso_idling_resources androidTestImplementation Deps.espresso_intents androidTestImplementation Deps.tools_test_runner androidTestImplementation Deps.tools_test_rules androidTestUtil Deps.orchestrator androidTestImplementation Deps.espresso_core, { exclude group: 'com.android.support', module: 'support-annotations' } androidTestImplementation Deps.androidx_junit androidTestImplementation Deps.androidx_work_testing androidTestImplementation Deps.mockwebserver testImplementation Deps.mozilla_support_test testImplementation Deps.androidx_junit testImplementation Deps.androidx_work_testing testImplementation (Deps.robolectric) { exclude group: 'org.apache.maven' } testImplementation 'org.apache.maven:maven-ant-tasks:2.1.3' implementation Deps.mozilla_support_rusthttp testImplementation Deps.mockito_core androidTestImplementation Deps.mockito_android testImplementation Deps.mockk // For the initial release of Glean 19, we require consumer applications to // depend on a separate library for unit tests. This will be removed in future releases. testImplementation "org.mozilla.telemetry:glean-forUnitTests:${project.ext.glean_version}" lintChecks project(":mozilla-lint-rules") } if (project.hasProperty("raptor")) { android.defaultConfig.manifestPlaceholders.isRaptorEnabled = "true" } if (project.hasProperty("coverage")) { tasks.withType(Test) { jacoco.includeNoLocationClasses = true } android.applicationVariants.all { variant -> task "jacoco${variant.name.capitalize()}TestReport"(type: JacocoReport, dependsOn: "test${variant.name.capitalize()}UnitTest") { reports { xml.enabled = true html.enabled = true } def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*', '**/*$[0-9].*'] def kotlinDebugTree = fileTree(dir: "$project.buildDir/tmp/kotlin-classes/${variant.name}", excludes: fileFilter) def javaDebugTree = fileTree(dir: "$project.buildDir/intermediates/classes/${variant.flavorName}/${variant.buildType.name}", excludes: fileFilter) def mainSrc = "$project.projectDir/src/main/java" sourceDirectories = files([mainSrc]) classDirectories = files([kotlinDebugTree, javaDebugTree]) executionData = fileTree(dir: project.buildDir, includes: [ "jacoco/test${variant.name.capitalize()}UnitTest.exec", 'outputs/code-coverage/connected/*coverage.ec' ]) } } android { buildTypes { debug { testCoverageEnabled true } } } } // ------------------------------------------------------------------------------------------------- // Task for printing APK information for the requested variant // Usage: "./gradlew printVariants // ------------------------------------------------------------------------------------------------- task printVariants { doLast { def variants = android.applicationVariants.collect {[ apks: it.variantData.outputScope.apkDatas.collect {[ abi: it.filters.find { it.filterType == 'ABI' }.identifier, fileName: it.outputFileName, ]}, build_type: it.buildType.name, engine: it.productFlavors.find { it.dimension == 'engine' }.name, name: it.name, ]} // AndroidTest is a special case not included above variants.add([ apks: [[ abi: 'noarch', fileName: 'app-geckoNightly-debug-androidTest.apk', ]], build_type: 'androidTest', engine: 'geckoNightly', name: 'androidTest', ]) println 'variants: ' + groovy.json.JsonOutput.toJson(variants) } } task buildTranslationArray { def foundLocales = new StringBuilder() foundLocales.append("new String[]{") fileTree("src/main/res").visit { FileVisitDetails details -> if(details.file.path.endsWith("/strings.xml")){ def languageCode = details.file.parent.tokenize('/').last().replaceAll('values-','').replaceAll('-r','-') languageCode = (languageCode == "values") ? "en-US" : languageCode foundLocales.append("\"").append(languageCode).append("\"").append(",") } } foundLocales.append("}") def foundLocalesString = foundLocales.toString().replaceAll(',}','}') android.defaultConfig.buildConfigField "String[]", "SUPPORTED_LOCALE_ARRAY", foundLocalesString } task lintUnitTestRunner(type: LintUnitTestRunner) afterEvaluate { // Format test output. Ported from AC #2401 tasks.matching {it instanceof Test}.all { systemProperty "robolectric.logging", "stdout" systemProperty "logging.test-mode", "true" testLogging.events = [] def out = services.get(StyledTextOutputFactory).create("tests") beforeSuite { descriptor -> if (descriptor.getClassName() != null) { out.style(Style.Header).println("\nSUITE: " + descriptor.getClassName()) } } beforeTest { descriptor -> out.style(Style.Description).println(" TEST: " + descriptor.getName()) } onOutput { descriptor, event -> logger.lifecycle(" " + event.message.trim()) } afterTest { descriptor, result -> switch (result.getResultType()) { case ResultType.SUCCESS: out.style(Style.Success).println(" SUCCESS") break case ResultType.FAILURE: out.style(Style.Failure).println(" FAILURE") logger.lifecycle("", result.getException()) break case ResultType.SKIPPED: out.style(Style.Info).println(" SKIPPED") break } logger.lifecycle("") } } } if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopsrcdir')) { if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopobjdir')) { ext.topobjdir = gradle."localProperties.dependencySubstitutions.geckoviewTopobjdir" } ext.topsrcdir = gradle."localProperties.dependencySubstitutions.geckoviewTopsrcdir" apply from: "${topsrcdir}/substitute-local-geckoview.gradle" } if (gradle.hasProperty('localProperties.autoPublish.android-components.dir')) { ext.acSrcDir = gradle."localProperties.autoPublish.android-components.dir" apply from: "../${acSrcDir}/substitute-local-ac.gradle" } if (gradle.hasProperty('localProperties.autoPublish.application-services.dir')) { ext.appServicesSrcDir = gradle."localProperties.autoPublish.application-services.dir" apply from: "../${appServicesSrcDir}/build-scripts/substitute-local-appservices.gradle" }