(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 { … }
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"
}
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"
}
]
(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
}
(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:
(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;
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
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
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.
🎉
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.
Top comments (0)