-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.html
914 lines (893 loc) · 39.5 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>reveal.js</title>
<link rel="stylesheet" href="dist/reset.css">
<link rel="stylesheet" href="dist/reveal.css">
<link rel="stylesheet" href="dist/theme/black.css">
<!-- Theme used for syntax highlighted code -->
<link rel="stylesheet" href="plugin/highlight/monokai.css">
<style>
.grid-area {
display: grid;
grid-auto-flow: column;
}
.reveal pre code {
max-height: none;
}
.reveal pre {
width: 100%;
}
.inline-image img {
margin: 0;
}
.inline-image {
display: inline-flex;
}
</style>
</head>
<body>
<div class="reveal">
<div class="slides">
<section data-timing="20">
<h1>Gradle Plugins</h1>
<img alt="necronomicon but with the gradle logo" src="gradlenomicon.jpg" height="466" width="386">
<aside class="notes">
Welcome to developing gradle plugins
Please, do not be scared off by this book. I have read this book so you don't have to.
I can tell you what you need to know and leave out the unspeakable horrors contained within.
</aside>
</section>
<section data-timing="60">
<h1>About Me</h1>
<aside class="notes">
You can also find me on the Chicago Tech and Kotlin slacks and the CJUG discord
</aside>
<h3>John Burns</h3>
<div class="grid-area">
<div style="text-align: left">
<p>Staff Engineer @ GrubHub</p>
<p>CKUG Co-Organizer</p>
<p class="inline-image"><img alt="twitter logo" src="Twitter-Logo.png" height="48" width="48"/>
<span style="padding-left: .5em">@wakingrufus</span></p>
<p class="inline-image"><img alt="fediverse logo" src="fediverse.png" height="48" width="48"/>
<span style="padding-left: .5em">@wakingrufus@mastodon.technology</span>
</p>
<p class="inline-image"><img alt="github logo" src="github-logo.png" height="48" width="48"/>
<span style="padding-left: .5em">wakingrufus</span>
</p>
</div>
</div>
</section>
<section data-timing="60">
<img alt="GrubHub logo" src="gh-logo.png" height="194" width="688">
<aside class="notes">
HQ is chicago
we have been publicly traded and operating off our own revenue and profits since 2014
What I do: Service Platform Engineering: JVM Stewardship
</aside>
<ul>
<li>Fully Remote and Hybrid Remote Roles</li>
<li>Unlimited PTO</li>
<li>8-16 weeks of parental leave</li>
<li>4.5 day work week</li>
</ul>
</section>
<section data-timing="90">
<h1>About You</h1>
<h4 class="fragment">Gradle Users</h4>
<h4 class="fragment">Plugin Authors</h4>
<h4 class="fragment">Terrified of Gradle</h4>
<aside class="notes">
how many use gradle in hobby or work projects?
how many are plugin authors, or even written a custom task
how many when you hear the word gradle a feeling of dread washes over you
my goal is to encourage more people to fall into that second category.
I might not fix the dread though.
</aside>
</section>
<section data-timing="90">
<h1>The Problem</h1>
<aside class="notes">
Every team has just one person who actually understand the buildscripts. if you are lucky, 2.
usually it starts when something breaks,
and 1 person figures out how to fix it, and in doing so they learn about how it works.
then next time something breaks, the person it breaks for gets frustrated b/c they just want their
code to run dangit! not understanding the arcane workings of a build tool.
so after trying and failing a few times, they just go to the person who fixed it before, and they
figure it out
and this happens again and again,
until the fixer is spending most of their time just fixing people problems instead of doing their
real job
but then before they realize it buildscripts ARE their real job
and all of a sudden they are on stage talking to a bunch of people about gradle plugins!
</aside>
</section>
<section data-timing="90">
<h1>The Goal</h1>
<ul>
<li class="fragment">Abstract away the complexity</li>
<li class="fragment">Simplify standard usage</li>
<li class="fragment">Without preventing customization</li>
<li class="fragment">Testable</li>
</ul>
<aside class="notes">
Everyone should understand WHAT the scripts do, but they shouldn't always need to know HOW.
Make the normal case turn-key, allow for customization.
Testability: document what it does, how it should be used, never out of date or it breaks.
I will show you how to achieve this using gradle plugins.
but first, lets get some background about our build tool
</aside>
</section>
<section>
<section data-auto-animate data-timing="90">
<h1>Maven vs Gradle</h1>
<div class="grid-area">
<div class="fragment">
<h3>Maven</h3>
<ul>
<li>Declarative via XML</li>
<li>Extended with Plugins</li>
</ul>
</div>
<div class="fragment">
<h3>Gradle</h3>
<ul>
<li>DSL</li>
<li>Extended with Plugins</li>
</ul>
</div>
</div>
<aside class="notes">
Maven is declarative. Gradle has a DSL.
with either tool, we can use plugins to abstract away complexity and follow DRY principles
The gradle DSL lets us break out of DSL and into imperative code: a blessing and a curse
</aside>
</section>
<section data-auto-animate data-timing="30">
<h1>DSL</h1>
<aside class="notes">
Here, the DSL looks very declarative great right?
</aside>
<pre class="groovy"><code data-trim data-noescape>
plugins {
id "java"
id "jacoco"
}
repositories {
mavenCentral()
}
dependencies {
implementations("commons-io:commons-io:2.7")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.4.+")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.4.+")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.4.+")
testImplementation("org.assertj:assertj-core:3.23.1")
}
test {
useJUnitPlatform()
}
</code></pre>
</section>
<section data-auto-animate>
<h1>DSL</h1>
<aside class="notes">
But say we need something a little custom eg the dependencyUpdates plugin.
This is not declarative. There is a lot of code here. complexity. regex!
Once figured out, everyone else copies this magical incantation,
and if you ever want to change it, you have 1000 places you need to change it.
</aside>
<pre class="groovy"><code data-trim data-noescape>
def isNonStable = { String version ->
def stableKeyword = ['RELEASE', 'FINAL', 'GA']
.any { it -> version.toUpperCase().contains(it) }
def regex = /^[0-9,.v-]+(-r)?$/
return !stableKeyword && !(version ==~ regex)
}
def excludeList = ["guice", "guava"]
tasks.named("dependencyUpdates").configure {
resolutionStrategy {
componentSelection {
all {
if (isNonStable(it.candidate.version)) {
reject('Release candidate')
} else if (excludeList.contains(it.candidate.module)){
reject('dependency excluded from upgrades')
}
}
}
}
}
</code></pre>
</section>
<section data-auto-animate data-timing="60">
<h1>DSL</h1>
<aside class="notes">
The ideal is a one liner that lets us say "apply my org's standard depupdates plugin"
Consider: imperative code, code repeated between projects
when sufficiently pluginified, then scripts are declarative
Before we talk about how to make a plugin, we first need to understand how gradle works
</aside>
<pre class="groovy"><code data-trim data-noescape data-line-numbers="4">
plugins {
id "java"
id "jacoco"
id "com.myorg.gradle.depupdates"
}
</code></pre>
</section>
</section>
<section>
<section data-timing="120">
<h1>Gradle lifecycle</h1>
<aside class="notes">
Gradle builds run in 3 phases
buildSrc is your incubator. it is a great place to try writing custom tasks and plugins before
pulling them out to a separate codebase / artifact
spring blog: https://spring.io/blog/2020/06/08/migrating-spring-boot-s-build-to-gradle
</aside>
<div class="grid-area">
<div class="fragment">
<h4>Initialization</h4>
<ul>
<li>buildSrc</li>
<li>init scripts</li>
<li>settings.gradle</li>
</ul>
</div>
<div class="fragment">
<h4>Configuration</h4>
<ul>
<li>Plugins</li>
<li>Build scripts</li>
</ul>
</div>
<div class="fragment">
<h4>Execution</h4>
<ul>
<li>Tasks</li>
</ul>
</div>
</div>
</section>
</section>
<section>
<section data-timing="120">
<h1>Groovy or Kotlin?</h1>
<aside class="notes">
Basic scripts: can be cross language source compatible
complex scripts:
More online examples are in groovy
groovy is more succinct so can be easier to read
plugins:
easier to write with type safety
caveat: When targeting many major versions of gradle, you may want to avoid kotlin to avoid kotlin language
incompatibilities
</aside>
<p class="fragment">Basic scripts: doesn't matter</p>
<p class="fragment">Complex scripts: Groovy</p>
<p class="fragment">Plugins: Kotlin</p>
</section>
<section data-timing="30">
<h1>Groovy DSL</h1>
<img src="groovy-dsl.png">
</section>
<section data-timing="30">
<h1>Kotlin DSL</h1>
<img src="kotlin-dsl.png">
</section>
</section>
<section>
<section data-auto-animate data-timing="30">
<h1>Project Setup</h1>
<aside class="notes">
Whether in buildSrc or a separate project, the way to set up a plugin project is the same.
kotlin jvm plugin
</aside>
<pre class="kotlin fragment" data-auto-animate-id="sample"><code data-trim data-noescape data-line-numbers="2">
plugins {
kotlin("jvm")
`kotlin-dsl`
`java-gradle-plugin`
}
</code></pre>
</section>
<section data-auto-animate data-timing="15">
<h1>Project Setup</h1>
<aside class="notes">
The kotlin-dsl plugin gives us access to the gradle kotlin DSL,
which makes interacting with gradle project via kotlin more ideomatic
</aside>
<pre class="kotlin" data-auto-animate-id="sample"><code data-trim data-noescape data-line-numbers="3">
plugins {
kotlin("jvm")
`kotlin-dsl`
`java-gradle-plugin`
}
</code></pre>
</section>
<section data-auto-animate data-timing="15">
<h1>Project Setup</h1>
<aside class="notes">
The java-gradle-plugin adds gradle api and test kit dependencies and allows us to package a gradle plugin
</aside>
<pre class="kotlin" data-auto-animate-id="sample"><code data-trim data-noescape data-line-numbers="4">
plugins {
kotlin("jvm")
`kotlin-dsl`
`java-gradle-plugin`
}
</code></pre>
</section>
<section data-auto-animate data-timing="30">
<h1>Project Setup</h1>
<aside class="notes">
Declare the id which will be used to apply your plugin to a project
</aside>
<pre class="kotlin" data-auto-animate-id="sample"><code data-trim data-noescape data-line-numbers="9">
plugins {
kotlin("jvm")
`kotlin-dsl`
`java-gradle-plugin`
}
gradlePlugin {
plugins {
create("myPlugin") {
id = "com.myorg.myplugin"
implementationClass = "com.myorg.myplugin.MyPlugin"
}
}
}
</code></pre>
</section>
<section data-auto-animate data-timing="30">
<h1>Project Setup</h1>
<aside class="notes">
Reference your Plugin class
</aside>
<pre class="kotlin" data-auto-animate-id="sample"><code data-trim data-noescape data-line-numbers="10">
plugins {
kotlin("jvm")
`kotlin-dsl`
`java-gradle-plugin`
}
gradlePlugin {
plugins {
create("myPlugin") {
id = "com.myorg.myplugin"
implementationClass = "com.myorg.myplugin.MyPlugin"
}
}
}
</code></pre>
</section>
</section>
<section>
<section data-auto-animate data-timing="45">
<h1>Building Blocks</h1>
<h3 class="fragment">Tasks</h3>
<h3 class="fragment">Extensions</h3>
<h3 class="fragment">Plugins</h3>
</section>
</section>
<section>
<section data-auto-animate data-timing="30">
<h1>Tasks</h1>
<h3>Workhorse of the Execution phase</h3>
<aside class="notes">
TaskGraph
</aside>
</section>
<section data-auto-animate data-timing="10">
<h1>Tasks</h1>
<h3>Inputs -> TaskAction -> Outputs</h3>
</section>
<section data-auto-animate data-timing="90">
<h1 data-id="title">Tasks</h1>
<aside class="notes">
The TaskAction is what the task does when it runs.
To define inputs and outputs, we use annotated properties
Property API
unopinionated
</aside>
<pre data-id="code" class="kotlin"><code data-trim data-noescape>
abstract class MyTask : DefaultTask() {
@get:OutputFile
abstract val outputFile: Property<File>
@get:InputFile
abstract val inputFile: Property<File>
@TaskAction
fun doWork(){
outputFile.get().writeText(doThing(inputFile.get()))
}
}
</code></pre>
</section>
<section data-auto-animate data-timing="60">
<h1 data-id="title">Tasks</h1>
<h3 data-id="subtitle">Companion Object</h3>
<aside class="notes">
for tasks that usually are only created once per project,
put a companion object in the Task class (kotlin) with a JvmStatic method that takes a project,
and registers the task, then applies all the default opinions,
just to make the plugin code simpler for the default case.
and then you can always just register the task the regular way if you want one with no opinions
you could do a similar thing with a static method in groovy/java
that kind of spiritually aligns with the "use static methods instead of public constructors" advice
from effective Java
without sacrificing the idiom of not hard-coding opinions on tasks/extensions
set properties and defaults
</aside>
<pre data-id="code" class="kotlin fragment"><code data-trim data-noescape data-line-numbers>
abstract class MyTask : DefaultTask() {
companion object {
@JvmStatic
fun create(project: Project,
taskName: String = "myTask"): MyTask {
return project.tasks.create<MyTask>(taskName).apply {
// TODO
}
}
}
}
</code></pre>
</section>
<section data-auto-animate data-timing="10">
<h1 data-id="title">Tasks</h1>
<h3 data-id="subtitle">Companion Object</h3>
<aside class="notes">
set properties and defaults
</aside>
<pre data-id="code" class="kotlin"><code data-trim data-noescape data-line-numbers="7-10">
abstract class MyTask : DefaultTask() {
companion object {
@JvmStatic
fun create(project: Project,
taskName: String = "myTask"): MyTask {
return project.tasks.create<MyTask>(taskName).apply {
outputFile.set(input.map {
project.buildDir.resolve("$it.txt")
})
input.convention("name")
}
}
}
}
</code></pre>
</section>
<section data-auto-animate data-timing="10">
<h1 data-id="title">Tasks</h1>
<h3 data-id="subtitle">Companion Object</h3>
<aside class="notes">
control up-to-date / caching
</aside>
<pre data-id="code" class="kotlin"><code data-trim data-noescape data-line-numbers="7">
abstract class MyTask : DefaultTask() {
companion object {
@JvmStatic
fun create(project: Project,
taskName: String = "myTask"): MyTask {
return project.tasks.create<MyTask>(taskName).apply {
outputs.upToDateWhen { false }
}
}
}
}
</code></pre>
</section>
<section data-auto-animate data-timing="15">
<h1 data-id="title">Tasks</h1>
<h3 data-id="subtitle">Companion Object</h3>
<aside class="notes">
bind properties to extension properties, best way to pass config from ext to tasks
</aside>
<pre data-id="code" class="kotlin"><code data-trim data-noescape data-line-numbers="6-9">
abstract class MyTask : DefaultTask() {
companion object {
@JvmStatic
fun create(project: Project,
taskName: String = "myTask"): MyTask {
val ext = project.extensions.findByType<MyExtension>()
return project.tasks.create<MyTask>(taskName).apply {
input.set(ext.setting)
}
}
}
}
</code></pre>
</section>
</section>
<section>
<section data-auto-animate>
<h1 data-id="title">Extensions</h1>
<h3>Exposing configuration to buildscripts</h3>
</section>
<section data-auto-animate>
<h1 data-id="title">Extensions</h1>
<pre class="kotlin"><code data-trim data-noescape>
open class MyExtension(objects: ObjectFactory) {
companion object {
@JvmStatic
fun create(project: Project): MyExtension {
return project.extensions.create<MyExtension>("my")
.apply { name.convention(project.rootProject.name) }
}
}
val name: Property<String> = objects.property(String::class.java)
fun name(newName: String) {
name.set(newName)
}
}
</code></pre>
<aside class="notes">
needs some boilerplate to create properties
use companion object to apply opinions
create functions to set values on your property: ideomatic gradle
</aside>
</section>
<section data-auto-animate>
<h1 data-id="title">Extensions</h1>
<h3>Usage</h3>
<pre class="groovy fragment"><code data-trim data-noescape>
my {
name("customName")
}
</code></pre>
<pre class="kotlin fragment"><code data-trim data-noescape>
extensions.findByType<MyExtension>() {
name("customName")
}
</code></pre>
</section>
</section>
<section>
<section data-auto-animate data-timing="15">
<h1>Plugins</h1>
<h3>Workhorse of the Configuration Phase</h3>
</section>
<section data-auto-animate data-timing="15">
<h1>Plugins</h1>
<pre class="kotlin"><code data-trim data-noescape>
class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
}
}
</code></pre>
</section>
<section data-auto-animate data-timing="30">
<h1>Plugins</h1>
<h3>Apply Tasks & Extensions</h3>
<pre class="kotlin"><code data-trim data-noescape>
class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
val ext = MyExtension.create(project)
val task = MyTask.create(project)
}
}
</code></pre>
</section>
<section data-auto-animate>
<h1>Plugins</h1>
<h3>Apply Other Plugins</h3>
<pre class="kotlin"><code data-trim data-noescape>
class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.pluginManager.apply(JavaPlugin::class.java)
}
}
</code></pre>
</section>
<section data-auto-animate>
<h1>Plugins</h1>
<h3>React to Other Plugins</h3>
<pre class="kotlin"><code data-trim data-noescape>
class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.pluginManager.withType(JavaPlugin::class) {
project.repositories {
// configure custom repo
}
project.withConvention(JavaPluginConvention::class) {
sourceSets.create("customSourceSet"){
// configure custom source set
}
}
}
}
}
</code></pre>
</section>
</section>
<section data-timing="30">
<section data-auto-animate>
<h1>Best Practices</h1>
</section>
<section data-auto-animate data-timing="60">
<h2>Avoid afterEvaluate</h2>
<aside class="notes">
afterEvaluate will run your code after the config phase
creates race conditions
used to be necessary evil
Property has MOSTLY eliminated the need.
</aside>
</section>
<section data-auto-animate data-timing="30">
<h2>Avoid afterEvaluate</h2>
<h3>Don't</h3>
<aside class="notes">
extension values set in scripts not set until after configuration phase
</aside>
<pre class="kotlin"><code data-trim data-noescape>
class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
val ext = MyExtension.create(project)
val task = MyTask.create(project)
project.afterEvaluate {
task.inputFile = ext.inputFile
}
}
}
</code></pre>
</section>
<section data-auto-animate data-timing="30">
<h2>Avoid afterEvaluate</h2>
<h3>Instead Do</h3>
<aside class="notes">
use properties to bind the value
</aside>
<pre class="kotlin"><code data-trim data-noescape>
class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
val ext = MyExtension.create(project)
val task = MyTask.create(project)
task.inputFile.set(ext.inputFile) // bind properties
}
}
</code></pre>
</section>
<section data-auto-animate data-timing="60">
<h2>Project Properties</h2>
<aside class="notes">
we already talked about using extension props to get buildscript config to your tasks
but plugin code runs before buildscripts are evaluated so cant use extensions
TODO: add a visualization of this problem before this slide
</aside>
<ul>
<li class="fragment">Use extension properties when used in task logic</li>
<li class="fragment">Do NOT read extension properties in plugin code</li>
<li class="fragment">Use project properties when used in plugin logic</li>
</ul>
</section>
<section data-auto-animate data-timing="30">
<h2>Project Properties</h2>
<div class="">
<h3>gradle.properties</h3>
<pre class="properties"><code data-trim data-noescape>
my.enabled=false
</code></pre>
</div>
<div class="fragment">
<h3>Command Line</h3>
<pre class="bash"><code data-trim data-noescape>
./gradlew -Pmy.enabled=false
</code></pre>
</div>
</section>
<section data-timing="30">
<h1>Implementation</h1>
<pre class="kotlin"><code data-trim data-noescape>
class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
if(!project.hasProperty("my.enabled")
|| project.property("my.enabled")){
// plugin code
}
}
}
</code></pre>
<aside class="notes">
always check hasProperty. if it doesnt exist it will throw exception, not return null
</aside>
</section>
<section>
<h2>More Best Practices</h2>
<p><a href="https://github.com/liutikas/gradle-best-practices">https://github.com/liutikas/gradle-best-practices</a> </p>
</section>
</section>
<section>
<section data-timing="10" data-auto-animate><h1>Testing</h1>
<aside class="notes">
Now that we have context on how to build plugins, we can talk about how to test (one of our goals)
</aside>
</section>
<section data-timing="15" data-auto-animate>
<h1 data-id="title">Testing</h1>
<h3 data-id="subtitle">Unit Tests</h3>
<aside class="notes">
unit test helper functions same as always
</aside>
</section>
<section data-timing="90" data-auto-animate>
<h1 data-id="title">Testing</h1>
<h3 data-id="subtitle">Component Tests</h3>
<ul>
<li class="fragment">Programmatic access to Project model</li>
<li class="fragment">Good for checking configuration</li>
<li class="fragment">If you run tasks, invoke them directly</li>
<li class="fragment">Avoid causing dependencies to resolve</li>
</ul>
<aside class="notes">
certain gradle object like tasks/plugins only work in the context of a project
</aside>
</section>
<section data-timing="60" data-auto-animate>
<h1 data-id="title">Testing</h1>
<h3 data-id="subtitle">Component Tests</h3>
<pre class="kotlin"><code data-trim data-noescape>
val rootProject = ProjectBuilder.builder().build() as ProjectInternal
val subProject = ProjectBuilder.builder()
.withParent(rootProject)
.build() as ProjectInternal
subProject.plugins.apply(JavaPlugin::class.java)
subProject.plugins.apply(MyPlugin::class.java)
subProject.evaluate()
rootProject.evaluate()
assertThat(subProject.tasks.findByName("myTask")).isNotNull()
subProject.tasks.findByName("myTask").doWork()
</code></pre>
</section>
<section data-auto-animate data-timing="60" >
<h1 data-id="title">Testing</h1>
<h3 data-id="subtitle">Integration Tests</h3>
<ul data-id="body">
<li class="fragment">Use Gradle TestKit</li>
<li class="fragment">Run a whole gradle build</li>
<li class="fragment">works on gradle scripts you write to a temp dir</li>
<li class="fragment">Pass in gradle version to use</li>
<li class="fragment">parameterize to test multiple gradle verisons</li>
</ul>
<aside class="notes">
TestKit applied automatically by java-gradle-plugin
</aside>
</section>
<section data-auto-animate data-timing="90">
<h1 data-id="title">Testing</h1>
<h3 data-id="subtitle">Integration Tests</h3>
<pre data-id="body" class="kotlin"><code data-trim data-noescape>
rootProjectDir.resolve("build.gradle").apply {
createNewFile()
writeText("""plugins {
id("java")
id("com.myorg.myplugin")
}""".trimMargin()
)
}
val buildResult = GradleRunner.create()
.withProjectDir(rootProjectDir)
.withPluginClasspath()
.withArguments("build", "--stacktrace")
.withGradleVersion("7.5")
.forwardOutput().build()
assertThat(buildResult.task(":myTask")?.outcome).isEqualTo(SUCCESS)
</code></pre>
<aside class="notes">
im leaving out unit test framework wiring
no access to Project API
we have BuildResult which has build output and TaskResult objects
</aside>
</section>
</section>
<section>
<section data-auto-animate data-timing="120">
<h2>Initialization Phase</h2>
<ul>
<li class="fragment">init scripts</li>
<li class="fragment">Settings Plugin</li>
<li class="fragment">Custom Wrapper Distribution</li>
</ul>
<aside class="notes">
we have discussed using plugins, extensions, tasks to commonize your config and exec phases
but what about the init phase, such as settings.gradle?
init scripts have to be copied to each user's home dir, applied to every project
settings plugins must be publicly accessible
custom distro can be behind basic auth
</aside>
</section>
<section data-auto-animate data-timing="30">
<h2>Initialization Phase</h2>
<h3>Gradle Wrapper</h3>
<aside class="notes">
Install gradle locally to your project to isolate your project from dev env
</aside>
<pre data-id="body" class="fragment kotlin"><code data-trim data-noescape>
wrapper {
gradleVersion = "7.5.1"
}
</code></pre>
<pre data-id="body" class="fragment bash"><code data-trim data-noescape>gradle wrapper</code></pre>
</section>
<section data-auto-animate data-timing="30">
<h2>Initialization Phase</h2>
<h3>Gradle Wrapper</h3>
<aside class="notes">
Install gradle locally to your project to isolate your project from dev env
</aside>
<pre data-id="body" class="kotlin"><code data-trim data-noescape>
wrapper {
gradleVersion = "7.5.1"
}
</code></pre>
<pre data-id="body" class="bash"><code data-trim data-noescape>./gradlew build</code></pre>
</section>
<section data-auto-animate data-timing="60">
<h2>Initialization Phase</h2>
<h3>Custom Gradle Wrapper</h3>
<aside class="notes">
Install gradle locally to your project to isolate your project from dev env
</aside>
<span>Base Gradle Wrapper</span>
<span>+ init scripts</span>
<pre data-id="body" class="kotlin"><code data-trim data-noescape>
wrapper {
distributionUrl(
"https://myorg.com/wrapper/gradle-7.5.1-mywrapper-2.1.0.zip"
)
}
</code></pre>
</section>
<section data-auto-animate data-timing="30">
<h2>Initialization Phase</h2>
<h3>Custom Gradle Wrapper</h3>
<aside class="notes">
Use this plugin to make generating these easy
</aside>
<span>tech.harmonysoft.oss.custom-gradle-dist-plugin</span>
</section>
<section data-auto-animate data-timing="90">
<h2>Initialization Phase</h2>
<ul>
<li class="fragment">pluginManagement (repo/plugins)</li>
<li class="fragment">Gradle Remote Build Cache</li>
</ul>
</section>
</section>
<section data-timing="10">
<aside class="notes">
Now you too can be the weirdo muttering to themselves in the corner
</aside>
<img alt="Red Swingline stapler" src="stapler.png" height="500" width="500"/>
</section>
<section data-timing="10">
<img alt="necronomicon but with the gradle logo" src="gradlenomicon.jpg" height="230" width="190">
<div class="grid-area">
<div style="text-align: left">
<div>wakingrufus.github.io/developing-gradle-plugins/</div>
<p class="inline-image"><img alt="twitter logo" src="Twitter-Logo.png" height="48" width="48"/>
<span style="padding-left: .5em">@wakingrufus</span></p>
<p class="inline-image"><img alt="fediverse logo" src="fediverse.png" height="48" width="48"/>
<span style="padding-left: .5em">@wakingrufus@mastodon.technology</span>
</p>
</div>
</div>
</section>
</div>
</div>
<script src="dist/reveal.js"></script>
<script src="plugin/notes/notes.js"></script>
<script src="plugin/markdown/markdown.js"></script>
<script src="plugin/highlight/highlight.js"></script>
<script>
// More info about initialization & config:
// - https://revealjs.com/initialization/
// - https://revealjs.com/config/
Reveal.initialize({
hash: true,
defaultTiming: 60,
// Learn about plugins: https://revealjs.com/plugins/
plugins: [RevealMarkdown, RevealHighlight, RevealNotes]
});
</script>
</body>
</html>