Skip to content

Commit f2657cf

Browse files
committed
Doc rewrite, phase 1
1 parent 94c1dc0 commit f2657cf

File tree

14 files changed

+1671
-891
lines changed

14 files changed

+1671
-891
lines changed

README.md

Lines changed: 15 additions & 871 deletions
Large diffs are not rendered by default.

TODO.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
- fix the issues related to `bazel mod tidy`
3+
- consider add better starlark example
4+
- consider nogo
5+
- test usage in bazel-aquery-differ
6+
- test usage in unity
7+
- check every file
8+
- fix manual tests, ideally
9+
- update documentation

cmd/gazelle/README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
# cmd/gazelle
22

3-
This is essentially a copy of the files in bazelbuild/bazel-gazelle/cmd/gazelle.
3+
This is essentially a copy of the files in bazel-contrib/bazel-gazelle/cmd/gazelle.
44

55
To upgrade gazelle, one must:
66

77
- Compare changes in the source repo@version to the files here. It's easiest to
88
just copy over each file and see where the diffs are. Make sure `langs.go`
9-
includes the `github.com/stackb/rules_proto/language/protobuf`. Internal
10-
packages referenced must also be copied over (ugh). There's probably a more
11-
elegant solution to keeping a modified copy of gazelle binary here.
9+
includes `github.com/stackb/rules_proto/language/protobuf`.
1210
- Since the `proto_gazelle.bzl` rule uses
1311
`@bazel_gazelle//internal:gazelle.bash.in`, changes there must remain
1412
compatible with proto_gazelle. Look at the diff there and make sure the
15-
proto_gazelle_impl is satifying the needs of that template.
13+
`_proto_gazelle_impl` is satifying the needs of that template.
1614
- Remember that this `cmd/gazelle` must be buildable via the standard go
1715
toolchain (see proto_repository_tools.bzl). All deps must be in the vendor
1816
tree.

docs/API.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# API
2+
3+
The go starlark API defines several core interfaces:
4+
5+
## Plugin
6+
7+
```go
8+
// Plugin implementations are capable of predicting the sources that are
9+
// generated by a particular protoc plugin.
10+
type Plugin interface {
11+
// Name is the name of the plugin and its associated configuration.
12+
Name() string
13+
// Configure creates the plugin configuration. If nil is returned, the
14+
// plugin should be skipped for the current package/library.
15+
Configure(ctx *PluginContext) *PluginConfiguration
16+
}
17+
```
18+
19+
Let's look at the `bufbuild:es` plugin implementation.
20+
21+
Plugins typically register themselves with a global object in an `init()` function:
22+
23+
```go
24+
func init() {
25+
protoc.Plugins().MustRegisterPlugin(&EsProto{})
26+
}
27+
```
28+
29+
The `Name()` function returns the NAME that is specified in a
30+
`gazelle:proto_plugin implementation NAME`.
31+
32+
```go
33+
// EsProto implements Plugin for the bufbuild/protoc-gen-es plugin.
34+
type EsProto struct{}
35+
36+
// Name implements part of the Plugin interface.
37+
func (p *EsProto) Name() string {
38+
return "bufbuild:es"
39+
}
40+
```
41+
42+
In the following `Configure` function, the main task is to read the
43+
`proto_library` passed in via the context object, iterate over the `srcs`, and
44+
assemble the expected `tsFiles` that the actual `connect-es` protoc plugin will
45+
be expected to produce. These are used for the `PluginConfiguration.Outputs`:
46+
47+
```go
48+
// Configure implements part of the Plugin interface.
49+
func (p *EsProto) Configure(ctx *protoc.PluginContext) *protoc.PluginConfiguration {
50+
flags := parseEsProtoOptions(p.Name(), ctx.PluginConfig.GetFlags())
51+
52+
var options = []string{"keep_empty_files=true", "target=ts"}
53+
tsFiles := make([]string, 0)
54+
for _, file := range ctx.ProtoLibrary.Files() {
55+
// TODO: outputs should be conditional on which target= value is used
56+
tsFile := file.Name + "_pb.ts"
57+
if flags.excludeOutput[filepath.Base(tsFile)] {
58+
continue
59+
}
60+
if ctx.Rel != "" {
61+
tsFile = path.Join(ctx.Rel, tsFile)
62+
}
63+
tsFiles = append(tsFiles, tsFile)
64+
}
65+
66+
pc := &protoc.PluginConfiguration{
67+
Label: label.New("build_stack_rules_proto", "plugin/bufbuild", "es"),
68+
Outputs: protoc.DeduplicateAndSort(tsFiles),
69+
Options: protoc.DeduplicateAndSort(options),
70+
}
71+
if len(pc.Outputs) == 0 {
72+
pc.Outputs = nil
73+
}
74+
75+
return pc
76+
}
77+
```
78+
79+
This plugin happens to define a custom set of flags:
80+
81+
```go
82+
// EsProtoOptions represents the parsed flag configuration for the
83+
// EsProto implementation.
84+
type EsProtoOptions struct {
85+
excludeOutput map[string]bool
86+
}
87+
88+
func parseEsProtoOptions(kindName string, args []string) *EsProtoOptions {
89+
flags := flag.NewFlagSet(kindName, flag.ExitOnError)
90+
91+
var excludeOutput string
92+
flags.StringVar(&excludeOutput, "exclude_output", "", "--exclude_output=foo.ts suppresses the file 'foo.ts' from the output list")
93+
94+
if err := flags.Parse(args); err != nil {
95+
log.Fatalf("failed to parse flags for %q: %v", kindName, err)
96+
}
97+
config := &EsProtoOptions{
98+
excludeOutput: make(map[string]bool),
99+
}
100+
for _, value := range strings.Split(excludeOutput, ",") {
101+
config.excludeOutput[value] = true
102+
}
103+
104+
return config
105+
}
106+
```

