diff options
Diffstat (limited to 'third_party/bazel/rules_haskell/haskell/doctest.bzl')
-rw-r--r-- | third_party/bazel/rules_haskell/haskell/doctest.bzl | 228 |
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`. +""" |