Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions java/gazelle/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package gazelle
import (
"flag"
"fmt"
"path"
"path/filepath"
"strings"

"github.com/bazel-contrib/rules_jvm/java/gazelle/javaconfig"
Expand Down Expand Up @@ -67,6 +69,8 @@ func (jc *Configurer) KnownDirectives() []string {
javaconfig.JavaMavenRepositoryName,
javaconfig.JavaAnnotationProcessorPlugin,
javaconfig.JavaResolveToJavaExports,
javaconfig.JavaSourcesetRoot,
javaconfig.JavaStripResourcesPrefix,
}
}

Expand All @@ -88,6 +92,51 @@ func (jc *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
cfgs[rel] = cfg
}

// Auto-detect sourceset structure if not explicitly set
if cfg.StripResourcesPrefix() == "" && cfg.SourcesetRoot() == "" {
// Walk up the directory tree looking for sourceset patterns
currentPath := rel
for currentPath != "" && currentPath != "." {
dir := filepath.Base(currentPath)
parent := filepath.Dir(currentPath)

// Check for Maven-style sourceset pattern: src/main/java or src/test/java
if dir == "java" && parent != "" && parent != "." {
grandparent := filepath.Dir(parent)
parentBase := filepath.Base(parent)
if grandparent != "" && filepath.Base(grandparent) == "src" {
// Found a sourceset pattern - store the sourceset root
// This handles src/main, src/test, src/sample, etc.
// Use path.Join to ensure forward slashes for Bazel paths
sourcesetRoot := path.Join(grandparent, parentBase)
cfg.SetSourcesetRoot(sourcesetRoot)
// Also set the strip prefix for resources
resourcesRoot := path.Join(sourcesetRoot, "resources")
cfg.SetStripResourcesPrefix(resourcesRoot)
break
}
}

// Also check if we're in a resources directory
if dir == "resources" && parent != "" && parent != "." {
grandparent := filepath.Dir(parent)
parentBase := filepath.Base(parent)
if grandparent != "" && filepath.Base(grandparent) == "src" {
// Found a sourceset pattern from resources side
sourcesetRoot := path.Join(grandparent, parentBase)
cfg.SetSourcesetRoot(sourcesetRoot)
// Also set the strip prefix for resources
resourcesRoot := path.Join(sourcesetRoot, "resources")
cfg.SetStripResourcesPrefix(resourcesRoot)
break
}
}

currentPath = parent
}
}

// Process directives from BUILD file
if f != nil {
for _, d := range f.Directives {
switch d.Key {
Expand Down Expand Up @@ -168,6 +217,12 @@ func (jc *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
jc.lang.logger.Fatal().Msgf("invalid value for directive %q: %s: possible values are true/false",
javaconfig.JavaResolveToJavaExports, d.Value)
}

case javaconfig.JavaSourcesetRoot:
cfg.SetSourcesetRoot(d.Value)

case javaconfig.JavaStripResourcesPrefix:
cfg.SetStripResourcesPrefix(d.Value)
}

}
Expand Down
184 changes: 172 additions & 12 deletions java/gazelle/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"sort"
"strings"
Expand Down Expand Up @@ -63,20 +64,40 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes

javaFilenamesRelativeToPackage := filterStrSlice(args.RegularFiles, func(f string) bool { return filepath.Ext(f) == ".java" })

isResourcesRoot := strings.HasSuffix(args.Rel, "/resources")
isResourcesSubdir := strings.Contains(args.Rel, "/resources/") && !isResourcesRoot

var javaPkg *java.Package

if len(javaFilenamesRelativeToPackage) == 0 {
if !isModule || !cfg.IsModuleRoot() {
if isResourcesSubdir {
// Skip subdirectories of resources roots - they shouldn't generate BUILD files
return res
}
if !isResourcesRoot {
if !isModule || !cfg.IsModuleRoot() {
return res
}
}
// For resources root directories, continue processing even without Java files
}

sort.Strings(javaFilenamesRelativeToPackage)
if len(javaFilenamesRelativeToPackage) == 0 && isResourcesRoot {
// Skip Java parsing for resources-only directories
javaPkg = &java.Package{
Name: types.NewPackageName(""),
}
} else {
sort.Strings(javaFilenamesRelativeToPackage)

javaPkg, err := l.parser.ParsePackage(context.Background(), &javaparser.ParsePackageRequest{
Rel: args.Rel,
Files: javaFilenamesRelativeToPackage,
})
if err != nil {
log.Fatal().Err(err).Str("package", args.Rel).Msg("Failed to parse package")
var err error
javaPkg, err = l.parser.ParsePackage(context.Background(), &javaparser.ParsePackageRequest{
Rel: args.Rel,
Files: javaFilenamesRelativeToPackage,
})
if err != nil {
log.Fatal().Err(err).Str("package", args.Rel).Msg("Failed to parse package")
}
}

// We exclude intra-package imports to avoid self-dependencies.
Expand Down Expand Up @@ -199,8 +220,92 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes
javaLibraryKind = kindMap.KindName
}

if productionJavaFiles.Len() > 0 {
l.generateJavaLibrary(args.File, args.Rel, filepath.Base(args.Rel), productionJavaFiles.SortedSlice(), allPackageNames, nonLocalProductionJavaImports, nonLocalJavaExports, annotationProcessorClasses, false, javaLibraryKind, &res, cfg, args.Config.RepoName)
// Check if this is a resources root directory and generate a pkg_files target
if isResourcesRoot && len(javaFilenamesRelativeToPackage) == 0 {
// Collect resource files recursively from this directory and all subdirectories
var allResourceFiles []string

collectResourceFiles := func(files []string, dirPrefix string) []string {
var resourceFiles []string
for _, f := range files {
base := filepath.Base(f)
// Skip Java files, BUILD files, and common non-resource files
if base == "BUILD" || base == "BUILD.bazel" { // files from our tests
continue
}
if dirPrefix != "" {
resourceFiles = append(resourceFiles, path.Join(dirPrefix, f))
} else {
resourceFiles = append(resourceFiles, f)
}
}
return resourceFiles
}

allResourceFiles = append(allResourceFiles, collectResourceFiles(args.RegularFiles, "")...)

for _, subdir := range args.Subdirs {
// Skip BUILD directories
if subdir == "BUILD" || subdir == "BUILD.bazel" {
continue
}
subdirFiles := collectResourceFilesRecursively(args, subdir)
allResourceFiles = append(allResourceFiles, subdirFiles...)
}

if len(allResourceFiles) > 0 {
// Sort the files for deterministic output
sort.Strings(allResourceFiles)

// Always generate a pkg_files target for resources
r := rule.NewRule("pkg_files", "resources")
r.SetAttr("srcs", allResourceFiles)

stripPrefix := cfg.StripResourcesPrefix()
if stripPrefix != "" {
r.SetAttr("strip_prefix", stripPrefix)
}

res.Gen = append(res.Gen, r)
res.Imports = append(res.Imports, types.ResolveInput{})

// In package mode, also generate a java_library wrapper for the resources
if !isModule {
resourceLib := rule.NewRule(javaLibraryKind, "resources_lib")
resourceLib.SetAttr("resources", []string{":resources"})
resourceLib.SetAttr("visibility", []string{"//:__subpackages__"})
res.Gen = append(res.Gen, resourceLib)
res.Imports = append(res.Imports, types.ResolveInput{})
}
}
} else if productionJavaFiles.Len() > 0 {
var resourcesDirectRef string // For module mode: direct reference to pkg_files
var resourcesRuntimeDep string // For package mode: runtime_dep on resources_lib

if cfg.SourcesetRoot() != "" {
// We have a sourceset root configured
// The sourceset root is the parent of both java and resources directories
// For example, if sourceset root is "src/sample", then:
// - Java files are in "src/sample/java/..."
// - Resources are in "src/sample/resources"

resourcesPath := path.Join(cfg.SourcesetRoot(), "resources")

// Check if the resources directory actually exists
fullResourcesPath := filepath.Join(args.Config.RepoRoot, filepath.FromSlash(resourcesPath))
if _, err := os.Stat(fullResourcesPath); err == nil {
// Resources directory exists, add the reference
if isModule {
// Module mode: reference pkg_files directly as resources
resourcesDirectRef = "//" + resourcesPath + ":resources"
} else {
// Package mode: reference resources_lib as runtime_deps
resourcesRuntimeDep = "//" + resourcesPath + ":resources_lib"
}
}
}

l.generateJavaLibrary(args.File, args.Rel, filepath.Base(args.Rel), productionJavaFiles.SortedSlice(), resourcesDirectRef, resourcesRuntimeDep, allPackageNames, nonLocalProductionJavaImports, nonLocalJavaExports, annotationProcessorClasses, false, javaLibraryKind, &res, cfg, args.Config.RepoName)
}

var testHelperJavaClasses *sorted_set.SortedSet[types.ClassName]
Expand Down Expand Up @@ -236,7 +341,8 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes
testJavaImportsWithHelpers.Add(tf.pkg)
srcs = append(srcs, tf.pathRelativeToBazelWorkspaceRoot)
}
l.generateJavaLibrary(args.File, args.Rel, filepath.Base(args.Rel), srcs, packages, testJavaImports, nonLocalJavaExports, annotationProcessorClasses, true, javaLibraryKind, &res, cfg, args.Config.RepoName)
// Test helper libraries typically don't have resources
l.generateJavaLibrary(args.File, args.Rel, filepath.Base(args.Rel), srcs, "", "", packages, testJavaImports, nonLocalJavaExports, annotationProcessorClasses, true, javaLibraryKind, &res, cfg, args.Config.RepoName)
}
}

Expand Down Expand Up @@ -471,7 +577,7 @@ func accumulateJavaFile(cfg *javaconfig.Config, testJavaFiles, testHelperJavaFil
}
}

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) {
func (l javaLang) generateJavaLibrary(file *rule.File, pathToPackageRelativeToBazelWorkspace, name string, srcsRelativeToBazelWorkspace []string, resourcesDirectRef string, resourcesRuntimeDep 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) {
r := rule.NewRule(javaLibraryRuleKind, name)

srcs := make([]string, 0, len(srcsRelativeToBazelWorkspace))
Expand All @@ -480,8 +586,25 @@ func (l javaLang) generateJavaLibrary(file *rule.File, pathToPackageRelativeToBa
}
sort.Strings(srcs)

// Handle resources based on mode
if resourcesDirectRef != "" {
// Module mode: add resources directly to the library
r.SetAttr("resources", []string{resourcesDirectRef})
}

// This is so we would default ALL runtime_deps to "keep" mode
runtimeDeps := l.collectRuntimeDeps(javaLibraryRuleKind, name, file)

// Package mode: add resources_lib as runtime_deps
if resourcesRuntimeDep != "" {
parsedLabel, err := label.Parse(resourcesRuntimeDep)
if err != nil {
l.logger.Error().Err(err).Str("label", resourcesRuntimeDep).Msg("Failed to parse resources runtime dep label")
} else {
runtimeDeps.Add(parsedLabel)
}
}

if runtimeDeps.Len() > 0 {
r.SetAttr("runtime_deps", labelsToStrings(runtimeDeps.SortedSlice()))
}
Expand Down Expand Up @@ -664,6 +787,43 @@ func (l javaLang) generateJavaTestSuite(file *rule.File, name string, srcs []str
res.Imports = append(res.Imports, resolveInput)
}

// collectResourceFilesRecursively walks through subdirectories and collects resource files
func collectResourceFilesRecursively(args language.GenerateArgs, subdirPath string) []string {
var resourceFiles []string

// Read the subdirectory using os.ReadDir
fullPath := filepath.Join(args.Dir, subdirPath)
entries, err := os.ReadDir(fullPath)
if err != nil {
// If we can't read the directory, skip it
return resourceFiles
}

for _, entry := range entries {
name := entry.Name()

// Skip BUILD files and other non-resource files
if name == "BUILD" || name == "BUILD.bazel" {
continue
}

entryPath := path.Join(subdirPath, name)

if entry.IsDir() {
subFiles := collectResourceFilesRecursively(args, entryPath)
resourceFiles = append(resourceFiles, subFiles...)
} else {
// Check if this is a resource file
ext := filepath.Ext(name)
if ext != ".java" {
resourceFiles = append(resourceFiles, entryPath)
}
}
}

return resourceFiles
}

func filterStrSlice(elts []string, f func(string) bool) []string {
var out []string
for _, elt := range elts {
Expand Down
30 changes: 30 additions & 0 deletions java/gazelle/javaconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ const (
// Can be either "true" or "false". Defaults to "true".
// Inherited by children packages, can only be set at the root of the repository.
JavaResolveToJavaExports = "java_resolve_to_java_exports"

// JavaSourcesetRoot explicitly marks a directory as the root of a sourceset.
// This provides a clear override to the auto-detection algorithm.
// Example: # gazelle:java_sourceset_root my/custom/src
JavaSourcesetRoot = "java_sourceset_root"

// JavaStripResourcesPrefix overrides the path-stripping behavior for resources.
// This is a direct way to specify the resource_strip_prefix for all resources in a directory.
// Example: # gazelle:java_strip_resources_prefix my/data/config
JavaStripResourcesPrefix = "java_strip_resources_prefix"
)

// Configs is an extension of map[string]*Config. It provides finding methods
Expand Down Expand Up @@ -124,6 +134,8 @@ type Config struct {
annotationToWrapper map[string]string
mavenRepositoryName string
annotationProcessorFullQualifiedClassToPluginClass map[string]*sorted_set.SortedSet[types.ClassName]
sourcesetRoot string
stripResourcesPrefix string
}

type LoadInfo struct {
Expand All @@ -148,6 +160,8 @@ func New(repoRoot string) *Config {
annotationToWrapper: make(map[string]string),
mavenRepositoryName: "maven",
annotationProcessorFullQualifiedClassToPluginClass: make(map[string]*sorted_set.SortedSet[types.ClassName]),
sourcesetRoot: "",
stripResourcesPrefix: "",
}
}

Expand Down Expand Up @@ -316,6 +330,22 @@ func (c *Config) SetResolveToJavaExports(resolve bool) {
c.resolveToJavaExports.Initialize(resolve)
}

func (c *Config) SourcesetRoot() string {
return c.sourcesetRoot
}

func (c *Config) SetSourcesetRoot(root string) {
c.sourcesetRoot = root
}

func (c *Config) StripResourcesPrefix() string {
return c.stripResourcesPrefix
}

func (c *Config) SetStripResourcesPrefix(prefix string) {
c.stripResourcesPrefix = prefix
}

func equalStringSlices(l, r []string) bool {
if len(l) != len(r) {
return false
Expand Down
6 changes: 6 additions & 0 deletions java/gazelle/lang.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ var baseJavaLoads = []rule.LoadInfo{
"java_export",
},
},
{
Name: "@rules_pkg//pkg:mappings.bzl",
Symbols: []string{
"pkg_files",
},
},
}

func (l javaLang) Loads() []rule.LoadInfo {
Expand Down
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"version": "2"
}
Loading
Loading