Thymis Documentation

Nix 101

When using Thymis, you will sometimes want to write Nix expressions to configure your devices or to package software. This guide will help you get started with Nix, the language used for these tasks.

What is Nix?

Nix is a programming language designed to manage software-build processes and deployments. It is declarative, meaning you describe what you want rather than how to achieve it. Nix is also functional, which means it treats functions as first-class citizens and avoids side effects.

What can I use Nix for?

Nix can be used for various tasks, including:

  • Package Management: Installing and managing software packages in a reproducible way.
  • Configuration Management: Defining NixOS/Linux system configurations and ensuring they are applied consistently across devices.
  • Development Environments: Creating isolated development environments with specific dependencies.

In Thymis, you will primarily use Nix to:

  • Package software for deployment on devices.
  • Write Modules that define how devices should be configured and what software they should run.

Getting Started with Nix

Having an installation of Nix available on your machine is useful for understanding this section. See the Setting up Nix guide for instructions on how to install Nix. Even if you are not using Nix on your local machine, you can still use all the Nix features in Thymis, as it will handle running Nix for you.

Basic Nix Syntax

Nix expressions are written in a syntax that may look unfamiliar at first. Primarily, Nix has interesting syntax for defining and calling functions:

For example this snippet defines a function that adds one to its argument:

let
  addTwo = arg1: arg1 + 2;
in
  addTwo 5

This will evaluate to 7.

In this snippet, you can also see the use of let ... in ... which is used to define local variables or functions, the semicolon ; is used to separate multiple definitions within the let block and the in keyword indicates where the expression will be evaluated with the defined variables or functions.

The colon : is used to separate the function’s argument from its body.

One of the most common value type in Nix is the attribute set, which is similar to a dictionary or map in other languages. It allows you to group related values together. For example:

{
  name = "example";
  version = "1.0";
}

This defines an attribute set with two attributes: name and version.

You can access attributes using the dot notation:

let
  myAttrSet = {
    name = "example";
    version = "1.0";
    nested = {
      value = "nestedValue";
    };
    nested2.value = "nestedValue2";
  };
in
  myAttrSet

Which will evaluate to an attribute set with .name as “example”, .version as “1.0”, .nested.value as “nestedValue”, and .nested2.value as “nestedValue2”.

Passing multiple arguments to a function is done in two ways, either one passes a complex object as a single argument, or one can use multiple function declarations like this:

let
  addTwoValuesCurrying = arg1: arg2: arg1 + arg2; # Using currying to define a function that takes two arguments
  addTwoValuesAttributeSet1 = args: args.arg1 + args.arg2; # Using an attribute set as a single argument
  addTwoValuesAttributeSet2 = { arg1, arg2 }: arg1 + arg2; # Destructuring the attribute set
  addTwoValuesAttributeSet3 = { arg1, arg2 ? 0 }: arg1 + arg2; # Default value for arg2
  addTwoValuesAttributeSet4 = { arg1, arg2 ? 0, ... }: arg1 + arg2;  # Using `...` to allow additional attributes
in
  {
    result1 = addTwoValuesCurrying 3 4; # Evaluates to 7
    result2 = addTwoValuesAttributeSet1 { arg1 = 3; arg2 = 4; }; # Evaluates to 7
    result3 = addTwoValuesAttributeSet2 { arg1 = 3; arg2 = 4; }; # Evaluates to 7
    result4 = addTwoValuesAttributeSet3 { arg1 = 3; }; # Evaluates to 3 (default value for arg2)
    result5 = addTwoValuesAttributeSet4 { arg1 = 3; unrelated = "value"; }; # Evaluates to 3 (default value for arg2)
  }

Other common value types in Nix include:

  • Lists: Ordered collections of values, defined with square brackets [], delimited by whitespace. For example:
    [ "value1" "value2" "value3" ]
  • Strings: Text values, defined with double quotes "" or multi-line strings with double single quotes ''...''. For example:
    "Hello, Nix!"
    ''
    This is a multi-line string.
    It can span multiple lines.
    ''
  • Booleans: True or false values, represented as true or false.
  • Numbers: Numeric values, which can be integers or floating-point numbers. For example:
    42
    3.14
  • Null: Represents the absence of a value, written as null.
  • Paths Values that represent file system paths, which can be absolute or relative. For example:
    /path/to/file
    ./relative/path
    Most of the time you will use relative paths when writing Nix expressions, since absolute paths are not portable across different systems.

Equipped with this knowledge, you can start writing Nix expressions to configure your devices or package software in Thymis.

