Splitting buildSrc into multiple projects
:buildSrc:plugins contains the plugins applied by projects
:buildSrc:private contains implementation
This allows the contents of plugins/ to not be added to the classpath that Gradle uses for evaluating projects, which means that changes to those classes don't necessarily invalidate the UP-TO-DATE status of other tasks applied by other plugins in those projects (most notably compileKotlin)
Bug: 140265324
Test: ./gradlew projects
Test: cd paging && ./gradlew checkApi
Test: Treehugger runs busytown/androidx.sh
Test: # demonstrate that unrelated changes in buildSrc no longer invalidate compileKotlin tasks unnecessarily
# build kotlin
./gradlew :core:core:compileDebugKotlin
# make some unrelated changes in buildSrc:
sed -i 's/ignoreCase = true/ignoreCase = false/g' buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
# build kotlin again
./gradlew :core:core:compileDebugKotlin
# see that the tasks were up-to-date
Test: # demonstrate that making a relevant change in buildSrc does invalidate kotlinCompile tasks
1: # change jvmTarget
sed -i 's/jvmTarget = "1.8"/jvmTarget = "9"/' buildSrc/impl/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
2: # run another build
./gradlew :core:core:compileDebugKotlin
3: # see that this task was out-of-date
Change-Id: I8fb321ca59f9eb0518f730748489a34f376de605
diff --git a/buildSrc/shared.gradle b/buildSrc/shared.gradle
new file mode 100644
index 0000000..a82ce62
--- /dev/null
+++ b/buildSrc/shared.gradle
@@ -0,0 +1,166 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+apply plugin: "kotlin"
+apply from: "../kotlin-dsl-dependency.gradle"
+
+buildscript {
+ project.ext.supportRootFolder = project.projectDir.getParentFile().getParentFile()
+ apply from: "../repos.gradle"
+ repos.addMavenRepositories(repositories)
+ dependencies {
+ classpath(libs.kotlinGradlePluginz)
+ }
+}
+
+configurations {
+ // Dependencies added to these configurations get copied into the corresponding configuration
+ // (cacheableApi gets copied into api, etc).
+ // Because we cache the resolutions of these configurations, performance is faster when
+ // artifacts are put into these configurations than when those artifacts are put into their
+ // corresponding configuration.
+ cacheableApi
+ cacheableImplementation {
+ extendsFrom(project.configurations.cacheableApi)
+ }
+ cacheableRuntimeOnly
+}
+
+dependencies {
+ cacheableApi(libs.androidGradlePluginz)
+ cacheableImplementation(libs.dexMemberList)
+ cacheableApi(libs.kotlinGradlePluginz)
+ cacheableImplementation(gradleApi())
+ cacheableApi(libs.dokkaGradlePluginz)
+ // needed by inspection plugin
+ cacheableImplementation(libs.protobufGradlePluginz)
+ cacheableImplementation(libs.wireGradlePluginz)
+ cacheableImplementation(libs.shadow)
+ // dependencies that aren't used by buildSrc directly but that we resolve here so that the
+ // root project doesn't need to re-resolve them and their dependencies on every build
+ cacheableRuntimeOnly(libs.hiltAndroidGradlePluginz)
+ // room kotlintestapp uses the ksp plugin but it does not publish a plugin marker yet
+ cacheableApi(libs.kspGradlePluginz)
+ cacheableApi(libs.japicmpPluginz)
+ // dependencies whose resolutions we don't need to cache
+ compileOnly(findGradleKotlinDsl()) // Only one file in this configuration, no need to cache it
+ implementation(project(":jetpad-integration")) // Doesn't have a .pom, so not slow to load
+}
+
+// Exclude dokka coming from AGP. We don't need it and it conflicts with dackka: b/195305339
+configurations.configureEach { conf ->
+ conf.exclude(group:"org.jetbrains.dokka", module:"dokka-core")
+}
+
+apply plugin: "java-gradle-plugin"
+
+sourceSets {
+ main.java.srcDirs += "../public/src/main/kotlin"
+ main.resources.srcDirs += "../public/src/main/resources"
+
+
+ main.java.srcDirs += "${supportRootFolder}/benchmark/gradle-plugin/src/main/kotlin"
+ main.resources.srcDirs += "${supportRootFolder}/benchmark/gradle-plugin/src/main/resources"
+
+ main.java.srcDirs += "${supportRootFolder}/inspection/inspection-gradle-plugin/src/main/kotlin"
+ main.resources.srcDirs += "${supportRootFolder}/inspection/inspection-gradle-plugin/src/main" +
+ "/resources"
+
+ main.java.srcDirs += "${supportRootFolder}/compose/material/material/icons/generator/src/main" +
+ "/kotlin"
+}
+
+gradlePlugin {
+ plugins {
+ benchmark {
+ id = "androidx.benchmark"
+ implementationClass = "androidx.benchmark.gradle.BenchmarkPlugin"
+ }
+ inspection {
+ id = "androidx.inspection"
+ implementationClass = "androidx.inspection.gradle.InspectionPlugin"
+ }
+ }
+}
+
+// Saves configuration into destFile
+// Each line of destFile will be the absolute filepath of one of the files in configuration
+def saveConfigurationResolution(configuration, destFile) {
+ def resolvedConfiguration = configuration.resolvedConfiguration
+ def files = resolvedConfiguration.files
+ def paths = files.collect { f -> f.toString() }
+ def serialized = paths.join("\n")
+ destFile.text = serialized
+}
+
+// Parses a file into a list of Dependency objects representing a ResolvedConfiguration
+def parseConfigurationResolution(savedFile, throwOnError) {
+ def savedText = savedFile.text
+ def filenames = savedText.split("\n")
+ def valid = true
+ def dependencies = filenames.collect { filename ->
+ if (!project.file(filename).exists()) {
+ if (throwOnError) {
+ throw new GradleException("\nFile " + filename + " listed as a resolved dependency in " + savedFile + " does not exist!\n\nFor more information, see b/187075069")
+ } else {
+ valid = false
+ }
+ }
+ project.dependencies.create(project.files(filename))
+ }
+ if (!valid) {
+ return null
+ }
+ return dependencies
+}
+
+// Resolves a Configuration into a list of Dependency objects
+def resolveConfiguration(configuration) {
+ def resolvedName = configuration.name
+ def cacheDir = new File(project.buildDir, "/" + resolvedName)
+ def inputsFile = new File(cacheDir, "/deps")
+ def outputsFile = new File(cacheDir, "/result")
+
+ def inputText = fingerprintConfiguration(configuration)
+ def parsed = null
+ if (inputsFile.exists() && inputsFile.text == inputText) {
+ // Try to parse the previously resolved configuration, but don't give up if it mentions a
+ // nonexistent file. If something has since deleted one of the referenced files, we will
+ // try to reresolve that file later
+ parsed = parseConfigurationResolution(outputsFile, false)
+ }
+ // If the configuration has changed or if any of its files have been deleted, reresolve it
+ if (parsed == null) {
+ cacheDir.mkdirs()
+ saveConfigurationResolution(configuration, outputsFile)
+ inputsFile.text = inputText
+ // confirm that the resolved configuration parses successfully
+ parsed = parseConfigurationResolution(outputsFile, true)
+ }
+ return parsed
+}
+
+// Computes a unique string from a Configuration based on its dependencies
+// This is used for up-to-date checks
+def fingerprintConfiguration(configuration) {
+ def dependencies = configuration.allDependencies
+ def dependencyTexts = dependencies.collect { dep -> dep.group + ":" + dep.name + ":" + dep.version }
+ return dependencyTexts.join("\n")
+}
+
+// Imports the contents of fromConf into toConf
+// Uses caching to often short-circuit the resolution of fromConf
+def loadConfigurationQuicklyInto(fromConf, toConf) {
+ def resolved = resolveConfiguration(fromConf)
+ resolved.each { dep ->
+ project.dependencies.add(toConf.name, dep)
+ }
+}
+
+loadConfigurationQuicklyInto(configurations.cacheableApi, configurations.api)
+loadConfigurationQuicklyInto(configurations.cacheableImplementation, configurations.implementation)
+loadConfigurationQuicklyInto(configurations.cacheableRuntimeOnly, configurations.runtimeOnly)
+
+project.tasks.withType(Jar) { task ->
+ task.reproducibleFileOrder = true
+ task.preserveFileTimestamps = false
+}