Skip to content

Flake Parts

The dendritic pattern uses flake-parts and is a modern pattern for organizing nix flakes. The documentation for it, like for everything else in the nix ecosystem, is totally disjointed, incomplete, and fucked generally.

From a High Level

Flakes already have an expected set of attribute names in the output schema, and flake-parts adds a few others. The important ones are packages, modules, nixosConfigurations, and homeConfigurations.

All the nix files in the configured directory are considered to also be flake-parts modules and are automatically imported by import-tree. This makes all the package and module names universally available.

Project Init

Start a new flake-parts repo from scratch
nix flake init -t github:vic/flake-file#dendritic

Patterns

Home Manager Configuration

{ self, inputs, ... }:clear
let
  username = "john";
in
{
  flake.modules.homeManager."${username}" = { config, pkgs, lib, ... }: {
    # Module code here
  };

  flake.homeConfigurations."${username}" = withSystem "x86_64-linux" (ctx@{ config, inputs', ...}:
    inputs.home-manager.lib.homeManagerConfiguration {
      pkgs = inputs'.nixpkgs.legacyPackages;
      modules = [ inputs.self.modules.homeManager."${username}" ];
  });
}

Module with Options

{ self, inputs, ... }:
{
  flake.modules.nixos.myModule = { config, pkgs, lib, ... }:
    let
      cfg = config.myModule; # (1)!
    in
    options.myModule = { # (2)!
      enable = lib.mkEnableOption "Description of my module";
      # Addtional module options here
      configDir = lib.mkOption = { # (3)!
        name = "Configuration directory";
        type = lib.types.str;
        default = "/etc/my-module";
      };
    };
    config = lib.mkIf cfg.enable {
      # Module config here
    }; 
  };
}
  1. This name has to match the one defined in the options attribute. It's just a convenient alias for this module to refer to its own options.
  2. This name has to match the one defined in the let/in block.
  3. These are all arbitrary example values

Script Packages

Minimalist

{ self, inputs, ... }: {
  perSystem = { system, pkgs, lib, ... }: {
    packages.myPackage = pkgs.writeShellScriptBin "my-script" ''
      ${lib.getExe pkgs.cowsay} "Hello world!"
      # comments
    '';
  };
}
The pattern with lib.getExe pkgs.cowsay gets the path the the cowsay binary in the nix store

Example usage
$ my-script
 ______________
< Hello world! >
 --------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
Resulting script
#!/nix/store/v8sa6r6q037ihghxfbwzjj4p59v2x0pv-bash-5.3p9/bin/bash
/nix/store/7qs31js7dw2ayj0q807v9apmwbw00jvb-cowsay-3.8.4/bin/cowsay "Hello world!"
# comments

Shell Application

{ self, inputs, ... }: {
  perSystem = { system, pkgs, lib, ... }: {
    packages.anotherPackage = pkgs.writeShellApplication {
      name = "another-script"; # (1)!
      runtimeInputs = [ # (2)!
        pkgs.cowsay
        inputs.self.packages.${system}.myPackage # (3)!
      ];
      text = ''
        my-script
        cowsay "Another cowsay"
      '';
    };
  };
}
  1. This defines the executable name of the script.
  2. The runtime inputs are made available to the script using the PATH environment variable.
  3. This is how to refer to another package. In this case it's myPackage from the minimalist package example. These packages are made available to nix by the import-tree mechanics.
Resulting script
#!/nix/store/v8sa6r6q037ihghxfbwzjj4p59v2x0pv-bash-5.3p9/bin/bash
set -o errexit
set -o nounset
set -o pipefail

export PATH="/nix/store/7qs31js7dw2ayj0q807v9apmwbw00jvb-cowsay-3.8.4/bin:/nix/store/9lb9clykka9m8wvmwzisd00a5ldn3pg8-my-script/bin:$PATH"

my-script
cowsay "Another cowsay"

Python Script

Search for more

{ self, inputs, ... }: {
  perSystem = { system, pkgs, lib, ... }: {
    packages.richPrinter = pkgs.writers.writePython3Bin "rich-printer" {
      libraries = with pkgs.python313Packages; [
        rich # (1)!
      ];
    } ''
      from rich.console import Console

      Console().log("Hello world!")
    '';
  };
}
  1. Packages added here will be built into the python environment.
