<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Untitled Publication]]></title><description><![CDATA[Untitled Publication]]></description><link>https://blog.mbonnin.net</link><generator>RSS for Node</generator><lastBuildDate>Sun, 12 Apr 2026 13:43:41 GMT</lastBuildDate><atom:link href="https://blog.mbonnin.net/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Gradle brainteasers 2/2: relocatable input files]]></title><description><![CDATA[This is a follow up to this other post about having fun with the Gradle APIs.
In this post, I'm talking about how I spent a shameful amount of time understanding how Gradle handles input files.
The problem
The Apollo Gradle Plugin is generating Kotli...]]></description><link>https://blog.mbonnin.net/gradle-brainteasers-22-relocatable-input-files</link><guid isPermaLink="true">https://blog.mbonnin.net/gradle-brainteasers-22-relocatable-input-files</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[gradle]]></category><category><![CDATA[Build tool]]></category><dc:creator><![CDATA[Martin Bonnin]]></dc:creator><pubDate>Wed, 17 Jul 2024 08:41:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1721037884401/ab1225bf-d512-4938-bb11-c38413464a0c.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is a follow up to <a target="_blank" href="https://blog.mbonnin.net/gradle-brainteasers-12-aggregating-artifacts">this other post</a> about having fun with the Gradle APIs.</p>
<p>In this post, I'm talking about how I spent a shameful amount of time understanding how Gradle handles input files.</p>
<h1 id="heading-the-problem">The problem</h1>
<p>The Apollo Gradle Plugin is generating Kotlin code from GraphQL files.</p>
<p>If you have GraphQL files like this:</p>
<pre><code class="lang-plaintext">.
└── src
    └── main
        └── graphql
            └── com
                └── example
                    ├── GetUser.graphql
                    ├── GetProduct.graphql
                    └── GetReview.graphql
</code></pre>
<p>The Apollo Compiler generates Kotlin classes like this:</p>
<pre><code class="lang-plaintext">com.example.GetUser
com.example.GetProduct
com.example.GetReview
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">ℹ</div>
<div data-node-type="callout-text"><em>This behaviour of using the path as package name is </em><a target="_blank" href="https://apollographql.github.io/apollo-kotlin/kdoc/apollo-gradle-plugin-external/com.apollographql.apollo.gradle.api/-service/package-names-from-file-paths.html"><em>optional</em></a><em> and probably questionable but provides a very good use case for us diving into file inputs APIs.</em></div>
</div>

<p>In a nutshell, the compiler looks at the relative path of the file and uses that as the package name.</p>
<h1 id="heading-build-cache-relocation">Build cache relocation</h1>
<p>A naive implementation could use the "base" directory as input so that it can compute the package names:</p>
<pre><code class="lang-kotlin">  <span class="hljs-meta">@get:PathSensitive</span>(PathSensitivity.RELATIVE)
  <span class="hljs-meta">@get:InputFiles</span>
  <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">val</span> inputFiles: Set&lt;File&gt;

  <span class="hljs-comment">// The base directory to compute the package name</span>
  <span class="hljs-comment">// This isn't great</span>
  <span class="hljs-meta">@get:Input</span>
  <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">var</span> baseDir: String

  <span class="hljs-meta">@TaskAction</span>
  <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">taskAction</span><span class="hljs-params">()</span></span> {
    inputFiles.forEach {
      <span class="hljs-keyword">val</span> packageName = it.relativeTo(File(baseDir))
                          .canonicalPath
                          .replace(File.separatorChar, <span class="hljs-string">'.'</span>)
      <span class="hljs-comment">// ... </span>
    }
  }
</code></pre>
<p>That works but it also completely breaks <a target="_blank" href="https://docs.gradle.org/current/userguide/build_cache_concepts.html#relocatability">build cache relocation</a>. If we were to copy our repository to another directory on our machine (or in CI), then no cache results could ever be reused.</p>
<h1 id="heading-entering-filecollection">Entering FileCollection</h1>
<p>The solution is to use <a target="_blank" href="https://docs.gradle.org/current/javadoc/org/gradle/api/file/FileCollection.html"><code>FileCollection</code></a> and its mutable cousin <a target="_blank" href="https://docs.gradle.org/current/javadoc/org/gradle/api/file/ConfigurableFileCollection.html"><code>ConfigurableFileCollection</code></a>.</p>
<p><code>FileCollection</code> is a <a target="_blank" href="https://docs.gradle.org/current/userguide/lazy_configuration.html">lazy API</a> that can resolve <a target="_blank" href="https://mbonnin.medium.com/actual-footage-of-different-kinds-of-gradle-configurations-9678bd681793"><code>Configuration</code></a>s but most importantly may contain "roots". In other words, a <code>FileCollection</code> contains `File`s but also their relative path to the root(s).</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Using<a target="_blank" href="https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/PathSensitivity.html#RELATIVE"><code>PathSensitivity.RELATIVE</code></a> on a <code>java.io.File</code> property is the same as using <a target="_blank" href="https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/PathSensitivity.html#NAME_ONLY"><code>PathSensitivity.NAME_ONLY</code></a> since it doesn't not contain any root.</div>
</div>

<p>At a high level, <strong>Gradle doesn't have</strong><code>java.io.File</code><strong>inputs</strong>. It's always <code>java.io.File</code> + <code>"fileIdentifier"</code>.</p>
<p><code>fileIdentifier</code> being a name, a relative or absolute path, or even empty if only the contents of the file matter and you're using <a target="_blank" href="https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/PathSensitivity.html#NONE"><code>PathSensitivity.NONE</code></a>.</p>
<p>So how do we get that identifier? Well, for <code>NAME_ONLY</code> we can use <code>file.name</code>, for <code>NONE</code>, we can use <code>""</code> and for <code>ABSOLUTE</code>, we can use <code>file.asbolutePath</code>. But what about <code>RELATIVE</code>?</p>
<p>The secret to getting the relative path from a <code>FileCollection</code> is using <a target="_blank" href="https://docs.gradle.org/current/javadoc/org/gradle/api/file/FileCollection.html#getAsFileTree()"><code>asFileTree</code></a> which returns the underlying <code>FileTree</code> if any (which is really a <code>FileForest</code> because <code>FileTree</code>s can contain multiple roots):</p>
<pre><code class="lang-kotlin">  <span class="hljs-meta">@get:PathSensitive</span>(PathSensitivity.RELATIVE)
  <span class="hljs-meta">@get:InputFiles</span>
  <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">val</span> inputFiles: ConfigurableFileCollection

  <span class="hljs-meta">@TaskAction</span>
  <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">taskAction</span><span class="hljs-params">()</span></span> {
    inputFiles.asFileTree.visit {
      <span class="hljs-comment">// Yay, we have the File here alongside its relative path 🎉</span>
      <span class="hljs-keyword">val</span> packageName = path.replace(File.separatorChar, <span class="hljs-string">'.'</span>)
      <span class="hljs-comment">// do codegen</span>
    }
  }
</code></pre>
<p>Using <code>ConfigurableFileCollection</code>, we get all the information about our files, including their relative path.</p>
<p>It's a very convenient API that non only allows resolving dependencies (<code>Configurations</code> are <code>FileCollections</code>) and filtering (<code>FileTree.include()</code>) and do so in a <a target="_blank" href="https://docs.gradle.org/current/userguide/lazy_configuration.html">lazy</a> way but most importantly, they make it possible to model input files without breaking cache relocation! SUCCESS!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721037893936/f23ea5e9-1bd2-4b6e-8d08-13312133e60b.webp" alt class="image--center mx-auto" /></p>
<p>Looking ahead, <a target="_blank" href="https://github.com/GradleUp/gratatouille">Gratatouille</a> will not allow simple `File` inputs to model the fact that task inputs also have a name (even if empty). Hopefully that will make the task of writing Gradle tasks easier!</p>
<hr />
<p><em>Brainteasers pictures from</em><a target="_blank" href="https://www.instructables.com/member/mtairymd/"><em>mtairymd</em></a><em>on</em><a target="_blank" href="https://www.instructables.com/Metal-Wire-Puzzle-Solutions/"><em>Instructables</em></a></p>
]]></content:encoded></item><item><title><![CDATA[Gradle brainteasers 1/2: aggregating artifacts]]></title><description><![CDATA[I've been developing Gradle plugins for 7 years now. Sometimes I love it, sometimes I hate it. But even when I hate it, I love hating it!
I just realized some of the Gradle APIs are like those metal wire brain teasers games:

Are they easy? No.

Do y...]]></description><link>https://blog.mbonnin.net/gradle-brainteasers-12-aggregating-artifacts</link><guid isPermaLink="true">https://blog.mbonnin.net/gradle-brainteasers-12-aggregating-artifacts</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[gradle]]></category><dc:creator><![CDATA[Martin Bonnin]]></dc:creator><pubDate>Mon, 15 Jul 2024 10:44:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1720876226185/c6a866b4-e512-483d-8e3a-decefe9261c9.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've been developing Gradle plugins for 7 years now. Sometimes <a target="_blank" href="https://github.com/GradleUp/gratatouille">I love it</a>, sometimes <a target="_blank" href="https://mbonnin.medium.com/actual-footage-of-different-kinds-of-gradle-configurations-9678bd681793">I hate it</a>. But even when I hate it, I love hating it!</p>
<p>I just realized some of the Gradle APIs are like those metal wire brain teasers games:</p>
<ul>
<li><p>Are they easy? No.</p>
</li>
<li><p>Do you have to turn them upside down for hours (or months...) trying to understand what you're supposed to do? Yup, most probably.</p>
</li>
<li><p>Do you get a nice feeling of completion once you figure out how to untangle them? Absolutely!</p>
</li>
</ul>
<p>I just had a couple of "Aha!" moment very recently. This post is about the "Aha!" moment of untangling dependency resolution and project isolation.<br />A follow up one will be about input files.<br />Warning, <strong>spoilers ahead</strong>, <strong>do not read if you want to play the game yourself.</strong></p>
<h1 id="heading-the-problem">The problem</h1>
<p>If you have a multi-project build, there are a number of occasions where aggregating artifacts is desirable. Dokka <a target="_blank" href="https://github.com/Kotlin/dokka?tab=readme-ov-file#get-started-with-dokka">is an example</a>. Maven publication is another one. Each project (<a target="_blank" href="https://github.com/autonomousapps/gradle-glossary/blob/main/README.asciidoc#module">also known as module</a>) creates their artifacts and you want to collect them in the root project to upload everything all at once.</p>
<pre><code class="lang-kotlin"># aggregate all publications accross all modules and publish them
# to the Maven Central Portal
./gradlew publishToMavenCentral
</code></pre>
<p>Sounds easy, right?</p>
<p>Well... Not that much. There are a lot of traps along the way.</p>
<h1 id="heading-project-isolation">Project isolation</h1>
<p><a target="_blank" href="https://docs.gradle.org/current/userguide/isolated_projects.html">Project isolation</a> is an initiative to make builds faster by parallelizing the creation of the task graph. You enable it with the <code>Dorg.gradle.unsafe.isolated-projects</code> flag.</p>
<p>Up until recently it wasn't really an option to enable it but it's been <a target="_blank" href="https://youtrack.jetbrains.com/issue/KT-54105">picking</a> <a target="_blank" href="https://github.com/google/ksp/issues/1943">up</a> <a target="_blank" href="https://github.com/gradle/gradle/issues/29045">steam</a> lately!</p>
<p>Let's take a simple build:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721041049710/41cedb0a-984e-4d81-83a1-c1aa9adfc19b.png" alt class="image--center mx-auto" /></p>
<p>By using project isolation, building the task graph for project1 and project2can be executed in parallel. That's right, actually using all the CPUs in your machine 🎉.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Project isolation is about building <strong>the task graph</strong> in parallel, not executing the tasks in parallel, which is already possible.</div>
</div>

<p>If you've read <a target="_blank" href="https://docs.gradle.org/current/userguide/cross_project_publications.html">the documentation about sharing outputs between projects</a>, you know that you shouldn't do things like this:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// build.gradle.kts</span>
dependencies {
   <span class="hljs-comment">// Don't do this!</span>
   add(<span class="hljs-string">"aggregate"</span>, project(<span class="hljs-string">":project1"</span>).tasks.named(<span class="hljs-string">"generatePublication"</span>)
}
</code></pre>
<p>From the documentation:</p>
<blockquote>
<p>This publication model is <em>unsafe</em> and can lead to non-reproducible and hard to parallelize builds.</p>
</blockquote>
<p>If the projects are evaluated concurrently then accessing the mutable <code>Project.tasks</code> is prone to race conditions.</p>
<p>If we want to support project isolation, we need to do this instead in our root (consumer) project:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// build.gradle.kts</span>
dependencies {
   <span class="hljs-comment">// Do this</span>
   <span class="hljs-comment">// This is project isolation compatible</span>
   <span class="hljs-comment">// (PS: this doesn't use attributes for REASONs!)</span>
   add(<span class="hljs-string">"aggregate"</span>, project(<span class="hljs-string">":project1"</span>, <span class="hljs-string">"outgoingPublication"</span>)
}
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">ℹ</div>
<div data-node-type="callout-text"><code>"outgoingPublication"</code> is the name of an <a target="_blank" href="https://docs.gradle.org/current/userguide/variant_model.html#outgoing_variants_report">outgoing configuration</a> exposing all our files.</div>
</div>

<p>To expose the artifacts in our producer project, we use configurations and the artifacts {} API:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// project1/build.gradle.kts</span>
<span class="hljs-keyword">val</span> configuration = configurations.consumable(<span class="hljs-string">"outgoingPublication"</span>).<span class="hljs-keyword">get</span>()

<span class="hljs-keyword">val</span> m2Dir = layout.buildDirectory.dir(<span class="hljs-string">"m2/"</span>).<span class="hljs-keyword">get</span>()
publishing {
  <span class="hljs-comment">// Add a local repository</span>
  repositories {
    maven {
      name = <span class="hljs-string">"m2"</span>
      setUrl(uri(m2Dir.asFile.path))
    }
  }
}
artifacts {
  artifacts {
    <span class="hljs-comment">// expose the repository to other projects</span>
    add(configuration.name, m2Dir) {
      builtBy(<span class="hljs-string">"publishAllPublicationsToM2Repository"</span>)
    }
  }
}
</code></pre>
<p>(There's a lot in there and if you're curious about how that works in details, take a look at <a target="_blank" href="https://mbonnin.medium.com/actual-footage-of-different-kinds-of-gradle-configurations-9678bd681793">this other post about Gradle configurations)</a>.</p>
<p>Using the configurations and artifacts API, we can express cross-project dependencies. But it's a bit verbose.</p>
<h1 id="heading-avoiding-repetition">Avoiding repetition</h1>
<p>The above works fine but assuming you have 50 modules, you now have to do something like this:</p>
<pre><code class="lang-kotlin">
<span class="hljs-comment">// build.gradle.kts</span>
dependencies {
   add(<span class="hljs-string">"aggregate"</span>, project(<span class="hljs-string">":project1"</span>, <span class="hljs-string">"outgoingPublication"</span>)
   add(<span class="hljs-string">"aggregate"</span>, project(<span class="hljs-string">":project2"</span>, <span class="hljs-string">"outgoingPublication"</span>)
   <span class="hljs-comment">// ...</span>
   <span class="hljs-comment">// Pfewwwww, that's a long list of projects to maintain</span>
   add(<span class="hljs-string">"aggregate"</span>, project(<span class="hljs-string">":project50"</span>, <span class="hljs-string">"outgoingPublication"</span>)
}
</code></pre>
<p>Wouldn't it be nice if instead we could register the dependency automatically? After all, the subprojects have some build logic that runs already. Could they automatically register themselves as dependencies of the root project?</p>
<p>To this day I haven't found a way to add cross-project dependencies in a project isolation compatible way (<a target="_blank" href="https://github.com/gradle/gradle/issues/29037">Gradle issue</a>).</p>
<p>But luckily there is another solution!</p>
<h1 id="heading-lenient-artifact-views">Lenient artifact views</h1>
<p>The trick is in using <code>subprojects</code> and <a target="_blank" href="https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/ArtifactView.ViewConfiguration.html#lenient(boolean)">lenient artifact views</a>:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// Create the configuration</span>
<span class="hljs-keyword">val</span> aggregate = configurations.create(<span class="hljs-string">"aggregate"</span>)
<span class="hljs-comment">// Add all subprojects as dependency</span>
<span class="hljs-comment">// ℹ️ subprojects {} is not PI compatible</span>
<span class="hljs-comment">// (but subprojects.forEach {} is)</span>
subprojects.forEach {
  aggregate.dependencies.add(
    dependencies.project(<span class="hljs-string">":<span class="hljs-subst">${it.name}</span>"</span>, <span class="hljs-string">"outgoingPublication"</span>)
  )
}

tasks.register(<span class="hljs-string">"zipAggregate"</span>, Zip::<span class="hljs-keyword">class</span>.java) {
  // Use a lenient artifact view to avoid failing <span class="hljs-keyword">if</span> some subprojects
  // <span class="hljs-keyword">do</span> not publish anything.
  from(aggregate.incoming.artifactView { lenient(<span class="hljs-literal">true</span>) }.files.asFileTree)
  destinationDirectory.<span class="hljs-keyword">set</span>(layout.buildDirectory.dir(<span class="hljs-string">"zip"</span>))
  archiveBaseName.<span class="hljs-keyword">set</span>(<span class="hljs-string">"aggregate"</span>)
}
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><em>Note how this leverages the </em><code>FileCollection</code><em>and </em><code>asFileTree</code><em>APIs, which will be the topic of the next post.</em></div>
</div>

<p>By using a lenient artifact view, the root project can still collect all subprojects. Because the dependency is added on the explicit <code>"outgoingPublication"</code> configuration, there is no risk of resolving other (wrong) artifacts. And because the resolution is lenient, it will fail silently if a project doesn't publish anything. <strong>SUCCESS!</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720982965178/f5f6ca6a-6e6a-4baa-90a7-c868f86ed984.webp" alt="A metal wire brain teaser game. The two parts are sitting aside on a wooden table." class="image--center mx-auto" /></p>
<h1 id="heading-wrap-up">Wrap-up</h1>
<p>I've been toying with publishing and Maven Central APIs <a target="_blank" href="https://github.com/martinbonnin/vespene">for a while now</a>. Project isolation was something that has been itching me for the last 6 months or so.<br />Using lenient artifact views provides a simple solution that would otherwise require a lot of fragile code.</p>
<p>With this itch gone, new opportunities for publishing arise, stay tuned!</p>
<hr />
<p><em>PS: this only works for cases where a single root projects depends on all the other ones but modeling a more complex project graph is still itching me...</em></p>
<p><em>Brainteasers pictures from</em> <a target="_blank" href="https://www.instructables.com/member/mtairymd/"><em>mtairymd</em></a> <em>on</em> <a target="_blank" href="https://www.instructables.com/Metal-Wire-Puzzle-Solutions/"><em>Instructables</em></a></p>
]]></content:encoded></item><item><title><![CDATA[My life after `afterEvaluate {}`]]></title><description><![CDATA[If you're writing Gradle build scripts, chances are you have already used afterEvaluate {}.
This is usually the last resort solution. But in my life of Gradle scripts engineer, I found the odds of afterEvaluate {} actually "fixing" your issue are in ...]]></description><link>https://blog.mbonnin.net/my-life-after-afterevaluate</link><guid isPermaLink="true">https://blog.mbonnin.net/my-life-after-afterevaluate</guid><category><![CDATA[Android]]></category><category><![CDATA[gradle]]></category><category><![CDATA[Kotlin]]></category><dc:creator><![CDATA[Martin Bonnin]]></dc:creator><pubDate>Thu, 24 Nov 2022 15:00:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1669302364335/tjMC7JDDy.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you're writing Gradle build scripts, chances are you have already used <code>afterEvaluate {}</code>.</p>
<p>This is usually the last resort solution. But in my life of Gradle scripts engineer, I found the odds of <code>afterEvaluate {}</code> actually "fixing" your issue are in effect quite high. II guess that explains while you'll still find it in <a target="_blank" href="https://github.com/search?q=afterEvaluate&amp;type=code">a lot of places</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1669213086874/YOZOmz0N9.jpg" alt="71v4bu.jpg" class="image--center mx-auto" /></p>
<p>Because <code>afterEvaluate {}</code> postpones your code's execution, it gives more time to other plugins and build logic to execute their "stuff", whatever that is. If you code relies on some of that "stuff" then your build is "fixed" (note all the quotes there).  </p>
<p>This is very fragile though for a number of reasons:</p>
<ul>
<li>Anyone wanting to use your code now also needs to use <code>afterEvaluate {}</code> which is not obvious at all</li>
<li>Anyone wanting to use the code that uses your code also needs to use <code>afterEvaluate{}</code> and so on, all the way down</li>
<li>It becomes even more complicated as more plugins/scripts are involved</li>
<li>More generally, it makes dependencies between plugins/scripts implicit and error prone</li>
</ul>
<p>So how can we make things more solid? The good news is solutions exist!</p>
<p>Usually, <code>afterEvaluate {}</code> is required in two occasions:</p>
<ol>
<li>reading extension properties</li>
<li>configuring defaults</li>
</ol>
<p>(if you have more use cases, please reach out, I'm curious to learn about them and will update this article)</p>
<h2 id="heading-reading-extension-properties">Reading extension properties</h2>
<p>Gradle plugins are configured with <a target="_blank" href="https://docs.gradle.org/current/userguide/custom_plugins.html#sec:getting_input_from_the_build">extensions</a>. I won't go into the details of extensions here, the Gradle doc is, as usual, very comprehensive on the matter. The tldr; is that extensions are simple classes that expose options for your plugin. In a sense, it is the public API of your plugin.</p>
<p>This is working well until you need something in your extension to change how the task graph is created. Assume you want to customise the name of your task for an exemple. It doesn't work.</p>
<pre><code>open <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyExtension</span> </span>{
  <span class="hljs-comment">// The name of the person to greet</span>
  <span class="hljs-keyword">var</span> personToGreet: <span class="hljs-built_in">String</span>? = <span class="hljs-literal">null</span>
}

open <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyPlugin</span>: <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{
  override fun apply(target: Project) {
    val extension = target.extensions.create(<span class="hljs-string">"myPlugin"</span>, <span class="hljs-attr">MyExtension</span>::<span class="hljs-keyword">class</span>.java)

    <span class="hljs-comment">// DOES NOT WORK 😞</span>
    <span class="hljs-comment">// extension.personToGreet is not set here because your </span>
    <span class="hljs-comment">// `build.gradle.kts` file is not evaluated yet</span>
    <span class="hljs-comment">// Your task will be named "greetnull"</span>
    target.tasks.register(<span class="hljs-string">"greet${extension.personToGreet}"</span>) {
      it.doLast {
        println(<span class="hljs-string">"Hello ${extension.personToGreet}"</span>)
      }
    }
  }
}
</code></pre><p>It is tempting to wrap everything in <code>afterEvaluate{}</code> and wait for your <code>build.gradle.kts</code> to be evaluated but please don't do this for the reasons explained above.</p>
<p>Instead, you can expose a function in your extension. This way, you can execute logic and tweak the task graph from your code. You have full control over when your code executes without relying on the <code>afterEvaluate {}</code> chaos:</p>
<pre><code><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GreetingOptions</span> </span>{
  <span class="hljs-comment">// The name of the person to greet</span>
  <span class="hljs-keyword">var</span> personToGreet: <span class="hljs-built_in">String</span>? = <span class="hljs-literal">null</span>
}

open <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyExtension</span>(<span class="hljs-title">val</span> <span class="hljs-title">project</span>: <span class="hljs-title">Project</span>) </span>{
  fun greeting(action: Action&lt;GreetingOptions&gt;) {
    val options = GreetingOptions()
    action.execute(options)

    <span class="hljs-comment">// WORKS 🤩</span>
    <span class="hljs-comment">// personToGreet is always set here</span>
    <span class="hljs-comment">// You have full control of the timing</span>
    project.tasks.register(<span class="hljs-string">"greet${options.personToGreet}"</span>) {
      it.doLast {
        println(<span class="hljs-string">"Hello ${options.personToGreet}"</span>)
      }
    }
  }
}
</code></pre><p>There is one drawback to this solution is that it makes the callsite a bit more verbose:</p>
<pre><code><span class="hljs-comment">// build.gradle.kts</span>
myPlugin {
  <span class="hljs-comment">// there's one additional level of nesting here</span>
  greeting {
    personToGreet = <span class="hljs-string">"Björn"</span>
  }
}
</code></pre><p>But trust me it's a minor drawback compared to the <code>afterEvaluate {}</code> hair pulling especially as your plugin grows, this initial function call will be very small in your total API surface.</p>
<h2 id="heading-configuring-defaults">Configuring defaults</h2>
<p>The other case is about default behaviour. Sometimes, you don't want your users to specify anything and have valid defaults that can be overridden by the user.</p>
<pre><code>apply {
  id(<span class="hljs-string">"myplugin"</span>).version(<span class="hljs-string">"1.0"</span>)
}

<span class="hljs-comment">// no other configuration, just use defaults</span>
</code></pre><p>This one is trickier. Because there is no extension, there is no function where you can run your code... I've looked at this from every direction and the more I look at this, the more I think you should not support this...</p>
<p>There are ways to do this without <code>afterEvaluate</code>. For an example, you could always create the default tasks and then disable them as soon as the extension gets configured. That could work. But this also adds more tasks in <code>./gradlew --tasks</code>, more clutter to your build for no obvious reason.</p>
<p>So I <em>think</em> (but I still have to <a target="_blank" href="https://github.com/apollographql/apollo-kotlin/blob/d2ce850a40f9cfa666d111febf2ed3225d6cf4a6/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultApolloExtension.kt#L132">do it</a>) that forcing the user to opt-in the default is an acceptable workaround here.</p>
<pre><code>apply {
  id(<span class="hljs-string">"myplugin"</span>).version(<span class="hljs-string">"1.0"</span>)
}

<span class="hljs-comment">// One time configuration</span>
myPlugin {
  <span class="hljs-comment">// opt-in the default behaviour</span>
  greeting()
}
</code></pre><p>Sure it's 3 extra lines and it's not great but in the grand scheme of things, it's a one-time cost that's going to save you a bunch of head-aches over time. And I don't really see another way around.</p>
<h2 id="heading-is-afterevaluate-that-bad">Is <code>afterEvaluate {}</code> that bad?</h2>
<p>Short answer as you guessed is yes!</p>
<p>There is one area though where I find it useful, it's to perform consistency checks. In the example above, it doesn't make sense to force the user to either use the default greeting or define their own. </p>
<p>In these cases, I found it useful. As it's localised to your own code and doesn't introduce timing issues or dependencies with other plugins, it usually works well.</p>
<pre><code>abstract <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyPlugin</span> : <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{
    override fun apply(target: Project) {
        val extension = target.extensions.create(<span class="hljs-string">"myPlugin"</span>, <span class="hljs-attr">MyExtension</span>::<span class="hljs-keyword">class</span>.java, target)

        target.afterEvaluate { 
            check(extension.setupDone) {
                <span class="hljs-string">""</span><span class="hljs-string">"
                    myplugin: you either need to define your greeting or use the default one with `defaultGreeting()`
                "</span><span class="hljs-string">""</span>.trimIndent()
            }
        }
    }
}
</code></pre><h2 id="heading-conclusion">Conclusion</h2>
<p>All in all, whether you trying to read extension properties or provide defaults, I like to think of the entry point to your plugin as a "function" and not just a class. It's something where you can write code. It has drawbacks for sure, it's more verbose and all but once you do that, you can finally say goodbye to that <code>afterEvaluate {}</code> chaos and start sailing to new Gradle horizons!</p>
]]></content:encoded></item><item><title><![CDATA[How Gradle compiles your build scripts]]></title><description><![CDATA[This post is inspired by a discussion on the Gradle community slack. Many thanks to @Vampire and @ephemient for their help understanding all of this 
Have you ever tried to do something like this?
val kotlinVersion = "1.7.21"

plugins {
    id("org.j...]]></description><link>https://blog.mbonnin.net/how-gradle-compiles-your-build-scripts</link><guid isPermaLink="true">https://blog.mbonnin.net/how-gradle-compiles-your-build-scripts</guid><category><![CDATA[gradle]]></category><category><![CDATA[Android]]></category><category><![CDATA[Kotlin]]></category><dc:creator><![CDATA[Martin Bonnin]]></dc:creator><pubDate>Fri, 18 Nov 2022 17:14:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1669302959968/jEJIB8e_7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>This post is inspired by a <a target="_blank" href="https://linen.dev/s/gradle-community/t/5100108/are-the-rules-for-how-gradle-parses-compiles-buildscript-and">discussion on the Gradle community slack</a>. Many thanks to <code>@Vampire</code> and <code>@ephemient</code> for their help understanding all of this</em> </p>
<p>Have you ever tried to do something like this?</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> kotlinVersion = <span class="hljs-string">"1.7.21"</span>

plugins {
    id(<span class="hljs-string">"org.jetbrains.kotlin.jvm"</span>).version(kotlinVersion)
}

dependencies {
    implementation(<span class="hljs-string">"org.jetbrains.kotlin:kotlin-stdlib:<span class="hljs-variable">$kotlinVersion</span>"</span>)
}
</code></pre>
<p>Looks pretty neat, right? Your Kotlin version is defined in a single place and it's easy to change it when a new version is release. </p>
<p>If you have tried this, you probably know it doesn't work. You'll get hit pretty quickly by this:</p>
<pre><code>e: build.gradle.kts:<span class="hljs-number">4</span>:<span class="hljs-number">44</span>: Unresolved reference: kotlinVersion
</code></pre><p>That's surprising. There are very good reasons for this but they're not obvious at first sight. </p>
<p>Let's dive in.</p>
<h2 id="heading-plugins-and-classpaths">Plugins and classpaths</h2>
<p>Gradle support <a target="_blank" href="https://docs.gradle.org/current/userguide/plugins.html">plugins</a>. Plugins are very handy because they allow you augment your builds. If you're an Android developer, you're certainly familiar with snippets like this:</p>
<pre><code class="lang-kotlin">android {
    compileSdk = <span class="hljs-number">32</span>

    defaultConfig {
        applicationId = <span class="hljs-string">"com.example"</span>
        minSdk = <span class="hljs-number">23</span>
        targetSdk = <span class="hljs-number">32</span>
        versionCode = <span class="hljs-number">1</span>
        versionName = <span class="hljs-string">"1.0"</span>
    }
}
</code></pre>
<p>Looking a bit closer, this is all compiled and typesafe. Gradle knows <code>compileSdk</code> is a <code>var Int</code>, same for <code>minSdk</code>, <code>targetSdk</code> and others.</p>
<p>This means that Gradle knows about <code>com.android.build.api.dsl.CommonExtension</code> the class that defines <code>compileSdk</code>. Since Gradle cannot put the whole world on the build script classpath, this has to be conveyed somehow. This is what <code>plugins {}</code> do.</p>
<h2 id="heading-multiple-passes-of-compilation">Multiple passes of compilation</h2>
<p>Gradle parses your <code>build.gradle.kts</code> file and extracts the <code>plugins {}</code> block. It does so <a target="_blank" href="https://github.com/gradle/gradle/blob/005cceed4ce15708888824566ef94aede20b11c7/subprojects/kotlin-dsl/src/main/kotlin/org/gradle/kotlin/dsl/execution/Lexer.kt#L223">using the same KotlinLexer</a> that the Kotlin compiler uses. Once the <code>plugins {}</code> block extracted, Gradle compiles it and runs it. This is also the moment when generated accessors are ... well... generated! </p>
<p>So after the <code>plugins {}</code> block is evaluated, Gradle has:</p>
<ul>
<li>the plugins used by the script and their matching jar</li>
<li>generated accessors</li>
</ul>
<p>In a second pass, it can compile and evaluate the script, with all the plugin jars on the classpath. It all makes sense! </p>
<p>This first pass is why the syntax of the <code>plugins {}</code> block is so <a target="_blank" href="https://docs.gradle.org/current/userguide/plugins.html#sec:constrained_syntax">constrained</a>.</p>
<h2 id="heading-its-not-only-plugins">It's not only plugins {}</h2>
<p><code>plugins {}</code> is the most used block but this also applies to other blocks:</p>
<ul>
<li><code>buildscript {}</code></li>
<li><code>pluginManagement {}</code></li>
<li><code>iniscript {}</code></li>
</ul>
<p>If you bump into errors, double check what block you're in. Chances are that your code is evaluated in a separate context.</p>
<h2 id="heading-whats-working">What's working</h2>
<p>Once we know that, how do we make the above work? In general, I'm not 100% sure what syntax is allowed or not in these blocks. The good news is that there are multiple solutions to define your Kotlin version in a single place (or do other things).</p>
<p>The most straightforward is to use <a target="_blank" href="https://docs.gradle.org/current/userguide/platforms.html">version catalogs</a>:</p>
<pre><code class="lang-toml">// libs.versions.toml
<span class="hljs-section">[versions]</span>
<span class="hljs-attr">kotlin</span> = <span class="hljs-string">"1.7.21"</span>

<span class="hljs-section">[libraries]</span>
<span class="hljs-attr">kotlin-stdlib</span> = { group = <span class="hljs-string">"org.jetbrains.kotlin"</span>, name = <span class="hljs-string">"kotlin-stdlib"</span>, version.ref = <span class="hljs-string">"kotlin"</span> }

<span class="hljs-section">[plugins]</span>
<span class="hljs-attr">kotlin</span> = { id = <span class="hljs-string">"org.jetbrains.kotlin.jvm"</span>, version.ref = <span class="hljs-string">"kotlin"</span> }
</code></pre>
<pre><code class="lang-kotlin"><span class="hljs-comment">// build.gradle.kts</span>
plugins {
    alias(libs.plugins.kotlin)
}

dependencies {
    implementation(libs.kotlin.stdlib)
}
</code></pre>
<p>There are other solutions <a target="_blank" href="https://gist.github.com/martinbonnin/e6a2af4a9e722e93b41ce31bf3ef19d0">using <code>pluginManagement</code> and Gradle properties</a>. Or you can build your own!</p>
<p>In all cases, I hope this post helped you understand how Gradle processes your build script. It's nothing magical!</p>
]]></content:encoded></item><item><title><![CDATA[About the Android Makers app, security and google-services.json]]></title><description><![CDATA[Android Makers is over! With over 650 participants and 50 sessions over 2 days in Paris, it was a lot of fun! It was nice to see everyone in person again and discuss all the Android things (and 🍻 too)! 
This year, we decided to rewrite the Android a...]]></description><link>https://blog.mbonnin.net/about-the-android-makers-app-security-and-google-servicesjson</link><guid isPermaLink="true">https://blog.mbonnin.net/about-the-android-makers-app-security-and-google-servicesjson</guid><category><![CDATA[Android]]></category><category><![CDATA[Security]]></category><category><![CDATA[google cloud]]></category><dc:creator><![CDATA[Martin Bonnin]]></dc:creator><pubDate>Fri, 06 May 2022 13:45:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1651853736770/3nQMK1bsA.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://androidmakers.fr/">Android Makers</a> is over! With over 650 participants and 50 sessions over 2 days in Paris, it was a lot of fun! It was nice to see everyone in person again and discuss all the Android things (and 🍻 too)! </p>
<p>This year, we decided to rewrite the Android app (<a target="_blank" href="https://github.com/paug/AndroidMakersApp">github</a>) in Jetpack Compose and this too was a lot of fun 😃! We got a functional app in no time and <a target="_blank" href="https://twitter.com/martinbonnin/status/1516377118355111936">sent it out for public scrutiny</a>. We got a lot of feedback and contributions (did I mention this community rocks? 🤘💙)! </p>
<p>This is the story of how (and why) we made our <code>google-services.json</code> public so that anyone could easily contribute.</p>
<h2 id="heading-about-google-servicesjson">About google-services.json</h2>
<p>Whenever you setup a Firebase project, <code>google-services.json</code> comes up pretty quickly in the installation instructions. You usually download it from the Firebase console:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1651251219633/E61JnkN3U.png" alt="Screenshot 2022-04-29 at 18.53.25.png" /></p>
<p>If you open it, you'll find something like this (some values redacted although I'm not 100% sure why, more on that later):</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"project_info"</span>: {
    <span class="hljs-attr">"project_number"</span>: <span class="hljs-string">"378302699578"</span>,
    <span class="hljs-attr">"project_id"</span>: <span class="hljs-string">"openfeedback-am-2022"</span>,
    <span class="hljs-attr">"storage_bucket"</span>: <span class="hljs-string">"openfeedback-am-2022.appspot.com"</span>
  },
  <span class="hljs-attr">"client"</span>: [
    {
      <span class="hljs-attr">"client_info"</span>: {
        <span class="hljs-attr">"android_client_info"</span>: {
          <span class="hljs-attr">"package_name"</span>: <span class="hljs-string">"fr.androidmakers"</span>
        }
      },
      <span class="hljs-attr">"api_key"</span>: [
        {
          <span class="hljs-attr">"current_key"</span>: <span class="hljs-string">"[redacted]"</span>
        }
      ],
    }
  ],
  <span class="hljs-attr">"configuration_version"</span>: <span class="hljs-string">"1"</span>
}
</code></pre>
<p>As you can see, there's something called <code>api_key</code> up there. That sounds pretty secret so certainly it's a good idea to hide it, right? Well... turns out it's not secret at all.</p>
<h2 id="heading-your-android-api-key-is-not-secret">Your Android API key is not secret!</h2>
<p>Because your app needs your API key at runtime, the API key must be accessible somewhere in the app. So really it can't be secret. To prove it, let's try to retrieve it. </p>
<p>Start by doing a <a target="_blank" href="https://www.google.com/search?q=fr.paug.androidmakers+apk&amp;oq=fr.paug.androidmakers+apk">google search for the Android Makers app apk</a>. This takes you to an ApkPure website that allows you to download the apk:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1651252524982/f96rPW-4L6.png" alt="Screenshot 2022-04-29 at 19.15.21.png" /></p>
<p>A click and and a few downloaded MB later, you can open that APK in the Android Studio APK analyzer (Build -&gt; Analyze APK...). Look for a resource that looks like an API key. This <code>google_api_key</code> string sounds like a good candidate 🙃:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1651253727280/rX9obkbQ1.png" alt="Screenshot 2022-04-29 at 19.34.49.png" /></p>
<p>Yup, zoom a little bit... That's the one! So I'm not sure why I redacted it above. Certainly because everything is made to make you believe this should be treated as a secret.</p>
<h2 id="heading-scary-warnings">Scary warnings</h2>
<p>If you ever commit such an API key to Github, all the repository admins will receive an email along these lines:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1651254177886/98_vuJSyi.png" alt="Screenshot 2022-04-29 at 19.41.03.png" /></p>
<p>Well, that's scary 🙀...</p>
<p>Same thing if you publish an app to the Google play that contains your API key in Kotlin code:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1651254315645/7LvuWI53Q.png" alt="Screenshot 2022-04-22 at 11.02.28.png" /></p>
<p>This is also scary 😱... </p>
<p>Why all these warnings if the API key can be retrieved in a few minutes by any malicious user? That remains a mystery to me. My current guess is that there's no way to distinguish between a client key and a server key so that warning is sent to anyone indiscriminately. If someone can see of a good reason to show these warnings for an Android API key, please tell me so I can edit this article and redact everything for good 😅 (and invalidate all the things too!).</p>
<h2 id="heading-developing-in-the-open">Developing in the open</h2>
<p>After <a target="_blank" href="https://stackoverflow.com/questions/37358340/should-i-add-the-google-services-json-from-firebase-to-my-repository">browsing the web a bunch</a>, realising that <a target="_blank" href="https://groups.google.com/g/firebase-talk/c/bamCgTDajkw/m/uVEJXjtiBwAJ">there was an official answer in the firebase forums</a> (<strong>edit</strong>: and it's even in the <a target="_blank" href="https://firebase.google.com/docs/projects/learn-more#config-files-objects">official Firebase documentation</a> as <a target="_blank" href="https://twitter.com/zsmb13/status/1524010043779211267">@zsmb13 made me realize later</a>) and double checking that the values were in the apk anyway, we decided to ignore the scary warnings, stop living in fear (🤘) and <a target="_blank" href="https://github.com/paug/AndroidMakersApp/pull/100">commit our <code>google-services.json</code></a>! </p>
<p>Now anyone can develop for the App using the real data. Special thanks to  <a target="_blank" href="https://github.com/underwindfall"><code>@underwindfall</code></a>, <a target="_blank" href="https://github.com/R4md4c"><code>@R4md4c</code></a> and <a target="_blank" href="https://github.com/oldergod"><code>@oldergod</code></a> for their awesome contributions 💙!</p>
<h2 id="heading-what-could-go-wrong">What could go wrong?</h2>
<p>Assuming someone has your Android API key. What can they do with it? </p>
<p>In our case, the main thing was using our Firestore instance and using our quota. We could imagine someone developing their own service squatting our Firestore instance and therefore not paying the bill at the end of the month 💸.</p>
<p>While technically possible, I'm not sure how practical that would be. Such a service would be dependant on a completely foreign infra and any change would break it almost instantly. I'm not aware any such attack has happened already but maybe it's a matter of time...</p>
<h2 id="heading-additional-actual-restrictions">Additional (actual) restrictions 🔐</h2>
<p>The good news is that the <a target="_blank" href="https://cloud.google.com/docs/authentication/api-keys#api_key_restrictions">Google Cloud console allows to restrict the API keys</a> 🎉</p>
<h3 id="heading-platform-restrictions">Platform restrictions</h3>
<p>You can restrict your API key per platform:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1651255957380/g7ivn5SmP.png" alt="Screenshot 2022-04-29 at 20.11.41.png" /></p>
<p>In the example above, we restricted the API key to only be callable from Android apps. As <a target="_blank" href="https://twitter.com/RickClephas/status/1524079729783066625">@RickClephas points out on Twitter</a>, this works by using two HTTP headers: <code>X-Android-Package</code> and <code>X-Android-Cert</code>. So it's not bullet proof but it's still something.</p>
<h3 id="heading-apis-restrictions">APIs restrictions</h3>
<p>In addition to platforms, you can also restrict your API key to some APIs:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1651837353169/VxVXKzVe3.png" alt="4V4UH3LUW.png" /></p>
<p>This makes sure no other APIs can be used so it's an additional safety. Make sure to include <code>Firebase Installations API</code> and <code>Token Service API</code> or your app will stop working after a few calls.</p>
<p>If your feedback was not counted during Android Makers, this is what happened 😅. Thanks to <a class="user-mention" href="https://hashnode.com/@HugoGresse">Hugo Gresse</a> and <a target="_blank" href="https://github.com/GerardPaligot"><code>@GerardPaligot</code></a> for catching this!</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You can hide your Android API key from Github but this is not going to prevent anyone to get it. If you need to share it with colleagues or your community, it should be pretty safe to commit it despite all the scary warnings.</p>
<p>At the end of the day, security is never a "yes" or "no" kind of question and has more of a continuum of answers. Some might argue that despites all the flaws, hiding your API key from GitHub is still making it harder to retrieve and that's very true. It's also very true that every security measure has a cost, either in terms of raw dollars or in terms of developer experience.</p>
<p>Of all the possibilities:</p>
<ul>
<li>Add <a target="_blank" href="https://cloud.google.com/docs/authentication/api-keys#api_key_restrictions">GCP API key restrictions</a></li>
<li>Fine tune your <a target="_blank" href="https://firebase.google.com/docs/firestore/security/get-started">Firestore security rules</a></li>
<li>Check your app integrity with something like <a target="_blank" href="https://firebase.google.com/docs/app-check">Firebase App Check</a></li>
<li>Obfuscate your client code with something like <a target="_blank" href="https://www.guardsquare.com/dexguard">DexGuard</a></li>
<li>Implement server side rate limiting and fraud detection</li>
<li>Hide your API key from Github</li>
</ul>
<p>I'd argue that hiding your API key from Github is the one with the worst cost/security ratio. 
If you're relying on that for your security, you'd better add API key restrictions and double check your Firestore rules instead.</p>
<blockquote>
<p>Note: none of the above applies to server API keys of course. If your API keys doesn't end up in a client, make sure to keep them secure in the depths of your infra.</p>
</blockquote>
<p><em>This article was edited on  May 10th to add a note about the Firebase official doc, the <code>X-Android-Package</code> and <code>X-Android-Cert</code> headers and rework the conclusion to display more possible solutions.</em></p>
<p><em>Many thanks to <a target="_blank" href="https://twitter.com/doriancussen">Dorian Cussen</a> and <a target="_blank" href="https://www.edouard-marquez.me/">Edouard Marquez</a> for proof reading this article</em></p>
<p><em>Cover image background by <a target="_blank" href="https://flic.kr/p/KjcUHo">Claire Tresse</a></em></p>
]]></content:encoded></item><item><title><![CDATA[Kotlin compatibility QuickSheet]]></title><description><![CDATA[Edit: Blog post updated for Kotlin 1.7, see the last paragraph for details. 

Kotlin 1.6 has just been released 🎉 (blog post). This is great news for everyone in the Kotlin ecosystem. As with every feature release, there are new features, new deprec...]]></description><link>https://blog.mbonnin.net/kotlin-compatibility-quicksheet</link><guid isPermaLink="true">https://blog.mbonnin.net/kotlin-compatibility-quicksheet</guid><category><![CDATA[Kotlin]]></category><dc:creator><![CDATA[Martin Bonnin]]></dc:creator><pubDate>Wed, 17 Nov 2021 15:22:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1637162434233/c10XKUx5y.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>Edit:</strong> Blog post updated for Kotlin 1.7, see the last paragraph for details. </p>
</blockquote>
<p>Kotlin 1.6 has just been released 🎉 (<a target="_blank" href="https://kotlinlang.org/docs/whatsnew16.html">blog post</a>). This is great news for everyone in the Kotlin ecosystem. As with every feature release, there are new features, new deprecations and other changes that might (but hopefully not too much) break your code.</p>
<p>The <a target="_blank" href="https://kotlinlang.org/docs/">Kotlin documentation</a> has two awesome pages to go into the implications of new feature releases:</p>
<ul>
<li>The <a target="_blank" href="https://kotlinlang.org/docs/kotlin-evolution.html#compatibility-tools">Kotlin evolution - compatibility tools</a> page</li>
<li>The <a target="_blank" href="https://kotlinlang.org/docs/compatibility-modes.html">Compatibility modes</a> page</li>
</ul>
<p>Additionally, a lot of additional information can be found in the <a target="_blank" href="https://youtrack.jetbrains.com/issues">Kotlin issue tracker</a>, like <a target="_blank" href="https://youtrack.jetbrains.com/issue/KT-42293">this issue about Kotlin native compatibility</a></p>
<p>In practice though, I often have a hard time choosing what <code>apiVersion</code>, <code>kotlin-stdlib</code> version to use, etc... This post is an attempt to list common scenarios to help build a better mental model of what's happening under the hood. And also answer the question of:</p>
<p>Should you use Kotlin 1.6 in a library?</p>
<p>Let's try to answer this question. </p>
<p><em>If you prefer reading code than blog posts, the source for this post is available at: https://github.com/martinbonnin/kotlin-compatibility/tree/main</em></p>
<h1 id="heading-project-compiled-with-13-using-a-lib-compiled-with-16">project compiled with 1.3 using a lib compiled with 1.6</h1>
<ul>
<li><code>appCompiler=1.3</code></li>
<li><code>libCompiler=1.6</code></li>
</ul>
<pre><code class="lang-text">e: /Users/mbonnin/git/kotlin-compatibility/lib-1-6/build/libs/lib-1-6.jar!
/META-INF/lib-1-6.kotlin_module: Module was compiled with an 
incompatible version of Kotlin. 
The binary version of its metadata is 1.6.0, expected version is 1.1.16.
</code></pre>
<p>That sounds appropriate. The <a target="_blank" href="https://kotlinlang.org/docs/kotlin-evolution.html#compatibility-flags">evolution principles</a> state that:</p>
<blockquote>
<p>Preferably (but we can't guarantee it), the binary format is mostly forwards compatible with the next feature release, but not later ones (in the cases when new features are not used, e.g. 1.3 can understand most binaries from 1.4, but not 1.5).</p>
</blockquote>
<p>Here, the 1.3 compiler cannot read the 1.6 metadata which sounds expected. Let's try with 1.4</p>
<h1 id="heading-project-compiled-with-14-using-a-lib-compiled-with-16">project compiled with 1.4 using a lib compiled with 1.6</h1>
<ul>
<li><code>appCompiler=1.4</code></li>
<li><code>libCompiler=1.6</code></li>
</ul>
<pre><code class="lang-text">&gt; Task :app-1-4:compileKotlin FAILED
e: /Users/mbonnin/git/kotlin-compatibility/lib-1-6/build/libs/lib-1-6.jar!
/META-INF/lib-1-6.kotlin_module: Module was compiled with an 
incompatible version of Kotlin. 
The binary version of its metadata is 1.6.0, expected version is 1.4.2.
</code></pre>
<p>Good 😌. This was expected. Note how 1.4 expects 1.4.2 metadata while 1.3 was expecting 1.1.16 so something changed but it's still not enough.</p>
<p>Let's try with 1.5</p>
<h1 id="heading-project-compiled-with-15-using-a-lib-compiled-with-16">project compiled with 1.5 using a lib compiled with 1.6</h1>
<ul>
<li><code>appCompiler=1.5</code></li>
<li><code>libCompiler=1.6</code></li>
</ul>
<pre><code class="lang-text">&gt; Task :app-1-5:compileKotlin

BUILD SUCCESSFUL in 5s
</code></pre>
<p>Huge success 🙌. Everything works as expected. </p>
<p>Now let's assume that we are a library author and we want our new shiny lib to use 1.6 while still allowing 1.3 users. Is that possible?</p>
<h1 id="heading-make-the-lib-use-apiversion13-languageversion13">Make the lib use apiVersion=1.3, languageVersion=1.3</h1>
<ul>
<li><code>appCompiler=1.3</code></li>
<li><code>libCompiler=1.6</code></li>
<li><code>libApiVersion=1.3</code></li>
<li><code>libLanguageVersion=1.3</code></li>
</ul>
<pre><code class="lang-text">e: Language version 1.3 is no longer supported; please, use version 
1.4 or greater.
</code></pre>
<p>Fair enough. The <a target="_blank" href="https://blog.jetbrains.com/kotlin/2021/11/kotlin-1-6-0-is-released/">1.6 release notes</a> state that:</p>
<blockquote>
<p>Starting with Kotlin 1.6.0, you can now develop using three previous API versions instead of two (along with the current stable one). Currently, this includes API versions 1.3, 1.4, 1.5, and 1.6.</p>
</blockquote>
<p>That doesn't say anything about <code>languageVersion</code>(Update: Starting with 1.7, <code>languageVersion</code> will also <a target="_blank" href="https://youtrack.jetbrains.com/issue/KT-49006">support three previous versions</a>). Let's try with just <code>apiVersion</code> then:</p>
<h1 id="heading-make-the-lib-use-apiversion13-languageversion16">Make the lib use apiVersion=1.3, languageVersion=1.6</h1>
<ul>
<li><code>appCompiler=1.3</code></li>
<li><code>libCompiler=1.6</code></li>
<li><code>libApiVersion=1.3</code></li>
<li><code>libLanguageVersion=1.6</code></li>
</ul>
<pre><code class="lang-text">e: /Users/mbonnin/git/kotlin-compatibility/lib-1-6-api-1-3-language-1-6
/build/libs/lib-1-6-api-1-3-language-1-6.jar!/META-INF
/lib-1-6-api-1-3-language-1-6.kotlin_module: Module was compiled 
with an incompatible version of Kotlin. The binary version of its 
metadata is 1.6.0, expected version is 1.1.16.
</code></pre>
<p>We're back to step one. The new metadata format cannot be read by the old compiler. Maybe if we could tell the 1.6 compiler to output "backward" compatible metadata that could help. Let's see if <code>languageVersion=1.4</code> can help</p>
<h1 id="heading-set-languageversion14">Set languageVersion=1.4</h1>
<ul>
<li><code>appCompiler=1.3</code></li>
<li><code>libCompiler=1.6</code></li>
<li><code>libApiVersion=1.3</code></li>
<li><code>libLanguageVersion=1.4</code></li>
</ul>
<pre><code class="lang-text">&gt; Task :lib-1-6-api-1-3-language-1-4:compileKotlin

BUILD SUCCESSFUL in 1s
</code></pre>
<p>🎉 That worked! So looks like <code>languageVersion</code> also affects the metadata format</p>
<h1 id="heading-takeaways">Takeaways</h1>
<p>As a library author wanting to use Kotlin 1.6, my current understanding is that:</p>
<ul>
<li><p>if your users are compiling against your lib with Kotlin 1.n</p>
<ul>
<li><code>languageVersion</code> should be set to n+1: <code>languageVersion=1.(n+1)</code></li>
<li><code>apiVersion</code> doesn't really matter as <code>kotlin-stdlib</code> should be resolved to <code>1.6</code>, which is backward compatible with any previous <code>kotlin-stdlib</code></li>
</ul>
</li>
<li><p>if your users run your lib with a fixed <code>kotlin-stdlib:1.p</code> at runtime (<a target="_blank" href="https://blog.mbonnin.net/use-kotlin-15-in-your-gradle-plugins">like Gradle</a>)</p>
<ul>
<li><code>apiVersion</code> should be set to n: <code>apiVersion=1.p</code></li>
</ul>
</li>
</ul>
<p>If you bump to <code>1.6</code> without configuring anything else, your consumers will have to update to Kotlin 1.5.</p>
<p><strong>Note</strong>: The other way around (backward compatibility?) is a way better story: if you don't bump to <code>1.6</code> and keep using <code>1.5</code> in your library, then all consumers can upgrade to <code>1.6</code> and continue using your lib (<a target="_blank" href="https://youtrack.jetbrains.com/issue/KT-42293">including native ones</a>).</p>
<h1 id="heading-update-for-kotlin-17">Update for Kotlin 1.7</h1>
<p>The same is still true. If you bump to Kotlin 1.7 in your lib, your consumers are forced into compiling with 1.6 by default (including for native).</p>
<p>You can keep compatibility with the 1.5 compiler with <code>languageVersion="1.5"</code> but that will only go so far because <code>kotlin-stdlib</code> itself contains 1.7 metadata so unless you're going to great length to <em>not</em> expose <code>kotlin-stdlib:1.7</code> transitively, setting the languageVersion is most likely not going to change much.</p>
<p><strong>Picture</strong>: <a target="_blank" href="https://flic.kr/p/ogzxmg">way down</a> by <a target="_blank" href="https://www.flickr.com/photos/mr-l/">Romain L</a></p>
]]></content:encoded></item><item><title><![CDATA[Use latest Kotlin in your Gradle plugins]]></title><description><![CDATA[This is the story of how I got down the rabbit hole of relocating classes while trying to workaround the Gradle classloaders and fixed Kotlin runtime limitations. I'm not sure if I'd recommend trying this at home but this was an interesting journey! ...]]></description><link>https://blog.mbonnin.net/use-latest-kotlin-in-your-gradle-plugins</link><guid isPermaLink="true">https://blog.mbonnin.net/use-latest-kotlin-in-your-gradle-plugins</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[gradle]]></category><dc:creator><![CDATA[Martin Bonnin]]></dc:creator><pubDate>Fri, 12 Nov 2021 15:13:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1636678886510/3rMt16zUJ.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>This is the story of how I got down the rabbit hole of relocating classes while trying to workaround the Gradle classloaders and fixed Kotlin runtime limitations. I'm not sure if I'd recommend trying this at home but this was an interesting journey! Read this (long) post if you're curious about what it takes to use the latest version of Kotlin in your plugins.</em></p>
<p><em>If you prefer reading source code, read the</em> <a target="_blank" href="https://github.com/apollographql/apollo-android/pull/3542/"><em>matching pull request in apollo-android</em></a></p>
<p><strong>Note</strong>: This post was written when Kotlin 1.5 was released and Gradle 7.1 was using 1.4. The same is true with Kotlin 1.7 (or any other newer versions)</p>
<h3 id="heading-the-and-problem">The 🐔 and 🥚 problem</h3>
<p>Say you have a Gradle build. This Gradle build uses Kotlin <code>build.gradle.kts</code> scripts that you wrote. These scripts themselves declare plugins:</p>
<pre><code class="lang-plaintext">// build.gradle.kts
plugins {
  id("org.jetbrains.kotlin.jvm").version("1.5.30")
  id("com.example").version("1.0")
}
</code></pre>
<p>Let's also assume that the <code>com.example</code> plugin uses <code>kotlin-stdlib:1.5</code> and uses some new 1.5 APIs like <a target="_blank" href="https://kotlinlang.org/docs/whatsnew15.html#stable-locale-agnostic-api-for-upper-lowercasing-text"><code>lowercase</code></a>.</p>
<p>Gradle needs to compile these scripts using the 1.4 embedded Kotlin compiler, run them, resolve plugins and their dependencies and put them in the buildscript classpath. So the build environment will be like:</p>
<pre><code class="lang-plaintext">+--- org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.30
|    +--- org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.5.30
|    |    \--- org.jetbrains.kotlin:kotlin-stdlib:1.5.30 -&gt; 1.5.31
|    |         +--- org.jetbrains:annotations:13.0
|    |         \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31
+--- com.example:plugin:1.0
|    +--- org.jetbrains.kotlin:kotlin-stdlib:1.5.31
</code></pre>
<p>Now in order to run the dependency resolution, and because the scripts themselves are written in Kotlin, Gradle needs a stdlib even before it starts the build [^1].</p>
<p>This is were the chicken and egg problem kicks in. It's too early for Gradle to know the build will ultimately require 1.5 so Gradle puts the version it knows about in the classpath. That's version 1.4 💥.</p>
<p>When our <code>com.example</code> plugin runs, it will crash because <code>lowercase</code> doesn't exist in 1.4 [^2]</p>
<p>Even if it doesn't crash, chances are that you will see a lot of these lines:</p>
<pre><code class="lang-plaintext">w: Runtime JAR files in the classpath should have the same version. These files were found in the classpath:
    /home/runner/.gradle/wrapper/dists/gradle-7.0-all/9m115ut5nwvtxli7nys8pggfr/gradle-7.0/lib/kotlin-stdlib-1.4.31.jar (version 1.4)
    /home/runner/.gradle/wrapper/dists/gradle-7.0-all/9m115ut5nwvtxli7nys8pggfr/gradle-7.0/lib/kotlin-stdlib-common-1.4.31.jar (version 1.4)
