diff options
Diffstat (limited to 'third_party/bazel/rules_haskell/haskell/protobuf.bzl')
-rw-r--r-- | third_party/bazel/rules_haskell/haskell/protobuf.bzl | 395 |
1 files changed, 395 insertions, 0 deletions
diff --git a/third_party/bazel/rules_haskell/haskell/protobuf.bzl b/third_party/bazel/rules_haskell/haskell/protobuf.bzl new file mode 100644 index 000000000000..5c8b9a817648 --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/protobuf.bzl @@ -0,0 +1,395 @@ +"""Support for protocol buffers""" + +load( + ":private/haskell_impl.bzl", + _haskell_library_impl = "haskell_library_impl", +) +load("@bazel_skylib//lib:paths.bzl", "paths") +load( + "@io_tweag_rules_haskell//haskell:providers.bzl", + "HaskellInfo", + "HaskellLibraryInfo", + "HaskellProtobufInfo", +) + +def _capitalize_first_letter(c): + """Capitalize the first letter of the input. Unlike the built-in + `capitalize()` method, doesn't lower-case the other characters. This helps + mimic the behavior of `proto-lens-protoc`, which turns `Foo/Bar/BAZ.proto` + into `Foo/Bar/BAZ.hs` (rather than `Foo/Bar/Baz.hs`). + + Args: + c: A non-empty string word. + + Returns: + The input with the first letter upper-cased. + """ + return c[0].capitalize() + c[1:] + +def _camel_case(comp): + """Camel-case the input string, preserving any existing capital letters. + """ + + # Split on both "-" and "_", matching the behavior of proto-lens-protoc. + # Be sure to ignore any empty segments from input with leading or trailing + # delimiters. + return "".join([ + _capitalize_first_letter(c2) + for c1 in comp.split("_") + for c2 in c1.split("-") + if len(c2) > 0 + ]) + +def _proto_lens_output_file(path): + """The output file from `proto-lens-protoc` when run on the given `path`. + """ + + path = path[:-len(".proto")] + result = "/".join([_camel_case(p) for p in path.split("/")]) + ".hs" + + return "Proto/" + result + +def _proto_lens_fields_file(path): + """The fields file from `proto-lens-protoc` when run on the given `path`. + """ + + path = path[:-len(".proto")] + result = "/".join([_camel_case(p) for p in path.split("/")]) + "_Fields.hs" + + return "Proto/" + result + +def _proto_path(proto, proto_source_roots): + """A path to the proto file which matches any import statements.""" + proto_path = proto.path + for p in proto_source_roots: + if proto_path.startswith(p): + return paths.relativize(proto_path, p) + + return paths.relativize( + proto_path, + paths.join(proto.root.path, proto.owner.workspace_root), + ) + +def _haskell_proto_aspect_impl(target, ctx): + pb = ctx.toolchains["@io_tweag_rules_haskell//protobuf:toolchain"].tools + + args = ctx.actions.args() + + src_prefix = paths.join( + ctx.label.workspace_root, + ctx.label.package, + ) + + args.add("--plugin=protoc-gen-haskell=" + pb.plugin.path) + + hs_files = [] + inputs = [] + + direct_proto_paths = [target.proto.proto_source_root] + transitive_proto_paths = target.proto.transitive_proto_path + + args.add_all([ + "-I{0}={1}".format(_proto_path(s, transitive_proto_paths), s.path) + for s in target.proto.transitive_sources.to_list() + ]) + + inputs.extend(target.proto.transitive_sources.to_list()) + + for src in target.proto.direct_sources: + inputs.append(src) + + # As with the native rules, require the .proto file to be in the same + # Bazel package as the proto_library rule. This allows us to put the + # output .hs file next to the input .proto file. Unfortunately Skylark + # doesn't let us check the package of the file directly, so instead we + # just look at its short_path and rely on the proto_library rule itself + # to check for consistency. We use the file's path rather than its + # dirname/basename in case it's in a subdirectory; for example, if the + # proto_library rule is in "foo/BUILD" but the .proto file is + # "foo/bar/baz.proto". + + if not src.path.startswith(paths.join(src.root.path, src_prefix)): + fail("Mismatch between rule context " + str(ctx.label.package) + + " and source file " + src.short_path) + if src.basename[-6:] != ".proto": + fail("bad extension for proto file " + src) + + args.add(src.path) + hs_files.append(ctx.actions.declare_file( + _proto_lens_output_file( + _proto_path(src, direct_proto_paths), + ), + )) + hs_files.append(ctx.actions.declare_file( + _proto_lens_fields_file( + _proto_path(src, direct_proto_paths), + ), + )) + + args.add_all([ + "--proto_path=" + target.proto.proto_source_root, + "--haskell_out=no-runtime:" + paths.join( + hs_files[0].root.path, + src_prefix, + ), + ]) + + ctx.actions.run( + inputs = depset([pb.protoc, pb.plugin] + inputs), + outputs = hs_files, + mnemonic = "HaskellProtoc", + executable = pb.protoc, + arguments = [args], + ) + + patched_attrs = { + "compiler_flags": [], + "src_strip_prefix": "", + "repl_interpreted": True, + "repl_ghci_args": [], + "version": "", + "linkstatic": False, + "_ghci_script": ctx.attr._ghci_script, + "_ghci_repl_wrapper": ctx.attr._ghci_repl_wrapper, + "hidden_modules": [], + "exports": {}, + "name": "proto-autogen-" + ctx.rule.attr.name, + "srcs": hs_files, + "deps": ctx.rule.attr.deps + + ctx.toolchains["@io_tweag_rules_haskell//protobuf:toolchain"].deps, + "prebuilt_dependencies": ctx.toolchains["@io_tweag_rules_haskell//protobuf:toolchain"].prebuilt_deps, + "plugins": [], + "_cc_toolchain": ctx.attr._cc_toolchain, + } + + patched_ctx = struct( + actions = ctx.actions, + attr = struct(**patched_attrs), + bin_dir = ctx.bin_dir, + disabled_features = ctx.rule.attr.features, + executable = struct( + _ls_modules = ctx.executable._ls_modules, + ), + # Necessary for CC interop (see cc.bzl). + features = ctx.rule.attr.features, + file = ctx.file, + files = struct( + srcs = hs_files, + _cc_toolchain = ctx.files._cc_toolchain, + extra_srcs = depset(), + ), + genfiles_dir = ctx.genfiles_dir, + label = ctx.label, + toolchains = ctx.toolchains, + var = ctx.var, + ) + + # TODO this pattern match is very brittle. Let's not do this. The + # order should match the order in the return value expression in + # haskell_library_impl(). + [hs_info, cc_info, coverage_info, default_info, library_info] = _haskell_library_impl(patched_ctx) + + return [ + cc_info, # CcInfo + hs_info, # HaskellInfo + library_info, # HaskellLibraryInfo + # We can't return DefaultInfo here because target already provides that. + HaskellProtobufInfo(files = default_info.files), + ] + +_haskell_proto_aspect = aspect( + _haskell_proto_aspect_impl, + attr_aspects = ["deps"], + attrs = { + "_ghci_script": attr.label( + allow_single_file = True, + default = Label("@io_tweag_rules_haskell//haskell:assets/ghci_script"), + ), + "_ghci_repl_wrapper": attr.label( + allow_single_file = True, + default = Label("@io_tweag_rules_haskell//haskell:private/ghci_repl_wrapper.sh"), + ), + "_ls_modules": attr.label( + executable = True, + cfg = "host", + default = Label("@io_tweag_rules_haskell//haskell:ls_modules"), + ), + "_cc_toolchain": attr.label( + default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), + ), + }, + toolchains = [ + "@io_tweag_rules_haskell//haskell:toolchain", + "@io_tweag_rules_haskell//protobuf:toolchain", + ], +) + +def _haskell_proto_library_impl(ctx): + dep = ctx.attr.deps[0] # FIXME + return [ + dep[CcInfo], + dep[HaskellInfo], + dep[HaskellLibraryInfo], + DefaultInfo(files = dep[HaskellProtobufInfo].files), + ] + +haskell_proto_library = rule( + _haskell_proto_library_impl, + attrs = { + "deps": attr.label_list( + mandatory = True, + allow_files = False, + aspects = [_haskell_proto_aspect], + doc = "List of `proto_library` targets to use for generation.", + ), + }, + toolchains = [ + "@io_tweag_rules_haskell//haskell:toolchain", + "@io_tweag_rules_haskell//protobuf:toolchain", + ], +) + +"""Generate Haskell library allowing to use protobuf definitions with help +of [`proto-lens`](https://github.com/google/proto-lens#readme). + +Example: + ```bzl + proto_library( + name = "foo_proto", + srcs = ["foo.proto"], + ) + + haskell_proto_library( + name = "foo_haskell_proto", + deps = [":foo_proto"], + ) + ``` + +`haskell_proto_library` targets require `haskell_proto_toolchain` to be +registered. +""" + +def _protobuf_toolchain_impl(ctx): + if ctx.attr.prebuilt_deps: + print("""The attribute 'prebuilt_deps' has been deprecated, +use the 'deps' attribute instead. +""") + + return [ + platform_common.ToolchainInfo( + name = ctx.label.name, + tools = struct( + plugin = ctx.executable.plugin, + protoc = ctx.executable.protoc, + ), + deps = ctx.attr.deps, + prebuilt_deps = ctx.attr.prebuilt_deps, + ), + ] + +_protobuf_toolchain = rule( + _protobuf_toolchain_impl, + attrs = { + "protoc": attr.label( + executable = True, + cfg = "host", + allow_single_file = True, + mandatory = True, + doc = "protoc compiler", + ), + "plugin": attr.label( + executable = True, + cfg = "host", + allow_single_file = True, + mandatory = True, + doc = "proto-lens-protoc plugin for protoc", + ), + "deps": attr.label_list( + doc = "List of other Haskell libraries to be linked to protobuf libraries.", + ), + "prebuilt_deps": attr.string_list( + doc = "Non-Bazel supplied Cabal dependencies for protobuf libraries.", + ), + }, +) + +def haskell_proto_toolchain( + name, + plugin, + deps = [], + prebuilt_deps = [], + protoc = Label("@com_google_protobuf//:protoc"), + **kwargs): + """Declare a Haskell protobuf toolchain. + + You need at least one of these declared somewhere in your `BUILD` files + for the `haskell_proto_library` rules to work. Once declared, you then + need to *register* the toolchain using `register_toolchains` in your + `WORKSPACE` file (see example below). + + Example: + + In a `BUILD` file: + + ```bzl + haskell_proto_toolchain( + name = "protobuf-toolchain", + protoc = "@com_google_protobuf//:protoc", + plugin = "@hackage-proto-lens-protoc//:bin/proto-lens-protoc", + prebuilt_deps = [ + "base", + "bytestring", + "containers", + "data-default-class", + "lens-family", + "lens-labels", + "proto-lens", + "text", + ], + ) + ``` + + The `prebuilt_deps` and `deps` arguments allow to specify Haskell + libraries to use to compile the auto-generated source files. + + In `WORKSPACE` you could have something like this: + + ```bzl + http_archive( + name = "com_google_protobuf", + sha256 = "cef7f1b5a7c5fba672bec2a319246e8feba471f04dcebfe362d55930ee7c1c30", + strip_prefix = "protobuf-3.5.0", + urls = ["https://github.com/google/protobuf/archive/v3.5.0.zip"], + ) + + nixpkgs_package( + name = "protoc_gen_haskell", + repository = "@nixpkgs", + attribute_path = "haskell.packages.ghc822.proto-lens-protoc + ) + + register_toolchains( + "//tests:ghc", # assuming you called your Haskell toolchain "ghc" + "//tests:protobuf-toolchain", + ) + ``` + """ + impl_name = name + "-impl" + _protobuf_toolchain( + name = impl_name, + plugin = plugin, + deps = deps, + prebuilt_deps = prebuilt_deps, + protoc = protoc, + visibility = ["//visibility:public"], + **kwargs + ) + + native.toolchain( + name = name, + toolchain_type = "@io_tweag_rules_haskell//protobuf:toolchain", + toolchain = ":" + impl_name, + exec_compatible_with = [ + "@bazel_tools//platforms:x86_64", + ], + ) |