Image for post extending-nixos-configurations
Mar 31, 2023 by Ana Hobden

Extending NixOS configurations

NixOS modules and configurations offer us a tantalizing way to express and share systems. My friends and I can publish our own flakes containing nixosModules and/or nixosConfigurations outputs which can be imported, reused, and remixed. When it comes to secret projects though, that openness and ease of sharing can be a bit of a problem.

Let’s pretend my friend wants to share a secret NixOS module or package with me, they’ve already given me access to the GitHub repository, and now I need to add it to my flake. My flake is public and has downstream users. I can’t just up and add it as an input. For one thing, it’d break everything downstream. More importantly, my friend asked me not to.

It’s terribly inconvenient to add the project as an input and be careful to never commit that change to the repository. Worse, if I did screw up and commit it, my friend might be disappointed in me. We just can’t have that.

Let’s explore how to create a flake which extends some existing flake, a pattern which can be combined with a git+ssh flake URL to resolve this precarious situation.

This same situation strategy can be applied to other outputs of a flake, and can be combined with nixpkgs.lib.makeOverridable.

Extending a Flake #

Let’s assume for a moment we have a simple flake called original with a nixosConfiguration and a nixosModule:

# original/flake.nix
{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11";
  };

  outputs = { self, nixpkgs }: {
    nixosModules.default = { config, pkgs, lib, ... }: {
      # Create a file `/etc/original-marker`
      environment.etc.original-marker.text = "Hello!";

      # You can ignore these, just keeping things small
      boot.initrd.includeDefaultModules = false;
      documentation.man.enable = false;
      boot.loader.grub.enable = false;
      fileSystems."/".device = "/dev/null";
      system.stateVersion = "22.11";
    };

    nixosConfigurations.teapot = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        self.nixosModules.default
      ];
    };
  };
}

While our little demo configuration, teapot, might not be a very practical system for hardware (or even a VM) it’s a perfectly valid NixOS expression which we can build and inspect the resultant output filesystem of:

extension-demo/original
❯ nix build .#nixosConfigurations.teapot.config.system.build.toplevel

extension-demo/original
❯ cat result/etc/original-marker
6ello!

Now, let’s assume there exists some other flake, called extension that looks like this:

# extension/flake.nix
{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11";
  };

  outputs = { self, nixpkgs }: {
    nixosModules.default = { config, pkgs, lib, ... }: {
      # Create a file `/etc/extension-marker`
      environment.etc.extension-marker.text = "Hi!";
    };
  };
}

Our goal is to create a teapot with the extension.nixosModules.default module included.

To do that, we’ll create a new flake, called extended which looks like this:

# extended/flake.nix
{
  inputs = {
    original.url = "/home/ana/git/extension-demo/original";
    nixpkgs.follows = "original/nixpkgs";
    extension = {
      url = "/home/ana/git/extension-demo/extension";
      inputs.nixpkgs.follows = "original/nixpkgs";
    };
  };

  outputs = { self, nixpkgs, original, extension }:
    original.outputs // {
    nixosConfigurations.teapot =
      original.nixosConfigurations.teapot.extendModules {
        modules = [
          extension.nixosModules.default
        ];
      };
  };
}

Because of outputs = { self, nixpkgs, original, extension }: original.outputs // { /* ... */ } this extended flake has the same outputs of original, plus whatever overrides we add inside { /* ... */ }.

extension-demo/extended
❯ nix flake show
path:/home/ana/git/extension-demo/extended?lastModified=1680125924&narHash=sha256-EV4jQJ5H3mypuOt4H174lII2yhnaUbZ9rbML2mjyRlI=
├───nixosConfigurations
│   └───teapot: NixOS configuration
└───nixosModules
    └───default: NixOS module

We call extendModules on the original nixosConfiguration.teapot to extend the configuration with new modules, in this case, our extension.nixosModules.default.

Now we can inspect the resultant output filesystem:

extension-demo/extended
❯ nix build .#nixosConfigurations.teapot.config.system.build.toplevel

extension-demo/extended took 2s
❯ cat result/etc/original-marker
Hello!⏎

extension-demo/extended
❯ cat result/etc/extension-marker
Hi!

Find out more about building Linux systems using Nix

Using private GitHub inputs in flakes #

While the github:username/repository flake paths utilize Github’s API and work well for public repositories, you may experience issues trying to use it with a private repository.

In these cases, try using the git+ssh protocol. For example:

{
  inputs = {
    original.url = "/home/ana/git/extension-demo/original";
    nixpkgs.follows = "original/nixpkgs";
    extension = {
      url = "git+ssh://git@github.com/hoverbear/top-secret-project.git";
      inputs.nixpkgs.follows = "original/nixpkgs";
    };
  };

  outputs = { self, nixpkgs, original, extension }:
    original.outputs // {
    nixosConfigurations.teapot =
      original.nixosConfigurations.teapot.extendModules {
        modules = [
          extension.nixosModules.default
        ];
      };
  };
}

Running nix flake update should then use your SSH key and work, as long as you have the ability to clone that repository over SSH.


Share
Avatar for Ana Hobden
Written by Ana Hobden

Ana is a hacker working in the Rust and Nix ecosystems. She's from Lək̓ʷəŋən territory in the Pacific Northwest, and holds a B.Sc. in Computer Science from the University of Victoria. She takes care of a golden retriever named Nami with her partner.