diff --git a/docs/TOOLCHAINS.md b/docs/TOOLCHAINS.md index f1fa8e324..5512389bd 100644 --- a/docs/TOOLCHAINS.md +++ b/docs/TOOLCHAINS.md @@ -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. diff --git a/rules/proto_compile.bzl b/rules/proto_compile.bzl index 793341ed4..5ac990038 100644 --- a/rules/proto_compile.bzl +++ b/rules/proto_compile.bzl @@ -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): @@ -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` 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. @@ -144,8 +162,8 @@ def _proto_compile_impl(ctx): # const verbosity flag verbose = ctx.attr.verbose - # const the protoc file from the toolchain - protoc = get_protoc_executable(ctx) + # const the protoc tool from the toolchain + protoc = get_protoc(ctx) # const > proto providers (from proto or protos attr) proto_infos = [] @@ -170,8 +188,8 @@ def _proto_compile_impl(ctx): for pi in proto_infos: descriptors += pi.transitive_descriptor_sets.to_list() - # mut > tools for the compile action - tools = [protoc] + # mut > tools for the compile action + tools = [protoc.tool] # mut > argument list for protoc execution args = [] + ctx.attr.args @@ -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 @@ -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(), ) diff --git a/toolchain/toolchain.bzl b/toolchain/toolchain.bzl index bd6ce2002..cb58aa1ff 100644 --- a/toolchain/toolchain.bzl +++ b/toolchain/toolchain.bzl @@ -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 that takes precedence over the toolchains. + + Returns: + struct: having an `executable` 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(