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
bazelcommand options.
For this, one needs awareness of four files at the root level of the Daml repository : WORKSPACE, deps.bzl, BUILD and .bazelrc.
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.
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.
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
c2hstoolchain
Lastly, there are some aliases defined here. For example,
alias(
name = "damlc",
actual = "//compiler/damlc"
)
The daml repository is configured to support ghcide with Bazel
and the ghcide executable is provided by the dev-env. Take a look at the
setup section for example configurations for various editors.
ghcide 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 ghcide by
absolute path, or make sure that the dev-env provided ghcide is in $PATH
for your editor.
Note, ghcide 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.
- "Bazel User Guide" (Daml specific)
- "A Users's Guide to Bazel" (official documentation)
rules_haskelldocumentation (core Haskell rules, Haddock support, Linting, Defining toolchains, Support for protocol buffers, Interop withcc_*rules, Workspace rules)