Skip to content

Commit

Permalink
Add support for building fully-static binaries
Browse files Browse the repository at this point in the history
This commit implements support for fully-statically-linked binaries by
extending functionality linked to the `is_static` attribute of an active
Haskell toolchain. In particular:

* Packages built with Cabal will be passed fields that ensure their
  static artifacts are relocatable (`-fPIC` and
  `-fexternal-dynamic-refs`).

* Intermediate artifacts (such as binaries built by `hsc2hs` for the
  purposes of generating Haskell code) will also be built statically to
  avoid issues caused by the absences of dynamic libraries.

* Linker flags for REPLs will be generated so that they correctly find
  static C library dependencies instead of failing/finding dynamic
  counterparts.
  • Loading branch information
lunaris committed Jul 20, 2020
1 parent 5073048 commit 787bcc4
Show file tree
Hide file tree
Showing 10 changed files with 324 additions and 26 deletions.
171 changes: 169 additions & 2 deletions docs/haskell-use-cases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ workspace in the current working directory with a few dummy build
targets. See the following sections about customizing the workspace.

Making rules_haskell available
----------------------------------
------------------------------

First of all, the ``WORKSPACE`` file must specify how to obtain
rules_haskell. To use a released version, do the following::
Expand Down Expand Up @@ -562,8 +562,175 @@ It is worth noting that Bazel's worker strategy is not sandboxed by default. Thi
confuse our worker relatively easily. Therefore, it is recommended to supply
``--worker_sandboxing`` to ``bazel build`` -- possibly, via your ``.bazelrc.local`` file.

Building fully-statically-linked binaries
-----------------------------------------

Fully-statically linked binaries have no runtime linkage dependencies and are
thus typically more portable and easier to package (e.g. in containers) than
their dynamically-linked counterparts. The trade-off is that
fully-statically-linked binaries can be larger than dynamically-linked binaries,
due to the fact that all symbols must be bundled into a single output.
``rules_haskell`` has support for building fully-statically-linked binaries
using Nix-provisioned GHC toolchains and the ``static_runtime`` and
``fully_static_link`` attributes of the ``haskell_register_ghc_nixpkgs`` macro::

load(
"@rules_haskell//haskell:nixpkgs.bzl",
"haskell_register_ghc_nixpkgs",
)

haskell_register_ghc_nixpkgs(
version = "X.Y.Z",
attribute_path = "staticHaskell.ghc",
repositories = {"nixpkgs": "@nixpkgs"},
static_runtime = True,
fully_static_link = True,
)

Note that the ``attribute_path`` must refer to a GHC derivation capable of
building fully-statically-linked binaries. Often this will require you to
customise a GHC derivation in your Nix package set. If you are unfamiliar with
Nix, one way to add such a custom package to an existing set is with an
*overlay*. Detailed documentation on overlays is available at
https://nixos.wiki/wiki/Overlays, but for the purposes of this documentation,
it's enough to know that overlays are essentially functions which accept package
sets (conventionally called ``super``) and produce new package sets. We can
write an overlay that modifies the ``ghc`` derivation in its argument to add
flags that allow it to produce fully-statically-linked binaries as follows::

let
# Pick a version of Nixpkgs that we will base our package set on (apply an
# overlay to).
baseCommit = "..."; # Pick a Nixpkgs version to pin to.
baseSha = "..."; # The SHA of the above version.

baseNixpkgs = builtins.fetchTarball {
name = "nixos-nixpkgs";
url = "https://github.com/NixOS/nixpkgs/archive/${baseCommit}.tar.gz";
sha256 = baseSha;
};

# Our overlay. We add a `staticHaskell.ghc` path matching that specified in
# the haskell_register_ghc_nixpkgs rule above which overrides the `ghc`
# derivation provided in the base set (`super.ghc`) with some necessary
# arguments.
overlay = self: super: {
staticHaskell = {
ghc = (super.ghc.override {
enableRelocatedStaticLibs = true;
enableShared = false;
}).overrideAttrs (oldAttrs: {
preConfigure = ''
${oldAttrs.preConfigure or ""}
echo "GhcLibHcOpts += -fPIC -fexternal-dynamic-refs" >> mk/build.mk
echo "GhcRtsHcOpts += -fPIC -fexternal-dynamic-refs" >> mk/build.mk
'';
});
};
};

