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
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
};
};
}
- 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.
- This name has to match the one defined in the
let/inblock. - 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
'';
};
}
lib.getExe pkgs.cowsay gets the path the the cowsay binary in the nix store
$ my-script
______________
< Hello world! >
--------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
#!/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"
'';
};
};
}
- This defines the executable name of the script.
- The runtime inputs are made available to the script using the
PATHenvironment variable. - This is how to refer to another package. In this case it's
myPackagefrom the minimalist package example. These packages are made available to nix by theimport-treemechanics.
#!/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
{ 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!")
'';
};
}
- Packages added here will be built into the python environment.
#! /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!" ];
};
};
}
- The name
myWrappedPackageis arbitrary and can be changed - This is important to make
pkgsavailable for use inpackageandruntimeInputs
The args and flags options are mutually exclusive because args is built from flags.
Example of using a custom wrapModule:
{ 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)!
];
};
}
- Calling
wrapModuleto create a wrapper module - Define options for the module to use here. These options will only be referenced in the
configattribute below. binNameis the final name of the executable- This is the base package to be wrapped.
- This shows how to use configured values from the
optionsattribute. Note that thisconfigis different than the one passed around byhome-managerornixos - Using the
applyattribute of the wrapper with an attribute set that will define all theoptionsfor the wrapper. - This is important to pass through the same
pkgsdefined inperSystem, which already has the relevant platform selected fromnixpkgs. - Setting the value for one of the defined
options - Creating a Home Manager module that will be exported by the
flake-partsmachinery. - Example of referring
Devenv
{ 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-partsattributes - vic/flake-file
-
Used to (re)generate the top-level flake file
- vic/import-tree
import-treerecursively 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.