foundry.gradle.StandardProjectConfigurations.kt Maven / Gradle / Ivy
/*
* Copyright (C) 2022 Slack Technologies, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("UnstableApiUsage")
package foundry.gradle
import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.HasAndroidTest
import com.android.build.api.variant.HasAndroidTestBuilder
import com.android.build.api.variant.HasUnitTestBuilder
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.api.variant.LibraryVariant
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.TestExtension
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
import com.android.build.gradle.tasks.JavaPreCompileTask
import com.autonomousapps.DependencyAnalysisSubExtension
import com.bugsnag.android.gradle.BugsnagPluginExtension
import foundry.gradle.Configurations.isPlatformConfigurationName
import foundry.gradle.artifacts.FoundryArtifact
import foundry.gradle.artifacts.Publisher
import foundry.gradle.dependencies.FoundryDependencies
import foundry.gradle.dependencyrake.RakeDependencies
import foundry.gradle.kgp.KgpTasks
import foundry.gradle.lint.LintTasks
import foundry.gradle.permissionchecks.PermissionChecks
import foundry.gradle.properties.setDisallowChanges
import foundry.gradle.tasks.AndroidTestApksTask
import foundry.gradle.tasks.CheckManifestPermissionsTask
import foundry.gradle.tasks.SimpleFileProducerTask
import foundry.gradle.tasks.publishWith
import foundry.gradle.tasks.robolectric.UpdateRobolectricJarsTask
import foundry.gradle.unittest.UnitTests
import net.ltgt.gradle.errorprone.CheckSeverity
import net.ltgt.gradle.errorprone.errorprone
import net.ltgt.gradle.nullaway.nullaway
import org.gradle.api.Action
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.MinimalExternalModuleDependency
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.api.logging.Logger
import org.gradle.api.plugins.JavaBasePlugin
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.jvm.toolchain.JavaCompiler
import org.gradle.jvm.toolchain.JavaLanguageVersion
import org.gradle.jvm.toolchain.JavaToolchainService
import org.gradle.language.base.plugins.LifecycleBasePlugin
private const val LOG = "Foundry:"
private const val FIVE_MINUTES_MS = 300_000L
private fun Logger.logWithTag(message: String) {
debug("$LOG $message")
}
/**
* Standard [Project] configurations. This class will be iterated on over time as we grow out our
* bootstrapping options for Gradle subprojects.
*
* Principles:
* - Avoid duplicating work and allocations. This runs at configuration time and should be as low
* overhead as possible.
* - Do not resolve dependencies at configuration-time. Use appropriate callback APIs!
* - Support Kotlin, Android, and Java projects.
* - One-off configuration should be left to individual projects to declare.
* - Use debug logging.
*/
@Suppress("TooManyFunctions")
internal class StandardProjectConfigurations(
private val globalProperties: FoundryProperties,
private val versionCatalog: VersionCatalog,
private val foundryTools: FoundryTools,
) {
fun applyTo(
project: Project,
foundryExtension: FoundryExtension,
foundryProperties: FoundryProperties,
) {
if (foundryProperties.eagerlyConfigureArtifactPublishing) {
setUpSubprojectArtifactPublishing(project)
}
project.applyCommonConfigurations(foundryProperties)
project.applyJvmConfigurations(foundryProperties, foundryExtension)
KgpTasks.configure(project, foundryTools, foundryProperties)
}
/**
* Always enables publishing of all SgpArtifacts, even if we never end up publishing artifacts
* This sucks but I don't see any other way to do this due to how tightly locked down Gradle's
* inter-project access APIs are in project isolation.
*
* Ideally, we would only add project dependencies when they are definitely able to contribute
* artifacts to that configuration, but that's not possible when:
* 1. Root projects can't reach into subprojects to ask about their configuration
* 2. Subprojects can't reach into root projects to add dependencies conditionally
* 3. There doesn't seem to be a way to depend on a certain project's configuration if that
* configuration doesn't exist. This sorta makes sense, but for the purpose of inter-project
* artifacts I wish it was possible to depend on a configuration that may not exist and just
* treat it as an empty config that publishes no artifacts.
*
* It _seems_ like #3 is possible via [ArtifactView.ViewConfiguration.lenient], so this function
* is behind a flag just as a failsafe.
*/
private fun setUpSubprojectArtifactPublishing(project: Project) {
for (artifact in FoundryArtifact::class.sealedSubclasses) {
Publisher.interProjectPublisher(project, artifact.objectInstance!!)
}
}
private fun Project.applyCommonConfigurations(foundryProperties: FoundryProperties) {
if (globalProperties.autoApplySortDependencies) {
if (project.buildFile.exists()) {
val sortDependenciesIgnoreSet =
globalProperties.sortDependenciesIgnore?.splitToSequence(',')?.toSet().orEmpty()
if (project.path !in sortDependenciesIgnoreSet) {
pluginManager.apply("com.squareup.sort-dependencies")
}
}
}
LintTasks.configureSubProject(
project,
foundryProperties,
foundryTools.globalConfig.affectedProjects,
foundryTools::logAvoidedTask,
)
}
@Suppress("unused")
private fun Project.javaCompilerFor(version: Int): Provider {
return extensions.getByType().compilerFor {
languageVersion.setDisallowChanges(JavaLanguageVersion.of(version))
foundryTools.globalConfig.jvmVendor?.let(vendor::set)
}
}
private fun Project.applyJvmConfigurations(
foundryProperties: FoundryProperties,
foundryExtension: FoundryExtension,
) {
val platformProjectPath = foundryProperties.platformProjectPath
if (platformProjectPath == null) {
if (foundryProperties.strictMode) {
logger.warn(
"foundry.location.foundry-platform is not set. Consider creating one to ensure consistent dependency versions across projects!"
)
}
} else if (!foundryProperties.noPlatform && path != platformProjectPath) {
applyPlatforms(foundryProperties.versions.boms, platformProjectPath)
}
checkAndroidXDependencies(foundryProperties)
AnnotationProcessing.configureFor(project)
pluginManager.onFirst(JVM_PLUGINS) { pluginId ->
foundryProperties.versions.bundles.commonAnnotations.ifPresent {
dependencies.add("implementation", it)
}
UnitTests.configureSubproject(
project,
pluginId,
foundryProperties,
foundryTools.globalConfig.affectedProjects,
foundryTools::logAvoidedTask,
)
if (pluginId != "com.android.test") {
// Configure dependencyAnalysis
// TODO move up once DAGP supports com.android.test projects
// https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin/issues/797
if (foundryProperties.enableAnalysisPlugin && project.path != platformProjectPath) {
val buildFile = project.buildFile
// This can run on some intermediate middle directories, like `carbonite` in
// `carbonite:carbonite`
if (buildFile.exists()) {
// Configure rake
plugins.withId("com.autonomousapps.dependency-analysis") {
val isNoApi = foundryProperties.rakeNoApi
val catalogNames =
extensions.findByType()?.catalogNames ?: return@withId
val catalogs =
catalogNames.map { catalogName -> project.getVersionsCatalog(catalogName) }
val rakeDependencies =
tasks.register("rakeDependencies") {
// TODO https://github.com/gradle/gradle/issues/25014
buildFileProperty.set(project.buildFile)
noApi.setDisallowChanges(isNoApi)
identifierMap.setDisallowChanges(
project.provider {
buildMap {
for (catalog in catalogs) {
putAll(
catalog.identifierMap().mapValues { (_, v) -> "${catalog.name}.$v" }
)
}
}
}
)
missingIdentifiersFile.set(
project.layout.buildDirectory.file("rake/missing_identifiers.txt")
)
}
configure {
registerPostProcessingTask(rakeDependencies)
}
val publisher =
Publisher.interProjectPublisher(project, FoundryArtifact.DAGP_MISSING_IDENTIFIERS)
publisher.publish(rakeDependencies.flatMap { it.missingIdentifiersFile })
}
}
}
}
}
// TODO always configure compileOptions here
configureAndroidProjects(foundryExtension, foundryProperties)
configureJavaProject(foundryProperties)
foundryExtension.applyTo(this)
}
/**
* Applies platform()/bom dependencies for projects, right now only on known
* [Configurations.Groups.PLATFORM].
*/
private fun Project.applyPlatforms(
boms: Set>,
platformProject: String,
) {
configurations.configureEach {
if (Configurations.isTest(name) && Configurations.isApi(name)) {
// Don't add dependencies to testApi configurations as these are never used
// https://youtrack.jetbrains.com/issue/KT-61653
project.logger.debug("Ignoring boms on ${project.path}:$name")
return@configureEach
}
if (isPlatformConfigurationName(name)) {
project.logger.debug("Adding boms to ${project.path}:$name")
project.dependencies.apply {
for (bom in boms) {
add(name, platform(bom))
}
add(name, platform(project(platformProject)))
}
}
}
}
/**
* We reject new `com.android.support` dependencies to eliminate the dependence on Jetifier. Note
* we apply this on all projects (not just android projects) because some android dependencies are
* plain jars that can be used in standard JVM projects.
*/
private fun Project.checkAndroidXDependencies(foundryProperties: FoundryProperties) {
if (!foundryProperties.skipAndroidxCheck) {
configurations.configureEach {
resolutionStrategy {
eachDependency {
if (requested.group == "com.android.support") {
throw IllegalArgumentException(
"Legacy support library dependencies are no longer " +
"supported. To trace this dependency, run './gradlew " +
"checkJetifier -Pandroid.enableJetifier=true --no-configuration-cache"
)
}
}
}
}
}
}
/** Adds common configuration for Java projects. */
private fun Project.configureJavaProject(foundryProperties: FoundryProperties) {
val releaseVersion =
foundryProperties.versions.jvmTarget.map(JavaVersion::toVersion).asProvider(providers)
plugins.withType(JavaBasePlugin::class.java).configureEach {
project.configure {
sourceCompatibility = releaseVersion.get()
targetCompatibility = releaseVersion.get()
}
foundryProperties.versions.jdk.ifPresent {
if (it >= 9) {
tasks.configureEach {
if (!isAndroid) {
logger.logWithTag("Configuring release option for $path")
options.release.setDisallowChanges(releaseVersion.map { it.majorVersion.toInt() })
}
}
}
}
}
val javaToolchains by lazy { project.serviceOf() }
tasks.withType(JavaCompile::class.java).configureEach {
// Keep parameter names, this is useful for annotation processors and static analysis tools
options.compilerArgs.addAll(listOf("-parameters"))
// Android is our lowest JVM target, so if we're an android project we'll always use that
// source target.
// TODO is this late enough to be safe?
// TODO if we set it in android, does the config from this get safely ignored?
// TODO re-enable in android at all after AGP 7.1
if (!isAndroid) {
val target =
if (isAndroid) releaseVersion
else
foundryProperties.versions.jdk.map(JavaVersion::toVersion).asProvider(project.providers)
logger.logWithTag("Configuring toolchain for $path")
// Can't use disallowChanges here because Gradle sets it again later for some reason
javaCompiler.set(
javaToolchains.compilerFor {
languageVersion.setDisallowChanges(
target.map { JavaLanguageVersion.of(it.majorVersion) }
)
foundryTools.globalConfig.jvmVendor?.let(vendor::set)
}
)
}
}
configureErrorProne(foundryProperties)
}
/**
* Adds common configuration for error prone on Java projects. Note that this still uses
* application of the error prone plugin as an opt-in marker for now, and is not applied to every
* project.
*/
private fun Project.configureErrorProne(foundryProperties: FoundryProperties) {
val autoPatchEnabled = foundryProperties.errorProneAutoPatch
pluginManager.withPlugin("net.ltgt.nullaway") {
val nullawayBaseline = foundryProperties.nullawayBaseline
val nullawayDep =
foundryProperties.versions.getOptionalValue("errorProne-nullaway").orElseThrow {
IllegalStateException("Could not find errorProne-nullaway in the catalog")
}
dependencies.apply { add("errorprone", nullawayDep) }
tasks.withType(JavaCompile::class.java).configureEach {
val nullAwaySeverity =
if (name.contains("test", ignoreCase = true)) {
CheckSeverity.OFF
} else {
CheckSeverity.ERROR
}
options.errorprone.nullaway {
severity.setDisallowChanges(nullAwaySeverity)
// TODO make this configurable
annotatedPackages.add("slack")
checkOptionalEmptiness.setDisallowChanges(true)
if (autoPatchEnabled && nullawayBaseline) {
suggestSuppressions.setDisallowChanges(true)
autoFixSuppressionComment.setDisallowChanges(
"Nullability issue auto-patched by NullAway."
)
// TODO make this configurable
castToNonNullMethod.setDisallowChanges("slack.commons.JavaPreconditions.castToNotNull")
}
}
}
}
pluginManager.withPlugin("net.ltgt.errorprone") {
dependencies.add("errorprone", FoundryDependencies.ErrorProne.core)
val isAndroidProject = isAndroid
tasks.withType(JavaCompile::class.java).configureEach {
options.errorprone {
disableWarningsInGeneratedCode.setDisallowChanges(true)
// The EP flag alone isn't enough
// https://github.com/google/error-prone/issues/2092
excludedPaths.setDisallowChanges(".*/build/generated/.*")
disable("HidingField")
error(*foundryTools().globalConfig.errorProneCheckNamesAsErrors.toTypedArray())
if (isAndroidProject) {
options.compilerArgs.add("-XDandroidCompatible=true")
}
// Enable autopatching via "-PepAutoPatch=true". This patches in-place and requires a
// recompilation after.
// This could be useful to enable on CI + a git porcelain check to see if there's any
// patchable error prone
// fixes.
if (autoPatchEnabled) {
// Always log this verbosely
logger.lifecycle("Enabling error-prone auto-patching on ${project.path}:$name")
errorproneArgs.addAll(
"-XepPatchChecks:${ERROR_PRONE_CHECKS.joinToString(",")}",
"-XepPatchLocation:IN_PLACE",
)
}
}
}
}
}
@Suppress("LongMethod")
private fun Project.configureAndroidProjects(
foundryExtension: FoundryExtension,
foundryProperties: FoundryProperties,
) {
val javaVersion = foundryProperties.versions.jvmTarget.map(JavaVersion::toVersion)
// Contribute these libraries to Fladle if they opt into it
val androidTestApksPublisher =
Publisher.interProjectPublisher(project, FoundryArtifact.ANDROID_TEST_APK_DIRS)
val projectPath = project.path
val isAffectedProject =
foundryTools.globalConfig.affectedProjects?.contains(projectPath) ?: true
val skippyAndroidTestProjectPublisher =
Publisher.interProjectPublisher(project, FoundryArtifact.SKIPPY_ANDROID_TEST_PROJECT)
val commonComponentsExtension =
Action> {
val variantsToDisable =
foundryProperties.disabledVariants?.splitToSequence(",")?.associate {
val (flavorName, buildType) = it.split("+")
flavorName to buildType
} ?: emptyMap()
if (variantsToDisable.isNotEmpty()) {
logger.debug("$LOG Disabling variants: $variantsToDisable")
val isApp = this is ApplicationAndroidComponentsExtension
for ((flavorName, buildType) in variantsToDisable) {
val selector =
selector().withBuildType(buildType).withFlavor("environment" to flavorName)
beforeVariants(selector) { builder ->
builder.enable = false
// AGP has confusing declaration mismatches about this deprecation so we cast it
if (builder is HasUnitTestBuilder) {
(builder as HasUnitTestBuilder).enableUnitTest = false
}
if (builder is HasAndroidTestBuilder) {
builder.androidTest.enable = false
}
}
}
if (isApp) {
beforeVariants { builder ->
// AGP has confusing declaration mismatches about this deprecation so we cast it
if (builder is HasUnitTestBuilder) {
(builder as HasUnitTestBuilder).enableUnitTest = false
}
}
}
}
// Configure androidTest
onVariants { variant ->
val isLibraryVariant = variant is LibraryVariant
val excluded =
isLibraryVariant &&
foundryExtension.androidHandler.featuresHandler.androidTestExcludeFromFladle
.getOrElse(false)
val isAndroidTestEnabled = variant is HasAndroidTest && variant.androidTest != null
if (isAndroidTestEnabled) {
if (!excluded && isAffectedProject) {
// Note this intentionally just uses the same task each time as they always produce
// the same output
SimpleFileProducerTask.registerOrConfigure(
project,
name = "androidTestProjectMetadata",
description =
"Produces a metadata artifact indicating this project path produces an androidTest APK.",
input = projectPath,
group = "skippy",
)
.publishWith(skippyAndroidTestProjectPublisher)
if (isLibraryVariant) {
(variant as LibraryVariant).androidTest?.artifacts?.get(SingleArtifact.APK)?.let {
apkArtifactsDir ->
// Wire this up to the aggregator. No need for an intermediate task here.
androidTestApksPublisher.publishDirs(apkArtifactsDir)
}
}
} else {
val reason = if (excluded) "excluded" else "not affected"
val taskPath = "${projectPath}:androidTest"
val log = "$LOG Skipping $taskPath because it is $reason."
foundryTools.logAvoidedTask(AndroidTestApksTask.NAME, taskPath)
if (foundryProperties.debug) {
project.logger.lifecycle(log)
} else {
project.logger.debug(log)
}
}
}
}
}
val sdkVersions = lazy { foundryProperties.requireAndroidSdkProperties() }
val shouldApplyCacheFixPlugin = foundryProperties.enableAndroidCacheFix
val commonBaseExtensionConfig: BaseExtension.(applyTestOptions: Boolean) -> Unit =
{ applyTestOptions ->
if (shouldApplyCacheFixPlugin) {
pluginManager.apply("org.gradle.android.cache-fix")
}
compileSdkVersion(sdkVersions.value.compileSdk)
foundryProperties.ndkVersion?.let { ndkVersion = it }
foundryProperties.buildToolsVersionOverride?.let { buildToolsVersion = it }
defaultConfig {
// TODO this won't work with SDK previews but will fix in a followup
minSdk = sdkVersions.value.minSdk
vectorDrawables.useSupportLibrary = true
// Default to the standard android runner, but note this is overridden in :app
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = javaVersion.get()
targetCompatibility = javaVersion.get()
isCoreLibraryDesugaringEnabled = true
}
dependencies.add(
Configurations.CORE_LIBRARY_DESUGARING,
versionCatalog.findLibrary("google-coreLibraryDesugaring").get(),
)
if (applyTestOptions) {
testOptions {
animationsDisabled = true
if (foundryProperties.useOrchestrator) {
logger.info(
"[android.testOptions]: Configured to run tests with Android Test Orchestrator"
)
execution = "ANDROIDX_TEST_ORCHESTRATOR"
} else {
logger.debug(
"[android.testOptions]: Configured to run tests without Android Test Orchestrator"
)
}
// Added to avoid unimplemented exceptions in some of the unit tests that have simple
// android dependencies like checking whether code is running on main thread.
// See https://developer.android.com/training/testing/unit-testing/local-unit-tests
// #error-not-mocked for more details
unitTests.isReturnDefaultValues = true
if (foundryProperties.alwaysEnableResourcesInTests) {
unitTests.isIncludeAndroidResources = true
}
// Configure individual Tests tasks.
if (globalProperties.versions.robolectric != null) {
unitTests.all { test ->
//
// Note that we can't configure this to _just_ be enabled for robolectric projects
// based on dependencies unfortunately, as the task graph is already wired by the
// time dependencies start getting resolved.
//
foundryProperties.versions.robolectric?.let {
logger.debug("Configuring $name test task to depend on Robolectric jar downloads")
// Depending on the root project task by name alone is ok for Project Isolation
test.dependsOn(":${UpdateRobolectricJarsTask.NAME}")
}
// Necessary for some OkHttp-using tests to work on JDK 11 in Robolectric
// https://github.com/robolectric/robolectric/issues/5115
test.systemProperty("javax.net.ssl.trustStoreType", "JKS")
}
}
}
}
}
val objenesis2Version = foundryProperties.versions.objenesis
val prepareAndroidTestConfigurations = {
configurations.configureEach {
if (name.contains("androidTest", ignoreCase = true)) {
// Cover for https://github.com/Kotlin/kotlinx.coroutines/issues/2023
exclude(mapOf("group" to "org.jetbrains.kotlinx", "module" to "kotlinx-coroutines-debug"))
objenesis2Version?.let {
// Cover for https://github.com/mockito/mockito/pull/2024, as objenesis 3.x is not
// compatible with Android SDK <26
resolutionStrategy.force("org.objenesis:objenesis:$it")
}
}
}
}
pluginManager.withPlugin("com.android.base") {
tasks.withType(JavaPreCompileTask::class.java).configureEach {
doFirst {
// JavaPreCompileTask incorrectly reads annotation processors from the ksp classpath
// and then warns about them ending up in the JavaCompile tasks even though they're
// not on the classpath. This works around that by clearing out that field before it
// tries to merge them in with annotationProcessorArtifacts.
// https://issuetracker.google.com/issues/331806519
CACHED_KSP_ARTIFACTS_FIELD.set(this, null)
}
}
}
pluginManager.withPlugin("com.android.test") {
configure {
foundryExtension.setAndroidExtension(this)
commonBaseExtensionConfig(false)
defaultConfig { targetSdk = sdkVersions.value.targetSdk }
}
}
pluginManager.withPlugin("com.android.application") {
prepareAndroidTestConfigurations()
configure {
commonComponentsExtension.execute(this)
// TODO maybe we want to disable release androidTest by default? (i.e. for slack kit
// playground, samples, etc)
// TODO would be nice if we could query just non-debuggable build types.
// Disable androidTest tasks unless they opt-in
beforeVariants { builder ->
// Disable unit tests on release variants, since it's unused
if (builder.buildType == "release") {
// AGP has confusing declaration mismatches about this deprecation so we cast it
(builder as HasUnitTestBuilder).enableUnitTest = false
}
// Must be in the beforeVariants block to defer read until after evaluation
val androidTestEnabled =
foundryExtension.androidHandler.featuresHandler.androidTest.getOrElse(false)
val variantEnabled =
androidTestEnabled &&
foundryExtension.androidHandler.featuresHandler.androidTestAllowedVariants.orNull
?.contains(builder.name) ?: true
logger.debug("$LOG AndroidTest for ${builder.name} enabled? $variantEnabled")
builder.androidTest.enable = variantEnabled
}
onVariants(selector().withBuildType("release")) { variant ->
// Metadata for coroutines not relevant to release builds
variant.packaging.resources.excludes.add("DebugProbesKt.bin")
}
}
configure {
foundryExtension.setAndroidExtension(this)
commonBaseExtensionConfig(true)
defaultConfig {
// TODO this won't work with SDK previews but will fix in a followup
targetSdk = sdkVersions.value.targetSdk
}
packaging {
resources.excludes +=
setOf(
"META-INF/LICENSE.txt",
"META-INF/LICENSE",
"META-INF/NOTICE.txt",
".readme",
"META-INF/maven/com.google.guava/guava/pom.properties",
"META-INF/maven/com.google.guava/guava/pom.xml",
"META-INF/DEPENDENCIES",
"**/*.pro",
"**/*.proto",
// Weird bazel build metadata brought in by Tink
"build-data.properties",
"LICENSE_*",
// We don't know where this comes from but it's 5MB
// https://slack-pde.slack.com/archives/C8EER3C04/p1621353426001500
"annotated-jdk/**",
)
jniLibs.pickFirsts +=
setOf(
// Some libs like Flipper bring their own copy of common native libs (like C++) and we
// need to de-dupe
"**/*.so"
)
}
buildTypes {
getByName("debug") {
// For upstream android libraries that just have a single release variant, use that.
matchingFallbacks += "release"
// Debug should be the default build type. This helps inform studio.
isDefault = true
}
}
signingConfigs.configureEach {
enableV3Signing = true
enableV4Signing = true
}
PermissionChecks.configure(
project = project,
allowListActionGetter = { foundryExtension.androidHandler.appHandler.allowlistAction },
) { taskName, file, allowListProvider ->
tasks.register(taskName) {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description =
"Checks merged manifest permissions against a known allowlist of permissions."
permissionAllowlistFile.setDisallowChanges(file)
permissionAllowlist.setDisallowChanges(allowListProvider)
}
}
pluginManager.withPlugin("com.bugsnag.android.gradle") {
val branchMatchesPatternProvider =
foundryProperties.bugsnagEnabledBranchPattern.zip(gitBranchProvider()) { pattern, branch
->
if (pattern == null || branch == null) {
return@zip false
}
pattern.toRegex().matches(branch)
}
val enabledProvider =
foundryProperties.bugsnagEnabled.orElse(branchMatchesPatternProvider).orElse(false).zip(
provider { isCi }
) { enabled, isRunningOnCi ->
// Only enable if we're also on CI
enabled && isRunningOnCi
}
configure {
variantFilter {
// disables plugin for all debug variants
// This is technically the default behavior, buuuuut let's be super sure eh?
if (name.contains("debug", ignoreCase = true)) {
setEnabled(false)
}
}
// 5 minute timeout because let's be real, if it's taking this long something is wrong
requestTimeoutMs.setDisallowChanges(FIVE_MINUTES_MS)
// Enable uploads if the enable prop is enabled or the branch matches a provided pattern
// Note we _don't_ use the BugsnagPluginExtension.enabled property itself because we do
// want bugsnag to do most of its regular process, just skipping uploads unless enabled.
uploadJvmMappings.setDisallowChanges(enabledProvider)
reportBuilds.setDisallowChanges(enabledProvider)
// We don't use these
uploadNdkMappings.setDisallowChanges(false)
uploadNdkUnityLibraryMappings.setDisallowChanges(false)
uploadReactNativeMappings.setDisallowChanges(false)
}
}
}
foundryExtension.androidHandler.applyTo(project)
}
pluginManager.withPlugin("com.android.library") {
prepareAndroidTestConfigurations()
val isLibraryWithVariants = foundryProperties.libraryWithVariants
configure {
commonComponentsExtension.execute(this)
if (!isLibraryWithVariants) {
beforeVariants { variant ->
when (variant.buildType) {
"debug" -> {
// Even in AGP 4 we can't fully remove this yet due to
// https://issuetracker.google.com/issues/153684320
variant.enable = false
}
}
}
}
// Disable androidTest tasks in libraries unless they opt-in
beforeVariants { builder ->
val androidTestEnabled =
foundryExtension.androidHandler.featuresHandler.androidTest.getOrElse(false)
val variantEnabled =
androidTestEnabled &&
foundryExtension.androidHandler.featuresHandler.androidTestAllowedVariants.orNull
?.contains(builder.name) ?: true
builder.androidTest.enable = variantEnabled
if (variantEnabled) {
// Ensure there's a manifest file present and has its debuggable flag set correctly
if (
foundryProperties.strictMode && foundryProperties.strictValidateAndroidTestManifest
) {
val manifest = project.file("src/androidTest/AndroidManifest.xml")
check(manifest.exists()) {
"AndroidManifest.xml is missing from src/androidTest. Ensure it exists and also is set to debuggable!"
}
check(manifest.readText().contains("android:debuggable=\"true\"")) {
"AndroidManifest.xml in src/androidTest is missing the debuggable flag! Ensure it is set to 'android:debuggable=\"true\"'"
}
}
}
}
// namespace is not a property but we can hook into DSL finalizing to set it at the end
// if the build script didn't declare one prior
finalizeDsl { libraryExtension ->
if (libraryExtension.namespace == null) {
libraryExtension.namespace =
foundryProperties.defaultNamespacePrefix +
projectPath
.asSequence()
.mapNotNull {
when (it) {
// Skip dashes and underscores. We could camelcase but it looks weird in a
// package name
'-',
'_' -> null
// Use the project path as the real dot namespacing
':' -> '.'
else -> it
}
}
.joinToString("")
}
}
}
configure {
foundryExtension.setAndroidExtension(this)
commonBaseExtensionConfig(true)
if (isLibraryWithVariants) {
buildTypes {
getByName("debug") {
// For upstream android libraries that just have a single release variant, use that.
matchingFallbacks += "release"
// Debug should be the default build type. This helps inform studio.
isDefault = true
}
}
} else {
buildTypes {
getByName("release") {
// Release should be the default build type. This helps inform studio.
isDefault = true
}
}
// Default testBuildType is "debug", but AGP doesn't relocate the testBuildType to
// "release" automatically even if there's only one.
testBuildType = "release"
}
// We don't set targetSdkVersion in libraries since this is controlled by the app.
}
foundryExtension.androidHandler.applyTo(project)
}
}
companion object {
private val CACHED_KSP_ARTIFACTS_FIELD =
JavaPreCompileTask::class.java.getDeclaredField("kspProcessorArtifacts").apply {
isAccessible = true
}
/** Top-level JVM plugin IDs. Usually only one of these is applied. */
private val JVM_PLUGINS =
setOf(
"application",
"java",
"java-library",
"org.jetbrains.kotlin.jvm",
"com.android.library",
"com.android.application",
"com.android.test",
)
}
}
/** A simple context for the current configuration being processed. */
internal data class ConfigurationContext(val project: Project, val configuration: Configuration) {
val isKaptConfiguration = configuration.name.endsWith("kapt", ignoreCase = true)
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy