about summary refs log blame commit diff
path: root/third_party/bazel/rules_haskell/haskell/protobuf.bzl
blob: 5c8b9a81764872dec6a7bcc91dcc0418763ce1b3 (plain) (tree)










































































































































































































































































































































































































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