about summary refs log tree commit diff
path: root/third_party/bazel/rules_haskell/docs/haskell-use-cases.rst
blob: a8c4340cf70f49562ba8555505cbeeccb311dcf5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
.. _use-cases:

Common Haskell Build Use Cases
==============================

Picking a compiler
------------------

Unlike Bazel's native C++ rules, rules_haskell does not auto-detect
a Haskell compiler toolchain from the environment. This is by design.
We require that you declare a compiler to use in your ``WORKSPACE``
file.

There are two common sources for a compiler. One is to use the
official binary distributions from `haskell.org`_. This is done using
the `ghc_bindist`_ rule.

The compiler can also be pulled from Nixpkgs_, a set of package
definitions for the `Nix package manager`_. Pulling the compiler from
Nixpkgs makes the build more hermetic, because the transitive closure
of the compiler and all its dependencies is precisely defined in the
``WORKSPACE`` file. Use `rules_nixpkgs`_ to do so (where ``X.Y.Z``
stands for any recent release)::

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

  http_archive(
      name = "io_tweag_rules_nixpkgs",
      strip_prefix = "rules_nixpkgs-X.Y.Z",
      urls = ["https://github.com/tweag/rules_nixpkgs/archive/vX.Y.Z.tar.gz"],
  )

  load(
      "@io_tweag_rules_nixpkgs//nixpkgs:nixpkgs.bzl",
      "nixpkgs_git_repository",
      "nixpkgs_package"
  )

  nixpkgs_git_repository(
      name = "nixpkgs",
      revision = "18.09", # Any tag or commit hash
  )

  nixpkgs_package(
      name = "ghc",
      repositories = { "nixpkgs": "@nixpkgs//:default.nix" }
      attribute_path = "haskell.compiler.ghc843", # Any compiler version
      build_file = "@io_tweag_rules_haskell//haskell:ghc.BUILD",
  )

  register_toolchains("//:ghc")

This workspace description specifies which Nixpkgs version to use,
then exposes a Nixpkgs package containing the GHC compiler. The
description assumes that there exists a ``BUILD`` file at the root of
the repository that includes the following::

  haskell_toolchain(
    name = "ghc",
    # Versions here and in WORKSPACE must match.
    version = "8.4.3",
    # Use binaries from @ghc//:bin to define //:ghc toolchain.
    tools = ["@ghc//:bin"],
  )

.. _Bazel+Nix blog post: https://www.tweag.io/posts/2018-03-15-bazel-nix.html
.. _Nix package manager: https://nixos.org/nix
.. _Nixpkgs: https://nixos.org/nixpkgs/manual/
.. _ghc_bindist: http://api.haskell.build/haskell/ghc_bindist.html#ghc_bindist
.. _haskell.org: https://haskell.org
.. _haskell_binary: http://api.haskell.build/haskell/haskell.html#haskell_binary
.. _haskell_library: http://api.haskell.build/haskell/haskell.html#haskell_library
.. _rules_nixpkgs: https://github.com/tweag/rules_nixpkgs

Loading targets in a REPL
-------------------------

Rebuilds are currently not incremental *within* a binary or library
target (rebuilds are incremental across targets of course). Any change
in any source file will trigger a rebuild of all source files listed
in a target. In Bazel, it is conventional to decompose libraries into
small units. In this way, libraries require less work to rebuild.
Still, for interactive development full incrementality and fast
recompilation times are crucial for a good developer experience. We
recommend making all development REPL-driven for fast feedback when
source files change.

Every `haskell_binary`_ and every `haskell_library`_ target has an
optional executable output that can be run to drop you into an
interactive session. If the target's name is ``foo``, then the REPL
output is called ``foo@repl``.

Consider the following binary target::

  haskell_binary(
      name = "hello",
      srcs = ["Main.hs", "Other.hs"],
      deps = ["//lib:some_lib"],
  )

The target above also implicitly defines ``hello@repl``. You can call
the REPL like this (requires Bazel 0.15 or later)::

  $ bazel run //:hello@repl

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.

Building code with Hackage dependencies (using Nix)
---------------------------------------------------

Each Haskell library or binary needs a simple build description to
tell Bazel what source files to use and what the dependencies are, if
any. Packages on Hackage don't usually ship with `BUILD.bazel` files.
So if your code depends on them, you either need to write a build
description for each package, generate one (see next section), or
decide not to use Bazel to build packages published on Hackage. This
section documents one way to do the latter.

Nix is a package manager. The set of package definitions is called
Nixpkgs. This repository contains definitions for most actively
maintained Cabal packages published on Hackage. Where these packages
depend on system libraries like zlib, ncurses or libpng, Nixpkgs also
contains package descriptions for those, and declares those as
dependencies of the Cabal packages. Since these definitions already
exist, we can reuse them instead of rewriting these definitions as
build definitions in Bazel. See the `Bazel+Nix blog post`_ for a more
detailed rationale.

