Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Plugging into the Gradle Matrix (Droidcon London 2022)

Plugging into the Gradle Matrix (Droidcon London 2022)

Whether consolidating build script logic, writing convention plugins, or adding tooling to your apps, Gradle plugins are powerful ways to extend your build.

This talk will show how to develop your plugins safely while avoiding some of the pitfalls you might otherwise encounter, especially the ones that can negatively impact your build speed!

We'll analyze various plugins in the wild and explore:
* how to manage cacheability - build and configuration!
* deferring task creation until they're actually needed
* working with properties, providers, and file collections
* how convention plugins can help you scale your app builds sanely
* what APIs you should stop using now!

Co-Presented with @AutonomousApps.

John Rodriguez

October 27, 2022
Tweet

More Decks by John Rodriguez

Other Decks in Programming

Transcript

  1. apply plugin: 'com.android.library' apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'org.jetbrains.kotlin.kapt' apply

    plugin: 'org.jetbrains.kotlin.plugin.parcelize' apply plugin: 'app.cash.paparazzi' apply plugin: 'app.cash.treehouse'
  2. JavaCompile task A.class A.java JDK 11 4 GB CACHED echo

    “org.gradle.caching=true” >> gradle.properties
  3. public interface TaskContainer extends TaskCollection<Task>, ... { ... @Override <T

    extends Task> T create( String name, Class<T> type, Action<? super T> configuration ) throws InvalidUserDataException; @Override TaskProvider<Task> register( String name, Action<? super Task> configurationAction ) throws InvalidUserDataException; ... }
  4. public interface TaskContainer extends TaskCollection<Task>, ... { ... @Override <T

    extends Task> T create( String name, Class<T> type, Action<? super T> configuration ) throws InvalidUserDataException; @Override TaskProvider<Task> register( String name, Action<? super Task> configurationAction ) throws InvalidUserDataException; ... }
  5. public interface Provider<T> { T get(); <S> Provider<S> map(Transformer<S, T>

    transformer); <S> Provider<S> flatMap(Transformer<Provider<S>, T> transformer); ... } Read-only
  6. public interface Property<T> extends Provider<T> { void set(@Nullable T value);

    void set(Provider<T> provider); Property<T> value(T value); Property<T> value(Provider<T> provider); ... } Mutable
  7. public interface ConfigurableFileCollection extends FileCollection public interface ConfigurableFileTree extends FileTree

    public interface RegularFileProperty extends Property<RegularFile> public interface DirectoryProperty extends Property<Directory> public interface FileCollection extends Iterable<File> public interface FileTree extends FileCollection Read-only Mutable
  8. abstract class Producer : DefaultTask() { @get:OutputFile abstract val outputFile:

    RegularFileProperty @TaskAction fun produce() { outputFile.get().asFile.writeText("...") } } abstract class Consumer : DefaultTask() { @get:InputFile abstract val inputFile: RegularFileProperty @TaskAction fun consume() { val message = inputFile.get().asFile.readText() } }
  9. val producer = project.tasks.register<Producer>("producer") val consumer = project.tasks.register<Consumer>("consumer") consumer {

    inputFile.set(producer.flatMap { it.outputFile }) } producer { outputFile.set(project.layout.buildDirectory.file(“file.txt")) }
  10. val producer = project.tasks.register<Producer>("producer") val consumer = project.tasks.register<Consumer>("consumer") consumer {

    inputFile.set(producer.flatMap { it.outputFile }) } producer { outputFile.set(project.layout.buildDirectory.file(“file.txt”)) dependsOn(consumer) }
  11. val producer = project.tasks.register<Producer>("producer") val consumer = project.tasks.register<Consumer>("consumer") consumer {

    inputFile.set(producer.flatMap { it.outputFile }) } producer { outputFile.set(project.layout.buildDirectory.file(“file.txt")) }
  12. val producer = project.tasks.register<Producer>("producer") val consumer = project.tasks.register<Consumer>("consumer") consumer {

    inputFile.set(producer.flatMap { it.outputFile }) } producer { outputFile.set(project.layout.buildDirectory.file(“file.txt")) } Using Provider-based task outputs as task inputs will implicitly add the correct task dependencies!
  13. val recordTaskProvider = project.tasks.register<PaparazziTask>(“recordPaparazzi") val verifyTaskProvider = project.tasks.register<PaparazziTask>("verifyPaparazzi") val isRecordRun

    = project.objects.property(Boolean::class.java) val isVerifyRun = project.objects.property(Boolean::class.java) project.gradle.taskGraph.whenReady { graph -> isRecordRun.set(grap h​ .hasTask(recordTaskProvide r​ .get())) isVerifyRun.set(grap h​ .hasTask(verifyTaskProvide r​ .get())) }
  14. val recordTaskProvider = project.tasks.register<PaparazziTask>(“recordPaparazzi") val verifyTaskProvider = project.tasks.register<PaparazziTask>("verifyPaparazzi") val isRecordRun

    = project.objects.property(Boolean::class.java) val isVerifyRun = project.objects.property(Boolean::class.java) project.gradle.taskGraph.whenReady { graph -> isRecordRun.set(recordTaskProvide r​ .map { graph .​ hasTask(it) }) isVerifyRun.set(verifyTaskProvide r​ .map { graph .​ hasTask(it) }) }
  15. val producer = project.tasks.register<Producer>("producer") val consumer = project.tasks.register<Consumer>("consumer") consumer {

    inputFile.set(producer.flatMap { it.outputFile }) } producer { outputFile.set(project.layout.buildDirectory.file(“file.txt")) }
  16. project.plugins.withType(JavaBasePlugin::class.java) { project.tasks.named("compile${testVariantSlug}JavaWithJavac") .configure { it.dependsOn(writeResourcesTask) } } project.plugins.withType(KotlinAndroidPluginWrapper::class.java) {

    project.tasks.named("compile${testVariantSlug}Kotlin") .configure { it.dependsOn(writeResourcesTask) } } project.plugins.withType(KotlinMultiplatformPluginWrapper::class.java) { project.tasks.named("compile${testVariantSlug}KotlinAndroid") .configure { it.dependsOn(writeResourcesTask) } }
  17. variants.all { variant -> val mergeResourcesOutputDir = variant.mergeResourcesProvider.flatMap { it.outputDir

    } val writeResourcesTask = project.tasks.register( "preparePaparazzi${variantSlug}Resources", PrepareResourcesTask::class.java ) { task -> task.mergeResourcesOutput.set(mergeResourcesOutputDir) ... } }
  18. variants.all { variant -> val packageAwareArtifacts = project.configurations .getByName("${variant.name}RuntimeClasspath") .incoming

    .artifactView { it.attributes.attribute( ARTIFACT_TYPE_ATTRIBUTE, “android-symbol-with-package-name" ) } .artifacts val writeResourcesTask = project.tasks.register( "preparePaparazzi${variantSlug}Resources", PrepareResourcesTask::class.java ) { task -> task.artifactFiles.from(packageAwareArtifacts.artifactFiles) ... } }
  19. abstract class DisplayApksTask : DefaultTask() { @get:InputFiles abstract valaapkFolder:aDirectoryProperty @get:Internal

    abstract valabuiltArtifactsLoader:aProperty<BuiltArtifactsLoader> @TaskAction fun taskAction() { val apks = builtArtifactsLoader.get().load(apkFolder.get()) apks.elements.forEach { println("Got an APK at ${it.outputFile}") } } } https://github.com/android/gradle-recipes/blob/agp-7.4/Kotlin/getApksTest/app/build.gradle.kts
  20. abstract class DisplayApksTask : DefaultTask() { ... @TaskAction fun taskAction()

    { val apks = builtArtifactsLoader.get().load(apkFolder.get()) apks.elements.forEach { println("Got an APK at ${it.outputFile}") } } } androidComponents { onVariants { variant -> project.tasks.register(“${variant.name}DisplayApks”) { val artifacts = variant.artifacts apkFolder.set(artifacts.get(SingleArtifact.APK)) builtArtifactsLoader.set(artifacts.getBuiltArtifactsLoader()) } } }
  21. fun Project.publishArtifact( configurationName: String, output: Provider<RegularFile> ): Configuration { return

    configurations.getByName(configurationName) { outgoing.variants.create(MY_ARTIFACT_TYPE) { isCanBeResolved = false isCanBeConsumed = true isVisible = false attributes { attribute(ARTIFACT_TYPE_ATTR, MY_ARTIFACT_TYPE) ... } artifact(output) { type = MY_ARTIFACT_TYPE } } } }
  22. fun Project.createResolvableConfiguration( configurationName: String, baseConfigurationName: String ): Configuration = configurations.create(configurationName)

    { isCanBeResolved = true isCanBeConsumed = false isVisible = false extendsFrom(configurations[baseConfigurationName]) attributes { attribute(ARTIFACT_TYPE_ATTR, MY_ARTIFACT_TYPE) } } https://tinyurl.com/58m8hk54
  23. $./gradlew producer:outgoingVariants --variant runtimeElements Attributes - org.gradle.category = library -

    org.gradle.dependency.bundling = external - org.gradle.jvm.version = 11 - org.gradle.libraryelements = jar - org.gradle.usage = java-runtime $./gradlew consumer:resolvableConfigurations --configuration testRuntimeClasspath Attributes - org.gradle.category = library - org.gradle.dependency.bundling = external - org.gradle.jvm.version = 11 - org.gradle.libraryelements = instrumented-jar - org.gradle.usage = java-runtime
  24. $./gradlew producer:outgoingVariants --variant runtimeElements Attributes - org.gradle.category = library -

    org.gradle.dependency.bundling = external - org.gradle.jvm.version = 11 - org.gradle.libraryelements = jar - org.gradle.usage = java-runtime $./gradlew consumer:resolvableConfigurations --configuration testRuntimeClasspath Attributes - org.gradle.category = library - org.gradle.dependency.bundling = external - org.gradle.jvm.version = 11 - org.gradle.libraryelements = instrumented-jar - org.gradle.usage = java-runtime
  25. abstract class CheckBestPracticesTask @Inject constructor( private val workerExecutor: WorkerExecutor )

    : DefaultTask() { interface Parametersb:bWorkParameters {b valbclassesDirs:bConfigurableFileCollectionb valboutput:bRegularFilePropertyb }b abstractbclassbActionb:bWorkAction<Parameters> {b override funbexecute()b{b valbclassesDirs = parameters.classesDirs valboutput = parameters.output ... }b }b }a
  26. abstract class CheckBestPracticesTask @Inject constructor( private val workerExecutor: WorkerExecutor )

    : DefaultTask() { @get:PathSensitive(PathSensitivity.NONE) @get:InputFiles abstract val classesDirs: ConfigurableFileCollection @get:OutputFile abstract val output: RegularFileProperty @TaskAction fun action() { workerExecutor.noIsolation().submit(Action::class.java) { it.classesDirs.setFrom(classesDirs) it.output.set(output) } } interface Parametersb:bWorkParameters {b
  27. • Good news: almost unnecessary if you use the configuration

    cache. • Lots of boilerplate, but helps with parallelization during execution phase. • Enforces best practices as a side effect. https://bit.ly/3sEQzJW
  28. abstract class InMemoryCache : BuildService<InMemoryCache.Params> { interface Params : BuildServiceParameters

    { val cacheSize: Property<Long> } ...don't forget to actually do stuff... internal companion object { fun register(project: Project): Provider<InMemoryCache> = project .gradle .sharedServices .registerIfAbsent("inMemoryCache", InMemoryCache::class.java) { parameters.cacheSize.set(project.cacheSize(-1L)) } } } https://tinyurl.com/bdhax5nz
  29. class MyPlugin : Plugin<Project> { override fun apply(target: Project): Unit

    = target.run { val cache = InMemoryCache.register(this) val myTask = tasks.register<MyTask>("myTask") { inMemoryCache.set(cache) usesService(cache) // stupid but (currently) necessary } } } abstract class MyTask : DefaultTask() { @get:Internal abstract val inMemoryCache: Property<InMemoryCache> }
  30. class BuildMetadataPlugin : Plugin<Project> { override fun apply(project: Project) {

    val androidComponents = project.extensions.getByType(AndroidComponentsExtension) androidComponents .onVariants { variant -> val commitShaProvider = project.tasks.register<GitCommitShaTask>( "commitSha${variant.name}" ) { task -> task.sha.set(File(project.buildDir, "commit-sha.txt")) task.doNotTrackState("Git already tracks state!") } variant.resValues.put( variant.makeResValueKey(“string", "commit_sha"), commitShaProvider.flatMap { sha }.map { file -> ResValue(
  31. class BuildMetadataPlugin : Plugin<Project> { override fun apply(project: Project) {

    val androidComponents = project.extensions.getByType(AndroidComponentsExtension) androidComponents .onVariants { variant -> val commitShaProvider = project.tasks.register<GitCommitShaTask>( "commitSha${variant.name}" ) { task -> task.sha.set(File(project.buildDir, "commit-sha.txt")) task.doNotTrackState("Git already tracks state!") } variant.resValues.put( variant.makeResValueKey(“string", "commit_sha"), commitShaProvider.flatMap { sha }.map { file -> ResValue(
  32. class BuildMetadataPlugin : Plugin<Project> { override fun apply(project: Project) {

    val androidComponents = project.extensions.getByType(AndroidComponentsExtension) androidComponents .onVariants { variant -> val commitShaProvider = project.tasks.register<GitCommitShaTask>( "commitSha${variant.name}" ) { task -> task.sha.set(File(project.buildDir, "commit-sha.txt")) task.doNotTrackState("Git already tracks state!") } variant.resValues.put( variant.makeResValueKey(“string", "commit_sha"), commitShaProvider.flatMap { sha }.map { file -> ResValue(
  33. class BuildMetadataPlugin : Plugin<Project> { override fun apply(project: Project) {

    ... androidComponents .onVariants { variant -> val commitShaProvider = ... variant.resValues.put( variant.makeResValueKey(“string", "commit_sha"), commitShaProvider.flatMap { sha }.map { file -> ResValue( file.asFile.readText(), "Value from default config." ) } ) } } }
  34. abstract class GitCommitShaTask : DefaultTask() { @get:OutputFile abstract val sha:

    RegularFileProperty @TaskAction fun taskAction() { val p = ProcessBuilder("git","rev-parse","--short","HEAD").start() p.waitFor() val gitCommitSha = p.inputStream.readString() // calculate whether git status is dirty val p2 = ProcessBuilder("git","status","--porcelain").start() p2.waitFor() val dirtyFilesOutput = p2.inputStream.readString() val outFile = sha().asFile outFile.writeText(gitCommitSha) if (dirtyFilesOutput.isNotEmpty()) { outFile.appendText(“-DIRTY") } } }
  35. apply plugin: 'com.android.library' apply plugin: 'org.jetbrains.kotlin.android' android { compileSdkVersion versions.compileSdk

    defaultConfig { minSdkVersion versions.minSdk } buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion versions.composeCompiler } compileOptions { sourceCompatibility versions.sourceCompatibility targetCompatibility versions.targetCompatibility } lintOptions { lintConfig file('lint.xml') textOutput 'stdout' textReport true // https://github.com/square/okhttp/issues/896 ignore 'InvalidPackage' } } dependencies {a compileOnly deps.androidxAnnotations implementation deps.androidxCore implementation deps.androidxCursorAdapter implementation deps.androidxFragment implementation deps.androidxStartup implementation deps.drawablePainter implementation deps.composeUi implementation deps.runtime implementation deps.foundation implementation deps.material }a
  36. apply plugin: 'com.android.library' apply plugin: 'org.jetbrains.kotlin.android' android { compileSdkVersion versions.compileSdk

    defaultConfig { minSdkVersion versions.minSdk } buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion versions.composeCompiler } compileOptions { sourceCompatibility versions.sourceCompatibility
  37. composeOptions { kotlinCompilerExtensionVersion versions.composeCompiler } compileOptions { sourceCompatibility versions.sourceCompatibility targetCompatibility

    versions.targetCompatibility } lintOptions { lintConfig file('lint.xml') textOutput 'stdout' textReport true // https://github.com/square/okhttp/issues/896 ignore 'InvalidPackage' } } dependencies {a compileOnly deps.androidxAnnotations implementation deps.androidxCore
  38. apply plugin: 'com.android.library' apply plugin: 'org.jetbrains.kotlin.android' android { compileSdkVersion versions.compileSdk

    defaultConfig { minSdkVersion versions.minSdk } buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion versions.composeCompiler } compileOptions { sourceCompatibility versions.sourceCompatibility
  39. apply plugin: 'org.example.conventions.android' dependencies {a compileOnly deps.androidxAnnotations implementation deps.androidxCore implementation

    deps.androidxCursorAdapter implementation deps.androidxFragment implementation deps.androidxStartup implementation deps.drawablePainter implementation deps.composeUi implementation deps.runtime implementation deps.foundation implementation deps.material }a
  40. object PluginId { object Convention { const val CASH_ANDROID_BASE_PLUGIN =

    “...” const val CASH_ANDROID_APPLICATION_PLUGIN = “...” const val CASH_ANDROID_LIBRARY_PLUGIN = “...” const val CASH_ANDROID_MODULES_PRESENTERS_PLUGIN = “...” const val CASH_ANDROID_MODULES_VIEWS_PLUGIN = “...” const val CASH_ANDROID_MODULES_SAMPLE_APPLICATION_PLUGIN = “...” } object External { const val KOTLIN_ANDROID_PLUGIN = "org.jetbrains.kotlin.android" const val AGP_ANDROID_APP_PLUGIN = "com.android.application" const val AGP_ANDROID_LIBRARY_PLUGIN = "com.android.library" const val PAPARAZZI_PLUGIN = "app.cash.paparazzi" const val JACOCO_PLUGIN = "jacoco" } }
  41. class AndroidApplicationPlugin : Plugin<Project> { companion object { const val

    ID = CASH_ANDROID_APPLICATION_PLUGIN } override fun apply(target: Project): Unit = target.run { pluginManager.apply(Convention.CASH_ANDROID_BASE_PLUGIN) pluginManager.apply(External.AGP_ANDROID_APPLICATION_PLUGIN) pluginManager.apply(External.KOTLIN_ANDROID_PLUGIN) // configure stuff here } }
  42. class AndroidViewsPlugin : Plugin<Project> { companion object { const val

    ID = CASH_ANDROID_MODULES_VIEWS_PLUGIN } override fun apply(target: Project): Unit = target.run { pluginManager.apply(CASH_ANDROID_LIBRARY_PLUGIN) pluginManager.apply(PAPARAZZI_PLUGIN) // configure stuff here } }
  43. // build.gradle buildscript { dependencies { classpath 'io.ratpack:ratpack-groovy:2.0.0-rc-1' classpath 'org.slf4j:slf4j-simple:1.7.36'

    } } ratpack.core.server.RatpackServer.start(server -> server.handlers(chain -> chain .get(ctx -> ctx.render('Hello World!')) .get(':name', ctx -> ctx.render("Hello ${ctx.getPathTokens().get(‘name')}!") ) ) )
  44. Case study: Dependency Analysis Gradle Plugin (DAGP) https://tinyurl.com/2ybm46s7 • Finds

    unused dependencies • Finds used transitive dependencies • Finds mis-declared dependencies • Works on Android and the JVM (Java, Kotlin, Groovy, Scala...)
  45. Avoid afterEvaluate Bad afterEvaluate { if (plugins.hasPlugin("com.android.library")) { /* do

    thing */ } } Good pluginManager.withPlugin("com.android.library") { /* do thing lazily */ }
  46. Dependency horror story: NoSuchMethodError?? com.google.protobuf:protobuf-java com.google.protobuf:protobuf-javalite $ jar tf ~/.gradle/caches/modules-2/files-2.1/

    com.google.protobuf/protobuf-javalite/ 3.12.0/8d8ca7983eae28ac654f657f6119e85a0bba9402 /protobuf-javalite-3.12.0.jar $ jar tf ~/.gradle/caches/modules-2/files-2.1/ com.google.protobuf/protobuf-java/ 3.10.0/410b61dd0088aab4caa05739558d43df248958c9 /protobuf-java-3.10.0.jar com/google/protobuf/WireFormat$1.class com/google/protobuf/ WireFormat$FieldType$1.class com/google/protobuf/ WireFormat$FieldType$2.class com/google/protobuf/ WireFormat$FieldType$3.class com/google/protobuf/ WireFormat$FieldType$4.class com/google/protobuf/WireFormat$FieldType.class com/google/protobuf/WireFormat$JavaType.class com/google/protobuf/ WireFormat$Utf8Validation$1.class com/google/protobuf/ WireFormat$Utf8Validation$2.class com/google/protobuf/WireFormat$1.class com/google/protobuf/ WireFormat$FieldType$1.class com/google/protobuf/ WireFormat$FieldType$2.class com/google/protobuf/ WireFormat$FieldType$3.class com/google/protobuf/ WireFormat$FieldType$4.class com/google/protobuf/WireFormat$FieldType.class com/google/protobuf/WireFormat$JavaType.class com/google/protobuf/ WireFormat$Utf8Validation$1.class com/google/protobuf/ WireFormat$Utf8Validation$2.class
  47. Shading • Pros and cons • Can bloat plugin size

    considerably • Shade selectively • Helps avoid diamond dependency problem
  48. plugins { ... id("com.github.johnrengelman.shadow") } val relocateShadowJar = tasks.register<ConfigureShadowRelocation>("relocateShadowJar") {

    notCompatibleWithConfigurationCache("Shadow plugin is incompatible") target = tasks.shadowJar.get() } tasks.shadowJar { dependsOn(relocateShadowJar) archiveClassifier.set("") relocate("org.antlr", "com.autonomousapps.internal.antlr") } val javaComponent = components["java"] as AdhocComponentWithVariants listOf("apiElements", "runtimeElements").forEach { unpublishable -> javaComponent.withVariantsFromConfiguration(configurations[unpublishable]) { skip() } } https://tinyurl.com/2bmt38h9
  49. Use compileOnly for optional dependencies Bad dependencies { implementation "com.android.tools.build:gradle:4.2.2"

    } Good dependencies { compileOnly "com.android.tools.build:gradle:4.2.2" }
  50. plugins { id 'java-gradle-plugin' id 'groovy' } testing { suites

    { functionalTest(JvmTestSuite) { useSpock() dependencies { implementation 'com.google.truth:truth:1.1.3' } } } } https://tinyurl.com/mzpe43p9
  51. Functional tests with Gradle TestKit (and Spock) • Reminder that

    there's a whole JVM ecosystem out there. • Groovy's not so bad once you've drank the kool-aid. • Enforces BDD-style testing, which is very readable. • Very easy parameterization and data-driven testing. https://tinyurl.com/4zhtsfkk
  52. class FunctionalSpec extends spock.lang.Specification { @TempDir Path tempDir def "can

    check best practices with 'checkBestPractices' task"() { given: def project = new SimplePluginProject(tempDir, 'reporting') when: buildAndFail(project.root, 'checkBestPractices') then: project.report.text.trim() == project.expectedReport.trim() } } https://bit.ly/3NbUO9l
  53. static BuildResult buildAndFail( Path projectDir, String... args ) { return

    GradleRunner.create() .withPluginClasspath() .forwardOutput() .withProjectDir(projectDir.toFile()) .withArguments(*args, '-s') // True when `--debug-jvm` is passed to Gradle .withDebug( ManagementFactory.getRuntimeMXBean() .inputArguments.toString().indexOf('-agentlib:jdwp') > 0 ) .buildAndFail() }
  54. class DataDrivenSpec extends spock.lang.Specification { def "myTask has expected output

    for AGP #agp"() { given: 'A test project' def project = MyTestProject.with(agp) when: 'We execute myTask' def result = build(project.projectDir, 'myTask') then: 'The build output contains the expected output' result.output.contains('some expected output') where: 'We test against several versions of AGP' agp << ['7.0', '7.1', '7.2'] } }
  55. static BuildResult build( Path projectDir, String... args ) { return

    GradleRunner.create() .withPluginClasspath() .forwardOutput() .withProjectDir(projectDir.toFile()) .withArguments(*args, '-s') .build() }
  56. •Don't inject the plugin classpath except for simple cases. •Instead,

    publish to local maven repo (but not "maven local"!). •Resolve your plugin from that repo like a normal binary plugin. •This leads to a much more "natural" scenario that is closer to what your users experience.
  57. plugins { id 'maven-publish' } publishing { publications { app(MavenPublication)

    { artifactId = 'sort-gradle-dependencies-app' from components['java'] } } repositories { maven { name = 'tests' url = layout.buildDirectory.dir('for-tests') } } } tasks.named('test') { dependsOn 'publishAppPublicationToTestsRepository' }
  58. https://tinyurl.com/534krym9 fun newAdapter(): AndroidGradlePlugin = when { agpVersion >= AgpVersion.version("7.4")

    -> { AndroidGradlePlugin7_4(project, versions) } agpVersion >= AgpVersion.version("7.3") -> { AndroidGradlePlugin7_3(project, versions) } agpVersion >= AgpVersion.version("7.2") -> { AndroidGradlePlugin7_2(project, versions) } else -> throw UnknownAgpException("No adapter for AGP $agpVersion") }
  59. class ResourceUsages7_2( project: Project, variant: VariantCreationConfig ) : AnalyzeResourceUsageOf<VariantCreationConfig>(project, variant)

    { private val variantSlug by lazy { variant.name.replaceFirstChar(Char::uppercase) } private val mergeTask = findAndroidTask<MergeResources>(“merge${variantSlug}Resources") private val packageTask = when { variant.variantType.isApk -> mergeTask else -> findAndroidTask("package${variantSlug}Resources") }
  60. class ResourceUsages7_2( project: Project, variant: VariantCreationConfig ) : AnalyzeResourceUsageOf<VariantCreationConfig>(project, variant)

    { private val variantSlug by lazy { variant.name.replaceFirstChar(Char::uppercase) } private val mergeTask = findAndroidTask<MergeResources>(“merge${variantSlug}Resources") private val packageTask = when { variant.variantType.isApk -> mergeTask else -> findAndroidTask("package${variantSlug}Resources") }
  61. class ResourceUsages7_3( project: Project, variant: VariantCreationConfig ) : AnalyzeResourceUsageOf<VariantCreationConfig>(project, variant)

    { private val variantSlug by lazy { variant.name.replaceFirstChar(Char::uppercase) } private val mergeTask = findAndroidTask<MergeResources>(“merge${variantSlug}Resources") private val packageTask = when { variant.componentType.isApk -> mergeTask else -> findAndroidTask("package${variantSlug}Resources") }
  62. • "Compute tasks" perform heavy computations and should be cacheable.

    • "Print tasks" are part of the "UI" of your plugin. They print to console.
  63. @CacheableTask abstractaclassaComputeTaska:aDefaultTask() { ... } abstract class PrintTask : DefaultTask()

    { @get:PathSensitive(PathSensitivity.RELATIVE) @get:InputFile abstractavalainput:aRegularFileProperty @TaskActionafunaaction()a{a valareporta=ainput.get() .asFile .fromJson<MyReport>() logger.quiet(report) a}a }a
  64. abstract class PrintTask : DefaultTask() { ... } class MyPlugin

    : Plugin<Project> { override fun apply(target: Project): Unit = target.run { val computeTask = tasks.register<ComputeTask>("computeTask") { ... } val printTask = tasks.register<PrintTask>("printTask") { input.set(computeTask.flatMap { it.output }) } } } @CacheableTask abstractaclassaComputeTaska:aDefaultTask() { ... }
  65. Configuration cache • Make your build compatible and start using

    it ASAP. • Square estimates we save ~5400 hours annually. • Easily in excess ~$1 million in recovered lost developer productivity. • ...blog post forthcoming! https://tinyurl.com/2p4k3rup • Technically "unsafe" but works very well. • Will be GA by end of this year, before Gradle 8.0. • Necessary pre-step for project isolation.
  66. class ConfigurationCacheSpec extends Specification { def "configuration cache works"() {

    given: def project = new SimplePluginProject() when: 'We build it the first time' def result = build(project, 'myTask', '--configuration-cache') then: 'Configuration cache entry stored' result.output().contains( "0 problems were found storing the configuration cache.") result.output().contains("Configuration cache entry stored.") } }
  67. class ConfigurationCacheSpec extends Specification { def "configuration cache works"() {

    given: def project = new SimplePluginProject() when: 'We build it the first time' def result = build(project, 'myTask', '--configuration-cache') then: 'Configuration cache entry stored' result.output().contains( "0 problems were found storing the configuration cache.") result.output().contains("Configuration cache entry stored.") when: 'We build it again' def result = build(project, 'myTask', '--configuration-cache') then: 'Configuration cache entry reused' result.output().contains("Reusing configuration cache.") } }
  68. Project isolation • Will be a major focus of Gradle

    in 2023. • Enables parallel configuration phase! • Enables incremental configuration! • Enables parallel and incremental IDE sync! • Early experiments at Square showed a 10x improvement in sync performance.
  69. incremental_build_abi_change_in_lib { tasks = [":lib:assembleDebug"] apply-abi-change-to = "lib/src/main/java/com/lib/CreateBody.java" apply-abi-change-to =

    "lib/src/main/java/com/lib/CreateResponse.java" apply-abi-change-to = "lib/src/main/java/com/lib/StandardResponse.kt" show-build-cache-size = true warm-ups = 4 iterations = 7 gradle-args = ["--offline", "--no-build-cache"] } Gradle Profiler https://github.com/gradle/gradle-profiler
  70. PSI/Lint • Lint checks are still here and more performant

    • Can be added to your CI pipeline! • Can use Quick Fixes to -- say -- update from tasks.create to tasks.register!
  71. Gradle Best Practices Gradle Plugin https://tinyurl.com/2mjc3r6h • Built for plugin

    authors. • Protects users' builds from anti-patterns. • Enforces best practices so that you don't have to remember everything yourself. • Bytecode analysis --> works for any JVM language. • Some graph theory, for fun. • Supports a baseline so that you can incrementally fix legacy plugins. • Currently looks for the following issues: • Usage of allprojects or subprojects (always bad) • Usage of Project instances in the context of a @TaskAction. • Can be added to your CI pipeline TODAY!