From 04cd13880e9233dab781aeff8ce9a2d212bd21f1 Mon Sep 17 00:00:00 2001 From: Kyle Butt Date: Thu, 15 May 2025 16:38:18 -0600 Subject: [PATCH 1/4] cabal: Always process package config files Drop the check for `libraries != []` in the check for processing the package config file. Haskell supports empty libraries. In order for Bazel to support them, we need to process the package config, even for empty libraries. --- haskell/private/cabal_wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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))) From 61d098d48f28b55592704e5fbc30e44be1776133 Mon Sep 17 00:00:00 2001 From: Kyle Butt Date: Thu, 15 May 2025 16:43:52 -0600 Subject: [PATCH 2/4] cabal: Add provider and rule for cabal settings Rather than continually proliferate additional settings on haskell_cabal_library, create a rule and provider that can hold cabal specific settings. Add `empty_lib` as the first such setting. When building an empty lib, bazel will look for the package file, instead of for any object files. This will fix several packages that are currently blacklisted. It will also allow the use of packages with sublibraries and an empty main lib. Currently there is no way to use this with a stack snapshot, short of vendoring a package and adding a `haskell_cabal_args` to the vendored build file. A simpler way is planned. --- haskell/cabal.bzl | 88 +++++++++++++++++++++++++++++++------------ haskell/providers.bzl | 7 ++++ 2 files changed, 71 insertions(+), 24 deletions(-) diff --git a/haskell/cabal.bzl b/haskell/cabal.bzl index b0631bd7d..1ce9cc542 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", @@ -436,6 +437,25 @@ 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 + cabal_args = HaskellCabalArgs( + is_empty = is_empty, + ) + 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.""", + ), + }, + 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 +466,10 @@ def _haskell_cabal_library_impl(ctx): override_cc_toolchain = hs.tools_config.maybe_exec_cc_toolchain, ) + is_empty = False + if ctx.attr.cabal_args: + is_empty = ctx.attr.cabal_args[HaskellCabalArgs].is_empty + # 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], @@ -486,7 +510,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 +523,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 +586,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 +612,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 +667,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 +677,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 +786,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", diff --git a/haskell/providers.bzl b/haskell/providers.bzl index d69dfabe3..de0b0e357 100644 --- a/haskell/providers.bzl +++ b/haskell/providers.bzl @@ -34,6 +34,13 @@ 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.", + }, +) + def all_package_ids(lib_info): return lib_info.exports.to_list() From 50e37a971238f655c1e7f97229fe678c4a5a2e21 Mon Sep 17 00:00:00 2001 From: Kyle Butt Date: Thu, 15 May 2025 16:50:20 -0600 Subject: [PATCH 3/4] cabal: add empty_lib to stack snapshot components Empty haskell libraries have been a longstanding problem. Add support for an additional component type `empty_lib`, which is like `lib`, except that an additional `haskell_cabal_args` target will be created for the library, instructing Bazel not to look for any object files. Remove the longstanding package blacklist for empty packages. Replace it with the now possible correct entries for those libraries, indicating that the main library is empty. --- haskell/cabal.bzl | 81 ++++++++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/haskell/cabal.bzl b/haskell/cabal.bzl index 1ce9cc542..9d0e60206 100644 --- a/haskell/cabal.bzl +++ b/haskell/cabal.bzl @@ -126,21 +126,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: @@ -1166,6 +1151,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): @@ -1177,6 +1172,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. @@ -1202,6 +1200,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`. @@ -1997,6 +2006,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) @@ -2075,20 +2088,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 @@ -2119,6 +2118,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( """ @@ -2134,6 +2139,7 @@ haskell_cabal_library( visibility = {visibility}, cabalopts = ["--ghc-option=-w", "--ghc-option=-optF=-w"], verbose = {verbose}, + {cabal_args} unique_name = True, ) """.format( @@ -2147,6 +2153,7 @@ haskell_cabal_library( tools = library_tools, visibility = visibility, verbose = repr(repository_ctx.attr.verbose), + cabal_args = cabal_args ), ) build_file_builder.append( @@ -2162,6 +2169,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( @@ -2174,6 +2185,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( @@ -2185,6 +2197,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), ), ) @@ -2193,6 +2206,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( @@ -2208,6 +2225,7 @@ haskell_cabal_library( tools = {tools}, visibility = {visibility}, cabalopts = ["--ghc-option=-w", "--ghc-option=-optF=-w"], + {cabal_args} verbose = {verbose}, ) """.format( @@ -2222,8 +2240,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) @@ -2273,6 +2293,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(), }, ) @@ -2484,6 +2505,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. @@ -2743,6 +2765,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, From 7302733b168ec602f205d9df59809e651ac4a54c Mon Sep 17 00:00:00 2001 From: Kyle Butt Date: Sat, 17 May 2025 00:10:45 -0600 Subject: [PATCH 4/4] cabal: Add ignore_setup flag to cabal_args It can occur that a module has a source file named "Setup.hs" that isn't a cabal setup module. The current setup finder will find such a module and then setup will break. Add a flag to cabal_args to indicate that any such module should be ignored. --- haskell/cabal.bzl | 31 ++++++++++++++++++++++++------- haskell/providers.bzl | 1 + 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/haskell/cabal.bzl b/haskell/cabal.bzl index 9d0e60206..90c7ecee3 100644 --- a/haskell/cabal.bzl +++ b/haskell/cabal.bzl @@ -103,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( @@ -424,8 +425,10 @@ def _shorten_library_symlink(dynamic_library): 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] @@ -437,6 +440,10 @@ haskell_cabal_args = rule( 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], ) @@ -452,8 +459,10 @@ def _haskell_cabal_library_impl(ctx): ) 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( @@ -482,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, @@ -823,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], @@ -844,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, @@ -984,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", diff --git a/haskell/providers.bzl b/haskell/providers.bzl index de0b0e357..0ae2fd79c 100644 --- a/haskell/providers.bzl +++ b/haskell/providers.bzl @@ -38,6 +38,7 @@ 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.", }, )