=
+ project.providers.gradleProperty(ADD_GROUP_CONSTRAINTS).map { s -> s.toBoolean() }.orElse(true)
+
+/**
+ * Returns alternative project url that will be used as "url" property in publishing maven artifact
+ * metadata.
+ *
+ * Returns null if there is no alternative project url.
+ */
+fun Project.getAlternativeProjectUrl(): String? =
+ project.providers.gradleProperty(ALTERNATIVE_PROJECT_URL).getOrNull()
+
+/** Validate the project structure against Jetpack guidelines */
+fun Project.isValidateProjectStructureEnabled(): Boolean =
+ findBooleanProperty(VALIDATE_PROJECT_STRUCTURE) ?: true
+
+/**
+ * Validates that all properties passed by the user of the form "-Pandroidx.*" are not misspelled
+ */
+fun Project.validateAllAndroidxArgumentsAreRecognized() {
+ for (propertyName in project.properties.keys) {
+ if (propertyName.startsWith("androidx")) {
+ if (!ALL_ANDROIDX_PROPERTIES.contains(propertyName)) {
+ val message =
+ "Unrecognized Androidx property '$propertyName'.\n" +
+ "\n" +
+ "Is this a misspelling? All recognized Androidx properties:\n" +
+ ALL_ANDROIDX_PROPERTIES.joinToString("\n") +
+ "\n" +
+ "\n" +
+ "See AndroidXGradleProperties.kt if you need to add this property to " +
+ "the list of known properties."
+ throw GradleException(message)
+ }
+ }
+ }
+}
+
+/**
+ * Returns whether tests in the project should display output. Build server scripts generally set
+ * displayTestOutput to false so that their failing test results aren't considered build failures,
+ * and instead pass their test failures on via build artifacts to be tracked and displayed on test
+ * dashboards in a different format
+ */
+fun Project.isDisplayTestOutput(): Boolean = findBooleanProperty(DISPLAY_TEST_OUTPUT) ?: true
+
+/**
+ * Returns whether the project should write versioned API files, e.g. `1.1.0-alpha01.txt`.
+ *
+ *
+ * When set to `true`, the `updateApi` task will write the current API surface to both `current.txt`
+ * and `.txt`. When set to `false`, only `current.txt` will be written. The default value
+ * is `true`.
+ */
+fun Project.isWriteVersionedApiFilesEnabled(): Boolean =
+ findBooleanProperty(WRITE_VERSIONED_API_FILES) ?: true
+
+/** Returns whether the build is for checking forward compatibility across projects */
+fun Project.usingMaxDepVersions(): Provider {
+ return project.providers.gradleProperty(USE_MAX_DEP_VERSIONS).map { true }.orElse(false)
+}
+
+/** Returns whether we should use the offline mirror for dependencies */
+fun Project.useYarnOffline() = findBooleanProperty(YARN_OFFLINE_MODE) ?: false
+
+/**
+ * Returns whether this is an integration test that is allowing lint checks to be skipped to save
+ * configuration time.
+ */
+fun Project.allowMissingLintProject() =
+ findBooleanProperty(ALLOW_MISSING_LINT_CHECKS_PROJECT) ?: false
+
+fun Project.findBooleanProperty(propName: String): Boolean? =
+ project.providers.gradleProperty(propName).map { it.toBoolean() }.getOrNull()
diff --git a/buildSrc-fork/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc-fork/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
new file mode 100644
index 0000000000000..ccf69d5a98361
--- /dev/null
+++ b/buildSrc-fork/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -0,0 +1,1659 @@
+/*
+ * 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.build.AndroidXImplPlugin.Companion.TASK_TIMEOUT_MINUTES
+import androidx.build.ProjectLayoutType.Companion.isJetBrainsFork
+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.dependencyTracker.AffectedModuleDetector
+import androidx.build.docs.CheckTipOfTreeDocsTask.Companion.setUpCheckDocsTask
+import androidx.build.gitclient.getHeadShaProvider
+import androidx.build.gradle.isRoot
+import androidx.build.kythe.configureProjectForKzipTasks
+import androidx.build.license.addLicensesToPublishedArtifacts
+import androidx.build.lint.ValidateLintChecks
+import androidx.build.resources.configurePublicResourcesStub
+import androidx.build.sbom.configureSbomPublishing
+import androidx.build.sbom.validateAllArchiveInputsRecognized
+import androidx.build.sources.configureMultiplatformSourcesForAndroid
+import androidx.build.sources.configureSourceJarForAndroid
+import androidx.build.sources.configureSourceJarForJava
+import androidx.build.sources.configureSourceJarForMultiplatform
+import androidx.build.sources.registerValidateMultiplatformSourceSetNamingTask
+import androidx.build.studio.StudioTask
+import androidx.build.testConfiguration.addAppApkToTestConfigGeneration
+import androidx.build.testConfiguration.addToModuleInfo
+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.attributes.BuildTypeAttr
+import com.android.build.api.dsl.AarMetadata
+import com.android.build.api.dsl.ApplicationExtension
+import com.android.build.api.dsl.KotlinMultiplatformAndroidDeviceTestCompilation
+import com.android.build.api.dsl.KotlinMultiplatformAndroidHostTestCompilation
+import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
+import com.android.build.api.dsl.LibraryExtension
+import com.android.build.api.dsl.TestBuildType
+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.LibraryVariant
+import com.android.build.api.variant.LibraryVariantBuilder
+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.google.devtools.ksp.gradle.KspExtension
+import com.google.devtools.ksp.gradle.KspGradleSubplugin
+import com.google.protobuf.gradle.ProtobufExtension
+import com.google.protobuf.gradle.ProtobufPlugin
+import java.io.File
+import java.time.Duration
+import java.util.Locale
+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.artifacts.ExternalDependency
+import org.gradle.api.attributes.Category
+import org.gradle.api.attributes.Usage
+import org.gradle.api.configuration.BuildFeatures
+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.testing.AbstractTestTask
+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.create
+import org.gradle.kotlin.dsl.dependencies
+import org.gradle.kotlin.dsl.findByType
+import org.gradle.kotlin.dsl.getByType
+import org.gradle.kotlin.dsl.named
+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.gradle.process.CommandLineArgumentProvider
+import org.jetbrains.androidx.build.jetBrainsGetDefaultAndroidBaseJavaVersion
+import org.jetbrains.androidx.build.jetBrainsGetDefaultTargetJavaVersion
+import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
+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.KotlinBaseApiPlugin
+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.KotlinSourceSet
+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() : Plugin {
+ @get:Inject abstract val registry: BuildEventsListenerRegistry
+ @get:Inject abstract val buildFeatures: BuildFeatures
+
+ override fun apply(project: Project) {
+ if (project.isRoot)
+ throw Exception("Root project should use AndroidXRootImplPlugin instead")
+ val androidXExtension = initializeAndroidXExtension(project)
+
+ val androidXKmpExtension =
+ project.extensions.create(
+ 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 ->
+ 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 KspGradleSubplugin -> configureWithKspPlugin(project)
+ is KotlinMultiplatformAndroidPlugin ->
+ configureWithKotlinMultiplatformAndroidPlugin(
+ project,
+ androidXKmpExtension.agpKmpExtension,
+ androidXExtension,
+ )
+ is KotlinBasePluginWrapper,
+ is KotlinBaseApiPlugin ->
+ configureWithKotlinPlugin(
+ project,
+ androidXExtension,
+ plugin,
+ androidXKmpExtension,
+ )
+ is ProtobufPlugin -> configureProtobufPlugin(project)
+ }
+ }
+
+ project.configureLint()
+ project.configureKtfmt()
+ project.configureKotlinVersion()
+ project.configureJavaFormat()
+
+ // 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")
+ // copy host side test results to DIST
+ project.tasks.withType(AbstractTestTask::class.java) { task ->
+ configureTestTask(project, task, allHostTests, androidXExtension)
+ }
+
+ project.configureTaskTimeouts()
+ project.configureMavenArtifactUpload(androidXExtension, androidXKmpExtension) {
+ if (buildFeatures.isIsolatedProjectsEnabled()) return@configureMavenArtifactUpload
+ project.addCreateLibraryBuildInfoFileTasks(androidXExtension, androidXKmpExtension)
+ }
+ project.configureProjectStructureValidation(androidXExtension)
+ project.configureProjectVersionValidation(androidXExtension)
+ 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().get()) {
+ project.configureSbomPublishing(androidXExtension.isIsolatedProjectsEnabled())
+ }
+ if (androidXExtension.shouldPublish.get()) {
+ project.validatePublishedMultiplatformHasDefault()
+ project.addLicensesToPublishedArtifacts(androidXExtension.license)
+ project.registerValidateRelocatedDependenciesTask()
+ }
+ project.registerValidateMultiplatformSourceSetNamingTask()
+ project.validateLintVersionTestExists(androidXExtension)
+ }
+ TaskUpToDateValidator.setup(project, registry)
+
+ project.workaroundAndroidXDependencyResolutions()
+ project.configureSamplesProject()
+ project.configureMaxDepVersions(androidXExtension)
+ project.configureUnzipChromeBuildService()
+
+ project.configureDependencyAnalysisPlugin()
+ }
+
+ private fun initializeAndroidXExtension(project: Project): AndroidXExtension {
+ val versionService = LibraryVersionsService.registerOrGet(project).get()
+ val listProjectsService = ListProjectsService.registerOrGet(project)
+ return project.extensions
+ .create(
+ EXTENSION_NAME,
+ project,
+ versionService.libraryVersions,
+ versionService.libraryGroups.values.toList(),
+ versionService.libraryGroupsByGroupId,
+ versionService.overrideLibraryGroupsByProjectPath,
+ listProjectsService.map { it.allPossibleProjects },
+ { project.getHeadShaProvider() },
+ { configurationName: String ->
+ configureAarAsJarForConfiguration(project, configurationName)
+ },
+ )
+ .apply { kotlinTarget.set(KotlinTarget.DEFAULT) }
+ }
+
+ /**
+ * 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 configureTestTask(
+ project: Project,
+ task: AbstractTestTask,
+ anchorTask: TaskProvider,
+ androidXExtension: AndroidXExtension,
+ ) {
+ if (isJetBrainsFork(project)) return
+ anchorTask.configure { it.dependsOn(task) }
+ val xmlReportDestDir = project.getHostTestResultDirectory()
+ val testName = "${project.path}:${task.name}"
+ project.addToModuleInfo(testName, buildFeatures.isIsolatedProjectsEnabled())
+ androidXExtension.testModuleNames.add(testName)
+ 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")
+ AffectedModuleDetector.configureTaskGuard(it)
+ }
+ task.finalizedBy(zipXmlTask)
+ }
+ }
+ }
+
+ /** Configures the project to use the Kotlin version specified by `androidx.kotlinTarget`. */
+ private fun Project.configureKotlinVersion() {
+ val kotlinVersionStringProvider = androidXConfiguration.kotlinBomVersion
+
+ // Resolve unspecified Kotlin versions to the target version.
+ // TODO(b/443037365): Remove when bug fixed as built-in Kotlin would handle this
+ configurations.configureEach { configuration ->
+ configuration.withDependencies { dependencySet ->
+ dependencySet.filterIsInstance().forEach { dependency ->
+ if (
+ dependency.group == "org.jetbrains.kotlin" &&
+ dependency.version.isNullOrEmpty()
+ ) {
+ project.dependencies.constraints.add(
+ configuration.name,
+ dependency.module.toString(),
+ ) {
+ it.version { constraint ->
+ constraint.require(kotlinVersionStringProvider.get())
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fun Provider.toKotlinVersionProvider() = map { version ->
+ KotlinVersion.fromVersion(version.substringBeforeLast('.'))
+ }
+
+ // Set the Kotlin compiler's API and language version to ensure bytecode is compatible.
+ val kotlinVersionProvider = kotlinVersionStringProvider.toKotlinVersionProvider()
+ tasks.configureEach { task ->
+ if (task is KotlinCompilationTask<*>) {
+ task.compilerOptions.apiVersion.set(kotlinVersionProvider)
+ task.compilerOptions.languageVersion.set(kotlinVersionProvider)
+ }
+ }
+
+ // 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()
+ }
+ if (evaluatedProject.androidXExtension.shouldPublish.get()) {
+ tasks.register(
+ CheckKotlinApiTargetTask.TASK_NAME,
+ CheckKotlinApiTargetTask::class.java,
+ ) {
+ it.kotlinTarget.set(kotlinVersionProvider)
+ it.outputFile.set(layout.buildDirectory.file("kotlinApiTargetCheckReport.txt"))
+ }
+ addToBuildOnServer(CheckKotlinApiTargetTask.TASK_NAME)
+ }
+ }
+
+ // 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(
+ "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: Any,
+ androidXMultiplatformExtension: AndroidXMultiplatformExtension,
+ ) {
+ val targetsAndroid =
+ project.provider {
+ project.plugins.hasPlugin(LibraryPlugin::class.java) ||
+ project.plugins.hasPlugin(AppPlugin::class.java) ||
+ project.plugins.hasPlugin(TestPlugin::class.java) ||
+ project.plugins.hasPlugin(KotlinMultiplatformAndroidPlugin::class.java)
+ }
+ val defaultJavaTargetVersion =
+ androidXExtension.type.map {
+ jetBrainsGetDefaultTargetJavaVersion(it, project).toString()
+ }
+ val defaultJvmTarget = defaultJavaTargetVersion.map { JvmTarget.fromTarget(it) }
+ if (plugin is KotlinMultiplatformPluginWrapper) {
+ project.extensions.getByType().apply {
+ targets.withType().configureEach { t ->
+ t.compilations.configureEach { compilation ->
+ // Replace with compilation.compileJavaTaskProvider?.configure {}
+ // when b/438995010 is fixed
+ @Suppress("DEPRECATION")
+ compilation.compilerOptions.configure { jvmTarget.set(defaultJvmTarget) }
+ compilation.compileTaskProvider.configure {
+ it.compilerOptions.jvmTarget.set(defaultJvmTarget)
+ }
+ }
+ }
+ targets.withType(KotlinJvmTarget::class.java).configureEach { target ->
+ val defaultTargetVersionForNonAndroidTargets =
+ androidXExtension.type.map {
+ jetBrainsGetDefaultTargetJavaVersion(
+ softwareType = it,
+ project = project,
+ targetName = target.name,
+ )
+ .toString()
+ }
+ val defaultJvmTargetForNonAndroidTargets =
+ defaultTargetVersionForNonAndroidTargets.map { JvmTarget.fromTarget(it) }
+ target.compilations.configureEach { compilation ->
+ compilation.compileJavaTaskProvider?.configure { javaCompile ->
+ javaCompile.targetCompatibility =
+ defaultTargetVersionForNonAndroidTargets.get()
+ javaCompile.sourceCompatibility =
+ defaultTargetVersionForNonAndroidTargets.get()
+ }
+ compilation.compileTaskProvider.configure { kotlinCompile ->
+ kotlinCompile.compilerOptions {
+ jvmTarget.set(defaultJvmTargetForNonAndroidTargets)
+ // Set jdk-release version for non-Android KMP targets
+ freeCompilerArgs.add(
+ defaultTargetVersionForNonAndroidTargets.map {
+ "-Xjdk-release=$it"
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+ } else {
+ project.tasks.withType(KotlinJvmCompile::class.java).configureEach { task ->
+ task.compilerOptions.jvmTarget.set(defaultJvmTarget)
+ task.compilerOptions.freeCompilerArgs.addAll(
+ targetsAndroid.zip(defaultJavaTargetVersion) { targetsAndroid, version ->
+ if (targetsAndroid) {
+ emptyList()
+ } else {
+ // Set jdk-release version for non-Android JVM projects
+ listOf("-Xjdk-release=$version")
+ }
+ }
+ )
+ }
+ }
+ project.tasks.withType(KotlinCompile::class.java).configureEach { task ->
+ val kotlinCompilerArgs =
+ project.provider {
+ val args =
+ mutableListOf(
+ "-Xskip-metadata-version-check",
+ "-jvm-default=no-compatibility",
+ )
+ if (androidXExtension.type.get().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
+ args +=
+ listOf(
+ "-Xno-param-assertions",
+ "-Xno-call-assertions",
+ "-Xno-receiver-assertions",
+ )
+ }
+
+ args
+ }
+ task.compilerOptions.freeCompilerArgs.addAll(kotlinCompilerArgs)
+ }
+ if (plugin is KotlinMultiplatformPluginWrapper) {
+ KonanPrebuiltsSetup.configureKonanDirectory(project)
+ project.afterEvaluate {
+ val libraryExtension = project.extensions.findByType()
+ 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 == "androidSourcesJar" || 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
+ }
+ }
+ }
+ }
+
+ project.afterEvaluate {
+ val kotlinExtension = project.kotlinExtensionOrNull
+ kotlinExtension?.explicitApi =
+ if (androidXExtension.shouldEnforceKotlinStrictApiMode().get()) {
+ ExplicitApiMode.Strict
+ } else {
+ ExplicitApiMode.Disabled
+ }
+ if (plugin is KotlinBaseApiPlugin) {
+ // TODO(b/443080559): Remove when built-in Kotlin adds kotlin-test-junit
+ // automatically
+ (kotlinExtension as KotlinAndroidProjectExtension)
+ .target
+ .compilations
+ .configureEach { compilation ->
+ if (!compilation.name.contains("test", ignoreCase = true))
+ return@configureEach
+ compilation.defaultSourceSet.dependencies {
+ implementation(kotlin("test-junit"))
+ }
+ }
+ }
+ }
+ }
+
+ private fun configureWithAppPlugin(project: Project, androidXExtension: AndroidXExtension) {
+ project.extensions.getByType().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().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(project.getKeystore()) }
+ }
+
+ project.configureJavaCompilationWarnings(
+ androidXExtension = androidXExtension,
+ isTestApp = true,
+ )
+ project.buildOnServerDependsOnAssembleRelease()
+ }
+
+ private fun configureWithTestPlugin(project: Project, androidXExtension: AndroidXExtension) {
+ project.extensions.getByType().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(
+ androidXExtension.isIsolatedProjectsEnabled(),
+ androidXExtension,
+ )
+ project.addAppApkToTestConfigGeneration(androidXExtension)
+ excludeVersionFiles(packaging.resources)
+ }
+ project.configureJavaCompilationWarnings(androidXExtension)
+ }
+
+ private fun configureWithKspPlugin(project: Project) =
+ project.extensions.getByType().useKsp2.set(true)
+
+ private fun configureCommonAndroidLibrary(
+ project: Project,
+ androidXExtension: AndroidXExtension,
+ androidComponents:
+ AndroidComponentsExtension<*, out LibraryVariantBuilder, out LibraryVariant>,
+ ) {
+ androidComponents.onVariants { variant ->
+ variant.configureTests(project.getKeystore())
+ variant.enableMicrobenchmarkInternalDefaults(project)
+ project.validateKotlinModuleFiles(
+ variant.name,
+ variant.artifacts.get(SingleArtifact.AAR),
+ )
+ }
+
+ project.disableStrictVersionConstraints()
+ project.configureJavaCompilationWarnings(androidXExtension)
+ project.setUpCheckDocsTask(androidXExtension)
+ }
+
+ private fun KotlinSourceSet.includesSourceSet(otherName: String): Boolean =
+ name == otherName || dependsOn.any { it.includesSourceSet(otherName) }
+
+ private fun AarMetadata.configure(compileSdk: Int?) {
+ // Taken from
+ // https://developer.android.com/build/releases/gradle-plugin#api-level-support
+ fun mapToMinAgpVersion(compileSdk: Int): String {
+ return when (compileSdk) {
+ 33 -> "7.2.0"
+ 34 -> "8.1.1"
+ 35 -> "8.6.0"
+ 36 -> "8.9.1"
+ 37 -> "9.1.0"
+ else -> throw Exception("Unknown compileSdk to minAgpVersion mapping")
+ }
+ }
+
+ // 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.
+ if (compileSdk == null) return
+ minCompileSdk = compileSdk
+ minAgpVersion = mapToMinAgpVersion(compileSdk)
+ }
+
+ private fun configureWithKotlinMultiplatformAndroidPlugin(
+ project: Project,
+ kotlinMultiplatformAndroidTarget: KotlinMultiplatformAndroidLibraryTarget,
+ androidXExtension: AndroidXExtension,
+ ) {
+ val kotlinMultiplatformAndroidComponentsExtension =
+ project.extensions.getByType()
+ kotlinMultiplatformAndroidTarget.configureAndroidBaseOptions(
+ project,
+ kotlinMultiplatformAndroidComponentsExtension,
+ androidXExtension,
+ )
+ configureCommonAndroidLibrary(
+ project,
+ androidXExtension,
+ kotlinMultiplatformAndroidComponentsExtension,
+ )
+ kotlinMultiplatformAndroidComponentsExtension.apply {
+ finalizeDsl {
+ it.aarMetadata.configure(it.compileSdk)
+ it.lint.targetSdk = project.defaultAndroidConfig.targetSdk
+ project.setUpBlankProguardFileForKmpAarIfNeeded(
+ kotlinMultiplatformAndroidTarget.optimization.consumerKeepRules
+ )
+ }
+ }
+
+ kotlinMultiplatformAndroidComponentsExtension.onVariants { variant ->
+ project.configureProjectForApiTasks(
+ AndroidMultiplatformApiTaskConfig(variant),
+ androidXExtension,
+ )
+ project.configureProjectForKzipTasks(
+ AndroidMultiplatformApiTaskConfig(variant),
+ androidXExtension,
+ )
+ project.configurePublicResourcesStub(variant)
+ project.configureMultiplatformSourcesForAndroid(androidXExtension.samplesProjects)
+ }
+
+ project.configureVersionFileWriter(project.multiplatformExtension!!, androidXExtension)
+
+ project.configureDependencyVerification(androidXExtension) { taskProvider ->
+ kotlinMultiplatformAndroidTarget.compilations.configureEach {
+ taskProvider.configure { task -> task.dependsOn(it.compileTaskProvider) }
+ }
+ }
+ project.afterEvaluate {
+ project.addToBuildOnServer("assembleAndroidMain")
+ project.addToBuildOnServer("lint")
+ // Created to be consumed by docs-tip-of-tree
+ project.configurations.register("androidIntermediates") {
+ it.isCanBeResolved = false
+ it.attributes.attribute(
+ Usage.USAGE_ATTRIBUTE,
+ project.objects.named(Usage.JAVA_RUNTIME),
+ )
+ it.attributes.attribute(
+ Category.CATEGORY_ATTRIBUTE,
+ project.objects.named(Category.LIBRARY),
+ )
+ it.attributes.attribute(
+ BuildTypeAttr.ATTRIBUTE,
+ project.objects.named("release"),
+ )
+ // disable, as it triggers android compilation during IDEA sync
+ if (!isJetBrainsFork(project)) it.outgoing.artifact(project.tasks.named("createFullJarAndroidMain"))
+ }
+ }
+ }
+
+ private fun configureProtobufPlugin(project: Project) {
+ project.extensions.getByType(ProtobufExtension::class.java).apply {
+ protoc { it.artifact = project.getLibraryByName("protobufCompiler").toString() }
+ generateProtoTasks {
+ it.all().configureEach { task ->
+ // java projects have "java" output enabled, however Android projects do not
+ // so we need to create it for Android projects.
+ // https://github.com/google/protobuf-gradle-plugin?tab=readme-ov-file#default-outputs
+ val java =
+ if (
+ project.plugins.hasPlugin("com.android.library") ||
+ project.plugins.hasPlugin("com.android.application")
+ ) {
+ task.builtins.register("java")
+ } else task.builtins.named("java")
+ java.configure { options -> options.option("lite") }
+ }
+ }
+ }
+ }
+
+ /**
+ * 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 HasDeviceTests.configureTests(keystore: File) {
+ 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")
+
+ // AGP is unable to merge these and multiple artifacts ship this files
+ // e.g. org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar
+ // org/bouncycastle/bcprov-jdk18on/1.78.1/bcprov-jdk18on-1.78.1.jar
+ pickFirsts.add("META-INF/versions/9/OSGI-INF/MANIFEST.MF")
+ }
+ }
+ }
+
+ private fun configureWithLibraryPlugin(project: Project, androidXExtension: AndroidXExtension) {
+ val buildTypeForTests = "release"
+ val libraryExtension = project.extensions.getByType()
+ libraryExtension.apply {
+ publishing { singleVariant(DEFAULT_PUBLISH_CONFIG) }
+
+ configureAndroidBaseOptions(project, androidXExtension)
+ 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
+ }
+ testBuildType = buildTypeForTests
+ project.configureTestConfigGeneration(
+ androidXExtension.isIsolatedProjectsEnabled(),
+ androidXExtension,
+ )
+ project.addAppApkToTestConfigGeneration(androidXExtension)
+ }
+
+ val libraryAndroidComponentsExtension =
+ project.extensions.getByType()
+ configureCommonAndroidLibrary(project, androidXExtension, libraryAndroidComponentsExtension)
+
+ libraryAndroidComponentsExtension.apply {
+ finalizeDsl {
+ it.defaultConfig.aarMetadata.configure(it.compileSdk)
+ project.setUpBlankProguardFileForAarIfNeeded(it.defaultConfig)
+ it.lint.targetSdk = project.defaultAndroidConfig.targetSdk
+ it.testOptions.targetSdk = project.defaultAndroidConfig.targetSdk
+ // Replace with a public API once available, see b/360392255
+ it.buildTypes.configureEach { buildType ->
+ if (buildType.name == buildTypeForTests && !project.hasBenchmarkPlugin())
+ (buildType as TestBuildType).isDebuggable = true
+ }
+ }
+ // Disable debug build type for Android Libraries
+ beforeVariants(selector().withBuildType("debug")) { variant -> variant.enable = false }
+ }
+
+ project.configureVersionFileWriter(libraryAndroidComponentsExtension, androidXExtension)
+
+ val prebuiltLibraries = listOf("libtracing_perfetto.so", "libc++_shared.so")
+ libraryAndroidComponentsExtension.onVariants { variant ->
+ if (variant.buildType == DEFAULT_PUBLISH_CONFIG) {
+ // Standard docs, resource API, and Metalava configuration for AndroidX projects.
+ project.configureProjectForApiTasks(
+ LibraryApiTaskConfig(variant),
+ androidXExtension,
+ )
+ project.configureProjectForKzipTasks(
+ LibraryApiTaskConfig(variant),
+ androidXExtension,
+ )
+ }
+ if (variant.name == DEFAULT_PUBLISH_CONFIG) {
+ project.configureSourceJarForAndroid(variant, androidXExtension.samplesProjects)
+ project.configurePublicResourcesStub(variant)
+ project.configureDependencyVerification(androidXExtension) { taskProvider ->
+ taskProvider.configure { task -> task.dependsOn("compileReleaseJavaWithJavac") }
+ }
+ }
+ 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.buildOnServerDependsOnAssembleRelease()
+ }
+
+ private fun configureGradlePluginPlugin(project: Project) {
+ project.tasks.withType(ValidatePlugins::class.java).configureEach {
+ it.enableStricterValidation.set(true)
+ it.failOnWarning.set(true)
+ }
+ project.addToBuildOnServer("validatePlugins")
+ SdkResourceGenerator.generateForHostTest(project)
+ }
+
+ private fun configureWithJavaPlugin(project: Project, androidXExtension: AndroidXExtension) {
+ if (
+ project.multiplatformExtension != null &&
+ !project.multiplatformExtension!!.hasJvmTarget()
+ ) {
+ return
+ }
+ project.configureErrorProneForJava()
+
+ // Force Java 1.8 source- and target-compatibility for all Java libraries.
+ val javaExtension = project.extensions.getByType()
+ project.afterEvaluate {
+ javaExtension.apply {
+ val defaultTargetJavaVersion =
+ jetBrainsGetDefaultTargetJavaVersion(androidXExtension.type.get(), project)
+ sourceCompatibility = defaultTargetJavaVersion
+ targetCompatibility = defaultTargetJavaVersion
+ }
+ if (
+ !project.plugins.hasPlugin(KotlinBasePluginWrapper::class.java) ||
+ !project.plugins.hasPlugin(KotlinBaseApiPlugin::class.java)
+ ) {
+ project.configureSourceJarForJava(androidXExtension.samplesProjects)
+ }
+ }
+
+ project.setUpBlankProguardFileForJarIfNeeded(javaExtension)
+ project.configureJavaCompilationWarnings(androidXExtension)
+
+ if (
+ project.multiplatformExtension == null ||
+ project.multiplatformExtension!!.hasJavaEnabled()
+ ) {
+ 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.configureProjectForKzipTasks(apiTaskConfig, androidXExtension)
+ project.setUpCheckDocsTask(androidXExtension)
+
+ 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)
+ }
+ }
+ }
+ }
+
+ private fun Project.configureProjectStructureValidation(androidXExtension: AndroidXExtension) {
+ if (isJetBrainsFork(project)) return
+ // AndroidXExtension.mavenGroup is not readable until afterEvaluate.
+ afterEvaluate {
+ val mavenGroup = androidXExtension.mavenGroup
+ val type = androidXExtension.type.get()
+ val isProbablyPublished =
+ type == SoftwareType.PUBLISHED_LIBRARY ||
+ type == SoftwareType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
+ if (
+ mavenGroup != null && isProbablyPublished && androidXExtension.shouldPublish.get()
+ ) {
+ 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 Any.configureAndroidBaseOptions(
+ project: Project,
+ androidXExtension: AndroidXExtension,
+ ) {
+ // Workaround to avoid specifying the parametrized types of CommonExtension explicitly
+ // So we can clean up the parameters in AGP
+ // The compiler can infer that this is CommonExtension from these checks
+ if (this !is ApplicationExtension && this !is LibraryExtension && this !is TestExtension) {
+ throw IllegalArgumentException("Unexpected extension: $this")
+ }
+ compileOptions.apply {
+ sourceCompatibility = jetBrainsGetDefaultAndroidBaseJavaVersion(project)
+ targetCompatibility = jetBrainsGetDefaultAndroidBaseJavaVersion(project)
+ }
+
+ val defaultMinSdk = project.defaultAndroidConfig.minSdk
+
+ // Suppress output of android:compileSdkVersion and related attributes (b/277836549).
+ androidResources.additionalParameters += "--no-compile-sdk-metadata"
+
+ compileSdk = project.defaultAndroidConfig.compileSdk
+
+ buildToolsVersion = project.defaultAndroidConfig.buildToolsVersion
+
+ defaultConfig.ndk.abiFilters.addAll(SUPPORTED_BUILD_ABIS)
+ defaultConfig.minSdk = defaultMinSdk
+ defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+ testOptions.animationsDisabled = !project.isMacrobenchmark()
+
+ project.afterEvaluate {
+ check(
+ !androidXExtension.shouldPublish.get() ||
+ !compileOptions.isCoreLibraryDesugaringEnabled
+ ) {
+ "AndroidX libraries are not permitted to use core library desugaring as it " +
+ "forces library users to also enable core library desugaring."
+ }
+
+ val minSdkVersion = defaultConfig.minSdk!!
+ check(minSdkVersion >= defaultMinSdk) {
+ "minSdkVersion $minSdkVersion lower than the default of $defaultMinSdk"
+ }
+ project.enforceBanOnVersionRanges()
+
+ if (androidXExtension.type.get().compilationTarget != CompilationTarget.DEVICE) {
+ throw IllegalStateException(
+ "${androidXExtension.type.get().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()
+ }
+ }
+
+ val componentsExtension =
+ project.extensions.getByType(AndroidComponentsExtension::class.java)
+ project.configureFtlRunner(componentsExtension)
+
+ // If a dependency is missing a debug variant, use release instead.
+ buildTypes.getByName("debug").matchingFallbacks.add("release")
+
+ // 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 KotlinMultiplatformAndroidLibraryTarget.configureAndroidBaseOptions(
+ project: Project,
+ componentsExtension: KotlinMultiplatformAndroidComponentsExtension,
+ androidXExtension: AndroidXExtension,
+ ) {
+ 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(KotlinMultiplatformAndroidDeviceTestCompilation::class.java)
+ .configureEach {
+ it.instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ it.animationsDisabled = true
+ }
+
+ withHostTestBuilder {} // enable Android host tests
+ withDeviceTestBuilder { sourceSetTreeName = "test" }
+ .configure { signing.storeFile = project.getKeystore() }
+ configureTargetSdkForTests(project.defaultAndroidConfig.targetSdk)
+
+ // 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"
+ }
+ project.enforceBanOnVersionRanges()
+ }
+
+ project.configureTestConfigGeneration(
+ buildFeatures.isIsolatedProjectsEnabled(),
+ androidXExtension,
+ )
+ project.configureFtlRunner(componentsExtension)
+ }
+
+ // TODO(b/425976012): Set targetSdkForTests to project.defaultAndroidConfig.targetSdk
+ private fun KotlinMultiplatformAndroidLibraryTarget.configureTargetSdkForTests(version: Int?) {
+ checkNotNull(version) {
+ "version must be set for tests. call `configureTargetSdkForTests` in the `finalizeDsl` block"
+ }
+ compilations
+ .withType(KotlinMultiplatformAndroidDeviceTestCompilation::class.java)
+ .configureEach { it.targetSdk { this.version = release(version) } }
+
+ compilations
+ .withType(KotlinMultiplatformAndroidHostTestCompilation::class.java)
+ .configureEach { it.targetSdk { this.version = release(version.coerceAtMost(35)) } }
+ }
+
+ /**
+ * 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 LibraryExtension.configureAndroidLibraryWithMultiplatformPluginOptions() {
+ sourceSets.findByName("main")!!.manifest.srcFile("src/androidMain/AndroidManifest.xml")
+ sourceSets
+ .findByName("androidTest")!!
+ .manifest
+ .srcFile("src/androidDeviceTest/AndroidManifest.xml")
+ }
+
+ private fun Project.configureKmp() {
+ val kmpExtension =
+ checkNotNull(project.extensions.findByType()) {
+ """
+ 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.compileTaskProvider.configure { task ->
+ task.compilerOptions.freeCompilerArgs.add("-Xexpect-actual-classes")
+ androidXConfiguration.kotlinApiVersion.let {
+ task.compilerOptions.apiVersion.set(it)
+ task.compilerOptions.languageVersion.set(it)
+ }
+ }
+ }
+ }
+ }
+
+ private fun ApplicationExtension.configureAndroidApplicationOptions(
+ project: Project,
+ androidXExtension: AndroidXExtension,
+ ) {
+ defaultConfig.apply {
+ versionCode = 1
+ versionName = "1.0"
+ }
+
+ project.configureTestConfigGeneration(
+ androidXExtension.isIsolatedProjectsEnabled(),
+ androidXExtension,
+ )
+ project.addAppApkToTestConfigGeneration(androidXExtension)
+ project.addAppApkToFtlRunner()
+ }
+
+ private fun Project.configureDependencyVerification(
+ androidXExtension: AndroidXExtension,
+ taskConfigurator: (TaskProvider) -> Unit,
+ ) {
+ if (buildFeatures.isIsolatedProjectsEnabled()) return
+ afterEvaluate {
+ if (androidXExtension.type.get().requiresDependencyVerification()) {
+ taskConfigurator(project.createVerifyDependencyVersionsTask())
+ }
+ }
+ }
+
+ // 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() || buildFeatures.isIsolatedProjectsEnabled()
+ ) {
+ 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 =
+ getSupportRootFolder().resolve(File(otherProject.filePath, "build.gradle"))
+ val parsed =
+ if (otherFilepath.exists()) {
+ parseBuildFile(otherFilepath)
+ } else {
+ parseBuildFile(
+ getSupportRootFolder()
+ .resolve(File(otherProject.filePath, "build.gradle.kts"))
+ )
+ }
+ if (!parsed.shouldRelease()) {
+ continue
+ }
+ if (parsed.softwareType == SoftwareType.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 = 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 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"
+
+ // b/366238650
+ val SUPPORTED_BUILD_ABIS = listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
+
+ /** Fail the build if a non-Studio task runs longer than expected */
+ const val TASK_TIMEOUT_MINUTES = 60L
+ }
+}
+
+internal fun aospGetDefaultTargetJavaVersion(
+ softwareType: SoftwareType,
+ projectName: String? = null,
+ targetName: String? = null,
+): JavaVersion {
+ return when {
+ // TODO(b/353328300): Move room-compiler-processing to Java 17 once Dagger is ready.
+ projectName != null && projectName.contains("room3-compiler-processing") -> VERSION_11
+ projectName != null && projectName.contains("desktop") -> VERSION_11
+ targetName != null && (targetName == "desktop" || targetName == "jvmStubs") -> VERSION_11
+ softwareType.compilationTarget == CompilationTarget.HOST -> VERSION_17
+ else -> VERSION_1_8
+ }
+}
+
+private fun Project.validateLintVersionTestExists(androidXExtension: AndroidXExtension) {
+ if (!androidXExtension.type.get().isLint()) {
+ return
+ }
+ kotlinExtensionOrNull?.let { extension ->
+ val validateLintChecks =
+ tasks.register("validateLintChecks", ValidateLintChecks::class.java) { task ->
+ task.cacheEvenIfNoOutputs()
+ task.sourceDirectories.from(
+ extension.sourceSets.flatMap { it.kotlin.sourceDirectories }
+ )
+ }
+ addToBuildOnServer(validateLintChecks)
+ }
+}
+
+/** Returns whether the configuration is used for testing. */
+private fun Configuration.isTest(): Boolean = name.lowercase().contains("test")
+
+/** Returns whether the configuration is part of publication. */
+internal fun Configuration.isPublished(): Boolean =
+ !isTest() && !name.lowercase().contains("metadata") && !name.endsWith("CInterop")
+
+internal val Project.androidExtension: AndroidComponentsExtension<*, *, *>
+ get() =
+ extensions.findByType()
+ ?: 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()
+
+/**
+ * Configures all non-Studio tasks in a project (see b/153193718 for background) to time out after
+ * [TASK_TIMEOUT_MINUTES].
+ */
+internal fun Project.configureTaskTimeouts() {
+ // A set of tasks that sometimes take >60 minutes. b/383874664
+ val slowTasks =
+ setOf(
+ ":compose:ui:ui:compileReleaseAndroidTestKotlinAndroid",
+ ":compose:foundation:foundation:compileReleaseAndroidTestKotlinAndroid",
+ ":compose:foundation:foundation:integration-tests:lazy-tests:compileReleaseAndroidTestKotlin",
+ )
+ 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(if (t.path in slowTasks) 80L else TASK_TIMEOUT_MINUTES)
+ )
+ }
+ }
+}
+
+private class JavaCompileArgumentProvider(
+ private val isTestApp: Boolean,
+ private val failOnDeprecationWarnings: Provider,
+ private val usingMaxDepVersions: Provider,
+) : CommandLineArgumentProvider {
+ override fun asArguments(): List {
+ // JDK 21 considers Java 8 an obsolete source and target value. Disable this warning.
+ val args = mutableListOf("-Xlint:-options")
+ // If we're running a hypothetical test build confirming that tip-of-tree versions
+ // are compatible, then we're not concerned about warnings
+ if (!usingMaxDepVersions.get() && !isTestApp) {
+ args.add("-Xlint:unchecked")
+ if (failOnDeprecationWarnings.get()) {
+ args.add("-Xlint:deprecation")
+ }
+ }
+ return args
+ }
+}
+
+private fun Project.configureJavaCompilationWarnings(
+ androidXExtension: AndroidXExtension,
+ isTestApp: Boolean = false,
+) {
+ project.tasks.withType(JavaCompile::class.java).configureEach { task ->
+ task.options.compilerArgumentProviders.add(
+ JavaCompileArgumentProvider(
+ isTestApp = isTestApp,
+ failOnDeprecationWarnings = androidXExtension.failOnDeprecationWarnings,
+ usingMaxDepVersions = usingMaxDepVersions(),
+ )
+ )
+ }
+}
+
+fun Project.hasBenchmarkPlugin(): Boolean {
+ return false
+}
+
+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 Project.addToCheckTask(task: TaskProvider) {
+ 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 that ProjectParser computes the correct values for this project */
+fun Project.validateProjectParser(androidXExtension: AndroidXExtension) {
+ if (isJetBrainsFork(project)) return
+ // If configuration fails, we don't want to validate the ProjectParser
+ // (otherwise it could report a confusing, unnecessary error)
+ project.gradle.taskGraph.whenReady {
+ val parsed = project.parse()
+ val errorPrefix = "ProjectParser error parsing ${project.path}."
+ check(androidXExtension.type.get() == parsed.softwareType) {
+ "$errorPrefix Incorrectly computed libraryType = ${parsed.softwareType} " +
+ "instead of ${androidXExtension.type.get()}"
+ }
+ check(androidXExtension.shouldPublish.get() == parsed.shouldPublish()) {
+ "$errorPrefix Incorrectly computed shouldPublish() = ${parsed.shouldPublish()} " +
+ "instead of ${androidXExtension.shouldPublish.get()}"
+ }
+ check(androidXExtension.shouldRelease.get() == parsed.shouldRelease()) {
+ "$errorPrefix Incorrectly computed shouldRelease() = ${parsed.shouldRelease()} " +
+ "instead of ${androidXExtension.shouldRelease.get()}"
+ }
+ 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()
+ )
+ }
+}
+
+/** Workarounds for configuration resolution */
+fun Project.workaroundAndroidXDependencyResolutions() {
+ project.configurations.configureEach { configuration ->
+ // https://github.com/gradle/gradle/issues/27407
+ configuration.resolutionStrategy.preferProjectModules()
+
+ // https://github.com/gradle/gradle/issues/7594
+ configuration.resolutionStrategy.eachDependency { dependency ->
+ if (dependency.requested.group.startsWith("androidx.")) {
+ // Drop aar classifier that comes from aar in POM files
+ // as it causes a bug in Gradle. Gradle does not actually need the
+ // classifiers for Android libraries for them to work correctly.
+ dependency.artifactSelection { it.withoutArtifactSelectors() }
+ }
+ }
+ }
+}
+
+private fun Project.configureUnzipChromeBuildService() {
+ if (ProjectLayoutType.isPlayground(this)) {
+ return
+ }
+ gradle.sharedServices.registerIfAbsent("unzipChrome", UnzipChromeBuildService::class.java) {
+ it.parameters.browserDir.set(File(getPrebuiltsRoot(), "androidx/chrome-for-testing/"))
+ it.parameters.unzipToDir.set(getOutDirectory().resolve("chrome-bin"))
+ }
+}
+
+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
+
+@Suppress("DEPRECATION")
+internal fun KotlinMultiplatformExtension.hasJavaEnabled(): Boolean =
+ targets.withType(KotlinJvmTarget::class.java).singleOrNull()?.withJavaEnabled ?: false
+
+internal fun KotlinMultiplatformExtension.hasJvmTarget(): Boolean =
+ targets.withType(KotlinJvmTarget::class.java).isEmpty().not()
+
+internal fun String.camelCase() = replaceFirstChar {
+ if (it.isLowerCase()) it.titlecase() else it.toString()
+}
diff --git a/buildSrc-fork/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt b/buildSrc-fork/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
new file mode 100644
index 0000000000000..93252641162c1
--- /dev/null
+++ b/buildSrc-fork/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
@@ -0,0 +1,1054 @@
+/*
+ * Copyright 2022 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.build.clang.AndroidXClang
+import androidx.build.clang.CombineObjectFilesTask
+import androidx.build.clang.KonanBuildService
+import androidx.build.clang.MultiTargetNativeCompilation
+import androidx.build.clang.NativeLibraryBundler
+import androidx.build.clang.configureCinterop
+import com.android.build.api.dsl.KotlinMultiplatformAndroidCompilation
+import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
+import com.android.build.gradle.api.KotlinMultiplatformAndroidPlugin
+import groovy.lang.Closure
+import java.io.File
+import javax.inject.Inject
+import org.gradle.api.Action
+import org.gradle.api.GradleException
+import org.gradle.api.NamedDomainObjectCollection
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.configuration.BuildFeatures
+import org.gradle.api.plugins.ExtensionAware
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.testing.Test
+import org.gradle.kotlin.dsl.the
+import org.gradle.kotlin.dsl.withType
+import org.jetbrains.androidx.build.configureForkWebTarget
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
+import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
+import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
+import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
+import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
+import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
+import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeCompilation
+import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
+import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetWithHostTests
+import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsTargetDsl
+import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinWasmTargetDsl
+import org.jetbrains.kotlin.gradle.targets.js.ir.DefaultIncrementalSyncTask
+import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsEnvSpec
+import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin
+import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest
+import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport
+import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin
+import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootEnvSpec
+import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
+import org.jetbrains.kotlin.gradle.targets.wasm.binaryen.BinaryenEnvSpec
+import org.jetbrains.kotlin.gradle.targets.wasm.binaryen.BinaryenPlugin
+import org.jetbrains.kotlin.gradle.targets.wasm.nodejs.WasmNodeJsEnvSpec
+import org.jetbrains.kotlin.gradle.targets.wasm.nodejs.WasmNodeJsPlugin
+import org.jetbrains.kotlin.gradle.targets.wasm.yarn.WasmYarnPlugin
+import org.jetbrains.kotlin.gradle.targets.wasm.yarn.WasmYarnRootEnvSpec
+import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
+import org.jetbrains.kotlin.konan.target.LinkerOutputKind
+
+/**
+ * [AndroidXMultiplatformExtension] is an extension that wraps specific functionality of the Kotlin
+ * multiplatform extension, and applies the Kotlin multiplatform plugin when it is used. The purpose
+ * of wrapping is to prevent targets from being added when the platform has not been enabled. e.g.
+ * the `macosX64` target is gated on a `project.enableMac` check.
+ */
+abstract class AndroidXMultiplatformExtension(val project: Project) {
+
+ @get:Inject abstract val buildFeatures: BuildFeatures
+
+ var enableBinaryCompatibilityValidator = true
+
+ /*
+ * Adds a kotlin stdlib klib directory as an input to test tasks.
+ * This is specifically useful for BCV, but it needs to be set up by our buildSrc code to
+ * make sure we use the correct installation and don't accidentally cause something to be
+ * downloaded from the internet.
+ *
+ * Sets the `kotlin.stdlib.klib.dir` property which can be accessed inside the tests
+ */
+ fun provideKlibStdLibForTests() {
+ val konanBuildService = KonanBuildService.obtain(project)
+ // directory format of stdlib klib for use during tests
+ val stdLibKlibDir =
+ konanBuildService.map { it.parameters.konanHome.dir("klib/common/stdlib") }
+ project.tasks.withType(Test::class.java).configureEach { task ->
+ task.inputs
+ .dir(stdLibKlibDir)
+ .withPropertyName("kotlinStdLib")
+ .withPathSensitivity(PathSensitivity.RELATIVE)
+ task.doFirst {
+ task.systemProperty(
+ "kotlin.stdlib.klib.dir",
+ stdLibKlibDir.get().get().asFile.absolutePath,
+ )
+ }
+ }
+ }
+
+ // Kotlin multiplatform plugin is only applied if at least one target / sourceset is added.
+ private val kotlinExtensionDelegate = lazy {
+ project.validateMultiplatformPluginHasNotBeenApplied()
+ project.plugins.apply(KotlinMultiplatformPluginWrapper::class.java)
+ project.multiplatformExtension!!.also { it.applyAndroidXDefaultHierarchyTemplate() }
+ }
+ private val kotlinExtension: KotlinMultiplatformExtension by kotlinExtensionDelegate
+ private val agpKmpExtensionDelegate = lazy {
+ // make sure to initialize the kotlin extension by accessing the property
+ val extension = (kotlinExtension as ExtensionAware)
+ project.plugins.apply(KotlinMultiplatformAndroidPlugin::class.java)
+ extension.extensions.getByType(KotlinMultiplatformAndroidLibraryTarget::class.java)
+ }
+
+ val agpKmpExtension: KotlinMultiplatformAndroidLibraryTarget by agpKmpExtensionDelegate
+
+ /**
+ * The list of platforms that have been declared as supported in the build configuration.
+ *
+ * This may be a superset of the currently enabled platforms in [targetPlatforms].
+ */
+ val supportedPlatforms: MutableSet = mutableSetOf()
+
+ /**
+ * Artifact-redirection (parallel-graph back-end): one entry per concrete target declared inside a
+ * `redirect { }` block. Each entry names a target that the fork builds *empty* (an empty,
+ * but valid, klib/jar/aar depending on the `androidx.*` coordinate) by re-rooting its
+ * source-sets onto an empty parallel graph (`redirectCommonMain`) instead of the real
+ * `commonMain`. The JetBrains plugin reads this registry in `afterEvaluate`.
+ *
+ * `redirectCoordinate` carries the `androidx.*` group from the `redirect("group") { }` argument
+ * (required) and the optional version override; when its version is null the back-end resolves it
+ * from the `[versions]` table of `redirectversions.toml`.
+ */
+ internal data class RedirectTargetDecl(
+ val targetName: String,
+ val redirectCoordinate: RedirectCoordinate
+ )
+
+ /** Targets registered for redirect via `redirect { }`. Consumed by the JetBrains plugin. */
+ internal val redirectTargetDecls: MutableList = mutableListOf()
+
+ /**
+ * Names of redirect targets, registered **before** the target is created (see `expectRedirect`).
+ * The hierarchy-template `excludeCompilations` predicate reads this to keep redirect targets out
+ * of the `commonMain` tree. Must be populated before the target's compilation is created, because
+ * the template evaluates the predicate at compilation-creation time.
+ */
+ internal val redirectTargetNames: MutableSet = mutableSetOf()
+
+ /** Pre-register expected redirect target names so the hierarchy predicate excludes them. */
+ private fun expectRedirect(vararg names: String) { redirectTargetNames += names }
+
+ /** The `androidx.*` coordinate a `redirect("group", version) { }` block points its targets at. */
+ internal data class RedirectCoordinate(val group: String, val version: String?)
+
+ // Ambient state for the `redirect { … }` scope: non-null while a redirect block is executing
+ // (holding that block's coordinate), null otherwise. A plain target function called inside the
+ // block sees it (via `potentiallyRedirecting`) and redirects its target to the coordinate instead
+ // of fork-building.
+ private var redirectCoordinate: RedirectCoordinate? = null
+
+ /**
+ * Empty parallel root for redirect targets. Created lazily on the first redirect target (declared
+ * inside `redirect { }`) so that redirect leaves can be wired to it **at target-creation time** —
+ * this is what keeps them off the real `commonMain`. KGP applies the default hierarchy template
+ * only when a source-set
+ * has no manual `dependsOn` edge; adding one here (synchronously, during configuration) opts the
+ * redirect leaf out of the auto-wiring to `commonMain`. Doing this in `afterEvaluate` is too late
+ * (the dependsOn closure is computed reactively on edge add and is not recomputed on removal).
+ */
+ private val redirectCommonMain: org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet by lazy {
+ kotlinExtension.sourceSets.maybeCreate("redirectCommonMain")
+ }
+
+ private fun recordRedirect(target: KotlinTarget, targetName: String, redirectCoordinate: RedirectCoordinate) {
+ // Invariant: the name `potentiallyRedirecting` pre-registered must match the created target,
+ // otherwise the hierarchy predicate excluded the wrong name from `commonMain`.
+ assert(target.name == targetName) {
+ "redirect target name mismatch: expected '$targetName' but created target is '${target.name}'"
+ }
+ redirectTargetNames += target.name
+ redirectTargetDecls += RedirectTargetDecl(target.name, redirectCoordinate)
+ // Wire the target's main compilation source-set to the parallel root up-front.
+ target.compilations.findByName("main")?.defaultSourceSet?.dependsOn(redirectCommonMain)
+ }
+
+ /**
+ * The list of platforms that are currently enabled.
+ *
+ * This will vary across build environments. For example, a project's build configuration may
+ * have requested `mac()` but this is not available when building on Linux.
+ */
+ val targetPlatforms: List
+ get() =
+ if (kotlinExtensionDelegate.isInitialized()) {
+ kotlinExtension.targets.mapNotNull {
+ if (it.targetName != "metadata") {
+ it.targetName
+ } else {
+ null
+ }
+ }
+ } else {
+ throw GradleException("Kotlin multi-platform extension has not been initialized")
+ }
+
+ /**
+ * Default platform identifier used for specifying POM dependencies.
+ *
+ * This platform will be added as a dependency to the multi-platform anchor artifact's POM
+ * publication. For example, if the anchor artifact is `collection` and the default platform is
+ * `jvm`, then the POM for `collection` will express a dependency on `collection-jvm`. This
+ * ensures that developers who are silently upgrade to KMP artifacts but are not using Gradle
+ * still see working artifacts.
+ *
+ * If no default was specified and a single platform is requested (ex. using [jvm]), returns the
+ * identifier for that platform.
+ */
+ var defaultPlatform: String? = null
+ get() = field ?: supportedPlatforms.singleOrNull()?.id
+ set(value) {
+ if (value != null) {
+ if (supportedPlatforms.none { it.id == value }) {
+ throw GradleException(
+ "Platform $value has not been requested as a target. " +
+ "Available platforms are: " +
+ supportedPlatforms.joinToString(", ") { it.id }
+ )
+ }
+ if (targetPlatforms.none { it == value }) {
+ throw GradleException(
+ "Platform $value is not available in this build " +
+ "environment. Available platforms are: " +
+ targetPlatforms.joinToString(", ")
+ )
+ }
+ }
+ field = value
+ }
+
+ val targets: NamedDomainObjectCollection
+ get() = kotlinExtension.targets
+
+ /** Helper class to access Clang functionality. */
+ private val clang = AndroidXClang(project)
+
+ /** Helper class to bundle outputs of clang compilation into an AAR / JAR. */
+ private val nativeLibraryBundler = NativeLibraryBundler(project)
+
+ internal fun hasNativeTarget(): Boolean {
+ // it is important to check initialized here not to trigger initialization
+ return kotlinExtensionDelegate.isInitialized() &&
+ targets.any { it.platformType == KotlinPlatformType.native }
+ }
+
+ internal fun hasAndroidMultiplatform(): Boolean {
+ return agpKmpExtensionDelegate.isInitialized()
+ }
+
+ fun sourceSets(closure: Closure<*>) {
+ if (kotlinExtensionDelegate.isInitialized()) {
+ kotlinExtension.sourceSets.configure(closure).also {
+ kotlinExtension.sourceSets.configureEach { sourceSet ->
+ if (sourceSet.name == "main" || sourceSet.name == "test") {
+ throw Exception(
+ "KMP-enabled projects must use target-prefixed " +
+ "source sets, e.g. androidMain or commonTest, rather than main or test"
+ )
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates a multi-target native compilation with the given [archiveName].
+ *
+ * The given [configure] action can be used to add targets, sources, includes etc.
+ *
+ * The outputs of this compilation is not added to any artifact by default.
+ * * To use the outputs via cinterop (kotlin native), use the [createCinterop] function.
+ * * To bundle the outputs inside a JAR (to be loaded at runtime), use the
+ * [addNativeLibrariesToResources] function.
+ * * To bundle the outputs inside an AAR (to be loaded at runtime), use the
+ * [addNativeLibrariesToJniLibs] function.
+ *
+ * @param archiveName The archive file name for the native artifacts (.so, .a or .o)
+ * @param outputKind The kind of output it should be produced (library or executable).
+ * @param configure Action block to configure the compilation.
+ */
+ @JvmOverloads
+ fun createNativeCompilation(
+ archiveName: String,
+ outputKind: LinkerOutputKind = LinkerOutputKind.DYNAMIC_LIBRARY,
+ configure: Action,
+ ): MultiTargetNativeCompilation {
+ return clang.createNativeCompilation(
+ archiveName = archiveName,
+ configure = configure,
+ outputKind = outputKind,
+ )
+ }
+
+ /**
+ * Creates a Kotlin Native cinterop configuration for the given [nativeTarget] main compilation
+ * from the outputs of [nativeCompilation].
+ *
+ * @param nativeTarget The kotlin native target for which a new cinterop will be added on the
+ * main compilation.
+ * @param nativeCompilation The [MultiTargetNativeCompilation] which will be embedded into the
+ * generated cinterop klib.
+ * @param cinteropName The name of the cinterop definition. A matching "" file
+ * needs to be present in the default cinterop location
+ * (src/nativeInterop/cinterop/).
+ */
+ @JvmOverloads
+ fun createCinterop(
+ nativeTarget: KotlinNativeTarget,
+ nativeCompilation: MultiTargetNativeCompilation,
+ cinteropName: String = nativeCompilation.archiveName,
+ ) {
+ createCinterop(
+ kotlinNativeCompilation =
+ nativeTarget.compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME)
+ as KotlinNativeCompilation,
+ nativeCompilation = nativeCompilation,
+ cinteropName = cinteropName,
+ )
+ }
+
+ /**
+ * Creates a Kotlin Native cinterop configuration for the given [kotlinNativeCompilation] from
+ * the outputs of [nativeCompilation].
+ *
+ * @param kotlinNativeCompilation The kotlin native compilation for which a new cinterop will be
+ * added
+ * @param nativeCompilation The [MultiTargetNativeCompilation] which will be embedded into the
+ * generated cinterop klib.
+ * @param cinteropName The name of the cinterop definition. A matching "" file
+ * needs to be present in the default cinterop location
+ * (src/nativeInterop/cinterop/).
+ */
+ @JvmOverloads
+ fun createCinterop(
+ kotlinNativeCompilation: KotlinNativeCompilation,
+ nativeCompilation: MultiTargetNativeCompilation,
+ cinteropName: String = nativeCompilation.archiveName,
+ ) {
+ nativeCompilation.configureCinterop(
+ kotlinNativeCompilation = kotlinNativeCompilation,
+ cinteropName = cinteropName,
+ )
+ }
+
+ /**
+ * Creates a Kotlin Native cinterop configuration for the given [kotlinNativeCompilation] from
+ * the single output of a configuration.
+ *
+ * @param kotlinNativeCompilation The kotlin native compilation for which a new cinterop will be
+ * added
+ * @param configuration The configuration to resolve. It is expected for the configuration to
+ * contain a single file of the archive file to be referenced in the C interop definition
+ * file.
+ */
+ fun createCinteropFromArchiveConfiguration(
+ kotlinNativeCompilation: KotlinNativeCompilation,
+ configuration: Configuration,
+ ) {
+ configureCinterop(project, kotlinNativeCompilation, configuration)
+ }
+
+ /**
+ * Adds the native outputs from [nativeCompilation] to the assets of the [androidTarget].
+ *
+ * @see CombineObjectFilesTask for details.
+ */
+ @JvmOverloads
+ fun addNativeLibrariesToVariantAssets(
+ androidTarget: KotlinMultiplatformAndroidLibraryTarget,
+ nativeCompilation: MultiTargetNativeCompilation,
+ forTest: Boolean = false,
+ ) =
+ nativeLibraryBundler.addNativeLibrariesToAndroidVariantSources(
+ androidTarget = androidTarget,
+ nativeCompilation = nativeCompilation,
+ forTest = forTest,
+ provideSourceDirectories = { assets },
+ )
+
+ /**
+ * Adds the native outputs from [nativeCompilation] to the jni libs dependency of the
+ * [androidTarget].
+ *
+ * @see CombineObjectFilesTask for details.
+ */
+ @JvmOverloads
+ fun addNativeLibrariesToJniLibs(
+ androidTarget: KotlinMultiplatformAndroidLibraryTarget,
+ nativeCompilation: MultiTargetNativeCompilation,
+ forTest: Boolean = false,
+ ) =
+ nativeLibraryBundler.addNativeLibrariesToAndroidVariantSources(
+ androidTarget = androidTarget,
+ nativeCompilation = nativeCompilation,
+ forTest = forTest,
+ provideSourceDirectories = { jniLibs },
+ )
+
+ /**
+ * Convenience method to add bundle native libraries with a test jar.
+ *
+ * @see addNativeLibrariesToResources
+ */
+ fun addNativeLibrariesToTestResources(
+ jvmTarget: KotlinJvmTarget,
+ nativeCompilation: MultiTargetNativeCompilation,
+ ) =
+ addNativeLibrariesToResources(
+ jvmTarget = jvmTarget,
+ nativeCompilation = nativeCompilation,
+ compilationName = KotlinCompilation.TEST_COMPILATION_NAME,
+ )
+
+ /** @see NativeLibraryBundler.addNativeLibrariesToResources */
+ @JvmOverloads
+ fun addNativeLibrariesToResources(
+ jvmTarget: KotlinJvmTarget,
+ nativeCompilation: MultiTargetNativeCompilation,
+ compilationName: String = KotlinCompilation.MAIN_COMPILATION_NAME,
+ ) =
+ nativeLibraryBundler.addNativeLibrariesToResources(
+ jvmTarget = jvmTarget,
+ nativeCompilation = nativeCompilation,
+ compilationName = compilationName,
+ )
+
+ /**
+ * Sets the default target platform.
+ *
+ * The default target platform *must* be enabled in all build environments. For projects which
+ * request multiple target platforms, this method *must* be called to explicitly specify a
+ * default target platform.
+ *
+ * See [defaultPlatform] for details on how the value is used.
+ */
+ fun defaultPlatform(value: PlatformIdentifier) {
+ defaultPlatform = value.id
+ }
+
+ @JvmOverloads
+ fun jvm(block: Action? = null): KotlinJvmTarget? = potentiallyRedirecting("jvm") {
+ supportedPlatforms.add(PlatformIdentifier.JVM)
+ if (project.enableJvm()) {
+ kotlinExtension.jvm { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ @JvmOverloads
+ fun jvmStubs(
+ runTests: Boolean = false,
+ block: Action? = null,
+ ): KotlinJvmTarget? {
+ supportedPlatforms.add(PlatformIdentifier.JVM_STUBS)
+ return if (project.enableJvm()) {
+ kotlinExtension.jvm("jvmStubs") {
+ block?.execute(this)
+ project.tasks.named("jvmStubsTest").configure {
+ // don't try running common tests for stubs target if disabled
+ it.enabled = runTests
+ }
+ }
+ } else {
+ null
+ }
+ }
+
+ @JvmOverloads
+ fun androidNative(block: Action? = null): List {
+ return listOfNotNull(
+ androidNativeX86(block),
+ androidNativeX64(block),
+ androidNativeArm64(block),
+ androidNativeArm32(block),
+ )
+ }
+
+ @JvmOverloads
+ fun androidNativeX86(block: Action? = null): KotlinNativeTarget? =
+ potentiallyRedirecting("androidNativeX86") {
+ supportedPlatforms.add(PlatformIdentifier.ANDROID_NATIVE_X86)
+ if (project.enableAndroidNative()) {
+ kotlinExtension.androidNativeX86 { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ @JvmOverloads
+ fun androidNativeX64(block: Action? = null): KotlinNativeTarget? =
+ potentiallyRedirecting("androidNativeX64") {
+ supportedPlatforms.add(PlatformIdentifier.ANDROID_NATIVE_X64)
+ if (project.enableAndroidNative()) {
+ kotlinExtension.androidNativeX64 { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ @JvmOverloads
+ fun androidNativeArm64(block: Action? = null): KotlinNativeTarget? =
+ potentiallyRedirecting("androidNativeArm64") {
+ supportedPlatforms.add(PlatformIdentifier.ANDROID_NATIVE_ARM64)
+ if (project.enableAndroidNative()) {
+ kotlinExtension.androidNativeArm64 { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ @JvmOverloads
+ fun androidNativeArm32(block: Action? = null): KotlinNativeTarget? =
+ potentiallyRedirecting("androidNativeArm32") {
+ supportedPlatforms.add(PlatformIdentifier.ANDROID_NATIVE_ARM32)
+ if (project.enableAndroidNative()) {
+ kotlinExtension.androidNativeArm32 { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ @JvmOverloads
+ fun androidLibrary(
+ block: Action? = null
+ ): KotlinMultiplatformAndroidLibraryTarget? = potentiallyRedirecting("android") {
+ supportedPlatforms.add(PlatformIdentifier.ANDROID)
+ if (project.enableJvm()) {
+ agpKmpExtension.also { block?.execute(it) }
+ } else {
+ null
+ }
+ }
+
+ @JvmOverloads
+ fun desktop(block: Action? = null): KotlinJvmTarget? =
+ potentiallyRedirecting("desktop") {
+ supportedPlatforms.add(PlatformIdentifier.DESKTOP)
+ if (project.enableDesktop()) {
+ kotlinExtension.jvm("desktop") { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ @JvmOverloads
+ fun mingwX64(block: Action? = null): KotlinNativeTargetWithHostTests? =
+ potentiallyRedirecting("mingwX64") {
+ supportedPlatforms.add(PlatformIdentifier.MINGW_X_64)
+ if (project.enableWindows()) {
+ kotlinExtension.mingwX64 { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ /** Configures all mac targets supported by AndroidX. */
+ @JvmOverloads
+ fun mac(block: Action? = null): List {
+ return listOfNotNull(macosArm64(block))
+ }
+
+ @JvmOverloads
+ fun macosArm64(block: Action? = null): KotlinNativeTargetWithHostTests? =
+ potentiallyRedirecting("macosArm64") {
+ supportedPlatforms.add(PlatformIdentifier.MAC_ARM_64)
+ if (project.enableMac()) {
+ kotlinExtension.macosArm64 { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ /** Configures all ios targets supported by AndroidX. */
+ @JvmOverloads
+ fun ios(block: Action? = null): List {
+ return listOfNotNull(iosArm64(block), iosSimulatorArm64(block))
+ }
+
+ @JvmOverloads
+ fun iosArm64(block: Action? = null): KotlinNativeTarget? =
+ potentiallyRedirecting("iosArm64") {
+ supportedPlatforms.add(PlatformIdentifier.IOS_ARM_64)
+ if (project.enableMac()) {
+ kotlinExtension.iosArm64 { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ @JvmOverloads
+ fun iosSimulatorArm64(block: Action? = null): KotlinNativeTarget? =
+ potentiallyRedirecting("iosSimulatorArm64") {
+ supportedPlatforms.add(PlatformIdentifier.IOS_SIMULATOR_ARM_64)
+ if (project.enableMac()) {
+ kotlinExtension.iosSimulatorArm64 { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ /** Configures all watchos targets supported by AndroidX. */
+ @JvmOverloads
+ fun watchos(block: Action? = null): List {
+ return listOfNotNull(
+ watchosArm32(block),
+ watchosArm64(block),
+ // TODO(https://youtrack.jetbrains.com/issue/CMP-9513) publish it
+ // watchosDeviceArm64()
+ watchosSimulatorArm64(block),
+ )
+ }
+
+ @JvmOverloads
+ fun watchosArm32(block: Action? = null): KotlinNativeTarget? =
+ potentiallyRedirecting("watchosArm32") {
+ supportedPlatforms.add(PlatformIdentifier.WATCHOS_ARM_32)
+ if (project.enableMac()) {
+ kotlinExtension.watchosArm32 { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ @JvmOverloads
+ fun watchosArm64(block: Action? = null): KotlinNativeTarget? =
+ potentiallyRedirecting("watchosArm64") {
+ supportedPlatforms.add(PlatformIdentifier.WATCHOS_ARM_64)
+ if (project.enableMac()) {
+ kotlinExtension.watchosArm64 { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ @JvmOverloads
+ fun watchosDeviceArm64(block: Action? = null): KotlinNativeTarget? =
+ potentiallyRedirecting("watchosDeviceArm64") {
+ supportedPlatforms.add(PlatformIdentifier.WATCHOS_DEVICE_ARM_64)
+ if (project.enableMac()) {
+ kotlinExtension.watchosDeviceArm64 { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ @JvmOverloads
+ fun watchosSimulatorArm64(block: Action? = null): KotlinNativeTarget? =
+ potentiallyRedirecting("watchosSimulatorArm64") {
+ supportedPlatforms.add(PlatformIdentifier.WATCHOS_SIMULATOR_ARM_64)
+ if (project.enableMac()) {
+ kotlinExtension.watchosSimulatorArm64 { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ /** Configures all tvos targets supported by AndroidX. */
+ @JvmOverloads
+ fun tvos(block: Action? = null): List {
+ return listOfNotNull(tvosArm64(block), tvosSimulatorArm64(block))
+ }
+
+ @JvmOverloads
+ fun tvosArm64(block: Action? = null): KotlinNativeTarget? =
+ potentiallyRedirecting("tvosArm64") {
+ supportedPlatforms.add(PlatformIdentifier.TVOS_ARM_64)
+ if (project.enableMac()) {
+ kotlinExtension.tvosArm64 { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ @JvmOverloads
+ fun tvosSimulatorArm64(block: Action? = null): KotlinNativeTarget? =
+ potentiallyRedirecting("tvosSimulatorArm64") {
+ supportedPlatforms.add(PlatformIdentifier.TVOS_SIMULATOR_ARM_64)
+ if (project.enableMac()) {
+ kotlinExtension.tvosSimulatorArm64 { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ @JvmOverloads
+ fun linux(block: Action? = null): List {
+ return listOfNotNull(linuxArm64(block), linuxX64(block))
+ }
+
+ @JvmOverloads
+ fun linuxArm64(block: Action? = null): KotlinNativeTarget? =
+ potentiallyRedirecting("linuxArm64") {
+ supportedPlatforms.add(PlatformIdentifier.LINUX_ARM_64)
+ if (project.enableLinux()) {
+ kotlinExtension.linuxArm64 { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ @JvmOverloads
+ fun linuxX64(block: Action? = null): KotlinNativeTarget? =
+ potentiallyRedirecting("linuxX64") {
+ supportedPlatforms.add(PlatformIdentifier.LINUX_X_64)
+ if (project.enableLinux()) {
+ kotlinExtension.linuxX64 { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ @JvmOverloads
+ fun linuxX64Stubs(block: Action? = null): KotlinNativeTarget? {
+ supportedPlatforms.add(PlatformIdentifier.LINUX_X_64_STUBS)
+ return if (project.enableLinux()) {
+ kotlinExtension.linuxX64("linuxx64Stubs") {
+ block?.execute(this)
+ project.tasks.named("linuxx64StubsTest").configure {
+ // don't try running common tests for stubs target
+ it.enabled = false
+ }
+ }
+ } else {
+ null
+ }
+ }
+
+ @JvmOverloads
+ fun js(block: Action? = null): KotlinJsTargetDsl? =
+ potentiallyRedirecting("js") {
+ configureForkWebTarget(
+ platform = PlatformIdentifier.JS,
+ isEnabled = project.enableJs(),
+ createTarget = { configure -> kotlinExtension.js(configure) },
+ block = block,
+ )
+ }
+
+ @OptIn(ExperimentalWasmDsl::class)
+ @JvmOverloads
+ fun wasmJs(block: Action? = null): KotlinWasmTargetDsl? =
+ potentiallyRedirecting("wasmJs") {
+ configureForkWebTarget(
+ platform = PlatformIdentifier.WASM_JS,
+ isEnabled = project.enableWasmJs(),
+ createTarget = { configure -> kotlinExtension.wasmJs(configure) },
+ block = block,
+ )
+ }
+
+ // --- Artifact redirection (parallel-graph back-end): see `redirect { }` below. --------------
+
+ /**
+ * Redirect scope: inside `redirect("androidx.foo") { … }` the plain target functions
+ * (`androidLibrary {}`, `ios()`, `jvm()`, …) build their target **empty** and redirect it to the
+ * `androidx.*` artifact instead of compiling the real `commonMain` — the parallel-graph back-end
+ * publishes an empty klib/jar/aar that depends on the androidx coordinate. Mix freely with plain
+ * (fork-built) targets declared outside the block for partial redirects (e.g.
+ * `redirect("androidx.foo") { androidLibrary {} }` then plain `desktop(); ios()`).
+ *
+ * [group] is the target `androidx.*` group and is **required** — every redirect declares it
+ * explicitly (no property fallback, no derivation). [version] is optional: omit it to resolve
+ * from the `[versions]` table of `redirectversions.toml`; one redirect coordinate per module.
+ *
+ * The receiver is the decorated `androidXMultiplatform` extension itself (no separate scope
+ * object), so the target list is not duplicated and Groovy nested config closures (e.g.
+ * `androidLibrary { namespace = … }`) delegate to their target as usual.
+ */
+ fun redirect(group: String, block: Action) =
+ redirect(group, null, block)
+
+ fun redirect(group: String, version: String?, block: Action) {
+ val prevRedirectScope = redirectCoordinate
+ redirectCoordinate = RedirectCoordinate(group, version)
+ try {
+ block.execute(this)
+ } finally {
+ redirectCoordinate = prevRedirectScope
+ }
+ }
+
+ /**
+ * Wraps a plain target function's creation. When called inside [redirect] { } the target's name
+ * is registered **before** the target (and its compilations) are created — so the
+ * default-hierarchy `excludeCompilations` predicate keeps the redirect leaf off the real
+ * `commonMain` — and the created target is recorded so the back-end re-roots it onto the empty
+ * `redirectCommonMain`. A no-op outside a redirect scope: the target is fork-built as usual.
+ *
+ * Every leaf target function (`jvm`, `androidLibrary`, `iosArm64`, …) routes its body through
+ * this helper, so any of them redirects automatically when invoked inside `redirect { }` —
+ * directly or via an aggregate like `ios()`/`mac()` that fans out to the leaves.
+ */
+ private fun potentiallyRedirecting(targetName: String, create: () -> T): T {
+ val redirectScope = redirectCoordinate ?: return create()
+ expectRedirect(targetName)
+ return create().also {
+ (it as? KotlinTarget)?.let { target ->
+ recordRedirect(target, targetName, redirectScope)
+ }
+ }
+ }
+
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ private fun KotlinMultiplatformExtension.applyAndroidXDefaultHierarchyTemplate() =
+ applyDefaultHierarchyTemplate {
+ common {
+ // Artifact redirection: keep redirect targets (declared inside `redirect { }`) OUT of
+ // the common hierarchy entirely, so the template never wires them to `commonMain`. Their
+ // leaf source-sets are instead wired to the empty `redirectCommonMain` at
+ // target-creation time (see recordRedirect). This predicate is evaluated lazily per
+ // compilation, so the redirect set — populated by `potentiallyRedirecting` before the
+ // target is created — is already visible here. No-op for modules that declare no redirects.
+ excludeCompilations { it.target.name in redirectTargetNames }
+ group("jvmAndAndroid") {
+ // TODO(b/442950553): Switch to withAndroidTarget when bug is fixed
+ withCompilations { it is KotlinMultiplatformAndroidCompilation }
+ withJvm()
+ }
+ group("nonJvm") {
+ withNative()
+ group("web") {
+ withWasmJs()
+ withJs()
+ }
+ }
+ }
+ }
+
+ private fun Project.configureWebTarget(
+ platform: PlatformIdentifier,
+ isEnabled: Boolean,
+ createTarget: (KotlinJsTargetDsl.() -> Unit) -> T,
+ block: Action? = null,
+ ): T? {
+ if (buildFeatures.isIsolatedProjectsEnabled()) return null
+ supportedPlatforms.add(platform)
+ return if (isEnabled) {
+ createTarget {
+ block?.execute(this)
+ binaries.library()
+ browser {
+ testTask {
+ it.useKarma {
+ useChromeHeadless()
+ useConfigDirectory(File(getSupportRootFolder(), "buildSrc/karmaconfig"))
+ }
+ }
+ }
+ // Do not place the config functions below before the browser DSL as the
+ // settings will be overridden
+ configureBinaryen()
+ configureDefaultIncrementalSyncTask()
+ configureKotlinJsTests()
+ configureNode()
+
+ // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same:
+ // https://youtrack.jetbrains.com/issue/KT-71032
+ configurePinnedKotlinLibraries(platform)
+ }
+ } else null
+ }
+
+ /** Locates a project by path. */
+ // This method is needed for Gradle project isolation to avoid calls to parent projects due to
+ // androidx { samples(project(":foo")) }
+ // Without this method, the call above results into a call to the parent object, because
+ // AndroidXExtension has `val project: Project`, which from groovy `project` call within
+ // `androidx` block tries retrieves that project object and calls to look for :foo property
+ // on it, then checking all the parents for it.
+ fun project(name: String): Project = project.project(name)
+
+ companion object {
+ const val EXTENSION_NAME = "androidXMultiplatform"
+ }
+
+ // FORK-only public extensions
+
+ /**
+ * Configures native compilation tasks with flags to link required frameworks
+ */
+ fun configureDarwinFlags() = org.jetbrains.androidx.build.configureDarwinFlags(project)
+
+ /**
+ * Configure instrumented tests to run on an actual iOS simulator.
+ */
+ fun iosInstrumentedTest() = org.jetbrains.androidx.build.addIosInstrumentedTestSourceset(project)
+}
+
+// TODO(https://youtrack.jetbrains.com/issue/KT-76874/):
+// Remove this function when the default destinationDirectory is different for each task
+private fun Project.configureDefaultIncrementalSyncTask() {
+ val destinationPaths =
+ mapOf(
+ "jsDevelopmentLibraryCompileSync" to "js/packages/js/dev/kotlin",
+ "jsProductionLibraryCompileSync" to "js/packages/js/prod/kotlin",
+ "jsTestTestDevelopmentExecutableCompileSync" to "js/packages/js-test/dev/kotlin",
+ "jsTestTestProductionExecutableCompileSync" to "js/packages/js-test/prod/kotlin",
+ "wasmJsDevelopmentLibraryCompileSync" to "js/packages/wasm-js/dev/kotlin",
+ "wasmJsProductionLibraryCompileSync" to "js/packages/wasm-js/prod/kotlin",
+ "wasmJsTestTestDevelopmentExecutableCompileSync" to
+ "js/packages/wasm-js-test/dev/kotlin",
+ "wasmJsTestTestProductionExecutableCompileSync" to
+ "js/packages/wasm-js-test/prod/kotlin",
+ )
+
+ tasks.withType(DefaultIncrementalSyncTask::class.java).configureEach { task ->
+ val relativePath =
+ destinationPaths[task.name]
+ ?: throw IllegalArgumentException(
+ "No destination path configured for incremental‑sync task '${task.name}'"
+ )
+ task.destinationDirectory.set(file(layout.buildDirectory.dir(relativePath)))
+ }
+}
+
+internal fun Project.configureNode() {
+ val nodeJsPrebuilt =
+ File(project.getPrebuiltsRoot(), "androidx/external/org/nodejs/node").toURI().toString()
+
+ plugins.withType().configureEach {
+ the().let {
+ it.version.set(getVersionByName("node"))
+ if (!ProjectLayoutType.isPlayground(this)) {
+ it.downloadBaseUrl.set(nodeJsPrebuilt)
+ }
+ }
+ }
+ plugins.withType().configureEach {
+ the().let {
+ it.version.set(getVersionByName("node"))
+ if (!ProjectLayoutType.isPlayground(this)) {
+ it.downloadBaseUrl.set(nodeJsPrebuilt)
+ }
+ }
+ }
+
+ if (!ProjectLayoutType.isPlayground(this)) {
+ val javascriptPrebuiltsRoot =
+ File(project.getPrebuiltsRoot(), "androidx/javascript-for-kotlin")
+
+ plugins.withType().configureEach {
+ the().let {
+ it.version.set(getVersionByName("yarn"))
+ it.yarnLockMismatchReport.set(YarnLockMismatchReport.FAIL)
+ it.downloadBaseUrl.set(javascriptPrebuiltsRoot.toURI().toString())
+ }
+ }
+
+ plugins.withType().configureEach {
+ the().let {
+ it.version.set(getVersionByName("yarn"))
+ it.yarnLockMismatchReport.set(YarnLockMismatchReport.FAIL)
+ it.downloadBaseUrl.set(javascriptPrebuiltsRoot.toURI().toString())
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalWasmDsl::class)
+private fun Project.configureBinaryen() {
+ if (ProjectLayoutType.isPlayground(project)) {
+ return
+ }
+ plugins.withType().configureEach {
+ the()
+ .downloadBaseUrl
+ .set(
+ File(project.getPrebuiltsRoot(), "androidx/javascript-for-kotlin/binaryen")
+ .toURI()
+ .toString()
+ )
+ }
+}
+
+internal fun Project.configurePinnedKotlinLibraries(platform: PlatformIdentifier) {
+ multiplatformExtension?.let {
+ val kotlinLibSuffix =
+ when (platform) {
+ PlatformIdentifier.JS -> "js"
+ PlatformIdentifier.WASM_JS -> "wasm-js"
+ else -> throw IllegalStateException("Unsupported platform: $platform")
+ }
+ val kotlinVersion = project.getVersionByName("kotlin")
+ it.sourceSets.getByName("${platform.id}Main").dependencies {
+ implementation("org.jetbrains.kotlin:kotlin-stdlib-$kotlinLibSuffix:$kotlinVersion")
+ }
+ it.sourceSets.getByName("${platform.id}Test").dependencies {
+ implementation("org.jetbrains.kotlin:kotlin-stdlib-$kotlinLibSuffix:$kotlinVersion")
+ implementation("org.jetbrains.kotlin:kotlin-test-$kotlinLibSuffix:$kotlinVersion")
+ }
+ }
+}
+
+private fun Project.configureKotlinJsTests() {
+ tasks.withType(KotlinJsTest::class.java).configureEach { task ->
+ if (!ProjectLayoutType.isPlayground(this)) {
+ val unzipChromeBuildServiceProvider =
+ gradle.sharedServices.registrations.getByName("unzipChrome").service
+ task.usesService(unzipChromeBuildServiceProvider)
+ // Remove doFirst and switch to FileProperty property to set browser path when issue
+ // https://youtrack.jetbrains.com/issue/KT-72514 is resolved
+ task.doFirst {
+ task.environment(
+ "CHROME_BIN",
+ (unzipChromeBuildServiceProvider.get() as UnzipChromeBuildService).chromePath,
+ )
+ }
+ }
+ // From: https://nodejs.org/api/cli.html
+ task.nodeJsArgs.addAll(listOf("--trace-warnings", "--trace-uncaught", "--trace-sigint"))
+ }
+
+ // Compiler Arg needed for tests only: https://youtrack.jetbrains.com/issue/KT-59081
+ tasks.withType(Kotlin2JsCompile::class.java).configureEach { task ->
+ if (task.name.lowercase().contains("test")) {
+ task.compilerOptions.freeCompilerArgs.add("-Xwasm-enable-array-range-checks")
+ }
+ }
+}
+
+fun Project.validatePublishedMultiplatformHasDefault() {
+ val extension = project.extensions.getByType(AndroidXMultiplatformExtension::class.java)
+ if (extension.defaultPlatform == null && extension.supportedPlatforms.isNotEmpty()) {
+ throw GradleException(
+ "Project is published and multiple platforms are requested. You " +
+ "must explicitly specify androidXMultiplatform.defaultPlatform as one of: " +
+ extension.targetPlatforms.joinToString(", ") {
+ "PlatformIdentifier.${PlatformIdentifier.fromId(it)!!.name}"
+ }
+ )
+ }
+}
diff --git a/buildSrc-fork/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt b/buildSrc-fork/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt
new file mode 100644
index 0000000000000..5f4e061952000
--- /dev/null
+++ b/buildSrc-fork/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2020 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.build.gradle.extraPropertyOrNull
+import androidx.build.gradle.isRoot
+import groovy.xml.DOMBuilder
+import java.net.URI
+import java.net.URL
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.artifacts.dsl.RepositoryHandler
+import org.gradle.api.tasks.testing.AbstractTestTask
+import org.gradle.work.DisableCachingByDefault
+
+/**
+ * This plugin is used in Playground projects and adds functionality like resolving to snapshot
+ * artifacts instead of projects or allowing access to public maven repositories.
+ */
+@Suppress("unused") // used in Playground Projects
+class AndroidXPlaygroundRootImplPlugin : Plugin {
+ private lateinit var rootProject: Project
+
+ /** List of snapshot repositories to fetch AndroidX artifacts */
+ private lateinit var repos: PlaygroundRepositories
+
+ /** The configuration for the plugin read from the gradle properties */
+ private lateinit var config: PlaygroundProperties
+
+ /** List of projects that were requested in the settings.gradle file */
+ private lateinit var primaryProjectPaths: Set
+
+ override fun apply(target: Project) {
+ if (!target.isRoot) {
+ throw GradleException("This plugin should only be applied to root project")
+ }
+ if (!target.plugins.hasPlugin(AndroidXRootImplPlugin::class.java)) {
+ throw GradleException(
+ "Must apply AndroidXRootImplPlugin before applying AndroidXPlaygroundRootImplPlugin"
+ )
+ }
+ rootProject = target
+ config = PlaygroundProperties.load(rootProject)
+ repos = PlaygroundRepositories(config)
+ rootProject.repositories.addPlaygroundRepositories()
+ GradleTransformWorkaround.maybeApply(rootProject)
+ PlaygroundCIHostTestsTask.register(rootProject)
+ primaryProjectPaths =
+ target.extensions.extraProperties.get("primaryProjects")!!.toString().split(",").toSet()
+ rootProject.subprojects { configureSubProject(it) }
+ }
+
+ private fun configureSubProject(project: Project) {
+ project.repositories.addPlaygroundRepositories()
+ project.configurations.configureEach { configuration ->
+ configuration.resolutionStrategy.eachDependency { details ->
+ val requested = details.requested
+ if (requested.version == SNAPSHOT_MARKER) {
+ val snapshotVersion = findSnapshotVersion(requested.group, requested.name)
+ details.useVersion(snapshotVersion)
+ }
+ }
+ }
+ if (project.path in primaryProjectPaths) {
+ project.tasks.withType(AbstractTestTask::class.java).configureEach {
+ PlaygroundCIHostTestsTask.addTask(project, it)
+ }
+ }
+ }
+
+ /**
+ * Finds the snapshot version from the AndroidX snapshot repository.
+ *
+ * This is initially done by reading the maven-metadata from the snapshot repository. The result
+ * of that query is cached in the build file so that subsequent build requests will not need to
+ * access the network.
+ */
+ private fun findSnapshotVersion(group: String, module: String): String {
+ @Suppress("DEPRECATION")
+ val snapshotVersionCache =
+ rootProject.buildDir.resolve("snapshot-version-cache/${config.snapshotBuildId}")
+ val groupPath = group.replace('.', '/')
+ val modulePath = module.replace('.', '/')
+ val metadataCacheFile = snapshotVersionCache.resolve("$groupPath/$modulePath/version.txt")
+ return if (metadataCacheFile.exists()) {
+ metadataCacheFile.readText(Charsets.UTF_8)
+ } else {
+ val metadataUrl = "${repos.snapshots.url}/$groupPath/$modulePath/maven-metadata.xml"
+ @Suppress("deprecation")
+ URL(metadataUrl).openStream().use {
+ val parsedMetadata = DOMBuilder.parse(it.reader())
+ val versionNodes = parsedMetadata.getElementsByTagName("latest")
+ if (versionNodes.length != 1) {
+ throw GradleException(
+ "AndroidXPlaygroundRootImplPlugin#findSnapshotVersion expected exactly " +
+ " one latest version in $metadataUrl, but got ${versionNodes.length}"
+ )
+ }
+ val snapshotVersion = versionNodes.item(0).textContent
+ metadataCacheFile.parentFile.mkdirs()
+ metadataCacheFile.writeText(snapshotVersion, Charsets.UTF_8)
+ snapshotVersion
+ }
+ }
+ }
+
+ private fun RepositoryHandler.addPlaygroundRepositories() {
+ repos.all.forEach { playgroundRepository ->
+ maven { repository ->
+ repository.url = URI(playgroundRepository.url)
+ repository.metadataSources {
+ it.mavenPom()
+ it.artifact()
+ }
+ repository.content {
+ it.includeGroupByRegex(playgroundRepository.includeGroupRegex)
+ if (playgroundRepository.includeModuleRegex != null) {
+ it.includeModuleByRegex(
+ playgroundRepository.includeGroupRegex,
+ playgroundRepository.includeModuleRegex,
+ )
+ }
+ }
+ }
+ }
+ google { repository ->
+ repository.content {
+ it.includeGroupByRegex("androidx.*")
+ it.includeGroupByRegex("com\\.android.*")
+ it.includeGroupByRegex("com\\.google.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+
+ private class PlaygroundRepositories(props: PlaygroundProperties) {
+ val snapshots =
+ PlaygroundRepository(
+ "https://androidx.dev/snapshots/builds/${props.snapshotBuildId}/artifacts" +
+ "/repository",
+ includeGroupRegex = """androidx\..*""",
+ )
+ val metalava =
+ PlaygroundRepository(
+ "https://androidx.dev/metalava/builds/${props.metalavaBuildId}/artifacts" +
+ "/repo/m2repository",
+ includeGroupRegex = """com\.android\.tools\.metalava""",
+ )
+ val prebuilts =
+ PlaygroundRepository(
+ INTERNAL_PREBUILTS_REPO_URL,
+ includeGroupRegex = """androidx\..*""",
+ )
+ val dokka =
+ PlaygroundRepository(
+ "https://packages.jetbrains.team/maven/p/kt/dokka-dev",
+ includeGroupRegex = """org\.jetbrains\.dokka""",
+ )
+ val kotlinDev =
+ PlaygroundRepository(
+ "https://packages.jetbrains.team/maven/p/kt/dev/",
+ includeGroupRegex = """org\.jetbrains\.kotlin.*""",
+ )
+ val mavenSnapshots =
+ PlaygroundRepository(
+ "https://central.sonatype.com/repository/maven-snapshots/",
+ includeGroupRegex = """com\.google\.devtools.*""",
+ )
+ val all = listOf(snapshots, metalava, dokka, prebuilts, kotlinDev, mavenSnapshots)
+ }
+
+ private data class PlaygroundRepository(
+ val url: String,
+ val includeGroupRegex: String,
+ val includeModuleRegex: String? = null,
+ )
+
+ private data class PlaygroundProperties(
+ val snapshotBuildId: String,
+ val metalavaBuildId: String,
+ ) {
+ companion object {
+ fun load(project: Project): PlaygroundProperties {
+ return PlaygroundProperties(
+ snapshotBuildId = project.requireProperty(PLAYGROUND_SNAPSHOT_BUILD_ID),
+ metalavaBuildId = project.requireProperty(PLAYGROUND_METALAVA_BUILD_ID),
+ )
+ }
+
+ private fun Project.requireProperty(name: String): String {
+ return checkNotNull(extraPropertyOrNull(name)) {
+ "missing $name property. It must be defined in the gradle.properties file"
+ }
+ .toString()
+ }
+ }
+ }
+
+ companion object {
+ const val INTERNAL_PREBUILTS_REPO_URL =
+ "https://androidx.dev/storage/prebuilts/androidx/internal/repository"
+ }
+
+ @DisableCachingByDefault(because = "This is an anchor task that does no work.")
+ abstract class PlaygroundCIHostTestsTask : DefaultTask() {
+ init {
+ group = "Verification"
+ description =
+ "Runs host tests that belong to the projects which were explicitly " +
+ "requested in the playground setup."
+ }
+
+ companion object {
+ private const val NAME = "playgroundCIHostTests"
+
+ fun addTask(project: Project, task: AbstractTestTask) {
+ project.rootProject.tasks.named(NAME).configure { it.dependsOn(task) }
+ }
+
+ fun register(project: Project) {
+ project.tasks.register(NAME, PlaygroundCIHostTestsTask::class.java)
+ }
+ }
+ }
+}
diff --git a/buildSrc-fork/private/src/main/kotlin/androidx/build/AndroidXRepackageImplPlugin.kt b/buildSrc-fork/private/src/main/kotlin/androidx/build/AndroidXRepackageImplPlugin.kt
new file mode 100644
index 0000000000000..2a0ee5429951b
--- /dev/null
+++ b/buildSrc-fork/private/src/main/kotlin/androidx/build/AndroidXRepackageImplPlugin.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2024 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 com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
+import groovy.lang.Closure
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.plugins.JavaLibraryPlugin
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.SourceSetContainer
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.jvm.tasks.Jar
+import org.gradle.kotlin.dsl.create
+import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin
+
+/**
+ * Plugin responsible for repackaging libraries. The plugin repackages what is set in the
+ * [RelocationExtension] by the user and reconfigures the JAR task to output the repackaged classes
+ * JAR.
+ */
+@Suppress("unused")
+class AndroidXRepackageImplPlugin : Plugin {
+
+ override fun apply(project: Project) {
+ val relocationExtension =
+ project.extensions.create(EXTENSION_NAME, project)
+ project.plugins.configureEach { plugin ->
+ when (plugin) {
+ is JavaLibraryPlugin,
+ is KotlinBasePlugin -> project.configureJavaOrKotlinLibrary(relocationExtension)
+ }
+ }
+ }
+
+ private fun Project.configureJavaOrKotlinLibrary(relocationExtension: RelocationExtension) {
+ createConfigurations()
+
+ val sourceSets = extensions.getByType(SourceSetContainer::class.java)
+ val libraryShadowJar =
+ tasks.register("shadowLibraryJar", ShadowJar::class.java) { task ->
+ task.transformers.add(
+ BundleInsideHelper.DontIncludeResourceTransformer().apply {
+ dropResourcesWithSuffix = ".proto"
+ }
+ )
+ task.transformers.add(
+ BundleInsideHelper.DontIncludeResourceTransformer().apply {
+ dropResourcesWithSuffix = ".proto.bin"
+ }
+ )
+ task.from(sourceSets.named("main").map { it.output })
+ relocationExtension.getRelocations().forEach {
+ task.relocate(it.sourcePackage, it.targetPackage)
+ }
+ relocationExtension.artifactId.orNull?.let {
+ task.configurations = listOf(configurations.getByName("repackageClasspath"))
+ }
+ }
+ addArchiveToVariants(libraryShadowJar)
+ }
+
+ private fun Project.createConfigurations() {
+ val repackage =
+ configurations.register("repackage") { config ->
+ config.isCanBeConsumed = false
+ config.isCanBeResolved = false
+ }
+
+ configurations.register("repackageClasspath") { config ->
+ config.isCanBeConsumed = false
+ config.isCanBeResolved = true
+ // remove .get() when https://github.com/gradle/gradle/issues/33396 is fixed
+ config.extendsFrom(repackage.get())
+ }
+
+ tasks.named("jar", Jar::class.java) {
+ // We cannot have two tasks with the same output as the ListTaskOutputsTask will fail.
+ // As we want the repackaged jar as the published artifact, we change the
+ // name of classifier of the JAR task
+ it.archiveClassifier.set("before-shadow")
+ }
+
+ forceJarUsageForAndroid()
+ }
+
+ /**
+ * This forces the use of repackaged JARs as opposed to the java-classes-directory for Android.
+ * Without this, AGP uses the artifacts in java-classes-directory, which do not have the classes
+ * repackaged to the target package.
+ *
+ * We attempted to extract the contents of the repackaged library JAR into classes/java/main,
+ * but the AGP transform depends on JavaCompile. We cannot make JavaCompile depend on the task
+ * that creates the shadowed library as that would result in a circular dependency.
+ */
+ private fun Project.forceJarUsageForAndroid() =
+ configurations.configureEach { configuration ->
+ if (configuration.name == "runtimeElements") {
+ configuration.outgoing.variants.removeIf { it.name == "classes" }
+ }
+ }
+
+ private fun Project.addArchiveToVariants(task: TaskProvider) =
+ configurations.configureEach { configuration ->
+ if (configuration.name == "apiElements" || configuration.name == "runtimeElements") {
+ configuration.outgoing.artifacts.clear()
+ configuration.outgoing.artifact(task)
+ }
+ }
+
+ companion object {
+ const val EXTENSION_NAME = "repackage"
+ }
+}
+
+class Relocation {
+ /* The package name and any import statements for a class that are to be relocated. */
+ var sourcePackage: String? = null
+
+ /* The package name and any import statements for a class to which they should be relocated. */
+ var targetPackage: String? = null
+}
+
+abstract class RelocationExtension(val project: Project) {
+
+ private var relocations: MutableCollection = ArrayList()
+
+ fun addRelocation(closure: Closure): Relocation {
+ val relocation = project.configure(Relocation(), closure) as Relocation
+ relocations.add(relocation)
+ return relocation
+ }
+
+ fun getRelocations(): Collection {
+ return relocations
+ }
+
+ /* Optional artifact id if the user wants to publish the dependency in the shadowed config. */
+ abstract val artifactId: Property
+}
diff --git a/buildSrc-fork/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc-fork/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
new file mode 100644
index 0000000000000..7e8ec94310dab
--- /dev/null
+++ b/buildSrc-fork/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2020 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.build.AndroidXImplPlugin.Companion.FINALIZE_TEST_CONFIGS_WITH_APKS_TASK
+import androidx.build.AndroidXImplPlugin.Companion.ZIP_TEST_CONFIGS_WITH_APKS_TASK
+import androidx.build.buildInfo.CreateAggregateLibraryBuildInfoFileTask
+import androidx.build.buildInfo.CreateAggregateLibraryBuildInfoFileTask.Companion.CREATE_AGGREGATE_BUILD_INFO_FILES_TASK
+import androidx.build.dependencyTracker.AffectedModuleDetector
+import androidx.build.gradle.isRoot
+import androidx.build.license.ValidateLicensesExistTask
+import androidx.build.logging.TERMINAL_RED
+import androidx.build.logging.TERMINAL_RESET
+import androidx.build.playground.ValidateIntegrationPatches
+import androidx.build.playground.VerifyPlaygroundGradleConfigurationTask
+import androidx.build.studio.StudioTask.Companion.registerStudioTask
+import androidx.build.testConfiguration.registerOwnersServiceTasks
+import androidx.build.uptodatedness.TaskUpToDateValidator
+import androidx.build.uptodatedness.cacheEvenIfNoOutputs
+import com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
+import java.io.File
+import java.util.concurrent.ConcurrentHashMap
+import javax.inject.Inject
+import org.gradle.api.GradleException
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.configuration.BuildFeatures
+import org.gradle.api.file.RelativePath
+import org.gradle.api.tasks.Copy
+import org.gradle.api.tasks.bundling.Zip
+import org.gradle.api.tasks.bundling.ZipEntryCompression
+import org.gradle.build.event.BuildEventsListenerRegistry
+import org.gradle.kotlin.dsl.extra
+import org.gradle.kotlin.dsl.register
+import org.gradle.kotlin.dsl.withType
+import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask
+import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinToolingSetupTask
+
+abstract class AndroidXRootImplPlugin : Plugin {
+ @get:Inject abstract val registry: BuildEventsListenerRegistry
+ @get:Inject abstract val buildFeatures: BuildFeatures
+
+ override fun apply(project: Project) {
+ if (!project.isRoot) {
+ throw Exception("This plugin should only be applied to root project")
+ }
+ project.configureRootProject()
+ }
+
+ private fun Project.configureRootProject() {
+ project.validateAllAndroidxArgumentsAreRecognized()
+ tasks.register("listAndroidXProperties", ListAndroidXPropertiesTask::class.java)
+ tasks.register("createProject", ProjectCreatorTask::class.java)
+ configureKtfmtCheckFile()
+ maybeRegisterFilterableTask()
+ registerListAffectedProjectsTask()
+
+ /* In JetBrains Fork we don't force AGP version.
+ // If we're running inside Studio, validate the Android Gradle Plugin version.
+ val expectedAgpVersion = System.getenv("EXPECTED_AGP_VERSION")
+ if (providers.gradleProperty("android.injected.invoked.from.ide").isPresent) {
+ if (expectedAgpVersion != ANDROID_GRADLE_PLUGIN_VERSION) {
+ throw GradleException(
+ """
+ Please close and restart Android Studio.
+
+ Expected AGP version \"$expectedAgpVersion\" does not match actual AGP version
+ \"$ANDROID_GRADLE_PLUGIN_VERSION\". This happens when AGP is updated while
+ Studio is running and can be fixed by restarting Studio.
+ """
+ .trimIndent()
+ )
+ }
+ }
+ */
+
+ val verifyPlayground = VerifyPlaygroundGradleConfigurationTask.createIfNecessary(project)
+
+ val aggregateBuildInfo =
+ if (!buildFeatures.isIsolatedProjectsEnabled()) {
+ tasks.register(
+ CREATE_AGGREGATE_BUILD_INFO_FILES_TASK,
+ CreateAggregateLibraryBuildInfoFileTask::class.java,
+ )
+ } else null
+
+ val attestationManifest =
+ if (!buildFeatures.isIsolatedProjectsEnabled()) {
+ tasks.register(ATTESTATION_TASK_NAME, AttestationManifestTask::class.java) { task ->
+ task.manifestFile.set(
+ getDistributionDirectory().file("attestation_manifest.json")
+ )
+ }
+ } else null
+ tasks.register(BUILD_ON_SERVER_TASK, BuildOnServerTask::class.java) { task ->
+ task.cacheEvenIfNoOutputs()
+ task.aggregateBuildInfoFile.set(
+ getDistributionDirectory().file(AGGREGATE_BUILD_INFO_FILE_NAME)
+ )
+ verifyPlayground?.let { task.dependsOn(it) }
+ aggregateBuildInfo?.let { task.dependsOn(it) }
+ attestationManifest?.let { task.dependsOn(it) }
+ }
+
+ extra.set("projects", ConcurrentHashMap())
+
+ /**
+ * Copy App APKs (from ApkOutputProviders) into [getTestConfigDirectory] before zipping.
+ * Flatten directory hierarchy as both TradeFed and FTL work with flat hierarchy.
+ */
+ val finalizeConfigsTask =
+ project.tasks.register(FINALIZE_TEST_CONFIGS_WITH_APKS_TASK, Copy::class.java) {
+ it.from(project.getAppApksFilesDirectory())
+ it.into(project.getTestConfigDirectory())
+ it.eachFile { f -> f.relativePath = RelativePath(true, f.name) }
+ it.includeEmptyDirs = false
+ }
+
+ // NOTE: this task is used by the Github CI as well. If you make any changes here,
+ // please update the .github/workflows files as well, if necessary.
+ project.tasks.register(ZIP_TEST_CONFIGS_WITH_APKS_TASK, Zip::class.java) {
+ // Flatten PrivacySandbox APKs in separate task to preserve file order in resulting ZIP.
+ it.dependsOn(finalizeConfigsTask)
+ it.destinationDirectory.set(project.getDistributionDirectory())
+ it.archiveFileName.set("androidTest.zip")
+ it.from(project.getTestConfigDirectory())
+ // We're mostly zipping a bunch of .apk files that are already compressed
+ it.entryCompression = ZipEntryCompression.STORED
+ // Archive is greater than 4Gb :O
+ it.isZip64 = true
+ it.isReproducibleFileOrder = true
+ }
+
+ AffectedModuleDetector.configure(gradle, this)
+
+ if (!buildFeatures.isIsolatedProjectsEnabled()) {
+ registerOwnersServiceTasks()
+ }
+ registerStudioTask()
+
+ project.tasks.register("listTaskOutputs", ListTaskOutputsTask::class.java) { task ->
+ task.outputFile.set(project.getDistributionDirectory().file("task_outputs.txt"))
+ task.removePrefix(project.getCheckoutRoot().path)
+ }
+
+ TaskUpToDateValidator.setup(project, registry)
+
+ /**
+ * Add dependency analysis plugin and add buildHealth task to buildOnServer when
+ * maxDepVersions is not enabled
+ */
+ if (!project.usingMaxDepVersions().get()) {
+ project.plugins.apply("com.autonomousapps.dependency-analysis")
+
+ // Ignore advice regarding ktx dependencies
+ val dependencyAnalysis =
+ project.extensions.getByType(
+ com.autonomousapps.DependencyAnalysisExtension::class.java
+ )
+ dependencyAnalysis.structure { it.ignoreKtx(true) }
+ }
+ project.configureTasksForKotlinWeb()
+
+ tasks.register("checkExternalLicenses", ValidateLicensesExistTask::class.java) {
+ it.prebuiltsDirectory.set(File(getPrebuiltsRoot(), "androidx/external"))
+ it.baseline.set(layout.projectDirectory.file("license-baseline.txt"))
+ it.cacheEvenIfNoOutputs()
+ }
+
+ ValidateIntegrationPatches.createTask(project)
+
+ fetchDevelocityKeysIfNeeded()
+ }
+
+ private fun Project.configureTasksForKotlinWeb() {
+ val offlineMirrorStorage =
+ if (ProjectLayoutType.isPlayground(this)) {
+ project.file(
+ layout.buildDirectory.dir("javascript-for-playground").map {
+ it.asFile.also { file -> file.mkdirs() }
+ }
+ )
+ } else {
+ File(getPrebuiltsRoot(), "androidx/javascript-for-kotlin")
+ }
+
+ val createYarnRcFileTask =
+ tasks.register("createYarnRcFile", CreateYarnRcFileTask::class.java) {
+ it.offlineMirrorStorage.set(offlineMirrorStorage)
+ it.cacheStorage.set(layout.buildDirectory.dir("yarnCache"))
+ it.yarnrcFile.set(layout.buildDirectory.file(".yarnrc"))
+ }
+ val createWasmYarnRcFileTask =
+ tasks.register("createWasmYarnRcFile", CreateYarnRcFileTask::class.java) {
+ it.offlineMirrorStorage.set(offlineMirrorStorage)
+ it.cacheStorage.set(layout.buildDirectory.dir("wasmYarnCache"))
+ it.yarnrcFile.set(layout.buildDirectory.file("wasm/.yarnrc"))
+ }
+
+ configureNode()
+
+ // ensure yarn install is complete before using it to install kotlin wasm tooling
+ tasks.withType().configureEach {
+ it.dependsOn(tasks.withType())
+ }
+
+ tasks.withType().configureEach {
+ when (it.name) {
+ "kotlinNpmInstall" -> it.dependsOn(createYarnRcFileTask)
+ "kotlinWasmNpmInstall" -> it.dependsOn(createWasmYarnRcFileTask)
+ }
+ it.args.addAll(listOf("--ignore-engines", "--verbose"))
+ if (project.useYarnOffline()) {
+ it.args.add("--offline")
+ it.additionalFiles.plus(offlineMirrorStorage)
+ it.doFirst {
+ println(
+ """
+ Fetching yarn packages from the offline mirror: ${offlineMirrorStorage.path}.
+ Your build will fail if a package is not in the offline mirror. To fix, run:
+
+ $TERMINAL_RED./gradlew kotlinNpmInstall kotlinWasmNpmInstall -Pandroidx.yarnOfflineMode=false && ./gradlew kotlinUpgradeYarnLock kotlinWasmUpgradeYarnLock$TERMINAL_RESET
+
+ this will download the dependencies from the internet and update the lockfile.
+ Don't forget to upload the changes to Gerrit!
+ """
+ .trimIndent()
+ .replace("\n", " ")
+ )
+ }
+ }
+ }
+ }
+}
+
+internal const val AGGREGATE_BUILD_INFO_FILE_NAME = "androidx_aggregate_build_info.txt"
diff --git a/buildSrc-fork/private/src/main/kotlin/androidx/build/AttestationManifestTask.kt b/buildSrc-fork/private/src/main/kotlin/androidx/build/AttestationManifestTask.kt
new file mode 100644
index 0000000000000..db94dca8cf8f1
--- /dev/null
+++ b/buildSrc-fork/private/src/main/kotlin/androidx/build/AttestationManifestTask.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2025 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 org.gradle.api.DefaultTask
+import org.gradle.api.Project
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.MapProperty
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+import org.gradle.kotlin.dsl.named
+
+@CacheableTask
+abstract class AttestationManifestTask : DefaultTask() {
+ @get:Input abstract val sbomMap: MapProperty
+
+ @get:Input abstract val zipMap: MapProperty
+
+ @get:OutputFile abstract val manifestFile: RegularFileProperty
+
+ @TaskAction
+ fun writeManifest() {
+ val output =
+ zipMap.get().keys.joinToString(separator = ",\n", prefix = "[\n", postfix = "\n]") { key
+ ->
+ check(sbomMap.get().containsKey(key)) {
+ "sbomMap is missing an entry for $key project"
+ }
+ """ {
+ "artifact_path": "${zipMap.get()[key]!!}",
+ "sbom_path": "${sbomMap.get()[key]!!}",
+ "attest_archive_contents": true
+ }"""
+ }
+ manifestFile.get().asFile.writeText(output)
+ }
+}
+
+internal fun Project.addSbomToAttestation(relativeSbomPath: Provider) {
+ rootProject.tasks.named(ATTESTATION_TASK_NAME).configure { manifestTask
+ ->
+ manifestTask.sbomMap.put(path, relativeSbomPath)
+ }
+}
+
+internal fun Project.addZipToAttestation(relativeZipPath: Provider) {
+ if (ProjectLayoutType.isPlayground(this)) return
+ rootProject.tasks.named(ATTESTATION_TASK_NAME).configure { manifestTask
+ ->
+ manifestTask.zipMap.put(path, relativeZipPath)
+ }
+}
+
+internal const val ATTESTATION_TASK_NAME = "attestationManifest"
diff --git a/buildSrc-fork/private/src/main/kotlin/androidx/build/BenchmarkConfiguration.kt b/buildSrc-fork/private/src/main/kotlin/androidx/build/BenchmarkConfiguration.kt
new file mode 100644
index 0000000000000..89725cdb00a7d
--- /dev/null
+++ b/buildSrc-fork/private/src/main/kotlin/androidx/build/BenchmarkConfiguration.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 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 com.android.build.api.variant.HasDeviceTests
+import org.gradle.api.Project
+
+/**
+ * Enable internal defaults for microbenchmark which can be used to set defaults we aren't ready to
+ * apply publicly, or which require root to function.
+ *
+ * See [androidx.build.testConfiguration.INST_ARG_BLOCKLIST], which can be used to suppress some of
+ * these args in CI.
+ */
+internal fun HasDeviceTests.enableMicrobenchmarkInternalDefaults(project: Project) {
+ if (project.hasBenchmarkPlugin()) {
+ deviceTests.forEach { (_, deviceTest) ->
+ // Enables CPU perf event counters both locally, and in CI
+ deviceTest.instrumentationRunnerArguments.put(
+ "androidx.benchmark.cpuEventCounter.enable",
+ "true",
+ )
+
+ // Set default events to aid in CI investigations of run to run noise
+ // Avoid using more than three, or capture may fail reporting all zeros, see b/291826415
+ deviceTest.instrumentationRunnerArguments.put(
+ "androidx.benchmark.cpuEventCounter.events",
+ "Instructions,L1DMisses,BranchMisses",
+ )
+
+ // Force AndroidX devs to disable JIT on rooted devices
+ deviceTest.instrumentationRunnerArguments.put(
+ "androidx.benchmark.requireJitDisabledIfRooted",
+ "true",
+ )
+
+ // Check that speed compilation always used when benchmark invoked
+ deviceTest.instrumentationRunnerArguments.put("androidx.benchmark.requireAot", "true")
+
+ // Throw if measureRepeated() called on main thread to avoid ANRs
+ deviceTest.instrumentationRunnerArguments.put(
+ "androidx.benchmark.throwOnMainThreadMeasureRepeated",
+ "true",
+ )
+
+ // Enables long-running method tracing on the UI thread, even if that risks ANR for
+ // profiling convenience.
+ // NOTE, this *must* be suppressed in CI!!
+ deviceTest.instrumentationRunnerArguments.put(
+ "androidx.benchmark.profiling.skipWhenDurationRisksAnr",
+ "false",
+ )
+ }
+ }
+}
diff --git a/buildSrc-fork/private/src/main/kotlin/androidx/build/BuildOnServerTask.kt b/buildSrc-fork/private/src/main/kotlin/androidx/build/BuildOnServerTask.kt
new file mode 100644
index 0000000000000..e6d7376ff1946
--- /dev/null
+++ b/buildSrc-fork/private/src/main/kotlin/androidx/build/BuildOnServerTask.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2019 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 java.io.FileNotFoundException
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+
+/**
+ * Task for building all of Androidx libraries and documentation
+ *
+ * AndroidXImplPlugin configuration adds dependencies to BuildOnServer for all of the tasks that
+ * produce artifacts that we want to build on server builds When BuildOnServer executes, it
+ * double-checks that all expected artifacts were built
+ */
+@CacheableTask
+abstract class BuildOnServerTask : DefaultTask() {
+
+ init {
+ group = "Build"
+ description = "Builds all of the Androidx libraries and documentation"
+ }
+
+ @get:InputFile
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val aggregateBuildInfoFile: RegularFileProperty
+
+ @TaskAction
+ fun checkAllBuildOutputs() {
+ if (!aggregateBuildInfoFile.get().asFile.exists()) {
+ throw FileNotFoundException(
+ "buildOnServer required output missing: " +
+ "${aggregateBuildInfoFile.get().asFile.path}"
+ )
+ }
+ }
+}
diff --git a/buildSrc-fork/private/src/main/kotlin/androidx/build/CheckKotlinApiTargetTask.kt b/buildSrc-fork/private/src/main/kotlin/androidx/build/CheckKotlinApiTargetTask.kt
new file mode 100644
index 0000000000000..23e117df41af1
--- /dev/null
+++ b/buildSrc-fork/private/src/main/kotlin/androidx/build/CheckKotlinApiTargetTask.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2024 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 org.gradle.api.DefaultTask
+import org.gradle.api.artifacts.component.ModuleComponentIdentifier
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+import org.gradle.work.DisableCachingByDefault
+import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
+
+/** Check if the kotlin-stdlib transitive dependencies are the same as the project specified one. */
+@DisableCachingByDefault(because = "not worth caching")
+abstract class CheckKotlinApiTargetTask : DefaultTask() {
+
+ @get:Input abstract val kotlinTarget: Property
+
+ @get:Internal val projectPath: String = project.path
+
+ @get:Input
+ val allDependencies: Provider>> =
+ project.provider {
+ project.configurations
+ .filter(project::shouldVerifyConfiguration)
+ .filter { it.isCanBeResolved && it.isPublished() }
+ .flatMap { config ->
+ config.incoming.resolutionResult.allComponents.mapNotNull { component ->
+ (component.id as? ModuleComponentIdentifier)?.let { id ->
+ "${id.module}:${id.version}" to config.name
+ }
+ }
+ }
+ }
+
+ @get:OutputFile abstract val outputFile: RegularFileProperty
+
+ @TaskAction
+ fun check() {
+ val incompatibleConfigurations =
+ allDependencies
+ .get()
+ .asSequence()
+ .filter { it.first.startsWith("kotlin-stdlib:") }
+ .map { it.first.substringAfter(":") to it.second }
+ .map { KotlinVersion.fromVersion(it.first.substringBeforeLast('.')) to it.second }
+ .filter { it.first > kotlinTarget.get() }
+ .map { "${it.second} (${it.first})" }
+ .toList()
+
+ val outputFile = outputFile.get().asFile
+ outputFile.parentFile.mkdirs()
+
+ if (incompatibleConfigurations.isNotEmpty()) {
+ val errorMessage =
+ incompatibleConfigurations.joinToString(
+ separator = "\n - ",
+ prefix =
+ "The project's kotlin-stdlib target is ${kotlinTarget.get()} but these " +
+ "configurations are pulling in higher versions of kotlin-stdlib:\n - ",
+ postfix =
+ "\n\nRun ./gradlew $projectPath:dependencies to see which dependency is " +
+ "pulling in the incompatible kotlin-stdlib",
+ )
+ outputFile.writeText("FAILURE: $errorMessage")
+ throw IllegalStateException(errorMessage)
+ }
+ }
+
+ companion object {
+ const val TASK_NAME = "checkKotlinApiTarget"
+ }
+}
diff --git a/collection/collection-compatibility-stub/src/commonMain/kotlin/EmptyFile.kt b/buildSrc-fork/private/src/main/kotlin/androidx/build/ClasspathBuilder.kt
similarity index 54%
rename from collection/collection-compatibility-stub/src/commonMain/kotlin/EmptyFile.kt
rename to buildSrc-fork/private/src/main/kotlin/androidx/build/ClasspathBuilder.kt
index cfcdef3ab267e..3d9c86d000465 100644
--- a/collection/collection-compatibility-stub/src/commonMain/kotlin/EmptyFile.kt
+++ b/buildSrc-fork/private/src/main/kotlin/androidx/build/ClasspathBuilder.kt
@@ -14,9 +14,17 @@
* limitations under the License.
*/
-// We prefer to have no source code here, but a module can't be empty.
-// We use this module to publish a dumb klib to be provided to the compilation of user projects.
-// It's needed because Kotlin tries to resolve the dependencies listed in klib manifest.
-// There is an intention to drop this behavior: https://youtrack.jetbrains.com/issue/KT-61096
-// The actual klib is published at androidx maven coordinates in Google maven.
-// This module depends on the actual klib, so the module API will be available transitively.
\ No newline at end of file
+package androidx.build
+
+import org.gradle.api.Project
+import org.gradle.api.file.FileCollection
+
+/**
+ * Returns a FileCollection that is a classpath of the library defined in the libs.versions.toml.
+ */
+fun Project.getLibraryClasspath(libraryName: String): FileCollection {
+ return configurations
+ .detachedConfiguration(dependencies.create(getLibraryByName(libraryName)))
+ .incoming
+ .files
+}
diff --git a/buildSrc-fork/private/src/main/kotlin/androidx/build/ConfigureAarAsJar.kt b/buildSrc-fork/private/src/main/kotlin/androidx/build/ConfigureAarAsJar.kt
new file mode 100644
index 0000000000000..09984dcf20739
--- /dev/null
+++ b/buildSrc-fork/private/src/main/kotlin/androidx/build/ConfigureAarAsJar.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2022 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 com.android.build.api.attributes.BuildTypeAttr
+import org.gradle.api.Project
+import org.gradle.api.artifacts.type.ArtifactTypeDefinition
+import org.gradle.api.attributes.Usage
+import org.gradle.api.attributes.java.TargetJvmEnvironment
+
+/**
+ * Creates `[configurationName]AarAsJar` config for JVM tests that need Android library classes on
+ * the classpath.
+ */
+internal fun configureAarAsJarForConfiguration(project: Project, configurationName: String) {
+ val releaseVariant =
+ project.objects.named(BuildTypeAttr::class.java, Release.DEFAULT_PUBLISH_CONFIG)
+ val javaApiUsage = project.objects.named(Usage::class.java, Usage.JAVA_API)
+ val androidJvmEnv =
+ project.objects.named(TargetJvmEnvironment::class.java, TargetJvmEnvironment.ANDROID)
+
+ val aarAsJarConfig =
+ project.configurations.register("${configurationName}AarAsJar") {
+ it.isTransitive = false
+ it.isCanBeConsumed = false
+ it.isCanBeResolved = true
+
+ it.attributes.apply {
+ attribute(BuildTypeAttr.ATTRIBUTE, releaseVariant)
+ attribute(Usage.USAGE_ATTRIBUTE, javaApiUsage)
+ attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE, androidJvmEnv)
+ attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "android-classes-jar")
+ }
+ }
+
+ project.configurations.named(configurationName) { config ->
+ config.dependencies.add(project.dependencies.create(aarAsJarConfig.get().incoming.files))
+ }
+}
diff --git a/buildSrc-fork/private/src/main/kotlin/androidx/build/CreateYarnRcTask.kt b/buildSrc-fork/private/src/main/kotlin/androidx/build/CreateYarnRcTask.kt
new file mode 100644
index 0000000000000..e9e96e6a359a9
--- /dev/null
+++ b/buildSrc-fork/private/src/main/kotlin/androidx/build/CreateYarnRcTask.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2024 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 org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.work.DisableCachingByDefault
+
+/**
+ * Creates an `.yarnrc` file in a specified directory. The `.yarnrc` file will contain the path to
+ * the offline storage of the required dependencies.
+ */
+@DisableCachingByDefault(because = "not worth caching")
+abstract class CreateYarnRcFileTask : DefaultTask() {
+
+ @get:InputDirectory
+ @get:PathSensitive(PathSensitivity.ABSOLUTE)
+ abstract val offlineMirrorStorage: DirectoryProperty
+
+ @get:OutputDirectory abstract val cacheStorage: DirectoryProperty
+
+ @get:OutputFile abstract val yarnrcFile: RegularFileProperty
+
+ @TaskAction
+ fun createFile() {
+ val offlineStoragePath = offlineMirrorStorage.get().asFile.absolutePath
+ val cacheStoragePath = cacheStorage.get().asFile.absolutePath
+ yarnrcFile.get().asFile.let {
+ it.parentFile.mkdirs()
+ it.writeText(
+ """
+ yarn-offline-mirror "$offlineStoragePath"
+ cache-folder "$cacheStoragePath"
+ """
+ .trimIndent()
+ )
+ }
+ }
+}
diff --git a/buildSrc-fork/private/src/main/kotlin/androidx/build/DependencyAnalysisPostProcessingTasks.kt b/buildSrc-fork/private/src/main/kotlin/androidx/build/DependencyAnalysisPostProcessingTasks.kt
new file mode 100644
index 0000000000000..2deb4e5785381
--- /dev/null
+++ b/buildSrc-fork/private/src/main/kotlin/androidx/build/DependencyAnalysisPostProcessingTasks.kt
@@ -0,0 +1,282 @@
+/*
+ * Copyright 2025 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.build.logging.TERMINAL_RED
+import androidx.build.logging.TERMINAL_RESET
+import androidx.build.uptodatedness.cacheEvenIfNoOutputs
+import com.autonomousapps.AbstractPostProcessingTask
+import com.autonomousapps.model.ModuleCoordinates
+import com.autonomousapps.model.ProjectAdvice
+import com.autonomousapps.model.ProjectCoordinates
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import java.io.File
+import kotlin.text.appendLine
+import org.gradle.api.Project
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+
+/**
+ * Task that reports dependency analysis advice for the project. It gets advice from the dependency
+ * analysis gradle plugin and checks the baselines for the advice already captured and only reports
+ * if additional violations are found.
+ */
+@CacheableTask
+abstract class ReportDependencyAnalysisAdviceTask : AbstractPostProcessingTask() {
+ init {
+ group = "Verification"
+ description = "Task for generating advice for dependency analysis"
+ }
+
+ @get:Internal abstract val baseLineFile: RegularFileProperty
+
+ @InputFile
+ @Optional
+ @PathSensitive(PathSensitivity.NONE)
+ fun getDependencyAnalysisBaseline(): File? = baseLineFile.get().asFile.takeIf { it.exists() }
+
+ @get:Internal val projectPath: String = project.path
+ @get:Internal val isKMP: Boolean = project.multiplatformExtension != null
+ @get:Internal
+ val isPublishedLibrary: Boolean =
+ project.extensions.getByType(AndroidXExtension::class.java).type ==
+ SoftwareType.PUBLISHED_LIBRARY
+
+ @TaskAction
+ fun getAdvice() {
+ val projectAdvice =
+ this@ReportDependencyAnalysisAdviceTask.projectAdvice().toAndroidxProjectAdvice()
+
+ val baselineAdvice =
+ Gson()
+ .fromJson(
+ getDependencyAnalysisBaseline()?.readText(),
+ AndroidxProjectAdvice::class.java,
+ )
+
+ val advice =
+ if (baselineAdvice != null) {
+ getIncrementalAdvice(
+ projectAdvice.dependencyAdvice.filter {
+ !baselineAdvice.dependencyAdvice.contains(it)
+ }
+ )
+ } else {
+ getIncrementalAdvice(projectAdvice.dependencyAdvice)
+ }
+
+ if (advice.isNotBlank()) {
+ error(
+ """
+ There are some new dependencies added to this change that might be misconfigured:
+ $advice
+ ********************************************************************************
+ $TERMINAL_RED
+ To get a complete list of misconfigured dependencies, please run:
+ ./gradlew $projectPath:projectHealth.
+ To update the dependency analysis baseline file, please run:
+ ./gradlew $projectPath:updateDependencyAnalysisBaseline
+ $TERMINAL_RESET
+ ********************************************************************************
+ """
+ .trimIndent()
+ )
+ }
+ }
+
+ private fun getIncrementalAdvice(missingDependencyAdvice: List): String {
+ // Skip the reporting of modify dependencies for now, so that advice is easier to follow.
+ val unused = mutableSetOf()
+ val transitive = mutableSetOf()
+ val advice = StringBuilder()
+
+ missingDependencyAdvice.forEach {
+ // Don't fail CI if test source set has misconfigured dependencies
+ if (it.fromConfiguration?.contains("test", ignoreCase = true) == true) {
+ return@forEach
+ }
+ if (it.toConfiguration?.contains("test", ignoreCase = true) == true) {
+ return@forEach
+ }
+
+ val isCompileOnly =
+ it.toConfiguration?.endsWith("compileOnly", ignoreCase = true) == true
+ val isTransitiveDependencyAdvice =
+ it.fromConfiguration == null && it.toConfiguration != null && !isCompileOnly
+ val isUnusedDependencyAdvice =
+ it.fromConfiguration != null && it.toConfiguration == null
+
+ val identifier =
+ if (it.coordinates.type == "project") {
+ "project(${it.coordinates.identifier})"
+ } else {
+ "'${it.coordinates.identifier}:${it.coordinates.resolvedVersion}'"
+ }
+ if (isTransitiveDependencyAdvice) {
+ transitive.add("${it.toConfiguration}($identifier)")
+ }
+ if (isUnusedDependencyAdvice) {
+ unused.add("${it.fromConfiguration}($identifier)")
+ }
+ }
+ if (unused.isNotEmpty()) {
+ advice.appendLine("Unused dependencies which should be removed:")
+ advice.appendLine(unused.sorted().joinToString(separator = "\n"))
+ }
+ if (transitive.isNotEmpty()) {
+ advice.appendLine("These transitive dependencies can be declared directly:")
+ advice.appendLine(transitive.sorted().joinToString(separator = "\n"))
+ }
+ return advice.toString()
+ }
+}
+
+/** Task to update dependency analysis baselines for the project. */
+@CacheableTask
+abstract class UpdateDependencyAnalysisBaseLineTask : AbstractPostProcessingTask() {
+ init {
+ group = "Verification"
+ description = "Task for updating dependency analysis baselines"
+ }
+
+ @get:OutputFile abstract val outputFile: RegularFileProperty
+ @get:Internal val isKMP: Boolean = project.multiplatformExtension != null
+ @get:Internal
+ val isPublishedLibrary: Boolean =
+ project.extensions.getByType(AndroidXExtension::class.java).type ==
+ SoftwareType.PUBLISHED_LIBRARY
+
+ @TaskAction
+ fun updateBaseLineForDependencyAnalysisAdvice() {
+ val projectAdvice =
+ this@UpdateDependencyAnalysisBaseLineTask.projectAdvice().toAndroidxProjectAdvice()
+ val outputFile = outputFile.get()
+ val gson = GsonBuilder().setPrettyPrinting().create()
+ outputFile.asFile.writeText(gson.toJson(projectAdvice))
+ }
+}
+
+/**
+ * Configure the dependency analysis gradle plugin and register new post-processing tasks:
+ * 1. Updating the baselines for advice provided by the plugin.
+ * 2. Getting any incremental advice not captured in the baselines.
+ */
+internal fun Project.configureDependencyAnalysisPlugin() {
+ plugins.apply("com.autonomousapps.dependency-analysis")
+
+ val updateDependencyAnalysisBaselineTask =
+ tasks.register(
+ "updateDependencyAnalysisBaseline",
+ UpdateDependencyAnalysisBaseLineTask::class.java,
+ ) { task ->
+ task.outputFile.set(layout.projectDirectory.file("dependencyAnalysis-baseline.json"))
+ task.cacheEvenIfNoOutputs()
+ // DAGP currently doesn't support KMP, enable KMP projects when b/394970486 is resolved
+ task.onlyIf { !(task.isKMP) && task.isPublishedLibrary }
+ }
+
+ val reportDependencyAnalysisAdviceTask =
+ tasks.register(
+ "reportDependencyAnalysisAdvice",
+ ReportDependencyAnalysisAdviceTask::class.java,
+ ) { task ->
+ task.baseLineFile.set(layout.projectDirectory.file("dependencyAnalysis-baseline.json"))
+ task.cacheEvenIfNoOutputs()
+ // DAGP currently doesn't support KMP, enable KMP projects when b/394970486 is resolved
+ task.onlyIf { !(task.isKMP) && task.isPublishedLibrary }
+ }
+
+ val dependencyAnalysisSubExtension =
+ extensions.getByType(com.autonomousapps.DependencyAnalysisSubExtension::class.java)
+ dependencyAnalysisSubExtension.registerPostProcessingTask(reportDependencyAnalysisAdviceTask)
+ dependencyAnalysisSubExtension.registerPostProcessingTask(updateDependencyAnalysisBaselineTask)
+
+ // Ignore advice for runTimeOnly, compileOnly or incorrect dependency configs
+ // since it affects downstream consumers
+ dependencyAnalysisSubExtension.issues { it.onIncorrectConfiguration { it.severity("ignore") } }
+ dependencyAnalysisSubExtension.issues { it.onRuntimeOnly { it.severity("ignore") } }
+ dependencyAnalysisSubExtension.issues { it.onCompileOnly { it.severity("ignore") } }
+
+ // DAGP currently doesn't support KMP, enable KMP projects when b/394970486 is resolved
+ // Enable CI check for published libraries
+ if (
+ multiplatformExtension == null &&
+ androidXExtension.type.get() == SoftwareType.PUBLISHED_LIBRARY
+ ) {
+ addToBuildOnServer(reportDependencyAnalysisAdviceTask)
+ }
+}
+
+/**
+ * Helper data classes to store the advice provided Dependency Analysis Gradle plugin in baselines.
+ */
+internal data class AndroidxProjectAdvice(
+ val projectPath: String,
+ val dependencyAdvice: List,
+)
+
+internal data class DependencyAdvice(
+ val coordinates: Coordinates,
+ val fromConfiguration: String?,
+ val toConfiguration: String?,
+)
+
+internal data class Coordinates(
+ val type: String,
+ val identifier: String,
+ val resolvedVersion: String?,
+)
+
+/** Convert advice reported by DAGP into format suitable for storing in baselines. */
+internal fun ProjectAdvice.toAndroidxProjectAdvice(): AndroidxProjectAdvice {
+ return AndroidxProjectAdvice(
+ projectPath = projectPath,
+ dependencyAdvice =
+ dependencyAdvice.map {
+ val type =
+ if (it.coordinates is ProjectCoordinates) {
+ "project"
+ } else {
+ "module"
+ }
+ val resolvedVersion =
+ if (it.coordinates is ModuleCoordinates) {
+ (it.coordinates as ModuleCoordinates).resolvedVersion
+ } else {
+ null
+ }
+ DependencyAdvice(
+ coordinates =
+ Coordinates(
+ identifier = it.coordinates.identifier,
+ resolvedVersion = resolvedVersion,
+ type = type,
+ ),
+ fromConfiguration = it.fromConfiguration,
+ toConfiguration = it.toConfiguration,
+ )
+ },
+ )
+}
diff --git a/buildSrc-fork/private/src/main/kotlin/androidx/build/DevelocityTokenFetcher.kt b/buildSrc-fork/private/src/main/kotlin/androidx/build/DevelocityTokenFetcher.kt
new file mode 100644
index 0000000000000..562129865adf0
--- /dev/null
+++ b/buildSrc-fork/private/src/main/kotlin/androidx/build/DevelocityTokenFetcher.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2025 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 com.google.cloud.secretmanager.v1.SecretManagerServiceClient
+import com.google.cloud.secretmanager.v1.SecretVersionName
+import java.io.File
+import org.gradle.api.Project
+import org.gradle.api.provider.ValueSource
+import org.gradle.api.provider.ValueSourceParameters
+
+/**
+ * If the user hasn't set up develocity on this machine then fetch a shared key to enable it for
+ * them.
+ */
+internal fun Project.fetchDevelocityKeysIfNeeded() {
+ // Playground users don't need Develocity set up
+ if (ProjectLayoutType.isPlayground(this)) return
+
+ // We are in CI, so we should not fetch these keys
+ if (System.getenv("IS_ANDROIDX_CI") != null) return
+
+ // User does not have remote cache enabled, so we will not have access to GCP
+ if (System.getenv("USE_ANDROIDX_REMOTE_BUILD_CACHE") !in setOf("gcp", "true")) return
+
+ val keys = File("${System.getenv("GRADLE_USER_HOME")}/develocity/keys.properties")
+
+ // User already has the keys
+ if (keys.exists()) return
+
+ keys.parentFile.mkdirs()
+
+ val keysProvider = providers.of(DevelocityKeysValueSource::class.java) {}
+ keys.writeText(keysProvider.get())
+}
+
+/**
+ * Using a ValueSource to fetch Develocity keys because the SecretManagerServiceClient on Macs use
+ * external processes (such as codesign and install_name_tool) and that is not allowed when
+ * configuration cache is enabled without wrapping those calls in a ValueSource.
+ */
+internal abstract class DevelocityKeysValueSource :
+ ValueSource {
+ override fun obtain(): String? {
+ var value: String? = null
+ try {
+ SecretManagerServiceClient.create().use { manager ->
+ val secretVersionName =
+ SecretVersionName.of("androidx-ge", "develocity-token", "latest")
+ val response = manager.accessSecretVersion(secretVersionName)
+ value = response.payload.data.toStringUtf8()
+ }
+ } catch (e: Exception) {
+ println("Failed to fetch develocity keys")
+ e.printStackTrace()
+ }
+ return value
+ }
+}
diff --git a/buildSrc-fork/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt b/buildSrc-fork/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
new file mode 100644
index 0000000000000..19c4ab840900f
--- /dev/null
+++ b/buildSrc-fork/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
@@ -0,0 +1,323 @@
+/*
+ * Copyright 2017 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 com.android.build.api.variant.AndroidComponentsExtension
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.logging.Logging
+import org.gradle.api.plugins.JavaPlugin.COMPILE_JAVA_TASK_NAME
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.SourceSetContainer
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.api.tasks.compile.JavaCompile
+import org.gradle.kotlin.dsl.exclude
+import org.gradle.kotlin.dsl.get
+import org.gradle.kotlin.dsl.getByName
+import org.gradle.process.CommandLineArgumentProvider
+import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
+
+const val ERROR_PRONE_TASK = "runErrorProne"
+
+private const val ERROR_PRONE_CONFIGURATION = "errorprone"
+private val log = Logging.getLogger("ErrorProneConfiguration")
+
+fun Project.configureErrorProneForJava() {
+ val errorProneConfiguration = createErrorProneConfiguration()
+ project.extensions.getByName("sourceSets").configureEach {
+ project.configurations[it.annotationProcessorConfigurationName].extendsFrom(
+ errorProneConfiguration
+ )
+ }
+ val kmpExtension = project.multiplatformExtension
+ log.info("Configuring error-prone for ${project.path}")
+ if (kmpExtension != null) { // KMP project
+ val compileJavaTaskProvider =
+ kmpExtension
+ .jvm()
+ .compilations
+ .getByName(KotlinCompilation.MAIN_COMPILATION_NAME)
+ .compileJavaTaskProvider
+ makeErrorProneTask(compileJavaTaskProvider)
+ } else { // non-KMP project
+ makeErrorProneTask(tasks.withType(JavaCompile::class.java).named(COMPILE_JAVA_TASK_NAME))
+ }
+}
+
+fun Project.configureErrorProneForAndroid() {
+ val androidComponents = extensions.findByType(AndroidComponentsExtension::class.java)
+ androidComponents?.onVariants { variant ->
+ if (variant.buildType == "release") {
+ @Suppress("UnstableApiUsage", "USELESS_ELVIS")
+ // b/397707182 this is still @Incubating in AGP
+ // b/328749039 This is being made nullable in AGP
+ val javaCompilation = variant.javaCompilation ?: return@onVariants
+ val errorProneConfiguration = createErrorProneConfiguration()
+ configurations
+ .getByName(variant.annotationProcessorConfiguration.name)
+ .extendsFrom(errorProneConfiguration)
+
+ log.info("Configuring error-prone for ${variant.name}'s java compile")
+ afterEvaluate {
+ makeErrorProneTask(
+ compileTaskProvider =
+ tasks
+ .withType(JavaCompile::class.java)
+ .named("compile${variant.name.camelCase()}JavaWithJavac"),
+ taskSuffix = variant.name.camelCase(),
+ ) { javaCompile ->
+ @Suppress("UnstableApiUsage") // JavaCompilation b/397707182
+ val annotationArgs = javaCompilation.annotationProcessor.arguments
+ javaCompile.options.compilerArgumentProviders.add(
+ CommandLineArgumentProviderAdapter(annotationArgs)
+ )
+ }
+ }
+ }
+ }
+}
+
+class CommandLineArgumentProviderAdapter(@get:Input val arguments: Provider