Skip to content

Commit a9a41ba

Browse files
authored
Add Rust proto support: proto_rust_library rule + prost/prost-serde/tonic plugins (#419)
* improve starlark plugin: add capability to configure output mappings * Add proto_compile aggregation for package-level plugins When gazelle:proto file mode generates per-file proto_library rules, plugins that produce package-level outputs (e.g. protoc-gen-prost) cause conflicting Bazel actions since multiple proto_compile rules try to declare the same output file. This adds automatic merging: when proto_compile detects overlapping outputs with an existing rule in the same package, it merges them into a single rule using a new "protos" (label_list) attribute instead of the singular "proto" attribute. Changes: - proto_compile.bzl: add "protos" attr, support multiple ProtoInfo providers, make "proto" non-mandatory - proto_compile.go: add Rule(otherGen) merge logic following the existing go_library aggregation pattern - proto_compile_test.go: unit tests for overlap detection and end-to-end aggregation via ExamplePackage_aggregation * initial protoc-gen-prost * use neoeinstein as group * Add Rust proto support: proto_rust_library rule + prost/prost-serde/tonic plugins Implements a Go-based proto_rust_library rule and three protoc-gen-* Go plugins (prost, prost-serde, tonic) so Rust codegen participates in dependency resolution instead of relying on starlark plugins. Adds proto_compile rule merging for package-level plugins whose outputs overlap across proto_libraries, and a Rust keyword escape utility (r# prefix) for proto packages whose segments collide with Rust reserved words (e.g. google.type → google/r#type). * tidy * tools: use generate mode for now * Fix rust extern path bugs; change generated name * Fix more rust extern path bugs; add reexports
1 parent f863b2d commit a9a41ba

30 files changed

Lines changed: 2407 additions & 31 deletions

language/protobuf/BUILD.bazel

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ go_library(
1717
"//pkg/plugin/grpc/grpcnode",
1818
"//pkg/plugin/grpc/grpcweb",
1919
"//pkg/plugin/grpcecosystem/grpcgateway",
20+
"//pkg/plugin/neoeinstein/prost",
21+
"//pkg/plugin/neoeinstein/prost_serde",
22+
"//pkg/plugin/neoeinstein/tonic",
2023
"//pkg/plugin/scalapb/scalapb",
2124
"//pkg/plugin/scalapb/zio_grpc",
2225
"//pkg/plugin/stackb/grpc_js",
@@ -27,6 +30,7 @@ go_library(
2730
"//pkg/rule/rules_java",
2831
"//pkg/rule/rules_nodejs",
2932
"//pkg/rule/rules_python",
33+
"//pkg/rule/rules_rust",
3034
"//pkg/rule/rules_scala",
3135
"@bazel_gazelle//language",
3236
],

language/protobuf/protobuf.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/stackb/rules_proto/v4/pkg/language/protobuf"
77

8+
_ "github.com/stackb/rules_proto/v4/pkg/plugin/bufbuild"
89
_ "github.com/stackb/rules_proto/v4/pkg/plugin/builtin"
910
_ "github.com/stackb/rules_proto/v4/pkg/plugin/gogo/protobuf"
1011
_ "github.com/stackb/rules_proto/v4/pkg/plugin/golang/protobuf"
@@ -14,17 +15,20 @@ import (
1415
_ "github.com/stackb/rules_proto/v4/pkg/plugin/grpc/grpcnode"
1516
_ "github.com/stackb/rules_proto/v4/pkg/plugin/grpc/grpcweb"
1617
_ "github.com/stackb/rules_proto/v4/pkg/plugin/grpcecosystem/grpcgateway"
18+
_ "github.com/stackb/rules_proto/v4/pkg/plugin/neoeinstein/prost"
19+
_ "github.com/stackb/rules_proto/v4/pkg/plugin/neoeinstein/prost_serde"
20+
_ "github.com/stackb/rules_proto/v4/pkg/plugin/neoeinstein/tonic"
1721
_ "github.com/stackb/rules_proto/v4/pkg/plugin/scalapb/scalapb"
1822
_ "github.com/stackb/rules_proto/v4/pkg/plugin/scalapb/zio_grpc"
1923
_ "github.com/stackb/rules_proto/v4/pkg/plugin/stackb/grpc_js"
20-
_ "github.com/stackb/rules_proto/v4/pkg/plugin/bufbuild"
2124
_ "github.com/stackb/rules_proto/v4/pkg/plugin/stephenh/ts-proto"
2225
_ "github.com/stackb/rules_proto/v4/pkg/rule/rules_cc"
2326
_ "github.com/stackb/rules_proto/v4/pkg/rule/rules_closure"
2427
_ "github.com/stackb/rules_proto/v4/pkg/rule/rules_go"
2528
_ "github.com/stackb/rules_proto/v4/pkg/rule/rules_java"
2629
_ "github.com/stackb/rules_proto/v4/pkg/rule/rules_nodejs"
2730
_ "github.com/stackb/rules_proto/v4/pkg/rule/rules_python"
31+
_ "github.com/stackb/rules_proto/v4/pkg/rule/rules_rust"
2832
_ "github.com/stackb/rules_proto/v4/pkg/rule/rules_scala"
2933
)
3034

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
2+
3+
go_library(
4+
name = "prost",
5+
srcs = [
6+
"extern_paths.go",
7+
"protoc-gen-prost.go",
8+
],
9+
importpath = "github.com/stackb/rules_proto/v4/pkg/plugin/neoeinstein/prost",
10+
visibility = ["//visibility:public"],
11+
deps = [
12+
"//pkg/protoc",
13+
"@bazel_gazelle//label",
14+
"@bazel_gazelle//rule",
15+
],
16+
)
17+
18+
go_test(
19+
name = "prost_test",
20+
srcs = [
21+
"extern_paths_test.go",
22+
"protoc-gen-prost_test.go",
23+
],
24+
deps = [
25+
":prost",
26+
"//pkg/plugintest",
27+
"//pkg/protoc",
28+
"@bazel_gazelle//label",
29+
"@bazel_gazelle//rule",
30+
],
31+
)
32+
33+
filegroup(
34+
name = "all_files",
35+
testonly = True,
36+
srcs = [
37+
"BUILD.bazel",
38+
] + glob(["*.go"]),
39+
visibility = ["//pkg:__pkg__"],
40+
)
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
package prost
2+
3+
import (
4+
"container/list"
5+
"path"
6+
"sort"
7+
"strings"
8+
9+
"github.com/bazelbuild/bazel-gazelle/label"
10+
"github.com/bazelbuild/bazel-gazelle/rule"
11+
12+
"github.com/stackb/rules_proto/v4/pkg/protoc"
13+
)
14+
15+
const (
16+
// TransitiveExternPathsKey caches the dependency-only extern_path option
17+
// strings on the library rule's private attrs.
18+
TransitiveExternPathsKey = "_transitive_extern_paths"
19+
// OwnProtoPackagesKey caches the set of proto packages the library
20+
// itself contributes, used to compute self-extern overrides for
21+
// reference-emitting plugins (serde, tonic).
22+
OwnProtoPackagesKey = "_own_proto_packages"
23+
)
24+
25+
// ResolveExternPathOptions filters existing extern_path= options from
26+
// cfg.Options, resolves transitive dependency extern paths, and returns the
27+
// combined options list.
28+
//
29+
// This variant is used by protoc-gen-prost. It does NOT add self-extern
30+
// overrides for the library's own packages because prost interprets such an
31+
// entry as "this package is external — skip generating types for it" and
32+
// emits an empty stub.
33+
//
34+
// It also drops any dependency extern_path whose proto package is a strict
35+
// prefix-parent of one of the library's own packages, for the same reason:
36+
// prost's prefix-matching extern_path semantics treat a sub-package as
37+
// matched and skip generation. Cross-crate references that would otherwise
38+
// have used those filtered extern_paths emerge from prost as relative
39+
// super::... paths; the proto_rust_library macro's generated lib.rs adds
40+
// re-export shims to satisfy them.
41+
func ResolveExternPathOptions(cfg *protoc.PluginConfiguration, r *rule.Rule, from label.Label) []string {
42+
parents := ResolveTransitiveExternPaths(r, from)
43+
owns := ownProtoPackages(r, from)
44+
if len(owns) > 0 {
45+
filtered := make([]string, 0, len(parents))
46+
for _, ep := range parents {
47+
pkg := externPathPackage(ep)
48+
if pkg != "" && isParentOfAnyOwn(pkg, owns) {
49+
continue
50+
}
51+
filtered = append(filtered, ep)
52+
}
53+
parents = filtered
54+
}
55+
return mergeExternPathOptions(cfg, parents)
56+
}
57+
58+
// externPathPackage extracts the proto package from an "extern_path=.{pkg}=..."
59+
// option string, or returns "" if the input doesn't match the expected
60+
// format.
61+
func externPathPackage(opt string) string {
62+
const prefix = "extern_path=."
63+
if !strings.HasPrefix(opt, prefix) {
64+
return ""
65+
}
66+
rest := opt[len(prefix):]
67+
eq := strings.IndexByte(rest, '=')
68+
if eq < 0 {
69+
return ""
70+
}
71+
return rest[:eq]
72+
}
73+
74+
// isParentOfAnyOwn reports whether pkg equals, or is a strict
75+
// proto-package-prefix parent of, any package in ownPackages.
76+
func isParentOfAnyOwn(pkg string, ownPackages map[string]bool) bool {
77+
for own := range ownPackages {
78+
if own == pkg || strings.HasPrefix(own, pkg+".") {
79+
return true
80+
}
81+
}
82+
return false
83+
}
84+
85+
// ResolveExternPathOptionsForReferences returns ResolveExternPathOptions plus
86+
// self extern_path entries for the library's own proto packages whenever any
87+
// of those packages is a strict sub-package of an imported (parent) package.
88+
//
89+
// Used by protoc-gen-prost-serde and protoc-gen-tonic. Both emit Rust code at
90+
// crate-root using absolute crate-qualified paths; without a self-extern
91+
// override prost's longest-prefix-wins matching would route a reference like
92+
// ".pkg.sub.MyType" through the parent's external crate instead of resolving
93+
// it to crate::pkg::sub::MyType.
94+
func ResolveExternPathOptionsForReferences(cfg *protoc.PluginConfiguration, r *rule.Rule, from label.Label) []string {
95+
parents := ResolveTransitiveExternPaths(r, from)
96+
owns := ownProtoPackages(r, from)
97+
selves := selfExternPathsForOverride(owns, parents)
98+
99+
all := make([]string, 0, len(parents)+len(selves))
100+
all = append(all, parents...)
101+
all = append(all, selves...)
102+
sort.Strings(all)
103+
return mergeExternPathOptions(cfg, all)
104+
}
105+
106+
// ResolveTransitiveExternPaths walks the transitive dependency graph of
107+
// proto files and builds an extern_path option string for each dependency
108+
// package. Self-extern overrides are NOT included — see
109+
// ResolveExternPathOptionsForReferences for the variant that adds them.
110+
func ResolveTransitiveExternPaths(r *rule.Rule, from label.Label) []string {
111+
lib := r.PrivateAttr(protoc.ProtoLibraryKey)
112+
if lib == nil {
113+
return nil
114+
}
115+
library := lib.(protoc.ProtoLibrary)
116+
libRule := library.Rule()
117+
118+
if cached, ok := libRule.PrivateAttr(TransitiveExternPathsKey).([]string); ok {
119+
return cached
120+
}
121+
122+
resolver := protoc.GlobalResolver()
123+
124+
ownFiles := make(map[string]bool)
125+
for _, src := range library.Srcs() {
126+
ownFiles[path.Join(from.Pkg, src)] = true
127+
}
128+
129+
seen := make(map[string]bool)
130+
stack := list.New()
131+
for _, src := range library.Srcs() {
132+
stack.PushBack(path.Join(from.Pkg, src))
133+
}
134+
135+
externPathsByPackage := make(map[string]string)
136+
137+
for stack.Len() > 0 {
138+
current := stack.Front()
139+
stack.Remove(current)
140+
141+
protofile := current.Value.(string)
142+
if seen[protofile] {
143+
continue
144+
}
145+
seen[protofile] = true
146+
147+
depends := resolver.Resolve("proto", "depends", protofile)
148+
for _, dep := range depends {
149+
depFile := path.Join(dep.Label.Pkg, dep.Label.Name)
150+
stack.PushBack(depFile)
151+
}
152+
153+
if ownFiles[protofile] {
154+
continue
155+
}
156+
157+
// Skip well-known types — prost ships these built-in.
158+
if strings.HasPrefix(protofile, "google/protobuf/") {
159+
continue
160+
}
161+
162+
results := resolver.Resolve("proto", "prost_extern", protofile)
163+
if len(results) == 0 {
164+
continue
165+
}
166+
167+
first := results[0]
168+
protoPackage := first.Label.Pkg
169+
crateName := first.Label.Name
170+
if protoPackage == "" {
171+
continue
172+
}
173+
if _, exists := externPathsByPackage[protoPackage]; exists {
174+
continue
175+
}
176+
177+
// extern_path=.{proto_package}=::{crate_name}::{rust_module_path}
178+
rustModulePath := strings.ReplaceAll(protoPackage, ".", "::")
179+
externPathsByPackage[protoPackage] = "extern_path=." + protoPackage + "=::" + crateName + "::" + rustModulePath
180+
}
181+
182+
result := make([]string, 0, len(externPathsByPackage))
183+
for _, ep := range externPathsByPackage {
184+
result = append(result, ep)
185+
}
186+
sort.Strings(result)
187+
188+
libRule.SetPrivateAttr(TransitiveExternPathsKey, result)
189+
return result
190+
}
191+
192+
// mergeExternPathOptions strips any pre-existing extern_path= entries from
193+
// cfg.Options and returns the remainder concatenated with the supplied
194+
// extern_path strings.
195+
func mergeExternPathOptions(cfg *protoc.PluginConfiguration, externPaths []string) []string {
196+
options := make([]string, 0, len(cfg.Options)+len(externPaths))
197+
for _, opt := range cfg.Options {
198+
if !strings.HasPrefix(opt, "extern_path=") {
199+
options = append(options, opt)
200+
}
201+
}
202+
options = append(options, externPaths...)
203+
return options
204+
}
205+
206+
// ownProtoPackages returns the set of proto packages the library itself
207+
// contributes, computed from prost_extern resolver entries for each own
208+
// proto file. Cached on the library rule.
209+
func ownProtoPackages(r *rule.Rule, from label.Label) map[string]bool {
210+
lib := r.PrivateAttr(protoc.ProtoLibraryKey)
211+
if lib == nil {
212+
return nil
213+
}
214+
library := lib.(protoc.ProtoLibrary)
215+
libRule := library.Rule()
216+
217+
if cached, ok := libRule.PrivateAttr(OwnProtoPackagesKey).(map[string]bool); ok {
218+
return cached
219+
}
220+
221+
resolver := protoc.GlobalResolver()
222+
out := make(map[string]bool)
223+
for _, src := range library.Srcs() {
224+
ownFile := path.Join(from.Pkg, src)
225+
for _, ext := range resolver.Resolve("proto", "prost_extern", ownFile) {
226+
if ext.Label.Pkg != "" {
227+
out[ext.Label.Pkg] = true
228+
}
229+
}
230+
}
231+
232+
libRule.SetPrivateAttr(OwnProtoPackagesKey, out)
233+
return out
234+
}
235+
236+
// selfExternPathsForOverride returns "extern_path=.{ownPkg}=crate::..."
237+
// entries for every own proto package whose path is a strict sub-package of
238+
// any package present in parents. parents is the slice of dependency
239+
// extern_path option strings (as returned by ResolveTransitiveExternPaths).
240+
func selfExternPathsForOverride(ownPackages map[string]bool, parents []string) []string {
241+
if len(ownPackages) == 0 || len(parents) == 0 {
242+
return nil
243+
}
244+
parentPkgs := parentExternPackages(parents)
245+
out := make([]string, 0)
246+
for ownPkg := range ownPackages {
247+
if !hasParentInImports(ownPkg, parentPkgs) {
248+
continue
249+
}
250+
rustModulePath := strings.ReplaceAll(ownPkg, ".", "::")
251+
out = append(out, "extern_path=."+ownPkg+"=crate::"+rustModulePath)
252+
}
253+
return out
254+
}
255+
256+
// parentExternPackages parses a slice of "extern_path=.{pkg}=..." strings
257+
// and returns the set of proto packages they cover.
258+
func parentExternPackages(opts []string) map[string]bool {
259+
out := make(map[string]bool, len(opts))
260+
const prefix = "extern_path=."
261+
for _, opt := range opts {
262+
if !strings.HasPrefix(opt, prefix) {
263+
continue
264+
}
265+
rest := opt[len(prefix):]
266+
eq := strings.IndexByte(rest, '=')
267+
if eq < 0 {
268+
continue
269+
}
270+
out[rest[:eq]] = true
271+
}
272+
return out
273+
}
274+
275+
// hasParentInImports reports whether any of importedPackages is a proto-
276+
// package-prefix parent of ownPkg (e.g. "a.b" is a parent of "a.b.c").
277+
func hasParentInImports(ownPkg string, importedPackages map[string]bool) bool {
278+
for imp := range importedPackages {
279+
if strings.HasPrefix(ownPkg, imp+".") {
280+
return true
281+
}
282+
}
283+
return false
284+
}

0 commit comments

Comments
 (0)