Dependency Analysis Gradle Plugin: What's an ABI?

autonomousapps

Tony Robalik

Posted on August 27, 2020

Dependency Analysis Gradle Plugin: What's an ABI?

(With thanks to Jack Douglass on Unsplash for the cover image)

Welcome to my series on dependency analysis with the Dependency Analysis Gradle Plugin. This post is intended as a direct follow-up to the prior post in this series, which discussed detecting unused dependencies.

What is an ABI?

A library’s ABI, or application binary interface, is what consumers compile against (not what you code against!). I like to think of it as a library’s binary API. Knowledge of the ABI is what enables Gradle features such as incremental compilation and compilation avoidance. You can think of a library’s ABI as the collection of its public interfaces and the dependencies required to compile against them: public methods and their return and parameter types, and public fields / properties. To understand how this relates to dependency analysis, consider the following:

fun newOkHttpClient(gson: Gson): OkHttpClient {  }
Enter fullscreen mode Exit fullscreen mode

This is a public function that takes a Gson instance as a parameter and returns an instance of an OkHttpClient. This means that both Gson and OkHttp are part of your ABI! In Gradle terms, that means you need the following in your build script:

dependencies {
  api "com.google.code.gson:gson:2.8.6"
  api "com.squareup.okhttp3:okhttp:4.8.0"
}
Enter fullscreen mode Exit fullscreen mode

If instead of api you used implementation, your project would still compile, but you’d be creating trouble for downstream consumers. Without those libraries on their compile classpath, they will not be able to compile. It is critically important that library authors get this right. Please see the Gradle documentation for an in-depth discussion of this separation.

The ABI of a library is determined with the help of the AbiAnalysisTask. It delegates to abiDependencies and PublicApiDump, the latter of which was pulled directly from a JetBrains repository. It produces two outputs: one is JSON and is meant for use by other tasks involved in dependency analysis, and the second is a more human-readable plaintext output. Excerpts of each follow (where <variant> would be debug (etc.) for an Android project and main for a JVM project):

json: build/reports/dependency-analysis/<variant>/intermediates/abi.json

[
  {
    "identifier": ":db",
    "configurationName": "implementation"
  },
  {
    "identifier": "androidx.appcompat:appcompat",
    "resolvedVersion": "1.1.0-rc01",
    "configurationName": "implementation"
  }
]
Enter fullscreen mode Exit fullscreen mode

(The above indicates that the project :db and the external dependency androidx.appcompat:appcompat are both part of this project's ABI, and are currently declared — incorrectly — on "implementation".)

plaintext: build/reports/dependency-analysis/<variant>/intermediates/abi-dump.txt

public abstract class com/seattleshelter/core/base/BaseActivity : androidx/appcompat/app/AppCompatActivity {
  public fun <init> ()V
  protected fun onCreate (Landroid/os/Bundle;)V
}
Enter fullscreen mode Exit fullscreen mode

(The above indicates that the public BaseActivity class of the project-under-analysis extends AppCompatActivity and has a function, onCreate, that takes a Bundle as a parameter.)

The plugin uses this information to advise users on how best to declare their project’s dependencies.

Case study

I was recently updating some libraries my team publishes for internal consumption. I ran buildHealth over them and followed the advice. In particular, I did this:

a git diff showing a change from implementation to api for RxJava and Dagger2

(Note: no version numbers because I use the Java Platform plugin.)

I filed a PR. A reviewer asked, entirely reasonably, why I was changing Dagger and RxJava from implementation to api. Where were they being exposed? Here's what I told him (with some paths and class names changed for Reasons):

If you run ./gradlew buildHealth on this project, and then open up hello/build/reports/dependency-analysis/debug/intermediates/abi-dump.txt, you'll find 24 usages of io/reactivex (from RxJava2). Here's one example:

public final class com/hello/MyService {
    public final fun disableService (Landroid/content/Context;)Lio/reactivex/Completable;
Enter fullscreen mode Exit fullscreen mode

Here we have a public class with a public function that returns a Completable. Therefore RxJava2 is part of this project's ABI.

I see 74 results for dagger. Here's one of them:

public final class com/hello/dao/DbOp {
    public fun <init> (Landroid/content/Context;Ldagger/Lazy;)V
Enter fullscreen mode Exit fullscreen mode

And here is a public class whose public constructor takes a Dagger.Lazy as a parameter. So, Dagger is also part of the ABI.

I've had to do this enough — that is, get surprised at my own plugin's results, tediously verify them manually, finally follow the advice — that now I just skip the first two steps and follow the advice. This isn't to say the plugin is always right — there are annoying corner cases that I haven't resolved yet — but it's right more than 99% of the time.[verification needed] That said, I'm glad it's possible to verify the results manually, albeit tediously.

I'm hoping the tedious part disappears soon, with an exciting new feature I'm working on called "provenance" that will automate this explanation process. Here's sample output from using this draft feature on another project

$ ./gradlew db:reasonDebug --id io.reactivex.rxjava2:rxjava
Enter fullscreen mode Exit fullscreen mode
You asked about the dependency io.reactivex.rxjava2:rxjava. You have been advised to add this dependency.

Shortest path to io.reactivex.rxjava2:rxjava from the current project:
:db
\--- androidx.room:room-rxjava2
     \--- io.reactivex.rxjava2:rxjava

Dependency io.reactivex.rxjava2:rxjava provides the following:
- 1651 classes
- 141 public constants

And this project exposes the following classes provided by this dependency:
- io.reactivex.Flowable

Please see abi-dump.txt for more information.
Enter fullscreen mode Exit fullscreen mode

🎉

Conclusion

In this post, we've learned what an ABI is, how it relates to dependency analysis, and how to verify whether the Dependency Analysis Gradle Plugin is emitting accurate advice on the subject. Please join me next time, when I hope to talk about the basics of using ANTLR for source-code parsing.

💖 💪 🙅 🚩
autonomousapps
Tony Robalik

Posted on August 27, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related