Skip to content
Open
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
39 changes: 39 additions & 0 deletions docs/TOOLCHAINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,42 @@ register_toolchains("@build_stack_rules_proto//toolchain:standard")
To define an alternative, prepare a toolchain of type
`@build_stack_rules_proto//toolchain:protoc` and register that instead. See
[//toolchain:BUILD.bazel](/toolchain/BUILD.bazel) for an example.

## `--incompatible_enable_proto_toolchain_resolution`

When the
[`--incompatible_enable_proto_toolchain_resolution`](https://github.com/bazelbuild/rules_proto/discussions/213)
flag is enabled, `protoc` is instead taken from the registered proto toolchain
(`@rules_proto//proto:toolchain_type`, provided by the `proto_toolchain` rule),
which is the same toolchain `proto_library` itself uses. The
`@build_stack_rules_proto//toolchain:protoc` toolchain is then optional and is
only consulted if no proto toolchain is registered.

This makes it possible to use a prebuilt `protoc` -- rather than building it
from source -- via a ruleset such as
[toolchains_protoc](https://github.com/aspect-build/toolchains_protoc):

```py
# MODULE.bazel
bazel_dep(name = "toolchains_protoc", version = "0.5.0")
bazel_dep(name = "build_stack_rules_proto", version = "4.1.0")

protoc = use_extension("@toolchains_protoc//protoc:extensions.bzl", "protoc")
protoc.toolchain(
google_protobuf = "com_google_protobuf",
version = "v29.0",
)
use_repo(protoc, "com_google_protobuf")
```

```py
# .bazelrc
common --incompatible_enable_proto_toolchain_resolution
```

With this configuration, `register_toolchains("@build_stack_rules_proto//toolchain:standard")`
is not required (registering it is harmless: the proto toolchain takes
precedence while the flag is enabled).

Note that the `protoc` attribute of the `proto_compile` rule still takes
precedence over both toolchains.
38 changes: 28 additions & 10 deletions rules/proto_compile.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This runs the protoc tool and generates output source files.
"""

load("@rules_proto//proto:defs.bzl", "ProtoInfo")
load("//toolchain:toolchain.bzl", "find_protoc", "use_protoc_toolchains")
load(":providers.bzl", "ProtoCompileInfo", "ProtoPluginInfo")

def _uniq(iterable):
Expand Down Expand Up @@ -57,11 +58,28 @@ def _plugin_label_key(label):

return key

def get_protoc(ctx):
"""Returns the protoc tool for the rule.

Args:
ctx: the rule context (must have a 'protoc' attribute).

Returns:
struct: having an `executable` <File> and a `tool` field suitable for
the `tools` argument of a ctx.actions method.
"""
return find_protoc(ctx, override = ctx.file.protoc)

def get_protoc_executable(ctx):
if ctx.file.protoc:
return ctx.file.protoc
protoc_toolchain_info = ctx.toolchains[str(Label("//toolchain:protoc"))]
return protoc_toolchain_info.protoc_executable
"""Deprecated: use `get_protoc(ctx).executable`.

Args:
ctx: the rule context (must have a 'protoc' attribute).

Returns:
File: the protoc executable.
"""
return get_protoc(ctx).executable

def _descriptor_proto_path(proto, proto_info):
"""Convert a proto File to the path within the descriptor file.
Expand Down Expand Up @@ -144,8 +162,8 @@ def _proto_compile_impl(ctx):
# const <bool> verbosity flag
verbose = ctx.attr.verbose

# const <File> the protoc file from the toolchain
protoc = get_protoc_executable(ctx)
# const <struct> the protoc tool from the toolchain
protoc = get_protoc(ctx)

# const <list<ProtoInfo>> proto providers (from proto or protos attr)
proto_infos = []
Expand All @@ -170,8 +188,8 @@ def _proto_compile_impl(ctx):
for pi in proto_infos:
descriptors += pi.transitive_descriptor_sets.to_list()

# mut <list<File>> tools for the compile action
tools = [protoc]
# mut <list<File|FilesToRunProvider>> tools for the compile action
tools = [protoc.tool]

# mut <list<string>> argument list for protoc execution
args = [] + ctx.attr.args
Expand Down Expand Up @@ -316,7 +334,7 @@ def _proto_compile_impl(ctx):
commands = [
"set -euo pipefail",
"mkdir -p ./" + ctx.label.package,
protoc.path + " $@", # $@ is replaced with args list
protoc.executable.path + " $@", # $@ is replaced with args list
]

# if the rule declares any mappings, setup copy file commands to move them
Expand Down Expand Up @@ -486,5 +504,5 @@ proto_compile = rule(
doc = "If set, copy the output files to a new set having this suffix",
),
},
toolchains = ["@build_stack_rules_proto//toolchain:protoc"],
toolchains = use_protoc_toolchains(),
)
79 changes: 78 additions & 1 deletion toolchain/toolchain.bzl
Original file line number Diff line number Diff line change
@@ -1,4 +1,81 @@
"toolchain.bzl provides the protoc toolchain rule"
"""toolchain.bzl provides the protoc toolchain rule"""

load("@rules_proto//proto:proto_common.bzl", "proto_common")

# PROTOC_TOOLCHAIN_TYPE is the toolchain type provided by this ruleset.
PROTOC_TOOLCHAIN_TYPE = Label("//toolchain:protoc")

# PROTO_TOOLCHAIN_TYPE is the toolchain type used by the `proto_toolchain` rule
# from protobuf/rules_proto. This is the toolchain type that
# --incompatible_enable_proto_toolchain_resolution resolves `protoc` from, and
# the one registered by rulesets that supply a prebuilt protoc such as
# https://github.com/aspect-build/toolchains_protoc.
PROTO_TOOLCHAIN_TYPE = Label("@com_google_protobuf//bazel/private:proto_toolchain_type")

# INCOMPATIBLE_ENABLE_PROTO_TOOLCHAIN_RESOLUTION reports whether the
# --incompatible_enable_proto_toolchain_resolution flag is enabled.
INCOMPATIBLE_ENABLE_PROTO_TOOLCHAIN_RESOLUTION = getattr(
proto_common,
"INCOMPATIBLE_ENABLE_PROTO_TOOLCHAIN_RESOLUTION",
False,
)

def use_protoc_toolchains():
"""Returns the list of toolchains that provide protoc.

When --incompatible_enable_proto_toolchain_resolution is enabled, protoc is
additionally sourced from the proto toolchain, and //toolchain:protoc
becomes optional (a build that resolves protoc via the proto toolchain does
not need to register one of ours at all).

Returns:
list: of toolchain types for the `toolchains` argument of a rule.
"""
if not INCOMPATIBLE_ENABLE_PROTO_TOOLCHAIN_RESOLUTION:
return [config_common.toolchain_type(PROTOC_TOOLCHAIN_TYPE, mandatory = True)]
return [
config_common.toolchain_type(PROTO_TOOLCHAIN_TYPE, mandatory = False),
config_common.toolchain_type(PROTOC_TOOLCHAIN_TYPE, mandatory = False),
]

def find_protoc(ctx, override = None):
"""Resolves the protoc tool for a rule that uses `use_protoc_toolchains`.

Resolution order:

1. the `override` file, if given (typically the `protoc` rule attribute).
2. the proto toolchain, if
--incompatible_enable_proto_toolchain_resolution is enabled and such a
toolchain is registered.
3. the //toolchain:protoc toolchain.

Args:
ctx: the rule context.
override: optional <File> that takes precedence over the toolchains.

Returns:
struct: having an `executable` <File> and a `tool` field suitable for
the `tools` argument of a ctx.actions method.
"""
if override:
return struct(executable = override, tool = override)

if INCOMPATIBLE_ENABLE_PROTO_TOOLCHAIN_RESOLUTION:
proto_toolchain = ctx.toolchains[PROTO_TOOLCHAIN_TYPE]
if proto_toolchain:
proto_compiler = proto_toolchain.proto.proto_compiler
return struct(executable = proto_compiler.executable, tool = proto_compiler)

protoc_toolchain = ctx.toolchains[PROTOC_TOOLCHAIN_TYPE]
if not protoc_toolchain:
fail("no protoc toolchain was resolved for %s: register one of type '%s' " % (ctx.label, PROTOC_TOOLCHAIN_TYPE) +
"(for example 'register_toolchains(\"@build_stack_rules_proto//toolchain:standard\")') " +
"or, with --incompatible_enable_proto_toolchain_resolution, one of type '%s'" % PROTO_TOOLCHAIN_TYPE)

return struct(
executable = protoc_toolchain.protoc_executable,
tool = protoc_toolchain.protoc_executable,
)

def _protoc_impl(ctx):
return [platform_common.ToolchainInfo(
Expand Down