Packaging Software with Nix

When packaging software with Nix, you will typically create a Nix expression that describes how to build and install the software. This expression is often called a “Nix package” or “Nix derivation”. Usually, you will write the package definition against a repository of pre-existing packages to define your package’s dependencies, such as the Nixpkgs repository, which is the default package repository used by Nix containing a wide range of software packages. Writing a package against Nixpkgs means that your function will be called by pkgs.callPackage YOUR_PACKAGE where pkgs is an instantiation of the Nixpkgs repository. Nix will arrange for the dependencies to be built and made available to your package, so you don’t have to worry about manually managing dependencies.

Example package (QT6 Application):

{ stdenv, cmake, ninja, nlohmann_json, pkg-config, libnfc, libmnl, qt6, qt6Packages }:
stdenv.mkDerivation {
    name = "yourqtapplication";
    src = ./.;
    nativeBuildInputs = [ cmake ninja pkg-config qt6.wrapQtAppsHook qt6.qttools qt6.qmake ];
    buildInputs = [ libmnl libnfc nlohmann_json qt6.qtbase qt6.qtsvg qt6.qtscxml qt6.qtdeclarative ];
}

In this example, the stdenv.mkDerivation function from nixpkgs is used to create the package. The name attribute specifies the name of the package, and the src attribute points to the source code of the application. The nativeBuildInputs attribute lists the build-time dependencies that are required to build the package, while the buildInputs attribute lists the runtime dependencies that are needed for the package to function.

For further details on how to write Nix packages, you can refer to nix.dev - Packaging existing software with Nix, the Nixpkgs manual and other resources available online.

You can find available packages in the Nixpkgs search.

Configuring Devices with Nix

In Thymis, you can use Nix to configure devices by writing Nix expressions that define the desired state of the device. This includes specifying which software packages should be installed, how the system should be configured, and any other settings that need to be applied. You can create Thymis Modules that use Nix expressions to define the configuration for a specific device type or application. These modules can then be applied to devices in Thymis, allowing you to manage the configuration of your devices in a declarative and reproducible way. The lowest friction way to use Nix to configure devices in Thymis is to use the Nix Language Module, which provides a textbox in the Thymis UI where you can write a Nix expression for a NixOS module directly.

An example for a NixOS module that configures a device to run a specific application might look like this:

{ config, pkgs, inputs, lib, ... }:

{
    hardware.raspberry-pi.config.all = {
        base-dt-params = {
            i2c_arm = {
                enable = true;
                value = "on";
            };
        };
    };
    hardware.i2c.enable = true;
    environment.systemPackages = [ inputs.your-app-your-app.packages.aarch64-linux.default pkgs.libnfc pkgs.i2c-tools];
    services.xserver.enable = true;
    services.displayManager.sddm.enable = true;
    services.displayManager.autoLogin.enable = true;
    services.displayManager.autoLogin.user = "thymiskiosk";
    users.users.thymiskiosk = {
        isNormalUser = true;
        createHome = true;
    };
    services.pipewire.enable = false;
    hardware.pulseaudio.enable = true;
    hardware.pulseaudio.support32Bit = true;
    services.xserver.windowManager.i3.enable = true;
    services.xserver.windowManager.i3.configFile = (pkgs.writeText "i3-config" ''
        # i3 config file (v4)
        bar {
            mode invisible
        }
        exec "/run/current-system/sw/bin/xrandr --newmode 1024x600_60.00  48.96  1024 1064 1168 1312  600 601 604 622  -HSync +Vsync; /run/current-system/sw/bin/xrandr --addmode HDMI-1 1024x600_60.00;"
        exec "/run/current-system/sw/bin/xset s off"
        exec "/run/current-system/sw/bin/xset -dpms"
        exec "${pkgs.unclutter}/bin/unclutter"
        exec ${pkgs.bash}/bin/bash -c "${inputs.your-app-your-app.packages.aarch64-linux.default}/bin/your-appApp --settings ${inputs.your-app-your-app}/thymis_settings.json"
    '');
}

This configuration sets various settings for the device, such as enabling I2C, installing the necessary packages, configuring the display manager, and setting up the window manager. The exec command in the i3 config file launches the application with the specified settings. You can see that the inputs.your-app-your-app is used to refer to a custom application that is pulled into the project using thymis.

This is just an example to show you how to write a NixOS module for Thymis. You can customize it further based on your specific requirements and the software you want to run on your devices.

You can find available configuration options for NixOS in the NixOS search.

ende