Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for building fully-static binaries #1390

Merged
merged 1 commit into from
Jul 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
32 changes: 30 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 @@ -230,6 +230,34 @@ 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, we need to ensure GHC generates
# position-independent code (PIC). 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 we 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:
args.add("--ghc-option=-fPIC")

if not hs.toolchain.is_windows:
args.add("--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 @@ -359,7 +387,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 @@ -315,7 +316,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
76 changes: 71 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},
cabalopts = {cabalopts},
Expand All @@ -90,7 +91,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 @@ -107,7 +109,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 @@ -162,7 +165,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 @@ -209,6 +214,17 @@ def haskell_register_ghc_nixpkgs(
```

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 avoid 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. This is
required in order to use statically-linked Haskell libraries with GHCi
and Template Haskell.
fully_static_link: True if and only if fully-statically-linked binaries are to be built.
compiler_flags_select: temporary workaround to pass conditional arguments.
See https://github.com/bazelbuild/bazel/issues/9199 for details.
sh_posix_attributes: List of attribute paths to extract standard Unix shell tools from.
Expand All @@ -219,6 +235,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 @@ -237,7 +259,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 Down Expand Up @@ -265,6 +288,49 @@ 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 avoid 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. This is
required in order to use statically-linked Haskell libraries with GHCi
and Template Haskell.
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.")

print("WARNING: 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