protobuf | starlark |
protoc-gen-starlark
is a scriptable protocol buffer plugin. It might just be
the the easiest way to write a protoc plugin! 😍
Download a binary from the releases page, or install from source:
go install github.com/stackb/protoc-gen-starlark/cmd/protoc-gen-starlark@latest
protoc-gen-starlark
works like any other typical protoc plugin: it reads an
encoded CodeGeneratorRequest
from stdin and writes an encoded
CodeGeneratorResponse
to stdout.
The logic of generating a response is performed within a starlark script that you must write. The simplest such script looks something like:
pb = proto.package("google.protobuf.compiler")
def generate(request):
"""generate prepares the response.
Args:
request: the pb.CodeGeneratorRequest that was read from stdin.
Returns:
a pb.CodeGeneratorResponse
"""
return pb.CodeGeneratorResponse(
error = "not implemented",
)
def main(ctx):
"""main is the entrypoint function.
Args:
ctx: the script context. It has a struct member named
'vars' which is a StringDict of variables injected into
the entrypoint. vars is guaranteed to have an entry named
"request" that is the pb.CodeGeneratorRequest read from stdin.
Returns:
A single pb.CodeGeneratorResponse. Per skycfg semantics,
the return value from `main` must be a list however so it
is wrapped accordingly.
"""
return [generate(ctx.vars["request"])]
Although starlark is an interpreted language, the protobuf message types are stongly typed: it is an error to set/get fields that are not part of the message definition. See stackb/grpc-starlark and stripe/skycfg for more details about this.
protoc-gen-starlark
is built using stackb/grpc-starlark and shares the same command line flags.
A sample protoc invocation might look something like:
$ export PROTOC_GEN_STARLARK_SCRIPT=foo.star
$ protoc \
--foo_out=./gendir \
--plugin=protoc-gen-foo=/usr/bin/protoc-gen-starlark
In this case protoc-gen-starlark
discovers which script to evaluate using the
PROTOC_GEN_STARLARK_SCRIPT
environment variable.
Another strategy is to copy/rename protoc-gen-starlark
and the script to a
common name. If a file named $0.star
exists where $0
is the name of the
executable itself, this will be loaded. For example:
$ ln -s /usr/bin/protoc-gen-starlark tools/protoc-gen-foo
$ mv foo.plugin.star tools/protoc-gen-foo.star
$ ln -s /usr/bin/protoc-gen-starlark tools/protoc-gen-bar
$ mv bar.plugin.star tools/protoc-gen-bar.star
$ protoc \
--foo_out=./gendir \
--plugin=protoc-gen-foo=./tools/protoc-gen-foo
--bar_out=./gendir \
--plugin=protoc-gen-bar=./tools/protoc-gen-bar
Alternatively, a shell script can be used to wrap the invocation of the plugin:
#!/bin/bash
# protoc-gen-foo.sh wraps protoc-gen-starlark and sets
# the file argument explicitly.
set -euox pipefail
/usr/bin/protoc-gen-starlark \
-file ./tools/protoc-gen-foo.star
$ protoc \
--foo_out=./gendir \
--plugin=protoc-gen-foo=./tools/protoc-gen-foo.sh
By default, the message types from
plugin.proto
are pre-loaded. You can make additional types available to proto.package
using the --protoset=/path/to/a/descriptor_set_out.pb
flag.