diff --git a/.ghcide b/.ghcide new file mode 100755 index 000000000..f1c2c2fa0 --- /dev/null +++ b/.ghcide @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail +build_ghcide() { + bazel build @ghcide//:ghcide \ + --experimental_show_artifacts \ + 2>&1 \ + | awk ' + /^>>>/ { print substr($1, 4); next } + { print $0 > "/dev/stderr" } + ' +} +ghcide="$(build_ghcide)" +"$ghcide" "$@" diff --git a/.hie-bios b/.hie-bios new file mode 100755 index 000000000..31f3736a8 --- /dev/null +++ b/.hie-bios @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail +hie_bios_flags() { + bazel build //tests:hie-bios \ + --output_groups=hie_bios \ + --experimental_show_artifacts \ + 2>&1 \ + | awk ' + /^>>>/ { + while ((getline line < substr($1, 4)) > 0) { + print line + } + next + } + { + print $0 > "/dev/stderr" + } + ' + # Make warnings non-fatal + echo -Wwarn +} +if [[ -z "${HIE_BIOS_OUTPUT-}" ]]; then + hie_bios_flags +else + hie_bios_flags >"$HIE_BIOS_OUTPUT" +fi diff --git a/WORKSPACE b/WORKSPACE index bf07c4215..12bece874 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -120,11 +120,89 @@ stack_snapshot( # In a separate repo because not all platforms support zlib. stack_snapshot( name = "stackage-zlib", - extra_deps = {"zlib": ["@zlib.win//:zlib" if is_windows else "@zlib.dev//:zlib"]}, + extra_deps = {"zlib": ["@zlib.dev//:zlib" if is_nix_shell else "@zlib.hs//:zlib"]}, packages = ["zlib"], snapshot = test_stack_snapshot, ) +stack_snapshot( + name = "stackage_ghcide", + extra_deps = {"zlib": ["@zlib.dev//:zlib" if is_nix_shell else "@zlib.hs//:zlib"]}, + haddock = False, + local_snapshot = "//:ghcide-stack-snapshot.yaml", + packages = [ + "aeson", + "base", + "base16-bytestring", + "binary", + "bytestring", + "containers", + "cryptohash-sha1", + "data-default", + "deepseq", + "directory", + "extra", + "filepath", + "ghc", + "ghc-check", + "ghc-paths", + "ghcide", + "gitrev", + "hashable", + "haskell-lsp", + "haskell-lsp-types", + "hie-bios", + "hslogger", + "optparse-applicative", + "shake", + "text", + "unordered-containers", + ], +) + +http_archive( + name = "ghcide", + build_file_content = """ +load("@rules_haskell//haskell:cabal.bzl", "haskell_cabal_binary") +haskell_cabal_binary( + name = "ghcide", + srcs = glob(["**"]), + deps = [ + "@stackage_ghcide//:hslogger", + "@stackage_ghcide//:aeson", + "@stackage_ghcide//:base", + "@stackage_ghcide//:binary", + "@stackage_ghcide//:base16-bytestring", + "@stackage_ghcide//:bytestring", + "@stackage_ghcide//:containers", + "@stackage_ghcide//:cryptohash-sha1", + "@stackage_ghcide//:data-default", + "@stackage_ghcide//:deepseq", + "@stackage_ghcide//:directory", + "@stackage_ghcide//:extra", + "@stackage_ghcide//:filepath", + "@stackage_ghcide//:ghc-check", + "@stackage_ghcide//:ghc-paths", + "@stackage_ghcide//:ghc", + "@stackage_ghcide//:gitrev", + "@stackage_ghcide//:hashable", + "@stackage_ghcide//:haskell-lsp", + "@stackage_ghcide//:haskell-lsp-types", + "@stackage_ghcide//:hie-bios", + "@stackage_ghcide//:ghcide", + "@stackage_ghcide//:optparse-applicative", + "@stackage_ghcide//:shake", + "@stackage_ghcide//:text", + "@stackage_ghcide//:unordered-containers", + ], + visibility = ["//visibility:public"], +) + """, + sha256 = "fa1f0cfb0357e7bfa6c86076493f038e0ea5fcd75c470473f2ede7e32566cd9a", + strip_prefix = "ghcide-0c9a0961abbeef851b4117e6408f15a6d46eb1f1", + urls = ["https://github.com/digital-asset/ghcide/archive/0c9a0961abbeef851b4117e6408f15a6d46eb1f1.tar.gz"], +) + load( "@io_tweag_rules_nixpkgs//nixpkgs:nixpkgs.bzl", "nixpkgs_cc_configure", @@ -308,8 +386,9 @@ filegroup( ) http_archive( - name = "zlib.win", + name = "zlib.hs", build_file_content = """ +load("@os_info//:os_info.bzl", "is_darwin") load("@rules_cc//cc:defs.bzl", "cc_library") cc_library( name = "zlib", @@ -318,9 +397,18 @@ cc_library( srcs = [":z"], hdrs = glob(["*.h"]), includes = ["."], + linkstatic = 1, visibility = ["//visibility:public"], ) -cc_library(name = "z", srcs = glob(["*.c"]), hdrs = glob(["*.h"])) +cc_library( + name = "z", + srcs = glob(["*.c"]), + hdrs = glob(["*.h"]), + # Cabal packages depending on dynamic C libraries fail on MacOS + # due to `-rpath` flags being forwarded indiscriminately. + # See https://github.com/tweag/rules_haskell/issues/1317 + linkstatic = is_darwin, +) """, sha256 = "c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1", strip_prefix = "zlib-1.2.11", diff --git a/docs/haskell-use-cases.rst b/docs/haskell-use-cases.rst index c9df66a63..30aa564da 100644 --- a/docs/haskell-use-cases.rst +++ b/docs/haskell-use-cases.rst @@ -143,6 +143,234 @@ This works for any ``haskell_binary`` or ``haskell_library`` target. Modules of all libraries will be loaded in interpreted mode and can be reloaded using the ``:r`` GHCi command when source files change. +Configuring IDE integration with ghcide +--------------------------------------- + +rules_haskell has preliminary support for IDE integration using `ghcide`_. The +ghcide project provides IDE features for Haskell projects through the Language +Server Protocol. To set this up you can define a `haskell_repl`_ target that +will collect the required compiler flags for your Haskell targets and pass them +to `hie-bios`_ which will then forward them to ghcide. + +Let's set this up for the following example project:: + + haskell_toolchain_library( + name = "base", + ) + + haskell_library( + name = "library-a", + srcs = ["Lib/A.hs"], + deps = [":base"], + ) + + haskell_library( + name = "library-b", + srcs = ["Lib/B.hs"], + deps = [":base"], + ) + + haskell_binary( + name = "binary", + srcs = ["Main.hs"], + deps = [ + ":base", + ":library-a", + ":library-b", + ], + ) + +We want to configure ghcide to provide IDE integration for all these three +targets. Start by defining a ``haskell_repl`` target as follows:: + + haskell_repl( + name = "hie-bios", + collect_data = False, + deps = [ + ":binary", + # ":library-a", + # ":library-b", + ], + ) + +Note, that ``library-a`` and ``library-b`` do not have to be listed explicitly. +By default haskell_repl will include all transitive dependencies that are not +external dependencies. Refer to the API documentation of `haskell_repl`_ for +details. + +We also disable building runtime dependencies using ``collect_data = False`` as +they are not required for an IDE session. + +You can test if this provides the expected compiler flags by running the +following Bazel command and taking a look at the generated file:: + + bazel build //:hie-bios --output_groups=hie_bios + +Next, we need to hook this up to `hie-bios`_ using the `bios cradle`_. To that +end, define a small shell script named ``.hie-bios`` that looks as follows:: + + #!/usr/bin/env bash + set -euo pipefail + bazel build //:hie-bios --output_groups=hie_bios + cat bazel-bin/hie-bios@hie-bios >"$HIE_BIOS_OUTPUT" + # Make warnings non-fatal + echo -Wwarn >>"$HIE_BIOS_OUTPUT" + +Then configure `hie-bios`_ to use this script in the bios cradle with the +following ``hie.yaml`` file:: + + cradle: + bios: + program: ".hie-bios" + +Now the hie-bios cradle is ready to use. The last step is to install ghcide. +Unfortunately, ghcide has to be compiled with the exact same GHC that you're +using to build your project. The easiest way to do this is in this context is +to build it with Bazel as part of your rules_haskell project. + +First, define a custom stack snapshot that provides the package versions that +ghcide requires based on `ghcide's stack.yaml`_ file. Let's call it +``ghcide-stack-snapshot.yaml``. Copy the ``resolver`` field and turn the +``extra-deps`` field into a ``packages`` field. Then add another entry to +``packages`` for the ghcide library itself:: + + # Taken from ghcide's stack.yaml + resolver: nightly-2019-09-21 + packages: + # Taken from the extra-deps field. + - haskell-lsp-0.21.0.0 + - haskell-lsp-types-0.21.0.0 + - lsp-test-0.10.2.0 + - hie-bios-0.4.0 + - fuzzy-0.1.0.0 + - regex-pcre-builtin-0.95.1.1.8.43 + - regex-base-0.94.0.0 + - regex-tdfa-1.3.1.0 + - shake-0.18.5 + - parser-combinators-1.2.1 + - haddock-library-1.8.0 + - tasty-rerun-1.1.17 + - ghc-check-0.1.0.3 + # Point to the ghcide revision that you would like to use. + - github: digital-asset/ghcide + commit: "39605333c34039241768a1809024c739df3fb2bd" + sha256: "47cca96a6e5031b3872233d5b9ca14d45f9089da3d45a068e1b587989fec4364" + +Then define a dedicated ``stack_snapshot`` for ghcide in your ``WORKSPACE`` +file. In the ``packages`` attribute we expose all dependencies of the ghcide +executable:: + + stack_snapshot( + name = "stackage_ghcide", + # The rules_haskell example project shows how to import libz. + # https://github.com/tweag/rules_haskell/blob/123e3817156f9135dfa44dcb5a796c424df1f436/examples/WORKSPACE#L42-L63 + extra_deps = {"zlib": ["@zlib.hs"]}, + haddock = False, + local_snapshot = "//:ghcide-stack-snapshot.yaml", + packages = [ + "hslogger", + "aeson", + "base", + "binary", + "base16-bytestring", + "bytestring", + "containers", + "cryptohash-sha1", + "data-default", + "deepseq", + "directory", + "extra", + "filepath", + "ghc-check", + "ghc-paths", + "ghc", + "gitrev", + "hashable", + "haskell-lsp", + "haskell-lsp-types", + "hie-bios", + "ghcide", + "optparse-applicative", + "shake", + "text", + "unordered-containers", + ], + ) + +Finally, define a ``haskell_cabal_binary`` target for the ghcide executable +itself. (Unfortunately, ``stack_snapshot`` does not support building +executables):: + + http_archive( + name = "ghcide", + build_file_content = """ + load("@rules_haskell//haskell:cabal.bzl", "haskell_cabal_binary") + haskell_cabal_binary( + name = "ghcide", + srcs = glob(["**"]), + deps = [ + # From build-depends field of executable section in ghcide.cabal + "@stackage_ghcide//:hslogger", + "@stackage_ghcide//:aeson", + "@stackage_ghcide//:base", + "@stackage_ghcide//:binary", + "@stackage_ghcide//:base16-bytestring", + "@stackage_ghcide//:bytestring", + "@stackage_ghcide//:containers", + "@stackage_ghcide//:cryptohash-sha1", + "@stackage_ghcide//:data-default", + "@stackage_ghcide//:deepseq", + "@stackage_ghcide//:directory", + "@stackage_ghcide//:extra", + "@stackage_ghcide//:filepath", + "@stackage_ghcide//:ghc-check", + "@stackage_ghcide//:ghc-paths", + "@stackage_ghcide//:ghc", + "@stackage_ghcide//:gitrev", + "@stackage_ghcide//:hashable", + "@stackage_ghcide//:haskell-lsp", + "@stackage_ghcide//:haskell-lsp-types", + "@stackage_ghcide//:hie-bios", + "@stackage_ghcide//:ghcide", + "@stackage_ghcide//:optparse-applicative", + "@stackage_ghcide//:shake", + "@stackage_ghcide//:text", + "@stackage_ghcide//:unordered-containers", + ], + visibility = ["//visibility:public"], + ) + """, + # Keep these in sync with ghcide-stack-snapshot.yaml + sha256 = "47cca96a6e5031b3872233d5b9ca14d45f9089da3d45a068e1b587989fec4364", + strip_prefix = "ghcide-39605333c34039241768a1809024c739df3fb2bd", + urls = ["https://github.com/digital-asset/ghcide/archive/39605333c34039241768a1809024c739df3fb2bd.tar.gz"], + ) + +You can test if this worked by building and executing ghcide as follows:: + + bazel build @ghcide//:ghcide + bazel-bin/external/ghcide/_install/bin/ghcide + +Write a small shell script to make it easy to invoke ghcide from your editor:: + + #!/usr/bin/env bash + set -euo pipefail + bazel build @ghcide//:ghcide + bazel-bin/external/ghcide/_install/bin/ghcide "$@" + +And, the last step, configure your editor to use ghcide. The upstream +documentation provides `ghcide setup instructions`_ for a few popular editors. +Be sure to configure your editor to invoke the above wrapper script instead of +another instance of `ghcide`. Also note, that if you are using Nix, then you +may need to invoke ghcide within a ``nix-shell``. + +.. _ghcide: https://github.com/digital-asset/ghcide +.. _haskell_repl: https://api.haskell.build/haskell/defs.html#haskell_repl +.. _hie-bios: https://github.com/mpickering/hie-bios +.. _bios cradle: https://github.com/mpickering/hie-bios#bios +.. _ghcide's stack.yaml: https://github.com/digital-asset/ghcide/blob/39605333c34039241768a1809024c739df3fb2bd/stack.yaml +.. _ghcide setup instructions: https://github.com/digital-asset/ghcide#using-with-vs-code + Building Cabal packages ----------------------- diff --git a/ghcide-stack-snapshot.yaml b/ghcide-stack-snapshot.yaml new file mode 100644 index 000000000..a02ec25e6 --- /dev/null +++ b/ghcide-stack-snapshot.yaml @@ -0,0 +1,13 @@ +# Taken from ghcide's stack88.yaml +resolver: nightly-2020-02-13 +packages: + # Taken from the extra-deps field. + - haskell-lsp-0.22.0.0 + - haskell-lsp-types-0.22.0.0 + - lsp-test-0.11.0.1 + - ghc-check-0.3.0.1 + - hie-bios-0.5.0 + # Point to the ghcide revision that you would like to use. + - github: digital-asset/ghcide + commit: "0c9a0961abbeef851b4117e6408f15a6d46eb1f1" + sha256: "fa1f0cfb0357e7bfa6c86076493f038e0ea5fcd75c470473f2ede7e32566cd9a" diff --git a/haskell/repl.bzl b/haskell/repl.bzl index af2d5366b..34fe0feec 100644 --- a/haskell/repl.bzl +++ b/haskell/repl.bzl @@ -25,6 +25,7 @@ load( "deps_HaskellCcLibrariesInfo", "get_cc_libraries", "get_ghci_library_files", + "get_library_files", "haskell_cc_libraries_aspect", "link_libraries", "merge_HaskellCcLibrariesInfo", @@ -222,7 +223,10 @@ def _create_HaskellReplInfo(from_source, from_binary, collect_info): dep_info = dep_info, ) -def _compiler_flags_and_inputs(hs, repl_info, path_prefix = ""): +def _concat(lists): + return [item for l in lists for item in l] + +def _compiler_flags_and_inputs(hs, repl_info, static = False, path_prefix = ""): """Collect compiler flags and inputs. Compiler flags: @@ -238,7 +242,9 @@ def _compiler_flags_and_inputs(hs, repl_info, path_prefix = ""): Args: hs: Haskell context. - args: list of string, output, the arguments to extend. + repl_info: HaskellReplInfo. + static: bool, Whether we're collecting libraries for static RTS. + Contrary to GHCi, ghcide is built as a static executable using the static RTS. path_prefix: string, optional, Prefix for package db paths. Returns: @@ -265,27 +271,24 @@ def _compiler_flags_and_inputs(hs, repl_info, path_prefix = ""): ]) all_libraries = cc_info.linking_context.libraries_to_link.to_list() cc_libraries = get_cc_libraries(cc_libraries_info, all_libraries) - link_libraries( - get_ghci_library_files(hs, cc_libraries_info, cc_libraries), - args, - ) - - # The `-pgmP` argument needs to be quoted. - args.extend([ - '"{}"'.format(arg) - for arg in ghc_cc_program_args( - paths.join(path_prefix, hs.toolchain.cc_wrapper.executable.path), - ) - ]) + if static: + cc_library_files = _concat(get_library_files(hs, cc_libraries_info, cc_libraries)) + else: + cc_library_files = get_ghci_library_files(hs, cc_libraries_info, cc_libraries) + link_libraries(cc_library_files, args) # Add import directories for import_dir in repl_info.load_info.import_dirs.to_list(): args.append("-i" + (import_dir if import_dir else ".")) + if static: + all_library_files = _concat(get_library_files(hs, cc_libraries_info, all_libraries)) + else: + all_library_files = get_ghci_library_files(hs, cc_libraries_info, all_libraries) inputs = depset(transitive = [ repl_info.load_info.source_files, repl_info.dep_info.package_databases, - depset(get_ghci_library_files(hs, cc_libraries_info, all_libraries)), + depset(all_library_files), depset([hs.toolchain.locale_archive] if hs.toolchain.locale_archive else []), ]) @@ -312,6 +315,13 @@ def _create_repl(hs, posix, ctx, repl_info, output): compiler_flags, inputs = _compiler_flags_and_inputs(hs, repl_info, path_prefix = "$RULES_HASKELL_EXEC_ROOT") args.extend(compiler_flags) + args.extend([ + '"{}"'.format(arg) + for arg in ghc_cc_program_args(paths.join( + "$RULES_HASKELL_EXEC_ROOT", + hs.toolchain.cc_wrapper.executable.path, + )) + ]) # Load source files # Force loading by source with `:add *...`. @@ -398,7 +408,8 @@ def _create_hie_bios(hs, posix, ctx, repl_info): List of providers: OutputGroupInfo provider for the hie-bios argument file. """ - args, inputs = _compiler_flags_and_inputs(hs, repl_info) + args, inputs = _compiler_flags_and_inputs(hs, repl_info, static = True) + args.extend(ghc_cc_program_args(hs.toolchain.cc_wrapper.executable.path)) args.extend(hs.toolchain.compiler_flags) args.extend(repl_info.load_info.compiler_flags) diff --git a/hie.yaml b/hie.yaml new file mode 100644 index 000000000..26ea7d20d --- /dev/null +++ b/hie.yaml @@ -0,0 +1 @@ +cradle: {bios: {program: ".hie-bios"}} diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index af6413f25..f46f759c4 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -10,6 +10,7 @@ load( "haskell_binary", "haskell_doc", "haskell_library", + "haskell_repl", ) load( "//haskell:doctest.bzl", @@ -386,3 +387,25 @@ haskell_doc( tags = ["requires_lz4"], deps = [":utils"], ) + +haskell_repl( + name = "hie-bios", + collect_data = False, + deps = [ + "//tests:run-tests", + ], +) + +sh_test( + name = "ghcide-smoke-test", + srcs = ["@ghcide"], + args = ["--version"], + tags = [ + # Building ghcide fails in profiling mode with: + # + # exe/Rules.hs:106:28: fatal: + # cannot find object file ‘/run/user/1000/tmp7b54rlve/build/ghcide/ghcide-tmp/Util.dyn_o’ + # while linking an interpreted expression + "requires_dynamic", + ], +) diff --git a/tests/RunTests.hs b/tests/RunTests.hs index afb286397..222f4a759 100644 --- a/tests/RunTests.hs +++ b/tests/RunTests.hs @@ -67,6 +67,10 @@ main = hspec $ do it "allows to manually load modules" $ do assertSuccess (bazel ["run", "//tests/multi_repl:c_multi_repl", "--", "-ignore-dot-ghci", "-e", ":load BC.C", "-e", "c"]) + describe "ghcide" $ do + it "loads RunTests.hs" $ + assertSuccess (Process.proc "./.ghcide" ["tests/RunTests.hs"]) + describe "failures" $ do -- Make sure not to include haskell_repl (@repl) or alias (-repl) targets -- in the query. Those would not fail under bazel test.