Skip to content

Commit 0f15b47

Browse files
authored
feat: Optionally respect java_export during resolution (#347)
Implement support for `java_export`s. See #344 for a description of the problem. Constraints we have to work with: - We make no attempt to modify `java_export` targets. They are meant to be hand-crafted by the user. - `java_export` uses an aspect to accumulate transitive dependency information about its dependencies. We need this transitive dependency information, and the Gazelle index doesn't give it to us. Therefore, we have to store it out of band, in `java_export_index.go`. The community seems to agree that the Gazelle API doesn't allow for this ([GH Issue](bazel-contrib/bazel-gazelle#2123), [Slack discussion](https://bazelbuild.slack.com/archives/C01HMGN77Q8/p1750412735065129)). - It's possible for a `java_export` to depend on a package that is _not_ in a subdirectory. Therefore, we can't rely on the `Imports` or `Embeds` phase to operate on the transitive dependency information, because some relevant packages may not have been analyzed by the time we get to a `java_export`. There is a test case covering this. As a result, the `java_export_index` is split into two phases: - The first phase happens during `GenerateRules`, we note down information about all `java_exports`, as well as information to resolve their transitive dependencies. - At the end of `GenerateRules`, in the `DoneGeneratingRules` hook, we traverse the information we've collected to figure out which Java packages are accessible from a given `java_export`. - The second phase happens during `Resolve`, where we use the data we've collected to resolve to a `java_export`, if possible. - This feature can only be turned on via the `# gazelle:java_resolve_to_java_exports` directive. This directive can only be set once, and it has to be set at the root of the repository. - It can only be set once to avoid edge cases where a package opts out of resolving to `java_exports`, but it is inside a `java_export` itself. - It can only be set at the root of the repository because we need to run Gazelle over the entire repository, to make sure we capture all the transitive dependencies. [This commit](https://bazelbuild.slack.com/archives/C01HMGN77Q8/p1750412735065129) is an example of running this Gazelle extension in the Selenium repository after massaging the BUILD files a bit to fit the constraints of `java_exports`. Closes #344
1 parent 84848ac commit 0f15b47

File tree

70 files changed

+2232
-10
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+2232
-10
lines changed

java/gazelle/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ go_library(
1717
deps = [
1818
"//java/gazelle/javaconfig",
1919
"//java/gazelle/private/java",
20+
"//java/gazelle/private/java_export_index",
2021
"//java/gazelle/private/javaparser",
2122
"//java/gazelle/private/logconfig",
2223
"//java/gazelle/private/maven",

java/gazelle/configure.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ func (jc *Configurer) KnownDirectives() []string {
6666
javaconfig.JavaGenerateProto,
6767
javaconfig.JavaMavenRepositoryName,
6868
javaconfig.JavaAnnotationProcessorPlugin,
69+
javaconfig.JavaResolveToJavaExports,
6970
}
7071
}
7172

@@ -146,7 +147,29 @@ func (jc *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
146147
jc.lang.logger.Fatal().Msgf("invalid value for directive %q: %q: couldn't parse annotation processor class-name: %v", javaconfig.JavaAnnotationProcessorPlugin, parts[1], err)
147148
}
148149
cfg.AddAnnotationProcessorPlugin(*annotationClassName, *processorClassName)
150+
151+
case javaconfig.JavaResolveToJavaExports:
152+
if !cfg.CanSetResolveToJavaExports() {
153+
jc.lang.logger.Fatal().
154+
Msgf("Detected multiple attempts to initialize directive %q. Please only initialize it once for the entire repository.",
155+
javaconfig.JavaResolveToJavaExports)
156+
}
157+
if rel != "" {
158+
jc.lang.logger.Fatal().
159+
Msgf("Enabling or disabling directive %q must be done from the root of the repository.",
160+
javaconfig.JavaResolveToJavaExports)
161+
}
162+
switch d.Value {
163+
case "true":
164+
cfg.SetResolveToJavaExports(true)
165+
case "false":
166+
cfg.SetResolveToJavaExports(false)
167+
default:
168+
jc.lang.logger.Fatal().Msgf("invalid value for directive %q: %s: possible values are true/false",
169+
javaconfig.JavaResolveToJavaExports, d.Value)
170+
}
149171
}
172+
150173
}
151174
}
152175

