Skip to content

Commit 5c7b2ab

Browse files
authored
Auto-sync root Cargo.toml workspace members and proto_compile_assets deps (#420)
After GenerateRules has run for every package, rewrite two marker-delimited sections at the repo root: - Cargo.toml between `# gazelle:proto_rust_members start/end` — populated with one entry per package that emitted a proto_rust_library. - BUILD.bazel between `# gazelle:vendor_proto_sources_deps start/end` — populated with the underlying _lib target of each proto_rust_library plus every proto_compiled_sources rule. Both rewrites are no-ops when the markers (or the target file) are absent, so existing repos without the markers see no change. Entries are sorted and deduplicated; the file is only rewritten when content actually changes.
1 parent a9a41ba commit 5c7b2ab

3 files changed

Lines changed: 167 additions & 0 deletions

File tree

pkg/language/protobuf/generate.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,23 @@ func (pl *protobufLang) GenerateRules(args language.GenerateArgs) language.Gener
102102
imports[i] = r.PrivateAttr(config.GazelleImportsKey)
103103
internalLabel := label.New("", args.Rel, r.Name())
104104
protoc.GlobalRuleIndex().Put(internalLabel, r)
105+
switch r.Kind() {
106+
case "proto_rust_library":
107+
pl.protoRustLibraryPackages = append(pl.protoRustLibraryPackages, args.Rel)
108+
// The proto_rust_library macro's underlying _proto_rust_lib rule
109+
// (named "<name>_lib") is what provides ProtoCompileInfo for the
110+
// wrapper lib.rs + Cargo.toml; that's the label that belongs in
111+
// the root proto_compile_assets aggregator.
112+
pl.vendorAssetLabels = append(pl.vendorAssetLabels, "//"+args.Rel+":"+r.Name()+"_lib")
113+
case "proto_compiled_sources":
114+
pl.vendorAssetLabels = append(pl.vendorAssetLabels, "//"+args.Rel+":"+r.Name())
115+
}
116+
}
117+
118+
// Capture the repo root on the first call so DoneGeneratingRules can
119+
// locate the root Cargo.toml without access to *config.Config.
120+
if pl.repoRoot == "" {
121+
pl.repoRoot = args.Config.RepoRoot
105122
}
106123

107124
// special case if this is the root BUILD file and the user requested to

pkg/language/protobuf/lang.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@ type protobufLang struct {
4040
starlarkRules arrayFlags
4141
// starlarkPlugins stores custom starlark proto plugin names in the form filename%pluginname
4242
starlarkPlugins arrayFlags
43+
// protoRustLibraryPackages collects the workspace-relative path of every
44+
// package that emits a proto_rust_library rule. Populated in
45+
// GenerateRules and consumed in DoneGeneratingRules to update the root
46+
// Cargo.toml [workspace] members list.
47+
protoRustLibraryPackages []string
48+
// vendorAssetLabels collects bazel labels of every generated rule that
49+
// provides ProtoCompileInfo and should appear in the root
50+
// `proto_compile_assets` aggregator. Populated in GenerateRules and
51+
// consumed in DoneGeneratingRules to update the deps list of the
52+
// vendoring target between the vendor_proto_sources_deps markers.
53+
vendorAssetLabels []string
54+
// repoRoot is captured from the first GenerateRules call so
55+
// DoneGeneratingRules (which receives no config) can locate the root
56+
// Cargo.toml and BUILD.bazel.
57+
repoRoot string
4358
}
4459

4560
// Name implements part of the language.Language interface.

pkg/language/protobuf/lifecycle.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,151 @@ package protobuf
22

33
import (
44
"context"
5+
"fmt"
6+
"log"
7+
"os"
8+
"path/filepath"
9+
"sort"
10+
"strings"
511
)
612

713
// Before implements part of the language.LifecycleManager interface.
814
func (pl *protobufLang) Before(context.Context) {
915
}
1016

1117
// DoneGeneratingRules implements part of the language.LifecycleManager interface.
18+
//
19+
// Performs two cross-package syncs that need every GenerateRules call to
20+
// have completed first:
21+
//
22+
// 1. Root Cargo.toml [workspace] members list — the lines between the
23+
// `# gazelle:proto_rust_members start/end` markers are replaced with
24+
// one entry per package that emitted a proto_rust_library.
25+
//
26+
// 2. Root BUILD.bazel proto_compile_assets aggregator deps — the lines
27+
// between the `# gazelle:vendor_proto_sources_deps start/end` markers
28+
// are replaced with one entry per generated proto_compiled_sources rule
29+
// and one entry per proto_rust_library's underlying _lib target.
30+
//
31+
// Both syncs are no-ops when the corresponding markers are absent (or the
32+
// target file does not exist).
1233
func (pl *protobufLang) DoneGeneratingRules() {
34+
if pl.repoRoot == "" {
35+
return
36+
}
37+
if err := updateRootCargoMembers(pl.repoRoot, pl.protoRustLibraryPackages); err != nil {
38+
log.Printf("warning: could not update root Cargo.toml proto_rust_members: %v", err)
39+
}
40+
if err := updateRootVendorAssetsDeps(pl.repoRoot, pl.vendorAssetLabels); err != nil {
41+
log.Printf("warning: could not update root BUILD.bazel vendor_proto_sources_deps: %v", err)
42+
}
1343
}
1444

