// Copyright (C) 2023 Gradle, Inc.
//
// Licensed under the Creative Commons Attribution-Noncommercial-ShareAlike 4.0 International License.;
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      https://creativecommons.org/licenses/by-nc-sa/4.0/
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

[[dependency-locking]]
= Locking dependency versions

Use of dynamic dependency versions (e.g. `1.+` or `[1.0,2.0)`) makes builds non-deterministic.
This causes builds to break without any obvious change, and worse, can be caused by a transitive dependency that the build author has no control over.

To achieve https://reproducible-builds.org/[reproducible builds], it is necessary to _lock_ versions of dependencies and transitive dependencies such that a build with the same inputs will always resolve the same module versions.
This is called _dependency locking_.

It enables several key scenarios, including:

* Companies managing multiple repositories no longer need to rely on `-SNAPSHOT` or changing dependencies, which can result in cascading failures when a dependency introduces a bug or incompatibility.
* Teams using the latest dependencies can use dynamic versions, locking their dependencies only for releases. The release tag will contain the lock states, ensuring the build is fully reproducible when bug fixes need to be developed.

Combined with <<publishing_maven.adoc#publishing_maven:resolved_dependencies,publishing resolved versions>>, you can replace the declared dynamic versions at publication time.
Consumers will see the resolved versions that your release used.

Locking is enabled per <<dependency_configurations.adoc#sec:what-are-dependency-configurations,dependency configuration>>.
Once enabled, you must create an initial lock state.
This will cause Gradle to verify that resolution results do not change, ensuring the same dependencies are selected even if newer versions are available.
Modifications to your build that impact the resolved set of dependencies will cause it to fail.
This ensures that changes in published dependencies or build definitions do not alter resolution without updating the lock state.

[NOTE]
====
Dependency locking is effective only with <<dynamic_versions.adoc#sub:declaring_dependency_with_dynamic_version,dynamic versions>>.
It has no impact on <<dynamic_versions.adoc#sub:declaring_dependency_with_changing_version,changing versions>> (e.g., `-SNAPSHOT`), where the coordinates remain the same but the content may change.
Gradle will emit a warning when persisting the lock state if changing dependencies are present in the resolution result.
====

== Locking specific configurations

Locking of a configuration happens through the link:{groovyDslPath}/org.gradle.api.artifacts.ResolutionStrategy.html[ResolutionStrategy]:

====
include::sample[dir="snippets/dependencyManagement/dependencyLocking-lockingSingleFilePerProject/kotlin",files="build.gradle.kts[tags=locking-single]"]
include::sample[dir="snippets/dependencyManagement/dependencyLocking-lockingSingleFilePerProject/groovy",files="build.gradle[tags=locking-single]"]
====

Only configurations that can be resolved will have lock state attached to them.
Applying locking on non resolvable-configurations is a no-op.

The following locks all configurations:

====
include::sample[dir="snippets/dependencyManagement/dependencyLocking-lockingAllConfigurations/kotlin",files="build.gradle.kts[tags=locking-all]"]
include::sample[dir="snippets/dependencyManagement/dependencyLocking-lockingAllConfigurations/groovy",files="build.gradle[tags=locking-all]"]
====

NOTE: The above will lock all _project_ configurations, but not the _buildscript_ ones.

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:

====
include::sample[dir="snippets/dependencyManagement/dependencyLocking-unlockingSingleConfiguration/kotlin",files="build.gradle.kts[tags=locking-one]"]
include::sample[dir="snippets/dependencyManagement/dependencyLocking-unlockingSingleConfiguration/groovy",files="build.gradle[tags=locking-one]"]
====

=== Locking buildscript classpath configuration

If you apply plugins to your build, you may want to leverage dependency locking there as well.
To lock the <<plugins.adoc#sec:applying_plugins_buildscript,`classpath` configuration>> used for script plugins:

====
include::sample[dir="snippets/dependencyManagement/dependencyLocking-lockingClasspathConfiguration/kotlin",files="build.gradle.kts[tags=locking-classpath]"]
include::sample[dir="snippets/dependencyManagement/dependencyLocking-lockingClasspathConfiguration/groovy",files="build.gradle[tags=locking-classpath]"]
====

== Generating and updating dependency locks

In order to generate or update lock state, you specify the `--write-locks` command line argument in addition to the normal tasks that would trigger configurations to be resolved.
This will cause the creation of lock state for each resolved configuration in that build execution.
If lock state existed previously, it is overwritten.

NOTE: Gradle will not write the lock state to disk if the build fails. This prevents persisting possibly invalid state.

[[lock_all_configurations_in_one_build_execution]]
=== Lock all configurations in one build execution

When locking multiple configurations, you may want to lock them all at once, during a single build execution.

For this, you have two options:

* Run `gradle dependencies --write-locks`.
This will effectively lock all resolvable configurations that have locking enabled.
Note that in a multi project setup, `dependencies` only is executed on _one_ project, the root one in this case.
* Declare a custom task that resolves all configurations.
This does not work for Android projects.

