diff --git a/haskell/cabal.bzl b/haskell/cabal.bzl index b0631bd7d..90c7ecee3 100644 --- a/haskell/cabal.bzl +++ b/haskell/cabal.bzl @@ -37,6 +37,7 @@ load( load( ":providers.bzl", "HaddockInfo", + "HaskellCabalArgs", "HaskellInfo", "HaskellLibraryInfo", "all_dependencies_package_ids", @@ -102,13 +103,14 @@ def _find_cabal(srcs): fail("A .cabal file was not found in the srcs attribute.") return cabal -def _find_setup(hs, cabal, srcs): +def _find_setup(hs, cabal, srcs, ignore_setup=False): """Check that a Setup script exists. If not, create a default one.""" setup = None - for f in srcs: - if f.basename in ["Setup.hs", "Setup.lhs"]: - if not setup or f.dirname < setup.dirname: - setup = f + if not ignore_setup: + for f in srcs: + if f.basename in ["Setup.hs", "Setup.lhs"]: + if not setup or f.dirname < setup.dirname: + setup = f if not setup: setup = hs.actions.declare_file("Setup.hs", sibling = cabal) hs.actions.write( @@ -125,21 +127,6 @@ main = defaultMain _CABAL_TOOLS = ["alex", "c2hs", "cpphs", "doctest", "happy"] -# Some old packages are empty compatibility shims. Empty packages -# cause Cabal to not produce the outputs it normally produces. Instead -# of detecting that, we blacklist the offending packages, on the -# assumption that such packages are old and rare. -# -# TODO: replace this with a more general solution. -_EMPTY_PACKAGES_BLACKLIST = [ - "bytestring-builder", - "fail", - "ghc-byteorder", - "haskell-gi-overloading", - "mtl-compat", - "nats", -] - def _cabal_tool_flag(tool): """Return a --with-PROG=PATH flag if input is a recognized Cabal tool. None otherwise.""" if tool.basename in _CABAL_TOOLS: @@ -436,6 +423,31 @@ def _shorten_library_symlink(dynamic_library): basename = dynamic_library.basename return paths.join(prefix, basename) +def _haskell_cabal_args_impl(ctx): + is_empty = ctx.attr.is_empty + ignore_setup = ctx.attr.ignore_setup + cabal_args = HaskellCabalArgs( + is_empty = is_empty, + ignore_setup = ignore_setup + ) + return [cabal_args] + +haskell_cabal_args = rule( + _haskell_cabal_args_impl, + attrs = { + "is_empty": attr.bool( + default = False, + doc = """True if this (sub) library is empty, with only re-exports, and no source files of its own. + It is necessary to set this, otherwise bazel will complain about missing "*libHS.a" files.""", + ), + "ignore_setup": attr.bool( + default = False, + doc = """True if this package has a "Setup.hs" that is not a cabal "Setup.hs". """, + ), + }, + provides = [HaskellCabalArgs], +) + def _haskell_cabal_library_impl(ctx): hs = haskell_context(ctx) dep_info = gather_dep_info(ctx.attr.name, ctx.attr.deps) @@ -446,6 +458,12 @@ def _haskell_cabal_library_impl(ctx): override_cc_toolchain = hs.tools_config.maybe_exec_cc_toolchain, ) + is_empty = False + ignore_setup = False + if ctx.attr.cabal_args: + is_empty = ctx.attr.cabal_args[HaskellCabalArgs].is_empty + ignore_setup = ctx.attr.cabal_args[HaskellCabalArgs].ignore_setup + # All C and Haskell library dependencies. cc_info = cc_common.merge_cc_infos( cc_infos = [dep[CcInfo] for dep in ctx.attr.deps if CcInfo in dep], @@ -473,7 +491,7 @@ def _haskell_cabal_library_impl(ctx): fail("ERROR: `compiler_flags` attribute was removed. Use `cabalopts` with `--ghc-option` instead.") cabal = _find_cabal(ctx.files.srcs) - setup = _find_setup(hs, cabal, ctx.files.srcs) + setup = _find_setup(hs, cabal, ctx.files.srcs, ignore_setup) package_database = hs.actions.declare_file( "_install/{}.conf.d/package.cache".format(package_id), sibling = cabal, @@ -486,7 +504,7 @@ def _haskell_cabal_library_impl(ctx): "_install/{}_data".format(package_id), sibling = cabal, ) - with_haddock = ctx.attr.haddock and hs.tools_config.supports_haddock + with_haddock = ctx.attr.haddock and hs.tools_config.supports_haddock and not is_empty if with_haddock: haddock_file = hs.actions.declare_file( "_install/{}_haddock/{}.haddock".format(package_id, package_name), @@ -499,30 +517,36 @@ def _haskell_cabal_library_impl(ctx): else: haddock_file = None haddock_html_dir = None - vanilla_library = hs.actions.declare_file( - "_install/lib/libHS{}.a".format(package_id), - sibling = cabal, - ) - if with_profiling: - profiling_library = hs.actions.declare_file( - "_install/lib/libHS{}_p.a".format(package_id), - sibling = cabal, - ) - static_library = profiling_library - else: + if is_empty: + vanilla_library = None + static_library = None profiling_library = None - static_library = vanilla_library - if hs.toolchain.static_runtime: dynamic_library = None else: - dynamic_library = hs.actions.declare_file( - "_install/lib/libHS{}-ghc{}.{}".format( - package_id, - hs.toolchain.version, - _so_extension(hs), - ), + vanilla_library = hs.actions.declare_file( + "_install/lib/libHS{}.a".format(package_id), sibling = cabal, ) + if with_profiling: + profiling_library = hs.actions.declare_file( + "_install/lib/libHS{}_p.a".format(package_id), + sibling = cabal, + ) + static_library = profiling_library + else: + profiling_library = None + static_library = vanilla_library + if hs.toolchain.static_runtime: + dynamic_library = None + else: + dynamic_library = hs.actions.declare_file( + "_install/lib/libHS{}-ghc{}.{}".format( + package_id, + hs.toolchain.version, + _so_extension(hs), + ), + sibling = cabal, + ) (tool_inputs, tool_input_manifests) = ctx.resolve_tools(tools = ctx.attr.tools) c = _prepare_cabal_inputs( hs, @@ -556,11 +580,12 @@ def _haskell_cabal_library_impl(ctx): outputs = [ package_database, interfaces_dir, - vanilla_library, data_dir, ] if with_haddock: outputs.extend([haddock_file, haddock_html_dir]) + if vanilla_library != None: + outputs.append(vanilla_library) if dynamic_library != None: outputs.append(dynamic_library) if with_profiling: @@ -581,8 +606,13 @@ def _haskell_cabal_library_impl(ctx): progress_message = "HaskellCabalLibrary {}".format(hs.label), ) + if not is_empty: + default_info_libs = depset([static_library] + ([dynamic_library] if dynamic_library != None else [])) + else: + default_info_libs = depset([package_database]) + default_info = DefaultInfo( - files = depset([static_library] + ([dynamic_library] if dynamic_library != None else [])), + files = default_info_libs, runfiles = ctx.runfiles( files = [data_dir], collect_default = True, @@ -631,7 +661,7 @@ def _haskell_cabal_library_impl(ctx): ) linker_input = cc_common.create_linker_input( owner = ctx.label, - libraries = depset(direct = [ + libraries = depset(direct = ([] if is_empty else [ cc_common.create_library_to_link( actions = ctx.actions, feature_configuration = feature_configuration, @@ -641,7 +671,7 @@ def _haskell_cabal_library_impl(ctx): static_library = static_library, cc_toolchain = cc_toolchain, ), - ]), + ])), ) compilation_context = cc_common.create_compilation_context() linking_context = cc_common.create_linking_context( @@ -750,6 +780,10 @@ haskell_cabal_library = rule( library symlink underneath `_solib_` will be shortened to avoid exceeding the MACH-O header size limit on MacOS.""", ), + "cabal_args": attr.label( + doc = """A haskell_cabal_args target with cabal specific settings for this package.""", + providers = [[HaskellCabalArgs]], + ), }, toolchains = use_cc_toolchain() + [ "@rules_haskell//haskell:toolchain", @@ -798,6 +832,10 @@ def _haskell_cabal_binary_impl(ctx): override_cc_toolchain = hs.tools_config.maybe_exec_cc_toolchain, ) + ignore_setup = False + if ctx.attr.cabal_args: + ignore_setup = ctx.attr.cabal_args[HaskellCabalArgs].ignore_setup + # All C and Haskell library dependencies. cc_info = cc_common.merge_cc_infos( cc_infos = [dep[CcInfo] for dep in ctx.attr.deps if CcInfo in dep], @@ -819,7 +857,7 @@ def _haskell_cabal_binary_impl(ctx): fail("ERROR: `compiler_flags` attribute was removed. Use `cabalopts` with `--ghc-option` instead.") cabal = _find_cabal(ctx.files.srcs) - setup = _find_setup(hs, cabal, ctx.files.srcs) + setup = _find_setup(hs, cabal, ctx.files.srcs, ignore_setup) package_database = hs.actions.declare_file( "_install/{}.conf.d/package.cache".format(hs.label.name), sibling = cabal, @@ -959,6 +997,10 @@ haskell_cabal_binary = rule( "flags": attr.string_list( doc = "List of Cabal flags, will be passed to `Setup.hs configure --flags=...`.", ), + "cabal_args": attr.label( + doc = """A haskell_cabal_args target with cabal specific settings for this package.""", + providers = [[HaskellCabalArgs]], + ), "_cabal_wrapper": attr.label( executable = True, cfg = "exec", @@ -1126,6 +1168,16 @@ _default_components = { "cpphs": struct(lib = True, exe = ["cpphs"], sublibs = []), "doctest": struct(lib = True, exe = ["doctest"], sublibs = []), "happy": struct(lib = False, exe = ["happy"], sublibs = []), + # Below are compatibility libraries that produce an empty cabal library. +} + +_default_components_args = { + "bytestring-builder:lib:bytestring-builder": "@rules_haskell//tools/cabal_args:empty_library", + "fail:lib:fail": "@rules_haskell//tools/cabal_args:empty_library", + "ghc-byteorder:lib:ghc-byteorder": "@rules_haskell//tools/cabal_args:empty_library", + "haskell-gi-overloading:lib:haskell-gi-overloading": "@rules_haskell//tools/cabal_args:empty_library", + "mtl-compat:lib:mtl-compat": "@rules_haskell//tools/cabal_args:empty_library", + "nats:lib:nats": "@rules_haskell//tools/cabal_args:empty_library", } def _get_components(components, package): @@ -1137,6 +1189,9 @@ def _get_components(components, package): """ return components.get(package, _default_components.get(package, struct(lib = True, exe = [], sublibs = []))) +def _get_components_args(components_args, component): + return components_args.get(component, _default_components_args.get(component, None)) + def _parse_json_field(json, field, ty, errmsg): """Read and type-check a field from a JSON object. @@ -1162,6 +1217,17 @@ def _parse_json_field(json, field, ty, errmsg): ))) return json[field] +def _parse_components_args_key(component): + pieces = component.split(':') + if len(pieces) == 1: + component = '{}:lib:{}'.format(component, component) + elif len(pieces) == 2 or (len(pieces) == 3 and pieces[2] == ''): + if pieces[1] == 'lib' or pieces[1] == 'exe': + component = '{}:{}:{}'.format(pieces[0], pieces[1], pieces[0]) + else: + component = '{}:lib:{}'.format(pieces[0], pieces[1]) + return component + def _parse_package_spec(package_spec, enable_custom_toolchain_libraries, custom_toolchain_libraries): """Parse a package description from `stack ls dependencies json`. @@ -1957,6 +2023,10 @@ def _stack_snapshot_impl(repository_ctx): for (name, components) in repository_ctx.attr.components.items() } all_components = {} + user_components_args = { + _parse_components_args_key(component): args + for (component, args) in repository_ctx.attr.components_args.items() + } for (name, spec) in resolved.items(): all_components[name] = _get_components(user_components, name) user_components.pop(name, None) @@ -2035,20 +2105,6 @@ alias(name = "{name}", actual = "{actual}", visibility = {visibility}) haskell_toolchain_library(name = "{name}", visibility = {visibility}) """.format(name = name, visibility = visibility), ) - elif name in _EMPTY_PACKAGES_BLACKLIST: - build_file_builder.append( - """ -haskell_library( - name = "{name}", - version = "{version}", - visibility = {visibility}, -) -""".format( - name = name, - version = version, - visibility = visibility, - ), - ) else: library_deps = [ dep @@ -2079,6 +2135,12 @@ haskell_library( )).relative(label)) for label in repository_ctx.attr.setup_deps.get(name, []) ] + + lib_args = _get_components_args(user_components_args, '{}:lib:{}'.format(name, name)) + cabal_args = "" + if lib_args != None: + cabal_args = "cabal_args = \"{}\",".format(lib_args) + if all_components[name].lib: build_file_builder.append( """ @@ -2094,6 +2156,7 @@ haskell_cabal_library( visibility = {visibility}, cabalopts = ["--ghc-option=-w", "--ghc-option=-optF=-w"], verbose = {verbose}, + {cabal_args} unique_name = True, ) """.format( @@ -2107,6 +2170,7 @@ haskell_cabal_library( tools = library_tools, visibility = visibility, verbose = repr(repository_ctx.attr.verbose), + cabal_args = cabal_args ), ) build_file_builder.append( @@ -2122,6 +2186,10 @@ haskell_cabal_library( for comp in ["exe:{}".format(exe)] + (["exe"] if exe == name else []) for comp_dep in package_components_dependencies.get(comp, []) ] + exe_args = _get_components_args(user_components_args, '{}:exe:{}'.format(name, exe)) + cabal_args = "" + if exe_args != None: + cabal_args = "cabal_args = \"{}\",".format(lib_args) build_file_builder.append( """ haskell_cabal_binary( @@ -2134,6 +2202,7 @@ haskell_cabal_binary( tools = {tools}, visibility = ["@{workspace}-exe//{name}:__pkg__"], cabalopts = ["--ghc-option=-w", "--ghc-option=-optF=-w", "--ghc-option=-static"], + {cabal_args} verbose = {verbose}, ) """.format( @@ -2145,6 +2214,7 @@ haskell_cabal_binary( deps = library_deps + exe_component_deps + ([name] if all_components[name].lib else []), setup_deps = setup_deps, tools = library_tools, + cabal_args = cabal_args, verbose = repr(repository_ctx.attr.verbose), ), ) @@ -2153,6 +2223,10 @@ haskell_cabal_binary( _resolve_component_target_name(name, c) for c in package_components_dependencies.get("lib:{}".format(sublib), []) ] + lib_args = _get_components_args(user_components_args, '{}:lib:{}'.format(name, sublib)) + cabal_args = "" + if lib_args != None: + cabal_args = "cabal_args = \"{}\",".format(lib_args) build_file_builder.append( """ haskell_cabal_library( @@ -2168,6 +2242,7 @@ haskell_cabal_library( tools = {tools}, visibility = {visibility}, cabalopts = ["--ghc-option=-w", "--ghc-option=-optF=-w"], + {cabal_args} verbose = {verbose}, ) """.format( @@ -2182,8 +2257,10 @@ haskell_cabal_library( tools = library_tools, verbose = repr(repository_ctx.attr.verbose), visibility = visibility, + cabal_args = cabal_args, ), ) + build_file_content = "\n".join(build_file_builder) repository_ctx.file("BUILD.bazel", build_file_content, executable = False) @@ -2233,6 +2310,7 @@ _stack_snapshot = repository_rule( "verbose": attr.bool(default = False), "custom_toolchain_libraries": attr.string_list(default = []), "enable_custom_toolchain_libraries": attr.bool(default = False), + "components_args": attr.string_dict(), }, ) @@ -2444,6 +2522,7 @@ def stack_snapshot( netrc = "", toolchain_libraries = None, setup_stack = True, + components_args = {}, label_builder = lambda l: Label(l), **kwargs): """Use Stack to download and extract Cabal source distributions. @@ -2703,6 +2782,7 @@ def stack_snapshot( tools = tools, components = components, components_dependencies = components_dependencies, + components_args = components_args, verbose = verbose, custom_toolchain_libraries = toolchain_libraries, enable_custom_toolchain_libraries = toolchain_libraries != None, diff --git a/haskell/private/cabal_wrapper.py b/haskell/private/cabal_wrapper.py index 82993fe48..8bc35717d 100755 --- a/haskell/private/cabal_wrapper.py +++ b/haskell/private/cabal_wrapper.py @@ -394,7 +394,7 @@ def make_relative_to_pkgroot(matchobj): line = re.sub(re.escape(cfg_execroot) + r'\S*', make_relative_to_pkgroot, line) return line -if libraries != [] and os.path.isfile(package_conf_file): +if os.path.isfile(package_conf_file): for lib in libraries: os.rename(lib, os.path.join(dynlibdir, os.path.basename(lib))) diff --git a/haskell/providers.bzl b/haskell/providers.bzl index d69dfabe3..0ae2fd79c 100644 --- a/haskell/providers.bzl +++ b/haskell/providers.bzl @@ -34,6 +34,14 @@ HaskellLibraryInfo = provider( }, ) +HaskellCabalArgs = provider( + doc = "Settings for a haskell_cabal_library", + fields = { + "is_empty": "True if this (sub) library is empty, with only re-exports, and no source files of its own.", + "ignore_setup": "True if this package contains a \"Setup.hs\" that isn't a cabal Setup module.", + }, +) + def all_package_ids(lib_info): return lib_info.exports.to_list()