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
pull/12156/head
Marcel Hlopko 2023-03-06 14:09:46 -08:00 committed by Copybara-Service
parent 77e3f2c38d
commit 3dc546daff
13 changed files with 453 additions and 15 deletions

View File

@ -24,4 +24,4 @@ jobs:
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"

View File

@ -136,4 +136,4 @@ http_archive(
load("@rules_rust//rust:repositories.bzl", "rules_rust_dependencies", "rust_register_toolchains")
rules_rust_dependencies()
rust_register_toolchains()
rust_register_toolchains(edition = "2021")

19
rust/BUILD Normal file
View File

@ -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"],
)

View File

@ -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"],
)

242
rust/defs.bzl Normal file
View File

@ -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],
),
},
)

View File

@ -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() {}

16
rust/test/BUILD Normal file
View File

@ -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"],
)

View File

@ -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;
}

View File

@ -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")

View File

@ -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]),
},
)

View File

@ -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",
],
)

View File

@ -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;

View File

@ -75,9 +75,20 @@ bool RustGenerator::Generate(const FileDescriptor* file,
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;
}