in
args@{ overlays ? [], ... }:
import baseNixpkgs (args // {
overlays = [overlay] ++ overlays;
})

In this example we use the ``override`` and ``overrideAttrs`` functions to
produce a GHC derivation suitable for our needs. Ideally,
``enableRelocatedStaticLibs`` and ``enableShared`` should be enough, but
upstream Nixpkgs does not at present reliably pass ``-fexternal-dynamic-refs``
when ``-fPIC`` is passed, which is required to generate fully-statically-linked
executables.

You may wish to base your GHC derivation on one which uses Musl, a C library
designed for static linking (unlike glibc, which can cause issues when linked
statically). `static-haskell-nix`_ is an example of a project which provides
such a GHC derivation and can be used like so::

let
baseCommit = "..."; # Pick a Nixpkgs version to pin to.
baseSha = "..."; # The SHA of the above version.

staticHaskellNixCommit = "..."; Pick a static-haskell-nix version to pin to.

baseNixpkgs = builtins.fetchTarball {
name = "nixos-nixpkgs";
url = "https://github.com/NixOS/nixpkgs/archive/${baseCommit}.tar.gz";
sha256 = baseSha;
};

staticHaskellNixpkgs = builtins.fetchTarball
"https://github.com/nh2/static-haskell-nix/archive/${staticHaskellNixCommit}.tar.gz";

# The `static-haskell-nix` repository contains several entry points for e.g.
# setting up a project in which Nix is used solely as the build/package
# management tool. We are only interested in the set of packages that underpin
# these entry points, which are exposed in the `survey` directory's
# `approachPkgs` property.
staticHaskellPkgs = (
import (staticHaskellNixpkgs + "/survey/default.nix") {}
).approachPkgs;

overlay = self: super: {
staticHaskell = staticHaskellPkgs.extend (selfSH: superSH: {
ghc = (superSH.ghc.override {
enableRelocatedStaticLibs = true;
enableShared = false;
}).overrideAttrs (oldAttrs: {
preConfigure = ''
${oldAttrs.preConfigure or ""}
echo "GhcLibHcOpts += -fPIC -fexternal-dynamic-refs" >> mk/build.mk
echo "GhcRtsHcOpts += -fPIC -fexternal-dynamic-refs" >> mk/build.mk
'';
});
});
};

in
args@{ overlays ? [], ... }:
import baseNixpkgs (args // {
overlays = [overlay] ++ overlays;
})

If you adopt a Musl-based GHC you should also take care to ensure that the C
toolchain used by ``rules_haskell`` also uses Musl; you can do this using the
``nixpkgs_cc_configure`` rule from ``rules_nixpkgs`` and providing a Nix
expression that supplies appropriate ``cc`` and ``binutils`` derivations::

nixpkgs_cc_configure(
repository = "@nixpkgs",

# The `staticHaskell` attribute in the previous example exposes the
# Musl-backed `cc` and `binutils` derivations already, so it's just a
# matter of exposing them to nixpkgs_cc_configure.
nix_file_content = """
with import <nixpkgs> { config = {}; overlays = []; }; buildEnv {
name = "bazel-cc-toolchain";
paths = [ staticHaskell.stdenv.cc staticHaskell.binutils ];
}
""",
)

With the toolchain taken care of, you can then create fully-statically-linked
binaries by passing the ``-optl-static`` and ``-optl-pthread`` flags as
``compiler_flags`` to GHC, e.g. in ``haskell_binary``::

haskell_binary(
name = ...,
srcs = [
...,
],
...,
compiler_flags = [
"-optl-static",
"-optl-pthread",
],
)

.. _static-haskell-nix: https://github.com/nh2/static-haskell-nix

Containerization with rules_docker
-------------------------------------
----------------------------------

