Share
Embed
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
Transcript
137 slides
-
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' -
JavaCompile task A.class, B.class A.java, B.java JDK 11 4 GB
UP-TO-DATE x 2 -
JavaCompile task A.class A.java JDK 11 4 GB CACHED echo
“org.gradle.caching=true” >> gradle.properties -
test { testLogging { showStandardStreams = true exceptionFormat = 'full'
} } Eager configuration -
tasks . withType(Test ) { testLogging { showStandardStreams = true
exceptionFormat = 'full' } } Eager configuration -
tasks . withType(Test ) . configureEach { testLogging { showStandardStreams
= true exceptionFormat = 'full' } } -
task helloa{ doLasta{ printlna"Hello!" }a }b tasks.create("hello") { doLast {
println "Hello!" } } Task immediately created -
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; ... } -
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; ... } -
def taskProvider = tasks.register("hello") { doLast { println "Hello!" }
} def task = taskProvider.get() -
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 -
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 -
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 -
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() } } -
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")) } -
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) } -
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")) } -
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! -
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())) } -
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) }) } -
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")) } -
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) } } -
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) ... } } -
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) ... } } -
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 -
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()) } } } -
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 } } } } -
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 -
$./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 -
$./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 -
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 -
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 -
• 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 -
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 -
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> } -
https://tinyurl.com/yfv2rja7 • The only safe way to share in-memory data.
• Never ever ever use static variables. -
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( -
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( -
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( -
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." ) } ) } } } -
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") } } } -
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 -
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 -
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 -
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 -
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 -
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" } } -
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 } } -
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 } } -
// build.gradle plugins { id "com.squareup.android.app" } <convention> { ...
} dependencies { ... } -
// 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')}!") ) ) ) -
// build.gradle plugins { id "com.squareup.android.app" } <convention> { ...
} dependencies { ... } -
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...) -
Avoid afterEvaluate Bad afterEvaluate { if (plugins.hasPlugin("com.android.library")) { /* do
thing */ } } Good pluginManager.withPlugin("com.android.library") { /* do thing lazily */ } -
VisitableURLClassLoader(groovy-script-/Users/trobalik/my-project/common/development-app/demo-tictactoe/build.gradle-loader) VisitableURLClassLoader(ClassLoaderScopeIdentifier.Id{coreAndPlugins:settings[:]:buildSrc[:]:root-project[:]:project-common:project-development-app:project- demo-tictactoe(export)}) VisitableURLClassLoader(ClassLoaderScopeIdentifier.Id{coreAndPlugins:settings[:]:buildSrc[:]:root-project[:](export)}) VisitableURLClassLoader(ClassLoaderScopeIdentifier.Id{coreAndPlugins:settings[:](export)}) CachingClassLoader(FilteringClassLoader(VisitableURLClassLoader(legacy-mixin-loader))) FilteringClassLoader(VisitableURLClassLoader(legacy-mixin-loader)) VisitableURLClassLoader(legacy-mixin-loader) VisitableURLClassLoader(ant-and-gradle-loader) VisitableURLClassLoader(ant-loader)
jdk.internal.loader.ClassLoaders$PlatformClassLoader@1331a37b https://tinyurl.com/mvup8etk // build.gradle class A {} def printClassLoaders(classLoader) { while (classLoader != null) { println(classLoader) classLoader = classLoader.parent } } printClassLoaders(A.classLoader) -
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 -
Shading • Pros and cons • Can bloat plugin size
considerably • Shade selectively • Helps avoid diamond dependency problem -
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 -
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" } -
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 -
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 -
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 -
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() } -
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'] } } -
static BuildResult build( Path projectDir, String... args ) { return
GradleRunner.create() .withPluginClasspath() .forwardOutput() .withProjectDir(projectDir.toFile()) .withArguments(*args, '-s') .build() } -
•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. -
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' } -
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") } -
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") } -
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") } -
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") } -
• "Compute tasks" perform heavy computations and should be cacheable.
• "Print tasks" are part of the "UI" of your plugin. They print to console. -
@CacheableTask abstractaclassaComputeTaska:aDefaultTask() { a@get:PathSensitive(PathSensitivity.RELATIVE)a a@get:InputFilea abstractavalainput:aRegularFileProperty a@get:OutputFilea abstractavalaoutput:aRegularFileProperty a@TaskAction fun
action()a{a valaoutput = output.get().asFile output.delete()a valaresult = /* heavy computation */ output.writeText(result.toJson()) a}a }a -
@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 -
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() { ... } -
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. -
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.") } } -
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.") } } -
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. -
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 -
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! -
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!