| /* |
| * 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.AndroidXImplPlugin.Companion.TASK_TIMEOUT_MINUTES |
| import androidx.build.Release.DEFAULT_PUBLISH_CONFIG |
| import androidx.build.buildInfo.addCreateLibraryBuildInfoFileTasks |
| import androidx.build.checkapi.AndroidMultiplatformApiTaskConfig |
| import androidx.build.checkapi.JavaApiTaskConfig |
| import androidx.build.checkapi.KmpApiTaskConfig |
| import androidx.build.checkapi.LibraryApiTaskConfig |
| import androidx.build.checkapi.configureProjectForApiTasks |
| import androidx.build.docs.CheckTipOfTreeDocsTask.Companion.setUpCheckDocsTask |
| import androidx.build.gradle.isRoot |
| import androidx.build.license.configureExternalDependencyLicenseCheck |
| import androidx.build.resources.CopyPublicResourcesDirTask |
| import androidx.build.resources.configurePublicResourcesStub |
| import androidx.build.sbom.configureSbomPublishing |
| import androidx.build.sbom.validateAllArchiveInputsRecognized |
| import androidx.build.studio.StudioTask |
| import androidx.build.testConfiguration.ModuleInfoGenerator |
| import androidx.build.testConfiguration.TestModule |
| import androidx.build.testConfiguration.addAppApkToTestConfigGeneration |
| import androidx.build.testConfiguration.configureTestConfigGeneration |
| import androidx.build.uptodatedness.TaskUpToDateValidator |
| import androidx.build.uptodatedness.cacheEvenIfNoOutputs |
| import com.android.build.api.artifact.SingleArtifact |
| import com.android.build.api.dsl.ApplicationExtension |
| import com.android.build.api.dsl.CommonExtension |
| import com.android.build.api.dsl.KotlinMultiplatformAndroidTarget |
| import com.android.build.api.dsl.KotlinMultiplatformAndroidTestOnDeviceCompilation |
| import com.android.build.api.dsl.KotlinMultiplatformAndroidTestOnJvmCompilation |
| import com.android.build.api.dsl.LibraryExtension |
| import com.android.build.api.dsl.PrivacySandboxSdkExtension |
| import com.android.build.api.dsl.TestExtension |
| import com.android.build.api.variant.AndroidComponentsExtension |
| import com.android.build.api.variant.ApplicationAndroidComponentsExtension |
| import com.android.build.api.variant.HasDeviceTests |
| import com.android.build.api.variant.HasUnitTestBuilder |
| import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension |
| import com.android.build.api.variant.LibraryAndroidComponentsExtension |
| import com.android.build.api.variant.Variant |
| import com.android.build.gradle.AppPlugin |
| import com.android.build.gradle.LibraryPlugin |
| import com.android.build.gradle.TestPlugin |
| import com.android.build.gradle.api.KotlinMultiplatformAndroidPlugin |
| import com.android.build.gradle.api.PrivacySandboxSdkPlugin |
| import com.android.build.gradle.tasks.factory.AndroidUnitTest |
| import com.android.utils.appendCapitalized |
| import java.io.File |
| import java.time.Duration |
| import java.time.LocalDateTime |
| import java.util.Locale |
| import java.util.concurrent.ConcurrentHashMap |
| import javax.inject.Inject |
| import org.gradle.api.DefaultTask |
| import org.gradle.api.GradleException |
| import org.gradle.api.JavaVersion |
| import org.gradle.api.JavaVersion.VERSION_11 |
| import org.gradle.api.JavaVersion.VERSION_17 |
| 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.artifacts.CacheableRule |
| import org.gradle.api.artifacts.ComponentMetadataContext |
| import org.gradle.api.artifacts.ComponentMetadataRule |
| import org.gradle.api.artifacts.Configuration |
| import org.gradle.api.component.SoftwareComponentFactory |
| import org.gradle.api.file.DuplicatesStrategy |
| import org.gradle.api.plugins.JavaPlugin |
| import org.gradle.api.plugins.JavaPluginExtension |
| import org.gradle.api.provider.Provider |
| import org.gradle.api.tasks.Copy |
| import org.gradle.api.tasks.TaskProvider |
| 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.AbstractTestTask |
| 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.build.event.BuildEventsListenerRegistry |
| import org.gradle.jvm.tasks.Jar |
| import org.gradle.kotlin.dsl.KotlinClosure1 |
| import org.gradle.kotlin.dsl.create |
| import org.gradle.kotlin.dsl.dependencies |
| import org.gradle.kotlin.dsl.extra |
| import org.gradle.kotlin.dsl.findByType |
| import org.gradle.kotlin.dsl.getByType |
| import org.gradle.kotlin.dsl.withModule |
| import org.gradle.kotlin.dsl.withType |
| import org.gradle.plugin.devel.plugins.JavaGradlePluginPlugin |
| import org.gradle.plugin.devel.tasks.ValidatePlugins |
| import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode |
| import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension |
| import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension |
| import org.jetbrains.kotlin.gradle.dsl.KotlinVersion |
| import org.jetbrains.kotlin.gradle.plugin.KotlinBasePluginWrapper |
| import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper |
| import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType |
| import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget |
| import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget |
| import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask |
| import org.jetbrains.kotlin.gradle.tasks.KotlinCompile |
| import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile |
| |
| /** |
| * 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. |
| */ |
| abstract class AndroidXImplPlugin |
| @Inject |
| constructor(private val componentFactory: SoftwareComponentFactory) : Plugin<Project> { |
| @get:javax.inject.Inject abstract val registry: BuildEventsListenerRegistry |
| |
| override fun apply(project: Project) { |
| if (project.isRoot) |
| throw Exception("Root project should use AndroidXRootImplPlugin instead") |
| val androidXExtension = |
| project.extensions.create<AndroidXExtension>(EXTENSION_NAME, project) |
| |
| val androidXKmpExtension = |
| project.extensions.create<AndroidXMultiplatformExtension>( |
| AndroidXMultiplatformExtension.EXTENSION_NAME, |
| project |
| ) |
| |
| project.tasks.register(BUILD_ON_SERVER_TASK, DefaultTask::class.java) |
| // Perform different actions based on which plugins have been applied to the project. |
| // Many of the actions overlap, ex. API tracking. |
| project.plugins.configureEach { plugin -> |
| @Suppress("UnstableApiUsage") // PrivacySandboxSdkPlugin, KMPAndroidPlugin |
| when (plugin) { |
| is JavaGradlePluginPlugin -> configureGradlePluginPlugin(project) |
| is JavaPlugin -> configureWithJavaPlugin(project, androidXExtension) |
| is LibraryPlugin -> configureWithLibraryPlugin(project, androidXExtension) |
| is AppPlugin -> configureWithAppPlugin(project, androidXExtension) |
| is TestPlugin -> configureWithTestPlugin(project, androidXExtension) |
| is KotlinMultiplatformAndroidPlugin -> |
| configureWithKotlinMultiplatformAndroidPlugin( |
| project, |
| androidXKmpExtension.agpKmpExtension, |
| androidXExtension |
| ) |
| is KotlinBasePluginWrapper -> |
| configureWithKotlinPlugin( |
| project, |
| androidXExtension, |
| plugin, |
| androidXKmpExtension |
| ) |
| is PrivacySandboxSdkPlugin -> configureWithPrivacySandboxSdkPlugin(project) |
| } |
| } |
| |
| project.configureLint() |
| val formatWithKtfmt = |
| project.getKtfmtOptInPathPrefixes().any { prefix -> project.path.startsWith(prefix) } |
| if (formatWithKtfmt) { |
| project.configureKtfmt() |
| } else { |
| project.configureKtlint() |
| } |
| project.configureKotlinVersion() |
| |
| // Avoid conflicts between full Guava and LF-only Guava. |
| project.configureGuavaUpgradeHandler() |
| |
| // Configure all Jar-packing tasks for hermetic builds. |
| project.tasks.withType(Zip::class.java).configureEach { it.configureForHermeticBuild() } |
| project.tasks.withType(Copy::class.java).configureEach { it.configureForHermeticBuild() } |
| |
| val allHostTests = project.tasks.register("allHostTests").get() |
| // copy host side test results to DIST |
| project.tasks.withType(AbstractTestTask::class.java) { task -> |
| configureTestTask(project, task, allHostTests) |
| } |
| |
| project.tasks.withType(Test::class.java).configureEach { task -> |
| configureJvmTestTask(project, task) |
| } |
| |
| project.configureTaskTimeouts() |
| project.configureMavenArtifactUpload( |
| androidXExtension, |
| androidXKmpExtension, |
| componentFactory |
| ) { |
| project.addCreateLibraryBuildInfoFileTasks(androidXExtension, androidXKmpExtension) |
| } |
| project.publishInspectionArtifacts() |
| project.configureExternalDependencyLicenseCheck() |
| project.configureProjectStructureValidation(androidXExtension) |
| project.configureProjectVersionValidation(androidXExtension) |
| project.registerProjectOrArtifact() |
| project.validateMultiplatformPluginHasNotBeenApplied() |
| |
| project.tasks.register("printCoordinates", PrintProjectCoordinatesTask::class.java) { |
| it.configureWithAndroidXExtension(androidXExtension) |
| } |
| project.configureConstraintsWithinGroup(androidXExtension) |
| project.validateProjectParser(androidXExtension) |
| project.validateAllArchiveInputsRecognized() |
| project.afterEvaluate { |
| if (androidXExtension.shouldPublishSbom()) { |
| project.configureSbomPublishing() |
| } |
| if (androidXExtension.shouldPublish()) { |
| project.validatePublishedMultiplatformHasDefault() |
| } |
| project.registerValidateMultiplatformSourceSetNamingTask() |
| } |
| project.disallowAccidentalAndroidDependenciesInKmpProject(androidXKmpExtension) |
| TaskUpToDateValidator.setup(project, registry) |
| |
| project.workaroundPrebuiltTakingPrecedenceOverProject() |
| project.configureSamplesProject() |
| } |
| |
| private fun Project.registerProjectOrArtifact() { |
| // Add a method for each sub project where they can declare an optional |
| // dependency on a project or its latest snapshot artifact. |
| if (!ProjectLayoutType.isPlayground(this)) { |
| // In AndroidX build, this is always enforced to the project |
| extra.set( |
| PROJECT_OR_ARTIFACT_EXT_NAME, |
| KotlinClosure1<String, Project>( |
| function = { |
| // this refers to the first parameter of the closure. |
| project.resolveProject(this) |
| } |
| ) |
| ) |
| } else { |
| // In Playground builds, they are converted to the latest SNAPSHOT artifact if the |
| // project is not included in that playground. |
| extra.set( |
| PROJECT_OR_ARTIFACT_EXT_NAME, |
| KotlinClosure1<String, Any>( |
| function = { |
| AndroidXPlaygroundRootImplPlugin.projectOrArtifact(rootProject, this) |
| } |
| ) |
| ) |
| } |
| } |
| |
| /** |
| * Disables timestamps and ensures filesystem-independent archive ordering to maximize |
| * cross-machine byte-for-byte reproducibility of artifacts. |
| */ |
| private fun Zip.configureForHermeticBuild() { |
| isReproducibleFileOrder = true |
| isPreserveFileTimestamps = false |
| } |
| |
| private fun Copy.configureForHermeticBuild() { |
| duplicatesStrategy = DuplicatesStrategy.FAIL |
| } |
| |
| private fun configureJvmTestTask(project: Project, task: Test) { |
| // Robolectric 1.7 increased heap size requirements, see b/207169653. |
| task.maxHeapSize = "3g" |
| |
| // For non-playground setup use robolectric offline |
| if (!ProjectLayoutType.isPlayground(project)) { |
| task.systemProperty("robolectric.offline", "true") |
| val robolectricDependencies = |
| File( |
| project.getPrebuiltsRoot(), |
| "androidx/external/org/robolectric/android-all-instrumented" |
| ) |
| task.systemProperty( |
| "robolectric.dependency.dir", |
| robolectricDependencies.relativeTo(project.projectDir) |
| ) |
| } |
| } |
| |
| private fun configureTestTask( |
| project: Project, |
| task: AbstractTestTask, |
| anchorTask: Task, |
| ) { |
| anchorTask.dependsOn(task) |
| val ignoreFailuresProperty = |
| project.providers.gradleProperty(TEST_FAILURES_DO_NOT_FAIL_TEST_TASK) |
| val ignoreFailures = ignoreFailuresProperty.isPresent |
| if (ignoreFailures) { |
| task.ignoreFailures = true |
| } |
| task.inputs.property("ignoreFailures", ignoreFailures) |
| |
| val xmlReportDestDir = project.getHostTestResultDirectory() |
| val testName = "${project.path}:${task.name}" |
| project.rootProject.tasks.named("createModuleInfo").configure { |
| it as ModuleInfoGenerator |
| it.testModules.add( |
| TestModule( |
| name = testName, |
| path = |
| listOf(project.projectDir.toRelativeString(project.getSupportRootFolder())) |
| ) |
| ) |
| } |
| val archiveName = "$testName.zip" |
| if (project.isDisplayTestOutput()) { |
| // Enable tracing to see results in command line |
| task.testLogging.apply { |
| events = |
| hashSetOf(TestLogEvent.FAILED, TestLogEvent.SKIPPED, TestLogEvent.STANDARD_OUT) |
| showExceptions = true |
| showCauses = true |
| showStackTraces = true |
| exceptionFormat = TestExceptionFormat.FULL |
| } |
| } else { |
| task.testLogging.apply { |
| showExceptions = false |
| // Disable all output, including the names of the failing tests, by specifying |
| // that the minimum granularity we're interested in is this very high number |
| // (which is higher than the current maximum granularity that Gradle offers (3)) |
| minGranularity = 1000 |
| } |
| val testTaskName = task.name |
| val capitalizedTestTaskName = |
| testTaskName.replaceFirstChar { |
| if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() |
| } |
| val xmlReport = task.reports.junitXml |
| if (xmlReport.required.get()) { |
| val zipXmlTask = |
| project.tasks.register( |
| "zipXmlResultsOf$capitalizedTestTaskName", |
| Zip::class.java |
| ) { |
| it.destinationDirectory.set(xmlReportDestDir) |
| it.archiveFileName.set(archiveName) |
| it.from(project.file(xmlReport.outputLocation)) |
| it.include("*.xml") |
| } |
| task.finalizedBy(zipXmlTask) |
| } |
| } |
| } |
| |
| /** Configures the project to use the Kotlin version specified by `androidx.kotlinTarget`. */ |
| private fun Project.configureKotlinVersion() { |
| val kotlinVersionStringProvider = androidXConfiguration.kotlinBomVersion |
| val kotlinTestVersionStringProvider = androidXConfiguration.kotlinTestBomVersion |
| |
| // Resolve unspecified Kotlin versions to the target version. |
| configurations.configureEach { configuration -> |
| val useVersionStringProvider = |
| if (configuration.isTest()) { |
| kotlinTestVersionStringProvider |
| } else { |
| kotlinVersionStringProvider |
| } |
| configuration.resolutionStrategy { strategy -> |
| strategy.eachDependency { details -> |
| if ( |
| details.requested.group == "org.jetbrains.kotlin" && |
| details.requested.version == null |
| ) { |
| details.useVersion(useVersionStringProvider.get()) |
| } |
| } |
| } |
| } |
| |
| fun Provider<String>.toKotlinVersionProvider() = map { version -> |
| KotlinVersion.fromVersion(version.substringBeforeLast('.')) |
| } |
| |
| fun KotlinCompilationTask<*>.isTestCompilation() = |
| multiplatformExtension?.targets?.any { target -> |
| target.compilations.findByName("test")?.compileKotlinTaskName == name |
| } ?: false |
| |
| // Set the Kotlin compiler's API and language version to ensure bytecode is compatible. |
| val kotlinVersionProvider = kotlinVersionStringProvider.toKotlinVersionProvider() |
| val kotlinTestVersionProvider = kotlinTestVersionStringProvider.toKotlinVersionProvider() |
| tasks.configureEach { task -> |
| if (task is KotlinCompilationTask<*>) { |
| // We can't directly determine if a Task is compiling test code, but we can scrape |
| // the names of all the compilation units and compare them to Task names. |
| val useVersionProvider = |
| if (task.isTestCompilation()) { |
| kotlinTestVersionProvider |
| } else { |
| kotlinVersionProvider |
| } |
| task.compilerOptions.apiVersion.set(useVersionProvider) |
| task.compilerOptions.languageVersion.set(useVersionProvider) |
| } |
| } |
| |
| // Specify coreLibrariesVersion for consumption by Kotlin Gradle Plugin. Note that KGP does |
| // not explicitly support varying the version between tasks/configurations for a given |
| // project, so this is not strictly correct. Picking the non-test (e.g. lower) value seems |
| // to work, though. |
| afterEvaluate { evaluatedProject -> |
| evaluatedProject.kotlinExtensionOrNull?.let { kotlinExtension -> |
| kotlinExtension.coreLibrariesVersion = kotlinVersionStringProvider.get() |
| } |
| } |
| |
| // Resolve classpath conflicts caused by kotlin-stdlib-jdk7 and -jdk8 artifacts by amending |
| // the kotlin-stdlib artifact metadata to add same-version constraints. |
| project.dependencies { |
| components { componentMetadata -> |
| componentMetadata.withModule<KotlinStdlibDependenciesRule>( |
| "org.jetbrains.kotlin:kotlin-stdlib" |
| ) |
| } |
| } |
| } |
| |
| @CacheableRule |
| internal abstract class KotlinStdlibDependenciesRule : ComponentMetadataRule { |
| override fun execute(context: ComponentMetadataContext) { |
| val module = context.details.id |
| val version = module.version |
| context.details.allVariants { variantMetadata -> |
| variantMetadata.withDependencyConstraints { constraintsMetadata -> |
| val reason = "${module.name} is in atomic group ${module.group}" |
| constraintsMetadata.add("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$version") { |
| it.because(reason) |
| } |
| constraintsMetadata.add("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$version") { |
| it.because(reason) |
| } |
| } |
| } |
| } |
| } |
| |
| private fun configureWithKotlinPlugin( |
| project: Project, |
| androidXExtension: AndroidXExtension, |
| plugin: KotlinBasePluginWrapper, |
| androidXMultiplatformExtension: AndroidXMultiplatformExtension |
| ) { |
| project.afterEvaluate { |
| val targetsAndroid = |
| project.plugins.hasPlugin(LibraryPlugin::class.java) || |
| project.plugins.hasPlugin(AppPlugin::class.java) || |
| project.plugins.hasPlugin(TestPlugin::class.java) || |
| @Suppress("UnstableApiUsage") |
| project.plugins.hasPlugin(KotlinMultiplatformAndroidPlugin::class.java) |
| val defaultJavaTargetVersion = |
| getDefaultTargetJavaVersion(androidXExtension.type).toString() |
| if (plugin is KotlinMultiplatformPluginWrapper) { |
| project.extensions.getByType<KotlinMultiplatformExtension>().apply { |
| targets.withType<KotlinAndroidTarget> { |
| compilations.configureEach { |
| it.kotlinOptions.jvmTarget = defaultJavaTargetVersion |
| } |
| } |
| targets.withType<KotlinJvmTarget> { |
| val defaultJavaTargetVersionForNonAndroidTargets = |
| getDefaultTargetJavaVersion( |
| libraryType = androidXExtension.type, |
| projectName = project.name, |
| targetName = name |
| ) |
| .toString() |
| compilations.configureEach { |
| it.compileJavaTaskProvider?.configure { javaCompile -> |
| javaCompile.targetCompatibility = |
| defaultJavaTargetVersionForNonAndroidTargets |
| javaCompile.sourceCompatibility = |
| defaultJavaTargetVersionForNonAndroidTargets |
| } |
| it.kotlinOptions { |
| jvmTarget = defaultJavaTargetVersionForNonAndroidTargets |
| // Set jdk-release version for non-Android KMP targets |
| freeCompilerArgs += |
| listOf( |
| "-Xjdk-release=$defaultJavaTargetVersionForNonAndroidTargets", |
| ) |
| } |
| } |
| } |
| } |
| } else { |
| project.tasks.withType(KotlinJvmCompile::class.java).configureEach { task -> |
| task.kotlinOptions.jvmTarget = defaultJavaTargetVersion |
| if (!targetsAndroid) { |
| // Set jdk-release version for non-Android JVM projects |
| task.kotlinOptions.freeCompilerArgs += |
| listOf( |
| "-Xjdk-release=$defaultJavaTargetVersion", |
| ) |
| } |
| } |
| } |
| project.tasks.withType(KotlinCompile::class.java).configureEach { task -> |
| val kotlinCompilerArgs = |
| mutableListOf( |
| "-Xskip-metadata-version-check", |
| ) |
| // TODO (b/259578592): enable -Xjvm-default=all for camera-camera2-pipe projects |
| if (!project.name.contains("camera-camera2-pipe")) { |
| kotlinCompilerArgs += "-Xjvm-default=all" |
| } |
| if (androidXExtension.type.targetsKotlinConsumersOnly) { |
| // The Kotlin Compiler adds intrinsic assertions which are only relevant |
| // when the code is consumed by Java users. Therefore we can turn this off |
| // when code is being consumed by Kotlin users. |
| |
| // Additional Context: |
| // https://github.com/JetBrains/kotlin/blob/master/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JVMCompilerArguments.kt#L239 |
| // b/280633711 |
| kotlinCompilerArgs += |
| listOf( |
| "-Xno-param-assertions", |
| "-Xno-call-assertions", |
| "-Xno-receiver-assertions" |
| ) |
| } |
| task.kotlinOptions.freeCompilerArgs += kotlinCompilerArgs |
| logScriptSources(task, project) |
| } |
| |
| val kotlinExtension = project.extensions.getByType(KotlinProjectExtension::class.java) |
| kotlinExtension.explicitApi = |
| if (androidXExtension.shouldEnforceKotlinStrictApiMode()) { |
| ExplicitApiMode.Strict |
| } else { |
| ExplicitApiMode.Disabled |
| } |
| } |
| if (plugin is KotlinMultiplatformPluginWrapper) { |
| KonanPrebuiltsSetup.configureKonanDirectory(project) |
| project.afterEvaluate { |
| val libraryExtension = project.extensions.findByType<LibraryExtension>() |
| if (libraryExtension != null) { |
| libraryExtension.configureAndroidLibraryWithMultiplatformPluginOptions() |
| } else if (!androidXMultiplatformExtension.hasAndroidMultiplatform()) { |
| // Kotlin MPP does not apply java plugin anymore, but we still want to configure |
| // all java-related tasks. |
| // We only need to do this when project does not have Android plugin, which |
| // already |
| // configures Java tasks. |
| configureWithJavaPlugin(project, androidXExtension) |
| } |
| } |
| project.configureKmp() |
| project.configureSourceJarForMultiplatform() |
| |
| // Disable any source JAR task(s) added by KotlinMultiplatformPlugin. |
| // https://youtrack.jetbrains.com/issue/KT-55881 |
| project.tasks.withType(Jar::class.java).configureEach { jarTask -> |
| if (jarTask.name == "jvmSourcesJar") { |
| // We can't set duplicatesStrategy directly on the Jar task since it will get |
| // overridden when the KotlinMultiplatformPlugin creates child specs, but we |
| // can set it on a per-file basis. |
| jarTask.eachFile { fileCopyDetails -> |
| fileCopyDetails.duplicatesStrategy = DuplicatesStrategy.EXCLUDE |
| } |
| } |
| } |
| } |
| } |
| |
| private fun configureWithAppPlugin(project: Project, androidXExtension: AndroidXExtension) { |
| project.extensions.getByType<ApplicationExtension>().apply { |
| configureAndroidBaseOptions(project, androidXExtension) |
| defaultConfig.targetSdk = project.defaultAndroidConfig.targetSdk |
| val debugSigningConfig = signingConfigs.getByName("debug") |
| // Use a local debug keystore to avoid build server issues. |
| debugSigningConfig.storeFile = project.getKeystore() |
| buildTypes.configureEach { buildType -> |
| // Sign all the builds (including release) with debug key |
| buildType.signingConfig = debugSigningConfig |
| } |
| configureAndroidApplicationOptions(project, androidXExtension) |
| excludeVersionFiles(packaging.resources) |
| } |
| |
| project.extensions.getByType<ApplicationAndroidComponentsExtension>().apply { |
| beforeVariants(selector().withBuildType("release")) { variant -> |
| // Cast is needed because ApplicationAndroidComponentsExtension implements both |
| // HasUnitTestBuilder and VariantBuilder, and VariantBuilder#enableUnitTest is |
| // deprecated in favor of HasUnitTestBuilder#enableUnitTest. |
| // Remove the cast when we upgrade to AGP 9.0.0 |
| (variant as HasUnitTestBuilder).enableUnitTest = false |
| } |
| onVariants { |
| it.configureTests() |
| it.configureLocalAsbSigning(project.getKeystore()) |
| } |
| } |
| |
| project.buildOnServerDependsOnAssembleRelease() |
| project.buildOnServerDependsOnLint() |
| } |
| |
| private fun configureWithTestPlugin(project: Project, androidXExtension: AndroidXExtension) { |
| project.extensions.getByType<TestExtension>().apply { |
| configureAndroidBaseOptions(project, androidXExtension) |
| defaultConfig.targetSdk = project.defaultAndroidConfig.targetSdk |
| val debugSigningConfig = signingConfigs.getByName("debug") |
| // Use a local debug keystore to avoid build server issues. |
| debugSigningConfig.storeFile = project.getKeystore() |
| buildTypes.configureEach { buildType -> |
| // Sign all the builds (including release) with debug key |
| buildType.signingConfig = debugSigningConfig |
| } |
| project.configureTestConfigGeneration(this) |
| project.addAppApkToTestConfigGeneration(androidXExtension) |
| excludeVersionFiles(packaging.resources) |
| } |
| |
| project.configureJavaCompilationWarnings(androidXExtension) |
| |
| project.addToProjectMap(androidXExtension) |
| } |
| |
| private fun configureWithKotlinMultiplatformAndroidPlugin( |
| project: Project, |
| kotlinMultiplatformAndroidTarget: KotlinMultiplatformAndroidTarget, |
| androidXExtension: AndroidXExtension |
| ) { |
| val kotlinMultiplatformAndroidComponentsExtension = |
| project.extensions.getByType<KotlinMultiplatformAndroidComponentsExtension>() |
| kotlinMultiplatformAndroidTarget.configureAndroidBaseOptions( |
| project, |
| kotlinMultiplatformAndroidComponentsExtension |
| ) |
| kotlinMultiplatformAndroidTarget.configureAndroidLibraryOptions(project, androidXExtension) |
| |
| project.configureProjectForApiTasks(AndroidMultiplatformApiTaskConfig, androidXExtension) |
| |
| kotlinMultiplatformAndroidComponentsExtension.onVariant { it.configureTests() } |
| |
| project.configurePublicResourcesStub(project.multiplatformExtension!!) |
| kotlinMultiplatformAndroidComponentsExtension.onVariant { variant -> |
| project.configureMultiplatformSourcesForAndroid( |
| variant.name, |
| kotlinMultiplatformAndroidTarget, |
| androidXExtension.samplesProjects |
| ) |
| } |
| project.configureVersionFileWriter(project.multiplatformExtension!!, androidXExtension) |
| project.configureJavaCompilationWarnings(androidXExtension) |
| |
| project.configureDependencyVerification(androidXExtension) { taskProvider -> |
| kotlinMultiplatformAndroidTarget.compilations.configureEach { |
| taskProvider.configure { task -> task.dependsOn(it.compileTaskProvider) } |
| } |
| } |
| |
| project.addToProjectMap(androidXExtension) |
| project.afterEvaluate { |
| project.addToBuildOnServer("assembleAndroidMain") |
| project.addToBuildOnServer("lint") |
| } |
| project.setUpCheckDocsTask(androidXExtension) |
| project.writeBlankPublicTxtToAar(kotlinMultiplatformAndroidComponentsExtension) |
| } |
| |
| private fun Project.writeBlankPublicTxtToAar( |
| componentsExtension: KotlinMultiplatformAndroidComponentsExtension |
| ) { |
| val blankPublicResourceDir = |
| project.getSupportRootFolder().resolve("buildSrc/blank-res-api") |
| componentsExtension.onVariant { variant -> |
| val taskProvider = |
| tasks.register( |
| "repackageAarWithResourceApi".appendCapitalized(variant.name), |
| RepackagingTask::class.java |
| ) { task -> |
| task.from(blankPublicResourceDir) |
| task.from(zipTree(task.aarFile)) |
| task.destinationDirectory.fileProvider( |
| task.output.locationOnly.map { location -> location.asFile.parentFile } |
| ) |
| task.archiveFileName.set( |
| task.output.locationOnly.map { location -> location.asFile.name } |
| ) |
| } |
| variant.artifacts |
| .use(taskProvider) |
| .wiredWithFiles(RepackagingTask::aarFile, RepackagingTask::output) |
| .toTransform(SingleArtifact.AAR) |
| } |
| } |
| |
| @Suppress("UnstableApiUsage") // usage of PrivacySandboxSdkExtension |
| private fun configureWithPrivacySandboxSdkPlugin(project: Project) { |
| project.extensions.getByType<PrivacySandboxSdkExtension>().apply { |
| configureLocalAsbSigning(experimentalProperties, project.getKeystore()) |
| } |
| } |
| |
| private fun configureLocalAsbSigning( |
| experimentalProperties: MutableMap<String, Any>, |
| keyStore: File |
| ) { |
| experimentalProperties[ASB_SIGNING_CONFIG_PROPERTY_NAME] = keyStore.absolutePath |
| } |
| |
| private val ASB_SIGNING_CONFIG_PROPERTY_NAME = |
| "android.privacy_sandbox.local_deployment_signing_store_file" |
| |
| /** Temporary diagnostics for b/321949384 */ |
| private fun logScriptSources(task: KotlinCompile, project: Project) { |
| if (getBuildId() == "0") return // don't need to log when not running on the build server |
| val logFile = File(project.getDistributionDirectory(), "KotlinCompile-scriptSources.log") |
| fun writeScriptSources(label: String) { |
| val now = LocalDateTime.now() |
| @Suppress("INVISIBLE_MEMBER") val scriptSources = task.scriptSources.files |
| logFile.appendText( |
| "${task.path} $label at $now with ${scriptSources.size} scriptSources: " + |
| "${scriptSources.joinToString()}\n" |
| ) |
| } |
| task.doFirst { writeScriptSources("starting") } |
| task.doLast { writeScriptSources("completed") } |
| } |
| |
| /** |
| * Excludes files telling which versions of androidx libraries were used in test apks, to avoid |
| * invalidating caches as often |
| */ |
| private fun excludeVersionFiles(packaging: com.android.build.api.variant.ResourcesPackaging) { |
| packaging.excludes.add("/META-INF/androidx*.version") |
| } |
| |
| /** |
| * Excludes files telling which versions of androidx libraries were used in test apks, to avoid |
| * invalidating caches as often |
| */ |
| private fun excludeVersionFiles(packaging: com.android.build.api.dsl.ResourcesPackaging) { |
| packaging.excludes.add("/META-INF/androidx*.version") |
| } |
| |
| private fun Project.buildOnServerDependsOnAssembleRelease() { |
| project.addToBuildOnServer("assembleRelease") |
| } |
| |
| private fun Project.buildOnServerDependsOnLint() { |
| if (!project.usingMaxDepVersions()) { |
| val androidComponents = extensions.findByType(AndroidComponentsExtension::class.java) |
| androidComponents?.onVariants { variant -> |
| if (!variant.name.lowercase(Locale.getDefault()).contains("release")) { |
| val taskName = |
| "lint${variant.name.replaceFirstChar { |
| if (it.isLowerCase()) { |
| it.titlecase(Locale.getDefault()) |
| } else { |
| it.toString() |
| } |
| }}" |
| project.addToBuildOnServer(taskName) |
| } |
| } |
| } |
| } |
| |
| @Suppress("UnstableApiUsage") // usage of HasDeviceTests |
| private fun HasDeviceTests.configureTests() { |
| deviceTests.forEach { deviceTest -> |
| deviceTest.packaging.resources.apply { |
| excludeVersionFiles(this) |
| |
| // Workaround a limitation in AGP that fails to merge these META-INF license files. |
| pickFirsts.add("/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 |
| excludes.add("/META-INF/LGPL2.1") |
| } |
| } |
| } |
| |
| /** |
| * Enable long-running method tracing on the UI thread, even if that risks ANR for profiling |
| * convenience. |
| * |
| * See [androidx.build.testConfiguration.INST_ARG_BLOCKLIST], which is used to suppress the arg |
| * in CI. |
| */ |
| @Suppress("UnstableApiUsage") // usage of HasDeviceTests |
| private fun HasDeviceTests.enableLongMethodTracingInMicrobenchmark(project: Project) { |
| if (project.hasBenchmarkPlugin()) { |
| deviceTests.forEach { deviceTest -> |
| deviceTest.instrumentationRunnerArguments.put( |
| "androidx.benchmark.profiling.skipWhenDurationRisksAnr", |
| "false" |
| ) |
| } |
| } |
| } |
| |
| @Suppress("UnstableApiUsage") // usage of experimentalProperties |
| private fun Variant.configureLocalAsbSigning(keyStore: File) { |
| experimentalProperties.put(ASB_SIGNING_CONFIG_PROPERTY_NAME, keyStore.absolutePath) |
| } |
| |
| private fun configureWithLibraryPlugin(project: Project, androidXExtension: AndroidXExtension) { |
| project.extensions.getByType<LibraryExtension>().apply { |
| publishing { singleVariant(DEFAULT_PUBLISH_CONFIG) } |
| |
| configureAndroidBaseOptions(project, androidXExtension) |
| |
| // Move to api that allows settings of targetSdk on each variant's androidTest when |
| // b/335257447 is fixed |
| @Suppress("DEPRECATION") |
| defaultConfig.targetSdk = project.defaultAndroidConfig.targetSdk |
| |
| val debugSigningConfig = signingConfigs.getByName("debug") |
| // Use a local debug keystore to avoid build server issues. |
| debugSigningConfig.storeFile = project.getKeystore() |
| buildTypes.configureEach { buildType -> |
| // Sign all the builds (including release) with debug key |
| buildType.signingConfig = debugSigningConfig |
| } |
| project.configureTestConfigGeneration(this) |
| project.addAppApkToTestConfigGeneration(androidXExtension) |
| } |
| |
| val libraryAndroidComponentsExtension = |
| project.extensions.getByType<LibraryAndroidComponentsExtension>() |
| |
| libraryAndroidComponentsExtension.apply { |
| finalizeDsl { |
| // Propagate the compileSdk value into minCompileSdk. Don't propagate |
| // compileSdkExtension, since only one library actually depends on the extension |
| // APIs and they can explicitly declare that in their build.gradle. Note that when |
| // we're using a preview SDK, the value for compileSdk will be null and the |
| // resulting AAR metadata won't have a minCompileSdk -- |
| // this is okay because AGP automatically embeds forceCompileSdkPreview in the AAR |
| // metadata and uses it instead of minCompileSdk. |
| it.defaultConfig.aarMetadata.minCompileSdk = it.compileSdk |
| } |
| beforeVariants(selector().withBuildType("release")) { variant -> |
| (variant as HasUnitTestBuilder).enableUnitTest = false |
| } |
| onVariants { variant -> |
| variant.configureTests() |
| variant.enableLongMethodTracingInMicrobenchmark(project) |
| } |
| } |
| |
| project.disableStrictVersionConstraints() |
| project.setPublishProperty(androidXExtension) |
| project.configureVersionFileWriter(libraryAndroidComponentsExtension, androidXExtension) |
| project.configureJavaCompilationWarnings(androidXExtension) |
| |
| val prebuiltLibraries = listOf("libtracing_perfetto.so", "libc++_shared.so") |
| val copyPublicResourcesDirTask = |
| project.tasks.register( |
| "generatePublicResourcesStub", |
| CopyPublicResourcesDirTask::class.java |
| ) { task -> |
| task.buildSrcResDir.set(File(project.getSupportRootFolder(), "buildSrc/res")) |
| } |
| libraryAndroidComponentsExtension.onVariants { variant -> |
| if (variant.buildType == DEFAULT_PUBLISH_CONFIG) { |
| // Standard docs, resource API, and Metalava configuration for AndroidX projects. |
| project.configureProjectForApiTasks( |
| LibraryApiTaskConfig(variant), |
| androidXExtension |
| ) |
| } |
| if (variant.name == DEFAULT_PUBLISH_CONFIG) { |
| project.configureSourceJarForAndroid(variant, androidXExtension.samplesProjects) |
| project.configureDependencyVerification(androidXExtension) { taskProvider -> |
| taskProvider.configure { task -> task.dependsOn("compileReleaseJavaWithJavac") } |
| } |
| } |
| configurePublicResourcesStub(variant, copyPublicResourcesDirTask) |
| val verifyELFRegionAlignmentTaskProvider = |
| project.tasks.register( |
| variant.name + "VerifyELFRegionAlignment", |
| VerifyELFRegionAlignmentTask::class.java |
| ) { task -> |
| task.files.from( |
| variant.artifacts.get(SingleArtifact.MERGED_NATIVE_LIBS).map { dir -> |
| dir.asFileTree.files |
| .filter { it.extension == "so" } |
| .filter { it.path.contains("arm64-v8a") } |
| .filterNot { prebuiltLibraries.contains(it.name) } |
| } |
| ) |
| task.cacheEvenIfNoOutputs() |
| } |
| project.addToBuildOnServer(verifyELFRegionAlignmentTaskProvider) |
| } |
| |
| project.setUpCheckDocsTask(androidXExtension) |
| |
| project.addToProjectMap(androidXExtension) |
| |
| project.buildOnServerDependsOnAssembleRelease() |
| project.buildOnServerDependsOnLint() |
| } |
| |
| private fun configureGradlePluginPlugin(project: Project) { |
| project.tasks.withType(ValidatePlugins::class.java).configureEach { |
| it.enableStricterValidation.set(true) |
| it.failOnWarning.set(true) |
| } |
| SdkResourceGenerator.generateForHostTest(project) |
| } |
| |
| private fun getDefaultTargetJavaVersion( |
| libraryType: LibraryType, |
| projectName: String? = null, |
| targetName: String? = null |
| ): JavaVersion { |
| return when { |
| projectName != null && projectName.contains("desktop") -> VERSION_11 |
| targetName != null && targetName == "desktop" -> VERSION_11 |
| libraryType == LibraryType.COMPILER_PLUGIN -> VERSION_11 |
| libraryType.compilationTarget == CompilationTarget.HOST -> VERSION_17 |
| else -> VERSION_1_8 |
| } |
| } |
| |
| private fun configureWithJavaPlugin(project: Project, androidXExtension: AndroidXExtension) { |
| project.configureErrorProneForJava() |
| |
| // Force Java 1.8 source- and target-compatibility for all Java libraries. |
| val javaExtension = project.extensions.getByType<JavaPluginExtension>() |
| project.afterEvaluate { |
| javaExtension.apply { |
| val defaultTargetJavaVersion = getDefaultTargetJavaVersion(androidXExtension.type) |
| sourceCompatibility = defaultTargetJavaVersion |
| targetCompatibility = defaultTargetJavaVersion |
| project.disableJava8TargetObsoleteWarnings() |
| } |
| if (!project.plugins.hasPlugin(KotlinBasePluginWrapper::class.java)) { |
| project.configureSourceJarForJava(androidXExtension.samplesProjects) |
| } |
| } |
| |
| project.configureJavaCompilationWarnings(androidXExtension) |
| |
| project.hideJavadocTask() |
| |
| project.configureDependencyVerification(androidXExtension) { taskProvider -> |
| taskProvider.configure { task -> |
| task.dependsOn(project.tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME)) |
| } |
| } |
| |
| val apiTaskConfig = |
| if (project.multiplatformExtension != null) { |
| KmpApiTaskConfig |
| } else { |
| JavaApiTaskConfig |
| } |
| |
| project.configureProjectForApiTasks(apiTaskConfig, androidXExtension) |
| project.setUpCheckDocsTask(androidXExtension) |
| |
| project.afterEvaluate { |
| if (androidXExtension.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.configureEach { configuration -> |
| configuration.resolutionStrategy.preferProjectModules() |
| } |
| |
| if (project.multiplatformExtension == null) { |
| project.addToBuildOnServer("jar") |
| } else { |
| val multiplatformExtension = project.multiplatformExtension!! |
| multiplatformExtension.targets.forEach { |
| if (it.platformType == KotlinPlatformType.jvm) { |
| val task = project.tasks.named(it.artifactsTaskName, Jar::class.java) |
| project.addToBuildOnServer(task) |
| } |
| } |
| } |
| |
| project.addToProjectMap(androidXExtension) |
| } |
| |
| private fun Project.configureProjectStructureValidation(androidXExtension: AndroidXExtension) { |
| // AndroidXExtension.mavenGroup is not readable until afterEvaluate. |
| afterEvaluate { |
| val mavenGroup = androidXExtension.mavenGroup |
| val isProbablyPublished = |
| androidXExtension.type == LibraryType.PUBLISHED_LIBRARY || |
| androidXExtension.type == |
| LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS || |
| androidXExtension.type == LibraryType.UNSET |
| if (mavenGroup != null && isProbablyPublished && androidXExtension.shouldPublish()) { |
| validateProjectMavenGroup(mavenGroup.group) |
| validateProjectMavenName(androidXExtension.name.get(), mavenGroup.group) |
| validateProjectStructure(mavenGroup.group) |
| } |
| } |
| } |
| |
| private fun Project.configureProjectVersionValidation(androidXExtension: AndroidXExtension) { |
| // AndroidXExtension.mavenGroup is not readable until afterEvaluate. |
| afterEvaluate { androidXExtension.validateMavenVersion() } |
| } |
| |
| private fun CommonExtension<*, *, *, *, *, *>.configureAndroidBaseOptions( |
| project: Project, |
| androidXExtension: AndroidXExtension |
| ) { |
| compileOptions.apply { |
| sourceCompatibility = VERSION_1_8 |
| targetCompatibility = VERSION_1_8 |
| } |
| project.disableJava8TargetObsoleteWarnings() |
| |
| val defaultMinSdk = project.defaultAndroidConfig.minSdk |
| val defaultCompileSdk = project.defaultAndroidConfig.compileSdk |
| |
| // Suppress output of android:compileSdkVersion and related attributes (b/277836549). |
| androidResources.additionalParameters += "--no-compile-sdk-metadata" |
| |
| compileSdk = project.defaultAndroidConfig.compileSdk |
| |
| buildToolsVersion = project.defaultAndroidConfig.buildToolsVersion |
| ndkVersion = project.defaultAndroidConfig.ndkVersion |
| |
| defaultConfig.minSdk = defaultMinSdk |
| defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" |
| |
| testOptions.animationsDisabled = !project.isMacrobenchmark() |
| testOptions.unitTests.isReturnDefaultValues = true |
| testOptions.unitTests.all { task -> task.configureForRobolectric() } |
| |
| // Include resources in Robolectric tests as a workaround for b/184641296 |
| testOptions.unitTests.isIncludeAndroidResources = true |
| |
| project.afterEvaluate { |
| val minSdkVersion = defaultConfig.minSdk!! |
| check(minSdkVersion >= defaultMinSdk) { |
| "minSdkVersion $minSdkVersion lower than the default of $defaultMinSdk" |
| } |
| check(compileSdk == defaultCompileSdk || project.isCustomCompileSdkAllowed()) { |
| "compileSdk must not be explicitly specified, was \"$compileSdk\"" |
| } |
| |
| project.enforceBanOnVersionRanges() |
| |
| if (androidXExtension.type.compilationTarget != CompilationTarget.DEVICE) { |
| throw IllegalStateException( |
| "${androidXExtension.type.name} libraries cannot apply the android plugin, as" + |
| " they do not target android devices" |
| ) |
| } |
| } |
| |
| project.configureErrorProneForAndroid() |
| |
| // workaround for b/120487939 |
| project.configurations.configureEach { configuration -> |
| // Gradle seems to crash on androidtest configurations |
| // preferring project modules... |
| if (!configuration.name.lowercase(Locale.US).contains("androidtest")) { |
| configuration.resolutionStrategy.preferProjectModules() |
| } |
| } |
| |
| project.configureFtlRunner( |
| project.extensions.getByType(AndroidComponentsExtension::class.java) |
| ) |
| |
| // AGP warns if we use project.buildDir (or subdirs) for CMake's generated |
| // build files (ninja build files, CMakeCache.txt, etc.). Use a staging directory that |
| // lives alongside the project's buildDir. |
| @Suppress("DEPRECATION") |
| externalNativeBuild.cmake.buildStagingDirectory = |
| File(project.buildDir, "../nativeBuildStaging") |
| |
| // Align the ELF region of native shared libs 16kb boundary |
| defaultConfig.externalNativeBuild.cmake.arguments.add( |
| "-DCMAKE_SHARED_LINKER_FLAGS=-Wl,-z,max-page-size=16384" |
| ) |
| } |
| |
| private fun KotlinMultiplatformAndroidTarget.configureAndroidBaseOptions( |
| project: Project, |
| componentsExtension: KotlinMultiplatformAndroidComponentsExtension |
| ) { |
| val defaultMinSdkVersion = project.defaultAndroidConfig.minSdk |
| val defaultCompileSdk = project.defaultAndroidConfig.compileSdk |
| |
| compileSdk = defaultCompileSdk |
| buildToolsVersion = project.defaultAndroidConfig.buildToolsVersion |
| |
| minSdk = defaultMinSdkVersion |
| |
| lint.targetSdk = project.defaultAndroidConfig.targetSdk |
| compilations |
| .withType(KotlinMultiplatformAndroidTestOnDeviceCompilation::class.java) |
| .configureEach { |
| it.instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" |
| it.animationsDisabled = true |
| } |
| compilations |
| .withType(KotlinMultiplatformAndroidTestOnJvmCompilation::class.java) |
| .configureEach { |
| it.isReturnDefaultValues = true |
| // Include resources in Robolectric tests as a workaround for b/184641296 |
| it.isIncludeAndroidResources = true |
| } |
| |
| project.tasks.withType(AndroidUnitTest::class.java).configureEach { task -> |
| task.configureForRobolectric() |
| } |
| |
| // validate that SDK versions haven't been altered during evaluation |
| project.afterEvaluate { |
| val minSdkVersion = minSdk!! |
| check(minSdkVersion >= defaultMinSdkVersion) { |
| "minSdkVersion $minSdkVersion lower than the default of $defaultMinSdkVersion" |
| } |
| check(compileSdk == defaultCompileSdk || project.isCustomCompileSdkAllowed()) { |
| "compileSdk must not be explicitly specified, was \"$compileSdk\"" |
| } |
| project.enforceBanOnVersionRanges() |
| } |
| |
| project.configureTestConfigGeneration(this, componentsExtension) |
| project.configureFtlRunner(componentsExtension) |
| } |
| |
| private fun KotlinMultiplatformAndroidTarget.configureAndroidLibraryOptions( |
| project: Project, |
| androidXExtension: AndroidXExtension |
| ) { |
| // Propagate the compileSdk value into minCompileSdk. |
| aarMetadata.minCompileSdk = compileSdk |
| project.disableStrictVersionConstraints() |
| project.setPublishProperty(androidXExtension) |
| } |
| |
| /** |
| * Adds a module handler replacement rule that treats full Guava (of any version) as an upgrade |
| * to ListenableFuture-only Guava. This prevents irreconcilable versioning conflicts and/or |
| * class duplication issues. |
| */ |
| private fun Project.configureGuavaUpgradeHandler() { |
| // The full Guava artifact is very large, so they split off a special artifact containing a |
| // standalone version of the commonly-used ListenableFuture interface. However, they also |
| // structured the artifacts in a way that causes dependency resolution conflicts: |
| // - `com.google.guava:listenablefuture:1.0` contains only ListenableFuture |
| // - `com.google.guava:listenablefuture:9999.0` contains nothing |
| // - `com.google.guava:guava` contains all of Guava, including ListenableFuture |
| // If a transitive dependency includes `guava` as implementation-type and we have a direct |
| // API-type dependency on `listenablefuture:1.0`, then we'll get `listenablefuture:9999.0` |
| // on the compilation classpath -- which does not have the ListenableFuture class. However, |
| // if we tell Gradle to upgrade all LF dependencies to Guava then we'll get `guava` as an |
| // API-type dependency. See b/274621238 for more details. |
| project.dependencies { |
| modules { moduleHandler -> |
| moduleHandler.module("com.google.guava:listenablefuture") { module -> |
| module.replacedBy("com.google.guava:guava") |
| } |
| } |
| } |
| } |
| |
| private fun Project.disableStrictVersionConstraints() { |
| // Gradle inserts strict version constraints to ensure that dependency versions are |
| // identical across main and test source sets. For normal projects, this ensures |
| // that test bytecode is binary- and behavior-compatible with the main source set's |
| // bytecode. For AndroidX, though, we require backward compatibility and therefore |
| // don't need to enforce such constraints. |
| project.configurations.configureEach { configuration -> |
| if (!configuration.isTest()) return@configureEach |
| |
| configuration.dependencyConstraints.configureEach { dependencyConstraint -> |
| val strictVersion = dependencyConstraint.versionConstraint.strictVersion |
| if (strictVersion != "") { |
| // Migrate strict-type version constraints to required-type to allow upgrades. |
| dependencyConstraint.version { versionConstraint -> |
| versionConstraint.strictly("") |
| versionConstraint.require(strictVersion) |
| } |
| } |
| } |
| } |
| } |
| |
| private fun Project.setPublishProperty(androidXExtension: AndroidXExtension) { |
| afterEvaluate { |
| if (androidXExtension.shouldRelease()) { |
| project.extra.set("publish", true) |
| } |
| } |
| } |
| |
| private fun LibraryExtension.configureAndroidLibraryWithMultiplatformPluginOptions() { |
| sourceSets.findByName("main")!!.manifest.srcFile("src/androidMain/AndroidManifest.xml") |
| sourceSets |
| .findByName("androidTest")!! |
| .manifest |
| .srcFile("src/androidInstrumentedTest/AndroidManifest.xml") |
| } |
| |
| private fun Project.configureKmp() { |
| val kmpExtension = |
| checkNotNull(project.extensions.findByType<KotlinMultiplatformExtension>()) { |
| """ |
| Project ${project.path} applies kotlin multiplatform plugin but we cannot find the |
| KotlinMultiplatformExtension. |
| """ |
| .trimIndent() |
| } |
| |
| kmpExtension.targets.configureEach { kotlinTarget -> |
| kotlinTarget.compilations.configureEach { compilation -> |
| // Configure all KMP targets to allow expect/actual classes that are not stable. |
| // (see https://youtrack.jetbrains.com/issue/KT-61573) |
| compilation.compilerOptions.options.freeCompilerArgs.add("-Xexpect-actual-classes") |
| androidXConfiguration.kotlinApiVersion.let { |
| compilation.compilerOptions.options.apiVersion.set(it) |
| compilation.compilerOptions.options.languageVersion.set(it) |
| } |
| } |
| } |
| } |
| |
| private fun ApplicationExtension.configureAndroidApplicationOptions( |
| project: Project, |
| androidXExtension: AndroidXExtension |
| ) { |
| defaultConfig.apply { |
| versionCode = 1 |
| versionName = "1.0" |
| } |
| |
| project.configureTestConfigGeneration(this) |
| project.addAppApkToTestConfigGeneration(androidXExtension) |
| project.addAppApkToFtlRunner() |
| } |
| |
| private fun Project.configureDependencyVerification( |
| androidXExtension: AndroidXExtension, |
| taskConfigurator: (TaskProvider<VerifyDependencyVersionsTask>) -> Unit |
| ) { |
| afterEvaluate { |
| if (androidXExtension.type !in listOf(LibraryType.UNSET, LibraryType.SAMPLES)) { |
| val verifyDependencyVersionsTask = project.createVerifyDependencyVersionsTask() |
| if (verifyDependencyVersionsTask != null) { |
| taskConfigurator(verifyDependencyVersionsTask) |
| } |
| } |
| } |
| } |
| |
| // If this project wants other project in the same group to have the same version, |
| // this function configures those constraints. |
| private fun Project.configureConstraintsWithinGroup(androidXExtension: AndroidXExtension) { |
| if (!project.shouldAddGroupConstraints().get()) { |
| return |
| } |
| project.afterEvaluate { |
| // make sure that the project has a group |
| val projectGroup = androidXExtension.mavenGroup ?: return@afterEvaluate |
| // make sure that this group is configured to use a single version |
| projectGroup.atomicGroupVersion ?: return@afterEvaluate |
| |
| // Under certain circumstances, a project is allowed to override its |
| // version see ( isGroupVersionOverrideAllowed ), in which case it's |
| // not participating in the versioning policy yet, |
| // and we don't assign it any version constraints |
| if (androidXExtension.mavenVersion != null) { |
| return@afterEvaluate |
| } |
| |
| // We don't want to emit the same constraint into our .module file more than once, |
| // and we don't want to try to apply a constraint to a configuration that doesn't accept |
| // them, |
| // so we create a configuration to hold the constraints and make each other constraint |
| // extend it |
| val constraintConfiguration = project.configurations.create("groupConstraints") |
| project.configurations.configureEach { configuration -> |
| if (configuration != constraintConfiguration) |
| configuration.extendsFrom(constraintConfiguration) |
| } |
| |
| val otherProjectsInSameGroup = androidXExtension.getOtherProjectsInSameGroup() |
| val constraints = project.dependencies.constraints |
| val allProjectsExist = buildContainsAllStandardProjects() |
| for (otherProject in otherProjectsInSameGroup) { |
| val otherGradlePath = otherProject.gradlePath |
| if (otherGradlePath == ":compose:ui:ui-android-stubs") { |
| // exemption for library that doesn't truly get published: b/168127161 |
| continue |
| } |
| // We only enable constraints for builds that we intend to be able to publish from. |
| // If a project isn't included in a build we intend to be able to publish from, |
| // the project isn't going to be published. |
| // Sometimes this can happen when a project subset is enabled: |
| // The KMP project subset enabled by androidx_multiplatform_mac.sh contains |
| // :benchmark:benchmark-common but not :benchmark:benchmark-benchmark |
| // This is ok because we don't intend to publish that artifact from that build |
| val otherProjectShouldExist = |
| allProjectsExist || findProject(otherGradlePath) != null |
| if (!otherProjectShouldExist) { |
| continue |
| } |
| // We only emit constraints referring to projects that will release |
| val otherFilepath = File(otherProject.filePath, "build.gradle") |
| val parsed = parseBuildFile(otherFilepath) |
| if (!parsed.shouldRelease()) { |
| continue |
| } |
| if (parsed.libraryType == LibraryType.SAMPLES) { |
| // a SAMPLES project knows how to publish, but we don't intend to actually |
| // publish it |
| continue |
| } |
| // Under certain circumstances, a project is allowed to override its |
| // version see ( isGroupVersionOverrideAllowed ), in which case it's |
| // not participating in the versioning policy yet and we don't emit |
| // version constraints referencing it |
| if (parsed.specifiesVersion) { |
| continue |
| } |
| val dependencyConstraint = project(otherGradlePath) |
| constraints.add(constraintConfiguration.name, dependencyConstraint) { |
| it.because("${project.name} is in atomic group ${projectGroup.group}") |
| } |
| } |
| |
| // disallow duplicate constraints |
| project.configurations.configureEach { config -> |
| // Allow duplicate constraints in test configurations. This is partially a |
| // workaround for duplication due to downgrading strict-type dependencies to |
| // required-type, but also we don't care if tests have duplicate constraints. |
| if (config.isTest()) return@configureEach |
| |
| // find all constraints contributed by this Configuration and its ancestors |
| val configurationConstraints: MutableSet<String> = mutableSetOf() |
| config.hierarchy.forEach { parentConfig -> |
| parentConfig.dependencyConstraints.configureEach { dependencyConstraint -> |
| dependencyConstraint.apply { |
| if ( |
| versionConstraint.requiredVersion != "" && |
| versionConstraint.requiredVersion != "unspecified" |
| ) { |
| val key = |
| "${dependencyConstraint.group}:${dependencyConstraint.name}" |
| if (configurationConstraints.contains(key)) { |
| throw GradleException( |
| "Constraint on $key was added multiple times in " + |
| "$config (version = " + |
| "${versionConstraint.requiredVersion}).\n\n" + |
| "This is unnecessary and can also trigger " + |
| "https://github.com/gradle/gradle/issues/24037 in " + |
| "builds trying to use the resulting artifacts." |
| ) |
| } |
| configurationConstraints.add(key) |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Tells whether this build contains the usual set of all projects (`./gradlew projects`) |
| * Sometimes developers request to include fewer projects because this may run more quickly |
| */ |
| private fun Project.buildContainsAllStandardProjects(): Boolean { |
| if (getProjectSubset() != null) return false |
| if (ProjectLayoutType.isPlayground(this)) return false |
| return true |
| } |
| |
| companion object { |
| const val CREATE_LIBRARY_BUILD_INFO_FILES_TASK = "createLibraryBuildInfoFiles" |
| const val GENERATE_TEST_CONFIGURATION_TASK = "GenerateTestConfiguration" |
| const val FINALIZE_TEST_CONFIGS_WITH_APKS_TASK = "finalizeTestConfigsWithApks" |
| 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 longer than expected */ |
| const val TASK_TIMEOUT_MINUTES = 60L |
| } |
| } |
| |
| private const val PROJECTS_MAP_KEY = "projects" |
| private const val ACCESSED_PROJECTS_MAP_KEY = "accessedProjectsMap" |
| |
| /** Returns whether the configuration is used for testing. */ |
| private fun Configuration.isTest(): Boolean = name.lowercase().contains("test") |
| |
| /** |
| * 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(androidXExtension: AndroidXExtension) { |
| // TODO(alanv): Move this out of afterEvaluate |
| afterEvaluate { |
| if (androidXExtension.shouldRelease()) { |
| val group = androidXExtension.mavenGroup?.group |
| if (group != null) { |
| val module = "$group:$name" |
| |
| if (project.rootProject.extra.has(ACCESSED_PROJECTS_MAP_KEY)) { |
| throw GradleException( |
| "Attempted to add $project to project map after " + |
| "the contents of the map were accessed" |
| ) |
| } |
| @Suppress("UNCHECKED_CAST") |
| val projectModules = |
| project.rootProject.extra.get(PROJECTS_MAP_KEY) |
| as ConcurrentHashMap<String, String> |
| projectModules[module] = path |
| } |
| } |
| } |
| } |
| |
| val Project.androidExtension: AndroidComponentsExtension<*, *, *> |
| get() = |
| extensions.findByType<LibraryAndroidComponentsExtension>() |
| ?: extensions.findByType<ApplicationAndroidComponentsExtension>() |
| ?: throw IllegalArgumentException("Failed to find any registered Android extension") |
| |
| val Project.multiplatformExtension |
| get() = extensions.findByType(KotlinMultiplatformExtension::class.java) |
| |
| val Project.kotlinExtensionOrNull: KotlinProjectExtension? |
| get() = extensions.findByType() |
| |
| val Project.androidXExtension: AndroidXExtension |
| get() = extensions.getByType() |
| |
| @Suppress("UNCHECKED_CAST") |
| fun Project.getProjectsMap(): ConcurrentHashMap<String, String> { |
| project.rootProject.extra.set(ACCESSED_PROJECTS_MAP_KEY, true) |
| return rootProject.extra.get(PROJECTS_MAP_KEY) as ConcurrentHashMap<String, String> |
| } |
| |
| /** |
| * Configures all non-Studio tasks in a project (see b/153193718 for background) to time out after |
| * [TASK_TIMEOUT_MINUTES]. |
| */ |
| internal 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.disableJava8TargetObsoleteWarnings() { |
| afterEvaluate { |
| project.tasks.withType(JavaCompile::class.java).configureEach { task -> |
| // JDK 21 considers Java 8 an obsolete source and target value. Disable this warning. |
| task.options.compilerArgs.add("-Xlint:-options") |
| } |
| } |
| } |
| |
| private fun Project.configureJavaCompilationWarnings(androidXExtension: AndroidXExtension) { |
| afterEvaluate { |
| project.tasks.withType(JavaCompile::class.java).configureEach { task -> |
| // If we're running a hypothetical test build confirming that tip-of-tree versions |
| // are compatible, then we're not concerned about warnings |
| if (!project.usingMaxDepVersions()) { |
| task.options.compilerArgs.add("-Xlint:unchecked") |
| if (androidXExtension.failOnDeprecationWarnings) { |
| task.options.compilerArgs.add("-Xlint:deprecation") |
| } |
| } |
| } |
| } |
| } |
| |
| fun Project.hasBenchmarkPlugin(): Boolean { |
| return this.plugins.hasPlugin(BenchmarkPlugin::class.java) |
| } |
| |
| fun Project.isMacrobenchmark(): Boolean { |
| return this.path.endsWith("macrobenchmark") |
| } |
| |
| /** |
| * 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 String.asFilenamePrefix(): String { |
| return this.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) } |
| } |
| |
| fun Project.validateMultiplatformPluginHasNotBeenApplied() { |
| if (plugins.hasPlugin(KotlinMultiplatformPluginWrapper::class.java)) { |
| throw GradleException( |
| "The Kotlin multiplatform plugin should only be applied by the AndroidX plugin." |
| ) |
| } |
| } |
| |
| /** Verifies we don't accidentially write "implementation" instead of "commonMainImplementation" */ |
| fun Project.disallowAccidentalAndroidDependenciesInKmpProject( |
| androidXKmpExtension: AndroidXMultiplatformExtension |
| ) { |
| project.afterEvaluate { |
| if (androidXKmpExtension.supportedPlatforms.isNotEmpty()) { |
| val androidConfiguration = project.configurations.findByName("implementation") |
| if (androidConfiguration != null) { |
| if ( |
| androidConfiguration.dependencies.isNotEmpty() || |
| androidConfiguration.dependencyConstraints.isNotEmpty() |
| ) { |
| throw GradleException( |
| "The 'implementation' Configuration should not be used in a " + |
| "multiplatform project: this Configuration is declared by the " + |
| "Android plugin rather than the kmp plugin. Did you mean " + |
| "'commonMainImplementation'?" |
| ) |
| } |
| } |
| } |
| } |
| } |
| |
| /** Verifies that ProjectParser computes the correct values for this project */ |
| fun Project.validateProjectParser(androidXExtension: AndroidXExtension) { |
| // If configuration fails, we don't want to validate the ProjectParser |
| // (otherwise it could report a confusing, unnecessary error) |
| project.gradle.taskGraph.whenReady { |
| if (!androidXExtension.runProjectParser) return@whenReady |
| |
| val parsed = project.parse() |
| val errorPrefix = "ProjectParser error parsing ${project.path}." |
| check(androidXExtension.type == parsed.libraryType) { |
| "$errorPrefix Incorrectly computed libraryType = ${parsed.libraryType} " + |
| "instead of ${androidXExtension.type}" |
| } |
| check(androidXExtension.publish == parsed.publish) { |
| "$errorPrefix Incorrectly computed publish = ${parsed.publish} " + |
| "instead of ${androidXExtension.publish}" |
| } |
| check(androidXExtension.shouldPublish() == parsed.shouldPublish()) { |
| "$errorPrefix Incorrectly computed shouldPublish() = ${parsed.shouldPublish()} " + |
| "instead of ${androidXExtension.shouldPublish()}" |
| } |
| check(androidXExtension.shouldRelease() == parsed.shouldRelease()) { |
| "$errorPrefix Incorrectly computed shouldRelease() = ${parsed.shouldRelease()} " + |
| "instead of ${androidXExtension.shouldRelease()}" |
| } |
| check(androidXExtension.projectDirectlySpecifiesMavenVersion == parsed.specifiesVersion) { |
| "$errorPrefix Incorrectly computed specifiesVersion = ${parsed.specifiesVersion} " + |
| " instead of ${androidXExtension.projectDirectlySpecifiesMavenVersion}" |
| } |
| } |
| } |
| |
| /** Validates the Maven version against Jetpack guidelines. */ |
| fun AndroidXExtension.validateMavenVersion() { |
| val mavenGroup = mavenGroup |
| val mavenVersion = mavenVersion |
| val forcedVersion = mavenGroup?.atomicGroupVersion |
| if (forcedVersion != null && forcedVersion == mavenVersion) { |
| throw GradleException( |
| """ |
| Unnecessary override of same-group library version |
| |
| Project version is already set to $forcedVersion by same-version group |
| ${mavenGroup.group}. |
| |
| To fix this error, remove "mavenVersion = ..." from your build.gradle |
| configuration. |
| """ |
| .trimIndent() |
| ) |
| } |
| } |
| |
| /** Workaround for https://github.com/gradle/gradle/issues/27407 */ |
| fun Project.workaroundPrebuiltTakingPrecedenceOverProject() { |
| project.configurations.configureEach { configuration -> |
| configuration.resolutionStrategy.preferProjectModules() |
| } |
| } |
| |
| private fun Test.configureForRobolectric() { |
| // https://github.com/robolectric/robolectric/issues/7456 |
| jvmArgs = |
| listOf( |
| "--add-opens=java.base/java.lang=ALL-UNNAMED", |
| "--add-opens=java.base/java.util=ALL-UNNAMED", |
| "--add-opens=java.base/java.io=ALL-UNNAMED", |
| ) |
| // Robolectric 1.7 increased heap size requirements, see b/207169653. |
| maxHeapSize = "3g" |
| } |
| |
| private fun Project.enforceBanOnVersionRanges() { |
| configurations.configureEach { configuration -> |
| configuration.resolutionStrategy.eachDependency { dep -> |
| val target = dep.target |
| val version = target.version |
| // Enforce the ban on declaring dependencies with version ranges. |
| // Note: In playground, this ban is exempted to allow unresolvable prebuilts |
| // to automatically get bumped to snapshot versions via version range |
| // substitution. |
| if ( |
| version != null && |
| Version.isDependencyRange(version) && |
| project.rootProject.rootDir == project.getSupportRootFolder() |
| ) { |
| 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." |
| ) |
| } |
| } |
| } |
| } |
| |
| internal fun Project.hasAndroidMultiplatformPlugin(): Boolean = |
| extensions.findByType(AndroidXMultiplatformExtension::class.java)?.hasAndroidMultiplatform() |
| ?: false |
| |
| internal fun String.camelCase() = replaceFirstChar { |
| if (it.isLowerCase()) it.titlecase() else it.toString() |
| } |
| |
| const val PROJECT_OR_ARTIFACT_EXT_NAME = "projectOrArtifact" |