How to use nixlang as a template language?

Hello everyone, I’m trying to design a github repo that leverages github actions to create custom ISO nix images. By custom, I mean including SSH keys, setting the hostname, username, etc. I’m evaluating different design ideas for the template system, but as I’m relatively new to nix, I thought of asking here for different (ideally better) approaches.

How does it work?

I want a user to fork the repo, click on the github action, fill the inputs (username, hostname, public ssh, etc), and the image would be created.
I have all of that done, but I’m doubting how to give those inputs to the nix configuration.

Current idea

To think of every file inside nixosModules as a string and use string interpolation. For example (a rough sketch):

nixosModules/user.nix as:

{ inputs, ... }@flakeContext:
{ config, lib, pkgs, ... }:
let
  ssh_pub_files = lib.filterAttrs (k: v: v == "regular" && lib.hasSuffix ".pub" k) (builtins.readDir ../authorized_keys);
in
{
  config = {
    users = {
      users = {
        ${username} = {
          isNormalUser = true;
          initialPassword = "nixos";
          extraGroups = [ ${groups} ];
          openssh.authorizedKeys.keys = lib.mapAttrsToList (k: v: builtins.readFile "${../authorized_keys}/${k}") ssh_pub_files;
        };
      };
    };
  };
}

and then in another nix file, I would import that “template”, use getEnv to get the inputs from the github action and then output new files, that nix would use to build the images.

Is there a better way?
Thanks

This is the repo: GitHub - woile/ganix-iso: Generate Nix ISO images using github actions and flakes

1 Like

Why use templating when Nix is a functional language? Any template substitutions you want to do can just be function arguments and you can use regular ole Nix evaluation to build the results you want.

1 Like

I understand what you mean, but I cannot bring it down to earth, could you give me an example?
Let say I have a variables.nix like this

{
   username = builtins.getEnv "USERNAME"
}

And then just do something like:

let
  variables = import ./variables.nix;

in
{ 
  users = {
    variables.username = { isNormalUser = true; }
}

Would something like that work?
Thanks!

That would basically do the trick, except for the missing } to close the users attrset, and the way you did variables.username = { isNormalUser = true; }; is wrong. In an attribute set, the left hand side of = is just a name and it can contain dots to make nested names, so that actually causes the result of that expression to be:

{
  users = {
    variables = {
      username = {
        isNormalUser = true;
      };
    };
  };
}

It literally uses the names variables and username instead of the variables.username expression, because the syntax in those positions is for names, not variables. So if you want it to look like this (assuming the USERNAME environment variable is set to foobar):

{
  users = {
    foobar = {
      isNormalUser = true;
    };
  };
}

You can use a neat little syntax in Nix:

let
  variables = import ./variables.nix;
in
{
  users = {
    "${variables.username}" = { isNormalUser = true; };
  };
}

This is just a convenient syntax of Nix. A name in an attrset can actually be the result of an expression, using "${expr}" = value; syntax. e.g. You could do something more complicated, like:

{
  ${if builtins.getEnv "FOO" == "bar" then "bar" else "foo"} = "baz";
}

I put a whole if expression in there, which works as long as the expression returns a string to use as the name.

EDIT: Also note that builtins.getEnv doesn’t actually work in flakes unless you use the --impure command line flag.

2 Likes

Thank you so much, that’s extremely helpful!

The impure flag being part of the build command?

Like this: nix build --impure --option system aarch64-linux --option sandbox false --extra-platforms aarch64-linux .#nixos

1 Like