Introduction

Nixago is a flake library for generating configuration files. It's primarily geared towards reducing the clutter at the root of your repository added by various development tools (i.e., formatters, linters, etc.). However, Nixago is flexible enough to generate configuration files for most scenarios.

Nixago is designed to be used in tandem with a nix shell to dynamically create and manage configuration files in your existing development environments. Define the configurations in the flake.nix file at the root of your repository, and Nixago will automatically generate shell hooks for managing the files.

Quick Start

Add Nixago as an input to your flake.nix:

{
  inputs = {
    # ...
    nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
    nixago.url = "github:nix-community/nixago";
    nixago.inputs.nixpkgs.follows = "nixpkgs";
    # ...
  };
}

Generate a Configuration

Nixago offers various engines which can be used for transforming input data into an output file. Nixago will default to the nix engine that utilizes pkgs.formats from nixpkgs.

let
  data = {
    "field1" = "value1";
    "field2" = true;
  };
in
nixago.lib.make {
  inherit data;
  output = "config.json";
  format = "json";
  engine = nixago.engines.nix { }; # Optional as this is the default value
}

The result of this invocation will be an attribute set with two attributes:

  • configFile: A derivation for building the configuration file.
  • shellHook: A shell hook for managing the file.

Building the derivation produces a file with the following output:

{
  "field1": "value1",
  "field2": true
}

The make function takes an attribute set that supports the options defined in the request module. Please refer to the module definition for all of the available options.

Using the Shell Hook

The generated shell hook will link the generated configuration file to one of two places:

  • If $PRJ_ROOT is defined, the file will be linked to $PRJ_ROOT/{output} where output is the relative path defined in the call to make.
  • If $PRJ_ROOT is not defined, the file will be linked to ./{output}, where the relative path is determined by where the Nix CLI was invoked.

For example, if $PRJ_ROOT is set to /home/user/code/myprj and the output is specified as configs/config.json then the file will be linked to /home/user/code/myprj/configs/config.json.

The shell hook is designed to be integrated into a development shell:

{
  # ...
  devShells = {
    default = pkgs.mkShell {
      shellHook = (nixago.lib.make config).shellHook;
    };
  };
 # ...
}

This will ensure the file is generated and linked when you enter the shell. The behavior of the hook can be modified via the available options.

Extending Nixago

An additional repository is available that provides extensions for Nixago. These simplify the process of generating configuration files for common development tools.

Engines

Nixago provides various engines which can be used for changing how the configuration file is being generated. If you would like to suggest a new engine, please open an issue.

CUE

This engine provides an interface for generating configuration files using the CUE language and its associated CLI tool. It allows combining the user-defined input with one or more CUE files and generating a file in the designated output format.

For more information on the design of CUE, see this doc. A good place to start learning the fundamentals is the Cuetorials website.

Concepts

Merging

When using CUE, you will typically define one or more CUE files that will be merged into the desired output. For example, if we had the below CUE file:

name: string
name: "John Doe"

We could then export it and get the following result:

$ cue export file.cue
{
    "name": "John Doe"
}

When CUE is presented with multiple files, they are all merged into one singular output:

name: string
name: "John Doe"
$ cue export file1.cue file2.cue
{
    "name": "John Doe"
}

Combining with JSON

CUE is a superset of JSON, so it's possible to merge JSON and CUE files. Using our previous example, this time with JSON:

name: string
{
  "name": "John Doe"
}
cue export file1.cue file2.json
{
    "name": "John Doe"
}

We get the same result because our JSON file and the previously defined CUE file are functionally equivalent.

Constraints

Performing input validation is a natural benefit when using the CUE language. We used a constraint earlier when we constrained the name field to a string. Continuing with our previous examples, if we modified the JSON structure to be the following:

{
  "name": 42
}

Then if we attempted to export both files, we would receive an error:

name: conflicting values 42 and string (mismatched types int and string)

CUE is informing us that we constrained name to a string, and yet we passed in an int (42) with our JSON data structure. This is a type mismatch and results in an error.

Exporting Modified Structures

A common use case is transforming an incoming structure into a modified outgoing structure. Take the following input structure:

{
  "first_name": "john",
  "last_name": "doe",
  "address": "123 Lane",
  "city": "Springfield",
  "state": "OR"
}

