blob: 6f0bbdaf7a70c16c0552876dbeb7fa8214a5e86e [file] [log] [blame]
/*
* Copyright 2018 The Android Open Source Project
*
* 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
*
* http://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.
*/
package androidx.build
import androidx.benchmark.gradle.BenchmarkPlugin
import androidx.build.AndroidXPlugin.Companion.CHECK_RELEASE_READY_TASK
import androidx.build.AndroidXPlugin.Companion.TASK_TIMEOUT_MINUTES
import androidx.build.SupportConfig.BUILD_TOOLS_VERSION
import androidx.build.SupportConfig.COMPILE_SDK_VERSION
import androidx.build.SupportConfig.DEFAULT_MIN_SDK_VERSION
import androidx.build.SupportConfig.INSTRUMENTATION_RUNNER
import androidx.build.SupportConfig.TARGET_SDK_VERSION
import androidx.build.dependencyTracker.AffectedModuleDetector
import androidx.build.dokka.Dokka.configureAndroidProjectForDokka
import androidx.build.dokka.Dokka.configureJavaProjectForDokka
import androidx.build.gradle.getByType
import androidx.build.gradle.isRoot
import androidx.build.jacoco.Jacoco
import androidx.build.license.configureExternalDependencyLicenseCheck
import androidx.build.checkapi.JavaApiTaskConfig
import androidx.build.checkapi.LibraryApiTaskConfig
import androidx.build.checkapi.configureProjectForApiTasks
import androidx.build.studio.StudioTask
import com.android.build.api.artifact.ArtifactType
import com.android.build.api.artifact.Artifacts
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.CommonExtension
import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.LibraryPlugin
import com.android.build.gradle.TestedExtension
import com.android.build.gradle.api.ApkVariant
import org.gradle.api.JavaVersion.VERSION_1_8
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.bundling.Zip
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.javadoc.Javadoc
import org.gradle.api.tasks.testing.Test
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.extra
import org.gradle.kotlin.dsl.findByType
import org.gradle.kotlin.dsl.getPlugin
import org.gradle.testing.jacoco.plugins.JacocoPluginExtension
import org.gradle.testing.jacoco.tasks.JacocoReport
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinBasePluginWrapper
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.File
import java.time.Duration
import java.util.Locale
import java.util.concurrent.ConcurrentHashMap
/**
* A plugin which enables all of the Gradle customizations for AndroidX.
* This plugin reacts to other plugins being added and adds required and optional functionality.
*/
class AndroidXPlugin : Plugin<Project> {
override fun apply(project: Project) {
if (project.isRoot) throw Exception("Root project should use AndroidXRootPlugin instead")
// This has to be first due to bad behavior by DiffAndDocs which is triggered on the root
// project. It calls evaluationDependsOn on each subproject. This eagerly causes evaluation
// *during* the root build.gradle evaluation. The subproject then applies this plugin (while
// we're still halfway through applying it on the root). The check licenses code runs on the
// subproject which then looks for the root project task to add itself as a dependency of.
// Without the root project having created the task prior to DiffAndDocs running this fails.
// TODO(alanv): do not use evaluationDependsOn in DiffAndDocs to break this cycle!
project.configureExternalDependencyLicenseCheck()
val extension = project.extensions.create<AndroidXExtension>(EXTENSION_NAME, project)
// This has to be first due to bad behavior by DiffAndDocs. It fails if this configuration
// is called after DiffAndDocs.configureDiffAndDocs. b/129762955
project.configureMavenArtifactUpload(extension)
if (project.isCoverageEnabled()) {
project.configureJacoco()
}
// Perform different actions based on which plugins have been applied to the project.
// Many of the actions overlap, ex. API tracking and documentation.
project.plugins.all { plugin ->
when (plugin) {
is JavaPlugin -> configureWithJavaPlugin(project, extension)
is LibraryPlugin -> configureWithLibraryPlugin(project, extension)
is AppPlugin -> configureWithAppPlugin(project, extension)
is KotlinBasePluginWrapper -> configureWithKotlinPlugin(project, extension, plugin)
}
}
project.configureKtlint()
// Configure all Jar-packing tasks for hermetic builds.
project.tasks.withType(Jar::class.java).configureEach { it.configureForHermeticBuild() }
project.tasks.withType(Copy::class.java).configureEach { it.configureForHermeticBuild() }
// copy host side test results to DIST
project.tasks.withType(Test::class.java) { task -> configureTestTask(project, task) }
project.configureTaskTimeouts()
}
/**
* Disables timestamps and ensures filesystem-independent archive ordering to maximize
* cross-machine byte-for-byte reproducibility of artifacts.
*/
private fun Jar.configureForHermeticBuild() {
isReproducibleFileOrder = true
isPreserveFileTimestamps = false
}
private fun Copy.configureForHermeticBuild() {
duplicatesStrategy = DuplicatesStrategy.FAIL
}
private fun configureTestTask(project: Project, task: Test) {
AffectedModuleDetector.configureTaskGuard(task)
val xmlReportDestDir = project.getHostTestResultDirectory()
val archiveName = "${project.asFilenamePrefix()}_${task.name}.zip"
if (project.isDisplayTestOutput()) {
// Enable tracing to see results in command line
task.testLogging.apply {
events = hashSetOf(
TestLogEvent.FAILED, TestLogEvent.PASSED,
TestLogEvent.SKIPPED, TestLogEvent.STANDARD_OUT
)
showExceptions = true
showCauses = true
showStackTraces = true
exceptionFormat = TestExceptionFormat.FULL
}
} else {
val htmlReport = task.reports.html
val zipHtmlTask = project.tasks.register(
"zipHtmlResultsOf${task.name.capitalize()}",
Zip::class.java
) {
val destinationDirectory = File("$xmlReportDestDir-html")
it.destinationDirectory.set(destinationDirectory)
it.archiveFileName.set(archiveName)
it.doLast {
// If the test itself didn't display output, then the report task should
// remind the user where to find its output
project.logger.lifecycle(
"Html results of ${task.name} zipped into " +
"$destinationDirectory/$archiveName"
)
}
}
task.finalizedBy(zipHtmlTask)
task.doFirst {
zipHtmlTask.configure {
it.from(htmlReport.destination)
}
}
val xmlReport = task.reports.junitXml
if (xmlReport.isEnabled) {
val zipXmlTask = project.tasks.register(
"zipXmlResultsOf${task.name.capitalize()}",
Zip::class.java
) {
it.destinationDirectory.set(xmlReportDestDir)
it.archiveFileName.set(archiveName)
}
if (project.hasProperty(TEST_FAILURES_DO_NOT_FAIL_TEST_TASK)) {
task.ignoreFailures = true
}
task.finalizedBy(zipXmlTask)
task.doFirst {
zipXmlTask.configure {
it.from(xmlReport.destination)
}
}
}
}
task.systemProperty("robolectric.offline", "true")
val robolectricDependencies =
File(project.getPrebuiltsRoot(), "androidx/external/org/robolectric/android-all")
task.systemProperty(
"robolectric.dependency.dir",
robolectricDependencies.absolutePath
)
}
private fun configureWithKotlinPlugin(
project: Project,
extension: AndroidXExtension,
plugin: KotlinBasePluginWrapper
) {
project.tasks.withType(KotlinCompile::class.java).configureEach { task ->
task.kotlinOptions.jvmTarget = "1.8"
project.configureCompilationWarnings(task)
if (project.hasProperty(EXPERIMENTAL_KOTLIN_BACKEND_ENABLED)) {
task.kotlinOptions.freeCompilerArgs += listOf("-Xuse-ir=true")
}
}
project.afterEvaluate {
if (extension.shouldEnforceKotlinStrictApiMode()) {
project.tasks.withType(KotlinCompile::class.java).configureEach { task ->
// Workaround for https://youtrack.jetbrains.com/issue/KT-37652
if (task.name.endsWith("TestKotlin")) return@configureEach
task.kotlinOptions.freeCompilerArgs += listOf("-Xexplicit-api=strict")
}
}
}
if (plugin is KotlinMultiplatformPluginWrapper) {
project.extensions.findByType<LibraryExtension>()?.apply {
configureAndroidLibraryWithMultiplatformPluginOptions()
}
}
}
private fun configureWithAppPlugin(project: Project, androidXExtension: AndroidXExtension) {
val appExtension = project.extensions.getByType<AppExtension>().apply {
configureAndroidCommonOptions(project, androidXExtension)
configureAndroidApplicationOptions(project)
}
// TODO: Replace this with a per-variant packagingOption for androidTest specifically once
// b/69953968 is resolved.
// Workaround for b/161465530 in AGP that fails to strip these <module>.kotlin_module files,
// which causes mergeDebugAndroidTestJavaResource to fail for sample apps.
appExtension.packagingOptions.exclude("/META-INF/*.kotlin_module")
// Workaround a limitation in AGP that fails to merge these META-INF license files.
appExtension.packagingOptions.pickFirst("/META-INF/AL2.0")
// In addition to working around the above issue, we exclude the LGPL2.1 license as we're
// approved to distribute code via AL2.0 and the only dependencies which pull in LGPL2.1
// are currently dual-licensed with AL2.0 and LGPL2.1. The affected dependencies are:
// - net.java.dev.jna:jna:5.5.0
appExtension.packagingOptions.exclude("/META-INF/LGPL2.1")
project.configureAndroidProjectForLint(appExtension.lintOptions, androidXExtension)
}
private fun configureWithLibraryPlugin(
project: Project,
androidXExtension: AndroidXExtension
) {
val libraryExtension = project.extensions.getByType<LibraryExtension>().apply {
configureAndroidCommonOptions(project, androidXExtension)
configureAndroidLibraryOptions(project, androidXExtension)
}
try {
val libraryAndroidComponentsExtensionType =
Class.forName("com.android.build.api.extension.LibraryAndroidComponentsExtension")
val androidComponentsExtension =
project.extensions.getByType(libraryAndroidComponentsExtensionType)
val selectorType = Class.forName("com.android.build.api.extension.VariantSelector")
val selector = libraryAndroidComponentsExtensionType.getMethod("selector")
.invoke(androidComponentsExtension)
libraryAndroidComponentsExtensionType
.getMethod("beforeUnitTest", selectorType, Function1::class.java)
.invoke(
androidComponentsExtension,
selectorType.getMethod("withBuildType", String::class.java)
.invoke(selector, "release"),
{ unitTest: Any ->
unitTest.javaClass.getMethod("setEnabled", Boolean::class.java)
.invoke(unitTest, false)
}
)
} catch (cnfe: ClassNotFoundException) {
// old iteration of the new API.
val allVariants = libraryExtension.javaClass.getMethod("getOnVariants")
.invoke(libraryExtension)
allVariants.javaClass.getMethod(
"withBuildType",
String::class.java,
Function1::class.java
).invoke(
allVariants,
"release",
{ variant: Any ->
variant.javaClass.getMethod(
"unitTest",
Function1::class.java
).invoke(
variant,
{ unitTest: Any ->
unitTest.javaClass.getMethod(
"setEnabled",
Boolean::class.java
).invoke(
unitTest,
false
)
}
)
}
)
}
// switch to this code once 4.2.0-beta1 can be used.
// project.extensions.getByType<LibraryAndroidComponentsExtension>().apply {
// beforeUnitTest(selector().withBuildType("release")) {
// // Disable unit test for release build type
// @Suppress("UnstableApiUsage")
// enabled = false
// }
// }
libraryExtension.packagingOptions {
// TODO: Replace this with a per-variant packagingOption for androidTest specifically
// once b/69953968 is resolved.
// Workaround for b/161465530 in AGP that fails to merge these META-INF license files
// for libraries that publish Java resources under the same name.
pickFirst("/META-INF/AL2.0")
// In addition to working around the above issue, we exclude the LGPL2.1 license as we're
// approved to distribute code via AL2.0 and the only dependencies which pull in LGPL2.1
// currently are dual-licensed with AL2.0 and LGPL2.1. The affected dependencies are:
// - net.java.dev.jna:jna:5.5.0
exclude("/META-INF/LGPL2.1")
// We need this as a work-around for b/155721209
// It can be removed when we have a newer plugin version
// 2nd workaround - this DSL was made saner in a breaking way which hasn't landed
// yes in AGP 4.1, that will allow just excludes -= "...".
// This reflection enables us to be source compatible with both for now.
javaClass.getMethod("setExcludes", Set::class.java).invoke(
this,
excludes.also {
it.remove("/META-INF/*.kotlin_module")
}
)
check(!excludes.contains("/META-INF/*.kotlin_module"))
}
project.configureSourceJarForAndroid(libraryExtension)
project.configureVersionFileWriter(libraryExtension, androidXExtension)
project.addCreateLibraryBuildInfoFileTask(androidXExtension)
val verifyDependencyVersionsTask = project.createVerifyDependencyVersionsTask()
val checkReleaseReadyTasks = mutableListOf<TaskProvider<out Task>>()
if (verifyDependencyVersionsTask != null) {
checkReleaseReadyTasks.add(verifyDependencyVersionsTask)
}
if (checkReleaseReadyTasks.isNotEmpty()) {
project.createCheckReleaseReadyTask(checkReleaseReadyTasks)
}
val reportLibraryMetrics = project.tasks.register<ReportLibraryMetricsTask>(
REPORT_LIBRARY_METRICS_TASK, ReportLibraryMetricsTask::class.java
)
project.addToBuildOnServer(reportLibraryMetrics)
libraryExtension.defaultPublishVariant { libraryVariant ->
reportLibraryMetrics.configure {
it.jarFiles.from(
libraryVariant.packageLibraryProvider.map { zip ->
zip.inputs.files
}
)
}
verifyDependencyVersionsTask?.configure { task ->
task.dependsOn(libraryVariant.javaCompileProvider)
}
libraryVariant.javaCompileProvider.configure { task ->
project.configureCompilationWarnings(task)
}
}
// Standard lint, docs, resource API, and Metalava configuration for AndroidX projects.
project.configureAndroidProjectForLint(libraryExtension.lintOptions, androidXExtension)
if (project.isDocumentationEnabled()) {
project.configureAndroidProjectForDokka(libraryExtension, androidXExtension)
}
project.configureProjectForApiTasks(
LibraryApiTaskConfig(libraryExtension),
androidXExtension
)
project.addToProjectMap(androidXExtension)
}
private fun configureWithJavaPlugin(project: Project, extension: AndroidXExtension) {
project.configureErrorProneForJava()
project.configureSourceJarForJava()
// Force Java 1.8 source- and target-compatibilty for all Java libraries.
val convention = project.convention.getPlugin<JavaPluginConvention>()
convention.apply {
sourceCompatibility = VERSION_1_8
targetCompatibility = VERSION_1_8
}
project.tasks.withType(JavaCompile::class.java) { task ->
project.configureCompilationWarnings(task)
}
project.hideJavadocTask()
val verifyDependencyVersionsTask = project.createVerifyDependencyVersionsTask()
verifyDependencyVersionsTask?.configure { task ->
task.dependsOn(project.tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME))
}
project.addCreateLibraryBuildInfoFileTask(extension)
if (verifyDependencyVersionsTask != null) {
project.createCheckReleaseReadyTask(listOf(verifyDependencyVersionsTask))
}
// Standard lint, docs, and Metalava configuration for AndroidX projects.
project.configureNonAndroidProjectForLint(extension)
if (project.isDocumentationEnabled()) {
project.configureJavaProjectForDokka(extension)
}
project.configureProjectForApiTasks(
JavaApiTaskConfig,
extension
)
project.afterEvaluate {
if (extension.type.publish.shouldRelease()) {
project.extra.set("publish", true)
}
}
// Workaround for b/120487939 wherein Gradle's default resolution strategy prefers external
// modules with lower versions over local projects with higher versions.
project.configurations.all { configuration ->
configuration.resolutionStrategy.preferProjectModules()
}
project.addToProjectMap(extension)
}
private fun TestedExtension.configureAndroidCommonOptions(
project: Project,
androidXExtension: AndroidXExtension
) {
compileOptions.apply {
sourceCompatibility = VERSION_1_8
targetCompatibility = VERSION_1_8
}
// Force AGP to use our version of JaCoCo
jacoco.version = Jacoco.VERSION
compileSdkVersion(COMPILE_SDK_VERSION)
buildToolsVersion = BUILD_TOOLS_VERSION
defaultConfig.targetSdkVersion(TARGET_SDK_VERSION)
defaultConfig.testInstrumentationRunner = INSTRUMENTATION_RUNNER
buildTypes.getByName("debug").isTestCoverageEnabled = project.isCoverageEnabled()
testOptions.animationsDisabled = true
testOptions.unitTests.isReturnDefaultValues = true
defaultConfig.minSdkVersion(DEFAULT_MIN_SDK_VERSION)
project.afterEvaluate {
val minSdkVersion = defaultConfig.minSdkVersion!!.apiLevel
check(minSdkVersion >= DEFAULT_MIN_SDK_VERSION) {
"minSdkVersion $minSdkVersion lower than the default of $DEFAULT_MIN_SDK_VERSION"
}
project.configurations.all { configuration ->
configuration.resolutionStrategy.eachDependency { dep ->
val target = dep.target
val version = target.version
// Enforce the ban on declaring dependencies with version ranges.
if (version != null && Version.isDependencyRange(version)) {
throw IllegalArgumentException(
"Dependency ${dep.target} declares its version as " +
"version range ${dep.target.version} however the use of " +
"version ranges is not allowed, please update the " +
"dependency to list a fixed version."
)
}
}
}
if (androidXExtension.type.compilationTarget != CompilationTarget.DEVICE) {
throw IllegalStateException(
"${androidXExtension.type.name} libraries cannot apply the android plugin, as" +
" they do not target android devices"
)
}
}
val debugSigningConfig = signingConfigs.getByName("debug")
// Use a local debug keystore to avoid build server issues.
debugSigningConfig.storeFile = project.getKeystore()
buildTypes.all { buildType ->
// Sign all the builds (including release) with debug key
buildType.signingConfig = debugSigningConfig
}
project.configureErrorProneForAndroid(variants)
// Set the officially published version to be the debug version with minimum dependency
// versions.
defaultPublishConfig(Release.DEFAULT_PUBLISH_CONFIG)
// workaround for b/120487939
project.configurations.all { configuration ->
// Gradle seems to crash on androidtest configurations
// preferring project modules...
if (!configuration.name.toLowerCase(Locale.US).contains("androidtest")) {
configuration.resolutionStrategy.preferProjectModules()
}
}
if (project.isCoverageEnabled()) {
Jacoco.registerClassFilesTask(project, this)
}
val commonExtension = project.extensions.getByType(CommonExtension::class.java)
if (hasAndroidTestSourceCode(project, this)) {
try {
val androidComponentsExtensionType =
Class.forName("com.android.build.api.extension.AndroidComponentsExtension")
val androidComponentsExtension =
project.extensions.getByType(androidComponentsExtensionType)
val selectorType = Class.forName("com.android.build.api.extension.VariantSelector")
val selector = androidComponentsExtensionType.getMethod("selector")
.invoke(androidComponentsExtension)
androidComponentsExtension.javaClass.getMethod(
"androidTest",
selectorType,
Function1::class.java
).invoke(
androidComponentsExtension,
selector.javaClass.getMethod("all").invoke(selector),
{ androidTest: Any ->
createTestConfigurationGenerationTask(
project,
androidTest.javaClass.getMethod(
"getName"
).invoke(
androidTest
) as String,
androidTest.javaClass.getMethod(
"getArtifacts"
).invoke(
androidTest
) as Artifacts
)
}
)
} catch (cnfe: ClassNotFoundException) {
commonExtension.configureTestConfigGeneration(project)
}
}
val buildTestApksTask = project.rootProject.tasks.named(BUILD_TEST_APKS_TASK)
testVariants.all { variant ->
buildTestApksTask.configure {
it.dependsOn(variant.assembleProvider)
}
variant.configureApkCopy(project, this, true)
}
}
private fun CommonExtension<*, *, *, *, *, *, *, *>
.configureTestConfigGeneration(project: Project) {
// old iteration of the new API.
javaClass.getMethod("onVariants", Function1::class.java)
.invoke(
this,
{ variant: Any ->
variant.javaClass.getMethod(
"androidTestProperties",
Function1::class.java
).invoke(
variant,
{ androidTest: Any ->
createTestConfigurationGenerationTask(
project,
androidTest.javaClass.getMethod(
"getName"
).invoke(androidTest) as String,
androidTest.javaClass.getMethod(
"getArtifacts"
).invoke(androidTest) as Artifacts
)
}
)
} as Function1<Any, Any>
)
}
private fun createTestConfigurationGenerationTask(
project: Project,
variantName: String,
artifacts: Artifacts
) {
val generateTestConfigurationTask = project.tasks.register(
"${project.name}${GENERATE_TEST_CONFIGURATION_TASK}$variantName",
GenerateTestConfigurationTask::class.java
) {
it.testFolder.set(artifacts.get(ArtifactType.APK))
it.testLoader.set(artifacts.getBuiltArtifactsLoader())
it.outputXml.fileValue(
File(
project.getTestConfigDirectory(),
"${project.asFilenamePrefix()}${variantName}AndroidTest.xml"
)
)
AffectedModuleDetector.configureTaskGuard(it)
}
project.rootProject.tasks.findByName(ZIP_TEST_CONFIGS_WITH_APKS_TASK)!!
.dependsOn(generateTestConfigurationTask)
}
private fun ApplicationExtension<*, *, *, *, *>
.addAppApkToTestConfigGeneration(project: Project) {
val allVariants = javaClass.getMethod("getOnVariantProperties")
.invoke(this)
allVariants.javaClass.getMethod("withBuildType", String::class.java, Function1::class.java)
.invoke(
allVariants,
"debug",
{ debugVariant: Any ->
val artifacts = debugVariant.javaClass.getMethod("getArtifacts")
.invoke(debugVariant) as Artifacts
project.tasks.withType(GenerateTestConfigurationTask::class.java) {
it.appFolder.set(artifacts.get(ArtifactType.APK))
it.appLoader.set(artifacts.getBuiltArtifactsLoader())
}
}
)
}
private fun hasAndroidTestSourceCode(project: Project, extension: TestedExtension): Boolean {
// check Java androidTest source set
extension.sourceSets.findByName("androidTest")?.let { sourceSet ->
// using getSourceFiles() instead of sourceFiles due to b/150800094
if (!sourceSet.java.getSourceFiles().isEmpty) return true
}
// check kotlin-android androidTest source set
project.extensions.findByType(KotlinAndroidProjectExtension::class.java)
?.sourceSets?.findByName("androidTest")?.let {
if (it.kotlin.files.isNotEmpty()) return true
}
// check kotlin-multiplatform androidAndroidTest source set
project.multiplatformExtension?.apply {
sourceSets.findByName("androidAndroidTest")?.let {
if (it.kotlin.files.isNotEmpty()) return true
}
}
return false
}
private fun ApkVariant.configureApkCopy(
project: Project,
extension: TestedExtension,
testApk: Boolean
) {
packageApplicationProvider.get().let { packageTask ->
AffectedModuleDetector.configureTaskGuard(packageTask)
// Skip copying AndroidTest apks if they have no source code (no tests to run).
if (testApk && !hasAndroidTestSourceCode(project, extension)) {
return
}
project.rootProject.tasks.named(ZIP_TEST_CONFIGS_WITH_APKS_TASK)
.configure { task ->
task as Zip
task.from(packageTask.outputDirectory)
task.dependsOn(packageTask)
}
packageTask.doLast {
project.copy {
it.from(packageTask.outputDirectory)
it.include("*.apk")
it.into(File(project.getDistributionDirectory(), "apks"))
it.rename { fileName ->
renameApkForTesting(fileName, project)
}
}
}
}
}
/**
* Guarantees unique names for the APKs, and modifies some of the suffixes. The APK name is used
* to determine what gets run by our test runner
*/
private fun renameApkForTesting(fileName: String, project: Project): String {
return if (fileName.contains("media-test") || fileName.contains("media2-test")) {
// Exclude media-test-* and media2-test-* modules from
// existing support library presubmit tests.
fileName.replace("-debug-androidTest", "")
} else if (project.plugins.hasPlugin(BenchmarkPlugin::class.java)) {
val name = fileName.replace("-androidTest", "-androidBenchmark")
"${project.asFilenamePrefix()}_$name"
} else {
"${project.asFilenamePrefix()}_$fileName"
}
}
private fun LibraryExtension.configureAndroidLibraryOptions(
project: Project,
androidXExtension: AndroidXExtension
) {
project.configurations.all { config ->
val isTestConfig = config.name.toLowerCase(Locale.US).contains("test")
config.dependencyConstraints.configureEach { dependencyConstraint ->
dependencyConstraint.apply {
// Remove strict constraints on test dependencies and listenablefuture:1.0
if (isTestConfig ||
group == "com.google.guava" &&
name == "listenablefuture" &&
version == "1.0"
) {
version { versionConstraint ->
versionConstraint.strictly("")
}
}
}
}
}
project.afterEvaluate {
if (androidXExtension.publish.shouldRelease()) {
project.extra.set("publish", true)
}
if (!project.rootProject.hasProperty(USE_MAX_DEP_VERSIONS)) {
defaultPublishVariant { libraryVariant ->
libraryVariant.javaCompileProvider.configure { javaCompile ->
if (androidXExtension.failOnDeprecationWarnings) {
javaCompile.options.compilerArgs.add("-Xlint:deprecation")
}
}
}
}
}
}
private fun TestedExtension.configureAndroidLibraryWithMultiplatformPluginOptions() {
sourceSets.findByName("main")!!.manifest.srcFile("src/androidMain/AndroidManifest.xml")
sourceSets.findByName("androidTest")!!
.manifest.srcFile("src/androidAndroidTest/AndroidManifest.xml")
}
private fun AppExtension.configureAndroidApplicationOptions(project: Project) {
defaultConfig.apply {
versionCode = 1
versionName = "1.0"
}
lintOptions.apply {
isAbortOnError = true
val baseline = project.lintBaseline
if (baseline.exists()) {
baseline(baseline)
}
}
try {
val androidComponentsExtensionType = Class.forName(
"com.android.build.api.extension.ApplicationAndroidComponentsExtension"
)
val androidComponentsExtension =
project.extensions.getByType(androidComponentsExtensionType)
val selectorType = Class.forName(
"com.android.build.api.extension.VariantSelector"
)
val selector = androidComponentsExtensionType.getMethod("selector")
.invoke(androidComponentsExtension)
androidComponentsExtensionType
.getMethod("onVariants", selectorType, Function1::class.java)
.invoke(
androidComponentsExtension,
selectorType.getMethod("withBuildType", String::class.java)
.invoke(selector, "debug"),
{ debugVariant: Any ->
val artifacts = debugVariant.javaClass.getMethod("getArtifacts")
.invoke(debugVariant) as Artifacts
project.tasks.withType(GenerateTestConfigurationTask::class.java) {
it.appFolder.set(artifacts.get(ArtifactType.APK))
it.appLoader.set(artifacts.getBuiltArtifactsLoader())
}
}
)
} catch (cnfe: ClassNotFoundException) {
val applicationExtension = project.extensions.getByType(
ApplicationExtension::class.java
)
applicationExtension.addAppApkToTestConfigGeneration(project)
}
val buildTestApksTask = project.rootProject.tasks.named(BUILD_TEST_APKS_TASK)
applicationVariants.all { variant ->
// Using getName() instead of name due to b/150427408
if (variant.buildType.name == "debug") {
buildTestApksTask.configure {
it.dependsOn(variant.assembleProvider)
}
}
variant.configureApkCopy(project, this, false)
}
}
private fun Project.createVerifyDependencyVersionsTask():
TaskProvider<VerifyDependencyVersionsTask>? {
/**
* Ignore -PuseMaxDepVersions when verifying dependency versions because it is a
* hypothetical build which is only intended to check for forward compatibility.
*/
if (hasProperty(USE_MAX_DEP_VERSIONS)) {
return null
}
val taskProvider = tasks.register(
"verifyDependencyVersions",
VerifyDependencyVersionsTask::class.java
)
addToBuildOnServer(taskProvider)
return taskProvider
}
// Task that creates a json file of a project's dependencies
private fun Project.addCreateLibraryBuildInfoFileTask(extension: AndroidXExtension) {
afterEvaluate {
if (extension.publish.shouldRelease()) {
// Only generate build info files for published libraries.
val task = tasks.register(
CREATE_LIBRARY_BUILD_INFO_FILES_TASK,
CreateLibraryBuildInfoFileTask::class.java
) {
it.outputFile.set(
File(
project.getBuildInfoDirectory(),
"${group}_${name}_build_info.txt"
)
)
}
rootProject.tasks.named(CREATE_LIBRARY_BUILD_INFO_FILES_TASK).configure {
it.dependsOn(task)
}
addTaskToAggregateBuildInfoFileTask(task)
}
}
}
private fun Project.addTaskToAggregateBuildInfoFileTask(
task: TaskProvider<CreateLibraryBuildInfoFileTask>
) {
rootProject.tasks.named(CREATE_AGGREGATE_BUILD_INFO_FILES_TASK).configure {
val aggregateLibraryBuildInfoFileTask: CreateAggregateLibraryBuildInfoFileTask = it
as CreateAggregateLibraryBuildInfoFileTask
aggregateLibraryBuildInfoFileTask.dependsOn(task)
aggregateLibraryBuildInfoFileTask.libraryBuildInfoFiles.add(
task.flatMap { task -> task.outputFile }
)
}
}
private fun Project.configureJacoco() {
apply(plugin = "jacoco")
configure<JacocoPluginExtension> {
toolVersion = Jacoco.VERSION
}
val zipEcFilesTask = Jacoco.getZipEcFilesTask(this)
tasks.withType(JacocoReport::class.java).configureEach { task ->
zipEcFilesTask.get().dependsOn(task) // zip follows every jacocoReport task being run
task.reports {
it.xml.isEnabled = true
it.html.isEnabled = false
it.csv.isEnabled = false
it.xml.destination = File(
getHostTestCoverageDirectory(),
"${project.asFilenamePrefix()}.xml"
)
}
}
}
companion object {
const val BUILD_ON_SERVER_TASK = "buildOnServer"
const val BUILD_TEST_APKS_TASK = "buildTestApks"
const val CHECK_RELEASE_READY_TASK = "checkReleaseReady"
const val CREATE_LIBRARY_BUILD_INFO_FILES_TASK = "createLibraryBuildInfoFiles"
const val CREATE_AGGREGATE_BUILD_INFO_FILES_TASK = "createAggregateBuildInfoFiles"
const val GENERATE_TEST_CONFIGURATION_TASK = "GenerateTestConfiguration"
const val REPORT_LIBRARY_METRICS_TASK = "reportLibraryMetrics"
const val ZIP_TEST_CONFIGS_WITH_APKS_TASK = "zipTestConfigsWithApks"
const val TASK_GROUP_API = "API"
const val EXTENSION_NAME = "androidx"
/**
* Fail the build if a non-Studio task runs for more than 30 minutes.
*/
const val TASK_TIMEOUT_MINUTES = 30L
/**
* Setting this property indicates that a build is being performed to check for forward
* compatibility.
*/
// TODO(alanv): This property should be prefixed with `androidx.`.
const val USE_MAX_DEP_VERSIONS = "useMaxDepVersions"
}
}
/**
* Hides a project's Javadoc tasks from the output of `./gradlew tasks` by setting their group to
* `null`.
*
* AndroidX projects do not use the Javadoc task for docs generation, so we don't want them
* cluttering up the task overview.
*/
private fun Project.hideJavadocTask() {
tasks.withType(Javadoc::class.java).configureEach {
if (it.name == "javadoc") {
it.group = null
}
}
}
private fun Project.addToProjectMap(extension: AndroidXExtension) {
// TODO(alanv): Move this out of afterEvaluate
afterEvaluate {
if (extension.publish.shouldRelease()) {
val group = extension.mavenGroup?.group
if (group != null) {
val module = "$group:$name"
@Suppress("UNCHECKED_CAST")
val projectModules = getProjectsMap()
projectModules[module] = path
}
}
}
}
val Project.multiplatformExtension
get() = extensions.findByType(KotlinMultiplatformExtension::class.java)
/**
* Creates the [CHECK_RELEASE_READY_TASK], which aggregates tasks that must pass for a
* project to be considered ready for public release.
*/
private fun Project.createCheckReleaseReadyTask(taskProviderList: List<TaskProvider<out Task>>) {
tasks.register(CHECK_RELEASE_READY_TASK) {
for (taskProvider in taskProviderList) {
it.dependsOn(taskProvider)
}
}
}
@Suppress("UNCHECKED_CAST")
fun Project.getProjectsMap(): ConcurrentHashMap<String, String> {
return rootProject.extra.get("projects") as ConcurrentHashMap<String, String>
}
/**
* Configures all non-Studio tasks in a project (see b/153193718 for background) to time out after
* [TASK_TIMEOUT_MINUTES].
*/
private fun Project.configureTaskTimeouts() {
tasks.configureEach { t ->
// skip adding a timeout for some tasks that both take a long time and
// that we can count on the user to monitor
if (t !is StudioTask) {
t.timeout.set(Duration.ofMinutes(TASK_TIMEOUT_MINUTES))
}
}
}
private fun Project.configureCompilationWarnings(task: JavaCompile) {
if (hasProperty(ALL_WARNINGS_AS_ERRORS)) {
task.options.compilerArgs.add("-Werror")
task.options.compilerArgs.add("-Xlint:unchecked")
}
}
private fun Project.configureCompilationWarnings(task: KotlinCompile) {
if (hasProperty(ALL_WARNINGS_AS_ERRORS)) {
task.kotlinOptions.allWarningsAsErrors = true
}
task.kotlinOptions.freeCompilerArgs += listOf(
"-Xskip-runtime-version-check",
"-Xskip-metadata-version-check"
)
}
/**
* Returns a string that is a valid filename and loosely based on the project name
* The value returned for each project will be distinct
*/
fun Project.asFilenamePrefix(): String {
return project.path.substring(1).replace(':', '-')
}
/**
* Sets the specified [task] as a dependency of the top-level `check` task, ensuring that it runs
* as part of `./gradlew check`.
*/
fun <T : Task> Project.addToCheckTask(task: TaskProvider<T>) {
project.tasks.named("check").configure {
it.dependsOn(task)
}
}