# Locking Versions

version 9.7.1

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:

build.gradle.kts

    dependencies {
        // Depend on the latest 5.x release of Spring available in the searched repositories
        implementation("org.springframework:spring-web:5.+")
    }

build.gradle

    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 `-SNAPSHOT` or 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 `SNAPSHOT` or 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., `-SNAPSHOT`), where the coordinates remain the same, but the content may change. 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:

build.gradle.kts

    configurations {
        compileClasspath {
            resolutionStrategy.activateDependencyLocking()
        }
    }

build.gradle

    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:

build.gradle.kts

    dependencyLocking {
        lockAllConfigurations()
    }

build.gradle

    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:

build.gradle.kts

    configurations.compileClasspath {
        resolutionStrategy.deactivateDependencyLocking()
    }

build.gradle

    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:

build.gradle.kts

    buildscript {
        configurations.classpath {
            resolutionStrategy.activateDependencyLocking()
        }
    }

build.gradle

    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.

gradle.lockfile

    # 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:

1. Run `gradle dependencies --write-locks`:

   - This command will lock all resolvable configurations that have locking enabled.

   - In a multi-project setup, note that `dependencies` is executed only on one project, typically the root project.

2. 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:

build.gradle.kts

    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() }
        }
    }

build.gradle

    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.matching {
                // 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`, locked `1.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`, locked `1.0.0`), Gradle will fail resolution, because the lockfile enforces `1.0.0` strictly.

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:

build.gradle.kts

    configurations {
        compileClasspath {
            resolutionStrategy.activateDependencyLocking()
        }
        runtimeClasspath {
            resolutionStrategy.activateDependencyLocking()
        }
        annotationProcessor {
            resolutionStrategy.activateDependencyLocking()
        }
    }

    dependencies {
        implementation("org.springframework:spring-beans:[5.0,6.0)")
    }

build.gradle

    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:

gradle.lockfile

    # 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.
    # To regenerate this file, run: ./gradlew :dependencies --write-locks
    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:version` format.

- 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:

1. Follow the documentation for writing or updating dependency lock state.

2. Upon writing the single lock file per project, Gradle will also delete all lock files per configuration for which the state was transferred.

| Migration can be done one configuration at a time. Gradle will keep sourcing the lock state from the per configuration files as long as there is no information for that configuration in the single lock file.

## Configuring the lock file name and location

When using a single lock file per project, you can configure its name and location.

This capability allows you to specify a file name based on project properties, enabling a single project to store different lock states for different execution contexts.

For example, in the JVM ecosystem, the Scala version is often included in artifact coordinates:

build.gradle.kts

    val scalaVersion = "2.12"
    dependencyLocking {
        lockFile = file("$projectDir/locking/gradle-${scalaVersion}.lockfile")
    }

build.gradle

    def scalaVersion = "2.12"
    dependencyLocking {
        lockFile = file("$projectDir/locking/gradle-${scalaVersion}.lockfile")
    }

## Running a build with lock state present

The moment a build needs to resolve a configuration that has locking enabled, and it finds a matching lock state, it will use it to verify that the given configuration still resolves the same versions.

A successful build indicates that the same dependencies are used by your build as stored in the lock state, regardless if new versions matching the dynamic selector are available in any of the repositories your build uses.

The complete validation is as follows:

- Existing entries in the lock state must be matched in the build

  - A version mismatch or missing resolved module causes a build failure

- Resolution result must not contain extra dependencies compared to the lock state

### Fine-tuning dependency locking behaviour with lock mode

While the default lock mode behaves as described above, two other modes are available:

Strict mode

  In this mode, in addition to the validations above, dependency locking will fail if a configuration marked as locked does not have lock state associated with it.

Lenient mode

  In this mode, dependency locking will still pin dynamic versions but otherwise changes to the dependency resolution are no longer errors. Other changes include:

  - Adding or removing dependencies, even if they are strictly versioned, without causing a build failure.

  - Allowing transitive dependencies to shift, as long as dynamic versions are still pinned.

This mode offers flexibility for situations where you might want to explore or test new dependencies or changes in versions without breaking the build, making it useful for testing nightly or snapshot builds.

The lock mode can be controlled from the `dependencyLocking` block as shown below:

build.gradle.kts

    dependencyLocking {
        lockMode = LockMode.STRICT
    }

build.gradle

    dependencyLocking {
        lockMode = LockMode.STRICT
    }

## Updating lock state entries selectively

In order to update only specific modules of a configuration, you can use the `--update-locks` command line flag. It takes a comma (`,`) separated list of module notations. In this mode, the existing lock state is still used as input to resolution, filtering out the modules targeted by the update:

    $ ./gradlew dependencies --update-locks org.apache.commons:commons-lang3,org.slf4j:slf4j-api

Wildcards, indicated with `*`, can be used in the group or module name. They can be the only character or appear at the end of the group or module respectively. The following wildcard notation examples are valid:

- `org.apache.commons:*`: will let all modules belonging to group `org.apache.commons` update

- `*:guava`: will let all modules named `guava`, whatever their group, update

- `org.springframework.spring*:spring*`: will let all modules having their group starting with `org.springframework.spring` and name starting with `spring` update

| The resolution may cause other module versions to update, as dictated by the Gradle resolution rules.

## Disabling dependency locking

To disable dependency locking for a configuration:

1. Remove Locking Configuration: Ensure that the configuration you no longer want to lock is not configured with dependency locking. This means removing or commenting out any `activateDependencyLocking()` calls for that configuration.

2. Update Lock State: The next time you update and save the lock state (using the `--write-locks` option), Gradle will automatically clean up any stale lock state associated with the configurations that are no longer locked.

| Gradle must resolve a configuration that is no longer marked as locked to detect and drop the associated lock state. Without resolving the configuration, Gradle cannot identify which lock state should be cleaned up.

## Ignoring specific dependencies from the lock state

In some scenarios, you may want to use dependency locking for other reasons than build reproducibility.

As a build author, you might want certain dependencies to update more frequently than others. For example, internal dependencies within an organization might always use the latest version, while third-party dependencies follow a different update cycle.

| This approach can compromise reproducibility. Consider using different lock modes or separate lock files for specific cases.

You can configure dependencies to be ignored in the `dependencyLocking` project extension:

build.gradle.kts

    dependencyLocking {
        ignoredDependencies.add("com.example:*")
    }

build.gradle

    dependencyLocking {
        ignoredDependencies.add('com.example:*')
    }

The notation `<group>:<name>` is used to specify dependencies, where `*` acts as a trailing wildcard. Note that `*:*` is not accepted, as it effectively disables locking. See the description on updating lock files for more details.

Ignoring dependencies will have the following effects:

- Ignored dependencies apply across all locked configurations, and the setting is project scoped.

- Ignoring a dependency does not exclude its transitive dependencies from the lock state.

- No validation ensures that an ignored dependency is present in any configuration resolution.

- If the dependency is present in lock state, loading it will filter out the dependency.

- If the dependency is present in the resolution result, it will be ignored when validating the resolution against the lock state.

- When the lock state is updated and persisted, any ignored dependency will be omitted from the written lock state.

## Understanding locking limitations

- Dependency locking does not currently apply to source dependencies.