Thought I would reach out to the Gradle community because I have been stuck on what seems to be a build issue for a few days now. I have a Gradle project that is acting as a monorepo for various android libraries.
In my root settings.gradle file, I use includeBuild to include the projects of interest. This works and I see that the builds have been included. Each one of these android libraries may contain a dependency on another module in a different project in the monorepo. All of these libraries produce artifacts that are published, so normally I would just target the recently released artifact. I want to use includeBuild with dependency substitution when developing locally. This way, if I make a change in a dependency I can make changes in all dependents immediately without having to release an artifact first.
Let me show you an example of one that I have working:
I have directory called base-implementation, this directory contains two gradle projects: base-api and base-ui-api. Each of these projects contains one singular android library module api and ui-api respectively. To further illustrate, one drilldown from a package structure would go base-implementation → base-api → api where api may be defined as an artifact dependency in other android libraries in this monorepo. For example ui-api defines a dependency on api.
Given this structure, in my base-ui-api project, I define an includeBuild on base-api and substitute out the dependency with the android library api. That looks like this:
def apiDependency = "com.myapp.example:api"
if(isIntegrationBuild.toBoolean()) {
includeBuild('../base-api') {
dependencySubstitution {
substitute module(apiDependency) using project(':api')
}
}
}
The isIntegrationBuild is just a gradle property I have set up as a development flag to use dependency substitution. I can run the gradle task provided by android called androidDependencies which will list all resolved dependencies for a given build. I can verify here that the dependency is indeed subbed out in ui-api by confirming this line “:api (variant: debug)”
It is important to note at this point, every single android library in this mono repo has only two build variants of debug and release there are no inconsistencies here with build variants.
The problem I am running into is this. I have a library that defines a dependency on another library which then defines a dependency on api. When going to build this library, a random nullPointerException is thrown without any message. What I have noticed however, is this; When I run androidDependencies task on the library that it is dependent on (the library that contains a dependency on api), the subbed out api dependency comes back with this “:api (variant: null)” and I cannot for the life of me figure out why. All includeBuilds and dependency substitution definitions are exactly the same. yet one resolves with variant debug and the other does not. They have next to identical build.gradle files at both the project and module level as well. There is nothing special happening to explicitly define a default variant implementation and all variants match across all libraries.
If what I explained above is unclear, let me drive home the issue with the project structure visual that doesn’t work. There is sensitive information in these library names so for sake of censoring, I will replace their names with A and B. Where A depends on B and B depends on api. A and B also share the same root dir but this root dir is just a container. Not a gradle project.
A depends on B so in the project level settings.gradle of A I define an includeBuild with a dependency substitution for B. This looks like the following:
def BDependency = "com.example.b:b"
if (isIntegrationBuild.toBoolean()) {
includeBuild('../project-b') {
dependencySubstitution {
substitute module(BDependency) using project(':b')
}
}
}
This block runs and works, I can see and confirm that the artifact is swapped out with the included build.
Now, B defines a dependency on api so its project level settings.gradle looks almost identical to the first case I stated where ui-api depends on api the only difference is relative pathing for includeBuild. This looks like the following:
def apiDependency = "com.myapp.example:api"
if(isIntegrationBuild.toBoolean()) {
includeBuild('../../base-api') {
dependencySubstitution {
substitute module(apiDependency) using project(':api')
}
}
}
Why is that when I build ui-api it pulls in api of variant debug, but when I build B it pulls in api of variant null? Is there a something I am missing about nested includedBuilds?