Having a go at using TOML to configure NixOS

TLDR: I’m bodging my way to using TOML for parts of my configuration and would like to gather opinions about how useful exactly that is.

I’ve recently been experimenting with converting part of my NixOS config from Nix to TOML which is surprisingly easy to do.
Lots of configuration is just plain enabling modules and filling in values, basically just creating a Nix set.
Nix even helpfully contains builtins.fromTOML which turns TOML code into a Nix set.

Using this we can import a TOML file with:

# configuration.nix

{
    imports = [ (builtins.fromTOML (builtins.readFile ./some-configuration.toml)) ];
}

And shape our TOML file like:

# some-configuration.toml

[programs.gnupg.agent]
enable = true
enableSSHSupport = true

I later found out this was actually already implemented as lib.importTOML by this PR (which also sets _file to provide better error messages).

Lots of problems

This section is not exhaustive and there are probably more problems with this approach, these just stuck out to me.

Problem 1: We can no longer introduce our own abstractions to reduce duplication and use any sort of clever Nix code.

Problem 2: We can no longer set derivations as the value of some option, e.g. for environment.systemPackages, because we need to get an attribute of pkgs which requires Nix code.

Problem 3: Errors become a bit more confusing here as nixos-rebuild always reports the Nix file (e.g. configuration.nix) as the origin of the configuration and thus error.

Some solutions

Problem 1: I think this is pretty much insurmountable without adding a lot of extra templating and evaluation steps into the process of importing TOML, I think that would not be a smart thing to do as the goal is to decrease complexity and you can just use Nix whenever that complexity is needed.

Problem 2: I added a small extra processing step which traverses the options of the NixOS modules and translates string values into derivations whenever some option’s type is (a list of) package or path. I.e. it turns the TOML environment.systemPackages = [ "hello" "xorg.xev" ] into the Nix { environment.systemPackages = [ pkgs.hello pkgs.xorg.xev ]; }.

It is a (very hacky) proof of concept and doesn’t handle everything right now, as an example: users.users has type = with types; attrsOf (submodule userOpts); and is detected as a leaf node which stops further recursive processing.
Overall it works well enough though to try out the concept and handles some diverse options like environment.systemPackages, services.printing.drivers, fonts.fonts and nix.package just fine.

Using packages inline in strings (e.g. "${pkgs.emacs}/bin/emacsclient") is also not possible, perhaps this functionality could be added somehow as this is a very useful and commonly used feature.

Problem 3: Setting the _file attribute of the generated module solves this problem.

Additional confusing error messages caused by my solution for problem 2 (like pkgs missing an attribute we configured in the TOML file) are made somewhat clearer by a combination of throw and addErrorContext.

Previous discussions

I’ve found two previous incarnations of this topic:

They also contain a discussion about using TOML to write derivations for nixpkgs and how hard that would be.
I agree that that’s probably very tricky to get right and I personally also think it’s not worth the effort (at the very least not right now) but NixOS configuration seems a lot easier.

Specifically with respect to NixOS modules I think it’s important to distinguish between reusable modules (those providing options) and end-user modules (those only setting options).
While the first kind probably also requires the complexity of Nix I think the second kind can be done reasonably well in TOML.

Note that this functionality does not need to be restricted to TOML and only needs a fromX function to also work for other configuration languages, I just picked TOML here because fromTOML already existed and I quite like its format (trying to comment JSON code is… problematic).
I might have a go at getting Dhall to work as it looks interesting.

Does this make sense?

When I told a friend about my experiment he replied with

“I looked at it yesterday and thought it was an idiotic idea, haha”

(translated from Dutch by me) and we talked about some of the problems.
I’d like to gather some more opinions here to see whether there are people interested in the idea or if the consensus is that this would not be worth exploring.

It would also be cool if somebody knows of a less hacky way to translate strings into packages (I’ve also been thinking about trying to override lib.types but don’t really like that approach either).

My code is available here and in the NUR via repos.syberant.lib.importToml if you’re curious.

1 Like

Thanks for posting this! I was just looking for this. I’m mostly interested in having a way to conveniently add/install packages using cli (like nix-env -iA), but doing so using a declarative configuration as part of my system configuration instead of installing packages to a separate ‘mutating’ profile.

TOML (or YAML, or JSON) seems nice as the cli can easily add/remove entries without fiddling with Nix syntax.

Have you progressed further with your TOML setup? Are you still using it?

idris2-pkgs uses TOML configuration and turns it into Nix code like so: https://github.com/claymager/idris2-pkgs/blob/39609a1fa56334b7461694033c27912a29c0237a/utils/callToml.nix#L10

Seemingly, this resolves all three problems:

  1. Have an “extra” section where you can introduce custom Nix code (this is a questionable choice, but still possible)
  2. Can probably be done somewhere on a line like this
  3. Errors are first reported from the TOML file (with a line number and error message). Even if they’re not, it shouldn’t be hard to understand where they originate from in the TOML file.