Skip to content

Commit 87f41b7

Browse files
committed
starlark_repository: capture upstream BUILD files as starlark_package targets
Adds a new "preserve" build_file_generation mode that runs cmd/preserve_packages between fetch_repo and gazelle. The tool mirrors fetch_repo -clean semantics (deletes WORKSPACE / MODULE.bazel / etc.) but renames BUILD and BUILD.bazel to BUILD.package and BUILD.bazel.package so the upstream package layout is preserved as opaque files rather than discarded. The starlarkrepository gazelle extension grows a parallel pair of kinds alongside starlark_module / starlark_module_library: - starlark_package: one per BUILD*.package, src points at the preserved file. Name is the dot-sanitized filename (BUILD.package -> BUILD_package) to avoid clashing with starlark_module names derived from .bzl files. - starlark_package_library: emitted at the configured root, named "starlark_packages" (not "packages", which would clash with packages.bzl). Its `packages` attr is populated in Resolve via the RuleIndex. starlark_repository.archive/.local default build_file_generation to "preserve" so users get the new behavior automatically; they can still override.
1 parent 721e152 commit 87f41b7

9 files changed

Lines changed: 325 additions & 15 deletions

File tree

cmd/preserve_packages/BUILD.bazel

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
2+
3+
go_library(
4+
name = "preserve_packages_lib",
5+
srcs = ["main.go"],
6+
importpath = "github.com/stackb/rules_proto/v4/cmd/preserve_packages",
7+
visibility = ["//visibility:private"],
8+
)
9+
10+
go_binary(
11+
name = "preserve_packages",
12+
embed = [":preserve_packages_lib"],
13+
visibility = ["//visibility:public"],
14+
)

cmd/preserve_packages/main.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// preserve_packages walks a fetched external repository and rewrites the
2+
// upstream files that fetch_repo -clean would otherwise delete. BUILD and
3+
// BUILD.bazel are renamed to BUILD.package / BUILD.bazel.package so the
4+
// starlarkrepository gazelle extension can capture them as starlark_package
5+
// rules; everything else fetch_repo -clean removes (MODULE.bazel,
6+
// WORKSPACE, …) is deleted outright.
7+
//
8+
// Intended to be invoked from rules/proto/proto_repository.bzl when
9+
// build_file_generation = "preserve".
10+
package main
11+
12+
import (
13+
"flag"
14+
"fmt"
15+
"io/fs"
16+
"log"
17+
"os"
18+
"path/filepath"
19+
)
20+
21+
var renameMap = map[string]string{
22+
"BUILD": "BUILD.package",
23+
"BUILD.bazel": "BUILD.bazel.package",
24+
}
25+
26+
var deleteSet = map[string]bool{
27+
"MODULE.bazel": true,
28+
"MODULE.bazel.lock": true,
29+
"WORKSPACE": true,
30+
"WORKSPACE.bazel": true,
31+
"WORKSPACE.bzlmod": true,
32+
}
33+
34+
func main() {
35+
root := flag.String("root", "", "repo root to walk (required)")
36+
flag.Parse()
37+
if *root == "" {
38+
log.Fatal("preserve_packages: -root is required")
39+
}
40+
41+
if err := run(*root); err != nil {
42+
log.Fatalf("preserve_packages: %v", err)
43+
}
44+
}
45+
46+
func run(root string) error {
47+
return filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
48+
if err != nil {
49+
return err
50+
}
51+
if info.IsDir() {
52+
return nil
53+
}
54+
name := info.Name()
55+
if dst, ok := renameMap[name]; ok {
56+
target := filepath.Join(filepath.Dir(path), dst)
57+
if err := os.Rename(path, target); err != nil {
58+
return fmt.Errorf("rename %s -> %s: %w", path, target, err)
59+
}
60+
return nil
61+
}
62+
if deleteSet[name] {
63+
if err := os.Remove(path); err != nil {
64+
return fmt.Errorf("remove %s: %w", path, err)
65+
}
66+
}
67+
return nil
68+
})
69+
}

extensions/starlark_repository.bzl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ def _extension_metadata(
3939
**metadata_kwargs
4040
)
4141