...
</code></pre>
<p>This problem is discussed at length in <a target="_blank" href="https://github.com/gradle/gradle/issues/16345">Github issue #16345</a>. There are multiple workarounds or fixes. Let's go over a few of them and see how relocation can help.</p>
<h2 id="heading-solution-1-dont-use-kotlin-15-in-plugins">Solution #1: Don't use Kotlin 1.5 in plugins!</h2>
<p>After all, it's not such a huge deal, right? We've lived without <code>lowercase</code> for years so we can wait a bit more. What's more, the Kotlin compiler supports <code>apiVersion</code>. If you're a plugin author, you can make sure your bytecode doesn't use any 1.5 APIs:</p>
<pre><code class="lang-plaintext">compileKotlin {
  kotlinOptions {
    // Compile against 1.4 stdlib for the time being to make sure it works 
    // with a wide range of Gradle versions.
    apiVersion = "1.4"
  }
}
</code></pre>
<p>That works but not being able to use the latest APIs feels bad. What's even worse is that <code>apiVersion</code> only works for your code, not for dependencies of your plugin. If you want to use okio in your Gradle plugins, <a target="_blank" href="https://github.com/square/okio/pull/960">you're out of luck</a> with this solution.</p>
<h2 id="heading-solution-2-use-gradle-workers-api">Solution #2: Use Gradle Worker's API</h2>
<p>For plugins, Gradle exposes the <a target="_blank" href="https://docs.gradle.org/current/userguide/worker_api.html">Worker API</a>. Especially, it has a <a target="_blank" href="https://docs.gradle.org/current/userguide/worker_api.html#changing_the_isolation_mode">classloaderIsolation</a> mode that allows to isolate the plugin's classloader from the Gradle classloader.</p>
<p>It's a bit more work because you'll have to pass parameters to the worker but it gives full flexibility about the classpath. I'm still not 100% clear how much this is a rock solid solution given that the public API (extensions, tasks, etc...) will also use Kotlin and this cannot be moved to a worker. Maybe if you make sure the public API is simple enough to not use any new API and keep the tasks implementations for 1.5... I'd be curious if anyone has had any success with this.</p>
<h2 id="heading-solution-3-always-update-gradle-to-the-latest-version">Solution #3: Always update Gradle to the latest version</h2>
<p>Given that Gradle is relatively fast to update the embedded version of Kotlin, you can wait until it's updated before using it in your plugin. That's always too much waiting but it can be acceptable in most cases. Of course, that also means any user of your plugin is also on this fast update path, which might or might not be acceptable.</p>
<h2 id="heading-solution-4-relocation">Solution #4: Relocation!</h2>
<p>Finally, the solution that should work in all cases!</p>
<p>No need to wait or change your plugin code, just ship your own kotlin-stdlib as part of your plugin jar. As a nice bonus, that even fixes other issues with <a target="_blank" href="https://github.com/gradle/gradle/issues/8301">buildSrc</a> or <a target="_blank" href="https://github.com/apollographql/apollo-android/issues/2939">classloaders</a>. Sounds simple, right? Well... let's see what it takes to relocate the <code>kotlin-stdlib</code></p>
<h3 id="heading-relocating-with-shadow">Relocating with Shadow?</h3>
<p>The <a target="_blank" href="https://imperceptiblethoughts.com/shadow/">Gradle Shadow Plugin</a> has been used to relocate countless Gradle plugins like <a target="_blank" href="https://www.alecstrong.com/posts/shading/">SQLDelight</a>. It's working well most of the time. Unfortunately, when it comes to Kotlin, there are some details that make it harder to use. The biggest issue is that Shadow uses <a target="_blank" href="https://maven.apache.org/plugins/maven-shade-plugin/">MavenShade</a> under the hood and this <a target="_blank" href="https://github.com/johnrengelman/shadow/issues/232">relocates constant strings that shouldn't be</a>. In practice, using it to relocate <code>kotlin</code> will transform code like this:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// Get the Kotlin plugin extension (to get sourceSets or anything else)</span>
project.extensions.getByName(<span class="hljs-string">"kotlin"</span>)
</code></pre>
<p>into code like that:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// Yikes, there's no "com.your.plugin.relocated.kotlin" extension :-(</span>
project.extensions.getByName(<span class="hljs-string">"com.your.plugin.relocated.kotlin"</span>)
</code></pre>
<p>This is pretty unexpected and breaks most of plugins interacting with the Kotlin plugin.</p>
<p>For plugins that generate source code and contain a lot of package names, this might require even weirder <a target="_blank" href="https://github.com/apollographql/apollo-android/blob/f72c3afd17655591aca90a6a118dbb7be9c50920/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/codegen/kotlin/OkioJavaTypeName.kt#L19">workarounds</a>.</p>
<h3 id="heading-using-r8">Using R8</h3>
<p><a target="_blank" href="https://r8.googlesource.com/r8">R8</a> is Google's replacement for Proguard. While R8 is mainly used for Android, it can be used with any jar file too, <a target="_blank" href="https://jakewharton.com/shrinking-a-kotlin-binary/">including Kotlin</a>. By using <a target="_blank" href="https://www.guardsquare.com/manual/configuration/usage">proguard rules</a>, we get a lot more control about the relocation, what is kept and what is not.</p>
<p>Unfortunately, R8 is not super easy to consume. It is only published in minified form at maven.google.com. I wrote <a target="_blank" href="https://github.com/martinbonnin/kscripts/blob/master/r8_release.main.kts">a small Kotlin script</a> to publish the non-minimized artifacts to <a target="_blank" href="https://repo1.maven.org/maven2/net/mbonnin/r8/r8/">Maven Central</a>. I also wrote a small plugin to make it easier to use R8 with Gradle. It's named GR8 because it's great and you can find it on Github at https://github.com/GradleUp/gr8.<br />(Edit: you can also call R8 directly from your Gradle scripts if you prefer as shown in <a target="_blank" href="https://github.com/ephemient/utf-8b/blob/e44560e3b24a24c48942ecc59d84e5fdc6c547a0/build.gradle.kts#L62">this repo</a> from <code>@ephemient</code> )</p>
<p>The rest of this article goes through the process of setup up GR8 for a fictional <code>com.example</code> plugin using Kotlin 1.5.</p>
<h3 id="heading-applying-the-gr8-plugin">Applying the <code>GR8</code> plugin</h3>
<p>Applying the <a target="_blank" href="https://github.com/GradleUp/gr8">GR8 plugin</a> is similar to any other plugins:</p>
<pre><code class="lang-kotlin">plugins {
  id(<span class="hljs-string">"org.jetbrains.kotlin.jvm"</span>).version(kotlinVersion)
  id(<span class="hljs-string">"java-gradle-plugin"</span>)
  <span class="hljs-comment">// You can use `kotlin-dsl`too, it's going to set apiLevel="1.4" automatically</span>
  id(<span class="hljs-string">"kotlin-dsl"</span>)
  id(<span class="hljs-string">"com.gradleup.gr8-plugin"</span>).version(<span class="hljs-string">"0.2"</span>)
}
</code></pre>
<p>Create a <code>"shade"</code> configuration that will contain all the dependencies to shadow (including <code>kotlin-stdlib.jar</code>):</p>
<pre><code class="lang-plaintext">// Configuration dependencies that will be shadowed
val shadeConfiguration = configurations.create("shade")
</code></pre>
<p>Declare your dependencies:</p>
<pre><code class="lang-kotlin">dependencies {
  <span class="hljs-comment">// no need to specify the version, this will use the same version as the kotlin plugin version </span>
  add(<span class="hljs-string">"shade"</span>, <span class="hljs-string">"org.jetbrains.kotlin:kotlin-stdlib"</span>)
  <span class="hljs-comment">// add your other dependencies here</span>
  <span class="hljs-comment">// Dependencies can use whatever version of Kotlin they want \o/</span>
  add(<span class="hljs-string">"shade"</span>, <span class="hljs-string">"com.squareup.okio:okio:3.0.0"</span>)
  <span class="hljs-comment">// ...</span>

  <span class="hljs-comment">// Add gradleApi() as a compile-only dependency, not shadowed</span>
  compileOnly(gradleApi())
  <span class="hljs-comment">// Alternatively, you can use Nokee distributions</span>
  compileOnly(<span class="hljs-string">"dev.gradleplugins:gradle-api:7.2"</span>)
}
</code></pre>
<p>Don't forget to remove <code>kotlin-stdlib</code> from the default dependencies to avoid having it in the pom file/Gradle module file. In your <code>gradle.properties</code> file, add:</p>
<pre><code class="lang-plaintext">kotlin.stdlib.default.dependency=false
</code></pre>
<p>(See <a target="_blank" href="https://kotlinlang.org/docs/gradle.html#dependency-on-the-standard-library">the Kotlin docs</a> for more details about <code>kotlin.stdlib.default.dependency</code>)</p>
<p>Now configure the <code>GR8</code> plugin:</p>
<pre><code class="lang-kotlin">gr8 {
  <span class="hljs-keyword">val</span> shadowedJar = create(<span class="hljs-string">"shadow"</span>) {
    configuration(<span class="hljs-string">"shade"</span>)

    <span class="hljs-comment">// The R8 configuration</span>
    proguardFile(<span class="hljs-string">"rules.pro"</span>)

    <span class="hljs-comment">// Remove proguard rules from dependencies, we'll manage them ourselves</span>
    exclude(<span class="hljs-string">"META-INF/proguard/.*"</span>)
  }

  <span class="hljs-comment">// If you're using the `java-gradle-plugin` plugin. It will add `gradleApi` to the API configuration</span>
  <span class="hljs-comment">// We don't want that, we want to control what's going out</span>
  removeGradleApiFromApi()

  <span class="hljs-comment">// Needed for the plugin to compile</span>
  configurations.named(<span class="hljs-string">"compileOnly"</span>).configure {
    extendsFrom(shadeConfiguration)
  }
  <span class="hljs-comment">// Needed for the tests to compile</span>
  configurations.named(<span class="hljs-string">"testImplementation"</span>).configure {
    extendsFrom(shadeConfiguration)
  }

  <span class="hljs-comment">// When publishing, publish the shadowed Jar</span>
  replaceOutgoingJar(shadowedJar)
}
</code></pre>
<p>That's it for the Gradle configuration! Now you need to tell R8 what to keep and what to relocate. This is done using <code>rules.pro</code></p>
<h2 id="heading-configuring-r8-rules">Configuring R8 rules</h2>
<p>This is where things begin to be project specific. If you use reflection or other dynamic features, you might need to fine tune the rules. As a rule of thumbs though, you will always need to keep your plugin public API:</p>
<pre><code class="lang-plaintext"># Keep the API as it's used from build scripts
-keep class com.example.gradle.api.** { *; }
-keep interface com.example.gradle.api.** { *; }
-keep enum com.example.gradle.api.** { *; }
</code></pre>
<p>Then tell R8 to repackage (relocate) classes:</p>
<pre><code class="lang-plaintext">-repackageclasses com.example.relocated

