KEMBAR78
Add support for gradle resolver by smocherla-brex · Pull Request #1357 · bazel-contrib/rules_jvm_external · GitHub
Skip to content

Conversation

@smocherla-brex
Copy link
Contributor

@smocherla-brex smocherla-brex commented Apr 27, 2025

This attempts to add support for a gradle resolver to support usecases like #864 and #909 (downloading the relevant JVM artifact without having to specify the JVM specific one, aka rely on Gradle metadata to do its magic).

I borrowed some of the work from @shs96c's PR in #1360. It works as follows:

  • Sets up a fake gradle project with the build file templated using handlebars
  • Uses the gradle-tooling-api and also injects a plugin dynamically into the gradle classpath
  • The plugin runs the dependency resolution for runtimeClasspath configuration, collects the dependency graph, collects all the resolved artifacts (and requests additional things like javadoc, sources, poms). In this process, conflicts are also collected, and failures are reported.
  • The gradle tooling api with ToolingModelBuilder requires serialization with interfaces, so there's a bit of boilerplate I had to add.
  • This supports BOMs, private repos and those tests pass.

Forgive me if my code is not idiomatic Java (it's been a while since I've worked with Java).

Checklist:

  • Added regression tests
  • Isolated gradle daemon and gradle cache per execution
  • Handle BOMs and pom files
  • Conflict resolution
  • Basic support for KMP artifacts (and support for gradle metadata) as illustrated by regression tests. I have not tested this with an android project though.
  • All tests pass

Follow-up work:

Process command line: /private/var/tmp/_bazel_smocherla/536b5c8af4e86fad57fc24f39380ac8a/external/rules_java~~toolchains~remotejdk11_macos_aarch64/zulu-11.jdk/Contents/Home/bin/java --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.xml/javax.xml.namespace=ALL-UNNAMED -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005 -Dfile.encoding=UTF-8 -Duser.country=CA -Duser.language=en -Duser.variant -cp /private/var/tmp/_bazel_smocherla/536b5c8af4e86fad57fc24f39380ac8a/execroot/_main/bazel-out/darwin_arm64-fastbuild/bin/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolverTest.runfiles/_main~_repo_rules~gradle/gradle-bin/lib/gradle-daemon-main-8.13.jar -javaagent:/private/var/tmp/_bazel_smocherla/536b5c8af4e86fad57fc24f39380ac8a/execroot/_main/bazel-out/darwin_arm64-fastbuild/bin/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolverTest.runfiles/_main~_repo_rules~gradle/gradle-bin/lib/agents/gradle-instrumentation-agent-8.13.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 8.13
  • Make tests faster (they're a bit slow now)
  • Create alias targets for KMP artifact that resolve to the actual target based on the selected platform (android, JVM...)

@shs96c
Copy link
Collaborator

shs96c commented May 8, 2025

This is great to see! Thank you!

@smocherla-brex, I've had a stalled branch to do this exact same thing for months. Would you like to work together on this? I've put up my WIP as #1360, but I'm more than happy to drop it in favour of your work. Please feel free to lift anything you think is useful from my WIP PR!

@smocherla-brex
Copy link
Contributor Author

@shs96c Thanks for sharing your in-progress changes. I'm happy to help get this over the line. I'll take a look at your PR and see if I can leverage some of it in here later this week when I have cycles and hopefully get it in a place where it can be open for review.

@smocherla-brex smocherla-brex changed the title [WIP] Add support for gradle resolver Add support for gradle resolver May 18, 2025
@smocherla-brex
Copy link
Contributor Author

smocherla-brex commented May 20, 2025

Update:

  • I have a lot of the tests passing now. Some are still failing and need to fix - related to handling BOMs and Kotlin Multiplatform/gradle metadata artifacts.
  • The tests are slow because Gradle incurs 10-15 seconds in setup for every test case, so I had to bump the timeout (but need to make them faster).
  • Need to cleanup the code and add comments especially in the tooling API.

I'm hoping to get the above unblocked and the tests passing in the next few days and will open it up for review.

@shs96c
Copy link
Collaborator

shs96c commented May 21, 2025

@smocherla-brex, I really appreciate all the work you're doing on this. Thank you! When you're ready for review, just let us know. If you need a hand with anything, just ask :)

