Using Nix Flakes with Devbox

Using Nix Flakes with Devbox

In Devbox 0.4.7, we added support for installing packages from Nix flakes. For power users of Nix, this provides more flexibility and customization for your Devbox project. Using the power of flakes, developers can now create their own packages, modify nixpkgs, or install packages from sources outside of the Nix store.

In this post, we'll demonstrate how to use a Nix flake to modify a package from the Nixpkg repository and then use that modified package in our Devbox project. We'll be using the overlay example from our Devbox repo.

What is a Flake?

A flake is a simplified way to package software with Nix that lets you declaratively define dependencies and build instructions. Unlike most Nix files or derivations, flakes have a strict schema for representing your packages and outputs.

The high-level schema looks something like this:

{
	description = "This flake outputs a modified version of Yarn that uses NodeJS 16"
	inputs = {} # A set of the dependencies our flake will use
	outputs = {}: {} # A function that turns our dependencies into packages, apps, and other outputs.
}

Let's go through each part in detail, to show how we will build our modified yarn package.

Inputs

inputs = {
    nixpkgs.url = "nixpkgs/nixos-21.11";
    flake-utils.url = "github:numtide/flake-utils";
  };

Our flake is going to use two inputs to create our packages. We'll define our input by mapping a name to a flake reference that tells Nix where to fetch it:

  1. Nixpkgs is the default repository of Packages for Nix. We define an input name, nixpkgs, and set its URL to "nixpkgs/nixos-21.11"
  2. Flake-utils is a set of functions that make developing Nix Flakes easier. We define a name flake-utils and set its reference to a Github-hosted flake using "github:numtide/flake-utils"

Whenever we build the outputs for our Flake, Nix will pull these inputs from their URL and pin them in a flake.lock file. This file ensures that we will use the same sources every time we build or run the Flake.

With these inputs defined, we can use them in our outputs closure to create the packages we want.

Further Reading

Outputs:

A flake's output is a function that takes a set of inputs, and returns an attribute set of packages, apps, templates, and other outputs that users can reference.

In this case, the input set consists of the following:

{ self, # A reference to the Flake itself
  nixpkgs, #Our nixpkgs input defined in the previous section
  flake-utils, #The flake-utils reference defined in the section above
}

We will now use these inputs to generate an attribute set of outputs.

eachDefaultSystem

Normally flakes require us to specify an output for each system we want to support (e.g., aarch64-darwin or x86_64-linux), but this can be tedious to write. To simplify things, we're going to use the eachDefaultSystem function from flake-utils to generate outputs for the default set of systems:

outputs = {self, nixpkgs, flake utils}:  
	flake-utils.lib.eachDefaultSystem (system:
   # Define out Outuputs here
  );

Now we can focus on the outputs we want to generate. We’ll define a few variables that we want to use in our outputs using a let expression:

  outputs = { self, nixpkgs, flake-utils }:
  flake-utils.lib.eachDefaultSystem (system:
    let
    
	  # Define an overlay function
      overlay = (final: prev: {
        yarn = prev.yarn.override { 
			     nodejs = final.pkgs.nodejs-16_x; 
		       };
      });

	  # Import Nixpkgs with our overlay applied
      pkgs = import nixpkgs {
          inherit system;
          overlays = [ overlay ];
      };

    in {...}
  );

Let’s take each of these definitions in more detail.

Overlay

An overlay is a Nix function that lets us modify a set of packages. You can use overlays to override values in one or more packages within a set like nixpkgs.

An overlay takes the following arguments and returns a modified package set:

  • final: which corresponds to the final package set that we are going to return
  • previous: which corresponds to the previous package set that we are modifying

In the function's closure, we modify packages using override to change the package's value. In this example, we override yarn to use a different version of Node.js:

overlay = (final: prev: {
		        yarn = prev.yarn.override { 
				 		nodejs = final.pkgs.nodejs-16_x;
		        };
          });

Further Reading

Import

This section evaluates nixpkgs and returns a set of packages based on our current system and overlay. We tell nixpkgs to inherit the system from its current scope and apply the overlay from the previous section. We assign the result of this evaluation to the pkgs variable, which we will use for our final output.

    pkgs = import nixpkgs {
        inherit system;
        overlays = [ overlay ];
    };

Further Reading

Our Output Package

Now that we have a modified nixpkgs set, we can output the modified package for our project. We'll use the packages attribute in our Flake to tell Nix that we want to return a package. The code for this is pretty simple:

in {
	packages = {
        yarn = pkgs.yarn;
	};
}

This section means that when someone builds the yarn output of the flake, we will return the yarn package from the pkgs set we defined above.

Further Reading

Putting all the Pieces Together

Our complete flake.nix file should look like this:

{
  description =
    "This flake outputs a modified version of Yarn that uses NodeJS 16";

  inputs = {
    nixpkgs.url = "nixpkgs/nixos-21.11";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        overlay = (final: prev: {
          yarn = prev.yarn.override { nodejs = final.pkgs.nodejs-16_x; };
        });

        # 
        pkgs = import nixpkgs {
          inherit system;
          overlays = [ overlay ];
        };

      in {
        packages = { yarn = pkgs.yarn; };
      });
}

If we want to test the flake, we can run nix build .#yarn. This will generate the yarn output and link the results in our local directory.

Using our Flake in Devbox

To use this yarn package in our devbox.json project, we provide a flake reference that points to our yarn output of our flake:

{
	"packages": [
			"path:yarn-overlay#yarn"
	]
}

Now when we start Devbox, our yarn package will use Node.js 16!


In this example we showed how you can use flakes to modify the default settings of a package from nixpkgs. For more ideas on how to use Nix Flakes, you can check out the flakes examples in our Devbox Repo.

Stay up to Date with Jetify

If you're reading this, we'd love to hear from you about how you've been using Devbox for your projects. You can follow us on Twitter, or chat with our developers live on our Discord Server. We also welcome issues and pull requests on our Github Repo.