We only want to export the name and a combined address:

import "strings"

first_name: string
last_name: string
address: string
city: string
state: string

result: {
    name: strings.ToTitle(strings.Join([first_name, last_name], " "))
    full_address: "\(address)\n\(city), \(state)"
}

We can then ask CUE to only render our result expression:

$ cue export -e result file1.cue file2.json
{
    "name": "John Doe",
    "full_address": "123 Lane\nSpringfield, OR"
}

This allows ingesting one data structure and exporting another. As shown in the example, we can perform many permutations on the incoming data using CUE's standard library and interpolation.

Usage

ArgumentRequiredDescription
filesYesA list of file paths to pass to CUE
preHookNoA shell hook to execute before CUE is invoked
postHookNoA shell hook to execute after CUE is invoked
flagsNoAdditional flags to pass to CUE
cueNoThe cue package to use
jqNoThe jq package to use

The CUE engine builds off the concepts above. It takes a single required argument (files) which is the list of files to pass to CUE. In addition to these files, the engine will also convert the incoming configuration data into JSON and pass it as a file to CUE. The result is that CUE will evaluate the given files as well as the incoming configuration data. As noted earlier, CUE will merge all of these together into one final output.

In addition to the files argument, pre and post-hooks can be provided that execute before and after CUE is invoked respectively. Finally, additional flags can be specified to further control the execution of the CUE CLI tool.

Nix Engine

The nix engine uses the set of functions provided by pkgs.formats. You can see the source here.

Concepts

The basic outline of an entry in pkgs.formats is as follows:

{
    format = {}: {
        generate = name: value: pkgs.runCommand {...};
    };
}

Where format is the name of the supported format (i.e., json).

Note that the format entry is a function that takes a single attribute set. In some cases, this is an empty set, but in other cases, the set contains additional options for configuring the format.

Usage

When using the nix engine from Nixago, one can pass attributes to the set with the following:

{
    engine = nixago.lib.engines.nix { opt1 = "value1"; };
}

The provided set will be passed to the format argument before calling its generate function.

When calling make, the specified value for the format attribute will be used to determine which entry in pkgs.formats to invoke. Thus, specifying an unsupported format will result in an assertion error. Refer to the nixpkgs source code to determine the currently supported output formats.

Contributing

This chapter covers the details required to contribute to Nixago. Before submitting a PR, please ensure you review this chapter thoroughly. Nixago has an opinionated design and expects a specific structure to work correctly. Understanding this structure ahead of time will make development more accessible and increase the chance of PRs going through without issues.

Design

When contributing to Nixago, it's essential to understand the general design principles that guide its development. This section provides details on how the library is structured and introduces the basic concepts required to contribute effectively.

Overview

%%{ init : { "theme" : "dark", "flowchart" : { "curve" : "linear" }}}%%
%%
flowchart TD
    flake{{User's flake.nix}}
    make(Call to make)
    engine(Call to engine)
    flake -- request ---> make
    make <-- request ---> engine
    make -- result --> flake

Nixago strives to meet the Unix philosophy of "do one thing and do it well." For Nixago, this is generating and managing configuration files. Anything that falls outside of this scope, or risks complicating internal structures, should be moved to an external project.

The interface for Nixago is purposefully simple: all interactions work around the request module. This module defines all required and optional fields necessary to generate and manage a configuration file. Extending this interface should not be the default choice, as unmitigated changes can result in muddying the usefulness of the interface it provides.

Nixago promises two outputs: a derivation for building the specified configuration file and a shell hook that manages the file locally. Nixago hands control over to the user regarding how the configuration should be built and how the hook should handle it.

Engines

There are many ways to generate a configuration file. The only restraint that Nixago imposes is that the input data must be a valid Nix expression. The entity that translates this Nix expression into a derivation is called an engine.

The purpose of an engine is to receive and process a request. The request's processing depends on the engine and its underlying tools. The only expectation that Nixago has is that a derivation is returned.

For example, the cue engine uses the CUE CLI tool to process a list of CUE files from the user, along with the user's input, to create a derivation that produces the desired configuration file. This modular design allows Nixago to be easily extended to support existing infrastructure.