Using dependency locking in local library

Hello, I just started using dependency locking with version catalogs & version ranges (e.g. latest.release, 2.+, etc.) in one of my Java projects (using Gradle 7.4.2). To simplify, let’s say it has a structure like this:

:root:lib1
:root:project1
:root:project2

where lib1 is shared by the other 2 subprojects.

I started using dependency locking in :root:project1 and everything works as intended. :smiling_face_with_three_hearts:

Then I started using dependency locking with version ranges in :root:lib1. e.g. I define the dependency as implementation software.amazon.awssdk:sqs:latest.release, but lock it to a specific version, say 1.0.0. This seems to work fine with the first project since I generate a separate lockfile for that project, so the version used in that project is the version defined in that project’s lockfile.

However, in project2, I have not yet switched to dependency locking, and I see that it’s pulling in transitive dependencies from lib1 as version latest.release rather than 1.0.0, which resolves to different version day to day. Is there a way to transitively bring the locked versions from lib1 instead of the latest.release value?

project2 declares a dependency on lib1 using the project annotation

    implementation project(':root:lib1')

Do I need to publish the lib1 jar and consume a specific version of it in order to get the locked transitive dependencies? Any help is much appreciated :slight_smile:

At the very bottom of the docs, there is a note about limitations: Locking dependency versions .

I was originally assuming this only applied when suing remote source dependencies and not composite builds, but since they are very similar under the hood, I’m guessing this is a known limitation. Would be grateful if anyone is able to confirm.

I don’t think that limitation applies.
That is, if you use source dependencies, you have a repository and a version for a dependency. This version can be a dynamic version and cannot be locked by dependency locking. At least that is what I understand from that sentence as that is the only thing called “source dependency”. Composite builds are only loosely related to that in so far that a source dependency is added as included build after the VCS shenenigans is handled.

But afair what you try is working as expected.
If you use a composite build, you consume the publication of a build.
At the time you consume that build, you only see the published facts, not the dependency lock state local to that build.
So project2 sees the dynamic version and as it has no dependency locking, resolves it like normal.

The part from the docs you linked to that is probably what you are after is

Combined with publishing resolved versions, you can also replace the declared dynamic version part at publication time. Consumers will instead see the versions that your release resolved.

Enable publishing resolved versions instead of declared versions and I think you should get the result you intended.

Hello and thank you for the response! I abandoned this since I got busy, but am finally getting back to it. I read up on publishing resolved versions as you mentioned, but I’m not able to see any changes if I apply any publishing blocks as listed in the linked docs.

My first comment may have been misleading – am not actually using a composite build here, just a simple multi-project setup, so it doesn’t look like the publishing section would apply (since i’m not using the ‘maven-publish’ plugin).

I generated a simple project using gradle init to make sure nothing else in my build was applying extra logic.

‘:utilities’ build.gradle:

plugins {
    id 'java-library'
}

dependencyLocking {
    lockAllConfigurations()
}

dependencies {
    api 'software.amazon.awssdk:sqs:latest.release'
}
...

‘:app’ build.gradle:

plugins {
    id 'java'
}

dependencies {
    implementation project(':utilities')
}

I can generate the lock file for the ‘:utilities’ project & could publish to some repo using ‘maven-publish’ with the lock versions as the documentation mentions. Then instead of using a project dependency from ‘:app’ project, I could reference the published version to get the locked versions. My main question is still: is there a way to do this without publishing?

I didn’t use locking much, so I’m not sure. But I think if you just lock the configurations in the utility project, this just does not influence the configurations in app and thus resolution is done as usual. You would probably need to lock in app itself.

It you could try whether it works to have the same lockfile configured for both projects. But I’m not sure whether that is a good idea.

Gotcha. Yeah it definitely works if the app project uses locking and will respect whatever values are set in the lockfile for that project. The issue is that the transitive dependency gets brought in as latest.release so when I generate the lock file for the ‘:app’, it fetches the latest published version instead of using the version that the library declares in the lockfile. It’s not as big a concern when the ‘:app’ project is using locking itself, but it would still be nice to control the transitive dependency version via the library instead of via the app.

So from what I’ve tried, I would need to first update all app-style projects (or projects that consume other local projects) to use locking, then after that I could update the library projects (& be sure to publish resolved versions there so external consumers could get the right versions).

Alternatively, I could update the app projects that don’t use locking yet to consume libs via their published versions. But this isn’t great for feature development that requires changes across multiple projects.

The main goal here is to programmatically update the dependencies for each project on some normal cadence (e.g. 1x/week or even nightly). So long as we have sufficient automated tests for these projects, we could even auto-merge the prs. I know there are tools like Dependabot that can do something similar, but after learning about dependency locking, I thought we might be able to bypass the need to use another tool.