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