228 lines
		
	
	
	
		
			6.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			228 lines
		
	
	
	
		
			6.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """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`.
 | |
| """
 |