# Help the relocation process by allowing to change the visibility of some members
-allowaccessmodification
</code></pre>
<p>Some helpful options:</p>
<pre><code class="lang-plaintext"># Ignore warnings for all the compileOnly dependencies that we didn't pass to R8
-ignorewarnings

# Makes it easier to debug on MacOS case-insensitive filesystem when unzipping the jars
-dontusemixedcaseclassnames

# Keep annotation and other things (might be overkeeping a bit but that's without consequences on the relocation itself)
-keepattributes Signature,Exceptions,*Annotation*,InnerClasses,PermittedSubclasses,EnclosingMethod,Deprecated,SourceFile,LineNumberTable
</code></pre>
<p>That's the minimal set of rules. Depending on what your plugin uses, you will most likely need some other ones.</p>
<h3 id="heading-a-note-about-kotlinmetadata">A note about <code>kotlin.Metadata</code></h3>
<p>An interesting question is whether to relocate <code>kotlin.Metadata</code> or not. The compiler uses <code>kotlin.Metadata</code> at compile time. That enables a lot of Kotlin-only features like extension functions, top-level functions, named parameters, default parameters, typealiases, properties, etc... If you relocate them, the compiler will miss a lot of information and fail to compile for things like top-level functions:</p>
<pre><code class="lang-plaintext">// build.gradle.kts
import com.example.gradle.api.someTopLevelFunction

