diff --git a/src/functionalTest/groovy/com/autonomousapps/android/AbiExclusionsSpec.groovy b/src/functionalTest/groovy/com/autonomousapps/android/AbiExclusionsSpec.groovy new file mode 100644 index 000000000..6a1962e8e --- /dev/null +++ b/src/functionalTest/groovy/com/autonomousapps/android/AbiExclusionsSpec.groovy @@ -0,0 +1,26 @@ +// Copyright (c) 2025. Tony Robalik. +// SPDX-License-Identifier: Apache-2.0 +package com.autonomousapps.android + +import com.autonomousapps.android.projects.AbiExcludedVariantProject + +import static com.autonomousapps.utils.Runner.build +import static com.google.common.truth.Truth.assertThat + +final class AbiExclusionsSpec extends AbstractAndroidSpec { + + def "can exclude variant from ABI analysis (#gradleVersion AGP #agpVersion)"() { + given: + def project = new AbiExcludedVariantProject(agpVersion) + gradleProject = project.gradleProject + + when: + build(gradleVersion, gradleProject.rootDir, 'buildHealth') + + then: + assertThat(project.actualBuildHealth()).containsExactlyElementsIn(project.expectedBuildHealth) + + where: + [gradleVersion, agpVersion] << gradleAgpMatrix() + } +} diff --git a/src/functionalTest/groovy/com/autonomousapps/android/projects/AbiExcludedVariantProject.groovy b/src/functionalTest/groovy/com/autonomousapps/android/projects/AbiExcludedVariantProject.groovy new file mode 100644 index 000000000..c1c45dd3f --- /dev/null +++ b/src/functionalTest/groovy/com/autonomousapps/android/projects/AbiExcludedVariantProject.groovy @@ -0,0 +1,93 @@ +// Copyright (c) 2025. Tony Robalik. +// SPDX-License-Identifier: Apache-2.0 +package com.autonomousapps.android.projects + +import com.autonomousapps.kit.GradleProject +import com.autonomousapps.kit.Source +import com.autonomousapps.kit.SourceType +import com.autonomousapps.model.Advice +import com.autonomousapps.model.AndroidScore +import com.autonomousapps.model.ModuleAdvice +import com.autonomousapps.model.PluginAdvice +import com.autonomousapps.model.ProjectAdvice + +import static com.autonomousapps.AdviceHelper.* +import static com.autonomousapps.kit.gradle.dependencies.Dependencies.commonsCollections + +final class AbiExcludedVariantProject extends AbstractAndroidProject { + + final GradleProject gradleProject + + AbiExcludedVariantProject(String agpVersion) { + super(agpVersion) + this.gradleProject = build() + } + + private GradleProject build() { + return newAndroidGradleProjectBuilder(agpVersion) + .withRootProject { root -> + root.withBuildScript { bs -> + bs.withGroovy("""\ + dependencyAnalysis { + abi { + exclusions { + excludeVariants('release') + } + } + }""" + ) + } + } + .withAndroidSubproject('lib') { l -> + l.sources = libSources + l.manifest = libraryManifest() + l.withBuildScript { bs -> + bs.plugins = androidLibWithKotlin + bs.android = defaultAndroidLibBlock() + bs.dependencies = [ + // This dependency would normally cause an unused dependency warning + // but we're excluding the release variant from analysis + commonsCollections("releaseImplementation") + ] + } + } + .write() + } + + private List libSources = [ + // Only has code in debug source set - no code in main source set + new Source( + SourceType.KOTLIN, "DebugLibrary", "com/example/lib", + """\ + package com.example.lib + + class DebugLibrary + """.stripIndent(), + "debug" + ) + ] + + Set actualBuildHealth() { + return actualProjectAdvice(gradleProject) + } + + private final AndroidScore androidScore = androidScoreBuilder().with { + hasAndroidAssets = false + hasAndroidRes = false + usesAndroidClasses = false + hasBuildConfig = false + hasAndroidDependencies = false + hasBuildTypeSourceSplits = true + build() + } + + final Set expectedBuildHealth = [ + projectAdvice( + ':lib', + [] as Set, + [] as Set, + [androidScore] as Set, + false + ), + ] +} diff --git a/src/main/kotlin/com/autonomousapps/extension/AbiHandler.kt b/src/main/kotlin/com/autonomousapps/extension/AbiHandler.kt index 9ac10e9b6..24be3be06 100644 --- a/src/main/kotlin/com/autonomousapps/extension/AbiHandler.kt +++ b/src/main/kotlin/com/autonomousapps/extension/AbiHandler.kt @@ -19,6 +19,7 @@ import javax.inject.Inject * abi { * exclusions { * excludeSourceSets(/* source sets to exclude from ABI analysis */) + * excludeVariants(/* variants to exclude from ABI analysis */) * * ignoreSubPackage("internal") * ignoreInternalPackages() @@ -46,6 +47,7 @@ abstract class ExclusionsHandler @Inject constructor(objects: ObjectFactory) { internal val annotationExclusions = objects.setProperty().convention(emptySet()) internal val pathExclusions = objects.setProperty().convention(emptySet()) internal val excludedSourceSets = objects.setProperty().convention(emptySet()) + internal val excludedVariants = objects.setProperty().convention(emptySet()) /** * Exclude the given [sourceSets] from ABI analysis, which means that regardless of the level of exposure of any given @@ -55,6 +57,14 @@ abstract class ExclusionsHandler @Inject constructor(objects: ObjectFactory) { excludedSourceSets.addAll(*sourceSets) } + /** + * Exclude the given [variants] from ABI analysis. This is useful for Android projects that have only debug sources + * and analyzing the release configuration is not useful. + */ + fun excludeVariants(vararg variants: String) { + excludedVariants.addAll(*variants) + } + /** * Exclude "internal" packages from ABI analysis, which means that any class in a package that contains ".internal." * will not be considered as part of the module's ABI. diff --git a/src/main/kotlin/com/autonomousapps/subplugin/ProjectPlugin.kt b/src/main/kotlin/com/autonomousapps/subplugin/ProjectPlugin.kt index 304bf80f3..b4d7995a8 100644 --- a/src/main/kotlin/com/autonomousapps/subplugin/ProjectPlugin.kt +++ b/src/main/kotlin/com/autonomousapps/subplugin/ProjectPlugin.kt @@ -236,7 +236,8 @@ internal class ProjectPlugin(private val project: Project) { /** Has the `com.android.application` plugin applied. */ private fun Project.configureAndroidAppProject() { val project = this - val ignoredVariantNames = androidIgnoredVariants() + val ignoredVariantNames = androidIgnoredVariants() + + dagpExtension.abiHandler.exclusionsHandler.excludedVariants.get() val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java) // val newAgpVersion = androidComponents.pluginVersion.toString().removePrefix("Android Gradle Plugin version ") @@ -312,7 +313,8 @@ internal class ProjectPlugin(private val project: Project) { /** Has the `com.android.library` plugin applied. */ private fun Project.configureAndroidLibProject() { val project = this - val ignoredVariantNames = androidIgnoredVariants() + val ignoredVariantNames = androidIgnoredVariants() + + dagpExtension.abiHandler.exclusionsHandler.excludedVariants.get() val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java) // val newAgpVersion = androidComponents.pluginVersion.toString().removePrefix("Android Gradle Plugin version ") @@ -391,7 +393,8 @@ internal class ProjectPlugin(private val project: Project) { /** Has the `com.android.test` plugin applied. */ private fun Project.configureAndroidTestProject() { val project = this - val ignoredVariantNames = androidIgnoredVariants() + val ignoredVariantNames = androidIgnoredVariants() + + dagpExtension.abiHandler.exclusionsHandler.excludedVariants.get() val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java) // val newAgpVersion = androidComponents.pluginVersion.toString().removePrefix("Android Gradle Plugin version ")