Dendrix - community-driven distribution of Dendritic Nix configurations.
Editor-distributions like those for nvim/emacs provide community-driven, opinionated configurations that can be easily reused and enabled by newcomers.
The dendrix
project aims to provide the same experience: having community-managed, author-maintained and no-barrier-of-entry setups for everything that can be configured using flake-parts modules.
In a sense, this repository is akin to nix-community/NUR but for flake-parts dendritic modules that can provide packages and aspects to many different nix configuration classes.
Dendrix is an on-going effort. We already have all the code infrastructure on this repo to provide Layers and aspect import-trees from community repositories.
However as any community project, the success of Dendrix depends not on a single person, but on people willing to participate on it.
Join us by using Dendrix' exposed trees or by sharing part of your configs with the community, or by joining the discussion on how to better come up with community-managed configurations.
It all depends on each of us.
Made with <3 by @vic
Getting Started
Usage (for existing flake-parts setups)
Add the dendrix
input to your flake:
# flake.nix -- add the dendrix input:
{
inputs.dendrix.url = "github:vic/dendrix";
# Flatten dependencies.
#inputs.dendrix.inputs.import-tree.follows = "import-tree";
#inputs.dendrix.inputs.nixpkgs-lib.follows = "nixpkgs-lib";
}
Then import Dendrix trees/layers on any flake-parts module of yours:
{ inputs, ... }:
{
imports = [
# inputs.dendrix.vic-vix.macos-keys # example <macos-keys> aspect.
# inputs.dendrix.vix # example layer, see https://github.com/vic/dendrix/tree/main/dev/layers
];
}
See usage instructions for either Dendrix Trees or Dendrix Layers.
Quick Start (for NixOS newcomers)
Dendrix is a work in progress. We aim to provide batteries-included preconfigured NixOS experience for newcomers. But we are currently working on it.
We provide some templates you can use to start a new system flake.
nix flake init github:vic/dendrix#template
Then edit your layers.nix
file.
Try it Online!
If you are not currently a NixOS user, you can try running an ephemereal NixOS on the web.
Start a machine and run the following:
nix run .#os-switch template
Customization
Once you have a ./modules/
directory on your flake, just add flake-parts modules following the dendritic pattern.
All files will be loaded automatically. Edit your layers.nix
to include dendrix provided aspects you choose.
Dendritic Nix
Dendritic is a pattern for writing nix configurations based on flake-parts's modules
option.
We say that Dendritic nix configurations are aspect-oriented meaning that each nix file provides config-values for the same aspect across different nix configuration classes.
This is done via flake-parts' flake.modules.<class>.<aspect>
options.
Where <class>
is a type of configuration, like nixos
, darwin
, homeManager
, nixvim
, etc.
And <aspect>
is the cross-cutting concern or feature that is being configured across
one or more of these classes.
Example of a dendritic configuration.
As an example of what a dendritic nix config looks like, suppose we want to configure ssh facilities
(the ssh
aspect) across our NixOS, Nix-darwin hosts and user homes.
# modules/ssh.nix -- like every other file inside modules, this is a flake-parts module.
{ config, ... }: {
flake.modules.nixos.ssh = {
# Linux config: server, firewall-ports, etc.
};
flake.modules.darwin.ssh = {
# MacOS config: enable builtin ssh server, etc.
};
flake.modules.homeManager.ssh = {
# setup ~/.ssh/config or keys.
};
perSystem = {pkgs, ...}: {
# expose custom package/checks/devshells by this aspect.
};
}
That's it. This is what Dendritic is all about. By following this configuration pattern you will notice your code now incorporates the following:
Denritic Advantages
No need to use specialArgs for communicating values.
A common pattern for passing values between different nix configurations types (eg between a nixos
config and a homeManager
one),
is to use the specialArgs
module argument or home-manager.extraSpecialArgs
.
This is considered an anti-pattern in dendritic setups, since there's no need to use specialArgs
at all. Because you can
always use let bindings (or even define your own options at the flake-parts level) to share values across different configuration classes.
# modules/vic.nix -- a flake-parts module that configures the "vic" user aspect.
let
userName = "vic"; # a shared value between classes
in
{
flake.modules.nixos.${userName} = {
users.users.${userName} = { isNormalUser = true; extraGroups = [ "wheel" ]; };
};
flake.modules.darwin.${userName} = {
system.primaryUser = userName; # configuring a user is different on MacOS than on NixOS.
};
flake.modules.homeManager.${userName} =
{ pkgs, lib, ... }:
{
home.username = lib.mkDefault userName;
home.homeDirectory = lib.mkDefault (if pkgs.stdenvNoCC.isDarwin then "/Users/${userName}" else "/home/${userName}");
home.stateVersion = lib.mkDefault "25.05";
};
}
No file organization restrictions.
The dendritic pattern imposes no restrictions on how you organize or name your nix files.
Unlike other nix-configuration libs/frameworks that mandate a predefined structure. In Dendritic, you are free to move or rename each file as it better suits your mental model.
This is possible because:
All nix files have the same semantic meaning.
In a Dendritic setup, each .nix
file has only one interpretation: A flake-parts module.
Unlike other kinds of setup where each nix file can be a nixos
configuration, or a home-manager
configuration, or
a package, or something entrely different. In such setups, loading a file requires you to know what kind of meaning
each file has before importing it.
This leads us to having:
No manual file imports.
All files being flake-parts modules, means we have no need for manually importing nix files. They can all be loaded at once into a single import.
The Dendritic community commonly uses vic/import-tree
for this.
Note: import-tree ignores any file that has an _
anywhere as part of its path.
# flake.nix
{
inputs = {
flake-parts.url = "github:hercules-ci/flake-parts";
import-tree.url = "github:vic/import-tree";
# all other inputs your flake needs, like nixpkgs.
};
outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } (inputs.import-tree ./modules);
}
This is the only place you will call import-tree
and it will load all files under ./modules
recursively.
This means we can have:
Minimal and focused flake.nix
Instead of having huge flake.nix
files with lots of nix logic inside the flake itself.
It is now possible move to all nix logic into ./modules
.
Your flake becomes minimal, focused on defining inputs and possibly cache, experimental-features config.
And any file inside modules can contribute to flake outputs (packages/checks/osConfigurations) as needed.
# modules/flake/formatter.nix
{
perSystem = {pkgs, ...}: {
formatter = pkgs.alejandra;
};
}
Feature Centric instead of Host Centric.
As noted by Pol Dellaiera in Flipping the Configuration Matrix:
the configuration is now structured around features, not hostnames. It is a shift in the axis of composition, essentially an inversion of configuration control. What may seem like a subtle change at first has profound implications for flexibility, reuse, and maintainability.
You will notice that you start naming your files around the aspect
s (features) they define
instead of where they are applied.
Feature Closures
By closure we mean: everything that is needed for a given feature to work is configured closely, in the same unit (file/directory named after the feature).
Because a single feature.nix
contributes to different configuration classes, it has all
the information on how feature works, instead of having to look at different files for
package definitions, nixos or home-manager configurations dispersed over all over the tree.
If you need to look where some feature is defined on a repo you don't know, it will be easier to simply guess by path name. Paths become documentation.
Incremental Features
Since all nix files are loaded automatically. You can increment the capabilities that an
existing feature-x/basic.nix
provides by just creating another feature-x/advanced.nix
.
Both of them should contribute to the same aspect: flake.modules.<class>.feature-x
, but
each file focuses on the different capabilities they provide to the system whole.
This way, you can split feature-x/advanced.nix
into more files. And adding or removing
files from your modules (or adding an _
for them to be ignored) has no
other impact than the overall capabilities provided into your systems.
This is an easy way to disable loading files while on a huge refactor. Or when some hosts or features should be decomissioned immediatly/temporarily.
No dependencies other than flake-parts
Since the dendritic pattern builds around flake-parts modules, the only dependency is
flake-parts
. You can load files using import-tree or any other means. You can also use
any flake-parts based library to define your configurations, like Unify,
as long as it exposes flake.modules.<class>.<aspect>
attribute sets.
Dendritic community.
Last but not least. By using the dendritic pattern you open the door to defining or re-using existing generic (user/host independent) configurations from the community.
This is the goal of the Dendrix project: To allow people share dendritic configurations and socially enhance their capabilities.
Dendrix import-trees
Our last section ended with the following mission statement:
This is the goal of the Dendrix project: To allow people share dendritic configurations and socially enhance their capabilities.
As a first step into this goal, Dendrix discovers what aspect
s and which nix configuration class
es are provided by the dendritic community repository sources.
The sidebar on the left shows a filtering UI that serves three purposes:
- Document which aspects/classes are found on each repo and their locations.
- Visibility on what names are used by the community for defining aspects/classes.
- Allow people to re-use existing aspects by providing import-trees to them.
Using Community import-trees
For each community repository, Dendrix exposes a top-level import-tree that provides all files in the repo shared paths. And for each discovered aspect in the repo, Dendrix defines another import-tree inside repo attribute.
# your ./modules/something.nix
{inputs, ...}:
{
imports = [
inputs.dendrix.some-repo # ALL shared files in a repository. intended to be used WITH `.filter`.
inputs.dendrix.some-repo.some-aspect # Files that define flake.modules.<class>.some-aspect
];
}
An example of a repo's import-tree is dendrix.vic-vix
.
It references all files shared by vic's modules/community
directory,
except for those having the private
infix (see conventions).
An example of an repo's aspect import-tree is dendrix.vic-vix.macos-keys
that provides MacOS like keys on Linux using keyd
.
Community Participation.
Sharing re-usable parts of your Dendritic configs.
You are free to send a PR adding/removing your dendritic repo into dev/npins/sources.json
.
If you have an uncommon directory layout, or you want to share custom import-trees, you can specify specific community paths
. (see all options)
As an example in the previous link, vic has a modules/community
subdir, indicating that
everything outside of it might not be ready for re-use by other people. perhaps because it depends on some hardware or
host-specifc or user-specific settings.
Organizing with other people around aspects.
The better we organize and name our files around aspects, the better it will be to discover their intent and re-usability.
It is our hope that using the UI at the left, we can discover how are community members naming our aspects or organizing our files, and we can participate in community discussions around aspect's naming conventions.
Dendrix layers
Layers are cross-repository flake-parts modules that provide community-managed
facilities by importing from different community import-trees
.
In the future, Layers can allow to provide custom batteries-included specialized NixOS distributions. Like, NixOS for Gaming, NixOS for AI, NixOS for DevSec Ops, etc.
Using Existing Layers
Layers are usable by anyone who uses flake-parts, you just need to import the corresponding module.
If you were to use the example AI layer (from below), you'd do the following on a flake module of yours:
# your ./modules/ai.nix
{ inputs, ...}:
{
imports = [ inputs.dendrix.ai ];
}
Creating Layers
A Dendrix Layer is defined at ./dev/layers/<name>/modules/**.nix
. If you create one, make sure you
also provide a ./dev/layers/<name>/README.md
with details about usage or input requirements.
For example, if the community comes up with an ai
aspect shared across different repos, we
could have a blessed ai layer providing files from both repos and community-managed configurations.
# This is an example of how an AI Layer might be defined in our Dendrix community repository.
#
# ./dev/layers/ai/modules/default.nix
{ inputs, lib, ... }:
let
ai-one = inputs.dendrix.repo-one.ai; # import-tree for AI aspect from repo-one.
ai-two = inputs.dendrix.repo-two.ai; # import-tree for AI aspect from repo-two.
in
{
imports = [ ai-one ai-two ];
# flake-level community options.
options.ai.something.enable = lib.mkEnableOption "Enable something for AI";
# packages,checks,devshells,etc for AI
perSystem = {pkgs, ...}: {
packages.ai-cli = { };
};
# extensions to the ai aspect.
flake.modules.nixos.ai = { };
flake.modules.darwin.ai = { };
flake.modules.homeManager.ai = { };
}
For this to be possible we first need to collaborate around configuring the ai
feature
in repo-one
and repo-two
so that their configurations do not depend on host/user specifics.
This is quite possible, because repos are free to share only part of their trees with the community. Keeping the non-shared parts for their private infra customization.
# only this is shared to the community
./modules/community/ai.nix
# these two are not community shared.
./modules/hosts/myhost/ai.nix # augmented for host specific gpu.
./modules/users/vic/ai.nix # with user specific credentials.
Dendrix conventions
There are some non-mandatory conventions on Dendrix. However using them can ease integration of your repo.
Some of these conventions translate as code in the default pipe-line used to configure each import-tree.
Having a modules/community
directory.
By default, dendrix detects if the community repo has a modules/community
directory. If so, only that directory is scanned for aspects. So we recommend having such a directory.
Otherwise the modules/
directory will be scanned and shared.
If you, however, prefer to have another structure, it is possible to define a paths
option.
Anything private
is not shared.
Any file (even inside modules/community
) having a private
infix anywhere in their path is not visible to the community.
This is akin to import-tree's _
convention used for ignoring files. However private
paths can be loaded by your flake but
not on Dendrix import-trees.
Flags
Flags are a convention for allowing consumers of your repo to easily select or skip collections of files.
A flag has the following form: +flag
. A plus-sign and a name consisting of alphanumeric characters and -
hypen.
Some examples of flags are: +lsp
, +local-ai
, +rust
.
Flags can be present anywhere on a path: directory or file names.
By using flags in your paths, it can be posible for people do things like including/excluding some capabilities:
{
imports = [
# Include anything with +vim on their path.
# Exclude anything with +emacs on their path.
(inputs.dendrix.your-repo.flagged "-emacs +vim")
];
}
A clean flake.nix
that just import-tree ./modules
As noted in the focused flake.nix, a best practice is to move all your nix logic into a nix file inside ./modules
.
People usually have a ./modules/flake
for this purpose.
Using standard nix features.
At least on community intended directories. Outside of your shared directories you can use whatever nix features you want or any custom input of yours. However, keeping the shared code usable for most people is best for code adoption.
Contributing
Since we all now have agreed to follow the Dendritic pattern to organize our files, lets take a few more guidelines to make eveybody's life easier:
- Always be nice, and have respect for others.
- Be professional and considerate we are giving our time and energy on this project as an invaluable good for others.
- Contributions are welcome as long as you also make a compromise to become maintainer for your aspect don't abandon your contribution easily. (Unmaintained files will be removed.)
- This is a community project, so as soon as your PR is merged you'll also get commit bit, however we restrict changes to be only via PRs and require code-owners review before merge.
- Prefer linear git history, squash PRs and no merge-commit. Vic recommends working with jujutsu.
Development
We recommend to use direnv
or simply run nix develop ./dev
to load the env.
If you are using direnv, we provide an .envrc
for it.
Upon entering the shell, you will see a menu of useful commands:
$ nix develop ./dev
🔨 Welcome to devshell
[[general commands]]
book - serve dev/book
check - run flake checks
discover - generate files with discovery-community-aspects enabled.
elm-build - compile elm debug mode
elm-check - run elm tests
elm-registry - use elm2nix to regen dependencies registry for nix
files - fmt / genfiles / fmt
fmt - run code formatter
genfiles - generate files from sources
menu - prints this menu
pins - run npins inside dev
Frequently Asked Questions
-
Are these configurations restriced to dendritic setups?
Yes. The reason is that using dendritic patterns allows us to easily combine configs from many different community sources knowing that each and every .nix file will be a flake-parts module.
Layers (blessed presets) are always loaded by import-tree, but only enabled when you include them as part of a top-level module of yours.
-
Is dendrix a NixOS based distribution ?
In a way, but we still don't provide customized bootable installers.
It is more a flake-parts configurations collection that can be included on any dendritic setup.
-
Can I contribute my awesome Desktop rice?
Sure! the best way to do that is to keep your desktop rice on your own repository. And use this repo to add an import-tree object pointing to it. See the notes above about contributing on
dev/npins/sources.json
anddev/community/*.nix
. -
How are layers made?
A layer is a blessed dendritic setup, aimed to reuse aspects from the community provided dendritic repositories. However these blessed configs might have conventions that community repositories not necessarily follow (since our repos are mainly used for our own infra). So it is part of this community to create discussions about how to name things so that best practices and conventions arise around sharing and extending known aspects.
For example, if we had a
gaming
aspect. We would need conventions on how to name aspect modules for gaming. And people would likely provide such dendritic configurations on their setups.So, long story short, layers are community owned setups. And repos are owned by a respective community member.
-
How about Games/AI/Devops/Security layers?
You are right on point!, that's precisely why this project started. We also want to provide specialized versions of NixOS focused on pre-configured security, gaming, development setups.