// Unresolved reference: someTopLevelFunction
someTopLevelFunction()

// Default parameters, type aliases, etc... also won't compile
</code></pre>
<p>If your public API uses a lot of Kotlin features, and if you want your users to be able to use them, keep <code>kotlin.Metadata</code>:</p>
<pre><code class="lang-plaintext">// Keep metadata used by the compiler
-keep class kotlin.Metadata { *; }
</code></pre>
<p>Of course, that's taking the risk that a future version of kotlin changes the definition of <code>kotlin.Metadata</code> and overrides that annotation. I'm not 100% sure what would happen there but hopefully it shouldn't happen too much. And if it does happen, most likely other things will break.</p>
<p>If you're keeping <code>kotlin.Metadata</code>, you'll also need to keep <code>kotlin.Unit</code>:</p>
<pre><code class="lang-plaintext">// The compiler also uses Unit
-keep class kotlin.Unit { *; }
</code></pre>
<p><code>kotlin.Unit</code> is a special value to the Kotlin compiler and if you relocated it to say, <code>com.example.relocated.aa</code>, a runtime error will happen on void methods because the bytecode references a method that returns <code>com.example.relocated.aa</code> instead.</p>
<h3 id="heading-profit">Profit!</h3>
<p>Run <code>./gradlew assemble</code> to build the shadowed jar. It will be available in</p>
<pre><code class="lang-plaintext">build/gr8/shadow/projectName-version-shadowed.jar
</code></pre>
<p>In order to verify that your classes were repackaged correctly, unzip the jar. Most of the classes should be under <code>com/example/relocated</code>. If not, iterate on the <code>rules.conf</code> to allow more relocations. Make sure to also test your plugin to make sure runtime reflexive accesses are still working.</p>
<p>If everything works, congrats! You can now use Kotlin 1.5 and all its APIs in your Gradle plugin! To see it in action, check <a target="_blank" href="https://github.com/apollographql/apollo-android/pull/3542/">the matching PR in apollo-android</a></p>
<h3 id="heading-should-i-use-this-in-production">Should I use this in production?</h3>
<p>That's always the million dollar question. I'll give the usual answer of "it depends" 🙃. This is all wildly experimental so come prepared for some hickups. Relocation is always fragile as you don't find issues until runtime. Also, running R8 takes some time. If you need to do this while debugging, that can become annoying.</p>
<p>All that being said, if you have a good test suite and it's working for you, you can actually make it a lot easier to consume your plugins.</p>
<h2 id="heading-moving-forward">Moving forward</h2>
<p>We've seen that R8 gives you a lot of flexibility to shadow, and even shrink/optimize your Gradle plugins. It has a lot of advantages like avoiding dependencies conflicts, allow to use newer Kotlin APIs as well as making self-contained Gradle plugins. It is an effective solution to shipping plugins that use latest Kotlin APIs today. It also has drawbacks as the configuration is not straightforward and reflective accesses require extra care.</p>
<p>Another important drawback is that this requires more resources. If every plugin starts shadowing all their dependencies, it means loading the Kotlin stdlib N times, loading okio P times, etc... That's a lot of classes loaded in the JVM. All of these add up and will take more memory and make your builds slower.</p>
<p>As a wrap-up from this long post, please take the time to upvote https://github.com/gradle/gradle/issues/16345 to prioritize removing the Kotlin stdlib fixed runtime limitation in Gradle. If you're a plugin consumer, move <code>buildSrc</code> and rootProjects <code>buildscript {}</code> to <a target="_blank" href="https://developer.squareup.com/blog/herding-elephants/">convention plugins</a> and hopefully one day we'll have Gradle plugin that can share their dependencies \o/.</p>
<p>Thanks for making it through! Have a wonderful day/evening/night!</p>
<p>[^1]: Actually that's not entirely true as the <code>plugins {}</code> block is a special block that doesn't allow all the Kotlin syntax so maybe it could have a special handling 🤷‍♂️</p>
<p>[^2]: This is not entirely true either 😅. In most cases, the compiler will just optimize the constant value, or use the 1.4 experimental <code>lowercase</code> but that was a nice example for a relatively simple and famous 1.5 API.</p>
<p>🙏 Many Thanks to <a target="_blank" href="https://blog.louiscad.com/">LouisCAD</a> for proofreading this article.</p>
<p>Cover Picture: <a target="_blank" href="https://flic.kr/p/ahAszj">Chiesetta sul Col del Nivolet</a> by <a target="_blank" href="https://www.flickr.com/photos/53191561@N03/">BORGHY52</a></p>
]]></content:encoded></item></channel></rss>