"""Multi target Haskell REPL.""" load("@bazel_skylib//lib:paths.bzl", "paths") load("@bazel_skylib//lib:shell.bzl", "shell") load("@io_tweag_rules_haskell//haskell:private/context.bzl", "haskell_context", "render_env") load( "@io_tweag_rules_haskell//haskell:private/path_utils.bzl", "link_libraries", "match_label", "parse_pattern", "target_unique_name", ) load( "@io_tweag_rules_haskell//haskell:providers.bzl", "HaskellInfo", "HaskellLibraryInfo", "empty_HaskellCcInfo", "get_libs_for_ghc_linker", "merge_HaskellCcInfo", ) load("@io_tweag_rules_haskell//haskell:private/set.bzl", "set") HaskellReplLoadInfo = provider( doc = """Haskell REPL target information. Information to a Haskell target to load into the REPL as source. """, fields = { "source_files": "Set of files that contain Haskell modules.", "cc_dependencies": "Direct cc library dependencies. See HaskellCcInfo.", "compiler_flags": "Flags to pass to the Haskell compiler.", "repl_ghci_args": "Arbitrary extra arguments to pass to GHCi. This extends `compiler_flags` and `repl_ghci_args` from the toolchain", }, ) HaskellReplDepInfo = provider( doc = """Haskell REPL dependency information. Information to a Haskell target to load into the REPL as a built package. """, fields = { "package_ids": "Set of workspace unique package identifiers.", "package_databases": "Set of package cache files.", }, ) HaskellReplCollectInfo = provider( doc = """Collect Haskell REPL information. Holds information to generate a REPL that loads some targets as source and some targets as built packages. """, fields = { "load_infos": "Dictionary from labels to HaskellReplLoadInfo.", "dep_infos": "Dictionary from labels to HaskellReplDepInfo.", "prebuilt_dependencies": "Transitive collection of info of wired-in Haskell dependencies.", "transitive_cc_dependencies": "Transitive cc library dependencies. See HaskellCcInfo.", }, ) HaskellReplInfo = provider( doc = """Haskell REPL information. Holds information to generate a REPL that loads a specific set of targets from source or as built packages. """, fields = { "load_info": "Combined HaskellReplLoadInfo.", "dep_info": "Combined HaskellReplDepInfo.", "prebuilt_dependencies": "Transitive collection of info of wired-in Haskell dependencies.", "transitive_cc_dependencies": "Transitive cc library dependencies. See HaskellCcInfo.", }, ) def _merge_HaskellReplLoadInfo(load_infos): source_files = set.empty() cc_dependencies = empty_HaskellCcInfo() compiler_flags = [] repl_ghci_args = [] for load_info in load_infos: set.mutable_union(source_files, load_info.source_files) cc_dependencies = merge_HaskellCcInfo( cc_dependencies, load_info.cc_dependencies, ) compiler_flags += load_info.compiler_flags repl_ghci_args += load_info.repl_ghci_args return HaskellReplLoadInfo( source_files = source_files, cc_dependencies = cc_dependencies, compiler_flags = compiler_flags, repl_ghci_args = repl_ghci_args, ) def _merge_HaskellReplDepInfo(dep_infos): package_ids = set.empty() package_databases = set.empty() for dep_info in dep_infos: set.mutable_union(package_ids, dep_info.package_ids) set.mutable_union(package_databases, dep_info.package_databases) return HaskellReplDepInfo( package_ids = package_ids, package_databases = package_databases, ) def _create_HaskellReplCollectInfo(target, ctx): load_infos = {} dep_infos = {} hs_info = target[HaskellInfo] prebuilt_dependencies = hs_info.prebuilt_dependencies transitive_cc_dependencies = hs_info.transitive_cc_dependencies load_infos[target.label] = HaskellReplLoadInfo( source_files = hs_info.source_files, cc_dependencies = hs_info.cc_dependencies, compiler_flags = getattr(ctx.rule.attr, "compiler_flags", []), repl_ghci_args = getattr(ctx.rule.attr, "repl_ghci_args", []), ) if HaskellLibraryInfo in target: lib_info = target[HaskellLibraryInfo] dep_infos[target.label] = HaskellReplDepInfo( package_ids = set.singleton(lib_info.package_id), package_databases = hs_info.package_databases, ) return HaskellReplCollectInfo( load_infos = load_infos, dep_infos = dep_infos, prebuilt_dependencies = prebuilt_dependencies, transitive_cc_dependencies = transitive_cc_dependencies, ) def _merge_HaskellReplCollectInfo(args): load_infos = {} dep_infos = {} prebuilt_dependencies = set.empty() transitive_cc_dependencies = empty_HaskellCcInfo() for arg in args: load_infos.update(arg.load_infos) dep_infos.update(arg.dep_infos) set.mutable_union( prebuilt_dependencies, arg.prebuilt_dependencies, ) transitive_cc_dependencies = merge_HaskellCcInfo( transitive_cc_dependencies, arg.transitive_cc_dependencies, ) return HaskellReplCollectInfo( load_infos = load_infos, dep_infos = dep_infos, prebuilt_dependencies = prebuilt_dependencies, transitive_cc_dependencies = transitive_cc_dependencies, ) def _load_as_source(from_source, from_binary, lbl): """Whether a package should be loaded by source or as binary.""" for pat in from_binary: if match_label(pat, lbl): return False for pat in from_source: if match_label(pat, lbl): return True return False def _create_HaskellReplInfo(from_source, from_binary, collect_info): """Convert a HaskellReplCollectInfo to a HaskellReplInfo. Args: from_source: List of patterns for packages to load by source. from_binary: List of patterns for packages to load as binary packages. collect_info: HaskellReplCollectInfo provider. Returns: HaskellReplInfo provider. """ # Collect all packages to load by source. load_info = _merge_HaskellReplLoadInfo([ load_info for (lbl, load_info) in collect_info.load_infos.items() if _load_as_source(from_source, from_binary, lbl) ]) # Collect all packages to load as binary packages. dep_info = _merge_HaskellReplDepInfo([ dep_info for (lbl, dep_info) in collect_info.dep_infos.items() if not _load_as_source(from_source, from_binary, lbl) ]) return HaskellReplInfo( load_info = load_info, dep_info = dep_info, prebuilt_dependencies = collect_info.prebuilt_dependencies, transitive_cc_dependencies = collect_info.transitive_cc_dependencies, ) def _create_repl(hs, ctx, repl_info, output): """Build a multi target REPL. Args: hs: Haskell context. ctx: Rule context. repl_info: HaskellReplInfo provider. output: The output for the executable REPL script. Returns: List of providers: DefaultInfo provider for the executable REPL script. """ # The base and directory packages are necessary for the GHCi script we use # (loads source files and brings in scope the corresponding modules). args = ["-package", "base", "-package", "directory"] # Load prebuilt dependencies (-package) for dep in set.to_list(repl_info.prebuilt_dependencies): args.extend(["-package", dep.package]) # Load built dependencies (-package-id, -package-db) for package_id in set.to_list(repl_info.dep_info.package_ids): args.extend(["-package-id", package_id]) for package_cache in set.to_list(repl_info.dep_info.package_databases): args.extend([ "-package-db", paths.join("$RULES_HASKELL_EXEC_ROOT", package_cache.dirname), ]) # Load C library dependencies link_ctx = repl_info.load_info.cc_dependencies.dynamic_linking libs_to_link = link_ctx.dynamic_libraries_for_runtime.to_list() # External C libraries that we need to make available to the REPL. libraries = link_libraries(libs_to_link, args) # Transitive library dependencies to have in runfiles. (library_deps, ld_library_deps, ghc_env) = get_libs_for_ghc_linker( hs, repl_info.transitive_cc_dependencies, path_prefix = "$RULES_HASKELL_EXEC_ROOT", ) library_path = [paths.dirname(lib.path) for lib in library_deps] ld_library_path = [paths.dirname(lib.path) for lib in ld_library_deps] # Load source files # Force loading by source with `:add *...`. # See https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/ghci.html#ghci-cmd-:add add_sources = [ "*" + f.path for f in set.to_list(repl_info.load_info.source_files) ] ghci_repl_script = hs.actions.declare_file( target_unique_name(hs, "ghci-repl-script"), ) hs.actions.expand_template( template = ctx.file._ghci_repl_script, output = ghci_repl_script, substitutions = { "{ADD_SOURCES}": " ".join(add_sources), "{COMMANDS}": "\n".join(ctx.attr.repl_ghci_commands), }, ) args += [ "-ghci-script", paths.join("$RULES_HASKELL_EXEC_ROOT", ghci_repl_script.path), ] # Extra arguments. # `compiler flags` is the default set of arguments for the repl, # augmented by `repl_ghci_args`. # The ordering is important, first compiler flags (from toolchain # and local rule), then from `repl_ghci_args`. This way the more # specific arguments are listed last, and then have more priority in # GHC. # Note that most flags for GHCI do have their negative value, so a # negative flag in `repl_ghci_args` can disable a positive flag set # in `compiler_flags`, such as `-XNoOverloadedStrings` will disable # `-XOverloadedStrings`. quote_args = ( hs.toolchain.compiler_flags + repl_info.load_info.compiler_flags + hs.toolchain.repl_ghci_args + repl_info.load_info.repl_ghci_args + ctx.attr.repl_ghci_args ) hs.actions.expand_template( template = ctx.file._ghci_repl_wrapper, output = output, is_executable = True, substitutions = { "{ENV}": render_env(ghc_env), "{TOOL}": hs.tools.ghci.path, "{ARGS}": " ".join( args + [ shell.quote(a) for a in quote_args ], ), }, ) extra_inputs = [ hs.tools.ghci, ghci_repl_script, ] extra_inputs.extend(set.to_list(repl_info.load_info.source_files)) extra_inputs.extend(set.to_list(repl_info.dep_info.package_databases)) extra_inputs.extend(library_deps) extra_inputs.extend(ld_library_deps) return [DefaultInfo( executable = output, runfiles = ctx.runfiles( files = extra_inputs, collect_data = ctx.attr.collect_data, ), )] def _haskell_repl_aspect_impl(target, ctx): if not HaskellInfo in target: return [] target_info = _create_HaskellReplCollectInfo(target, ctx) deps_infos = [ dep[HaskellReplCollectInfo] for dep in ctx.rule.attr.deps if HaskellReplCollectInfo in dep ] collect_info = _merge_HaskellReplCollectInfo([target_info] + deps_infos) # This aspect currently does not generate an executable REPL script by # itself. This could be extended in future. Note, to that end it's # necessary to construct a Haskell context without `ctx.attr.name`. return [collect_info] haskell_repl_aspect = aspect( implementation = _haskell_repl_aspect_impl, attr_aspects = ["deps"], ) """ Haskell REPL aspect. Used to implement the haskell_repl rule. Does not generate an executable REPL by itself. """ def _haskell_repl_impl(ctx): collect_info = _merge_HaskellReplCollectInfo([ dep[HaskellReplCollectInfo] for dep in ctx.attr.deps if HaskellReplCollectInfo in dep ]) from_source = [parse_pattern(ctx, pat) for pat in ctx.attr.experimental_from_source] from_binary = [parse_pattern(ctx, pat) for pat in ctx.attr.experimental_from_binary] repl_info = _create_HaskellReplInfo(from_source, from_binary, collect_info) hs = haskell_context(ctx) return _create_repl(hs, ctx, repl_info, ctx.outputs.repl) haskell_repl = rule( implementation = _haskell_repl_impl, attrs = { "_ghci_repl_script": attr.label( allow_single_file = True, default = Label("@io_tweag_rules_haskell//haskell:assets/ghci_script"), ), "_ghci_repl_wrapper": attr.label( allow_single_file = True, default = Label("@io_tweag_rules_haskell//haskell:private/ghci_repl_wrapper.sh"), ), "deps": attr.label_list( aspects = [haskell_repl_aspect], doc = "List of Haskell targets to load into the REPL", ), "experimental_from_source": attr.string_list( doc = """White-list of targets to load by source. Wild-card targets such as //... or //:all are allowed. The black-list takes precedence over the white-list. Note, this attribute will change depending on the outcome of https://github.com/bazelbuild/bazel/issues/7763. """, default = ["//..."], ), "experimental_from_binary": attr.string_list( doc = """Black-list of targets to not load by source but as packages. Wild-card targets such as //... or //:all are allowed. The black-list takes precedence over the white-list. Note, this attribute will change depending on the outcome of https://github.com/bazelbuild/bazel/issues/7763. """, default = [], ), "repl_ghci_args": attr.string_list( doc = "Arbitrary extra arguments to pass to GHCi. This extends `compiler_flags` and `repl_ghci_args` from the toolchain", default = [], ), "repl_ghci_commands": attr.string_list( doc = "Arbitrary extra commands to execute in GHCi.", default = [], ), "collect_data": attr.bool( doc = "Whether to collect the data runfiles from the dependencies in srcs, data and deps attributes.", default = True, ), }, executable = True, outputs = { "repl": "%{name}@repl", }, toolchains = ["@io_tweag_rules_haskell//haskell:toolchain"], ) """Build a REPL for multiple targets. Example: ```bzl haskell_repl( name = "repl", deps = [ "//lib:some_lib", "//exe:some_exe", ], experimental_from_source = [ "//lib/...", "//exe/...", "//common/...", ], experimental_from_binary = [ "//lib/vendored/...", ], ) ``` Collects all transitive Haskell dependencies from `deps`. Those that match `experimental_from_binary` or are defined in an external workspace will be loaded as binary packages. Those that match `experimental_from_source` and are defined in the local workspace will be loaded by source. You can call the REPL like this: ``` $ bazel run //:repl ``` """