====
include::sample[dir="snippets/dependencyManagement/dependencyLocking-lockingAllConfigurations/kotlin",files="build.gradle.kts[tags=resolve-all]"]
include::sample[dir="snippets/dependencyManagement/dependencyLocking-lockingAllConfigurations/groovy",files="build.gradle[tags=resolve-all]"]
====

That second option, with proper selection of configurations, can be the only option in the native world, where not all configurations can be resolved on a single platform.

== Lock state location and format

A lockfile is a file that stores the exact versions of dependencies used in a project, preventing unexpected changes in dependencies when a project is built on different machines or at different times.

Lock state will be preserved in a file located at the root of the project or subproject directory.
Each file is named `gradle.lockfile`.

The one exception to this rule is for the lock file for the <<plugins.adoc#sec:applying_plugins_buildscript,buildscript itself>>.
In that case the file will be named `buildscript-gradle.lockfile`.

For the following dependency declaration:

====
include::sample[dir="snippets/dependencyManagement/dependencyLocking-lockingSingleFilePerProject/kotlin",files="build.gradle.kts[tags=locking-explicit]"]
include::sample[dir="snippets/dependencyManagement/dependencyLocking-lockingSingleFilePerProject/groovy",files="build.gradle[tags=locking-explicit]"]
====

The lockfile will have the following content:

[listing]
.gradle.lockfile
----
include::{snippetsPath}/dependencyManagement/dependencyLocking-lockingSingleFilePerProject/groovy/gradle.lockfile[]
----

Where:

* Each line represents a single dependency in the `group:artifact:version` notation
* It then lists all configurations that contain the given dependency
* Module and configurations are ordered alphabetically, to ease diffs
* The last line of the file lists all empty configurations, that is configurations known to have no dependencies

[[migrate_single_lockfile]]
=== Migrating from the lockfile per configuration format

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 <<#lock_all_configurations_in_one_build_execution, writing>> or <<#selectively_updating_lock_state_entries, 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.

NOTE: 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_per_project_lock_file_name_and_location]]
=== 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:

====
include::sample[dir="snippets/dependencyManagement/dependencyLocking-lockingSingleFilePerProject/kotlin",files="build.gradle.kts[tags=locking-file-name]"]
include::sample[dir="snippets/dependencyManagement/dependencyLocking-lockingSingleFilePerProject/groovy",files="build.gradle[tags=locking-file-name]"]
====

== 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 as stored in the lock state, regardless if new versions matching the dynamic selector have been produced.

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]]
=== 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.

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

====
include::sample[dir="snippets/dependencyManagement/dependencyLocking-lockModeSelection/kotlin",files="build.gradle.kts[tags=lock-mode]"]
include::sample[dir="snippets/dependencyManagement/dependencyLocking-lockModeSelection/groovy",files="build.gradle[tags=lock-mode]"]
====

[[selectively_updating_lock_state_entries]]
== 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.

----
❯ gradle 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

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

== Disabling dependency locking

. Make sure that the configuration for which you no longer want locking is not configured with locking.
. Next time you update the save lock state, Gradle will automatically clean up all stale lock state from it.

Gradle needs to resolve a configuration, no longer marked as locked, to detect that associated lock state can be dropped.

[[ignoring_dependencies]]
== Ignoring specific dependencies from the lock state

Dependency locking can be used in cases where reproducibility is not the main goal.

As a build author, you may want to have different frequency of dependency version updates, based on their origin for example.
In that case, it might be convenient to ignore some dependencies because you always want to use the latest version for those.
An example is the internal dependencies in an organization which should always use the latest version as opposed to third party dependencies which have a different upgrade cycle.

WARNING: This feature can break reproducibility and should be used with caution.
There are scenarios that are better served with leveraging  <<#fine_tuning_dependency_locking_behaviour_with_lock_mode,different lock modes>> or <<#configuring_the_per_project_lock_file_name_and_location,using different names for lock files>>.

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

====
include::sample[dir="snippets/dependencyManagement/dependencyLocking-lockingSingleFilePerProject/kotlin",files="build.gradle.kts[tags=ignore-dependencies]"]
include::sample[dir="snippets/dependencyManagement/dependencyLocking-lockingSingleFilePerProject/groovy",files="build.gradle[tags=ignore-dependencies]"]
====

The notation is a `<group>:<name>` dependency notation, where `\*` can be used as a trailing wildcard.
See <<#selectively_updating_lock_state_entries,the description>> on updating lock files for more details.
Note that the value `*:*` is not accepted as it is equivalent to disabling locking.

Ignoring dependencies will have the following effects:

* An ignored dependency applies to all locked configurations. The setting is project scoped.
* Ignoring a dependency does not mean lock state ignores its transitive dependencies.
* There is no validation 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 that resolution matches the lock state.
* Finally, if the dependency is present in the resolution result and the lock state is persisted, it will be absent from the written lock state.

[[locking_limitations]]
== Understanding locking limitations

* Locking cannot yet be applied to source dependencies.
