A Bazel 9 ruleset providing aspect-driven synchronization of Bazel-generated Go protobuf source files back into your project's source tree.
When using rules_go, Go protobuf bindings (.pb.go) are generated inside Bazel's output sandbox (bazel-out/). While this is perfect for hermetic builds, it causes issues for standard Go tooling (e.g. go build, go test) and IDEs (VS Code, GoLand) which cannot locate the generated packages and display "unresolved import" errors.
rules_go_write_protos solves this by automatically discovering all generated .pb.go files for a target using a custom Starlark aspect and copying them to their corresponding package folders in your source tree via bazel run. It also provides a drift-detection test to ensure checked-in files never get out of sync.
Add the following to your MODULE.bazel:
bazel_dep(name = "rules_go_write_protos", version = "0.0.1")In your package BUILD.bazel (e.g., pkg/foo/BUILD.bazel), define your compilation targets:
load("@rules_proto//proto:defs.bzl", "proto_library")
load("@rules_go//proto:def.bzl", "go_proto_library")
proto_library(
name = "foo_proto",
srcs = ["foo.proto"],
visibility = ["//visibility:public"],
)
go_proto_library(
name = "foo_go_proto",
importpath = "github.com/example/project/pkg/foo",
proto = ":foo_proto",
visibility = ["//visibility:public"],
)In your root BUILD.bazel file, load and register the sync macro referencing the Go proto library:
load("@rules_go_write_protos//rules:defs.bzl", "write_go_proto_srcs")
write_go_proto_srcs(
name = "update_protos",
srcs = [
"//pkg/foo:foo_go_proto",
],
)Alternatively, you can map Go packages to custom directory structures. This is especially useful if you keep .proto source files under one folder structure (e.g. protos/foo) but want to output the generated Go source files to another location (e.g. pkg/foo).
Standard Go toolchains (running go build with go.mod) expect Go package files to reside strictly in the directory matching their import paths. In contrast, Bazel's rules_go is tolerant of compiling Go packages from any location. Using out_dir_map bridges this gap (note that mapping to "pkg/foo" here is equivalent to the default package-based output):
load("@rules_go_write_protos//rules:defs.bzl", "write_go_proto_srcs")
write_go_proto_srcs(
name = "update_protos_custom",
srcs = [
"//pkg/foo:foo_go_proto",
],
out_dir_map = {
"github.com/example/project/pkg/foo": "pkg/foo",
},
)Run the executable sync target to copy generated .pb.go files into the source tree:
bazel run //:update_protosThis will write the files to pkg/foo/foo.pb.go and mark them writable (chmod +w).
A companion test target [name]_test is automatically created. To verify that checked-in files match what Bazel generates:
bazel test //:update_protos_testThis test runs outside the sandbox, does not cache results, and will fail with a file diff if any checked-in files are out of sync.
Aspect.dev provides a standard write_source_files rule (maintained in bazel-lib) to write generated files back to the source tree.
However, you cannot use it directly for Go protobufs because write_source_files expects direct outputs of Bazel targets. The generated .pb.go files are not direct outputs of go_proto_library (its default output is the compiled .a archive). Instead, they are exposed in the target's internal go_generated_srcs output group.
rules_go_write_protos solves this by using a custom Starlark aspect to traverse the target dependency graph, extract the hidden generated files from the output group, and automatically map them back to their package locations.
Apache License 2.0. See LICENSE for details.