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 aspects (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 aspects and which nix configuration classes 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 and dev/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.