"""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`.
"""