Making use of both ``rules_docker`` and ``rules_nixpkgs``, it's possible to containerize
``rules_haskell`` ``haskell_binary`` build targets for deployment. In a nutshell, first we must use
Expand Down
31 changes: 29 additions & 2 deletions haskell/cabal.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def _prepare_cabal_inputs(
dynamic = True,
)

# Executables build by Cabal will link Haskell libraries statically, so we
# Executables built by Cabal will link Haskell libraries statically, so we
# only need to include dynamic C libraries in the runfiles tree.
(_, runfiles_libs) = get_library_files(
hs,
Expand Down Expand Up @@ -226,6 +226,33 @@ def _prepare_cabal_inputs(
],
uniquify = True,
)

# When building in a static context, we need to make sure that Cabal passes
# a couple of options that ensure any static code it builds can be linked
# correctly.
#
# * If we are using a static runtime, then on Unix we need to pass GHC both
# `-fPIC` and `-fexternal-dynamic-refs`. With `-fPIC` alone, GHC will
# generate `R_X86_64_PC32` relocations on Unix, which prevent loading its
# static libraries as PIC.
#
# * If are building fully-statically-linked binaries, we need to ensure that
# we pass arguments to `hsc2hs` such that objects it builds are
# statically linked, otherwise we'll get dynamic linking errors when
# trying to execute those objects to generate code as part of the build.
# Since the static configuration should ensure that all the objects
# involved are themselves statically built, this is just a case of passing
# `-static` to the linker used by `hsc2hs` (which will be our own wrapper
# script which eventually calls `gcc`, etc.).
if hs.toolchain.static_runtime and not hs.toolchain.is_windows:
args.add_all([
"--ghc-option=-fPIC",
"--ghc-option=-fexternal-dynamic-refs",
])

if hs.toolchain.fully_static_link:
args.add("--hsc2hs-option=--lflag=-static")

