# Copyright 2020 The Bazel Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. "Protocol Buffers" load("@build_bazel_rules_nodejs//:providers.bzl", "DeclarationInfo") load("@build_bazel_rules_nodejs//third_party/github.com/bazelbuild/bazel-skylib:lib/paths.bzl", "paths") load("@rules_proto//proto:defs.bzl", "ProtoInfo") typescript_proto_library_aspect = provider( fields = { "deps_dts": "The transitive dependencies' TS definitions", "deps_js": "The transitive JS dependencies", "dts_outputs": "Ths TS definition files produced directly from the src protos", "js_outputs": "The JS files produced directly from the src protos", }, ) def _direct_source_infos(proto_info, provided_sources = []): """Returns sequence of `ProtoFileInfo` for `proto_info`'s direct sources. Files that are both in `proto_info`'s direct sources and in `provided_sources` are skipped. This is useful, e.g., for well-known protos that are already provided by the Protobuf runtime. Args: proto_info: An instance of `ProtoInfo`. provided_sources: Optional. A sequence of files to ignore. Usually, these files are already provided by the Protocol Buffer runtime (e.g. Well-Known protos). Returns: A sequence of `ProtoFileInfo` containing information about `proto_info`'s direct sources. """ source_root = proto_info.proto_source_root if "." == source_root: return [struct(file = src, import_path = src.path) for src in proto_info.direct_sources] offset = len(source_root) + 1 # + '/'. infos = [] for src in proto_info.direct_sources: infos.append(struct(file = src, import_path = src.path[offset:])) return infos def _get_protoc_inputs(target, ctx): inputs = [] inputs += target[ProtoInfo].direct_sources inputs += target[ProtoInfo].transitive_descriptor_sets.to_list() return inputs def _build_protoc_command(target, ctx): protoc_command = "%s" % (ctx.executable._protoc.path) protoc_command += " --plugin=protoc-gen-grpc-web=%s" % (ctx.executable._protoc_gen_grpc_web.path) protoc_output_dir = paths.join(ctx.bin_dir.path, ctx.label.workspace_root) protoc_command += " --grpc-web_out=import_style=commonjs+dts,mode=grpcweb:%s" % (protoc_output_dir) protoc_command += " --js_out=import_style=commonjs,binary:%s" % (protoc_output_dir) descriptor_sets_paths = [desc.path for desc in target[ProtoInfo].transitive_descriptor_sets.to_list()] pathsep = ctx.configuration.host_path_separator protoc_command += " --descriptor_set_in=\"%s\"" % (pathsep.join(descriptor_sets_paths)) proto_file_infos = _direct_source_infos(target[ProtoInfo]) for f in proto_file_infos: protoc_command += " %s" % f.import_path return protoc_command def _create_post_process_command(target, ctx, js_outputs): """ Builds a post-processing command that creates output files not created by protoc. """ output_paths = "" for output in js_outputs: output_paths += " {}".format(output.path) return ctx.executable._post_process.path + output_paths def _get_outputs(target, ctx): """ Calculates all of the files that will be generated by the aspect. """ js_outputs = [] dts_outputs = [] for src in target[ProtoInfo].direct_sources: file_name = src.basename[:-len(src.extension) - 1] generated_files = ["_pb", "_grpc_web_pb"] for f in generated_files: full_name = file_name + f output = ctx.actions.declare_file(full_name + ".js") js_outputs.append(output) output_d_ts = ctx.actions.declare_file(file_name + f + ".d.ts") dts_outputs.append(output_d_ts) return [js_outputs, dts_outputs] def ts_proto_library_aspect_(target, ctx): """ A bazel aspect that is applied on every proto_library rule on the transitive set of dependencies of a ts_proto_library rule. Handles running protoc to produce the generated JS and TS files. """ [js_outputs, dts_outputs] = _get_outputs(target, ctx) protoc_outputs = dts_outputs + js_outputs all_commands = [ _build_protoc_command(target, ctx), _create_post_process_command(target, ctx, js_outputs), ] tools = [] tools.extend(ctx.files._protoc) tools.extend(ctx.files._protoc_gen_grpc_web) tools.extend(ctx.files._post_process) ctx.actions.run_shell( inputs = depset( direct = _get_protoc_inputs(target, ctx), transitive = [depset(ctx.files._well_known_protos)], ), outputs = protoc_outputs, progress_message = "Creating Typescript pb files %s" % ctx.label, command = " && ".join(all_commands), tools = depset(tools), ) dts_outputs = depset(dts_outputs) js_outputs = depset(js_outputs) deps_dts = [] deps_js = [] for dep in ctx.rule.attr.deps: aspect_data = dep[typescript_proto_library_aspect] deps_dts.append(aspect_data.dts_outputs) deps_dts.append(aspect_data.deps_dts) deps_js.append(aspect_data.js_outputs) deps_js.append(aspect_data.deps_js) return [typescript_proto_library_aspect( dts_outputs = dts_outputs, js_outputs = js_outputs, deps_dts = depset(transitive = deps_dts), deps_js = depset(transitive = deps_js), )] ts_proto_library_aspect = aspect( implementation = ts_proto_library_aspect_, attr_aspects = ["deps"], attrs = { "_post_process": attr.label( executable = True, cfg = "host", allow_files = True, default = Label("//@bazel/labs/grpc_web:post_process"), ), "_protoc": attr.label( allow_single_file = True, executable = True, cfg = "host", default = Label("@com_google_protobuf//:protoc"), ), "_protoc_gen_grpc_web": attr.label( allow_files = True, executable = True, cfg = "host", default = Label("@com_github_grpc_grpc_web//javascript/net/grpc/web/generator:protoc-gen-grpc-web"), ), "_well_known_protos": attr.label( default = "@com_google_protobuf//:well_known_protos", allow_files = True, ), }, ) def _ts_proto_library_impl(ctx): """ Handles converting the aspect output into a provider compatible with the rules_typescript rules. """ aspect_data = ctx.attr.proto[typescript_proto_library_aspect] dts_outputs = aspect_data.dts_outputs transitive_declarations = depset(transitive = [dts_outputs, aspect_data.deps_dts]) js_outputs = aspect_data.js_outputs outputs = depset(transitive = [js_outputs, dts_outputs]) return [ DefaultInfo(files = outputs), DeclarationInfo( declarations = dts_outputs, transitive_declarations = transitive_declarations, type_blocklisted_declarations = depset([]), ), ] ts_proto_library = rule( attrs = { "proto": attr.label( allow_single_file = True, aspects = [ts_proto_library_aspect], mandatory = True, providers = [ProtoInfo], ), "_protoc": attr.label( allow_single_file = True, cfg = "host", default = Label("@com_google_protobuf//:protoc"), executable = True, ), "_protoc_gen_grpc_web": attr.label( allow_files = True, cfg = "host", default = Label("@com_github_grpc_grpc_web//javascript/net/grpc/web/generator:protoc-gen-grpc-web"), executable = True, ), "_well_known_protos": attr.label( allow_files = True, default = "@com_google_protobuf//:well_known_protos", ), }, implementation = _ts_proto_library_impl, )