diff --git a/extensions/stack_snapshot.bzl b/extensions/stack_snapshot.bzl index 3e1221949..53b13c581 100644 --- a/extensions/stack_snapshot.bzl +++ b/extensions/stack_snapshot.bzl @@ -129,6 +129,17 @@ _package_tag = tag_class( default = False, doc = "Configuring a package makes it visible by default, unless `hidden` is set to True", ), + "components_args": attr.string_dict( + doc = """ components_args: Defines map from component to cabal settings for that package. + The cabal settings are a target of type `haskell_cabal_args`. Currently handles empty + libraries and telling the build system to ignore `Setup.hs`. + Use `lib` for the main library component, `exe:` for an executable component, + and `lib:` for a sublibrary. + `exe` is a short-cut for `exe:`. + Either use the builtin "@rules_haskell//tools/cabal_args:empty_library", or create your own + if necessary. + """, + ), }, ) @@ -180,6 +191,8 @@ def _add_packages(conf, module, root_or_rules_haskell): if package_tag.components != ["DETECT_DEFAULT"]: # Some packages have default components set conf.components[package_name] = package_tag.components + if package_tag.components_args: + conf.components[package_name] = package_tag.components_args if package_tag.components_dependencies: conf.components_dependencies[package_name] = json.encode(package_tag.components_dependencies) if package_tag.extra_deps: @@ -295,6 +308,7 @@ def _stack_snapshot_impl(mctx): extra_deps = {}, components = {}, components_dependencies = {}, + components_args = {}, vendored_packages = {}, ) @@ -318,6 +332,7 @@ def _stack_snapshot_impl(mctx): kwargs["flags"] = packages_conf.flags kwargs["components"] = packages_conf.components kwargs["components_dependencies"] = packages_conf.components_dependencies + kwargs["components_args"] = packages_conf.components_args kwargs["extra_deps"] = packages_conf.extra_deps kwargs["vendored_packages"] = packages_conf.vendored_packages kwargs["name"] = "stackage" diff --git a/haskell/cabal.bzl b/haskell/cabal.bzl index d9f92908d..7286c12f0 100644 --- a/haskell/cabal.bzl +++ b/haskell/cabal.bzl @@ -37,6 +37,7 @@ load( load( ":providers.bzl", "HaddockInfo", + "HaskellCabalArgsInfo", "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: @@ -437,6 +424,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 = HaskellCabalArgsInfo( + 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 = [HaskellCabalArgsInfo], +) + def _haskell_cabal_library_impl(ctx): hs = haskell_context(ctx) dep_info = gather_dep_info(ctx.attr.name, ctx.attr.deps) @@ -447,6 +459,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[HaskellCabalArgsInfo].is_empty + ignore_setup = ctx.attr.cabal_args[HaskellCabalArgsInfo].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], @@ -474,7 +492,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, @@ -487,7 +505,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), @@ -500,30 +518,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, @@ -557,11 +581,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: @@ -582,8 +607,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, @@ -632,7 +662,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, @@ -642,7 +672,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( @@ -751,6 +781,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 = [[HaskellCabalArgsInfo]], + ), }, toolchains = use_cc_toolchain() + [ "@rules_haskell//haskell:toolchain", @@ -799,6 +833,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[HaskellCabalArgsInfo].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], @@ -820,7 +858,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, @@ -960,6 +998,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 = [[HaskellCabalArgsInfo]], + ), "_cabal_wrapper": attr.label( executable = True, cfg = "exec", @@ -1124,6 +1166,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): @@ -1135,6 +1187,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. @@ -1160,6 +1215,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`. @@ -1955,6 +2021,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) @@ -2039,20 +2109,6 @@ alias(name = "_{name}_exe_{exe}", actual = "{actual}_exe_{exe}", visibility = {v 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 @@ -2083,6 +2139,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( """ @@ -2098,6 +2160,7 @@ haskell_cabal_library( visibility = {visibility}, cabalopts = ["--ghc-option=-w", "--ghc-option=-optF=-w"], verbose = {verbose}, + {cabal_args} unique_name = True, ) """.format( @@ -2111,6 +2174,7 @@ haskell_cabal_library( tools = library_tools, visibility = visibility, verbose = repr(repository_ctx.attr.verbose), + cabal_args = cabal_args, ), ) build_file_builder.append( @@ -2126,6 +2190,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( @@ -2138,6 +2206,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( @@ -2149,6 +2218,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), ), ) @@ -2157,6 +2227,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( @@ -2172,6 +2246,7 @@ haskell_cabal_library( tools = {tools}, visibility = {visibility}, cabalopts = ["--ghc-option=-w", "--ghc-option=-optF=-w"], + {cabal_args} verbose = {verbose}, ) """.format( @@ -2186,8 +2261,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) @@ -2237,6 +2314,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(), }, ) @@ -2449,6 +2527,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. @@ -2708,6 +2787,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 f17803c8f..404db0781 100644 --- a/haskell/providers.bzl +++ b/haskell/providers.bzl @@ -34,6 +34,14 @@ HaskellLibraryInfo = provider( }, ) +HaskellCabalArgsInfo = 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() diff --git a/tools/cabal_args/BUILD.bazel b/tools/cabal_args/BUILD.bazel new file mode 100644 index 000000000..bce5b5b7c --- /dev/null +++ b/tools/cabal_args/BUILD.bazel @@ -0,0 +1,13 @@ +load("@rules_haskell//haskell:cabal.bzl", "haskell_cabal_args") + +haskell_cabal_args( + name = "empty_library", + is_empty = True, + visibility = ["//visibility:public"], +) + +haskell_cabal_args( + name = "ignore_setup", + ignore_setup = True, + visibility = ["//visibility:public"], +)