about summary refs log tree commit diff
path: root/third_party/bazel/rules_haskell/haskell/doctest.bzl
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/bazel/rules_haskell/haskell/doctest.bzl')
-rw-r--r--third_party/bazel/rules_haskell/haskell/doctest.bzl228
1 files changed, 228 insertions, 0 deletions
diff --git a/third_party/bazel/rules_haskell/haskell/doctest.bzl b/third_party/bazel/rules_haskell/haskell/doctest.bzl
new file mode 100644
index 000000000000..dec00a5d758f
--- /dev/null
+++ b/third_party/bazel/rules_haskell/haskell/doctest.bzl
@@ -0,0 +1,228 @@
+"""Doctest support"""
+
+load("@bazel_skylib//lib:dicts.bzl", "dicts")
+load("@bazel_skylib//lib:paths.bzl", "paths")
+load(":private/context.bzl", "haskell_context", "render_env")
+load(
+    ":private/path_utils.bzl",
+    "get_lib_name",
+)
+load(":providers.bzl", "get_libs_for_ghc_linker")
+load(":private/set.bzl", "set")
+load(
+    "@io_tweag_rules_haskell//haskell:providers.bzl",
+    "HaskellInfo",
+    "HaskellLibraryInfo",
+)
+
+def _doctest_toolchain_impl(ctx):
+    return platform_common.ToolchainInfo(
+        name = ctx.label.name,
+        doctest = ctx.files.doctest,
+    )
+
+_doctest_toolchain = rule(
+    _doctest_toolchain_impl,
+    attrs = {
+        "doctest": attr.label(
+            doc = "Doctest executable",
+            cfg = "host",
+            executable = True,
+            allow_single_file = True,
+            mandatory = True,
+        ),
+    },
+)
+
+def haskell_doctest_toolchain(name, doctest, **kwargs):
+    """Declare a toolchain for the `haskell_doctest` rule.
+
+    You need at least one of these declared somewhere in your `BUILD`files
+    for `haskell_doctest` to work.  Once declared, you then need to *register*
+    the toolchain using `register_toolchains` in your `WORKSPACE` file.
+
+    Example:
+
+      In a `BUILD` file:
+
+      ```bzl
+      haskell_doctest_toolchain(
+        name = "doctest",
+        doctest = "@doctest//:bin",
+      )
+      ```
+      And in `WORKSPACE`:
+      ```
+      register_toolchains("//:doctest")
+      ```
+    """
+    impl_name = name + "-impl"
+    _doctest_toolchain(
+        name = impl_name,
+        doctest = doctest,
+        visibility = ["//visibility:public"],
+        **kwargs
+    )
+    native.toolchain(
+        name = name,
+        toolchain_type = "@io_tweag_rules_haskell//haskell:doctest-toolchain",
+        toolchain = ":" + impl_name,
+    )
+
+def _haskell_doctest_single(target, ctx):
+    """Doctest a single Haskell `target`.
+
+    Args:
+      target: Provider(s) of the target to doctest.
+      ctx: Rule context.
+
+    Returns:
+      File: the doctest log.
+    """
+
+    if HaskellInfo not in target:
+        return []
+
+    hs = haskell_context(ctx, ctx.attr)
+
+    hs_info = target[HaskellInfo]
+    cc_info = target[CcInfo]
+    lib_info = target[HaskellLibraryInfo] if HaskellLibraryInfo in target else None
+
+    args = ctx.actions.args()
+    args.add("--no-magic")
+
+    doctest_log = ctx.actions.declare_file(
+        "doctest-log-" + ctx.label.name + "-" + target.label.name,
+    )
+
+    toolchain = ctx.toolchains["@io_tweag_rules_haskell//haskell:doctest-toolchain"]
+
+    # GHC flags we have prepared before.
+    args.add_all(hs_info.compile_flags)
+
+    # Add any extra flags specified by the user.
+    args.add_all(ctx.attr.doctest_flags)
+
+    # Direct C library dependencies to link against.
+    link_ctx = hs_info.cc_dependencies.dynamic_linking
+    libs_to_link = link_ctx.libraries_to_link.to_list()
+
+    # External libraries.
+    seen_libs = set.empty()
+    for lib in libs_to_link:
+        lib_name = get_lib_name(lib)
+        if not set.is_member(seen_libs, lib_name):
+            set.mutable_insert(seen_libs, lib_name)
+            if hs.toolchain.is_darwin:
+                args.add_all([
+                    "-optl-l{0}".format(lib_name),
+                    "-optl-L{0}".format(paths.dirname(lib.path)),
+                ])
+            else:
+                args.add_all([
+                    "-l{0}".format(lib_name),
+                    "-L{0}".format(paths.dirname(lib.path)),
+                ])
+
+    # Transitive library dependencies for runtime.
+    (library_deps, ld_library_deps, ghc_env) = get_libs_for_ghc_linker(
+        hs,
+        hs_info.transitive_cc_dependencies,
+    )
+
+    sources = set.to_list(hs_info.source_files)
+
+    if ctx.attr.modules:
+        inputs = ctx.attr.modules
+    else:
+        inputs = [source.path for source in sources]
+
+    ctx.actions.run_shell(
+        inputs = depset(transitive = [
+            depset(sources),
+            set.to_depset(hs_info.package_databases),
+            set.to_depset(hs_info.interface_dirs),
+            set.to_depset(hs_info.dynamic_libraries),
+            cc_info.compilation_context.headers,
+            depset(library_deps),
+            depset(ld_library_deps),
+            depset(
+                toolchain.doctest +
+                [hs.tools.ghc],
+            ),
+        ]),
+        outputs = [doctest_log],
+        mnemonic = "HaskellDoctest",
+        progress_message = "HaskellDoctest {}".format(ctx.label),
+        command = """
+        {env}
+        {doctest} "$@" {inputs} > {output} 2>&1 || (rc=$? && cat {output} && exit $rc)
+        """.format(
+            doctest = toolchain.doctest[0].path,
+            output = doctest_log.path,
+            inputs = " ".join(inputs),
+            # XXX Workaround
+            # https://github.com/bazelbuild/bazel/issues/5980.
+            env = render_env(hs.env),
+        ),
+        arguments = [args],
+        # NOTE It looks like we must specify the paths here as well as via -L
+        # flags because there are at least two different "consumers" of the info
+        # (ghc and linker?) and they seem to prefer to get it in different ways
+        # in this case.
+        env = dicts.add(
+            ghc_env,
+            hs.env,
+        ),
+        execution_requirements = {
+            # Prevents a race condition among concurrent doctest tests on Linux.
+            #
+            # The reason is that the doctest process uses its own PID to determine the name
+            # of its working directory. In presence of PID namespacing, this occasionally results
+            # in multiple concurrent processes attempting to create the same directory.
+            # See https://github.com/sol/doctest/issues/219 for details.
+            #
+            # For some reason, setting "exclusive": "1" does not fix the issue, so we disable
+            # sandboxing altogether for doctest tests.
+            "no-sandbox": "1",
+        },
+    )
+    return doctest_log
+
+def _haskell_doctest_impl(ctx):
+    logs = []
+
+    for dep in ctx.attr.deps:
+        logs.append(_haskell_doctest_single(dep, ctx))
+
+    return DefaultInfo(
+        files = depset(logs),
+    )
+
+haskell_doctest = rule(
+    _haskell_doctest_impl,
+    attrs = {
+        "deps": attr.label_list(
+            doc = "List of Haskell targets to lint.",
+        ),
+        "doctest_flags": attr.string_list(
+            doc = "Extra flags to pass to doctest executable.",
+        ),
+        "modules": attr.string_list(
+            doc = """List of names of modules that will be tested. If the list is
+omitted, all exposed modules provided by `deps` will be tested.
+""",
+        ),
+    },
+    toolchains = [
+        "@io_tweag_rules_haskell//haskell:toolchain",
+        "@io_tweag_rules_haskell//haskell:doctest-toolchain",
+    ],
+)
+"""Run doctest test on targets in `deps`.
+
+Note that your toolchain must be equipped with `doctest` executable, i.e.
+you should specify location of the executable using the `doctest` attribute
+of `haskell_doctest_toolchain`.
+"""