1 52 Nix Language Essentials
Nimmo edited this page 2026-09-13 13:00:25 +01:00

Nix Language Essentials

  • Type: Tutorial
  • Status: Current
  • Scope: Nix syntax needed to read and make small changes in nixos-config
  • Canonical sources: flake.nix; modules/common/default-config.nix; modules/hardware/bluetooth.nix; modules/services/jellyfin.nix; hosts/electra/default.nix; Nix language basics
  • Last verified: Source commit 556df88494686003b1c4f20c8e0b99b5afc16a6e (2026-09-12); source HEAD b2831d33132967b4c4fb69e6068b2b05fff8fdff changes only flake.lock. Examples checked 2026-09-13; no Nix file changed.
  • Review triggers: Changes to cited modules, flake builder, commonly used language patterns, or linked upstream tutorial

Learning goal and prerequisites

Read the expressions this repository uses most: attribute sets, lists, strings, paths, functions, let, conditionals, and library calls. First follow the Guided Repository Tour. This is a reading primer, not a Nix language specification; use the official Nix language basics and language reference for complete syntax.

Read one module from the outside in

modules/services/jellyfin.nix begins with a function argument pattern:

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

The braces select named arguments from an attribute set; ... permits additional arguments supplied by the NixOS module system. The colon starts the function body. In this repository, config is the merged NixOS configuration, pkgs the package set, lib the Nixpkgs library, and inputs the flake inputs passed through specialArgs. A module does not gain an inputs variable by naming it elsewhere in the file; it must accept it as an argument if used.

The module then uses a let ... in expression:

let
  cfg = config.nixosConfig.jellyfin;
  dataDir = config.services.jellyfin.dataDir;
in
{ ... }

cfg and dataDir are names for values, not mutable variables. The final attribute set is the function result. Dot notation selects a nested attribute, as in config.services.jellyfin.dataDir; assignment such as services.jellyfin.enable = true; builds a nested attribute set. An assignment ends with ;. A list uses square brackets and whitespace, not commas: [ "video" "render" ].

Trace conditions and composition

modules/services/jellyfin.nix declares an option with lib.mkEnableOption and contributes its service definition only under lib.mkIf cfg.enable. Those are library/module-system functions, not special Nix syntax. lib.mkAfter [ dataDir ] adjusts merge ordering for a list; it is not a shell append. The NixOS module lesson covers the merge semantics.

In home/users/nimmo/default.nix, lib.optional (!isServer) ./plasma.nix evaluates to either a one-item list or an empty list. ++ concatenates that result with the fixed imports list. ./plasma.nix is a relative Nix path, resolved from the containing file, not a quoted string. When a path becomes a flake source input, Git tracking matters; a newly created, untracked file is invisible to a Git-backed flake.

In modules/common/default-config.nix, the default flake path is a string with interpolation:

default = "/home/${config.nixosConfig.primaryUser}/nixos-config";

${...} evaluates the expression inside the string. The same syntax can appear within attribute selection, as in inputs.nixpkgs-stable.legacyPackages.${pkgs.stdenv.hostPlatform.system}.jellyfin in the Jellyfin module. That chooses a package for the current host platform; it does not mean the host itself uses the stable Nixpkgs input as its main channel. The host builder choice is in flake.nix.

hosts/electra/default.nix also uses with pkgs; [ coreutils gnused ] for package names in a runtime-input list. with brings attributes into scope; it can make origins less obvious, so read the surrounding pkgs binding before copying a name. inherit inputs; in flake.nix is shorthand for inputs = inputs;.

Guided check

Open modules/hardware/bluetooth.nix and explain (without deploying) which parts are Nix syntax and which are NixOS option names. Then open modules/services/jellyfin.nix and identify: its function arguments, two let bindings, enable gate, one list, one interpolated attribute path, and one relative path or explain why that module has none. Compare your result to Adding a Module or Profile.

Common mistakes: confusing a quoted path with a Nix path, putting commas between list items, forgetting a semicolon, assuming mkIf is a language keyword, or treating config as state that changes sequentially. Nix evaluates an expression; NixOS later merges module definitions and builds the resulting system. The next lesson, Declarative Configuration, Store, and Generations, explains that lifecycle.