Cédric Champeau (@CedricChampeau) & Louis Jacomet (@ljacomet) - Gradle Inc.
Dependency management team
Including a quick recap of
Build and automation tool with rich dependency management functionality
JVM based and implemented in Java
100% Free Open Source - Apache Standard License 2.0
Agnostic build tool (Java, Groovy, Kotlin, Scala, Android, C, C++, Swift, …)
plugins {
`java-library`
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.slf4j:slf4j-api:1.7.2")
}
Run build with --scan
to create a private scan on scans.gradle.com
Gradle Enterprise (commercial product) can be installed on premises
Identified by group, name and version (e.g. org.slf4j:slf4j-api:1.7.2
)
Consists of artifacts (e.g. .jar
) and metadata (.module
, .pom
or ivy.xml
)
Repositories are visited sequentially
By default transitive dependencies are brought in
dependencies {
implementation("org.apache.httpcomponents:httpclient:4.5.9")
}
Show dependency graph:
gradle dependencies --configuration compileClasspath
compileClasspath - Compile classpath for source set 'main'.
\--- org.apache.httpcomponents:httpclient:4.5.9
+--- org.apache.httpcomponents:httpcore:4.4.11
+--- commons-logging:commons-logging:1.2
\--- commons-codec:commons-codec:1.11
Show path to a dependency:
gradle dependencyInsight
--configuration compileClasspath
--dependency codec
Result:
...
commons-codec:commons-codec:1.11
\--- org.apache.httpcomponents:httpclient:4.5.9
\--- compileClasspath
...
scans.gradle.com (--scan
) or Gradle Enterprise provide a dependency inspector
Introduce additional constraints on versions
Does not override versions of transitive dependencies
dependencies {
constraints {
implementation("org.slf4j:slf4j-api:1.7.26")
implementation("org.apache:commons-lang3:3.3.0")
}
dependencies {
implementation("org.slf4j:slf4j-api") // no version
}
}
Work also with dependency constraints
Does not override versions of transitive dependencies
dependencies {
implementation("info.picocli:picocli") {
version {
strictly("[3.9, 4.0[")
prefer("3.9.5")
}
because("Provides command-line argument parsing")
}
}
plugins {
`java-platform`
}
dependencies {
constraints {
api("org.slf4j:slf4j-api:1.7.26")
runtime("org.slf4j:slf4j-simple:1.7.26")
}
}
dependencies {
api(platform(project(":platform")))
implementation("org.slf4j:slf4j-api") // <-- no version here
}
Gradle has a strong modelling of dependencies:
Semantic difference between compilation and runtime
Semantic difference between building a library and building against a library
Ability for a module to produce more than one variant
Version conflicts
2 or more dependencies want a different version of the same thing
Implementation conflicts
there are more than one implementation of the same thing
There’s a version conflict whenever two components:
depend on the same module D
depend on different versions of module D
For example:
A → D:1.0
B → D:1.1
Maven selects version 20.0
Gradle selects version 25.1-android
Why?
"Nearest" dependency version wins
root
→ guava:20.0
which is closer than root
→ guice
→ guava:25.1-android
So 20.0
wins
Selects guava:25.1-android
Selects guava:20
Nearest first is basically unpredictable for any reasonably sized dependency graph
Adding exclusions
+ first level dependency
leaks dependencies
Adding a dependencyManagement
block
but non transitive!
Performs full conflict resolution
All opinions matter
Highest wins: we always choose 25.1-android whatever the order
dependencies {
constraints {
implementation("com.google.guava:guava:guava:25.1-android")
}
}
are honored transitively
Ivy uses pluggable conflict resolvers
can be different per dependency
hard to reason about
configurations {
compileClasspath {
resolutionStrategy.failOnVersionConflict()
}
}
Forces you to think about conflicts
Requires an explicit way to select a version
Offer one way to solve conflicts
Limited to the configuration being resolved
configurations.all {
resolutionStrategy.eachDependency {
if (requested.group == "org.apache.commons" &&
requested.name == "commons-lang3") {
useVersion("3.3.1")
because("tested with this version")
}
}
}
Sometimes metadata of a module is simply wrong
"hard" dependency when it should be optional
dependency on broken version
missing dependencies
…
Allow fixing metadata
Applied to all configurations (unconditionally true)
fun DependencyHandler.fixBadMetadata() = components {
withModule("org.apache.camel:camel-cdi", FixApacheCamel::class.java)
}
class FixApacheCamel: ComponentMetadataRule {
override fun execute(context: ComponentMetadataContext) = context.details.run {
if (id.version.startsWith("2.15")) {
allVariants {
withDependencies {
removeAll { it.group.contains("deltaspike") }
}
}
}
}
}
A capability describes a feature that is mutually exclusive
All components provide their GAV as a capability by default
Slf4j and is bridging / replacement modules
log4j
vs. slf4j-log4j12
vs. log4j-over-slf4j
And similarly for java.util.logging
And again for commons-logging
asm:asm
now known as org.ow2.asm:asm
junit:junit-dep
and junit:junit
cglib:cglib-nodep
and cglib:cglib
fun DependencyHandler.detectLoggerConflicts() = components {
// both commons-logging and jcl-over-slf4j implement the commons-logging api
withModule("org.slf4j:jcl-over-slf4j", CommonLoggingCapabilities::class.java)
withModule("org.apache.commons:commons-logging", CommonLoggingCapabilities::class.java)
// both slf4j-simple and slf4j-log4j12 implement the slf4j api
withModule("org.slf4j:slf4j-simple", Slf4jImplementationCapabilities::class.java)
withModule("org.slf4j:slf4j-log4j12", Slf4jImplementationCapabilities::class.java)
// ...
class CommonLoggingCapabilities : ComponentMetadataRule {
override fun execute(context: ComponentMetadataContext) = context.run {
details.allVariants {
withCapabilities {
addCapability("org.apache.commons", "commons-logging-impl", "1.0")
}
}
}
}
class Slf4jImplementationCapabilities : ComponentMetadataRule {
override fun execute(context: ComponentMetadataContext) = context.run {
details.allVariants {
withCapabilities {
addCapability("org.slf4j", "slf4j-impl", "1.0")
}
}
}
}
Uses dependency substitution
This effectively replaces one module with another
fun Configuration.preferSlf4JSimple() = resolutionStrategy.dependencySubstitution {
substitute(module("org.slf4j:slf4j-log4j12"))
.because("prefer slf4j-simple over other implementations")
.with(moduleInGraph("org.slf4j:slf4j-simple"))
substitute(module("log4j:log4j"))
.because("use the slf4j bridge for log4j")
.with(module("org.slf4j:log4j-over-slf4j:1.7.16"))
}
fun Configuration.useSlf4jEverywhere() = resolutionStrategy.eachDependency {
if (requested.name == "commons-logging") {
useTarget("org.slf4j:jcl-over-slf4j:0")
}
}
An option for consumers to prune transitive sections of the graph
Maven and Gradle have different handling of exclusions
exclusion
With Maven if a dependency is excluded on a given edge, it is excluded for the whole graph
C
is excluded in Maven
exclude
With Gradle all paths need to exclude for a dependency to be excluded
C
is excluded
exclude
A dependency is many times and deep into the dependency graph and you don’t need it
Exclude it on all paths
configurations.all {
exclude group:'commons-logging', module:'commons-logging'
}
However:
Leaks to downstream consumers
Even worse: added to all first level dependencies at publication
Semantics should be: "I don’t use this library that this module says it needs"
Should be limited to cases where the module actually needs it but your execution paths don’t
implementation("org.apache.spark:spark-core_2.12:2.4.3") {
withoutFeatureWhichRequires("org.apache.ivy", "ivy")
}
// ...
fun ModuleDependency.withoutFeatureWhichRequires(group: String, module: String) {
exclude(mapOf("group" to group, "module" to module))
because("we don't use the features of ${name} which require ${group}:${module}")
}
forcing
Forcing dependencies is not recommended
Behavior can be non reproducible (ordering issues in case of competing forces)
Use it only if you don’t have consumers
But prefer resolution rules
Sometimes you don’t care about transitivity
e.g: download a set of files
configurations {
myFiles {
transitive = false
}
}
// ...
tasks {
downloadFiles {
from configurations["myFiles"]
}
}
2 categories of consumers
libraries: can be consumed, including transitively
applications: at the end of the chain
Wrong decisions on libraries affect the whole chain
e.g: excluding a dependency
e.g: forcing a version
e.g: adding a constraint
e.g: using ranges
Dependencies do not work in isolation
Most projects will use more than a couple
Having a consistent set of dependencies is a different problem than having the right dependency at the right version
Organizations may want to
Share information on recommended dependencies, including versions and rejections
Control available dependencies and their origin
Repository content filtering
Bill Of Materials (BOM) / Recommendation platforms
Alignment of library modules
and more
Identified by group, name and version (e.g. org.slf4j:slf4j-api:1.7.2
)
Consists of artifacts (e.g. .jar
) and metadata (.module
, .pom
or ivy.xml
)
Repositories are visited sequentially
Gives control to where dependencies can be sourced from
Improves the reproducibility aspect
Has security implications as well
repositories {
maven {
url = 'https://repo.company.com/'
content {
includeGroupByRegex "my\\.company.*"
}
}
jcenter() {
content {
excludeGroup "org.slf4j"
excludeGroupByRegex "my\\.company.*"
}
}
}
Defines versions for dependencies (and more)
Used for obtaining a consistent set of versions
For modules composing a library, that work better together at the same version
For heterogeneous modules that form an ecosystem
Allows to omit versions in the build file that imports it
dependencies {
implementation(
platform(
'org.springframework.boot:spring-boot-dependencies:2.1.6.RELEASE'
)
)
}
BOM dependency declarations are imported as constraints
BOMs are like regular dependencies, they can be visible by consumers
When declared in api
for example
Gradle always considers all versions in a dependency graph
This creates an important difference with application of BOMs in Maven:
In Gradle, the transitive dependency graph can still resolve a dependency with a different version than specified in the BOM. |
dependencies {
implementation(
enforcedPlatform(
'org.springframework.boot:spring-boot-dependencies:2.1.6.RELEASE'
)
)
}
Will result in constraints that force versions instead of participating in version conflict resolution.
If you have consumers, you should not use this.
✅ for most use cases
Unsupported scenarios:
If you want to override properties defined in the BOM to have a different set of constraints
Local dependency declaration causing a downgrade
By using the java-platform
plugin
Full feature set requires Gradle Module Metadata publication
Will translate to a Maven BOM for the most part
⚠️ In Maven order matters, so order in your Gradle build file matters if you need the compatibility
plugins {
`java-platform`
}
javaPlatform {
allowDependencies()
}
dependencies {
constraints {
runtime('org.postgresql:postgresql:42.2.6')
}
api(platform('com.fasterxml.jackson:jackson-bom:2.9.9'))
api(platform('org.springframework.boot:spring-boot-dependencies:2.1.5.RELEASE'))
}
A group of dependencies are designed to work together.
Think jackson
or even the Spring framework
Through transitive dependency updates, modules end up having different versions
So the goal would be to have the tools to make sure that any upgrade of any module would cause the whole set to upgrade its version.
Name a virtual platform module
Enhance library modules metadata to declare they belongsTo
the platform
The platform collects the modules that belongs to it
When jackson-dataformat-yaml
says it belongs to the platform, all platform versions now point to it as well
Alignment!
A virtual platform can only align to a version it sees in the graph
A virtual platform will attempt alignment on all modules
com.fasterxml.jackson.core:jackson-databind:2.8.8.1
✅
com.fasterxml.jackson.core:jackson-core:2.8.8.1
❌
The platform will align modules existing in 2.8.8.1
to that version,
Other modules will have the highest lower version that exists for them
Saw 2.8.8.1
, 2.8.8
, … then others most likely in 2.8.8
Saw 2.8.8.1
, 2.7.8
, … then others mostl likely in 2.7.8
Yes, but
You still need the belongsTo
Alignment will be according to the BOM definitions
Leverages the ability to declare the cycle in Gradle
A platform depends on all the modules
Each modules depends on the platform
Requires Gradle Module Metadata to be fully functional
plugins {
`java-platform`
}
dependencies {
constraints {
api(project(':common'))
api(project(':server'))
api(project(':client'))
}
}
For the projects:
dependencies {
api(project(':platform'))
}
Real world projects will face dependency conflicts
Gradle provides tools to model conflicts
not all conflicts are equal (version, implementation, …)
better modeling → better decisions
Sometimes a hammer is an acceptable solution
We’re adding tooling to cover more cases!
Focus on publishing and the importance of Gradle Module Metadata
Publishing components
Maven and Ivy compatibility
Published capabilities
Published alignment
Feature variants
Test fixtures