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
17 changes: 17 additions & 0 deletions pkg/language/protobuf/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,23 @@ func (pl *protobufLang) GenerateRules(args language.GenerateArgs) language.Gener
imports[i] = r.PrivateAttr(config.GazelleImportsKey)
internalLabel := label.New("", args.Rel, r.Name())
protoc.GlobalRuleIndex().Put(internalLabel, r)
switch r.Kind() {
case "proto_rust_library":
pl.protoRustLibraryPackages = append(pl.protoRustLibraryPackages, args.Rel)
// The proto_rust_library macro's underlying _proto_rust_lib rule
// (named "<name>_lib") is what provides ProtoCompileInfo for the
// wrapper lib.rs + Cargo.toml; that's the label that belongs in
// the root proto_compile_assets aggregator.
pl.vendorAssetLabels = append(pl.vendorAssetLabels, "//"+args.Rel+":"+r.Name()+"_lib")
case "proto_compiled_sources":
pl.vendorAssetLabels = append(pl.vendorAssetLabels, "//"+args.Rel+":"+r.Name())
}
}

// Capture the repo root on the first call so DoneGeneratingRules can
// locate the root Cargo.toml without access to *config.Config.
if pl.repoRoot == "" {
pl.repoRoot = args.Config.RepoRoot
}

// special case if this is the root BUILD file and the user requested to
Expand Down
15 changes: 15 additions & 0 deletions pkg/language/protobuf/lang.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ type protobufLang struct {
starlarkRules arrayFlags
// starlarkPlugins stores custom starlark proto plugin names in the form filename%pluginname
starlarkPlugins arrayFlags
// protoRustLibraryPackages collects the workspace-relative path of every
// package that emits a proto_rust_library rule. Populated in
// GenerateRules and consumed in DoneGeneratingRules to update the root
// Cargo.toml [workspace] members list.
protoRustLibraryPackages []string
// vendorAssetLabels collects bazel labels of every generated rule that
// provides ProtoCompileInfo and should appear in the root
// `proto_compile_assets` aggregator. Populated in GenerateRules and
// consumed in DoneGeneratingRules to update the deps list of the
// vendoring target between the vendor_proto_sources_deps markers.
vendorAssetLabels []string
// repoRoot is captured from the first GenerateRules call so
// DoneGeneratingRules (which receives no config) can locate the root
// Cargo.toml and BUILD.bazel.
repoRoot string
}

// Name implements part of the language.Language interface.
Expand Down
135 changes: 135 additions & 0 deletions pkg/language/protobuf/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,151 @@ package protobuf

import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strings"
)

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

// DoneGeneratingRules implements part of the language.LifecycleManager interface.
//
// Performs two cross-package syncs that need every GenerateRules call to
// have completed first:
//
// 1. Root Cargo.toml [workspace] members list — the lines between the
// `# gazelle:proto_rust_members start/end` markers are replaced with
// one entry per package that emitted a proto_rust_library.
//
// 2. Root BUILD.bazel proto_compile_assets aggregator deps — the lines
// between the `# gazelle:vendor_proto_sources_deps start/end` markers
// are replaced with one entry per generated proto_compiled_sources rule
// and one entry per proto_rust_library's underlying _lib target.
//
// Both syncs are no-ops when the corresponding markers are absent (or the
// target file does not exist).
func (pl *protobufLang) DoneGeneratingRules() {
if pl.repoRoot == "" {
return
}
if err := updateRootCargoMembers(pl.repoRoot, pl.protoRustLibraryPackages); err != nil {
log.Printf("warning: could not update root Cargo.toml proto_rust_members: %v", err)
}
if err := updateRootVendorAssetsDeps(pl.repoRoot, pl.vendorAssetLabels); err != nil {
log.Printf("warning: could not update root BUILD.bazel vendor_proto_sources_deps: %v", err)
}
}

// AfterResolvingDeps implements part of the language.LifecycleManager interface.
func (pl *protobufLang) AfterResolvingDeps(context.Context) {
}

const (
cargoMembersStartMarker = "# gazelle:proto_rust_members start"
cargoMembersEndMarker = "# gazelle:proto_rust_members end"
vendorAssetsDepsStartMarker = "# gazelle:vendor_proto_sources_deps start"
vendorAssetsDepsEndMarker = "# gazelle:vendor_proto_sources_deps end"
)

// updateRootCargoMembers rewrites the gazelle:proto_rust_members marker
// section in the root Cargo.toml with a sorted, deduplicated list of
// `"<pkg>",` entries. No-op if the file is missing or the markers are
// absent.
func updateRootCargoMembers(repoRoot string, packages []string) error {
return rewriteMarkerSection(
filepath.Join(repoRoot, "Cargo.toml"),
cargoMembersStartMarker,
cargoMembersEndMarker,
packages,
"[workspace] members list",
)
}

// updateRootVendorAssetsDeps rewrites the gazelle:vendor_proto_sources_deps
// marker section in the root BUILD.bazel with a sorted, deduplicated list
// of `"<label>",` entries. No-op if the file is missing or the markers are
// absent.
func updateRootVendorAssetsDeps(repoRoot string, labels []string) error {
return rewriteMarkerSection(
filepath.Join(repoRoot, "BUILD.bazel"),
vendorAssetsDepsStartMarker,
vendorAssetsDepsEndMarker,
labels,
"vendor_proto_sources deps list",
)
}

// rewriteMarkerSection replaces every line between startMarker and
// endMarker in path with a sorted, deduplicated quoted-comma list of
// entries. Marker lines themselves are preserved; indentation is taken
// from the start marker. The file is not written if the resulting content
// is identical (avoids spurious mtime bumps). When entries is non-empty
// but markers are absent, a warning is logged once with the supplied
// description so the maintainer knows where to add them.
func rewriteMarkerSection(path, startMarker, endMarker string, entries []string, description string) error {
src, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("read %s: %w", path, err)
}

seen := make(map[string]bool, len(entries))
uniq := make([]string, 0, len(entries))
for _, e := range entries {
if seen[e] {
continue
}
seen[e] = true
uniq = append(uniq, e)
}
sort.Strings(uniq)

lines := strings.Split(string(src), "\n")
startIdx, endIdx := -1, -1
var indent string
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if startIdx < 0 && trimmed == startMarker {
startIdx = i
indent = line[:len(line)-len(strings.TrimLeft(line, " \t"))]
continue
}
if startIdx >= 0 && trimmed == endMarker {
endIdx = i
break
}
}
if startIdx < 0 || endIdx < 0 {
if len(uniq) > 0 {
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)
}
return nil
}

newSection := make([]string, 0, len(uniq)+2)
newSection = append(newSection, lines[startIdx])
for _, e := range uniq {
newSection = append(newSection, fmt.Sprintf("%s\"%s\",", indent, e))
}
newSection = append(newSection, lines[endIdx])

out := append([]string{}, lines[:startIdx]...)
out = append(out, newSection...)
out = append(out, lines[endIdx+1:]...)

updated := strings.Join(out, "\n")
if updated == string(src) {
return nil
}
if err := os.WriteFile(path, []byte(updated), 0o644); err != nil {
return fmt.Errorf("write %s: %w", path, err)
}
return nil
}
Loading