java/gazelle/generate.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes
200200
}
201201

202202
if productionJavaFiles.Len() > 0 {
203-
l.generateJavaLibrary(args.File, args.Rel, filepath.Base(args.Rel), productionJavaFiles.SortedSlice(), allPackageNames, nonLocalProductionJavaImports, nonLocalJavaExports, annotationProcessorClasses, false, javaLibraryKind, &res)
203+
l.generateJavaLibrary(args.File, args.Rel, filepath.Base(args.Rel), productionJavaFiles.SortedSlice(), allPackageNames, nonLocalProductionJavaImports, nonLocalJavaExports, annotationProcessorClasses, false, javaLibraryKind, &res, cfg, args.Config.RepoName)
204204
}
205205

206206
var testHelperJavaClasses *sorted_set.SortedSet[types.ClassName]
@@ -236,7 +236,7 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes
236236
testJavaImportsWithHelpers.Add(tf.pkg)
237237
srcs = append(srcs, tf.pathRelativeToBazelWorkspaceRoot)
238238
}
239-
l.generateJavaLibrary(args.File, args.Rel, filepath.Base(args.Rel), srcs, packages, testJavaImports, nonLocalJavaExports, annotationProcessorClasses, true, javaLibraryKind, &res)
239+
l.generateJavaLibrary(args.File, args.Rel, filepath.Base(args.Rel), srcs, packages, testJavaImports, nonLocalJavaExports, annotationProcessorClasses, true, javaLibraryKind, &res, cfg, args.Config.RepoName)
240240
}
241241
}
242242

@@ -471,7 +471,7 @@ func accumulateJavaFile(cfg *javaconfig.Config, testJavaFiles, testHelperJavaFil
471471
}
472472
}
473473

474-
func (l javaLang) generateJavaLibrary(file *rule.File, pathToPackageRelativeToBazelWorkspace string, name string, srcsRelativeToBazelWorkspace []string, packages, imports *sorted_set.SortedSet[types.PackageName], exports *sorted_set.SortedSet[types.PackageName], annotationProcessorClasses *sorted_set.SortedSet[types.ClassName], testonly bool, javaLibraryRuleKind string, res *language.GenerateResult) {
474+
func (l javaLang) generateJavaLibrary(file *rule.File, pathToPackageRelativeToBazelWorkspace, name string, srcsRelativeToBazelWorkspace []string, packages, imports, exports *sorted_set.SortedSet[types.PackageName], annotationProcessorClasses *sorted_set.SortedSet[types.ClassName], testonly bool, javaLibraryRuleKind string, res *language.GenerateResult, cfg *javaconfig.Config, repoName string) {
475475
const ruleKind = "java_library"
476476
r := rule.NewRule(ruleKind, name)
477477

@@ -508,6 +508,10 @@ func (l javaLang) generateJavaLibrary(file *rule.File, pathToPackageRelativeToBa
508508
AnnotationProcessors: annotationProcessorClasses,
509509
}
510510
res.Imports = append(res.Imports, resolveInput)
511+
512+
if cfg.ResolveToJavaExports() {
513+
l.javaExportIndex.RecordRuleWithResolveInput(repoName, file, r, resolveInput)
514+
}
511515
}
512516

513517
func (l javaLang) generateJavaBinary(file *rule.File, m types.ClassName, libName string, testonly bool, res *language.GenerateResult) {

java/gazelle/javaconfig/config.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ const (
5353
// JavaAnnotationProcessorPlugin tells the code generator about specific java_plugin targets needed to process
5454
// specific annotations.
5555
JavaAnnotationProcessorPlugin = "java_annotation_processor_plugin"
56+
57+
// JavaResolveToJavaExports tells the code generator to favour resolving dependencies to java_exports where possible.
58+
// If enabled, generated libraries will try to depend on java_exports targets that export a given package, instead of the underlying library.
59+
// This allows monorepos to closely match a traditional Gradle/Maven model where subprojects are published in jars.
60+
// Can be either "true" or "false". Defaults to "true".
61+
// Inherited by children packages, can only be set at the root of the repository.
62+
JavaResolveToJavaExports = "java_resolve_to_java_exports"
5663
)
5764

5865
// Configs is an extension of map[string]*Config. It provides finding methods
@@ -75,6 +82,7 @@ func (c *Config) NewChild() *Config {
7582
extensionEnabled: c.extensionEnabled,
7683
isModuleRoot: false,
7784
generateProto: true,
85+
resolveToJavaExports: c.resolveToJavaExports,
7886
mavenInstallFile: c.mavenInstallFile,
7987
moduleGranularity: c.moduleGranularity,
8088
repoRoot: c.repoRoot,
@@ -105,6 +113,7 @@ type Config struct {
105113
extensionEnabled bool
106114
isModuleRoot bool
107115
generateProto bool
116+
resolveToJavaExports *types.LateInit[bool]
108117
mavenInstallFile string
109118
moduleGranularity string
110119
repoRoot string
@@ -128,6 +137,7 @@ func New(repoRoot string) *Config {
128137
extensionEnabled: true,
129138
isModuleRoot: false,
130139
generateProto: true,
140+
resolveToJavaExports: types.NewLateInit[bool](true),
131141
mavenInstallFile: "maven_install.json",
132142
moduleGranularity: "package",
133143
repoRoot: repoRoot,
@@ -294,6 +304,18 @@ func (c *Config) AddAnnotationProcessorPlugin(annotationClass types.ClassName, p
294304
c.annotationProcessorFullQualifiedClassToPluginClass[fullyQualifiedAnnotationClass].Add(processorClass)
295305
}
296306

307+
func (c *Config) ResolveToJavaExports() bool {
308+
return c.resolveToJavaExports.Value()
309+
}
310+
311+
func (c *Config) CanSetResolveToJavaExports() bool {
312+
return !c.resolveToJavaExports.IsInitialized()
313+
}
314+
315+
func (c *Config) SetResolveToJavaExports(resolve bool) {
316+
c.resolveToJavaExports.Initialize(resolve)
317+
}
318+
297319
func equalStringSlices(l, r []string) bool {
298320
if len(l) != len(r) {
299321
return false

java/gazelle/lang.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"context"
55
"os"
66

7+
"github.com/bazel-contrib/rules_jvm/java/gazelle/javaconfig"
78
"github.com/bazel-contrib/rules_jvm/java/gazelle/private/java"
9+
"github.com/bazel-contrib/rules_jvm/java/gazelle/private/java_export_index"
810
"github.com/bazel-contrib/rules_jvm/java/gazelle/private/javaparser"
911
"github.com/bazel-contrib/rules_jvm/java/gazelle/private/logconfig"
1012
"github.com/bazel-contrib/rules_jvm/java/gazelle/private/maven"
@@ -31,6 +33,9 @@ type javaLang struct {
3133
// Key is the path to the java package from the Bazel workspace root.
3234
javaPackageCache map[string]*java.Package
3335

36+
// javaExportIndex holds information about java_export targets and which symbols they make available.
37+
javaExportIndex *java_export_index.JavaExportIndex
38+
3439
// hasHadErrors triggers the extension to fail at destroy time.
3540
//
3641
// this is used to return != 0 when some errors during the generation were
@@ -64,6 +69,7 @@ func NewLanguage() language.Language {
6469
logger: logger,
6570
javaLogLevel: javaLevel,
6671
javaPackageCache: make(map[string]*java.Package),
72+
javaExportIndex: java_export_index.NewJavaExportIndex(languageName, logger),
6773
}
6874

6975
l.logger = l.logger.Hook(shutdownServerOnFatalLogHook{
@@ -112,11 +118,25 @@ var javaLibraryKind = rule.KindInfo{
112118
},
113119
}
114120

121+
var javaExportKind = rule.KindInfo{
122+
NonEmptyAttrs: map[string]bool{
123+
"deps": true,
124+
"exports": true,
125+
"runtime_deps": true,
126+
},
127+
ResolveAttrs: map[string]bool{
128+
"deps": true,
129+
"exports": true,
130+
"runtime_deps": true,
131+
},
132+
}
133+
115134
func (l javaLang) Kinds() map[string]rule.KindInfo {
116135
kinds := map[string]rule.KindInfo{
117136
"java_binary": kindWithRuntimeDeps,
118137
"java_junit5_test": kindWithRuntimeDeps,
119138
"java_library": javaLibraryKind,
139+
"java_export": javaExportKind,
120140
"java_test": kindWithRuntimeDeps,
121141
"java_test_suite": kindWithRuntimeDeps,
122142
"java_proto_library": kindWithoutRuntimeDeps,
@@ -152,6 +172,7 @@ var baseJavaLoads = []rule.LoadInfo{
152172
Symbols: []string{
153173
"java_junit5_test",
154174
"java_test_suite",
175+
"java_export",
155176
},
156177
},
157178
}
@@ -183,10 +204,25 @@ func (l javaLang) Loads() []rule.LoadInfo {
183204
return loads
184205
}
185206

186-
func (l javaLang) Fix(c *config.Config, f *rule.File) {}
207+
func (l javaLang) Fix(c *config.Config, f *rule.File) {
208+
209+
// We can't put this code in `GenerateRule`, because it doesn't parse the BUILD file at that point,
210+
// so we can't identify the `java_export`s already in the file.
211+
// And we can't do it at `Imports()` time, because we need to hook into `DoneGeneratingRules`
212+
// to know when to populate l.javaExportIndex.
213+
packageConfig := c.Exts[languageName].(javaconfig.Configs)[f.Pkg]
214+
if packageConfig != nil && packageConfig.ResolveToJavaExports() {
215+
for _, r := range f.Rules {
216+
if r.Kind() == "java_export" {
217+
l.javaExportIndex.RecordJavaExport(c.RepoName, r, f)
218+
}
219+
}
220+
}
221+
}
187222

188223
func (l javaLang) DoneGeneratingRules() {
189224
l.parser.ServerManager().Shutdown()
225+
l.javaExportIndex.FinalizeIndex()
190226
}
191227

192228
func (l javaLang) AfterResolvingDeps(_ context.Context) {

java/gazelle/private/java/java.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"github.com/bazel-contrib/rules_jvm/java/gazelle/private/types"
77
)
88

9-
// IsTestPackage tries to detect if the directory would contain test files of not.
9+
// IsTestPackage tries to detect if the directory would contain test files or not.
1010
// It assumes dir is a forward-slashed package name, not a possibly-back-slashed filepath.
1111
func IsTestPackage(pkg string) bool {
1212
if strings.HasPrefix(pkg, "javatests/") {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
load("@rules_go//go:def.bzl", "go_library")
2+
3+
go_library(
4+
name = "java_export_index",
5+
srcs = ["java_export_index.go"],
6+
importpath = "github.com/bazel-contrib/rules_jvm/java/gazelle/private/java_export_index",
7+
visibility = ["//visibility:public"],
8+
deps = [
9+
"//java/gazelle/private/sorted_set",
10+
"//java/gazelle/private/types",
11+
"@bazel_gazelle//label",
12+
"@bazel_gazelle//rule",
13+
"@com_github_rs_zerolog//:zerolog",
14+
],
15+
)

0 commit comments

Comments
 (0)