How can I specify dependency versions once and reuse them in a multi module project?

Hi

In a project structure like this:

project
├── gradle.settings
├── module1
│   └── build.gradle
├── module2
│   └── build.gradle
├── module3
│   └── build.gradle
├── module4
│   └── build.gradle
├── module5
│   └── build.gradle
└── module6
    └── build.gradle
  1. What is the best/a good practice for declaring version of dependencies in one location and in modules that require that dependency reference it like "commons-io:commons-io:${commonsIoVersion}"? I thought I might be able to declare these dependencies by adding a gradle.properties file under project/ and defining the versions there, but looks like the purpose of this file is to fine tune and configure the build environment and not the right place to store dependency versions.
  2. Is there a way to accomplish this but keeping the modules decoupled?

Thank you.

Assuming you have something like this:

include 'module1', 'module12', ... , 'moduleN'

… in the settings.gradle of the root project. Then adding something like this to the root’s build.gradle:

   ...
   subprojects {
       ...
       ext{
          artifactVersion = "99.9"
       }
       ...
   }
   ...

…you should be able to do something like this in subprojects modules1moduleN:

dependencies{
    ...
    implementation "com.example:foo:${artifactVersion}"
    ...
}

If that’s not the answer you’re after, please share with us the solution you eventually find? Please?

1 Like

We had the same problem with many multiple-module projects that needed coordinated 3pp dependency versions.
We made a separate file in the root of the project, dependencies.gradle:

ext.libraries = [ // Groovy map literal
// apache commons
commons_beanutils:			'commons-beanutils:commons-beanutils:1.9.3',
commons_compress: 			'org.apache.commons:commons-compress:1.12',
]

ext.test3ppLibraries = [
junit:						'junit:junit:4.+',
junit_params:				'pl.pragmatists:JUnitParams:1.0.5',
]

In the build.gradle of the project:

apply from: file("${rootDir}/dependencies.gradle")
[...]
dependencies {
    testCompile (test3ppLibraries.junit)
}

The fancy part is next: We publish that dependencies.gradle to a maven repo, then we can apply it to other projects in the department:

 apply from: "https://<our-artifactory-server>/artifactory/<our-project"/com/blah/scripts/dependencies.gradle"
2 Likes

If I am not mistaken, subprojects {}, will couple the sub modules and preferably I want to avoid that.

Having said that looks like the properties defined inside an ext {} block in the root build.gradle file will be available to the sub modules so for the time being I have taken this route:

// project/build.gradle

ext {
     commonsIoVersion = '2.5'
}

// project/module1/build.gradle

dependencies {
    implementation "commons-io:commons-io:${commonsIoVersion}"
}

But I think I eventually have to use your suggested approach (subprojects {}, etc. ) at the cost of coupling the modules.

Thanks.

1 Like

This is neat, but I prefer not to rely on unconventional techniques. Is this a common technique?

Looks like apply from: is meant to be used for loading script plugins.

Thanks.

1 Like

I don’t know how common it is. I found it on StackOverflow, I believe.
It provides us what we needed: only one version of each 3pp library is ever used in the build.

What about the solution looks unconventional to you?

Yes, is is meant for loading script plugins. dependencies.gradle is a script plugin in this regard.

You can think of apply from: like import in most languages, where it just inlines that script into this one. I’m sure that’s technically not what happens, but it has the same effect, assuming you don’t do something silly like using it in the buildscript closure.

1 Like

Clever. But what happens if you do this:

// project/build.gradle
/*
ext {
     commonsIoVersion = '2.5'
}*/

…and you don’t change anything in the sub-modules? Then the sub-modules’ builds will fail. Won’t they?

So doesn’t that mean that they’re still coupled?

I think the approach you’ve chosen is semantically exactly the same approach that I suggested.

The only difference is the first approach expresses the intention explicitly. Whereas the second approach could be prone to somebody removing that ext block from the base project; not knowing that the sub-modules need it to be there for their builds to succeed.

In my opinion, the subprojects{} block makes it obvious to everybody that if the stuff inside it is fooled with, it will have a knock-on effect.

Do you see what I mean?

1 Like

Coupling, as explained in the link that I shared, has a specific meaning, not what the general term “coupling” means:

Gradle allows any project to access any other project during both the configuration and execution phases. While this provides a great deal of power and flexibility to the build author, it also limits the flexibility that Gradle has when building those projects. For instance, this effectively prevents Gradle from correctly building multiple projects in parallel, configuring only a subset of projects, or from substituting a pre-built artifact in place of a project dependency.

Two projects are said to be decoupled if they do not directly access each other’s project model. Decoupled projects may only interact in terms of declared dependencies: project dependencies and/or task dependencies. Any other form of project interaction (i.e. by modifying another project object or by reading a value from another project object) causes the projects to be coupled. The consequence of coupling during the configuration phase is that if gradle is invoked with the ‘configuration on demand’ option, the result of the build can be flawed in several ways. The consequence of coupling during execution phase is that if gradle is invoked with the parallel option, one project task runs too late to influence a task of a project building in parallel. Gradle does not attempt to detect coupling and warn the user, as there are too many possibilities to introduce coupling.

A very common way for projects to be coupled is by using configuration injection. It may not be immediately apparent, but using key Gradle features like the allprojects and subprojects keywords automatically cause your projects to be coupled.

1 Like

Why don’t use Gradle Dependency Constraints as a dependency manager?

dependencies {
    implementation("org.apache.httpcomponents:httpclient")
    constraints {
        implementation("org.apache.httpcomponents:httpclient:4.5.3") {
            because("previous versions have a bug impacting this application")
        }
        implementation("commons-codec:commons-codec:1.11") {
            because("version 1.9 pulled from httpclient has bugs affecting this application")
        }
    }
}

If I understand the issue correctly, it looks like gradle resolved your exact problem in gradle 6.0 with the java-platform plugin. I know your question was before gradle 6, but for those of us coming to this question after gradle 6, check it out: https://gradle.org/whats-new/gradle-6/ https://www.baeldung.com/gradle-6-features