args.add("--")
args.add_all(package_databases, map_each = _dirname, format_each = "--package-db=%s")
args.add_all(direct_include_dirs, format_each = "--extra-include-dirs=%s")
Expand Down Expand Up @@ -349,7 +376,7 @@ def _haskell_cabal_library_impl(ctx):
else:
profiling_library = None
static_library = vanilla_library
if hs.toolchain.is_static:
if hs.toolchain.static_runtime:
dynamic_library = None
else:
dynamic_library = hs.actions.declare_file(
Expand Down
9 changes: 7 additions & 2 deletions haskell/ghc_bindist.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,8 @@ haskell_toolchain(
tools = [":bin"],
libraries = toolchain_libraries,
version = "{version}",
is_static = {is_static},
static_runtime = {static_runtime},
fully_static_link = {fully_static_link},
compiler_flags = {compiler_flags},
haddock_flags = {haddock_flags},
repl_ghci_args = {repl_ghci_args},
Expand All @@ -314,7 +315,11 @@ haskell_toolchain(
""".format(
toolchain_libraries = toolchain_libraries,
version = ctx.attr.version,
is_static = os == "windows",
static_runtime = os == "windows",

# At present we don't support fully-statically-linked binaries with GHC
# bindists.
fully_static_link = False,
compiler_flags = ctx.attr.compiler_flags,
haddock_flags = ctx.attr.haddock_flags,
repl_ghci_args = ctx.attr.repl_ghci_args,
Expand Down
62 changes: 57 additions & 5 deletions haskell/nixpkgs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ haskell_toolchain(
tools = {tools},
libraries = toolchain_libraries,
version = "{version}",
is_static = {is_static},
static_runtime = {static_runtime},
fully_static_link = {fully_static_link},
compiler_flags = {compiler_flags} + {compiler_flags_select},
haddock_flags = {haddock_flags},
repl_ghci_args = {repl_ghci_args},
Expand All @@ -89,7 +90,8 @@ haskell_toolchain(
toolchain_libraries = toolchain_libraries,
tools = ["@rules_haskell_ghc_nixpkgs//:bin"],
version = repository_ctx.attr.version,
is_static = repository_ctx.attr.is_static,
static_runtime = repository_ctx.attr.static_runtime,
fully_static_link = repository_ctx.attr.fully_static_link,
compiler_flags = repository_ctx.attr.compiler_flags,
compiler_flags_select = compiler_flags_select,
haddock_flags = repository_ctx.attr.haddock_flags,
Expand All @@ -105,7 +107,8 @@ _ghc_nixpkgs_haskell_toolchain = repository_rule(
# These attributes just forward to haskell_toolchain.
# They are documented there.
"version": attr.string(),
"is_static": attr.bool(),
"static_runtime": attr.bool(),
"fully_static_link": attr.bool(),
"compiler_flags": attr.string_list(),
"compiler_flags_select": attr.string_list_dict(),
"haddock_flags": attr.string_list(),
Expand Down Expand Up @@ -159,7 +162,9 @@ _ghc_nixpkgs_toolchain = repository_rule(_ghc_nixpkgs_toolchain_impl)

def haskell_register_ghc_nixpkgs(
version,
is_static = False,
is_static = None, # DEPRECATED. See _check_static_attributes_compatibility.
static_runtime = None,
fully_static_link = None,
build_file = None,
build_file_content = None,
compiler_flags = None,
Expand Down Expand Up @@ -215,6 +220,12 @@ def haskell_register_ghc_nixpkgs(
haskell_toolchain_repo_name = "rules_haskell_ghc_nixpkgs_haskell_toolchain"
toolchain_repo_name = "rules_haskell_ghc_nixpkgs_toolchain"

static_runtime, fully_static_link = _check_static_attributes_compatibility(
is_static = is_static,
static_runtime = static_runtime,
fully_static_link = fully_static_link,
)

# The package from the system.
nixpkgs_package(
name = nixpkgs_ghc_repo_name,
Expand All @@ -233,7 +244,8 @@ def haskell_register_ghc_nixpkgs(
_ghc_nixpkgs_haskell_toolchain(
name = haskell_toolchain_repo_name,
version = version,
is_static = is_static,
static_runtime = static_runtime,
fully_static_link = fully_static_link,
compiler_flags = compiler_flags,
compiler_flags_select = compiler_flags_select,
haddock_flags = haddock_flags,
Expand All @@ -260,6 +272,46 @@ def haskell_register_ghc_nixpkgs(
**sh_posix_nixpkgs_kwargs
)

def _check_static_attributes_compatibility(is_static, static_runtime, fully_static_link):
"""Asserts that attributes passed to `haskell_register_ghc_nixpkgs` for
controlling use of GHC's static runtime and whether or not to build
fully-statically-linked binaries are compatible.
Args:
is_static: Deprecated. The functionality it previously gated
(supporting GHC versions with static runtime systems) now sits under
static_runtime, a name chosen to avoiding confusion with the new flag
fully_static_link, which controls support for fully-statically-linked
binaries. During the deprecation period, we rewrite is_static to
static_runtime in this macro as long as the new attributes aren't also
used. This argument and supporting code should be removed in a future release.
static_runtime: True if and only if a static GHC runtime is to be used.
fully_static_link: True if and only if fully-statically-linked binaries are to be built.
Returns:
A tuple of static_runtime, fully_static_link attributes, which are guaranteed
not to be None, taking into account the deprecated is_static argument.
"""

# Check for use of the deprecated `is_static` attribute.
if is_static != None:
if static_runtime != None or fully_static_link != None:
fail("is_static is deprecated. Please use the static_runtime attribute instead.")

static_runtime = is_static

# Currently we do not support the combination of a dynamic runtime system
# and fully-statically-linked binaries, so fail if this has been selected.
if not static_runtime and fully_static_link:
fail(
"""\
Fully-statically-linked binaries with a dynamic runtime are not currently supported.
Please pass static_runtime = True if you wish to build fully-statically-linked binaries.
"""
)

return bool(static_runtime), bool(fully_static_link)

def _find_children(repository_ctx, target_dir):
find_args = [
"find",
Expand Down
Loading

0 comments on commit 787bcc4

Please sign in to comment.