From 1193ab7fe2029ac46eba5a6e2e42a6068b9d8f29 Mon Sep 17 00:00:00 2001 From: Andreas Herrmann Date: Thu, 7 Mar 2019 11:11:15 +0100 Subject: [PATCH 01/16] More flexible get_libs_for_ghc_linker Now accepts transitive_cc_dependencies instead of build_info, so that it can be used for the multi target REPL as well. --- haskell/doctest.bzl | 5 ++++- haskell/lint.bzl | 5 ++++- haskell/private/actions/compile.bzl | 5 ++++- haskell/private/actions/repl.bzl | 2 +- haskell/private/actions/runghc.bzl | 2 +- haskell/private/providers.bzl | 6 +++--- 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/haskell/doctest.bzl b/haskell/doctest.bzl index e8c426136..068a7714e 100644 --- a/haskell/doctest.bzl +++ b/haskell/doctest.bzl @@ -129,7 +129,10 @@ def _haskell_doctest_single(target, ctx): ]) # Transitive library dependencies for runtime. - (library_deps, ld_library_deps, ghc_env) = get_libs_for_ghc_linker(hs, build_info) + (library_deps, ld_library_deps, ghc_env) = get_libs_for_ghc_linker( + hs, + build_info.transitive_cc_dependencies, + ) header_files = lib_info.header_files if lib_info != None else bin_info.header_files diff --git a/haskell/lint.bzl b/haskell/lint.bzl index 1526e1c61..27e5d5d86 100644 --- a/haskell/lint.bzl +++ b/haskell/lint.bzl @@ -74,7 +74,10 @@ def _haskell_lint_aspect_impl(target, ctx): ) # Transitive library dependencies for runtime. - (library_deps, ld_library_deps, _ghc_env) = get_libs_for_ghc_linker(hs, build_info) + (library_deps, ld_library_deps, _ghc_env) = get_libs_for_ghc_linker( + hs, + build_info.transitive_cc_dependencies, + ) ctx.actions.run_shell( inputs = depset(transitive = [ diff --git a/haskell/private/actions/compile.bzl b/haskell/private/actions/compile.bzl index 098c2862f..0aee8d07a 100644 --- a/haskell/private/actions/compile.bzl +++ b/haskell/private/actions/compile.bzl @@ -245,7 +245,10 @@ def _compilation_defaults(hs, cc, java, dep_info, srcs, import_dir_map, extra_sr args.add(f) # Transitive library dependencies for runtime. - (library_deps, ld_library_deps, ghc_env) = get_libs_for_ghc_linker(hs, dep_info) + (library_deps, ld_library_deps, ghc_env) = get_libs_for_ghc_linker( + hs, + dep_info.transitive_cc_dependencies, + ) return DefaultCompileInfo( args = args, diff --git a/haskell/private/actions/repl.bzl b/haskell/private/actions/repl.bzl index cdaf9097c..8bf98e2c2 100644 --- a/haskell/private/actions/repl.bzl +++ b/haskell/private/actions/repl.bzl @@ -84,7 +84,7 @@ def build_haskell_repl( # Transitive library dependencies to have in runfiles. (library_deps, ld_library_deps, ghc_env) = get_libs_for_ghc_linker( hs, - build_info, + build_info.transitive_cc_dependencies, path_prefix = "$RULES_HASKELL_EXEC_ROOT", ) library_path = [paths.dirname(lib.path) for lib in library_deps] diff --git a/haskell/private/actions/runghc.bzl b/haskell/private/actions/runghc.bzl index ac37ee548..fc459df97 100644 --- a/haskell/private/actions/runghc.bzl +++ b/haskell/private/actions/runghc.bzl @@ -73,7 +73,7 @@ def build_haskell_runghc( # Transitive library dependencies to have in runfiles. (library_deps, ld_library_deps, ghc_env) = get_libs_for_ghc_linker( hs, - build_info, + build_info.transitive_cc_dependencies, path_prefix = "$RULES_HASKELL_EXEC_ROOT", ) diff --git a/haskell/private/providers.bzl b/haskell/private/providers.bzl index 7941bb4d2..c26ad952b 100644 --- a/haskell/private/providers.bzl +++ b/haskell/private/providers.bzl @@ -107,7 +107,7 @@ HaskellBuildInfo = provider( }, ) -def get_libs_for_ghc_linker(hs, build_info, path_prefix = None): +def get_libs_for_ghc_linker(hs, transitive_cc_dependencies, path_prefix = None): """Return all C library dependencies for GHC's linker. GHC has it's own builtin linker. It is used for Template Haskell, for GHCi, @@ -120,7 +120,7 @@ def get_libs_for_ghc_linker(hs, build_info, path_prefix = None): Args: hs: Haskell context. - build_info: HaskellBinaryInfo provider. + transitive_cc_dependencies: HaskellCcInfo provider. path_prefix: Prefix for paths in GHC environment variables. Returns: @@ -131,7 +131,7 @@ def get_libs_for_ghc_linker(hs, build_info, path_prefix = None): env: A mapping environment variables LIBRARY_PATH and LD_LIBRARY_PATH, to the corresponding values as expected by GHC. """ - trans_link_ctx = build_info.transitive_cc_dependencies.dynamic_linking + trans_link_ctx = transitive_cc_dependencies.dynamic_linking libs_to_link = trans_link_ctx.libraries_to_link.to_list() libs_for_runtime = trans_link_ctx.dynamic_libraries_for_runtime.to_list() From 60f6691b9cc9aeebb06bc7611b99728a2651f4e7 Mon Sep 17 00:00:00 2001 From: Andreas Herrmann Date: Thu, 7 Mar 2019 13:42:47 +0100 Subject: [PATCH 02/16] ghci: remove SCRIPT_LOCATION This variable was unused. --- haskell/private/actions/repl.bzl | 1 - haskell/private/actions/runghc.bzl | 1 - haskell/private/ghci_repl_wrapper.sh | 1 - 3 files changed, 3 deletions(-) diff --git a/haskell/private/actions/repl.bzl b/haskell/private/actions/repl.bzl index 8bf98e2c2..0c72b6dc8 100644 --- a/haskell/private/actions/repl.bzl +++ b/haskell/private/actions/repl.bzl @@ -135,7 +135,6 @@ def build_haskell_repl( "{LIBPATH}": ghc_env["LIBRARY_PATH"], "{LDLIBPATH}": ghc_env["LD_LIBRARY_PATH"], "{TOOL}": hs.tools.ghci.path, - "{SCRIPT_LOCATION}": output.path, "{ARGS}": " ".join([shell.quote(a) for a in args]), }, is_executable = True, diff --git a/haskell/private/actions/runghc.bzl b/haskell/private/actions/runghc.bzl index fc459df97..903460180 100644 --- a/haskell/private/actions/runghc.bzl +++ b/haskell/private/actions/runghc.bzl @@ -105,7 +105,6 @@ def build_haskell_runghc( "{LIBPATH}": ghc_env["LIBRARY_PATH"], "{LDLIBPATH}": ghc_env["LD_LIBRARY_PATH"], "{TOOL}": hs.tools.runghc.path, - "{SCRIPT_LOCATION}": output.path, "{ARGS}": " ".join([shell.quote(a) for a in runghc_args]), }, is_executable = True, diff --git a/haskell/private/ghci_repl_wrapper.sh b/haskell/private/ghci_repl_wrapper.sh index 86f32caaa..a63267b05 100644 --- a/haskell/private/ghci_repl_wrapper.sh +++ b/haskell/private/ghci_repl_wrapper.sh @@ -34,7 +34,6 @@ fi RULES_HASKELL_EXEC_ROOT=$(dirname $(readlink ${BUILD_WORKSPACE_DIRECTORY}/bazel-out)) TOOL_LOCATION="$RULES_HASKELL_EXEC_ROOT/{TOOL}" -SCRIPT_LOCATION="$RULES_HASKELL_EXEC_ROOT/{SCRIPT_LOCATION}" export LIBRARY_PATH={LIBPATH} export LD_LIBRARY_PATH={LDLIBPATH} From 4ee0c4c43b9fd400107fb808a275ae8172cc8bdf Mon Sep 17 00:00:00 2001 From: Andreas Herrmann Date: Thu, 7 Mar 2019 13:47:30 +0100 Subject: [PATCH 03/16] Prefix -ghci-script by exec root Before the GHCi wrapper located the ghci script relative to the working directory, which was expected to be the workspace directory. This worked fine for executing the `@repl` files using `bazel run` as these were executed from the repository root and not from a dedicated exec root. In case of a dedicated repl target this will no longer work, as the target will be executed in its exec root. This change modifies the wrapper to first change into the workspace directory, and to refer to the ghci script relative to the exec root. The same is done for library dependencies. --- haskell/private/actions/repl.bzl | 12 +++++++++--- haskell/private/ghci_repl_wrapper.sh | 6 ++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/haskell/private/actions/repl.bzl b/haskell/private/actions/repl.bzl index 0c72b6dc8..569a10383 100644 --- a/haskell/private/actions/repl.bzl +++ b/haskell/private/actions/repl.bzl @@ -113,8 +113,6 @@ def build_haskell_repl( source_files = lib_info.source_files if lib_info != None else bin_info.source_files - args += ["-ghci-script", ghci_repl_script.path] - # Extra arguments. # `compiler flags` is the default set of arguments for the repl, # augmented by `repl_ghci_args`. @@ -135,7 +133,15 @@ def build_haskell_repl( "{LIBPATH}": ghc_env["LIBRARY_PATH"], "{LDLIBPATH}": ghc_env["LD_LIBRARY_PATH"], "{TOOL}": hs.tools.ghci.path, - "{ARGS}": " ".join([shell.quote(a) for a in args]), + "{ARGS}": " ".join( + [ + "-ghci-script", + paths.join("$RULES_HASKELL_EXEC_ROOT", ghci_repl_script.path), + ] + [ + shell.quote(a) + for a in args + ], + ), }, is_executable = True, ) diff --git a/haskell/private/ghci_repl_wrapper.sh b/haskell/private/ghci_repl_wrapper.sh index a63267b05..a7e7d545d 100644 --- a/haskell/private/ghci_repl_wrapper.sh +++ b/haskell/private/ghci_repl_wrapper.sh @@ -20,6 +20,12 @@ EOF exit 1 fi +# GHCi script and libraries are loaded relative to workspace directory. +# bazel run //some:target@repl will be executed from the workspace directory. +# bazel run //some:haskell_repl will be executed from its execroot. +# Explicitly change into the workspace root in that case. +cd "$BUILD_WORKSPACE_DIRECTORY" + # This is a workaround for https://github.com/bazelbuild/bazel/issues/5506 # and also for the fact that REPL script relies on so-called “convenience # links” and the names of those links are controlled by the --symlink_prefix From 074261870e6c01e17aef73faa8d05a27647fb73f Mon Sep 17 00:00:00 2001 From: Andreas Herrmann Date: Fri, 8 Mar 2019 15:34:03 +0100 Subject: [PATCH 04/16] Factor out environment variable generation --- haskell/doctest.bzl | 7 ++----- haskell/haddock.bzl | 7 ++----- haskell/lint.bzl | 7 ++----- haskell/private/context.bzl | 15 +++++++++++++++ 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/haskell/doctest.bzl b/haskell/doctest.bzl index 068a7714e..9d43431c8 100644 --- a/haskell/doctest.bzl +++ b/haskell/doctest.bzl @@ -2,7 +2,7 @@ load("@bazel_skylib//lib:dicts.bzl", "dicts") load("@bazel_skylib//lib:paths.bzl", "paths") -load(":private/context.bzl", "haskell_context") +load(":private/context.bzl", "haskell_context", "render_env") load( ":private/path_utils.bzl", "get_lib_name", @@ -176,10 +176,7 @@ def _haskell_doctest_single(target, ctx): module_list = exposed_modules_file.path, # XXX Workaround # https://github.com/bazelbuild/bazel/issues/5980. - env = "\n".join([ - "export {}={}".format(k, v) - for k, v in hs.env.items() - ]), + env = render_env(hs.env), ), arguments = [args], # NOTE It looks like we must specify the paths here as well as via -L diff --git a/haskell/haddock.bzl b/haskell/haddock.bzl index b4fc4f908..e852959ce 100644 --- a/haskell/haddock.bzl +++ b/haskell/haddock.bzl @@ -7,7 +7,7 @@ load( "HaskellBuildInfo", "HaskellLibraryInfo", ) -load(":private/context.bzl", "haskell_context") +load(":private/context.bzl", "haskell_context", "render_env") load(":private/set.bzl", "set") def _get_haddock_path(package_id): @@ -102,10 +102,7 @@ def _haskell_doc_aspect_impl(target, ctx): "%{haddock}": hs.tools.haddock.path, # XXX Workaround # https://github.com/bazelbuild/bazel/issues/5980. - "%{env}": "\n".join([ - "export {}={}".format(k, v) - for k, v in hs.env.items() - ]), + "%{env}": render_env(hs.env), }, is_executable = True, ) diff --git a/haskell/lint.bzl b/haskell/lint.bzl index 27e5d5d86..c773ef905 100644 --- a/haskell/lint.bzl +++ b/haskell/lint.bzl @@ -7,7 +7,7 @@ load( "HaskellLibraryInfo", "HaskellLintInfo", ) -load(":private/context.bzl", "haskell_context") +load(":private/context.bzl", "haskell_context", "render_env") load(":private/packages.bzl", "expose_packages", "pkg_info_to_ghc_args") load( ":private/path_utils.bzl", @@ -101,10 +101,7 @@ def _haskell_lint_aspect_impl(target, ctx): output = lint_log.path, # XXX Workaround # https://github.com/bazelbuild/bazel/issues/5980. - env = "\n".join([ - "export {}={}".format(k, v) - for k, v in hs.env.items() - ]), + env = render_env(hs.env), ), arguments = [args], use_default_shell_env = True, diff --git a/haskell/private/context.bzl b/haskell/private/context.bzl index f2fa261e8..3cd3ff92c 100644 --- a/haskell/private/context.bzl +++ b/haskell/private/context.bzl @@ -47,3 +47,18 @@ def haskell_context(ctx, attr = None): genfiles_dir = ctx.genfiles_dir, coverage_enabled = coverage_enabled, ) + +def render_env(env): + """Render environment dict to shell exports. + + Example: + + >>> render_env({"PATH": "foo:bar", "LANG": "lang"}) + export PATH=foo:bar + export LANG=lang + + """ + return "\n".join([ + "export {}={}".format(k, v) + for k, v in env.items() + ]) From 69e9172a9a1db4e6fbed0619cf4fb7a26ed846c9 Mon Sep 17 00:00:00 2001 From: Andreas Herrmann Date: Fri, 8 Mar 2019 16:18:18 +0100 Subject: [PATCH 05/16] Use render_env for ghci_repl_wrapper.sh This allows users of the ghci wrapper to pass in additional environment variables easily. --- haskell/private/actions/repl.bzl | 4 ++-- haskell/private/actions/runghc.bzl | 4 ++-- haskell/private/ghci_repl_wrapper.sh | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/haskell/private/actions/repl.bzl b/haskell/private/actions/repl.bzl index 569a10383..bdb478eb1 100644 --- a/haskell/private/actions/repl.bzl +++ b/haskell/private/actions/repl.bzl @@ -1,5 +1,6 @@ """GHCi REPL support""" +load(":private/context.bzl", "render_env") load(":private/packages.bzl", "expose_packages", "pkg_info_to_ghc_args") load( ":private/path_utils.bzl", @@ -130,8 +131,7 @@ def build_haskell_repl( template = ghci_repl_wrapper, output = repl_file, substitutions = { - "{LIBPATH}": ghc_env["LIBRARY_PATH"], - "{LDLIBPATH}": ghc_env["LD_LIBRARY_PATH"], + "{ENV}": render_env(ghc_env), "{TOOL}": hs.tools.ghci.path, "{ARGS}": " ".join( [ diff --git a/haskell/private/actions/runghc.bzl b/haskell/private/actions/runghc.bzl index 903460180..6be11a087 100644 --- a/haskell/private/actions/runghc.bzl +++ b/haskell/private/actions/runghc.bzl @@ -1,5 +1,6 @@ """runghc support""" +load(":private/context.bzl", "render_env") load(":private/packages.bzl", "expose_packages", "pkg_info_to_ghc_args") load( ":private/path_utils.bzl", @@ -102,8 +103,7 @@ def build_haskell_runghc( template = runghc_wrapper, output = runghc_file, substitutions = { - "{LIBPATH}": ghc_env["LIBRARY_PATH"], - "{LDLIBPATH}": ghc_env["LD_LIBRARY_PATH"], + "{ENV}": render_env(ghc_env), "{TOOL}": hs.tools.runghc.path, "{ARGS}": " ".join([shell.quote(a) for a in runghc_args]), }, diff --git a/haskell/private/ghci_repl_wrapper.sh b/haskell/private/ghci_repl_wrapper.sh index a7e7d545d..dd5c4bdd4 100644 --- a/haskell/private/ghci_repl_wrapper.sh +++ b/haskell/private/ghci_repl_wrapper.sh @@ -41,6 +41,5 @@ cd "$BUILD_WORKSPACE_DIRECTORY" RULES_HASKELL_EXEC_ROOT=$(dirname $(readlink ${BUILD_WORKSPACE_DIRECTORY}/bazel-out)) TOOL_LOCATION="$RULES_HASKELL_EXEC_ROOT/{TOOL}" -export LIBRARY_PATH={LIBPATH} -export LD_LIBRARY_PATH={LDLIBPATH} +{ENV} "$TOOL_LOCATION" {ARGS} "$@" From cb50935fee46d40d7443bc3ace5fcfa9780d20ea Mon Sep 17 00:00:00 2001 From: Andreas Herrmann Date: Thu, 14 Mar 2019 10:53:32 +0100 Subject: [PATCH 06/16] make_path: Deduplicate directories --- haskell/private/path_utils.bzl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/haskell/private/path_utils.bzl b/haskell/private/path_utils.bzl index f2249b126..5d0922165 100644 --- a/haskell/private/path_utils.bzl +++ b/haskell/private/path_utils.bzl @@ -113,16 +113,16 @@ def make_path(libs, prefix = None): Returns: String: paths to the given library directories separated by ":". """ - r = [] + r = set.empty() for lib in libs: lib_dir = paths.dirname(lib.path) if prefix: lib_dir = paths.join(prefix, lib_dir) - r.append(lib_dir) + set.mutable_insert(r, lib_dir) - return ":".join(r) + return ":".join(set.to_list(r)) def darwin_convert_to_dylibs(hs, libs): """Convert .so dynamic libraries to .dylib. From a15d14b4efc352b043a99302cc5326be22fcb2a9 Mon Sep 17 00:00:00 2001 From: Andreas Herrmann Date: Wed, 6 Mar 2019 13:28:07 +0100 Subject: [PATCH 07/16] Multi target REPL Loads all targets given in the deps attribute as well as all dependencies in the local workspace as sources into GHCi. --- haskell/assets/ghci_script | 1 + haskell/haskell.bzl | 9 + haskell/private/actions/repl.bzl | 1 + haskell/repl.bzl | 571 +++++++++++++++++++++++++++++++ 4 files changed, 582 insertions(+) create mode 100644 haskell/repl.bzl diff --git a/haskell/assets/ghci_script b/haskell/assets/ghci_script index 4d23c31f2..816402f67 100644 --- a/haskell/assets/ghci_script +++ b/haskell/assets/ghci_script @@ -36,3 +36,4 @@ let rules_haskell_add_loaded_modules _ = :undef rules_haskell_add_loaded_modules -- reload modules to drop the rules_haskell* definitions :reload +{COMMANDS} diff --git a/haskell/haskell.bzl b/haskell/haskell.bzl index 7d26a6d35..77e1e2a5f 100644 --- a/haskell/haskell.bzl +++ b/haskell/haskell.bzl @@ -36,6 +36,11 @@ load( _haskell_library_impl = "haskell_library_impl", _haskell_test_impl = "haskell_test_impl", ) +load( + ":repl.bzl", + _haskell_repl = "haskell_repl", + _haskell_repl_aspect = "haskell_repl_aspect", +) # For re-exports: load( @@ -304,6 +309,10 @@ haskell_register_ghc_bindists = _haskell_register_ghc_bindists haskell_register_ghc_nixpkgs = _haskell_register_ghc_nixpkgs +haskell_repl = _haskell_repl + +haskell_repl_aspect = _haskell_repl_aspect + haskell_toolchain = _haskell_toolchain haskell_proto_library = _haskell_proto_library diff --git a/haskell/private/actions/repl.bzl b/haskell/private/actions/repl.bzl index bdb478eb1..f336664a6 100644 --- a/haskell/private/actions/repl.bzl +++ b/haskell/private/actions/repl.bzl @@ -109,6 +109,7 @@ def build_haskell_repl( output = ghci_repl_script, substitutions = { "{ADD_SOURCES}": " ".join(add_sources), + "{COMMANDS}": "", }, ) diff --git a/haskell/repl.bzl b/haskell/repl.bzl new file mode 100644 index 000000000..653da2714 --- /dev/null +++ b/haskell/repl.bzl @@ -0,0 +1,571 @@ +"""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", + "get_lib_name", + "is_shared_library", + "ln", + "target_unique_name", +) +load( + "@io_tweag_rules_haskell//haskell:private/providers.bzl", + "HaskellBinaryInfo", + "HaskellBuildInfo", + "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_id": "Workspace unique package identifier.", + "package_caches": "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 = { + "source_files": "Set Haskell source files to load.", + "package_ids": "Set of package ids to load.", + "package_caches": "Set of package cache files.", + "prebuilt_dependencies": "Transitive collection of info of wired-in Haskell dependencies.", + "cc_dependencies": "Direct cc library dependencies. See HaskellCcInfo.", + "transitive_cc_dependencies": "Transitive 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", + }, +) + +def _create_HaskellReplCollectInfo(target, ctx): + load_infos = {} + dep_infos = {} + if HaskellBuildInfo in target: + build_info = target[HaskellBuildInfo] + prebuilt_dependencies = build_info.prebuilt_dependencies + transitive_cc_dependencies = build_info.transitive_cc_dependencies + else: + prebuilt_dependencies = set.empty() + transitive_cc_dependencies = empty_HaskellCcInfo() + + if HaskellLibraryInfo in target: + lib_info = target[HaskellLibraryInfo] + build_info = target[HaskellBuildInfo] + load_infos[target.label] = HaskellReplLoadInfo( + source_files = set.union( + lib_info.boot_files, + lib_info.source_files, + ), + cc_dependencies = build_info.cc_dependencies, + compiler_flags = getattr(ctx.rule.attr, "compiler_flags", []), + repl_ghci_args = getattr(ctx.rule.attr, "repl_ghci_args", []), + ) + dep_infos[target.label] = HaskellReplDepInfo( + package_id = lib_info.package_id, + package_caches = build_info.package_caches, + ) + elif HaskellBinaryInfo in target: + bin_info = target[HaskellBinaryInfo] + build_info = target[HaskellBuildInfo] + load_infos[target.label] = HaskellReplLoadInfo( + source_files = bin_info.source_files, + cc_dependencies = build_info.cc_dependencies, + compiler_flags = ctx.rule.attr.compiler_flags, + repl_ghci_args = ctx.rule.attr.repl_ghci_args, + ) + + 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 _parse_pattern(pattern_str): + """Parses a string label pattern. + + Args: + pattern_str: The pattern to parse. + Patterns are absolute labels in the local workspace. E.g. + `//some/package:some_target`. The following wild-cards are allowed: + `...`, `:all`, and `:*`. Also the `//some/package` shortcut is allowed. + + Returns: + A struct of + package: A list of package path components. May end on the wildcard `...`. + target: The target name. None if the package ends on `...`. May be one + of the wildcards `all` or `*`. + + """ + + # We only load targets in the local workspace anyway. So, it's never + # necessary to specify a workspace. Therefore, we don't allow it. + if pattern_str.startswith("@"): + fail("Invalid haskell_repl pattern. Patterns may not specify a workspace. They only apply to the current workspace") + + # To keep things simple, all patterns have to be absolute. + if not pattern_str.startswith("//"): + fail("Invalid haskell_repl pattern. Patterns must start with //.") + + # Separate package and target (if present). + package_target = pattern_str[2:].split(":", maxsplit = 2) + package_str = package_target[0] + target_str = None + if len(package_target) == 2: + target_str = package_target[1] + + # Parse package pattern. + package = [] + dotdotdot = False # ... has to be last component in the pattern. + for s in package_str.split("/"): + if dotdotdot: + fail("Invalid haskell_repl pattern. ... has to appear at the end.") + if s == "...": + dotdotdot = True + package.append(s) + + # Parse target pattern. + if dotdotdot: + if target_str != None: + fail("Invalid haskell_repl pattern. ... has to appear at the end.") + elif target_str == None: + if len(package) > 0 and package[-1] != "": + target_str = package[-1] + else: + fail("Invalid haskell_repl pattern. The empty string is not a valid target.") + + return struct( + package = package, + target = target_str, + ) + +def _match_label(pattern, label): + """Whether the given local workspace label matches any of the patterns. + + Args: + patterns: A list of parsed patterns to match the label against. + Apply `_parse_pattern` before passing patterns into this function. + label: Match this label against the patterns. + + Returns: + A boolean. True if the label is in the local workspace and matches any of + the given patterns. False otherwise. + + """ + + # Only local workspace labels can match. + # Despite the docs saying otherwise, labels don't have a workspace_name + # attribute. So, we use the workspace_root. If it's empty, the target is in + # the local workspace. Otherwise, it's an external target. + if label.workspace_root != "": + return False + + package = label.package.split("/") + target = label.name + + # Match package components. + for i in range(min(len(pattern.package), len(package))): + if pattern.package[i] == "...": + return True + elif pattern.package[i] != package[i]: + return False + + # If no wild-card or mismatch was encountered, the lengths must match. + # Otherwise, the label's package is not covered. + if len(pattern.package) != len(package): + return False + + # Match target. + if pattern.target == "all" or pattern.target == "*": + return True + else: + return pattern.target == target + +def _load_as_source(include, exclude, lbl): + """Whether a package should be loaded by source or as binary.""" + for pat in exclude: + if _match_label(pat, lbl): + return False + + for pat in include: + if _match_label(pat, lbl): + return True + + return False + +def _create_HaskellReplInfo(include, exclude, collect_info): + """Convert a HaskellReplCollectInfo to a HaskellReplInfo. + + Args: + include: List of patterns for packages to load by source. + exclude: List of patterns for packages to load as binary packages. + collect_info: HaskellReplCollectInfo provider. + + Returns: + HaskellReplInfo provider. + """ + + source_files = set.empty() + package_ids = set.empty() + package_caches = set.empty() + cc_dependencies = empty_HaskellCcInfo() + compiler_flags = [] + repl_ghci_args = [] + + # Collect all packages to load by source. + for (lbl, load_info) in collect_info.load_infos.items(): + if not _load_as_source(include, exclude, lbl): + continue + + 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 + + # Collect all packages to load as binary packages. + for (lbl, dep_info) in collect_info.dep_infos.items(): + if _load_as_source(include, exclude, lbl): + continue + + set.mutable_insert(package_ids, dep_info.package_id) + set.mutable_union(package_caches, dep_info.package_caches) + + return HaskellReplInfo( + source_files = source_files, + package_ids = package_ids, + package_caches = package_caches, + prebuilt_dependencies = collect_info.prebuilt_dependencies, + cc_dependencies = cc_dependencies, + transitive_cc_dependencies = collect_info.transitive_cc_dependencies, + compiler_flags = compiler_flags, + repl_ghci_args = repl_ghci_args, + ) + +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.package_ids): + args.extend(["-package-id", package_id]) + for package_cache in set.to_list(repl_info.package_caches): + args.extend([ + "-package-db", + paths.join("$RULES_HASKELL_EXEC_ROOT", package_cache.dirname), + ]) + + # Load C library dependencies + link_ctx = repl_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. + seen_libs = set.empty() + libraries = [] + for lib in libs_to_link: + lib_name = get_lib_name(lib) + if not set.is_member(seen_libs, lib_name): + set.mutable_insert(seen_libs, lib_name) + args += ["-l{0}".format(lib_name)] + libraries.append(lib_name) + + # 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 + add_sources = [ + "*" + f.path + for f in set.to_list(repl_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.compiler_flags + + hs.toolchain.repl_ghci_args + + repl_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 + { + # Export RUNFILES_DIR so that targets that require runfiles + # can be executed in the REPL. + "RUNFILES_DIR": paths.join("$RULES_HASKELL_EXEC_ROOT", output.path + ".runfiles"), + }), + "{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.source_files)) + extra_inputs.extend(set.to_list(repl_info.package_caches)) + 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): + is_eligible = ( + HaskellLibraryInfo in target or + HaskellBinaryInfo in target + ) + if not is_eligible: + 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 + ]) + include = [_parse_pattern(pat) for pat in ctx.attr.include] + exclude = [_parse_pattern(pat) for pat in ctx.attr.exclude] + repl_info = _create_HaskellReplInfo(include, exclude, 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", + ), + "include": 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. + """, + default = ["//..."], + ), + "exclude": 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. + """, + 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", + ], + include = [ + "//lib/...", + "//exe/...", + "//common/...", + ], + exclude = [ + "//lib/vendored/...", + ], + ) + ``` + + Collects all transitive Haskell dependencies from `deps`. Those that match + `exclude` or are defined in an external workspace will be loaded as binary + packages. Those that match `include` and are defined in the local workspace + will be loaded by source. + + You can call the REPL like this: + +``` +$ bazel run //:repl +``` + +""" From 6ab087535e50093b2bc0ccaacb51f5cf31fd4a23 Mon Sep 17 00:00:00 2001 From: Andreas Herrmann Date: Mon, 11 Mar 2019 09:43:08 +0100 Subject: [PATCH 08/16] Add test cases for haskell_multi REPL Based on #471 --- tests/RunTests.hs | 10 +++++++++- tests/multi_repl/BUILD.bazel | 16 ++++++++++++++++ tests/multi_repl/a/BUILD.bazel | 17 +++++++++++++++++ tests/multi_repl/a/src/A/A.hs | 4 ++++ tests/multi_repl/bc/BUILD.bazel | 30 ++++++++++++++++++++++++++++++ tests/multi_repl/bc/src/BC/B.hs | 6 ++++++ tests/multi_repl/bc/src/BC/C.hs | 6 ++++++ 7 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 tests/multi_repl/BUILD.bazel create mode 100644 tests/multi_repl/a/BUILD.bazel create mode 100644 tests/multi_repl/a/src/A/A.hs create mode 100644 tests/multi_repl/bc/BUILD.bazel create mode 100644 tests/multi_repl/bc/src/BC/B.hs create mode 100644 tests/multi_repl/bc/src/BC/C.hs diff --git a/tests/RunTests.hs b/tests/RunTests.hs index 50d05d6a9..b0cb08d4e 100644 --- a/tests/RunTests.hs +++ b/tests/RunTests.hs @@ -4,7 +4,7 @@ {-# LANGUAGE QuasiQuotes #-} import Data.Foldable (for_) -import Data.List (isInfixOf) +import Data.List (isInfixOf, sort) import System.Exit (ExitCode(..)) import qualified System.Process as Process @@ -66,6 +66,14 @@ main = hspec $ do it "repl flags" $ do assertSuccess (bazel ["run", "//tests/repl-flags:repl_flags@repl", "--", "-ignore-dot-ghci", "-e", "foo"]) + describe "multi_repl" $ do + it "loads transitive library dependencies" $ do + let p' (stdout, _stderr) = lines stdout == ["tests/multi_repl/bc/src/BC/C.hs"] + outputSatisfy p' (bazel ["run", "//tests/multi_repl:c_only_repl", "--", "-ignore-dot-ghci", "-e", ":show targets"]) + it "loads transitive source dependencies" $ do + let p' (stdout, _stderr) = sort (lines stdout) == ["tests/multi_repl/a/src/A/A.hs","tests/multi_repl/bc/src/BC/B.hs","tests/multi_repl/bc/src/BC/C.hs"] + outputSatisfy p' (bazel ["run", "//tests/multi_repl:c_multi_repl", "--", "-ignore-dot-ghci", "-e", ":show targets"]) + it "startup script" $ do assertSuccess (safeShell [ "pwd=$(pwd)" diff --git a/tests/multi_repl/BUILD.bazel b/tests/multi_repl/BUILD.bazel new file mode 100644 index 000000000..99b892d92 --- /dev/null +++ b/tests/multi_repl/BUILD.bazel @@ -0,0 +1,16 @@ +load( + "@io_tweag_rules_haskell//haskell:haskell.bzl", + "haskell_repl", +) + +haskell_repl( + name = "c_only_repl", + # To only load :c by source. + include = ["//tests/multi_repl/bc:c"], + deps = ["//tests/multi_repl/bc:c"], +) + +haskell_repl( + name = "c_multi_repl", + deps = ["//tests/multi_repl/bc:c"], +) diff --git a/tests/multi_repl/a/BUILD.bazel b/tests/multi_repl/a/BUILD.bazel new file mode 100644 index 000000000..7d17e834a --- /dev/null +++ b/tests/multi_repl/a/BUILD.bazel @@ -0,0 +1,17 @@ +package(default_visibility = ["//visibility:public"]) + +load( + "@io_tweag_rules_haskell//haskell:haskell.bzl", + "haskell_library", +) + +haskell_library( + name = "a", + srcs = [ + "src/A/A.hs", + ], + src_strip_prefix = "src", + deps = [ + "//tests/hackage:base", + ], +) diff --git a/tests/multi_repl/a/src/A/A.hs b/tests/multi_repl/a/src/A/A.hs new file mode 100644 index 000000000..92eb0e607 --- /dev/null +++ b/tests/multi_repl/a/src/A/A.hs @@ -0,0 +1,4 @@ +module A.A ( a ) where + +a :: () +a = () diff --git a/tests/multi_repl/bc/BUILD.bazel b/tests/multi_repl/bc/BUILD.bazel new file mode 100644 index 000000000..98372a4a1 --- /dev/null +++ b/tests/multi_repl/bc/BUILD.bazel @@ -0,0 +1,30 @@ +package(default_visibility = ["//visibility:public"]) + +load( + "@io_tweag_rules_haskell//haskell:haskell.bzl", + "haskell_library", +) + +haskell_library( + name = "b", + srcs = [ + "src/BC/B.hs", + ], + src_strip_prefix = "src", + deps = [ + "//tests/hackage:base", + "//tests/multi_repl/a", + ], +) + +haskell_library( + name = "c", + srcs = [ + "src/BC/C.hs", + ], + src_strip_prefix = "src", + deps = [ + ":b", + "//tests/hackage:base", + ], +) diff --git a/tests/multi_repl/bc/src/BC/B.hs b/tests/multi_repl/bc/src/BC/B.hs new file mode 100644 index 000000000..b86223c41 --- /dev/null +++ b/tests/multi_repl/bc/src/BC/B.hs @@ -0,0 +1,6 @@ +module BC.B ( b ) where + +import A.A ( a ) + +b :: () +b = a diff --git a/tests/multi_repl/bc/src/BC/C.hs b/tests/multi_repl/bc/src/BC/C.hs new file mode 100644 index 000000000..6e6f31f0c --- /dev/null +++ b/tests/multi_repl/bc/src/BC/C.hs @@ -0,0 +1,6 @@ +module BC.C ( c ) where + +import BC.B ( b ) + +c :: () +c = b From 3ef1b1c897e7b19c9997180dd35f444079d85238 Mon Sep 17 00:00:00 2001 From: Andreas Herrmann Date: Fri, 15 Mar 2019 10:42:19 +0100 Subject: [PATCH 09/16] ghci_script: Avoid name shadowing Names such as `tmpDir` or `h` are likely to shadown names from loaded modules, which will cause errors if `-Werror=name-shadowing` is active. --- haskell/assets/ghci_script | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/haskell/assets/ghci_script b/haskell/assets/ghci_script index 816402f67..ddf9e7917 100644 --- a/haskell/assets/ghci_script +++ b/haskell/assets/ghci_script @@ -6,10 +6,10 @@ import qualified System.Directory as Dir rules_haskell_stdout_dupe <- Handle.hDuplicate IO.stdout :{ (rules_haskell_stdout_copy_file, rules_haskell_stdout_copy_h) <- do - tmpDir <- Dir.getTemporaryDirectory Prelude.>>= Dir.canonicalizePath - (fn, h) <- IO.openTempFile tmpDir "rules-haskell-ghci-repl" - Handle.hDuplicateTo h IO.stdout - Prelude.return (fn, h) + rules_haskell_tmp_dir <- Dir.getTemporaryDirectory Prelude.>>= Dir.canonicalizePath + (rules_haskell_fn, rules_haskell_h) <- IO.openTempFile rules_haskell_tmp_dir "rules-haskell-ghci-repl" + Handle.hDuplicateTo rules_haskell_h IO.stdout + Prelude.return (rules_haskell_fn, rules_haskell_h) :} :show modules :{ @@ -19,10 +19,10 @@ rules_haskell_loaded_modules <- do -- stream at offset 0 did not work (no data is there, although the -- corresponding file certainly contained it after flushing). Couldn't -- figure this one out, so we first close the file and then read from it. - h <- IO.openFile rules_haskell_stdout_copy_file IO.ReadMode - xs <- Handle.hGetContents h + rules_haskell_h <- IO.openFile rules_haskell_stdout_copy_file IO.ReadMode + rules_haskell_xs <- Handle.hGetContents rules_haskell_h Dir.removeFile rules_haskell_stdout_copy_file - Prelude.return Prelude.$ Prelude.takeWhile (Prelude./= ' ') Prelude.<$> Prelude.lines xs + Prelude.return Prelude.$ Prelude.takeWhile (Prelude./= ' ') Prelude.<$> Prelude.lines rules_haskell_xs :} hDuplicateTo rules_haskell_stdout_dupe IO.stdout :{ From c4b86c6011279b12a87a29a0296c45c506d36fcc Mon Sep 17 00:00:00 2001 From: Andreas Herrmann Date: Tue, 19 Mar 2019 11:16:35 +0100 Subject: [PATCH 10/16] Compose HaskellReplInfo of LoadInfo and DepInfo Addressing review comment https://github.com/tweag/rules_haskell/pull/736/files/cb50935fee46d40d7443bc3ace5fcfa9780d20ea..a15d14b4efc352b043a99302cc5326be22fcb2a9#r266604355 --- haskell/repl.bzl | 105 ++++++++++++++++++++++++++--------------------- 1 file changed, 59 insertions(+), 46 deletions(-) diff --git a/haskell/repl.bzl b/haskell/repl.bzl index 653da2714..d912701c7 100644 --- a/haskell/repl.bzl +++ b/haskell/repl.bzl @@ -40,7 +40,7 @@ HaskellReplDepInfo = provider( Information to a Haskell target to load into the REPL as a built package. """, fields = { - "package_id": "Workspace unique package identifier.", + "package_ids": "Set of workspace unique package identifiers.", "package_caches": "Set of package cache files.", }, ) @@ -66,17 +66,48 @@ HaskellReplInfo = provider( from source or as built packages. """, fields = { - "source_files": "Set Haskell source files to load.", - "package_ids": "Set of package ids to load.", - "package_caches": "Set of package cache files.", + "load_info": "Combined HaskellReplLoadInfo.", + "dep_info": "Combined HaskellReplDepInfo.", "prebuilt_dependencies": "Transitive collection of info of wired-in Haskell dependencies.", - "cc_dependencies": "Direct cc library dependencies. See HaskellCcInfo.", "transitive_cc_dependencies": "Transitive 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", }, ) +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_caches = set.empty() + + for dep_info in dep_infos: + set.mutable_union(package_ids, dep_info.package_ids) + set.mutable_union(package_caches, dep_info.package_caches) + + return HaskellReplDepInfo( + package_ids = package_ids, + package_caches = package_caches, + ) + def _create_HaskellReplCollectInfo(target, ctx): load_infos = {} dep_infos = {} @@ -101,7 +132,7 @@ def _create_HaskellReplCollectInfo(target, ctx): repl_ghci_args = getattr(ctx.rule.attr, "repl_ghci_args", []), ) dep_infos[target.label] = HaskellReplDepInfo( - package_id = lib_info.package_id, + package_ids = set.singleton(lib_info.package_id), package_caches = build_info.package_caches, ) elif HaskellBinaryInfo in target: @@ -269,43 +300,25 @@ def _create_HaskellReplInfo(include, exclude, collect_info): HaskellReplInfo provider. """ - source_files = set.empty() - package_ids = set.empty() - package_caches = set.empty() - cc_dependencies = empty_HaskellCcInfo() - compiler_flags = [] - repl_ghci_args = [] - # Collect all packages to load by source. - for (lbl, load_info) in collect_info.load_infos.items(): - if not _load_as_source(include, exclude, lbl): - continue - - 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 + load_info = _merge_HaskellReplLoadInfo([ + load_info + for (lbl, load_info) in collect_info.load_infos.items() + if _load_as_source(include, exclude, lbl) + ]) # Collect all packages to load as binary packages. - for (lbl, dep_info) in collect_info.dep_infos.items(): - if _load_as_source(include, exclude, lbl): - continue - - set.mutable_insert(package_ids, dep_info.package_id) - set.mutable_union(package_caches, dep_info.package_caches) + dep_info = _merge_HaskellReplDepInfo([ + dep_info + for (lbl, dep_info) in collect_info.dep_infos.items() + if not _load_as_source(include, exclude, lbl) + ]) return HaskellReplInfo( - source_files = source_files, - package_ids = package_ids, - package_caches = package_caches, + load_info = load_info, + dep_info = dep_info, prebuilt_dependencies = collect_info.prebuilt_dependencies, - cc_dependencies = cc_dependencies, transitive_cc_dependencies = collect_info.transitive_cc_dependencies, - compiler_flags = compiler_flags, - repl_ghci_args = repl_ghci_args, ) def _create_repl(hs, ctx, repl_info, output): @@ -332,16 +345,16 @@ def _create_repl(hs, ctx, repl_info, output): args.extend(["-package", dep.package]) # Load built dependencies (-package-id, -package-db) - for package_id in set.to_list(repl_info.package_ids): + 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.package_caches): + for package_cache in set.to_list(repl_info.dep_info.package_caches): args.extend([ "-package-db", paths.join("$RULES_HASKELL_EXEC_ROOT", package_cache.dirname), ]) # Load C library dependencies - link_ctx = repl_info.cc_dependencies.dynamic_linking + 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. @@ -366,7 +379,7 @@ def _create_repl(hs, ctx, repl_info, output): # Load source files add_sources = [ "*" + f.path - for f in set.to_list(repl_info.source_files) + 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"), @@ -397,9 +410,9 @@ def _create_repl(hs, ctx, repl_info, output): # `-XOverloadedStrings`. quote_args = ( hs.toolchain.compiler_flags + - repl_info.compiler_flags + + repl_info.load_info.compiler_flags + hs.toolchain.repl_ghci_args + - repl_info.repl_ghci_args + + repl_info.load_info.repl_ghci_args + ctx.attr.repl_ghci_args ) @@ -427,8 +440,8 @@ def _create_repl(hs, ctx, repl_info, output): hs.tools.ghci, ghci_repl_script, ] - extra_inputs.extend(set.to_list(repl_info.source_files)) - extra_inputs.extend(set.to_list(repl_info.package_caches)) + extra_inputs.extend(set.to_list(repl_info.load_info.source_files)) + extra_inputs.extend(set.to_list(repl_info.dep_info.package_caches)) extra_inputs.extend(library_deps) extra_inputs.extend(ld_library_deps) return [DefaultInfo( From a47d1c962f469968146cbea9e5c6e6c4ccfe5cb6 Mon Sep 17 00:00:00 2001 From: Andreas Herrmann Date: Tue, 19 Mar 2019 11:42:30 +0100 Subject: [PATCH 11/16] Always require HaskellBuildInfo Addressing review comments https://github.com/tweag/rules_haskell/pull/736/files/cb50935fee46d40d7443bc3ace5fcfa9780d20ea..a15d14b4efc352b043a99302cc5326be22fcb2a9#r266604868 https://github.com/tweag/rules_haskell/pull/736/files/cb50935fee46d40d7443bc3ace5fcfa9780d20ea..a15d14b4efc352b043a99302cc5326be22fcb2a9#r266605247 https://github.com/tweag/rules_haskell/pull/736/files/cb50935fee46d40d7443bc3ace5fcfa9780d20ea..a15d14b4efc352b043a99302cc5326be22fcb2a9#r266605433 --- haskell/repl.bzl | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/haskell/repl.bzl b/haskell/repl.bzl index d912701c7..7e9dd752d 100644 --- a/haskell/repl.bzl +++ b/haskell/repl.bzl @@ -111,17 +111,13 @@ def _merge_HaskellReplDepInfo(dep_infos): def _create_HaskellReplCollectInfo(target, ctx): load_infos = {} dep_infos = {} - if HaskellBuildInfo in target: - build_info = target[HaskellBuildInfo] - prebuilt_dependencies = build_info.prebuilt_dependencies - transitive_cc_dependencies = build_info.transitive_cc_dependencies - else: - prebuilt_dependencies = set.empty() - transitive_cc_dependencies = empty_HaskellCcInfo() + + build_info = target[HaskellBuildInfo] + prebuilt_dependencies = build_info.prebuilt_dependencies + transitive_cc_dependencies = build_info.transitive_cc_dependencies if HaskellLibraryInfo in target: lib_info = target[HaskellLibraryInfo] - build_info = target[HaskellBuildInfo] load_infos[target.label] = HaskellReplLoadInfo( source_files = set.union( lib_info.boot_files, @@ -137,13 +133,14 @@ def _create_HaskellReplCollectInfo(target, ctx): ) elif HaskellBinaryInfo in target: bin_info = target[HaskellBinaryInfo] - build_info = target[HaskellBuildInfo] load_infos[target.label] = HaskellReplLoadInfo( source_files = bin_info.source_files, cc_dependencies = build_info.cc_dependencies, compiler_flags = ctx.rule.attr.compiler_flags, repl_ghci_args = ctx.rule.attr.repl_ghci_args, ) + else: + fail("Missing HaskellLibraryInfo or HaskellBinaryInfo.") return HaskellReplCollectInfo( load_infos = load_infos, @@ -454,8 +451,10 @@ def _create_repl(hs, ctx, repl_info, output): def _haskell_repl_aspect_impl(target, ctx): is_eligible = ( - HaskellLibraryInfo in target or - HaskellBinaryInfo in target + HaskellBuildInfo in target and ( + HaskellLibraryInfo in target or + HaskellBinaryInfo in target + ) ) if not is_eligible: return [] From 893f51480417e35c66efc5eb2096a574673ae622 Mon Sep 17 00:00:00 2001 From: Andreas Herrmann Date: Tue, 19 Mar 2019 12:03:09 +0100 Subject: [PATCH 12/16] Cross-platform runfiles handling in repl The RUNFILES_DIR environment variable is not meaningful on Windows, where Bazel instead creates a RUNFILES_MANIFEST_FILE which lists all runfile dependencies. This changes the REPL wrapper to automatically construct RUNFILES_DIR and RUNFILES_MANIFEST_FILE in a manner compatible with Bazel's Bash runfiles library. --- haskell/private/ghci_repl_wrapper.sh | 14 ++++++++++++++ haskell/repl.bzl | 6 +----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/haskell/private/ghci_repl_wrapper.sh b/haskell/private/ghci_repl_wrapper.sh index dd5c4bdd4..cd6acefc7 100644 --- a/haskell/private/ghci_repl_wrapper.sh +++ b/haskell/private/ghci_repl_wrapper.sh @@ -20,6 +20,20 @@ EOF exit 1 fi +# Derived from Bazel's Bash runfiles library (tools/bash/runfiles/runfiles.bash). +if [[ -z "$RUNFILES_DIR" ]]; then + if [[ -d "$0.runfiles" ]]; then + export RUNFILES_DIR="$0.runfiles" + fi +fi +if [[ -z "$RUNFILES_MANIFEST_FILE" ]]; then + if [[ -f "$0.runfiles_manifest" ]]; then + export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest" + elif [[ -f "$0.runfiles/MANIFEST" ]]; then + export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST" + fi +fi + # GHCi script and libraries are loaded relative to workspace directory. # bazel run //some:target@repl will be executed from the workspace directory. # bazel run //some:haskell_repl will be executed from its execroot. diff --git a/haskell/repl.bzl b/haskell/repl.bzl index 7e9dd752d..93d968363 100644 --- a/haskell/repl.bzl +++ b/haskell/repl.bzl @@ -418,11 +418,7 @@ def _create_repl(hs, ctx, repl_info, output): output = output, is_executable = True, substitutions = { - "{ENV}": render_env(ghc_env + { - # Export RUNFILES_DIR so that targets that require runfiles - # can be executed in the REPL. - "RUNFILES_DIR": paths.join("$RULES_HASKELL_EXEC_ROOT", output.path + ".runfiles"), - }), + "{ENV}": render_env(ghc_env), "{TOOL}": hs.tools.ghci.path, "{ARGS}": " ".join( args + [ From 0976c961b999db47b2dd4a6b854ffaabc9d02d4f Mon Sep 17 00:00:00 2001 From: Andreas Herrmann Date: Tue, 19 Mar 2019 12:08:02 +0100 Subject: [PATCH 13/16] _merge_HaskellReplCollectInfo take list Addressing review comment https://github.com/tweag/rules_haskell/pull/736/files/cb50935fee46d40d7443bc3ace5fcfa9780d20ea..a15d14b4efc352b043a99302cc5326be22fcb2a9#r266618173 --- haskell/repl.bzl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/haskell/repl.bzl b/haskell/repl.bzl index 93d968363..cb80dfe09 100644 --- a/haskell/repl.bzl +++ b/haskell/repl.bzl @@ -149,7 +149,7 @@ def _create_HaskellReplCollectInfo(target, ctx): transitive_cc_dependencies = transitive_cc_dependencies, ) -def _merge_HaskellReplCollectInfo(*args): +def _merge_HaskellReplCollectInfo(args): load_infos = {} dep_infos = {} prebuilt_dependencies = set.empty() @@ -461,7 +461,7 @@ def _haskell_repl_aspect_impl(target, ctx): for dep in ctx.rule.attr.deps if HaskellReplCollectInfo in dep ] - collect_info = _merge_HaskellReplCollectInfo(*([target_info] + deps_infos)) + 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 @@ -481,7 +481,7 @@ by itself. """ def _haskell_repl_impl(ctx): - collect_info = _merge_HaskellReplCollectInfo(*[ + collect_info = _merge_HaskellReplCollectInfo([ dep[HaskellReplCollectInfo] for dep in ctx.attr.deps if HaskellReplCollectInfo in dep From 53b9292e3fad78dd956b52b266225570b9f23ba6 Mon Sep 17 00:00:00 2001 From: Andreas Herrmann Date: Tue, 19 Mar 2019 12:12:18 +0100 Subject: [PATCH 14/16] Rename include/exclude attributes Addressing review comments https://github.com/tweag/rules_haskell/pull/736/files/cb50935fee46d40d7443bc3ace5fcfa9780d20ea..a15d14b4efc352b043a99302cc5326be22fcb2a9#r266610644 https://github.com/tweag/rules_haskell/pull/736#pullrequestreview-215805023 --- haskell/repl.bzl | 42 ++++++++++++++++++++---------------- tests/multi_repl/BUILD.bazel | 2 +- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/haskell/repl.bzl b/haskell/repl.bzl index cb80dfe09..f9be566d5 100644 --- a/haskell/repl.bzl +++ b/haskell/repl.bzl @@ -273,24 +273,24 @@ def _match_label(pattern, label): else: return pattern.target == target -def _load_as_source(include, exclude, lbl): +def _load_as_source(from_source, from_binary, lbl): """Whether a package should be loaded by source or as binary.""" - for pat in exclude: + for pat in from_binary: if _match_label(pat, lbl): return False - for pat in include: + for pat in from_source: if _match_label(pat, lbl): return True return False -def _create_HaskellReplInfo(include, exclude, collect_info): +def _create_HaskellReplInfo(from_source, from_binary, collect_info): """Convert a HaskellReplCollectInfo to a HaskellReplInfo. Args: - include: List of patterns for packages to load by source. - exclude: List of patterns for packages to load as binary packages. + 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: @@ -301,14 +301,14 @@ def _create_HaskellReplInfo(include, exclude, collect_info): load_info = _merge_HaskellReplLoadInfo([ load_info for (lbl, load_info) in collect_info.load_infos.items() - if _load_as_source(include, exclude, lbl) + 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(include, exclude, lbl) + if not _load_as_source(from_source, from_binary, lbl) ]) return HaskellReplInfo( @@ -486,9 +486,9 @@ def _haskell_repl_impl(ctx): for dep in ctx.attr.deps if HaskellReplCollectInfo in dep ]) - include = [_parse_pattern(pat) for pat in ctx.attr.include] - exclude = [_parse_pattern(pat) for pat in ctx.attr.exclude] - repl_info = _create_HaskellReplInfo(include, exclude, collect_info) + from_source = [_parse_pattern(pat) for pat in ctx.attr.experimental_from_source] + from_binary = [_parse_pattern(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) @@ -507,21 +507,27 @@ haskell_repl = rule( aspects = [haskell_repl_aspect], doc = "List of Haskell targets to load into the REPL", ), - "include": attr.string_list( + "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 = ["//..."], ), - "exclude": attr.string_list( + "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 = [], ), @@ -554,21 +560,21 @@ Example: "//lib:some_lib", "//exe:some_exe", ], - include = [ + experimental_from_source = [ "//lib/...", "//exe/...", "//common/...", ], - exclude = [ + experimental_from_binary = [ "//lib/vendored/...", ], ) ``` Collects all transitive Haskell dependencies from `deps`. Those that match - `exclude` or are defined in an external workspace will be loaded as binary - packages. Those that match `include` and are defined in the local workspace - will be loaded by source. + `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: diff --git a/tests/multi_repl/BUILD.bazel b/tests/multi_repl/BUILD.bazel index 99b892d92..b2edce37a 100644 --- a/tests/multi_repl/BUILD.bazel +++ b/tests/multi_repl/BUILD.bazel @@ -6,7 +6,7 @@ load( haskell_repl( name = "c_only_repl", # To only load :c by source. - include = ["//tests/multi_repl/bc:c"], + experimental_from_source = ["//tests/multi_repl/bc:c"], deps = ["//tests/multi_repl/bc:c"], ) From af40bb42a70955dcce90596d9fad50589dec8dd3 Mon Sep 17 00:00:00 2001 From: Andreas Herrmann Date: Wed, 20 Mar 2019 09:48:18 +0100 Subject: [PATCH 15/16] Factor out link_libraries Note, in some instances this adds static libraries to the link commands that were previously filtered out. A corresponding comment stated that GHCi cannot load static libraries. However, this is not true. GHCi can load static PIC libraries. --- haskell/private/actions/repl.bzl | 15 +++------------ haskell/private/actions/runghc.bzl | 13 +++---------- haskell/private/path_utils.bzl | 20 ++++++++++++++++++++ haskell/repl.bzl | 10 ++-------- 4 files changed, 28 insertions(+), 30 deletions(-) diff --git a/haskell/private/actions/repl.bzl b/haskell/private/actions/repl.bzl index f336664a6..88246c1b6 100644 --- a/haskell/private/actions/repl.bzl +++ b/haskell/private/actions/repl.bzl @@ -6,6 +6,7 @@ load( ":private/path_utils.bzl", "get_lib_name", "is_shared_library", + "link_libraries", "ln", "target_unique_name", ) @@ -69,18 +70,8 @@ def build_haskell_repl( link_ctx = build_info.cc_dependencies.dynamic_linking libs_to_link = link_ctx.dynamic_libraries_for_runtime.to_list() - # External shared libraries that we need to make available to the REPL. - # This only includes dynamic libraries as including static libraries here - # would cause linking errors as ghci cannot load static libraries. - # XXX: Verify that static libraries can't be loaded by GHCi. - seen_libs = set.empty() - libraries = [] - for lib in libs_to_link: - lib_name = get_lib_name(lib) - if is_shared_library(lib) and not set.is_member(seen_libs, lib_name): - set.mutable_insert(seen_libs, lib_name) - args += ["-l{0}".format(lib_name)] - libraries.append(lib_name) + # 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( diff --git a/haskell/private/actions/runghc.bzl b/haskell/private/actions/runghc.bzl index 6be11a087..4770dd8e9 100644 --- a/haskell/private/actions/runghc.bzl +++ b/haskell/private/actions/runghc.bzl @@ -6,6 +6,7 @@ load( ":private/path_utils.bzl", "get_lib_name", "is_shared_library", + "link_libraries", "ln", "target_unique_name", ) @@ -60,16 +61,8 @@ def build_haskell_runghc( link_ctx = build_info.cc_dependencies.dynamic_linking libs_to_link = link_ctx.dynamic_libraries_for_runtime.to_list() - # External shared libraries that we need to make available to runghc. - # This only includes dynamic libraries as including static libraries here - # would cause linking errors as ghci cannot load static libraries. - # XXX: Verify that static libraries can't be loaded by GHCi. - seen_libs = set.empty() - for lib in libs_to_link: - lib_name = get_lib_name(lib) - if is_shared_library(lib) and not set.is_member(seen_libs, lib_name): - set.mutable_insert(seen_libs, lib_name) - args += ["-l{0}".format(lib_name)] + # External C libraries that we need to make available to runghc. + 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( diff --git a/haskell/private/path_utils.bzl b/haskell/private/path_utils.bzl index 5d0922165..db8ca3525 100644 --- a/haskell/private/path_utils.bzl +++ b/haskell/private/path_utils.bzl @@ -172,6 +172,26 @@ def get_lib_name(lib): end = paths.replace_extension(base, "") if n == -1 else base[:n] return end +def link_libraries(libs_to_link, args): + """Add linker flags to link against the given libraries. + + Args: + libs_to_link: List of library Files. + args: Append arguments to this list. + + Returns: + List of library names that were linked. + + """ + seen_libs = set.empty() + libraries = [] + for lib in libs_to_link: + lib_name = get_lib_name(lib) + if not set.is_member(seen_libs, lib_name): + set.mutable_insert(seen_libs, lib_name) + args += ["-l{0}".format(lib_name)] + libraries.append(lib_name) + def is_shared_library(f): """Check if the given File is a shared library. diff --git a/haskell/repl.bzl b/haskell/repl.bzl index f9be566d5..2bd35b963 100644 --- a/haskell/repl.bzl +++ b/haskell/repl.bzl @@ -7,6 +7,7 @@ load( "@io_tweag_rules_haskell//haskell:private/path_utils.bzl", "get_lib_name", "is_shared_library", + "link_libraries", "ln", "target_unique_name", ) @@ -355,14 +356,7 @@ def _create_repl(hs, ctx, repl_info, output): libs_to_link = link_ctx.dynamic_libraries_for_runtime.to_list() # External C libraries that we need to make available to the REPL. - seen_libs = set.empty() - libraries = [] - for lib in libs_to_link: - lib_name = get_lib_name(lib) - if not set.is_member(seen_libs, lib_name): - set.mutable_insert(seen_libs, lib_name) - args += ["-l{0}".format(lib_name)] - libraries.append(lib_name) + 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( From 96ddda49d409b38c8f17185719b664c3d01da4db Mon Sep 17 00:00:00 2001 From: Andreas Herrmann Date: Wed, 20 Mar 2019 12:20:12 +0100 Subject: [PATCH 16/16] Add comment explaining `:add *...` syntax --- haskell/repl.bzl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/haskell/repl.bzl b/haskell/repl.bzl index 2bd35b963..7c6f8a151 100644 --- a/haskell/repl.bzl +++ b/haskell/repl.bzl @@ -368,6 +368,8 @@ def _create_repl(hs, ctx, repl_info, output): 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)