1545
// AfterResolvingDeps implements part of the language.LifecycleManager interface.
1646
func (pl *protobufLang) AfterResolvingDeps(context.Context) {
1747
}
48+
49+
const (
50+
cargoMembersStartMarker = "# gazelle:proto_rust_members start"
51+
cargoMembersEndMarker = "# gazelle:proto_rust_members end"
52+
vendorAssetsDepsStartMarker = "# gazelle:vendor_proto_sources_deps start"
53+
vendorAssetsDepsEndMarker = "# gazelle:vendor_proto_sources_deps end"
54+
)
55+
56+
// updateRootCargoMembers rewrites the gazelle:proto_rust_members marker
57+
// section in the root Cargo.toml with a sorted, deduplicated list of
58+
// `"<pkg>",` entries. No-op if the file is missing or the markers are
59+
// absent.
60+
func updateRootCargoMembers(repoRoot string, packages []string) error {
61+
return rewriteMarkerSection(
62+
filepath.Join(repoRoot, "Cargo.toml"),
63+
cargoMembersStartMarker,
64+
cargoMembersEndMarker,
65+
packages,
66+
"[workspace] members list",
67+
)
68+
}
69+
70+
// updateRootVendorAssetsDeps rewrites the gazelle:vendor_proto_sources_deps
71+
// marker section in the root BUILD.bazel with a sorted, deduplicated list
72+
// of `"<label>",` entries. No-op if the file is missing or the markers are
73+
// absent.
74+
func updateRootVendorAssetsDeps(repoRoot string, labels []string) error {
75+
return rewriteMarkerSection(
76+
filepath.Join(repoRoot, "BUILD.bazel"),
77+
vendorAssetsDepsStartMarker,
78+
vendorAssetsDepsEndMarker,
79+
labels,
80+
"vendor_proto_sources deps list",
81+
)
82+
}
83+
84+
// rewriteMarkerSection replaces every line between startMarker and
85+
// endMarker in path with a sorted, deduplicated quoted-comma list of
86+
// entries. Marker lines themselves are preserved; indentation is taken
87+
// from the start marker. The file is not written if the resulting content
88+
// is identical (avoids spurious mtime bumps). When entries is non-empty
89+
// but markers are absent, a warning is logged once with the supplied
90+
// description so the maintainer knows where to add them.
91+
func rewriteMarkerSection(path, startMarker, endMarker string, entries []string, description string) error {
92+
src, err := os.ReadFile(path)
93+
if err != nil {
94+
if os.IsNotExist(err) {
95+
return nil
96+
}
97+
return fmt.Errorf("read %s: %w", path, err)
98+
}
99+
100+
seen := make(map[string]bool, len(entries))
101+
uniq := make([]string, 0, len(entries))
102+
for _, e := range entries {
103+
if seen[e] {
104+
continue
105+
}
106+
seen[e] = true
107+
uniq = append(uniq, e)
108+
}
109+
sort.Strings(uniq)
110+
111+
lines := strings.Split(string(src), "\n")
112+
startIdx, endIdx := -1, -1
113+
var indent string
114+
for i, line := range lines {
115+
trimmed := strings.TrimSpace(line)
116+
if startIdx < 0 && trimmed == startMarker {
117+
startIdx = i
118+
indent = line[:len(line)-len(strings.TrimLeft(line, " \t"))]
119+
continue
120+
}
121+
if startIdx >= 0 && trimmed == endMarker {
122+
endIdx = i
123+
break
124+
}
125+
}
126+
if startIdx < 0 || endIdx < 0 {
127+
if len(uniq) > 0 {
128+
log.Printf("warning: %s has %d entries to enroll in %s but lacks the %s / %s markers — add them to enable auto-update", path, len(uniq), description, startMarker, endMarker)
129+
}
130+
return nil
131+
}
132+
133+
newSection := make([]string, 0, len(uniq)+2)
134+
newSection = append(newSection, lines[startIdx])
135+
for _, e := range uniq {
136+
newSection = append(newSection, fmt.Sprintf("%s\"%s\",", indent, e))
137+
}
138+
newSection = append(newSection, lines[endIdx])
139+
140+
out := append([]string{}, lines[:startIdx]...)
141+
out = append(out, newSection...)
142+
out = append(out, lines[endIdx+1:]...)
143+
144+
updated := strings.Join(out, "\n")
145+
if updated == string(src) {
146+
return nil
147+
}
148+
if err := os.WriteFile(path, []byte(updated), 0o644); err != nil {
149+
return fmt.Errorf("write %s: %w", path, err)
150+
}
151+
return nil
152+
}

0 commit comments

Comments
 (0)