about summary refs log tree commit diff
path: root/third_party/bazel/rules_haskell/haskell/protobuf.bzl
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/bazel/rules_haskell/haskell/protobuf.bzl')
-rw-r--r--third_party/bazel/rules_haskell/haskell/protobuf.bzl395
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",
+        ],
+    )