Resulting script
#! /nix/store/ydgmc3p7y49ffw363hfb4gcvjw54c8lx-python3-3.13.12-env/bin/python3.13
from rich.console import Console

Console().log("Hello world!")

Wrappers

There are 2 competing systems for wrapping packages and/or modules.

Lassulus / wrappers

Example of using wrapPackage with perSystem from flake-parts

{ self, inputs, ... }: {
  flake-file.inputs.wrappers = {
    url = "github:lassulus/wrappers";
    inputs.nixpkgs.follows = "nixpkgs";
  };

  perSystem = { system, pkgs, lib, ... }: {
    packages.myWrappedPackage = inputs.wrappers.lib.wrapPackage { # (1)!
      inherit pkgs; # (2)!
      binName = "wrapped-hello";
      package = pkgs.cowsay;
      args = [ "Wrapped hello world!" ]; 
    };
  };
}
  1. The name myWrappedPackage is arbitrary and can be changed
  2. This is important to make pkgs available for use in package and runtimeInputs

The args and flags options are mutually exclusive because args is built from flags.

Example of using a custom wrapModule:

Creating and using a custom wrapper module
{ self, inputs, ... }:
let
  myWrapper = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: { # (1)!
    options = { # (2)! 
      text-to-say = lib.mkOption {
        type = lib.types.str;
        description = "Text for the ascii cow to say.";
      };
    };

    config = {
      binName = "my-pkg"; # (3)!
      package = config.pkgs.cowsay; # (4)!
      args = [ config.text-to-say ]; # (5)!
    };
  });
in
{
  perSystem = { system, pkgs, lib, ... }: {
    packages.myPackage = (myWrapper.apply { # (6)!
      inherit pkgs; # (7)!
      text-to-say = "Hello from wrapped module!"; # (8)!
    }).wrapper;
  };

  flake.modules.homeManager.myPackage = { config, pkgs, lib, ... }: { # (9)!
    home.packages = [
      inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.myPackage # (10)!
    ];
  };
}
  1. Calling wrapModule to create a wrapper module
  2. Define options for the module to use here. These options will only be referenced in the config attribute below.
  3. binName is the final name of the executable
  4. This is the base package to be wrapped.
  5. This shows how to use configured values from the options attribute. Note that this config is different than the one passed around by home-manager or nixos
  6. Using the apply attribute of the wrapper with an attribute set that will define all the options for the wrapper.
  7. This is important to pass through the same pkgs defined in perSystem, which already has the relevant platform selected from nixpkgs.
  8. Setting the value for one of the defined options
  9. Creating a Home Manager module that will be exported by the flake-parts machinery.
  10. Example of referring

Devenv

Define a flake-parts devenv shell
{ self, inputs, ... }: {
  flake-file.inputs.devenv.url = "github:cachix/devenv";

  perSystem = { config, self', inputs', pkgs, system, ... }: {
    # Per-system attributes can be defined here. The self' and inputs'
    # module parameters provide easy access to attributes of the same
    # system.

    # Equivalent to  inputs'.nixpkgs.legacyPackages.hello;
    packages.default = pkgs.hello;

    devenv.shells.default = {
      name = "my-project";

      imports = [
        # This is just like the imports in devenv.nix.
        # See https://devenv.sh/guides/using-with-flake-parts/#import-a-devenv-module
        # ./devenv-foo.nix
      ];

      # https://devenv.sh/reference/options/
      packages = [ config.packages.default ];

      enterShell = ''
        hello
      '';

      processes.hello.exec = "hello";
    };
  };
}

Tools

hercules-ci/flake-parts
Provides the flake-parts attributes
vic/flake-file

Used to (re)generate the top-level flake file

Start new project
nix flake init -t github:vic/flake-file#default
(Re)generate nix inputs
nix run "$FLAKEDIR#write-flake"
vic/import-tree
import-tree recursively discovers and imports all Nix files in a directory. In Dendritic setups, where every file is Nix module (for instance flake-parts module) with the same semantic meaning, this eliminates manual import lists entirely.