To use Nixpkgs in Bazel, we need `rules_nixpkgs`_. See `Picking
a compiler`_ for how to import Nixpkgs rules into your workspace and
how to use a compiler from Nixpkgs. To use Cabal packages from
Nixpkgs, replace the compiler definition with the following::

  nixpkgs_package(
      name = "ghc",
      repositories = { "nixpkgs": "@nixpkgs//:default.nix" },
      nix_file = "//:ghc.nix",
      build_file = "@io_tweag_rules_haskell//haskell:ghc.BUILD",
  )

This definition assumes a ``ghc.nix`` file at the root of the
repository. In this file, you can use the Nix expression language to
construct a compiler with all the packages you depend on in scope::

  with (import <nixpkgs> {});

  haskellPackages.ghcWithPackages (p: with p; [
    containers
    lens
    text
  ])

Each package mentioned in ``ghc.nix`` can then be imported using
`haskell_toolchain_library`_ in ``BUILD`` files.

.. _haskell_toolchain_library: http://api.haskell.build/haskell/haskell.html#haskell_toolchain_library

Building code with Hackage dependencies (using Hazel)
-----------------------------------------------------

.. todo::

   Explain how to use Hazel instead of Nix

Generating API documentation
----------------------------

The `haskell_doc`_ rule can be used to build API documentation for
a given library (using Haddock). Building a target called
``//my/pkg:mylib_docs`` would make the documentation available at
``bazel-bin/my/pkg/mylib_docs/index/index.html``.

Alternatively, you can use the
``@io_tweag_rules_haskell//haskell:haskell.bzl%haskell_doc_aspect``
aspect to ask Bazel from the command-line to build documentation for
any given target (or indeed all targets), like in the following:

.. code-block:: console

  $ bazel build //my/pkg:mylib \
      --aspects @io_tweag_rules_haskell//haskell:haskell.bzl%haskell_doc_aspect

.. _haskell_doc: http://api.haskell.build/haskell/haddock.html#haskell_doc

Linting your code
-----------------

The `haskell_lint`_ rule does not build code but runs the GHC
typechecker on all listed dependencies. Warnings are treated as
errors.

Alternatively, you can directly check a target using

.. code-block:: console

  $ bazel build //my/haskell:target \
      --aspects @io_tweag_rules_haskell//haskell:haskell.bzl%haskell_lint_aspect

.. _haskell_lint: http://api.haskell.build/haskell/lint.html#haskell_lint

Checking code coverage
----------------------

"Code coverage" is the name given to metrics that describe how much source 
code is covered by a given test suite.  A specific code coverage metric 
implemented here is expression coverage, or the number of expressions in 
the source code that are explored when the tests are run.

Haskell's ``ghc`` compiler has built-in support for code coverage analysis, 
through the hpc_ tool. The Haskell rules allow the use of this tool to analyse 
``haskell_library`` coverage by ``haskell_test`` rules. To do so, you have a 
few options. You can add 
``expected_covered_expressions_percentage=<some integer between 0 and 100>`` to
the attributes of a ``haskell_test``, and if the expression coverage percentage
is lower than this amount, the test will fail. Alternatively, you can add
``expected_uncovered_expression_count=<some integer greater or equal to 0>`` to
the attributes of a ``haskell_test``, and instead the test will fail if the
number of uncovered expressions is greater than this amount. Finally, you could
do both at once, and have both of these checks analyzed by the coverage runner.
To see the coverage details of the test suite regardless of if the test passes
or fails, add ``--test_output=all`` as a flag when invoking the test, and there 
will be a report in the test output. You will only see the report if you
required a certain level of expression coverage in the rule attributes.

For example, your BUILD file might look like this: ::

  haskell_library(
    name = "lib",
    srcs = ["Lib.hs"],
    deps = [
        "//tests/hackage:base",
    ],
  )

  haskell_test(
    name = "test",
    srcs = ["Main.hs"],
    deps = [
        ":lib",
        "//tests/hackage:base",
    ],
    expected_covered_expressions_percentage = 80,
    expected_uncovered_expression_count = 10,
  )

And if you ran ``bazel coverage //somepackage:test --test_output=all``, you 
might see a result like this: ::

  INFO: From Testing //somepackage:test:
  ==================== Test output for //somepackage:test:
  Overall report
  100% expressions used (9/9)
  100% boolean coverage (0/0)
      100% guards (0/0)
      100% 'if' conditions (0/0)
      100% qualifiers (0/0)
  100% alternatives used (0/0)
  100% local declarations used (0/0)
  100% top-level declarations used (3/3)
  =============================================================================

Here, the test passes because it actually has 100% expression coverage and 0
uncovered expressions, which is even better than we expected on both counts.

There is an optional ``haskell_test`` attribute called
``strict_coverage_analysis``, which is a boolean that changes the coverage
analysis such that even having better coverage than expected fails the test.
This can be used to enforce that developers must upgrade the expected test
coverage when they improve it. On the other hand, it requires changing the
expected coverage for almost any change.

There a couple of notes regarding the coverage analysis functionality:

- Coverage analysis currently is scoped to all source files and all
  locally-built Haskell dependencies (both direct and transitive) for a given
  test rule.
- Coverage-enabled build and execution for ``haskell_test`` targets may take
  longer than regular. However, this has not effected regular ``run`` /
  ``build`` / ``test`` performance.

.. _hpc: <http://hackage.haskell.org/package/hpc>