Skip to content

vic/import-tree

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

41 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🌲🌴 import-tree πŸŽ„πŸŒ³

Helper functions for import of Nixpkgs module system modules under a directory recursively

The following goes recursively through the provided ./modules path and imports the files whose names end with .nix.

{config, ...} {
  imports = [  (import-tree ./modules)  ];
}
  • Module class agnostic; e.g. can be used for NixOS, nix-darwin, home-manager, flake-parts, NixVim.

Ignored files

Paths that have a component that begins with an underscore are ignored.

Example flake-parts usage

{
  inputs.import-tree.url = "github:vic/import-tree";
  inputs.flake-parts.url = "github:hercules-ci/flake-parts";

  outputs = inputs: inputs.flake-parts.mkFlake { inherit inputs; } (inputs.import-tree ./modules);
}

Obtaining the API

When used as a flake, the flake outputs attrset is the primary callable. Otherwise, importing the default.nix that is at the root of this repository will evaluate into the same attrset. This callable attrset is referred to as import-tree in this documentation.

import-tree

Takes a single argument: path or deeply nested list of path. Returns a module that imports the discovered files. For example, given the following file tree:

default.nix
modules/
  a.nix
  subdir/
    b.nix

The following

{lib, config, ...} {
  imports = [ (import-tree ./modules) ];
}

Is similar to

{lib, config, ...} {
  imports = [
    {
      imports = [
        ./modules/a.nix
        ./modules/subdir/b.nix
      ];
    }
  ];
}

If given a deeply nested list of paths the list will be flattened and results concatenated. The following is valid usage:

{lib, config, ...} {
  imports = [ (import-tree [./a [./b]]) ];
}

Configurable behavior

import-tree functions with custom behavior can be obtained using a builder pattern. For example:

lib.pipe import-tree [
  (i: i.mapWith lib.traceVal) # trace all paths
  (i: i.filtered (lib.hasInfix ".mod.")) # filter nix files by some predicate
  (i: i ./modules) # finally, call the configured callable with a path
]

Here is a less readable equivalent:

((import-tree.mapWith lib.traceVal).filtered (lib.hasInfix ".mod.")) ./modules

import-tree.withLib

Note

withLib is required prior to invocation of any of .leafs or .pipeTo. Because with the use of those functions the implementation does not have access to a lib that is provided as a module argument.

# import-tree.withLib : lib -> import-tree

import-tree.withLib pkgs.lib

import-tree.filtered

filtered takes a predicate function path -> bool. true means included.

Note

Only files with suffix .nix are candidates.

# import-tree.filtered : (path -> bool) -> import-tree

import-tree.filtered (lib.hasInfix ".mod.") ./some-dir

import-tree.matching

matching takes a regular expression. The regex should match the full path for the path to be selected. Match is done with lib.strings.match;

# import-tree.matching : regex -> import-tree

import-tree.matching ".*/[a-z]+@(foo|bar)\.nix" ./some-dir

import-tree.mapWith

mapWith can be used to transform each path by providing a function. e.g. to convert the path into a module explicitly.

# import-tree.mapWith : (path -> any) -> import-tree

import-tree.mapWith (path: {
  imports = [ path ];
  # assuming such an option is declared
  automaticallyImportedPaths = [ path ];
})

import-tree.pipeTo

pipeTo takes a function that will receive the list of paths. When configured with this, import-tree will not return a nix module but the result of the function being piped to.

# import-tree.pipeTo : ([paths] -> any) -> import-tree

import-tree.pipeTo lib.id # equivalent to  `.leafs`

import-tree.leafs

leafs takes no arguments, it is equivalent to calling import-tree.pipeTo lib.id, that is, instead of producing a nix module, just return the list of results.

# import-tree.leafs : import-tree

import-tree.leafs

Why

Importing a tree of nix modules has some advantages:

Pattern: each file is a flake-parts module

That pattern was the original inspiration for publishing this library. Some of the benefits are described in the author's personal infrastructure repository.

Sharing subtrees of modules as flake parts

People could share sub-trees of modules as different sets of functionality. for example, by-feature layers in a neovim distribution.

# flake.nix (layered configs-distro)
{
  outputs = _: {
    flakeModules = {
      options = {inputs, ...}: inputs.import-tree ./flakeModules/options;
      minimal = {inputs, ...}: inputs.import-tree [./flakeModules/options ./flakeModules/minimal];
      maximal = {inputs, ...}: inputs.import-tree ./flakeModules;

      byFeature = featureName: {inputs, lib, ...}: inputs.import-tree.filtered (lib.hasSuffix "${featureName}.nix") ./flakeModules;
    };
  };
}

Note that in the previous example, the flake does not requires inputs. That's not actually a requirement of this library, the flake could define its own inputs just as any other flake does. However, this example can help illustrate another pattern:

Flakes with no inputs exposing just flakeModules

This pattern (as illustrated by the flake code above) declares no inputs. Yet the exposed flakeModules have access to the final user's flake inputs.

This bypasses the flake.lock advantages--nix flake lock wont even generate a file-- and since the code has no guarantee on which version of the dependency inputs it will run using library code will probably break. So, clearly this pattern is not for every situation, but most likely for sharing modules. However, one advantage of this is that the dependency tree would be flat, giving the final user's flake absolute control on what inputs are used, without having to worry whether some third-party forgot to use foo.inputs.nixpkgs.follows = "nixpkgs"; on any flake we are trying to re-use.

About

Import all nix files in a directory tree.

Topics

Resources

License

Stars

Watchers

Forks

Languages