docs/BUILD_RULES.md

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
2+
## Build Rules
3+
4+
The core of `stackb/rules_proto` contains two build rules:
5+
6+
| Rule | Description |
7+
|-----------------|---------------------------------------------------------|
8+
| `proto_compile` | Executes the `protoc` tool. |
9+
| `proto_plugin` | Provides static `protoc` plugin-specific configuration. |
10+
11+
### proto_compile
12+
13+
Example:
14+
15+
```python
16+
load("@rules_proto//proto:defs.bzl", "proto_library")
17+
load("@build_stack_rules_proto//rules:proto_compile.bzl", "proto_compile")
18+
19+
proto_library(
20+
name = "thing_proto",
21+
srcs = ["thing.proto"],
22+
deps = ["@com_google_protobuf//:timestamp_proto"],
23+
)
24+
25+
proto_plugin(name = "cpp")
26+
27+
proto_compile(
28+
name = "person_cpp_compile",
29+
outputs = [
30+
"person.pb.cc",
31+
"person.pb.h",
32+
],
33+
plugins = [":cpp"],
34+
proto = "person_proto",
35+
)
36+
```
37+
38+
Takeaways:
39+
40+
- A `proto_library` rule forms the basis for other language-specific derived
41+
rules.
42+
- `proto_library` is provided by
43+
[bazelbuild/rules_proto](https://github.com/bazelbuild/rules_proto).
44+
- A `proto_compile` rule references a single `proto_library` target.
45+
- The `plugins` attribute is a list of labels to `proto_plugin` targets.
46+
- The `outputs` attribute names the files that will be generated by the protoc
47+
invocation.
48+
- The
49+
[proto](https://github.com/bazelbuild/bazel-gazelle/blob/master/language/proto/lang.go)
50+
extension provided by [bazel-gazelle] is responsible for generating
51+
`proto_library`.
52+
53+
### proto_plugin
54+
55+
`proto_plugin` primarily provides the plugin tool executable. The example seen
56+
above is the simplest case where the plugin is builtin to `protoc` itself; no
57+
separate plugin tool is required. In this case the `proto_plugin` rule
58+
degenerates into just a `name`.
59+
60+
It is possible to add additional plugin-specific
61+
`name = "foo", options = ["bar"]` on the `proto_plugin` rule, but the use-case
62+
for this is narrow. Generally it is preferred to say
63+
`# gazelle:proto_plugin foo option bar` such that the option can be interpreted
64+
during a gazelle run.
65+
66+
### proto_compiled_sources
67+
68+
`proto_compiled_sources` is used when you prefer to check the generated files
69+
into source control. This may be necessary for legacy reasons, during an initial
70+
Bazel migration, or to support better IDE integration.
71+
72+
The shape of a `proto_compiled_sources` rule is essentially identical to
73+
`proto_compile` with one exception: generated source are named in the `srcs`
74+
attribute rather than `outputs`.
75+
76+
For example, a `proto_compiled_sources` named `//example/thing:proto_go_sources`
77+
is a macro that generates three rules:
78+
79+
1. `bazel build //example/thing:proto_go_sources` emits the generated files.
80+
2. `bazel run //example/thing:proto_go_sources.update` copies the generated
81+
files back into the source package.
82+
3. `bazel test //example/thing:proto_go_sources_test` asserts the source files
83+
are identical to generated files.
84+
85+
In this scenario, `2.` is used to build the generated files (in the `bazel-bin/`
86+
output tree) and copy the `example/thing/thing.pb.go` back into place where it
87+
will be committed under source control. `3.` is used to prevent drift: if a
88+
developer modifies `thing.proto` and neglects to run the `.update` the test will
89+
fail in CI.
90+
91+
### proto_compile_assets
92+
93+
The macro `proto_compile_assets` aggregates a list of dependencies (which
94+
provide `ProtoCompileInfo`) into a single runnable target that copies files in
95+
bulk.
96+
97+
For example, `bazel run //proto:assets` will copy all the generated `.pb.go`
98+
files back into the source tree:
99+
100+
```py
101+
load("@build_stack_rules_proto//rules:proto_compile_assets.bzl", "proto_compile_assets")
102+
103+
proto_compile_assets(
104+
name = "assets",
105+
deps = [,
106+
"//proto/api/v1:proto_go_compile",
107+
"//proto/api/v2:proto_go_compile",
108+
"//proto/api/v3:proto_go_compile",
109+
],
110+
)
111+
```
112+
113+
### The `output_mappings` attribute
114+
115+
Consider the following rule within the package `example/thing`:
116+
117+
```python
118+
proto_compile(
119+
name = "thing_go_compile",
120+
output_mappings = ["thing.pb.go=github.com/stackb/rules_proto/example/thing/thing.pb.go"],
121+
outputs = ["thing.pb.go"],
122+
plugins = ["@build_stack_rules_proto//plugin/golang/protobuf:protoc-gen-go"],
123+
proto = "thing_proto",
124+
)
125+
```
126+
127+
This rule is declaring that a file `bazel-bin/example/thing/thing.pb.go` will be
128+
output when the action is run. When we
129+
`bazel build //example/thing:thing_go_compile`, the file is indeed created.
130+
131+
Let's temporarily comment out the `output_mappings` attribute and rebuild:
132+
133+
```python
134+
proto_compile(
135+
name = "thing_go_compile",
136+
# output_mappings = ["thing.pb.go=github.com/stackb/rules_proto/example/thing/thing.pb.go"],
137+
outputs = ["thing.pb.go"],
138+
plugins = ["@build_stack_rules_proto//plugin/golang/protobuf:protoc-gen-go"],
139+
proto = "thing_proto",
140+
)
141+
```
142+
143+
```sh
144+
$ bazel build //example/thing:thing_go_compile
145+
ERROR: /github.com/stackb/rules_proto/example/thing/BUILD.bazel:54:14: output 'example/thing/thing.pb.go' was not created
146+
```
147+
148+
What happened? Let's add a debugging attribute `verbose = True` on the rule:
149+
this will print debugging information and show the bazel sandbox before and
150+
after the `protoc` tool is invoked:
151+
152+
```python
153+
proto_compile(
154+
name = "thing_go_compile",
155+
# output_mappings = ["thing.pb.go=github.com/stackb/rules_proto/example/thing/thing.pb.go"],
156+
outputs = ["thing.pb.go"],
157+
plugins = ["@build_stack_rules_proto//plugin/golang/protobuf:protoc-gen-go"],
158+
proto = "thing_proto",
159+
verbose = True,
160+
)
161+
```
162+
163+
```sh
164+
$ bazel build //example/thing:thing_go_compile
165+
##### SANDBOX BEFORE RUNNING PROTOC
166+
./bazel-out/host/bin/external/com_google_protobuf/protoc
167+
./bazel-out/darwin-opt-exec-2B5CBBC6/bin/external/com_github_golang_protobuf/protoc-gen-go/protoc-gen-go_/protoc-gen-go
168+
./bazel-out/darwin-fastbuild/bin/example/thing/thing_proto-descriptor-set.proto.bin
169+
./bazel-out/darwin-fastbuild/bin/external/com_google_protobuf/timestamp_proto-descriptor-set.proto.bin
170+
171+
##### SANDBOX AFTER RUNNING PROTOC
172+
./bazel-out/darwin-fastbuild/bin/github.com/stackb/rules_proto/example/thing/thing.pb.go
173+
```
174+
175+
So, the file was created, but not in the location we wanted. In this case the
176+
`protoc-gen-go` plugin is not "playing nice" with Bazel. Because this
177+
`thing.proto` has
178+
`option go_package = "github.com/stackb/rules_proto/example/thing;thing";`, the
179+
output location is no longer based on the `package`. This is a problem, because
180+
Bazel semantics disallow declaring a File outside its package boundary. As a
181+
result, we need to do a
182+
`mv ./bazel-out/darwin-fastbuild/bin/github.com/stackb/rules_proto/example/thing/thing.pb.go ./bazel-out/darwin-fastbuild/bin/example/thing/thing.pb.go`
183+
to relocate the file into its expected location before the action terminates.
184+
185+
Therefore, the `output_mappings` attribute is a list of entries that map file
186+
locations `want=got` relative to the action execution root. It is required when
187+
the actual output location does not match the desired location. This can occur
188+
if the proto `package` statement does not match the Bazel package path, or in
189+
special circumstances specific to the plugin itself (like `go_package`).
190+
191+
192+
## proto_gazelle
193+
194+
`proto_gazelle` is not a repository rule: it's just like the typical `gazelle`
195+
rule, but with extra deps resolution superpowers. But, we discuss it here since
196+
it works in conjunction with `proto_repository`:
197+
198+
```python
199+
load("@build_stack_rules_proto//rules:proto_gazelle.bzl", "DEFAULT_LANGUAGES", "proto_gazelle")
200+
201+
proto_gazelle(
202+
name = "gazelle",
203+
cfgs = ["//proto:config.yaml"],
204+
command = "update",
205+
gazelle = ":gazelle-protobuf",
206+
imports = [
207+
"@bazelapis//:imports.csv",
208+
"@googleapis//:imports.csv",
209+
"@protobufapis//:imports.csv",
210+
"@remoteapis//:imports.csv",
211+
],
212+
)
213+
```
214+
215+
In this example, we are again setting the base gazelle config using the YAML
216+
file (the same one used in for the `proto_repository` rules). We are also now
217+
importing resolve information from four external sources.
218+
219+
With this setup, we can simply place an import statement like
220+
`import "src/main/java/com/google/devtools/build/lib/buildeventstream/proto/build_event_stream.proto";`
221+
in a `foo.proto` file in the default workspace, and gazelle will automagically
222+
figure out the import dependency tree spanning `@bazelapis`, `@remoteapis`,
223+
`@googleapis`, and the well-known types from `@protobufapis`.
224+
225+
This works for any `proto_language`, with any set of custom protoc plugins.
226+
227+
## golden_filegroup
228+
229+
`golden_filegroup` is a utility macro for golden file testing. It works like a
230+
native filegroup, but adds `.update` and `.test` targets. Example:
231+
232+
```py
233+
load("@build_stack_rules_proto//rules:golden_filegroup.bzl", "golden_filegroup")
234+
235+
# golden_filegroup asserts that generated files named in 'srcs' are
236+
# identical to the ones checked into source control.
237+
#
238+
# Usage:
239+
#
240+
# $ bazel build :golden # not particularly useful, just a regular filegroup
241+
#
242+
# $ bazel test :golden.test # checks that generated files are identical to
243+
# ones in git (for CI)
244+
#
245+
# $ bazel run :golden.update # copies the generated files into source tree
246+
# (then 'git add' to your PR if it looks good)
247+
golden_filegroup(
248+
name = "golden",
249+
srcs = [
250+
":some_generated_file1.json",
251+
":some_generated_file2.json",
252+
],
253+
)
254+
```

0 commit comments

Comments
 (0)