Skip to main content

Using Nix Flakes with Bazel

Nix Flakes are a new feature of Nix 2.4 (still prerelease as of this writing) to make Nix derivations composable and can be used to distribute derivations to other people.

This is not a post about what Nix Flakes are though and I recommend reading the series from Tweag (first here) to get up to speed if you haven’t already.

Support for flakes is of course not very widespread. This is a very short explanation of how I bridged the gap between Nix Flakes and Bazel on a project of mine. It’s not complicated but not exactly obvious at first glance.


A simple development environment

First, we create a simple flake.nix at the root of the project. It uses flake-utils to easily create environments for various systems, e.g. x86_64-linux. We take Bazel from a different version of Nixpkgs since it was broken on nixpkgs-unstable last time I tried.

flake.nix
{
  description = "Development environment";

  inputs.nixpkgs.url = github:NixOS/nixpkgs/nixpkgs-unstable;
  inputs.nixpkgs-bazel.url = github:NixOS/nixpkgs/20.09;
  inputs.flake-utils.url = github:numtide/flake-utils;

  outputs = { self, nixpkgs, nixpkgs-bazel, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        overlays = {
          haskell = import ./nix/haskell/overlay.nix;
        };

        pkgs = builtins.foldl'
          (acc: overlay: acc.extend overlay)
          nixpkgs.legacyPackages.${system}
          (builtins.attrValues overlays);

      in
      {
        inherit overlays; # Needed by `bazel-nixpkgs.nix` later on.

        devShell = import ./nix/shell.nix {
          inherit pkgs;
          nixpkgs-bazel = nixpkgs-bazel.legacyPackages.${system};
        };
      }
    );
}

The "old" nixpkgs (as in import <nixpkgs> {}) is available at attributes such as nixpkgs.legacyPackages.x86_64-linux where nixpkgs here is a flake used as an input to our flake.

Just for the sake of demonstration, we define a simple Nix overlay which overrides ghc from nixpkgs with a different derivation.

nix/haskell/overlay.nix
nixpkgsSelf: nixpkgsSuper:

{
  ghc = nixpkgsSuper.ghcWithPackages (hsPkgs: with hsPkgs; [
    aeson
  ]);
}

In order let Bazel know that the Nix environment depends on this file, we can create BUILD.bazel files in nix/haskell/ with the following rule. This is used later on in WORKSPACE.

nix/haskell/BUILD.bazel
exports_files(glob(["**"]))

Next, we can define our development environment as before. I like to put it in nix/shell.nix so (almost) all Nix code ends up in nix/.

nix/shell.nix

{ pkgs, nixpkgs-bazel }:

pkgs.mkShell {
  buildInputs = [
    nixpkgs-bazel.bazel
    pkgs.python3

    # Just to show overlays.
    pkgs.ghc
  ];
}

Running nix develop should drop you into a shell with Bazel and Python available. Since it’s the first time it runs, another file flake.lock should be generated with the pinned versions of inputs.


The bridge

rules_nixpkgs doesn’t have native support for Nix Flakes so we need to bridge the gap with a simple file that exposes the same nixpkgs as in the flake. We also want any overlay to be applied so Bazel uses the correct version of dependencies.

bazel-nixpkgs.nix
{ ... }:

let
  flake = builtins.getFlake (toString ./.);

  baseNixpkgs = flake.inputs.nixpkgs.legacyPackages.${builtins.currentSystem};
  overlays = flake.overlays.${builtins.currentSystem};

  nixpkgs = builtins.foldl'
    (acc: overlay: acc.extend overlay)
    baseNixpkgs
    (builtins.attrValues overlays);

in
nixpkgs

Bazel workspace

Finally, we can use rules_nixpkgs with our bridge as if we were not using flakes at all.

WORKSPACE
workspace(name = "myproject")

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

http_archive(
    name = "rules_nixpkgs",
    sha256 = "7aee35c95251c1751e765f7da09c3bb096d41e6d6dca3c72544781a5573be4aa",
    strip_prefix = "rules_nixpkgs-0.8.0",
    urls = ["https://github.com/tweag/rules_nixpkgs/archive/v0.8.0.tar.gz"],
)

load("@rules_nixpkgs//nixpkgs:repositories.bzl", "rules_nixpkgs_dependencies")

rules_nixpkgs_dependencies()

http_archive(
    name = "rules_haskell",
    sha256 = "b4e2c00da9bc6668fa0404275fecfdb31beb700abdba0e029e74cacc388d94d6",
    strip_prefix = "rules_haskell-0.13",
    urls = ["https://github.com/tweag/rules_haskell/archive/v0.13.tar.gz"],
)

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

rules_haskell_dependencies()

load(
    "@io_tweag_rules_nixpkgs//nixpkgs:nixpkgs.bzl",
    "nixpkgs_local_repository",
    "nixpkgs_python_configure",
)

# This loads Nixpkgs from `bazel-nixpkgs.nix` so it is the same version as the
# input of the flake with overlays applied.
nixpkgs_local_repository(
    name = "nixpkgs",
    nix_file = "//:bazel-nixpkgs.nix",
    nix_file_deps = [
        "//:flake.nix",
        "//:flake.lock",
        "//nix/haskell:overlay.nix",
    ],
)

# The Python executable used by Bazel is built by Nix and is therefore pinned.
nixpkgs_python_configure(
    repository = "@nixpkgs",
)

# Same goes for the C compiler.
nixpkgs_cc_configure(
    repository = "@nixpkgs",
)

# Rules for Haskell libraries and executables will be compiled with GHC from our
# Nix overlay.
haskell_register_ghc_nixpkgs(
    attribute_path = "ghc",
    repository = "@nixpkgs",
    version = "8.10.4",
)

As you can see, the key is just that file bazel-nixpkgs.nix which is pretty straightforward. One nice side effect of having this file is also being able to load Nixpkgs in the nix repl by simply typing :l ./bazel-nixpkgs.nix. This trick turned out to be very useful for debugging.