42+
def _default_preserve(kwargs):
43+
"""Sets build_file_generation = "preserve" by default.
44+
45+
starlark_repository exists specifically to capture upstream module
46+
contents for introspection. The "preserve" mode is the only mode that
47+
produces the starlark_package_library aggregator, so it's the desired
48+
default. Users can still override (e.g. to "on" or "clean") by passing
49+
build_file_generation explicitly on the tag.
50+
"""
51+
if not kwargs.get("build_file_generation"):
52+
kwargs["build_file_generation"] = "preserve"
53+
4254
def _starlark_repository_impl(module_ctx):
4355
# named_archives / named_locals are dicts<K,V> where V is the kwargs for
4456
# the underlying "starlark_repository" repo rule and K is the tag.name
@@ -58,6 +70,7 @@ def _starlark_repository_impl(module_ctx):
5870
for attr in _starlark_repository_archive_attrs.keys()
5971
if hasattr(tag, attr)
6072
}
73+
_default_preserve(kwargs)
6174
named_archives[tag.name] = kwargs
6275
for tag in module.tags.local:
6376
kwargs = {
@@ -69,6 +82,7 @@ def _starlark_repository_impl(module_ctx):
6982
# The user-facing attr is "path"; the underlying repo rule expects
7083
# "local_path" (a sibling of "urls" / "commit" / "version").
7184
kwargs["local_path"] = kwargs.pop("path")
85+
_default_preserve(kwargs)
7286
named_locals[tag.name] = kwargs
7387

7488
# declare a repository rule foreach one

language/starlarkrepository/language.go

Lines changed: 121 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,20 @@ import (
4545
)
4646

4747
const (
48-
languageName = "starlarkrepository"
49-
repoNameDirectiveName = languageName + "_repo_name"
50-
rootDirectiveName = languageName + "_root"
51-
excludeDirectiveName = languageName + "_exclude"
52-
logFileDirectiveName = languageName + "_log_file"
53-
starlarkModuleKind = "starlark_module"
54-
starlarkModuleLibraryKind = "starlark_module_library"
55-
starlarkModuleLibraryName = "modules"
56-
fileType = ".bzl"
57-
visibilityPublic = "//visibility:public"
48+
languageName = "starlarkrepository"
49+
repoNameDirectiveName = languageName + "_repo_name"
50+
rootDirectiveName = languageName + "_root"
51+
excludeDirectiveName = languageName + "_exclude"
52+
logFileDirectiveName = languageName + "_log_file"
53+
starlarkModuleKind = "starlark_module"
54+
starlarkModuleLibraryKind = "starlark_module_library"
55+
starlarkModuleLibraryName = "modules"
56+
starlarkPackageKind = "starlark_package"
57+
starlarkPackageLibraryKind = "starlark_package_library"
58+
starlarkPackageLibraryName = "starlark_packages"
59+
bzlFileType = ".bzl"
60+
packageFileType = ".package"
61+
visibilityPublic = "//visibility:public"
5862
)
5963

6064
var (
@@ -73,6 +77,17 @@ var (
7377
NonEmptyAttrs: map[string]bool{"src": true},
7478
},
7579
}
80+
starlarkPackageLibraryKindInfo = map[string]rule.KindInfo{
81+
starlarkPackageLibraryKind: {
82+
NonEmptyAttrs: map[string]bool{"packages": true},
83+
ResolveAttrs: map[string]bool{"packages": true},
84+
},
85+
}
86+
starlarkPackageKindInfo = map[string]rule.KindInfo{
87+
starlarkPackageKind: {
88+
NonEmptyAttrs: map[string]bool{"src": true},
89+
},
90+
}
7691
starlarkModuleLibraryLoadInfo = rule.LoadInfo{
7792
Name: "@build_stack_rules_proto//rules:starlark_module_library.bzl",
7893
Symbols: []string{starlarkModuleLibraryKind},
@@ -81,6 +96,14 @@ var (
8196
Name: "@build_stack_rules_proto//rules:starlark_module.bzl",
8297
Symbols: []string{starlarkModuleKind},
8398
}
99+
starlarkPackageLibraryLoadInfo = rule.LoadInfo{
100+
Name: "@build_stack_rules_proto//rules:starlark_package_library.bzl",
101+
Symbols: []string{starlarkPackageLibraryKind},
102+
}
103+
starlarkPackageLoadInfo = rule.LoadInfo{
104+
Name: "@build_stack_rules_proto//rules:starlark_package.bzl",
105+
Symbols: []string{starlarkPackageKind},
106+
}
84107
)
85108

86109
type starlarkRepositoryLang struct {
@@ -201,6 +224,8 @@ func (*starlarkRepositoryLang) Kinds() map[string]rule.KindInfo {
201224
kinds := map[string]rule.KindInfo{}
202225
maps.Copy(kinds, starlarkModuleLibraryKindInfo)
203226
maps.Copy(kinds, starlarkModuleKindInfo)
227+
maps.Copy(kinds, starlarkPackageLibraryKindInfo)
228+
maps.Copy(kinds, starlarkPackageKindInfo)
204229
return kinds
205230
}
206231

@@ -211,6 +236,8 @@ func (*starlarkRepositoryLang) Loads() []rule.LoadInfo {
211236
return []rule.LoadInfo{
212237
starlarkModuleLibraryLoadInfo,
213238
starlarkModuleLoadInfo,
239+
starlarkPackageLibraryLoadInfo,
240+
starlarkPackageLoadInfo,
214241
}
215242
}
216243

@@ -229,6 +256,8 @@ func (ext *starlarkRepositoryLang) Imports(c *config.Config, r *rule.Rule, f *ru
229256
switch r.Kind() {
230257
case starlarkModuleKind:
231258
return ext.starlarkModuleImports(c, r, f)
259+
case starlarkPackageKind:
260+
return ext.starlarkPackageImports(c, r, f)
232261
default:
233262
return nil
234263
}
@@ -242,6 +271,13 @@ func (ext *starlarkRepositoryLang) starlarkModuleImports(_ *config.Config, r *ru
242271
}
243272
}
244273

274+
func (ext *starlarkRepositoryLang) starlarkPackageImports(_ *config.Config, r *rule.Rule, f *rule.File) []resolve.ImportSpec {
275+
return []resolve.ImportSpec{
276+
{Lang: languageName, Imp: fmt.Sprintf("//%s:%s", f.Pkg, r.AttrString("src"))},
277+
{Lang: languageName, Imp: starlarkPackageKind},
278+
}
279+
}
280+
245281
// Embeds returns a list of labels of rules that the given rule embeds. If a
246282
// rule is embedded by another importable rule of the same language, only the
247283
// embedding rule will be indexed. The embedding rule will inherit the imports
@@ -261,6 +297,8 @@ func (ext *starlarkRepositoryLang) Resolve(c *config.Config, ix *resolve.RuleInd
261297
switch r.Kind() {
262298
case starlarkModuleLibraryKind:
263299
ext.starlarkModuleLibraryResolve(c, ix, rc, r, importsRaw, from)
300+
case starlarkPackageLibraryKind:
301+
ext.starlarkPackageLibraryResolve(c, ix, rc, r, importsRaw, from)
264302
}
265303
}
266304

@@ -309,10 +347,24 @@ func (ext *starlarkRepositoryLang) GenerateRules(args language.GenerateArgs) (re
309347
log.Printf("generated %s %s/%s", r.Kind(), args.Rel, r.Name())
310348
}
311349

350+
for _, f := range args.RegularFiles {
351+
if !isBuildPackageFile(f) {
352+
continue
353+
}
354+
r, imports := ext.starlarkPackageRule(args, f)
355+
result.Gen = append(result.Gen, r)
356+
result.Imports = append(result.Imports, imports)
357+
log.Printf("generated %s %s/%s", r.Kind(), args.Rel, r.Name())
358+
}
359+
312360
if _, ok := getMatchingRoot(args.Rel, ext.roots); ok {
313361
r, imports := ext.starlarkModuleLibraryRule(args)
314362
result.Gen = append(result.Gen, r)
315363
result.Imports = append(result.Imports, imports)
364+
365+
r, imports = ext.starlarkPackageLibraryRule(args)
366+
result.Gen = append(result.Gen, r)
367+
result.Imports = append(result.Imports, imports)
316368
}
317369

318370
return
@@ -344,7 +396,7 @@ func mustListFiles(logf LogFunc, dir string) []string {
344396
}
345397
func (ext *starlarkRepositoryLang) starlarkModuleRule(args language.GenerateArgs, src string, loadStmts []*build.LoadStmt) (*rule.Rule, []any) {
346398

347-
name := strings.TrimSuffix(src, fileType)
399+
name := strings.TrimSuffix(src, bzlFileType)
348400
ext.logf("generating %s rule for %s //%s:%s", starlarkModuleKind, src, args.Rel, name)
349401

350402
loads := make([]string, 0, len(loadStmts))
@@ -376,6 +428,59 @@ func (ext *starlarkRepositoryLang) starlarkModuleLibraryRule(_ language.Generate
376428
return r, []any{}
377429
}
378430

431+
func (ext *starlarkRepositoryLang) starlarkPackageRule(args language.GenerateArgs, src string) (*rule.Rule, []any) {
432+
// Sanitize the filename into a target name: "BUILD.package" -> "BUILD_package",
433+
// "BUILD.bazel.package" -> "BUILD_bazel_package". This avoids clashing with
434+
// `pkg.bzl`-derived `starlark_module(name = "pkg")` targets that exist in many
435+
// Starlark codebases.
436+
name := strings.ReplaceAll(src, ".", "_")
437+
ext.logf("generating %s rule for %s //%s:%s", starlarkPackageKind, src, args.Rel, name)
438+
439+
r := rule.NewRule(starlarkPackageKind, name)
440+
r.SetAttr("src", src)
441+
r.SetAttr("visibility", []string{visibilityPublic})
442+
443+
return r, []any{}
444+
}
445+
446+
func (ext *starlarkRepositoryLang) starlarkPackageLibraryRule(_ language.GenerateArgs) (*rule.Rule, []any) {
447+
r := rule.NewRule(starlarkPackageLibraryKind, starlarkPackageLibraryName)
448+
if ext.bazelVersion != "" {
449+
r.SetAttr("bazelversion", ext.bazelVersion)
450+
}
451+
if len(ext.bazelIgnore) > 0 {
452+
r.SetAttr("bazelignore", ext.bazelIgnore)
453+
}
454+
r.SetAttr("visibility", []string{visibilityPublic})
455+
return r, []any{}
456+
}
457+
458+
func (ext *starlarkRepositoryLang) starlarkPackageLibraryResolve(c *config.Config, ix *resolve.RuleIndex, _ *repo.RemoteCache, r *rule.Rule, _ interface{}, from label.Label) {
459+
root, isRoot := getMatchingRoot(from.Pkg, ext.roots)
460+
if !isRoot {
461+
ext.logf("skipping packages resolution for %v (not a root: %v)", from, ext.roots)
462+
return
463+
}
464+
465+
var packages []string
466+
467+
matches := ix.FindRulesByImportWithConfig(c, resolve.ImportSpec{
468+
Lang: languageName,
469+
Imp: starlarkPackageKind,
470+
}, languageName)
471+
for _, m := range matches {
472+
depLabel := m.Label.Rel(from.Repo, from.Pkg)
473+
if strings.HasPrefix(depLabel.Pkg, root) {
474+
packages = append(packages, depLabel.String())
475+
}
476+
}
477+
478+
if len(packages) > 0 {
479+
sort.Strings(packages)
480+
r.SetAttr("packages", packages)
481+
}
482+
}
483+
379484
func (ext *starlarkRepositoryLang) starlarkModuleLibraryResolve(c *config.Config, ix *resolve.RuleIndex, _ *repo.RemoteCache, r *rule.Rule, _ interface{}, from label.Label) {
380485
// only perform resolve if this is one of the roots
381486
root, isRoot := getMatchingRoot(from.Pkg, ext.roots)
@@ -511,7 +616,11 @@ func readFileLines(filePath string, logf LogFunc) ([]string, error) {
511616
}
512617

513618
func isBzlSourceFile(f string) bool {
514-
return strings.HasSuffix(f, fileType) && !ignoreSuffix.Matches(f)
619+
return strings.HasSuffix(f, bzlFileType) && !ignoreSuffix.Matches(f)
620+
}
621+
622+
func isBuildPackageFile(f string) bool {
623+
return f == "BUILD.package" || f == "BUILD.bazel.package"
515624
}
516625

517626
func getBzlFileLoadsStmts(path, rel string, logf LogFunc) (*build.File, []*build.LoadStmt, error) {

rules/private/proto_repository_tools.bzl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ filegroup(
2424
name = "gazelle",
2525
srcs = ["bin/gazelle{extension}"],
2626
)
27+
28+
filegroup(
29+
name = "preserve_packages",
30+
srcs = ["bin/preserve_packages{extension}"],
31+
)
2732
"""
2833

2934
def _proto_repository_tools_impl(ctx):
@@ -90,6 +95,7 @@ def _proto_repository_tools_impl(ctx):
9095
"-asmflags",
9196
"all=-trimpath=" + env["GOPATH"],
9297
"github.com/stackb/rules_proto/v4/cmd/gazelle",
98+
"github.com/stackb/rules_proto/v4/cmd/preserve_packages",
9399
]
94100
result = env_execute(ctx, args, environment = env)
95101
if result.return_code:

rules/private/proto_repository_tools_srcs.bzl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ PROTO_REPOSITORY_TOOLS_SRCS = [
2121
"@build_stack_rules_proto//cmd/gazelle:wspace.go",
2222
"@build_stack_rules_proto//cmd/gencopy:BUILD.bazel",
2323
"@build_stack_rules_proto//cmd/gencopy:gencopy.go",
24+
"@build_stack_rules_proto//cmd/preserve_packages:BUILD.bazel",
25+
"@build_stack_rules_proto//cmd/preserve_packages:main.go",
2426
"@build_stack_rules_proto//example:BUILD.bazel",
2527
"@build_stack_rules_proto//example/assets:BUILD.bazel",
2628
"@build_stack_rules_proto//example/assets:api.pb.go",

0 commit comments

Comments
 (0)