daml/sdk/BAZEL-haskell.md
Gary Verhaegen e40aad897f
move to subdir 3.0 (#18520)
* move most files

* update CI configuration
2024-03-22 02:27:46 +01:00

12 KiB

Haskell in Bazel

Finding your way around our Bazel build system from a Haskell developers' point of view might seem confusing at first. Going beyond just adding targets to BUILD.bazel files requires a more detailed understanding of the system:

  • Where rules come from;
  • How toolchains and external dependencies are defined;
  • Specifiying stock bazel command options.

For this, one needs awareness of four files at the root level of the Daml repository : WORKSPACE, deps.bzl, BUILD and .bazelrc.

.bazelrc the Bazel configuration file

The bazel command accepts many options. To avoid having to specify them manually for every build they can be collected into a .bazelrc file. The root of daml.git contains such a file. There doesn't seem to be anything in ours that is Haskell specific.

WORKSPACE

The root of daml.git is a Bazel "workspace" : there exists a file WORKSPACE. In short, in a WORKSPACE we declare external packages and register toolchains. Visible in a WORKSPACE are the targets of the BUILD.bazel file at the same level as WORKSPACE and any BUILD.bazel files contained in sub-directories of the directory containing WORKSPACE.

Bazel extensions are loaded by a load statement. More or less the first couple of lines of our WORKSPACE reads:

load("//:deps.bzl", "daml_deps")
daml_deps()

Much of the contents of the WORKSPACE file have been factored out into deps.bzl so that other projects can share the definitions contained there. Looking into deps.bzl it begins:

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")

This loads the contents of the files http.bzl and git.bzl from the external workspace bazel_tools into the "environment". bazel_tools is an external workspace builtin to Bazel and provides rules for working with archives and git.

[Note : Confusingly (?), //bazel_tools is a Daml package (a sub-directory of the root package directory containing a BUILD.bazel file). Don't confuse @bazel_tools//.. with //bazel_tools/..].

Straight after the loading of those rules, deps.bzl reads,

http_archive(
  name = "rules_haskell",
  strip_prefix = 'rules_haskell-%s' % rules_haskell_version,
  urls = ["https://github.com/tweag/rules_haskell/archive/%s.tar.gz" % rules_haskell_version],
)

This defines the workspace rules_haskell (we call this "rules_haskell" informally - in short, build rules for Haskell) as an external workspace that is downloaded via http. From here on we can refer to things in that workspace by prefixing them with @rules_haskell as in the next command from WORKSPACE,

load("@rules_haskell//haskell:repositories.bzl", "rules_haskell_dependencies")

which has the effect of making the macro rules_haskell_dependencies available in the environment which provides "all repositories necessary for rules_haskell to function":

rules_haskell_dependencies()

As mentioned earlier, targets of any BUILD.bazel file in a package are visible within WORKSPACE. In fact, its a rule that toolchains can only be defined in BUILD.bazel files and registered in WORKSPACE files. register_toolchains registers a toolchain created with the toolchain rule so that it is available for toolchain resolution.

register_toolchains(
  "//:c2hs-toolchain",
)

Those toolchains are defined in BUILD (we'll skip listing their definitions here).

The GHC toolchain is registered within macros provided by rules_haskell:

haskell_register_ghc_nixpkgs(
    attribute_path = "ghc",
    build_file = "@io_tweag_rules_nixpkgs//nixpkgs:BUILD.pkg",
    compiler_flags = [ ... ],
    ...
    version = "8.6.5",
)
haskell_register_ghc_bindists(
    compiler_flags = common_ghc_flags,
    version = "8.6.5",
) if is_windows else None

On Linux and macOS we import GHC from nixpkgs, while on Windows we download an official bindist.

Rules for importing nix packages are provided in the workspace io_tweag_rules_nixpkgs:

http_archive(
    name = "io_tweag_rules_nixpkgs",
    strip_prefix = "rules_nixpkgs-%s" % rules_nixpkgs_version,
    urls = ["https://github.com/tweag/rules_nixpkgs/archive/%s.tar.gz" % rules_nixpkgs_version],
)
load(
  "@io_tweag_rules_nixpkgs//nixpkgs:nixpkgs.bzl",
  "nixpkgs_local_repository", "nixpkgs_git_repository", "nixpkgs_package", "nixpkgs_cc_configure",
)

nixpkgs_local_repository creates an external repository representing the content of of a Nix package collection, based on Nix expressions stored in files in our //nix directory.

nixpkgs_local_repository(
    name = "nixpkgs",
    nix_file = "//nix:bazel-nixpkgs.nix",
)
nixpkgs_local_repository(
    name = 'dev_env_nix',
    nix_file = '//nix:default.nix',
)

nixpkgs_cc_configure tells Bazel to use compilers and linkers from the Nix package collection for the CC toolchain (overriding auto-detection from the current PATH):

nixpkgs_cc_configure(
    nix_file = "//nix:bazel-cc-toolchain.nix",
    repositories = dev_env_nix_repos,
)

where,

dev_env_nix_repos = {
    "nixpkgs": "@nixpkgs",
    "damlSrc": "@dev_env_nix",
}

Finally, we use the bazel-haskell-deps.bzl file which is loaded from WORKSPACE to define the set of Hackage packages that we want to import into Bazel using the stack_snapshot macro.

stack_snapshot(
    name = "stackage",
    packages = [
        "aeson",
        "aeson-pretty",
        ...
    ],
    vendored_packages = {
        "grpc-haskell-core": "@grpc_haskell_core//:grpc-haskell-core",
        "proto3-suite": "@proto3-suite//:proto3-suite",
    },
    local_snapshot = "//:stack-snapshot.yaml",
    stack_snapshot_json = "//:stackage_snapshot.json",
    flags = {
        "integer-logarithms": ["-integer-gmp"],
        "text": ["integer-simple"],
        ...
    },
    tools = [
        "@alex",
        "@happy",
        ...
    ],
    deps = {
        "digest": ["@com_github_madler_zlib//:libz"],
        "zlib": ["@com_github_madler_zlib//:libz"],
    },

This will generate an external workspace called @stackage that exports all the Hackage packages listed in packages or vendored_packages. We use a custom stack snapshot defined in stack-snapshot.yaml. The items listed in the packages attribute will be fetched using the stack tool as defined in the custom snapshot and will be built using the Cabal library. Additionally, we can provide custom Bazel build definitions for packages using the vendored_packages attribute.

The packages are pinned by the Stackage snapshot, in this case a local_snapshot and in the lock-file defined by stack_snapshot_json. If you wish to update packages, then you need to change the packages and local_snapshot attributes accordingly and afterwards execute the following command on Unix and Windows to update the lock-files:

bazel run @stackage-unpinned//:pin

You can use the ad-hoc Windows machines as described in the release documentation to get access to a Windows machine.

The flags attribute can be used to override default Cabal flags. The tools attribute defines Bazel targets for known Cabal tools, e.g. alex, happy, or c2hs. Finally, the deps attribute can be used to define additional dependencies to individual packages. E.g. the zlib Hackage packages depends on the C library libz.

If you wish to override the version of a package that is fetch from Hackage, or fetch it from a different source such as GitHub at a specific commit, then you should modify the stack-snapshot.yaml file. If, additionally, you wish to patch a package, e.g. to override Cabal version bounds, then you should define a custom Bazel build and add the package to the vendored_packages attribute.

For example, to patch the proto3-suite package add the following snippet to the bazel-haskell-deps.bzl file.

http_archive(
    name = "proto3_suite",
    build_file_content = """
load("@rules_haskell//haskell:cabal.bzl", "haskell_cabal_library")
load("@stackage//:packages.bzl", "packages")
haskell_cabal_library(
    name = "proto3-suite",
    version = "0.4.0.0",
    srcs = glob(["**"]),
    deps = packages["proto3-suite"].deps,
    visibility = ["//visibility:public"],
)
    """,
    patch_args = ["-p1"],
    patches = ["@com_github_digital_asset_daml//bazel_tools:haskell-proto3-suite.patch"],
    sha256 = "6a803b1655824e5bec2c518b39b6def438af26135d631b60c9b70bf3af5f0db2",
    strip_prefix = "proto3-suite-f5ca2bee361d518de5c60b9d05d0f54c5d2f22af",
    urls = ["https://github.com/awakesecurity/proto3-suite/archive/f5ca2bee361d518de5c60b9d05d0f54c5d2f22af.tar.gz"],
)

This will fetch the sources from GitHub at the specified revision and apply the patch located in bazel_tools/haskell-proto3-suite.patch in the daml repository.

BUILD

At the root of the repository, alongside WORKSPACE there exists the top-level package definition file BUILD. The primary purpose of this BUILD file is to define toolchains (but it does a couple of other little things as well).

The directive

package(default_visibility = ["//visibility:public"])

sets the default visibility property globally for our targets as public. This means that our targets can freely be depended upon by other targets.

The load statments

load("@rules_haskell//haskell:defs.bzl",
  "haskell_toolchain", "haskell_toolchain_library",
)
load("@rules_haskell//haskell:c2hs.bzl",
  "c2hs_toolchain",
)

bring the macros haskell_toolchain, haskell_toolchain_library, and c2hs_toolchain into scope from rules_haskell.

haskell_toolchain_library:

  • import a package that is prebuilt outside of Bazel

haskell_toolchain:

  • declare a GHC compiler toolchain

c2hs_toolchain:

  • declare a Haskell c2hs toolchain

Lastly, there are some aliases defined here. For example,

alias(
  name = "damlc",
  actual = "//compiler/damlc"
)

Editor integration

The daml repository is configured to support haskell-language-server with Bazel and the da-hls executable is provided by the dev-env. Take a look at the setup section for example configurations for various editors. haskell-language-server has to be built with the same ghc as the project you're working on. Be sure to either point your editor to the dev-env-provided haskell-language-server by absolute path, or make sure that the dev-env-provided haskell-language-server is in $PATH for your editor.

Note, hls itself is built by Bazel and to load a target into the editor some of its dependencies have to be built by Bazel. This means that start-up may take some time if the required artifacts are not built or cached already.

Also note that the current setup works for modules in the bazel target //compiler/damlc:damlc or in its dependencies. To work on other modules, it should be enough to replace //compiler/damlc:damlc in .hie-bios with the appropriate bazel target and restart the language server.

Further reading: