Locking Versions
- Activate locking for specific configurations
- Activate locking for all configurations
- Disable locking for specific configurations
- Activate locking for a buildscript classpath configuration
- Generating and updating dependency locks
- Understanding the locking mechanism during dependency resolution
- Understanding lock state location and format
- Migrating your legacy lockfile
- Configuring the lock file name and location
- Running a build with lock state present
- Updating lock state entries selectively
- Disabling dependency locking
- Ignoring specific dependencies from the lock state
- Understanding locking limitations
Using dynamic dependency versions (e.g., 1.+ or [1.0,2.0)) can cause builds to break unexpectedly because the exact version of a dependency that gets resolved can change over time:
dependencies {
// Depend on the latest 5.x release of Spring available in the searched repositories
implementation("org.springframework:spring-web:5.+")
}
dependencies {
// Depend on the latest 5.x release of Spring available in the searched repositories
implementation 'org.springframework:spring-web:5.+'
}
To ensure reproducible builds, it’s necessary to lock versions of dependencies and their transitive dependencies. This guarantees that a build with the same inputs will always resolve to the same module versions, a process known as dependency locking.
Dependency locking is a process where Gradle saves the resolved versions of dependencies to a lock file, ensuring that subsequent builds use the same dependency versions. This lock state is stored in a file and helps to prevent unexpected changes in the dependency graph.
Dependency locking offers several key advantages:
-
Avoiding Cascading Failures: Teams managing multiple repositories no longer need to rely on
-SNAPSHOTor changing dependencies, which can lead to unexpected failures if a dependency introduces a bug or incompatibility. -
Dynamic Version Flexibility with Stability: Teams that use the latest versions of dependencies can rely on dynamic versions during development and testing phases, locking them only for releases.
-
Publishing Resolved Versions: By combining dependency locking with the practice of publishing resolved versions, dynamic versions are replaced with the actual resolved versions at the time of publication.
-
Optimizing Build Cache Usage: Since dynamic or changing dependencies violate the principle of stable task inputs, locking dependencies ensures that tasks have consistent inputs.
-
Enhanced Development Workflow: Developers can lock dependencies locally for stability while working on a feature or debugging an issue, while CI environments can test the latest
SNAPSHOTor nightly versions to provide early feedback on integration issues. This allows teams to balance stability and early feedback during development.
Activate locking for specific configurations
Locking is enabled per dependency configuration.
Once enabled, you must create an initial lock state, causing Gradle to verify that resolution results do not change. This ensures that if the selected dependencies differ from the locked ones (due to newer versions being available), the build will fail, preventing unexpected version changes.
|
Dependency locking is effective with dynamic versions, but it should not be used with changing versions (e.g., Using dependency locking with changing versions indicates a misunderstanding of these features and can lead to unpredictable results. Gradle will emit a warning when persisting the lock state if changing dependencies are present in the resolution result. |
Locking of a configuration happens through the ResolutionStrategy API:
configurations {
compileClasspath {
resolutionStrategy.activateDependencyLocking()
}
}
configurations {
compileClasspath {
resolutionStrategy.activateDependencyLocking()
}
}
Only configurations that can be resolved will have lock state attached to them. Applying locking on non resolvable-configurations is a no-op.
Activate locking for all configurations
The following locks all configurations:
dependencyLocking {
lockAllConfigurations()
}
dependencyLocking {
lockAllConfigurations()
}
The above will lock all project configurations, but not the buildscript ones.
Disable locking for specific configurations
You can also disable locking on a specific configuration.
This can be useful if a plugin configured locking on all configurations, but you happen to add one that should not be locked:
configurations.compileClasspath {
resolutionStrategy.deactivateDependencyLocking()
}
configurations {
compileClasspath {
resolutionStrategy.deactivateDependencyLocking()
}
}
Activate locking for a buildscript classpath configuration
If you apply plugins to your build, you may want to leverage dependency locking there as well.
To lock the classpath configuration used for script plugins:
buildscript {
configurations.classpath {
resolutionStrategy.activateDependencyLocking()
}
}
buildscript {
configurations.classpath {
resolutionStrategy.activateDependencyLocking()
}
}
Generating and updating dependency locks
To generate or update the lock state, add the --write-locks argument while invoking whatever tasks that would trigger the locked configurations to be resolved:
$ ./gradlew dependencies --write-locks
This will create or update the lock state for each resolved configuration during that build execution. If a lock state already exists, it will be overwritten.
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
com.google.code.findbugs:jsr305:3.0.2=classpath
com.google.errorprone:error_prone_annotations:2.3.2=classpath
com.google.gradle:osdetector-gradle-plugin:1.7.1=classpath
com.google.guava:failureaccess:1.0.1=classpath
com.google.guava:guava:28.1-jre=classpath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=classpath
com.google.j2objc:j2objc-annotations:1.3=classpath
empty=
| Gradle won’t write the lock state to disk if the build fails, preventing the persistence of potentially invalid states. |
Lock all configurations in a single build execution
When working with multiple configurations, you may want to lock them all at once in a single build execution. You have two options for this:
-
Run
gradle dependencies --write-locks:-
This command will lock all resolvable configurations that have locking enabled.
-
In a multi-project setup, note that
dependenciesis executed only on one project, typically the root project.
-
-
Declare a Custom Task to Resolve All Configurations:
-
This approach is particularly useful if you need more control over which configurations are locked.
-
This custom task resolves all configurations, locking them in the process:
tasks.register("resolveAndLockAll") {
notCompatibleWithConfigurationCache("Filters configurations at execution time")
doFirst {
require(gradle.startParameter.isWriteDependencyLocks) { "$path must be run from the command line with the `--write-locks` flag" }
}
doLast {
configurations.filter {
// Add any custom filtering on the configurations to be resolved
it.isCanBeResolved
}.forEach { it.resolve() }
}
}
tasks.register('resolveAndLockAll') {
notCompatibleWithConfigurationCache("Filters configurations at execution time")
doFirst {
assert gradle.startParameter.writeDependencyLocks : "$path must be run from the command line with the `--write-locks` flag"
}
doLast {
configurations.findAll {
// Add any custom filtering on the configurations to be resolved
it.canBeResolved
}.each { it.resolve() }
}
}
By filtering and resolving specific configurations, you ensure that only the relevant ones are locked, tailoring the locking process to your project’s needs. This is especially useful in environments like native builds, where not all configurations can be resolved on a single platform.
Understanding the locking mechanism during dependency resolution
When using dependency locking, Gradle enforces the locked versions as if they were declared with strictly().
This means:
-
If the declared version is lower than the locked version (e.g., declared
1.0.0, locked1.2.0), Gradle will silently upgrade to the locked version. -
If the declared version is higher than the locked version (e.g., declared
1.2.0, locked1.0.0), Gradle will fail resolution, because the lockfile enforces1.0.0strictly.
This can be surprising if you assume the declared version takes precedence.
To resolve this, update the lockfile with --write-locks.
Understanding lock state location and format
A lockfile is a critical component that records the exact versions of dependencies used in a project, allowing for verification during builds to ensure consistent results across different environments and over time. It helps identify discrepancies in dependencies when a project is built on different machines or at different times.
| Lockfiles should be checked in to source control. |
Location of lock files
-
The lock state is preserved in a file named
gradle.lockfile, located at the root of each project or subproject directory. -
The exception is the lockfile for the buildscript itself, which is named
buildscript-gradle.lockfile.
Structure of lock files
Consider the following dependency declaration:
configurations {
compileClasspath {
resolutionStrategy.activateDependencyLocking()
}
runtimeClasspath {
resolutionStrategy.activateDependencyLocking()
}
annotationProcessor {
resolutionStrategy.activateDependencyLocking()
}
}
dependencies {
implementation("org.springframework:spring-beans:[5.0,6.0)")
}
configurations {
compileClasspath {
resolutionStrategy.activateDependencyLocking()
}
runtimeClasspath {
resolutionStrategy.activateDependencyLocking()
}
annotationProcessor {
resolutionStrategy.activateDependencyLocking()
}
}
dependencies {
implementation 'org.springframework:spring-beans:[5.0,6.0)'
}
With the above configuration, the generated gradle.lockfile will look like this:
# This is a Gradle generated file for dependency locking. # Manual edits can break the build and are not advised. # This file is expected to be part of source control. org.springframework:spring-beans:5.0.5.RELEASE=compileClasspath, runtimeClasspath org.springframework:spring-core:5.0.5.RELEASE=compileClasspath, runtimeClasspath org.springframework:spring-jcl:5.0.5.RELEASE=compileClasspath, runtimeClasspath empty=annotationProcessor
Where:
-
Each line represents a single dependency in the
group:artifact:versionformat. -
Configurations: After the version, the configurations that include the dependency are listed.
-
Ordering: Dependencies and configurations are listed alphabetically to make version control diffs easier to manage.
-
Empty Configurations: The last line lists configurations that are empty, meaning they contain no dependencies.
This lockfile should be included in source control to ensure that all team members and environments use the exact same dependency versions.
Migrating your legacy lockfile
If your project uses the legacy lock file format of a file per locked configuration, follow these instructions to migrate to the new format:
-
Follow the documentation for