"""Implementation of core Haskell rules"""
load(
"@io_tweag_rules_haskell//haskell:providers.bzl",
"C2hsLibraryInfo",
"HaskellInfo",
"HaskellLibraryInfo",
"HaskellPrebuiltPackageInfo",
)
load(":cc.bzl", "cc_interop_info")
load(
":private/actions/link.bzl",
"link_binary",
"link_library_dynamic",
"link_library_static",
)
load(":private/actions/package.bzl", "package")
load(":private/actions/repl.bzl", "build_haskell_repl")
load(":private/actions/runghc.bzl", "build_haskell_runghc")
load(":private/context.bzl", "haskell_context")
load(":private/dependencies.bzl", "gather_dep_info")
load(":private/java.bzl", "java_interop_info")
load(":private/mode.bzl", "is_profiling_enabled")
load(
":private/path_utils.bzl",
"ln",
"match_label",
"parse_pattern",
"target_unique_name",
)
load(":private/pkg_id.bzl", "pkg_id")
load(":private/set.bzl", "set")
load(":private/version_macros.bzl", "generate_version_macros")
load(":providers.bzl", "GhcPluginInfo", "HaskellCoverageInfo")
load("@bazel_skylib//lib:paths.bzl", "paths")
load("@bazel_skylib//lib:collections.bzl", "collections")
load("@bazel_skylib//lib:shell.bzl", "shell")
def _prepare_srcs(srcs):
srcs_files = []
import_dir_map = {}
for src in srcs:
# If it has the "files" attribute, it must be a Target
if hasattr(src, "files"):
if C2hsLibraryInfo in src:
srcs_files += src.files.to_list()
for f in src.files.to_list():
import_dir_map[f] = src[C2hsLibraryInfo].import_dir
else:
srcs_files += src.files.to_list()
# otherwise it's just a file
else:
srcs_files.append(src)
return srcs_files, import_dir_map
def haskell_test_impl(ctx):
return _haskell_binary_common_impl(ctx, is_test = True)
def haskell_binary_impl(ctx):
return _haskell_binary_common_impl(ctx, is_test = False)
def _should_inspect_coverage(ctx, hs, is_test):
return hs.coverage_enabled and is_test
def _coverage_enabled_for_target(coverage_source_patterns, label):
for pat in coverage_source_patterns:
if match_label(pat, label):
return True
return False
# Mix files refer to genfile srcs including their root. Therefore, we
# must condition the src filepaths passed in for coverage to match.
def _condition_coverage_src(hs, src):
if not src.path.startswith(hs.genfiles_dir.path):
return src
""" Genfiles have the genfile directory as part of their path,
so declaring a file with the sample path actually makes the new
file double-qualified by the genfile directory.
This is necessary because mix files capture the genfile
path before compilation, and then expect those files to be
qualified by the genfile directory when `hpc report` or
`hpc markup` are used. But, genfiles included as runfiles
are no longer qualified. So, double-qualifying them results in
only one level of qualification as runfiles.
"""
conditioned_src = hs.actions.declare_file(src.path)
hs.actions.run_shell(
inputs = [src],
outputs = [conditioned_src],
arguments = [
src.path,
conditioned_src.path,
],
command = """
mkdir -p $(dirname "$2") && cp "$1" "$2"
""",
)
return conditioned_src
def _haskell_binary_common_impl(ctx, is_test):
hs = haskell_context(ctx)
dep_info = gather_dep_info(ctx, ctx.attr.deps)
plugin_dep_info = gather_dep_info(
ctx,
[dep for plugin in ctx.attr.plugins for dep in plugin[GhcPluginInfo].deps],
)
# Add any interop info for other languages.
cc = cc_interop_info(ctx)
java = java_interop_info(ctx)
with_profiling = is_profiling_enabled(hs)
srcs_files, import_dir_map = _prepare_srcs(ctx.attr.srcs)
inspect_coverage = _should_inspect_coverage(ctx, hs, is_test)
c = hs.toolchain.actions.compile_binary(
hs,
cc,
java,
dep_info,
plugin_dep_info,
srcs = srcs_files,
ls_modules = ctx.executable._ls_modules,
import_dir_map = import_dir_map,
extra_srcs = depset(ctx.files.extra_srcs),
user_compile_flags = ctx.attr.compiler_flags,
dynamic = False if hs.toolchain.is_windows else not ctx.attr.linkstatic,
with_profiling = False,
main_function = ctx.attr.main_function,
version = ctx.attr.version,
inspect_coverage = inspect_coverage,
plugins = ctx.attr.plugins,
)
# gather intermediary code coverage instrumentation data
coverage_data = c.coverage_data
for dep in ctx.attr.deps:
if HaskellCoverageInfo in dep:
coverage_data += dep[HaskellCoverageInfo].coverage_data
c_p = None
if with_profiling:
c_p = hs.toolchain.actions.compile_binary(
hs,
cc,
java,
dep_info,
plugin_dep_info,
srcs = srcs_files,
ls_modules = ctx.executable._ls_modules,
import_dir_map = import_dir_map,
# NOTE We must make the object files compiled without profiling
# available to this step for TH to work, presumably because GHC is
# linked against RTS without profiling.
extra_srcs = depset(transitive = [
depset(ctx.files.extra_srcs),
depset([c.objects_dir]),
]),
user_compile_flags = ctx.attr.compiler_flags,
# NOTE We can't have profiling and dynamic code at the
# same time, see:
# https://ghc.haskell.org/trac/ghc/ticket/15394
dynamic = False,
with_profiling = True,
main_function = ctx.attr.main_function,
version = ctx.attr.version,
plugins = ctx.attr.plugins,
)
(binary, solibs) = link_binary(
hs,
cc,
dep_info,
ctx.files.extra_srcs,
ctx.attr.compiler_flags,
c_p.objects_dir if with_profiling else c.objects_dir,
dynamic = False if hs.toolchain.is_windows else not ctx.attr.linkstatic,
with_profiling = with_profiling,
version = ctx.attr.version,
)
hs_info = HaskellInfo(
package_ids = dep_info.package_ids,
package_databases = dep_info.package_databases,
version_macros = set.empty(),
source_files = c.source_files,
extra_source_files = c.extra_source_files,
import_dirs = c.import_dirs,
static_libraries = dep_info.static_libraries,
static_libraries_prof = dep_info.static_libraries_prof,
dynamic_libraries = dep_info.dynamic_libraries,
interface_dirs = dep_info.interface_dirs,
compile_flags = c.compile_flags,
prebuilt_dependencies = dep_info.prebuilt_dependencies,
cc_dependencies = dep_info.cc_dependencies,
transitive_cc_dependencies = dep_info.transitive_cc_dependencies,
)
cc_info = cc_common.merge_cc_infos(
cc_infos = [dep[CcInfo] for dep in ctx.attr.deps if CcInfo in dep],
)
target_files = depset([binary])
build_haskell_repl(
hs,
ghci_script = ctx.file._ghci_script,
ghci_repl_wrapper = ctx.file._ghci_repl_wrapper,
user_compile_flags = ctx.attr.compiler_flags,
repl_ghci_args = ctx.attr.repl_ghci_args,
output = ctx.outputs.repl,
package_databases = dep_info.package_databases,
version = ctx.attr.version,
hs_info = hs_info,
)
# XXX Temporary backwards compatibility hack. Remove eventually.
# See https://github.com/tweag/rules_haskell/pull/460.
ln(hs, ctx.outputs.repl, ctx.outputs.repl_deprecated)
build_haskell_runghc(
hs,
runghc_wrapper = ctx.file._ghci_repl_wrapper,
extra_args = ctx.attr.runcompile_flags,
user_compile_flags = ctx.attr.compiler_flags,
output = ctx.outputs.runghc,
package_databases = dep_info.package_databases,
version = ctx.attr.version,
hs_info = hs_info,
)
executable = binary
extra_runfiles = []
if inspect_coverage:
binary_path = paths.join(ctx.workspace_name, binary.short_path)
hpc_path = paths.join(ctx.workspace_name, hs.toolchain.tools.hpc.short_path)
tix_file_path = hs.label.name + ".tix"
mix_file_paths = [
paths.join(ctx.workspace_name, datum.mix_file.short_path)
for datum in coverage_data
]
mix_file_paths = collections.uniq(mix_file_paths) # remove duplicates
# find which modules to exclude from coverage analysis, by using the specified source patterns
raw_coverage_source_patterns = ctx.attr.experimental_coverage_source_patterns
coverage_source_patterns = [parse_pattern(ctx, pat) for pat in raw_coverage_source_patterns]
modules_to_exclude = [paths.split_extension(datum.mix_file.basename)[0] for datum in coverage_data if not _coverage_enabled_for_target(coverage_source_patterns, datum.target_label)]
modules_to_exclude = collections.uniq(modules_to_exclude) # remove duplicates
expected_covered_expressions_percentage = ctx.attr.expected_covered_expressions_percentage
expected_uncovered_expression_count = ctx.attr.expected_uncovered_expression_count
strict_coverage_analysis = ctx.attr.strict_coverage_analysis
coverage_report_format = ctx.attr.coverage_report_format
if coverage_report_format != "text" and coverage_report_format != "html":
fail("""haskell_test attribute "coverage_report_format" must be one of "text" or "html".""")
wrapper = hs.actions.declare_file("{}_coverage/coverage_wrapper.sh".format(ctx.label.name))
ctx.actions.expand_template(
template = ctx.file._coverage_wrapper_template,
output = wrapper,
substitutions = {
"{binary_path}": shell.quote(binary_path),
"{hpc_path}": shell.quote(hpc_path),
"{tix_file_path}": shell.quote(tix_file_path),
"{expected_covered_expressions_percentage}": str(expected_covered_expressions_percentage),
"{expected_uncovered_expression_count}": str(expected_uncovered_expression_count),
"{mix_file_paths}": shell.array_literal(mix_file_paths),
"{modules_to_exclude}": shell.array_literal(modules_to_exclude),
"{strict_coverage_analysis}": str(strict_coverage_analysis),
"{coverage_report_format}": shell.quote(ctx.attr.coverage_report_format),
"{package_path}": shell.quote(ctx.label.package),
},
is_executable = True,
)
executable = wrapper
mix_runfiles = [datum.mix_file for datum in coverage_data]
srcs_runfiles = [_condition_coverage_src(hs, datum.src_file) for datum in coverage_data]
extra_runfiles = [
ctx.file._bash_runfiles,
hs.toolchain.tools.hpc,
binary,
] + mix_runfiles + srcs_runfiles
return [
hs_info,
cc_info,
DefaultInfo(
executable = executable,
files = target_files,
runfiles = ctx.runfiles(
files =
solibs +
extra_runfiles,
collect_data = True,
),
),
]
def haskell_library_impl(ctx):
hs = haskell_context(ctx)
dep_info = gather_dep_info(ctx, ctx.attr.deps)
plugin_dep_info = gather_dep_info(
ctx,
[dep for plugin in ctx.attr.plugins for dep in plugin[GhcPluginInfo].deps],
)
version = ctx.attr.version if ctx.attr.version else None
my_pkg_id = pkg_id.new(ctx.label, version)
with_profiling = is_profiling_enabled(hs)
with_shared = False if hs.toolchain.is_windows else not ctx.attr.linkstatic
# Add any interop info for other languages.
cc = cc_interop_info(ctx)
java = java_interop_info(ctx)
srcs_files, import_dir_map = _prepare_srcs(ctx.attr.srcs)
other_modules = ctx.attr.hidden_modules
exposed_modules_reexports = _exposed_modules_reexports(ctx.attr.exports)
c = hs.toolchain.actions.compile_library(
hs,
cc,
java,
dep_info,
plugin_dep_info,
srcs = srcs_files,
ls_modules = ctx.executable._ls_modules,
other_modules = other_modules,
exposed_modules_reexports = exposed_modules_reexports,
import_dir_map = import_dir_map,
extra_srcs = depset(ctx.files.extra_srcs),
user_compile_flags = ctx.attr.compiler_flags,
with_shared = with_shared,
with_profiling = False,
my_pkg_id = my_pkg_id,
plugins = ctx.attr.plugins,
)
c_p = None
if with_profiling:
c_p = hs.toolchain.actions.compile_library(
hs,
cc,
java,
dep_info,
plugin_dep_info,
srcs = srcs_files,
ls_modules = ctx.executable._ls_modules,
other_modules = other_modules,
exposed_modules_reexports = exposed_modules_reexports,
import_dir_map = import_dir_map,
# NOTE We must make the object files compiled without profiling
# available to this step for TH to work, presumably because GHC is
# linked against RTS without profiling.
extra_srcs = depset(transitive = [
depset(ctx.files.extra_srcs),
depset([c.objects_dir]),
]),
user_compile_flags = ctx.attr.compiler_flags,
# NOTE We can't have profiling and dynamic code at the
# same time, see:
# https://ghc.haskell.org/trac/ghc/ticket/15394
with_shared = False,
with_profiling = True,
my_pkg_id = my_pkg_id,
plugins = ctx.attr.plugins,
)
static_library = link_library_static(
hs,
cc,
dep_info,
c.objects_dir,
my_pkg_id,
with_profiling = False,
)
if with_shared:
dynamic_library = link_library_dynamic(
hs,
cc,
dep_info,
depset(ctx.files.extra_srcs),
c.objects_dir,
my_pkg_id,
)
dynamic_libraries = set.insert(
dep_info.dynamic_libraries,
dynamic_library,
)
else:
dynamic_library = None
dynamic_libraries = dep_info.dynamic_libraries
static_library_prof = None
if with_profiling:
static_library_prof = link_library_static(
hs,
cc,
dep_info,
c_p.objects_dir,
my_pkg_id,
with_profiling = True,
)
conf_file, cache_file = package(
hs,
dep_info,
c.interfaces_dir,
c_p.interfaces_dir if c_p != None else None,
static_library,
dynamic_library,
c.exposed_modules_file,
other_modules,
my_pkg_id,
static_library_prof = static_library_prof,
)
static_libraries_prof = dep_info.static_libraries_prof
if static_library_prof != None:
static_libraries_prof = [static_library_prof] + dep_info.static_libraries_prof
interface_dirs = set.union(
dep_info.interface_dirs,
set.singleton(c.interfaces_dir),
)
if c_p != None:
interface_dirs = set.mutable_union(
interface_dirs,
set.singleton(c_p.interfaces_dir),
)
version_macros = set.empty()
if version != None:
version_macros = set.singleton(
generate_version_macros(ctx, hs.name, version),
)
hs_info = HaskellInfo(
package_ids = set.insert(dep_info.package_ids, pkg_id.to_string(my_pkg_id)),
package_databases = set.insert(dep_info.package_databases, cache_file),
version_macros = version_macros,
source_files = c.source_files,
extra_source_files = c.extra_source_files,
import_dirs = c.import_dirs,
# NOTE We have to use lists for static libraries because the order is
# important for linker. Linker searches for unresolved symbols to the
# left, i.e. you first feed a library which has unresolved symbols and
# then you feed the library which resolves the symbols.
static_libraries = [static_library] + dep_info.static_libraries,
static_libraries_prof = static_libraries_prof,
dynamic_libraries = dynamic_libraries,
interface_dirs = interface_dirs,
compile_flags = c.compile_flags,
prebuilt_dependencies = dep_info.prebuilt_dependencies,
cc_dependencies = dep_info.cc_dependencies,
transitive_cc_dependencies = dep_info.transitive_cc_dependencies,
)
lib_info = HaskellLibraryInfo(
package_id = pkg_id.to_string(my_pkg_id),
version = version,
)
dep_coverage_data = []
for dep in ctx.attr.deps:
if HaskellCoverageInfo in dep:
dep_coverage_data += dep[HaskellCoverageInfo].coverage_data
coverage_info = HaskellCoverageInfo(
coverage_data = dep_coverage_data + c.coverage_data,
)
target_files = depset([file for file in [static_library, dynamic_library] if file])
if hasattr(ctx, "outputs"):
build_haskell_repl(
hs,
ghci_script = ctx.file._ghci_script,
ghci_repl_wrapper = ctx.file._ghci_repl_wrapper,
repl_ghci_args = ctx.attr.repl_ghci_args,
user_compile_flags = ctx.attr.compiler_flags,
output = ctx.outputs.repl,
package_databases = dep_info.package_databases,
version = ctx.attr.version,
hs_info = hs_info,
lib_info = lib_info,
)
# XXX Temporary backwards compatibility hack. Remove eventually.
# See https://github.com/tweag/rules_haskell/pull/460.
ln(hs, ctx.outputs.repl, ctx.outputs.repl_deprecated)
build_haskell_runghc(
hs,
runghc_wrapper = ctx.file._ghci_repl_wrapper,
extra_args = ctx.attr.runcompile_flags,
user_compile_flags = ctx.attr.compiler_flags,
output = ctx.outputs.runghc,
package_databases = dep_info.package_databases,
version = ctx.attr.version,
hs_info = hs_info,
lib_info = lib_info,
)
default_info = None
if hasattr(ctx, "runfiles"):
default_info = DefaultInfo(
files = target_files,
runfiles = ctx.runfiles(collect_data = True),
)
else:
default_info = DefaultInfo(
files = target_files,
)
# Create a CcInfo provider so that CC rules can work with
# a haskell library as if it was a regular CC one.
# XXX Workaround https://github.com/bazelbuild/bazel/issues/6874.
# Should be find_cpp_toolchain() instead.
cc_toolchain = ctx.attr._cc_toolchain[cc_common.CcToolchainInfo]
feature_configuration = cc_common.configure_features(
cc_toolchain = cc_toolchain,
requested_features = ctx.features,
unsupported_features = ctx.disabled_features,
)
library_to_link = cc_common.create_library_to_link(
actions = ctx.actions,
feature_configuration = feature_configuration,
dynamic_library = dynamic_library,
static_library = static_library,
cc_toolchain = cc_toolchain,
)
compilation_context = cc_common.create_compilation_context()
linking_context = cc_common.create_linking_context(
libraries_to_link = [library_to_link],
)
cc_info = cc_common.merge_cc_infos(
cc_infos = [
CcInfo(
compilation_context = compilation_context,
linking_context = linking_context,
),
] + [dep[CcInfo] for dep in ctx.attr.deps if CcInfo in dep],
)
return [
hs_info,
cc_info,
coverage_info,
default_info,
lib_info,
]
def haskell_toolchain_library_impl(ctx):
hs = haskell_context(ctx)
if ctx.attr.package:
package = ctx.attr.package
else:
package = ctx.label.name
id_file = hs.actions.declare_file(target_unique_name(hs, "id"))
hs.actions.run_shell(
inputs = [hs.tools.ghc_pkg],
outputs = [id_file],
command = """
"$1" --simple-output -v1 field "$2" id > "$3"
""",
arguments = [
hs.tools.ghc_pkg.path,
package,
id_file.path,
],
)
version_macros_file = hs.actions.declare_file("{}_version_macros.h".format(hs.name))
hs.actions.run_shell(
inputs = [hs.tools.ghc_pkg, ctx.executable._version_macros],
outputs = [version_macros_file],
command = """
"$1" \\
`"$2" --simple-output -v1 field "$3" name` \\
`"$2" --simple-output -v1 field "$3" version` \\
> "$4"
""",
arguments = [
ctx.executable._version_macros.path,
hs.tools.ghc_pkg.path,
package,
version_macros_file.path,
],
)
prebuilt_package_info = HaskellPrebuiltPackageInfo(
package = package,
id_file = id_file,
version_macros_file = version_macros_file,
)
return [prebuilt_package_info]
def _exposed_modules_reexports(exports):
"""Creates a ghc-pkg-compatible list of reexport declarations.
A ghc-pkg registration file declares reexports as part of the
exposed-modules field in the following format:
exposed-modules: A, B, C from pkg-c:C, D from pkg-d:Original.D
Here, the Original.D module from pkg-d is renamed by virtue of a
different name being used before the "from" keyword.
This function creates a ghc-pkg-compatible list of reexport declarations
(as shown above) from a dictionary mapping package targets to "Cabal-style"
reexported-modules declarations. That is, something like:
{
":pkg-c": "C",
":pkg-d": "Original.D as D",
":pkg-e": "E1, Original.E2 as E2",
}
Args:
exports: a dictionary mapping package targets to "Cabal-style"
reexported-modules declarations.
Returns:
a ghc-pkg-compatible list of reexport declarations.
"""
exposed_reexports = []
for dep, cabal_decls in exports.items():
for cabal_decl in cabal_decls.split(","):
stripped_cabal_decl = cabal_decl.strip()
cabal_decl_parts = stripped_cabal_decl.split(" as ")
original = cabal_decl_parts[0]
if len(cabal_decl_parts) == 2:
reexported = cabal_decl_parts[1]
else:
reexported = cabal_decl_parts[0]
if HaskellPrebuiltPackageInfo in dep:
pkg = dep[HaskellPrebuiltPackageInfo].package
elif HaskellLibraryInfo in dep:
pkg = dep[HaskellLibraryInfo].package_id
exposed_reexport = "{reexported} from {pkg}:{original}".format(
reexported = reexported,
pkg = pkg,
original = original,
)
exposed_reexports.append(exposed_reexport)
return exposed_reexports