Implement a minimal, internal, experimental rust_proto_library rule. The internal design is consistent with other <lang>_proto_library rules. rust_proto_library attaches rust_proto_library_aspect on its `deps` attribute. The aspect traverses the dependency, and when it visits proto_library (detected by ProtoInfo provider) it registers 2 actions: 1) to run protoc with Rust backend to emit gencode 2) to compile the gencode using Rustc Action (2) gets the Rust proto runtime as an input as well. Coming in a followup is support and test coverage for proto_library.deps. PiperOrigin-RevId: 514521285
diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 26b4a32..2c329e6 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml
@@ -24,4 +24,4 @@ with: check_filenames: true skip: ./.git,./third_party,./conformance/third_party,*.snk,*.pb,*.pb.cc,*.pb.h,./src/google/protobuf/testdata,./objectivec/Tests,./python/compatibility_tests/v2.5.0/tests/google/protobuf/internal,./.github/workflows/codespell.yml - ignore_words_list: "alow,alse,atleast,ba,chec,cleare,copyable,cloneable,dedup,dur,errorprone,falsy,files',fo,fundementals,hel,importd,inout,leapyear,nd,nin,ois,ons,parseable,process',ro,te,testof,ue,unparseable,wasn,wee,gae,keyserver,objext,od,optin,streem,sur,falsy" + ignore_words_list: "alow,alse,atleast,ba,chec,cleare,copyable,cloneable,crate,dedup,dur,errorprone,falsy,files',fo,fundementals,hel,importd,inout,leapyear,nd,nin,ois,ons,parseable,process',ro,te,testof,ue,unparseable,wasn,wee,gae,keyserver,objext,od,optin,streem,sur,falsy"
diff --git a/WORKSPACE b/WORKSPACE index 2301093..af7d63a 100644 --- a/WORKSPACE +++ b/WORKSPACE
@@ -136,4 +136,4 @@ load("@rules_rust//rust:repositories.bzl", "rules_rust_dependencies", "rust_register_toolchains") rules_rust_dependencies() -rust_register_toolchains() +rust_register_toolchains(edition = "2021")
diff --git a/rust/BUILD b/rust/BUILD new file mode 100644 index 0000000..bd2eeb7 --- /dev/null +++ b/rust/BUILD
@@ -0,0 +1,19 @@ +# Protobuf Rust runtime packages. + +load("@rules_rust//rust:defs.bzl", "rust_library") + +package(default_visibility = ["//src/google/protobuf:__subpackages__"]) + +rust_library( + name = "protobuf", + srcs = ["lib.rs"], +) + +# TODO(b/270125787): Move to the right location once rust_proto_library is no longer experimental. +proto_lang_toolchain( + name = "proto_lang_toolchain", + command_line = "--rust_out=experimental-codegen=enabled:$(OUT)", + progress_message = "Generating Rust proto_library %{label}", + runtime = ":protobuf", + visibility = ["//visibility:public"], +)
diff --git a/rust/BUILD.bazel b/rust/BUILD.bazel deleted file mode 100644 index 5efdab5..0000000 --- a/rust/BUILD.bazel +++ /dev/null
@@ -1,10 +0,0 @@ -# Protobuf Rust runtime packages. - -load("@rules_rust//rust:defs.bzl", "rust_library") - -package(default_visibility = ["//src/google/protobuf:__subpackages__"]) - -rust_library( - name = "protobuf", - srcs = ["lib.rs"], -)
diff --git a/rust/defs.bzl b/rust/defs.bzl new file mode 100644 index 0000000..11f8f71 --- /dev/null +++ b/rust/defs.bzl
@@ -0,0 +1,242 @@ +"""This file implements an experimental, do-not-use-kind of rust_proto_library. + +Disclaimer: This project is experimental, under heavy development, and should not +be used yet.""" + +# buildifier: disable=bzl-visibility +load("@rules_rust//rust/private:providers.bzl", "CrateInfo", "DepInfo", "DepVariantInfo") + +# buildifier: disable=bzl-visibility +load("@rules_rust//rust/private:rustc.bzl", "rustc_compile_action") +load("@rules_rust//rust:defs.bzl", "rust_common") + +proto_common = proto_common_do_not_use + +RustProtoInfo = provider( + doc = "Rust protobuf provider info", + fields = { + "dep_variant_info": "DepVariantInfo for the compiled Rust gencode (also covers its " + + "transitive dependencies)", + }, +) + +def _generate_rust_gencode( + ctx, + proto_info, + proto_lang_toolchain): + """Generates Rust gencode + + This function uses proto_common APIs and a ProtoLangToolchain to register an action + that invokes protoc with the right flags. + Args: + ctx (RuleContext): current rule context + proto_info (ProtoInfo): ProtoInfo of the proto_library target for which we are generating + gencode + proto_lang_toolchain (ProtoLangToolchainInfo): proto lang toolchain for Rust + Returns: + rs_outputs ([File]): generated Rust source files + """ + actions = ctx.actions + rs_outputs = proto_common.declare_generated_files( + actions = actions, + proto_info = proto_info, + extension = ".pb.rs", + ) + + proto_common.compile( + actions = ctx.actions, + proto_info = proto_info, + generated_files = rs_outputs, + proto_lang_toolchain_info = proto_lang_toolchain, + plugin_output = ctx.bin_dir.path, + ) + return rs_outputs + +def _get_crate_info(providers): + for provider in providers: + if hasattr(provider, "name"): + return provider + fail("Couldn't find a CrateInfo in the list of providers") + +def _get_dep_info(providers): + for provider in providers: + if hasattr(provider, "direct_crates"): + return provider + fail("Couldn't find a DepInfo in the list of providers") + +def _get_cc_info(providers): + for provider in providers: + if hasattr(provider, "linking_context"): + return provider + fail("Couldn't find a CcInfo in the list of providers") + +def _compile_rust(ctx, attr, src, extra_srcs, deps): + """Compiles a Rust source file. + + Eventually this function could be upstreamed into rules_rust and be made present in rust_common. + + Args: + ctx (RuleContext): The rule context. + attr (Attrs): The current rule's attributes (`ctx.attr` for rules, `ctx.rule.attr` for aspects) + src (File): The crate root source file to be compiled. + extra_srcs ([File]): Additional source files to include in the crate. + deps (List[DepVariantInfo]): A list of dependencies needed. + + Returns: + A DepVariantInfo provider. + """ + toolchain = ctx.toolchains["@rules_rust//rust:toolchain"] + output_hash = repr(hash(src.path)) + + # TODO(b/270124215): Use the import! macro once available + crate_name = ctx.label.name.replace("-", "_") + + lib_name = "{prefix}{name}-{lib_hash}{extension}".format( + prefix = "lib", + name = crate_name, + lib_hash = output_hash, + extension = ".rlib", + ) + + rmeta_name = "{prefix}{name}-{lib_hash}{extension}".format( + prefix = "lib", + name = crate_name, + lib_hash = output_hash, + extension = ".rmeta", + ) + + lib = ctx.actions.declare_file(lib_name) + rmeta = ctx.actions.declare_file(rmeta_name) + + providers = rustc_compile_action( + ctx = ctx, + attr = attr, + toolchain = toolchain, + crate_info = rust_common.create_crate_info( + name = crate_name, + type = "rlib", + root = src, + srcs = depset([src] + extra_srcs), + deps = depset(deps), + proc_macro_deps = depset([]), + aliases = {}, + output = lib, + metadata = rmeta, + edition = "2021", + is_test = False, + rustc_env = {}, + compile_data = depset([]), + compile_data_targets = depset([]), + owner = ctx.label, + ), + output_hash = output_hash, + ) + + return DepVariantInfo( + crate_info = _get_crate_info(providers), + dep_info = _get_dep_info(providers), + cc_info = _get_cc_info(providers), + build_info = None, + ) + +def _rust_proto_aspect_impl(target, ctx): + if ProtoInfo not in target: + return None + + proto_lang_toolchain = ctx.attr._proto_lang_toolchain[proto_common.ProtoLangToolchainInfo] + + gencode = _generate_rust_gencode(ctx, target[ProtoInfo], proto_lang_toolchain) + + runtime = proto_lang_toolchain.runtime + dep_variant_info_for_runtime = DepVariantInfo( + crate_info = runtime[CrateInfo] if CrateInfo in runtime else None, + dep_info = runtime[DepInfo] if DepInfo in runtime else None, + cc_info = runtime[CcInfo] if CcInfo in runtime else None, + build_info = None, + ) + + dep_variant_info = _compile_rust( + ctx = ctx, + attr = ctx.rule.attr, + src = gencode[0], + extra_srcs = gencode[1:], + deps = [dep_variant_info_for_runtime], + ) + return [RustProtoInfo( + dep_variant_info = dep_variant_info, + )] + +rust_proto_library_aspect = aspect( + implementation = _rust_proto_aspect_impl, + attr_aspects = ["deps"], + attrs = { + "_cc_toolchain": attr.label( + doc = ( + "In order to use find_cc_toolchain, your rule has to depend " + + "on C++ toolchain. See `@rules_cc//cc:find_cc_toolchain.bzl` " + + "docs for details." + ), + default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), + ), + "_collect_cc_coverage": attr.label( + default = Label("@rules_rust//util:collect_coverage"), + executable = True, + cfg = "exec", + ), + "_error_format": attr.label( + default = Label("@rules_rust//:error_format"), + ), + "_extra_exec_rustc_flag": attr.label( + default = Label("@rules_rust//:extra_exec_rustc_flag"), + ), + "_extra_exec_rustc_flags": attr.label( + default = Label("@rules_rust//:extra_exec_rustc_flags"), + ), + "_extra_rustc_flag": attr.label( + default = Label("@rules_rust//:extra_rustc_flag"), + ), + "_extra_rustc_flags": attr.label( + default = Label("@rules_rust//:extra_rustc_flags"), + ), + "_process_wrapper": attr.label( + doc = "A process wrapper for running rustc on all platforms.", + default = Label("@rules_rust//util/process_wrapper"), + executable = True, + allow_single_file = True, + cfg = "exec", + ), + "_proto_lang_toolchain": attr.label( + default = Label("//rust:proto_lang_toolchain"), + ), + }, + fragments = ["cpp"], + host_fragments = ["cpp"], + toolchains = [ + str(Label("@rules_rust//rust:toolchain")), + "@bazel_tools//tools/cpp:toolchain_type", + ], + incompatible_use_toolchain_transition = True, +) + +def _rust_proto_library_impl(ctx): + deps = ctx.attr.deps + if not deps: + fail("Exactly 1 dependency in `deps` attribute expected, none were provided.") + if len(deps) > 1: + fail("Exactly 1 dependency in `deps` attribute expected, too many were provided.") + + dep = deps[0] + rust_proto_info = dep[RustProtoInfo] + dep_variant_info = rust_proto_info.dep_variant_info + return [dep_variant_info.crate_info, dep_variant_info.dep_info, dep_variant_info.cc_info] + +rust_proto_library = rule( + implementation = _rust_proto_library_impl, + attrs = { + "deps": attr.label_list( + mandatory = True, + providers = [ProtoInfo], + aspects = [rust_proto_library_aspect], + ), + }, +)
diff --git a/rust/lib.rs b/rust/lib.rs index 8974efa..d860245 100644 --- a/rust/lib.rs +++ b/rust/lib.rs
@@ -31,3 +31,6 @@ //! Rust Protobuf Runtime // Not yet implemented. + +// TODO(b/270138878): Remove once we have real logic in the runtime. +pub fn do_nothing() {}
diff --git a/rust/test/BUILD b/rust/test/BUILD new file mode 100644 index 0000000..2b5bbec --- /dev/null +++ b/rust/test/BUILD
@@ -0,0 +1,16 @@ +load("//rust:defs.bzl", "rust_proto_library") +load("@rules_rust//rust:defs.bzl", "rust_test") + +rust_proto_library( + name = "hello_world_rs_proto", + testonly = True, + deps = ["//third_party/protobuf:unittest_proto"], +) + +rust_test( + name = "hello_world_test", + srcs = ["hello_world_test.rs"], + # TODO(b/270274576): Enable testing on arm once we have a Rust Arm toolchain. + tags = ["not_build:arm"], + deps = [":hello_world_rs_proto"], +)
diff --git a/rust/test/hello_world_test.rs b/rust/test/hello_world_test.rs new file mode 100644 index 0000000..24cad65 --- /dev/null +++ b/rust/test/hello_world_test.rs
@@ -0,0 +1,35 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +fn main() { + // This is currently just a smoke test checking that we can generate gencode, compile it, and + // link the test binary. + let _test_all_types: unittest_proto::TestAllTypes; +}
diff --git a/rust/test/rust_proto_library_unit_test/BUILD b/rust/test/rust_proto_library_unit_test/BUILD new file mode 100644 index 0000000..de7da61 --- /dev/null +++ b/rust/test/rust_proto_library_unit_test/BUILD
@@ -0,0 +1,3 @@ +load(":rust_proto_library_unit_test.bzl", "rust_proto_library_unit_test") + +rust_proto_library_unit_test(name = "rust_proto_library_unit_test")
diff --git a/rust/test/rust_proto_library_unit_test/defs.bzl b/rust/test/rust_proto_library_unit_test/defs.bzl new file mode 100644 index 0000000..81741aa --- /dev/null +++ b/rust/test/rust_proto_library_unit_test/defs.bzl
@@ -0,0 +1,19 @@ +"""Support for rust_proto_library_aspect unit-tests.""" + +load("//rust:defs.bzl", "RustProtoInfo", "rust_proto_library_aspect") + +ActionsInfo = provider( + doc = ("A provider that exposes what actions were registered by rust_proto_library_aspect " + + "on proto_libraries."), + fields = {"actions": "List[Action]: actions registered on proto_libraries."}, +) + +def _attach_aspect_impl(ctx): + return [ctx.attr.dep[RustProtoInfo], ActionsInfo(actions = ctx.attr.dep.actions)] + +attach_aspect = rule( + implementation = _attach_aspect_impl, + attrs = { + "dep": attr.label(aspects = [rust_proto_library_aspect]), + }, +)
diff --git a/rust/test/rust_proto_library_unit_test/rust_proto_library_unit_test.bzl b/rust/test/rust_proto_library_unit_test/rust_proto_library_unit_test.bzl new file mode 100644 index 0000000..78c2e1e --- /dev/null +++ b/rust/test/rust_proto_library_unit_test/rust_proto_library_unit_test.bzl
@@ -0,0 +1,61 @@ +"""This module contains unit tests for rust_proto_library and its aspect.""" + +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") +load(":defs.bzl", "ActionsInfo", "attach_aspect") + +def _find_action_with_mnemonic(actions, mnemonic): + action = [a for a in actions if a.mnemonic == mnemonic] + if not action: + fail("Couldn't find action with mnemonic {} among {}".format(mnemonic, actions)) + return action[0] + +def _find_rust_lib_input(inputs, target_name): + inputs = inputs.to_list() + input = [i for i in inputs if i.basename.startswith("lib" + target_name) and + (i.basename.endswith(".rlib") or i.basename.endswith(".rmeta"))] + if not input: + fail("Couldn't find lib{}-<hash>.rlib or lib{}-<hash>.rmeta among {}".format( + target_name, + target_name, + [i.basename for i in inputs], + )) + return input[0] + +def _rust_compilation_action_has_runtime_as_input_test_impl(ctx): + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + actions = target_under_test[ActionsInfo].actions + rustc_action = _find_action_with_mnemonic(actions, "Rustc") + _find_rust_lib_input(rustc_action.inputs, "protobuf") + asserts.true(env, rustc_action.outputs.to_list()[0].path.endswith(".rlib")) + + return analysistest.end(env) + +rust_compilation_action_has_runtime_as_input_test = analysistest.make( + _rust_compilation_action_has_runtime_as_input_test_impl, +) + +def _test_rust_compilation_action_has_runtime_as_input(): + native.proto_library(name = "some_proto", srcs = ["some_proto.proto"]) + attach_aspect(name = "some_proto_with_aspect", dep = ":some_proto") + + rust_compilation_action_has_runtime_as_input_test( + name = "rust_compilation_action_has_runtime_as_input_test", + target_under_test = ":some_proto_with_aspect", + # TODO(b/270274576): Enable testing on arm once we have a Rust Arm toolchain. + tags = ["not_build:arm"], + ) + +def rust_proto_library_unit_test(name): + """Sets up rust_proto_library_unit_test test suite. + + Args: + name: name of the test suite""" + _test_rust_compilation_action_has_runtime_as_input() + + native.test_suite( + name = name, + tests = [ + ":rust_compilation_action_has_runtime_as_input_test", + ], + )
diff --git a/rust/test/rust_proto_library_unit_test/some_proto.proto b/rust/test/rust_proto_library_unit_test/some_proto.proto new file mode 100644 index 0000000..1cc4a52 --- /dev/null +++ b/rust/test/rust_proto_library_unit_test/some_proto.proto
@@ -0,0 +1,39 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// This file exists because of the design of the Starlark unit testing +// framework. We call these tests unittests, but Blaze still sees them as full +// builds. And while Blaze doesn't build the test-setup targets unless it really +// needs to, it checks that all input files are present. Therefore, we need +// these "empty" files to be present. + +syntax = "proto2"; + +package third_party_protobuf_rust_test_rust_proto_library_unit_test;
diff --git a/src/google/protobuf/compiler/rust/generator.cc b/src/google/protobuf/compiler/rust/generator.cc index b9e7ccb..a01a82a 100644 --- a/src/google/protobuf/compiler/rust/generator.cc +++ b/src/google/protobuf/compiler/rust/generator.cc
@@ -75,9 +75,20 @@ auto outfile = absl::WrapUnique( generator_context->Open(absl::StrCat(basename, ".pb.rs"))); - google::protobuf::io::Printer(outfile.get()).Emit(R"cc( - // TODO: Generate Bindings - )cc"); + google::protobuf::io::Printer p(outfile.get()); + // TODO(b/270138878): Remove `do_nothing` import once we have real logic. This + // is there only to smoke test rustc actions in rust_proto_library. + p.Emit(R"rs( +#[allow(unused_imports)] + use protobuf::do_nothing; + )rs"); + for (int i = 0; i < file->message_type_count(); ++i) { + // TODO(b/270138878): Implement real logic + p.Emit({{"Msg", file->message_type(i)->name()}}, R"rs( + pub struct $Msg$ {} + )rs"); + } + return true; }