diff --git a/haskell/cabal.bzl b/haskell/cabal.bzl index 41ce68554..750292d39 100644 --- a/haskell/cabal.bzl +++ b/haskell/cabal.bzl @@ -573,6 +573,7 @@ def _haskell_cabal_library_impl(ctx): ) hs_info = HaskellInfo( package_databases = depset([package_database], transitive = [dep_info.package_databases]), + empty_lib_package_databases = depset(transitive = [dep_info.empty_lib_package_databases]), version_macros = set.empty(), source_files = depset(), boot_files = depset(), @@ -583,6 +584,7 @@ def _haskell_cabal_library_impl(ctx): transitive = [dep_info.hs_libraries], order = "topological", ), + empty_hs_libraries = dep_info.empty_hs_libraries, interface_dirs = depset([interfaces_dir], transitive = [dep_info.interface_dirs]), compile_flags = [], user_compile_flags = [], @@ -864,12 +866,14 @@ def _haskell_cabal_binary_impl(ctx): hs_info = HaskellInfo( package_databases = dep_info.package_databases, + empty_lib_package_databases = dep_info.empty_lib_package_databases, version_macros = set.empty(), source_files = depset(), boot_files = depset(), extra_source_files = depset(), import_dirs = set.empty(), hs_libraries = dep_info.hs_libraries, + empty_hs_libraries = dep_info.empty_hs_libraries, interface_dirs = dep_info.interface_dirs, compile_flags = [], user_compile_flags = [], diff --git a/haskell/experimental/defs.bzl b/haskell/experimental/defs.bzl index 8ffcb091b..8559ddaa6 100644 --- a/haskell/experimental/defs.bzl +++ b/haskell/experimental/defs.bzl @@ -26,6 +26,7 @@ _haskell_module = rule( ), "deps": attr.label_list(), "cross_library_deps": attr.label_list(), + "enable_th": attr.bool(), "ghcopts": attr.string_list(), "plugins": attr.label_list( aspects = [haskell_cc_libraries_aspect], @@ -59,6 +60,7 @@ def haskell_module( module_name = "", deps = [], cross_library_deps = [], + enable_th = False, ghcopts = [], plugins = [], tools = [], @@ -124,6 +126,9 @@ def haskell_module( cross_library_deps: List of other Haskell modules needed to compile this module that come from other libraries. They need to be included in the `modules` attribute of any library in the `narrowed_deps` attribute of the enclosing library, binary, or test + enable_th: Exposes object files or libraries to the build action. This is necessary when the module uses + Template Haskell. The libraries of narrowed deps are exposed instead of object files in profiling + builds due to technical limitations. ghcopts: Flags to pass to Haskell compiler. Subject to Make variable substitution. This is merged with the ghcopts attribute of rules that depend directly on this haskell_module rule. plugins: Compiler plugins to use during compilation. (Not implemented, yet) @@ -140,6 +145,7 @@ def haskell_module( module_name = module_name, deps = deps, cross_library_deps = cross_library_deps, + enable_th = enable_th, ghcopts = ghcopts, plugins = plugins, tools = tools, diff --git a/haskell/experimental/private/module.bzl b/haskell/experimental/private/module.bzl index 99f134497..6c66145b9 100644 --- a/haskell/experimental/private/module.bzl +++ b/haskell/experimental/private/module.bzl @@ -12,6 +12,7 @@ load( "//haskell:private/mode.bzl", "is_profiling_enabled", ) +load("//haskell:private/pkg_id.bzl", "pkg_id") load( "//haskell:private/packages.bzl", "expose_packages", @@ -32,6 +33,44 @@ load( ) load("//haskell:providers.bzl", "HaskellInfo", "HaskellLibraryInfo") +# Note [Narrowed Dependencies] +# +# Usually, when a module M depends on a library L, it doesn't depend on +# all the modules of the library. The user expresses which modules are +# needed with the cross_library_deps attribute and rules_haskell looks +# for the modules in the libraries of the narrowed_deps attribute in the +# enclosing haskell_library. +# +# build_haskell_modules from module.bzl produces dictionaries that say +# for every module label which interface and object files it produces +# and which interface and object files it depends upon transitively. +# These dictionaries end up in the HaskellInfo provider. +# +# It is not strictly necessary to depend on all interface and object +# files transitively, but we have no easy way to discern which files +# from the transitive dependencies are actually needed, so we +# over-approximate by depending on all of them. +# +# When compilation doesn't involve Template Haskell, a module only needs +# the interface files of its module dependencies. When the user +# specifies that a module uses Template Haskell via the enable_th +# attribute, we also pass the potentially needed object files in the +# inputs of the build action of haskell_module. +# +# Telling ghc which interface files are available is easy by specifying +# package databases and package-ids on the command line. Passing object +# files is harder because ghc only expects the object files of libraries +# to be linked together in installed packages. It is possible to tell to +# ghc about individual object files by listing their filepaths on the +# command line, but see Note [Empty Libraries] in haskell_impl.bzl for an +# extra nuance. +# +# Unfortunately, passing object files in the command line doesn't cause +# ghc to load them in the external interpreter, so narrowing doesn't +# work in any configuration needing the external interpreter. Therefore, +# profiling builds use the libraries of narrowed_deps instead of the +# their object files. + def _build_haskell_module( ctx, hs, @@ -46,6 +85,7 @@ def _build_haskell_module( module_outputs, interface_inputs, object_inputs, + narrowed_objects, module): """Build a module @@ -63,6 +103,7 @@ def _build_haskell_module( module_outputs: A struct containing the interfaces and object files produced for a haskell_module. interface_inputs: A depset containing the interface files needed as input object_inputs: A depset containing the object files needed as input + narrowed_objects: A depset containing the narrowed object files needed as arguments to ghc. module: The Target of the haskell_module rule """ @@ -151,7 +192,12 @@ def _build_haskell_module( hs, pkg_info = expose_packages( package_ids = hs.package_ids, - package_databases = depset(transitive = [dep_info.package_databases, narrowed_deps_info.package_databases]), + package_databases = depset( + transitive = [ + dep_info.package_databases, + narrowed_deps_info.empty_lib_package_databases, + ], + ), # TODO[AH] Support version macros version = None, ), @@ -192,6 +238,7 @@ def _build_haskell_module( moduleAttr.tools, ] args.add_all(expand_make_variables("ghcopts", ctx, moduleAttr.ghcopts, module_extra_attrs)) + args.add_all(narrowed_objects) outputs = [module_outputs.hi] if module_outputs.o: @@ -210,8 +257,8 @@ def _build_haskell_module( transitive = [ dep_info.package_databases, dep_info.interface_dirs, - dep_info.hs_libraries, - narrowed_deps_info.package_databases, + narrowed_deps_info.empty_hs_libraries, + narrowed_deps_info.empty_lib_package_databases, pkg_info_inputs, plugin_dep_info.package_databases, plugin_dep_info.interface_dirs, @@ -219,7 +266,12 @@ def _build_haskell_module( plugin_tool_inputs, preprocessors_inputs, interface_inputs, - object_inputs, + narrowed_objects, + ] + [ + files + for files in [dep_info.hs_libraries, object_inputs] + # libraries and object inputs are only needed if the module uses TH + if module[HaskellModuleInfo].attr.enable_th ], ), input_manifests = preprocessors_input_manifests + plugin_tool_input_manifests, @@ -377,10 +429,15 @@ def _merge_narrowed_deps_dicts(rule_label, narrowed_deps): narrowed_deps: The contents of the narrowed_deps attribute Returns: - dict of module labels to their interfaces and the interfaces of their transitive - module dependencies + pair of per_module_transitive_interfaces, per_module_transitive_objects: + per_module_transitive_interfaces: dict of module labels to their + interfaces and the interfaces of their transitive module dependencies + per_module_transitive_objects: dict of module labels to their + object files and the object file of their transitive module + dependencies """ per_module_transitive_interfaces = {} + per_module_transitive_objects = {} for dep in narrowed_deps: if not HaskellInfo in dep or not HaskellLibraryInfo in dep: fail("{}: depedency {} is not a haskell_library as required when used in narrowed_deps".format( @@ -394,7 +451,8 @@ def _merge_narrowed_deps_dicts(rule_label, narrowed_deps): str(dep.label), )) _merge_depset_dicts(per_module_transitive_interfaces, lib_info.per_module_transitive_interfaces) - return per_module_transitive_interfaces + _merge_depset_dicts(per_module_transitive_objects, lib_info.per_module_transitive_objects) + return per_module_transitive_interfaces, per_module_transitive_objects def interfaces_as_list(with_shared, o): if with_shared: @@ -411,43 +469,71 @@ def build_haskell_modules(ctx, hs, cc, posix, package_name, with_shared, hidir, hs: Haskell context cc: CcInteropInfo, information about C dependencies posix: posix toolchain + package_name: package name if building a library or empty if building a binary with_shared: Whether to build dynamic object files hidir: The directory in which to output interface files odir: The directory in which to output object files Returns: - struct(his, dyn_his, os, dyn_os, per_module_transitive_interfaces): + struct(his, dyn_his, os, dyn_os, per_module_transitive_interfaces, per_module_transitive_objects): his: interface files of all modules in ctx.attr.modules dyn_his: dynamic interface files of all modules in ctx.attr.modules os: object files of all modules in ctx.attr.modules dyn_os: dynamic object files of all modules in ctx.attr.modules per_module_transitive_interfaces: dict of module labels to their interfaces and the interfaces of their transitive module - dependencies + dependencies. See Note [Narrowed Dependencies]. + per_module_transitive_objects: dict of module labels to their + object files and the object files of their transitive module + dependencies. See Note [Narrowed Dependencies]. """ + per_module_transitive_interfaces, per_module_transitive_objects = _merge_narrowed_deps_dicts(ctx.label, ctx.attr.narrowed_deps) - per_module_transitive_interfaces = _merge_narrowed_deps_dicts(ctx.label, ctx.attr.narrowed_deps) - + # We produce separate infos for narrowed_deps and deps because the module + # files in dep_info are given as inputs to the build action, but the + # modules files from narrowed_deps_info are only given when haskell_module + # declares to depend on them. dep_info = gather_dep_info(ctx.attr.name, ctx.attr.deps) narrowed_deps_info = gather_dep_info(ctx.attr.name, ctx.attr.narrowed_deps) + all_deps_info = gather_dep_info(ctx.attr.name, ctx.attr.deps + ctx.attr.narrowed_deps) + empty_deps_info = gather_dep_info(ctx.attr.name, []) transitive_module_deps = _reorder_module_deps_to_postorder(ctx.label, ctx.attr.modules) module_outputs = {dep.label: _declare_module_outputs(hs, with_shared, hidir, odir, dep) for dep in transitive_module_deps} module_interfaces = {} module_objects = {} for dep in transitive_module_deps: + # called in all cases to validate cross_library_deps, although the output + # might be ignored when disabling narrowing + narrowed_interfaces = _collect_narrowed_deps_module_files(ctx.label, per_module_transitive_interfaces, dep) + enable_th = dep[HaskellModuleInfo].attr.enable_th + + # Narrowing doesn't work when using the external interpreter so we disable it here + if enable_th and is_profiling_enabled(hs): + per_module_transitive_interfaces_i = [] + per_module_transitive_objects_i = [] + dep_info_i = all_deps_info + narrowed_deps_info_i = empty_deps_info + narrowed_interfaces = depset() + narrowed_objects = depset() + else: + dep_info_i = dep_info + narrowed_deps_info_i = narrowed_deps_info + + # objects are only needed if the module uses TH + narrowed_objects = _collect_narrowed_deps_module_files(ctx.label, per_module_transitive_objects, dep) if enable_th else depset() + his, os = _collect_module_outputs_of_direct_deps(with_shared, module_outputs, dep) interface_inputs = _collect_module_inputs(module_interfaces, his, dep) object_inputs = _collect_module_inputs(module_objects, os, dep) - narrowed_interfaces = _collect_narrowed_deps_module_files(ctx.label, per_module_transitive_interfaces, dep) _build_haskell_module( ctx, hs, cc, posix, - dep_info, - narrowed_deps_info, + dep_info_i, + narrowed_deps_info_i, package_name, with_shared, hidir, @@ -455,6 +541,7 @@ def build_haskell_modules(ctx, hs, cc, posix, package_name, with_shared, hidir, module_outputs[dep.label], depset(transitive = [interface_inputs, narrowed_interfaces]), object_inputs, + narrowed_objects, dep, ) @@ -477,6 +564,14 @@ def build_haskell_modules(ctx, hs, cc, posix, package_name, with_shared, hidir, for dep in transitive_module_deps } _merge_depset_dicts(per_module_transitive_interfaces0, per_module_transitive_interfaces) + per_module_transitive_objects0 = { + dep.label: depset( + [module_outputs[dep.label].o], + transitive = [module_objects[dep.label]], + ) + for dep in transitive_module_deps + } + _merge_depset_dicts(per_module_transitive_objects0, per_module_transitive_objects) return struct( his = hi_set, @@ -484,6 +579,7 @@ def build_haskell_modules(ctx, hs, cc, posix, package_name, with_shared, hidir, os = o_set, dyn_os = dyn_o_set, per_module_transitive_interfaces = per_module_transitive_interfaces0, + per_module_transitive_objects = per_module_transitive_objects0, ) def haskell_module_impl(ctx): diff --git a/haskell/private/actions/link.bzl b/haskell/private/actions/link.bzl index 426fdf13c..6265de120 100644 --- a/haskell/private/actions/link.bzl +++ b/haskell/private/actions/link.bzl @@ -1,6 +1,7 @@ """Actions for linking object code produced by compilation""" load("@bazel_skylib//lib:dicts.bzl", "dicts") +load("@bazel_skylib//lib:paths.bzl", "paths") load(":private/packages.bzl", "expose_packages", "pkg_info_to_compile_flags") load(":private/pkg_id.bzl", "pkg_id") load(":private/cc_libraries.bzl", "create_link_config") @@ -207,14 +208,17 @@ def _so_extension(hs): """ return "dylib" if hs.toolchain.is_darwin else "so" -def link_library_static(hs, cc, posix, dep_info, object_files, my_pkg_id, with_profiling): +def link_library_static(hs, cc, posix, dep_info, object_files, my_pkg_id, with_profiling, libdir = ""): """Link a static library for the package using given object files. Returns: File: Produced static library. """ static_library = hs.actions.declare_file( - "lib{0}.a".format(pkg_id.library_name(hs, my_pkg_id, prof_suffix = with_profiling)), + paths.join( + libdir, + "lib{0}.a".format(pkg_id.library_name(hs, my_pkg_id, prof_suffix = with_profiling)), + ), ) inputs = depset(cc.files, transitive = [object_files]) args = hs.actions.args() @@ -247,7 +251,7 @@ def link_library_static(hs, cc, posix, dep_info, object_files, my_pkg_id, with_p return static_library -def link_library_dynamic(hs, cc, posix, dep_info, extra_srcs, object_files, my_pkg_id, compiler_flags): +def link_library_dynamic(hs, cc, posix, dep_info, extra_srcs, object_files, my_pkg_id, compiler_flags, empty_lib_prefix = ""): """Link a dynamic library for the package using given object files. Returns: @@ -255,10 +259,13 @@ def link_library_dynamic(hs, cc, posix, dep_info, extra_srcs, object_files, my_p """ dynamic_library = hs.actions.declare_file( - "lib{0}-ghc{1}.{2}".format( - pkg_id.library_name(hs, my_pkg_id), - hs.toolchain.version, - _so_extension(hs), + paths.join( + empty_lib_prefix, + "lib{0}-ghc{1}.{2}".format( + pkg_id.library_name(hs, my_pkg_id), + hs.toolchain.version, + _so_extension(hs), + ), ), ) @@ -267,15 +274,16 @@ def link_library_dynamic(hs, cc, posix, dep_info, extra_srcs, object_files, my_p args.add_all(["-shared", "-dynamic"]) args.add_all(hs.toolchain.ghcopts) args.add_all(compiler_flags) + extra_prefix = empty_lib_prefix (pkg_info_inputs, pkg_info_args) = pkg_info_to_compile_flags( hs, pkg_info = expose_packages( - package_ids = hs.package_ids, + package_ids = [] if empty_lib_prefix else hs.package_ids, package_databases = dep_info.package_databases, version = my_pkg_id.version if my_pkg_id else None, ), - prefix = "link-", + prefix = "link-" + extra_prefix + "-", ) args.add_all(pkg_info_args) @@ -288,6 +296,7 @@ def link_library_dynamic(hs, cc, posix, dep_info, extra_srcs, object_files, my_p pic = True, binary = dynamic_library, args = args, + dirprefix = empty_lib_prefix, ) args.add_all(["-o", dynamic_library.path]) @@ -309,6 +318,7 @@ def link_library_dynamic(hs, cc, posix, dep_info, extra_srcs, object_files, my_p mnemonic = "HaskellLinkDynamicLibrary", arguments = args, env = dicts.add(hs.env, cc.env), + extra_name = "__" + extra_prefix, ) return dynamic_library diff --git a/haskell/private/actions/package.bzl b/haskell/private/actions/package.bzl index bebeaa1a2..fddf30fd2 100644 --- a/haskell/private/actions/package.bzl +++ b/haskell/private/actions/package.bzl @@ -52,7 +52,8 @@ def package( exposed_modules, other_modules, my_pkg_id, - has_hs_library): + has_hs_library, + empty_libs_dir = ""): """Create GHC package using ghc-pkg. Args: @@ -65,13 +66,17 @@ def package( other_modules: List of hidden modules. my_pkg_id: Package id object for this package. has_hs_library: Whether hs-libraries should be non-null. + empty_libs_dir: Directory name where the empty library should be. + If empty, this is assumed to be a package description + for a real library. See Note [Empty Libraries] in haskell_impl.bzl. Returns: (File, File): GHC package conf file, GHC package cache file """ pkg_db_dir = pkg_id.to_string(my_pkg_id) + empty_libs_suffix = "_" + empty_libs_dir if empty_libs_dir else "" conf_file = hs.actions.declare_file( - paths.join(pkg_db_dir, "{0}.conf".format(pkg_db_dir)), + paths.join(pkg_db_dir + empty_libs_suffix, "{0}.conf".format(pkg_db_dir)), ) import_dir = paths.join( @@ -85,6 +90,8 @@ def package( else: extra_dynamic_lib_dirs = extra_lib_dirs + pkgroot_lib_path = paths.join("${pkgroot}", empty_libs_dir) + # Create a file from which ghc-pkg will create the actual package # from. write_package_conf(hs, conf_file, { @@ -95,8 +102,8 @@ def package( "exposed": "True", "hidden-modules": other_modules, "import-dirs": [import_dir], - "library-dirs": ["${pkgroot}"] + extra_lib_dirs, - "dynamic-library-dirs": ["${pkgroot}"] + extra_dynamic_lib_dirs, + "library-dirs": [pkgroot_lib_path] + extra_lib_dirs, + "dynamic-library-dirs": [pkgroot_lib_path] + extra_dynamic_lib_dirs, "hs-libraries": [pkg_id.library_name(hs, my_pkg_id)] if has_hs_library else [], "extra-libraries": extra_libs, "depends": hs.package_ids, diff --git a/haskell/private/cc_libraries.bzl b/haskell/private/cc_libraries.bzl index 3c80587bb..56047882c 100644 --- a/haskell/private/cc_libraries.bzl +++ b/haskell/private/cc_libraries.bzl @@ -164,7 +164,7 @@ def link_libraries(libs, args, path_prefix = "", prefix_optl = False): args.extend([dirfmt % lib.dirname for lib in libs]) args.extend([libfmt % get_lib_name(lib) for lib in libs]) -def create_link_config(hs, posix, cc_libraries_info, libraries_to_link, binary, args, dynamic = None, pic = None): +def create_link_config(hs, posix, cc_libraries_info, libraries_to_link, binary, args, dynamic = None, pic = None, dirprefix = ""): """Configure linker flags and inputs. Configure linker flags for C library dependencies and runtime dynamic @@ -180,6 +180,7 @@ def create_link_config(hs, posix, cc_libraries_info, libraries_to_link, binary, args: Arguments to the linking action. dynamic: Whether to link dynamically, or statically. pic: Whether position independent code is required. + dirprefix: Where to put the config file. Returns: (cache_file, static_libs, dynamic_libs): @@ -205,7 +206,7 @@ def create_link_config(hs, posix, cc_libraries_info, libraries_to_link, binary, ) package_name = target_unique_name(hs, "link-config").replace("_", "-").replace("@", "-") - conf_path = paths.join(package_name, package_name + ".conf") + conf_path = paths.join(dirprefix, package_name, package_name + ".conf") conf_file = hs.actions.declare_file(conf_path) libs = cc_static_libs + cc_dynamic_libs write_package_conf(hs, conf_file, { diff --git a/haskell/private/dependencies.bzl b/haskell/private/dependencies.bzl index 83207a9cc..3c9ac7cd1 100644 --- a/haskell/private/dependencies.bzl +++ b/haskell/private/dependencies.bzl @@ -21,11 +21,21 @@ def gather_dep_info(name, deps): for dep in deps if HaskellInfo in dep ]) + empty_lib_package_databases = depset(transitive = [ + dep[HaskellInfo].empty_lib_package_databases + for dep in deps + if HaskellInfo in dep + ]) hs_libraries = depset(transitive = [ dep[HaskellInfo].hs_libraries for dep in deps if HaskellInfo in dep ]) + empty_hs_libraries = depset(transitive = [ + dep[HaskellInfo].empty_hs_libraries + for dep in deps + if HaskellInfo in dep + ]) interface_dirs = depset(transitive = [ dep[HaskellInfo].interface_dirs for dep in deps @@ -62,8 +72,10 @@ def gather_dep_info(name, deps): acc = HaskellInfo( package_databases = package_databases, + empty_lib_package_databases = empty_lib_package_databases, version_macros = set.empty(), hs_libraries = hs_libraries, + empty_hs_libraries = empty_hs_libraries, interface_dirs = interface_dirs, source_files = source_files, boot_files = boot_files, @@ -81,8 +93,10 @@ def gather_dep_info(name, deps): fail("Target {0} cannot depend on binary".format(name)) acc = HaskellInfo( package_databases = acc.package_databases, + empty_lib_package_databases = acc.empty_lib_package_databases, version_macros = set.mutable_union(acc.version_macros, binfo.version_macros), hs_libraries = depset(transitive = [acc.hs_libraries, binfo.hs_libraries]), + empty_hs_libraries = depset(transitive = [acc.empty_hs_libraries, binfo.empty_hs_libraries]), interface_dirs = acc.interface_dirs, import_dirs = import_dirs, compile_flags = compile_flags, @@ -98,12 +112,14 @@ def gather_dep_info(name, deps): # in the `CcInfo` provider. acc = HaskellInfo( package_databases = acc.package_databases, + empty_lib_package_databases = acc.empty_lib_package_databases, version_macros = acc.version_macros, import_dirs = acc.import_dirs, source_files = acc.source_files, boot_files = acc.boot_files, compile_flags = acc.compile_flags, hs_libraries = acc.hs_libraries, + empty_hs_libraries = acc.empty_hs_libraries, extra_source_files = acc.extra_source_files, interface_dirs = acc.interface_dirs, user_compile_flags = [], diff --git a/haskell/private/ghc_wrapper.sh b/haskell/private/ghc_wrapper.sh index 273d262a7..06b016f11 100755 --- a/haskell/private/ghc_wrapper.sh +++ b/haskell/private/ghc_wrapper.sh @@ -12,7 +12,15 @@ if [ "$2" == "--persistent_worker" ]; then # wrapping GHC. Not ready for production usage. exec "${compile_flags[@]}" --persistent_worker else + # Drop messages that GHC produces on features that we rely upon. + # + # "Loaded" is emitted when using GHC environment files, which we + # use as poor man's response files for GHC. + # + # "Warning: the following files ..." is produced when we tell GHC + # to load object files in the interpreter in the build action + # of haskell_module which doesn't do any linking. while IFS= read -r line; do extra_args+=("$line"); done < "$2" "${compile_flags[@]}" "${extra_args[@]}" 2>&1 \ - | while IFS= read -r line; do [[ $line =~ ^Loaded ]] || echo "$line"; done >&2 + | while IFS= read -r line; do [[ $line =~ ^(Loaded|Warning: the following files would be used as linker inputs, but linking is not being done:) ]] || echo "$line"; done >&2 fi diff --git a/haskell/private/haskell_impl.bzl b/haskell/private/haskell_impl.bzl index 08c2140e0..06c23b7c7 100644 --- a/haskell/private/haskell_impl.bzl +++ b/haskell/private/haskell_impl.bzl @@ -53,6 +53,23 @@ load("@rules_cc//cc:find_cc_toolchain.bzl", "find_cc_toolchain") load("//haskell/experimental:providers.bzl", "HaskellModuleInfo") load("//haskell/experimental/private:module.bzl", "build_haskell_modules", "get_module_path_from_target") +# Note [Empty Libraries] +# +# GHC 8.10.x wants to load the shared libraries corresponding to packages needed +# for running TemplateHaskell splices. It wants to do this even when all the +# necessary object files are passed in the command line. +# +# In order to satisfy GHC, and yet avoid passing the linked library as input, we +# create a ficticious package which points to an empty shared library. The +# ficticious and the real package share the same interface files. +# +# Avoiding to pass the real shared library as input is necessary when building +# individual modules with haskell_module, otherwise building the module would +# need to wait until all of the modules of library dependencies have been built. +# +# See Note [Narrowed Dependencies] for an overview of what this feature is +# needed for. + def _prepare_srcs(srcs): srcs_files = [] import_dir_map = {} @@ -361,10 +378,45 @@ def _haskell_binary_common_impl(ctx, is_test): )), ] +def _create_empty_library(hs, cc, posix, my_pkg_id, with_shared, with_profiling, empty_libs_dir): + """See Note [Empty Libraries]""" + dep_info = gather_dep_info("haskell_module-empty_lib", []) + empty_c = hs.actions.declare_file("empty.c") + hs.actions.write(empty_c, "") + + static_library = link_library_static( + hs, + cc, + posix, + dep_info, + depset([empty_c]), + my_pkg_id, + with_profiling = with_profiling, + libdir = empty_libs_dir, + ) + libs = [static_library] + + if with_shared: + dynamic_library = link_library_dynamic( + hs, + cc, + posix, + dep_info, + depset(), + depset([empty_c]), + my_pkg_id, + [], + empty_libs_dir, + ) + libs = [dynamic_library, static_library] + + return libs + def haskell_library_impl(ctx): hs = haskell_context(ctx) deps = ctx.attr.deps + ctx.attr.exports + ctx.attr.narrowed_deps dep_info = gather_dep_info(ctx.attr.name, ctx.attr.deps + ctx.attr.exports) + narrowed_deps_info = gather_dep_info(ctx.attr.name, ctx.attr.narrowed_deps) all_deps_info = gather_dep_info(ctx.attr.name, deps) all_plugins = ctx.attr.plugins + ctx.attr.non_default_plugins plugin_dep_info = gather_dep_info( @@ -487,6 +539,20 @@ def haskell_library_impl(ctx): non_empty, ) + empty_libs_dir = "empty_libs" + conf_file_empty, cache_file_empty = package( + hs, + cc, + posix, + all_deps_info, + with_shared, + exposed_modules, + other_modules, + my_pkg_id, + non_empty, + empty_libs_dir, + ) + interface_dirs = depset( direct = c.interface_files, transitive = [all_deps_info.interface_dirs, module_outputs.his, module_outputs.dyn_his], @@ -501,9 +567,19 @@ def haskell_library_impl(ctx): generate_version_macros(ctx, package_name, version), ) + empty_libs = _create_empty_library(hs, cc, posix, my_pkg_id, with_shared, with_profiling, empty_libs_dir) + export_infos = gather_dep_info(ctx.attr.name, ctx.attr.exports) hs_info = HaskellInfo( package_databases = depset([cache_file], transitive = [all_deps_info.package_databases, export_infos.package_databases]), + empty_lib_package_databases = depset( + direct = [cache_file_empty], + transitive = [ + dep_info.package_databases, + narrowed_deps_info.empty_lib_package_databases, + export_infos.empty_lib_package_databases, + ], + ), version_macros = version_macros, source_files = c.source_files, boot_files = c.boot_files, @@ -513,11 +589,16 @@ def haskell_library_impl(ctx): direct = [lib for lib in [static_library, dynamic_library] if lib], transitive = [all_deps_info.hs_libraries, export_infos.hs_libraries], ), + empty_hs_libraries = depset( + direct = empty_libs, + transitive = [all_deps_info.empty_hs_libraries, export_infos.empty_hs_libraries], + ), interface_dirs = depset(transitive = [interface_dirs, export_infos.interface_dirs]), compile_flags = c.compile_flags, user_compile_flags = user_compile_flags, user_repl_flags = _expand_make_variables("repl_ghci_args", ctx, ctx.attr.repl_ghci_args), per_module_transitive_interfaces = module_outputs.per_module_transitive_interfaces, + per_module_transitive_objects = module_outputs.per_module_transitive_objects, ) exports = [ @@ -864,12 +945,14 @@ def haskell_import_impl(ctx): hs_info = HaskellInfo( # XXX Empty set of conf and cache files only works for global db. package_databases = depset(), + empty_lib_package_databases = depset(), version_macros = version_macros, source_files = depset(), boot_files = depset(), extra_source_files = depset(), import_dirs = set.empty(), hs_libraries = depset(), + empty_hs_libraries = depset(), interface_dirs = depset(), compile_flags = [], user_compile_flags = [], diff --git a/haskell/providers.bzl b/haskell/providers.bzl index 0a03ee44c..e14d3c606 100644 --- a/haskell/providers.bzl +++ b/haskell/providers.bzl @@ -4,17 +4,20 @@ HaskellInfo = provider( doc = "Common information about build process: dependencies, etc.", fields = { "package_databases": "Depset of package cache files.", + "empty_lib_package_databases": "Depset of package cache files corresponding to empty libraries.", "version_macros": "Depset of version macro files.", "import_dirs": "Import hierarchy roots.", "source_files": "Depset of files that contain Haskell modules.", "boot_files": "Depset of Haskell boot files", "extra_source_files": "Depset of non-Haskell source files.", "hs_libraries": "Depset of compiled Haskell libraries in all available GHC ways.", + "empty_hs_libraries": "Depset of compiled empty Haskell libraries in all available GHC ways.", "interface_dirs": "Depset of interface dirs belonging to the packages.", "compile_flags": "Arguments that were used to compile the code.", "user_compile_flags": "Compiler flags specified by the user, after location expansion.", "user_repl_flags": "REPL flags specified by the user, after location expansion.", - "per_module_transitive_interfaces": "Dict of module labels to interfaces of transitive module dependencies", + "per_module_transitive_interfaces": "Dict of module labels to depsets of interface files of transitive module dependencies", + "per_module_transitive_objects": "Dict of module labels to depsets of object files of transitive module dependencies", }, ) diff --git a/tests/haskell_module/BUILD.bazel b/tests/haskell_module/BUILD.bazel index d56ffdceb..fd2ce01f4 100644 --- a/tests/haskell_module/BUILD.bazel +++ b/tests/haskell_module/BUILD.bazel @@ -8,6 +8,7 @@ filegroup( "//tests/haskell_module/library:all_files", "//tests/haskell_module/library-dep:all_files", "//tests/haskell_module/dep-narrowing:all_files", + "//tests/haskell_module/dep-narrowing-th:all_files", "//tests/haskell_module/multiple:all_files", "//tests/haskell_module/nested:all_files", "//tests/haskell_module/plugin:all_files", diff --git a/tests/haskell_module/dep-narrowing-th/BUILD.bazel b/tests/haskell_module/dep-narrowing-th/BUILD.bazel new file mode 100644 index 000000000..f4095da47 --- /dev/null +++ b/tests/haskell_module/dep-narrowing-th/BUILD.bazel @@ -0,0 +1,96 @@ +load( + "@rules_haskell//haskell:defs.bzl", + "haskell_library", + "haskell_test", +) +load("@rules_haskell//haskell/experimental:defs.bzl", "haskell_module") + +package(default_testonly = 1) + +haskell_library( + name = "TestLib", + srcs = ["TestLibModule.hs"], + deps = [ + "//tests/hackage:base", + ], +) + +haskell_library( + name = "TestLib2", + modules = [ + ":TestLibModule2", + ":SimpleFoo", + ], + deps = [ + "//tests/hackage:base", + ], +) + +haskell_module( + name = "TestLibModule2", + src = "TestLibModule2.hs", + deps = [":SimpleFoo"], +) + +haskell_module( + name = "SimpleFoo", + src = "SimpleFoo.hs", +) + +haskell_module( + name = "TestModule", + src = "TestModule.hs", + cross_library_deps = [":TestLibModule2"], +) + +# Modifying TestLibModule2 from TestLib2 doesn't cause a rebuild of +# TestModule2 thanks to narrowing. +# +# If cross_library_deps and narrowed_deps weren't used, then a change +# in TestLib2 would cause all modules in lib to be rebuilt. +haskell_module( + name = "TestModule2", + src = "TestModule2.hs", + cross_library_deps = [":SimpleFoo"], + enable_th = True, +) + +haskell_library( + name = "lib", + modules = [ + ":TestModule", + ":TestModule2", + ], + narrowed_deps = [ + ":TestLib2", + ], + deps = [ + ":TestLib", + "//tests/hackage:base", + "//tests/hackage:template-haskell", + ], +) + +haskell_module( + name = "TestBinModule", + src = "TestBinModule.hs", + cross_library_deps = [":TestModule"], + module_name = "Main", +) + +haskell_test( + name = "Test", + modules = [":TestBinModule"], + narrowed_deps = [":lib"], + visibility = ["//visibility:public"], + deps = [ + "//tests/hackage:base", + ], +) + +filegroup( + name = "all_files", + testonly = True, + srcs = glob(["**"]), + visibility = ["//visibility:public"], +) diff --git a/tests/haskell_module/dep-narrowing-th/SimpleFoo.hs b/tests/haskell_module/dep-narrowing-th/SimpleFoo.hs new file mode 100644 index 000000000..9f8cf4f15 --- /dev/null +++ b/tests/haskell_module/dep-narrowing-th/SimpleFoo.hs @@ -0,0 +1,4 @@ +module SimpleFoo where + +foo :: Int +foo = 23 diff --git a/tests/haskell_module/dep-narrowing-th/TestBinModule.hs b/tests/haskell_module/dep-narrowing-th/TestBinModule.hs new file mode 100644 index 000000000..67dc442e9 --- /dev/null +++ b/tests/haskell_module/dep-narrowing-th/TestBinModule.hs @@ -0,0 +1,4 @@ +import TestModule + +main :: IO () +main = print bar diff --git a/tests/haskell_module/dep-narrowing-th/TestLibModule.hs b/tests/haskell_module/dep-narrowing-th/TestLibModule.hs new file mode 100644 index 000000000..0ea487190 --- /dev/null +++ b/tests/haskell_module/dep-narrowing-th/TestLibModule.hs @@ -0,0 +1,4 @@ +module TestLibModule where + +foo :: Int +foo = 21 diff --git a/tests/haskell_module/dep-narrowing-th/TestLibModule2.hs b/tests/haskell_module/dep-narrowing-th/TestLibModule2.hs new file mode 100644 index 000000000..7166d47bf --- /dev/null +++ b/tests/haskell_module/dep-narrowing-th/TestLibModule2.hs @@ -0,0 +1,6 @@ +module TestLibModule2 where + +import SimpleFoo + +foo2 :: Int +foo2 = foo - 2 diff --git a/tests/haskell_module/dep-narrowing-th/TestModule.hs b/tests/haskell_module/dep-narrowing-th/TestModule.hs new file mode 100644 index 000000000..be5b4e4a6 --- /dev/null +++ b/tests/haskell_module/dep-narrowing-th/TestModule.hs @@ -0,0 +1,7 @@ +module TestModule where + +import TestLibModule (foo) +import TestLibModule2 (foo2) + +bar :: Int +bar = 2 * foo + foo2 diff --git a/tests/haskell_module/dep-narrowing-th/TestModule2.hs b/tests/haskell_module/dep-narrowing-th/TestModule2.hs new file mode 100644 index 000000000..b86fdc284 --- /dev/null +++ b/tests/haskell_module/dep-narrowing-th/TestModule2.hs @@ -0,0 +1,11 @@ +{-# LANGUAGE TemplateHaskell #-} +module TestModule2 where + +import Control.Exception (evaluate) +import Language.Haskell.TH +import SimpleFoo + +runIO (evaluate foo) >> return [] + +f :: IO () +f = return () diff --git a/tests/haskell_module/th/BUILD.bazel b/tests/haskell_module/th/BUILD.bazel index 1b59f2dd5..57a55fe31 100644 --- a/tests/haskell_module/th/BUILD.bazel +++ b/tests/haskell_module/th/BUILD.bazel @@ -27,6 +27,7 @@ haskell_module( haskell_module( name = "leaf", src = "Leaf.hs", + enable_th = True, deps = [ ":branch_left", ":branch_right",