name = "regression_testing_gradle",
artifacts = [
# https://github.com/bazel-contrib/rules_jvm_external/issues/909
"androidx.compose.foundation:foundation-layout:1.5.0-beta01",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a couple of regression tests for the issues linked above/below.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's great. Thank you


java_test(
name = "GradleResolverTest",
timeout = "eternal",
Copy link
Contributor Author

@smocherla-brex smocherla-brex Jun 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Increased the timeout here since the target takes about 5-6 minutes on my machine (seems to be longer about 8-9 minutes in CI) to complete as using an isolated instance of gradle per test and running things serially adds up quite a bit. We could look into test sharding if this is unacceptably slow.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation. I think it's fine to leave as-is

@smocherla-brex
Copy link
Contributor Author

smocherla-brex commented Jun 1, 2025

This should be ready for review now @shs96c. It's possible my gradle implementation may not be optimal (as I'm not a gradle expert) but all tests are passing and I also added some regression tests and added comments where relevant for context. There's one job failure in rules-jvm-external-examples but that seems potentially unrelated (kt_jvm_export) to this PR.

@shs96c
Copy link
Collaborator

shs96c commented Jun 1, 2025

This is fantastic. Thank you! I'll make sure to review this next week.

Copy link
Collaborator

@shs96c shs96c left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great stuff. Thank you so much for working on it. The only real blocker is the comment about the exclusions being included in the handlebars context as a Stream rather than a collection.

name = "regression_testing_gradle",
artifacts = [
# https://github.com/bazel-contrib/rules_jvm_external/issues/909
"androidx.compose.foundation:foundation-layout:1.5.0-beta01",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's great. Thank you

MODULE.bazel Outdated
fail_if_repin_required = True,
generate_compat_repositories = True,
lock_file = "//tests/custom_maven_install:regression_testing_gradle_install.json",
repin_instructions = "Please run `REPIN=1 bazel run @regression_testing_gradle//:pin` to refresh the lock file.",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't need this line.

MODULE.bazel Outdated
"regression_testing_gradle",
"regression_testing_maven",
"unpinned_regression_testing_coursier",
"unpinned_regression_testing_gradle",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we shouldn't need to expose the unpinned_ version either. We used to need it for the :pin target, but that's no longer the case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed


# How do we do artifact resolution?
"resolver": attr.string(doc = "The resolver to use. Only honoured for the root module.", values = ["coursier", "maven"], default = _DEFAULT_RESOLVER),
"resolver": attr.string(doc = "The resolver to use. Only honoured for the root module.", values = ["coursier", "maven", "gradle"], default = _DEFAULT_RESOLVER),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you bazel run scripts:format this wild card import should be expanded properly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

List<GradleResolvedDependency> resolvedRoots,
Map<Coordinates, GradleResolvedDependency> coordinatesGradleResolvedDependencyMap,
List<GradleDependency> declaredDeps) {
ArtifactView sourcesView =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had no idea about this mechanism for getting artifacts. Great stuff

}

private boolean isVerbose() {
return System.getenv("RJE_VERBOSE") != null && System.getenv("RJE_VERBOSE").equals("true");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite often people set RJE_VERBOSE to 1. It's enough that the env var isn't null

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

public class PomUtil {
public static String extractPackagingFromPom(File pomFile) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine for now, but experience suggests that we'll need to use the Maven classes for parsing poms. We can definitely leave that change for later.


java_test(
name = "GradleResolverTest",
timeout = "eternal",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation. I think it's fine to leave as-is

import com.github.bazelbuild.rules_jvm_external.resolver.netrc.Netrc;


public class GradleResolverTest extends ResolverTestBase {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am genuinely impressed that you managed to get all the tests passing. Very cool.

@smocherla-brex
Copy link
Contributor Author

This is great stuff. Thank you so much for working on it. The only real blocker is the comment about the exclusions being included in the handlebars context as a Stream rather than a collection.

Thanks, updated the PR and addressed the feedback you shared. Also added a small update to the README about the new resolver.

@smocherla-brex
Copy link
Contributor Author

smocherla-brex commented Jun 15, 2025

I've been looking at improving performance of the resolver as it's extremely slow right now (on our monorepo, it takes 15 mins+ which isn't great). My hunch for one of the reasons (not everything) based on testing so far is that it's really slow right now due to the serial nature of artifact resolution in the plugin for various artifact views (javadocs, source jars) and additionally serially downloading POMs for every artifact rather than batching. I'm going to try make some of those resolutions async and see if it helps the performance.

@smocherla-brex
Copy link
Contributor Author

smocherla-brex commented Jun 15, 2025

After looking further into the slowness, I've made a few optimizations to improve the resolver:

  1. I removed the artifact views for javadocs/source jars which I added earlier. This actually isn't required (I assumed it was needed because a javadoc related test was failing at one point) as we fetch javadoc and sources explicitly in the Downloader after resolution. This gives a massive improvement as this also ballooned the dependency graph into thousands of nodes and resulted in unnecessary downloads. After removing this, the tests still pass. Building the dependency graph was taking 2 minutes+ as a result, and removing them brought it down to seconds on an example.
  2. The Downloader has support for populating ~/.m2/repository currently with RJE_UNSAFE_CACHE. I also added support for it to populate the gradle cache. (While gradle supports m2local, it only appears to be for reading).

To illustrate the improvement, I tested the gradle resolver on the rules_jvm_external_deps_install.json in this repo before and after the improvements:

Before:

Currently: building lock file
RJE_VERBOSE=true REPIN=1 bazel run @rules_jvm_external_deps//:pin  168.25s user 5.99s system 30% cpu 9:39.62 total

It took nearly 9 minutes to build the lockfile even for the small repo here.

After (cold cache/no unsafe cache with removal of Javadoc/source jar resolution)

RJE_VERBOSE=true REPIN=1 bazel run @rules_jvm_external_deps//:pin  13.82s user 2.65s system 10% cpu 2:31.35 total

It takes around 2.5 minutes, which is pretty significant.

With warm/unsafe/gradle cache support, it is even faster

Currently: building lock file
RJE_UNSAFE_CACHE=1 RJE_VERBOSE=true REPIN=1 bazel run   7.76s user 0.82s system 29% cpu 29.441 total

It takes about 30 seconds in total.

Overall, I think that's a pretty healthy improvement and could be considered something close to usable for an experimental implementation. There's likely room for more improvement though, especially for larger repos..

@shs96c
Copy link
Collaborator

shs96c commented Jun 19, 2025

@smocherla-brex, I plan on merging this on Monday since it LGTM, but I'd like the other committers to also have a final chance to review. Thank you for working on this!

Copy link
Collaborator

@shs96c shs96c left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's great to see this working. We've had enough time to review, and I think the right thing to do now is to land this, and then iterate on top of it as we find things that we need to tinker with. The other committers have had time to review this, so I think it's reasonable to land this now.

Thank you for putting this together, and thank you for focusing on improving the speed!

@shs96c shs96c merged commit e8c84f8 into bazel-contrib:master Jun 23, 2025
7 checks passed
@smocherla-brex smocherla-brex deleted the smocherla/gradle-resolver branch June 23, 2025 14:10
shs96c added a commit to shs96c/rules_jvm_external that referenced this pull request Jun 23, 2025
* master:
  Update maven-metadata.xml when publishing locally (bazel-contrib#1369)
  Add support for gradle resolver (bazel-contrib#1357)
  Prepare for 6.8 release (bazel-contrib#1380)
shs96c added a commit to jin/rules_jvm_external that referenced this pull request Jul 4, 2025
* master: (39 commits)
  Fix resolution of Android/AAR artifacts with Gradle resolver (bazel-contrib#1395)
  fail_if_repin_required is now True by default and minor improvement to failure message (bazel-contrib#1397)
  Housekeeping before we release 6.8 (bazel-contrib#1384)
  Add dll, dylib and so types to maven package mappings (bazel-contrib#1392)
  [bzlmod] Allow suppressing warning about multiple contributing modules. (bazel-contrib#1393)
  Use the artifact default values when adding them to a struct and add tests for coursier artifacts that have empty versions provided by BOMs and don't inlcude them in outdated (bazel-contrib#1390)
  Allow package exclusions and inclusions in javadocs (bazel-contrib#1293)
  Document well-known issues with `bzlmod` (bazel-contrib#1388)
  Begin documenting the gradle resolver (bazel-contrib#1389)
  Run gradle regression tests in CI (bazel-contrib#1385)
  Modify maven_export to allow exporting zip archives (bazel-contrib#1368)
  Allow root module's override tags to take precedence over the overridees from transitive deps. (bazel-contrib#1381)
  Ensure root module artifacts and boms take precedence with warnings (bazel-contrib#1373)
  Update maven-metadata.xml when publishing locally (bazel-contrib#1369)
  Add support for gradle resolver (bazel-contrib#1357)
  Prepare for 6.8 release (bazel-contrib#1380)
  Remove the Windows `kt_jvm_export` example (bazel-contrib#1383)
  Avoid spurious warnings about poorly formatted artifact coordinates (bazel-contrib#1374)
  Flip `fail_if_repin_required` to `True` by default (bazel-contrib#1371)
  Allow the same coordinate to be overridden in different repos (bazel-contrib#1378)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants