diff --git a/java/gazelle/configure.go b/java/gazelle/configure.go index 488f8d67..a1c862a0 100644 --- a/java/gazelle/configure.go +++ b/java/gazelle/configure.go @@ -3,6 +3,8 @@ package gazelle import ( "flag" "fmt" + "path" + "path/filepath" "strings" "github.com/bazel-contrib/rules_jvm/java/gazelle/javaconfig" @@ -67,6 +69,8 @@ func (jc *Configurer) KnownDirectives() []string { javaconfig.JavaMavenRepositoryName, javaconfig.JavaAnnotationProcessorPlugin, javaconfig.JavaResolveToJavaExports, + javaconfig.JavaSourcesetRoot, + javaconfig.JavaStripResourcesPrefix, } } @@ -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 { @@ -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) } } diff --git a/java/gazelle/generate.go b/java/gazelle/generate.go index 63413ed2..876adef9 100644 --- a/java/gazelle/generate.go +++ b/java/gazelle/generate.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "path" "path/filepath" "sort" "strings" @@ -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. @@ -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] @@ -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) } } @@ -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)) @@ -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())) } @@ -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 { diff --git a/java/gazelle/javaconfig/config.go b/java/gazelle/javaconfig/config.go index 3604f3e4..80eda03d 100644 --- a/java/gazelle/javaconfig/config.go +++ b/java/gazelle/javaconfig/config.go @@ -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 @@ -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 { @@ -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: "", } } @@ -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 diff --git a/java/gazelle/lang.go b/java/gazelle/lang.go index affcee8d..9c023651 100644 --- a/java/gazelle/lang.go +++ b/java/gazelle/lang.go @@ -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 { diff --git a/java/gazelle/testdata/resources_custom_sourceset/WORKSPACE b/java/gazelle/testdata/resources_custom_sourceset/WORKSPACE new file mode 100644 index 00000000..e69de29b diff --git a/java/gazelle/testdata/resources_custom_sourceset/expectedStderr.txt b/java/gazelle/testdata/resources_custom_sourceset/expectedStderr.txt new file mode 100644 index 00000000..e69de29b diff --git a/java/gazelle/testdata/resources_custom_sourceset/maven_install.json b/java/gazelle/testdata/resources_custom_sourceset/maven_install.json new file mode 100644 index 00000000..59b5aec7 --- /dev/null +++ b/java/gazelle/testdata/resources_custom_sourceset/maven_install.json @@ -0,0 +1,3 @@ +{ + "version": "2" +} \ No newline at end of file diff --git a/java/gazelle/testdata/resources_custom_sourceset/src/sample/java/com/example/App.java b/java/gazelle/testdata/resources_custom_sourceset/src/sample/java/com/example/App.java new file mode 100644 index 00000000..2235cbaa --- /dev/null +++ b/java/gazelle/testdata/resources_custom_sourceset/src/sample/java/com/example/App.java @@ -0,0 +1,7 @@ +package com.example; + +public class App { + public static void main(String[] args) { + System.out.println("Hello from sample sourceset!"); + } +} diff --git a/java/gazelle/testdata/resources_custom_sourceset/src/sample/java/com/example/BUILD.in b/java/gazelle/testdata/resources_custom_sourceset/src/sample/java/com/example/BUILD.in new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/java/gazelle/testdata/resources_custom_sourceset/src/sample/java/com/example/BUILD.in @@ -0,0 +1 @@ + diff --git a/java/gazelle/testdata/resources_custom_sourceset/src/sample/java/com/example/BUILD.out b/java/gazelle/testdata/resources_custom_sourceset/src/sample/java/com/example/BUILD.out new file mode 100644 index 00000000..980b14d8 --- /dev/null +++ b/java/gazelle/testdata/resources_custom_sourceset/src/sample/java/com/example/BUILD.out @@ -0,0 +1,15 @@ +load("@rules_java//java:defs.bzl", "java_binary", "java_library") + +java_library( + name = "example", + srcs = ["App.java"], + visibility = ["//:__subpackages__"], + runtime_deps = ["//src/sample/resources:resources_lib"], +) + +java_binary( + name = "App", + main_class = "com.example.App", + visibility = ["//visibility:public"], + runtime_deps = [":example"], +) diff --git a/java/gazelle/testdata/resources_custom_sourceset/src/sample/resources/BUILD.in b/java/gazelle/testdata/resources_custom_sourceset/src/sample/resources/BUILD.in new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/java/gazelle/testdata/resources_custom_sourceset/src/sample/resources/BUILD.in @@ -0,0 +1 @@ + diff --git a/java/gazelle/testdata/resources_custom_sourceset/src/sample/resources/BUILD.out b/java/gazelle/testdata/resources_custom_sourceset/src/sample/resources/BUILD.out new file mode 100644 index 00000000..cad10be9 --- /dev/null +++ b/java/gazelle/testdata/resources_custom_sourceset/src/sample/resources/BUILD.out @@ -0,0 +1,17 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("@rules_pkg//pkg:mappings.bzl", "pkg_files") + +pkg_files( + name = "resources", + srcs = [ + "com/example/app.properties", + "config.xml", + ], + strip_prefix = "src/sample/resources", +) + +java_library( + name = "resources_lib", + resources = [":resources"], + visibility = ["//:__subpackages__"], +) diff --git a/java/gazelle/testdata/resources_custom_sourceset/src/sample/resources/com/example/app.properties b/java/gazelle/testdata/resources_custom_sourceset/src/sample/resources/com/example/app.properties new file mode 100644 index 00000000..27f8ea94 --- /dev/null +++ b/java/gazelle/testdata/resources_custom_sourceset/src/sample/resources/com/example/app.properties @@ -0,0 +1,3 @@ +app.name=Sample Application +app.version=1.0.0 +app.sourceset=sample diff --git a/java/gazelle/testdata/resources_custom_sourceset/src/sample/resources/config.xml b/java/gazelle/testdata/resources_custom_sourceset/src/sample/resources/config.xml new file mode 100644 index 00000000..b0a95a41 --- /dev/null +++ b/java/gazelle/testdata/resources_custom_sourceset/src/sample/resources/config.xml @@ -0,0 +1,5 @@ + + + sample + value +