background grid image
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 small flake called original with a nixosConfiguration and a nixosModule:

original/flake.nix
1
{
2
inputs = {
3
nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11";
4
};
5
6
outputs = { self, nixpkgs }: {
7
nixosModules.default = { config, pkgs, lib, ... }: {
8
# Create a file `/etc/original-marker`
9
environment.etc.original-marker.text = "Hello!";
10
11
# You can ignore these, just keeping things small
12
boot.initrd.includeDefaultModules = false;
13
documentation.man.enable = false;
14
boot.loader.grub.enable = false;
15
fileSystems."/".device = "/dev/null";
16
system.stateVersion = "22.11";
17
};
18
19
nixosConfigurations.teapot = nixpkgs.lib.nixosSystem {
20
system = "x86_64-linux";
21
modules = [
22
self.nixosModules.default
23
];
24
};
25
};
26
}

While our little demo configuration, teapot, might not be a 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:

Terminal window
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
1
{
2
inputs = {
3
nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11";
4
};
5
6
outputs = { self, nixpkgs }: {
7
nixosModules.default = { config, pkgs, lib, ... }: {
8
# Create a file `/etc/extension-marker`
9
environment.etc.extension-marker.text = "Hi!";
10
};
11
};
12
}

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
1
{
2
inputs = {
3
original.url = "/home/ana/git/extension-demo/original";
4
nixpkgs.follows = "original/nixpkgs";
5
extension = {
6
url = "/home/ana/git/extension-demo/extension";
7
inputs.nixpkgs.follows = "original/nixpkgs";
8
};
9
};
10
11
outputs = { self, nixpkgs, original, extension }:
12
original.outputs // {
13
nixosConfigurations.teapot =
14
original.nixosConfigurations.teapot.extendModules {
15
modules = [
16
extension.nixosModules.default
17
];
18
};
19
};
20
}

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:

Terminal window
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!⏎

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:

1
{
2
inputs = {
3
original.url = "/home/ana/git/extension-demo/original";
4
nixpkgs.follows = "original/nixpkgs";
5
extension = {
6
url = "git+ssh://git@github.com/AmbiguousTechnologies/top-secret-project.git";
7
inputs.nixpkgs.follows = "original/nixpkgs";
8
};
9
};
10
11
outputs = { self, nixpkgs, original, extension }:
12
original.outputs // {
13
nixosConfigurations.teapot =
14
original.nixosConfigurations.teapot.extendModules {
15
modules = [
16
extension.nixosModules.default
17
];
18
};
19
};
20
}

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.