Module
Modules can be defined in two formats:
As attrset, aka. object (JSON), dict (Python):
{ # <|
imports = []; # |
config = {}; # | module info
options = {}; # |
} # <|
All those attributes are optional
- imports: array with paths to other modules
- config: object with actual configurations
- options: object with our config type definition
As function:
Functions has following arguments:
config
with all evaluated configs values,pkgs
with all nixpkgs available.lib
library of useful functions.- And may receive others (we use
...
to ignore them)
{ config, pkgs, lib, ... }: # <| function args
{ # <|
imports = []; # |
config = {}; # | module info
options = {}; # |
} # <|
Imports
Points to other modules files to be imported in this module
{
imports = [
./gh-actions-options.nix
./gh-actions-impl.nix
];
}
Hint, split modules in two files:
- One mostly with options, where your definition goes
- Other with config, where your information goes
It has two advantages, let share options definitions across projects more easily.
And it hides complexity, hiding complexity is what abstraction is all about, we didn't share options definitions across projects to type less, but because we could reuse an abstraction that helps hiding complexity.
Config
Are values to our options
We can set value by ourself, or use lib functions to import json/toml/text files.
{ lib, ...}:
{
config.files.text."/HW.txt" = "Hello World!";
config.files.text."/EO.txt" = lib.concatStringsSep "" ["48" "65" "6c" "6c" "6f"];
config.files.text."/LR.txt" = (lib.importJSON ./hello.json).msg; # { "msg": "Hello World!" }
config.files.text."/LL.txt" = (lib.importTOML ./hello.toml).msg; # msg = Hello World!
config.files.text."/OD.txt" = lib.readFile ./hello.txt; # Hello World!
}
If file has no options.
, config.
can be ommited.
And this file produce the same result
{ lib, ...}:
{
files.text."/HW.txt" = "Hello World!";
files.text."/EO.txt" = lib.concatStringsSep "" ["48" "65" "6c" "6c" "6f"];
files.text."/LR.txt" = (lib.importJSON ./hello.json).msg; # { "msg": "Hello World!" }
files.text."/LL.txt" = (lib.importTOML ./hello.toml).msg; # msg = Hello World!
files.text."/OD.txt" = lib.readFile ./hello.txt; # Hello World!
}
Options
Options are schema definition for configs values.
Example, to create a github action file, it could be done like this:
{
config.files.yaml."/.github/workflows/ci-cd.yaml" = {
on = "push";
jobs.ci-cd.runs-on = "ubuntu-latest";
jobs.ci-cd.steps = [
{ uses = "actions/checkout@v2.4.0"; }
{ run = "npm i"; }
{ run = "npm run build"; }
{ run = "npm run test"; }
{ run = "aws s3 sync ./build s3://some-s3-bucket"; }
];
};
}
This only works because this project has another module with:
{lib, ...}:
{
options.files = submodule {
options.yaml.type = lib.types.attrsOf lib.types.anything;
};
}
But if we always set ci-cd.yaml like that, no complexity has been hidden, and requires copy and past it in every project.
Since most CI/CD are just: 'Pre Build', 'Build', 'Test', 'Deploy'
What most projects really need is something like:
# any module file (maybe project.nix)
{
# our build steps
config.gh-actions.setup = "npm i";
config.gh-actions.build = "npm run build";
config.gh-actions.test = "npm run test";
config.gh-actions.deploy = "aws s3 sync ./build s3://some-s3-bucket";
}
Adding this to project.nix, throws an error undefined config.gh-actions
, and command fails.
It doesn't knows these options.
To make aware of it, we had to add options
schema of that.
# gh-actions-options.nix
{ lib, ...}:
{
# a property 'gh-actions.setup'
options.gh-actions.setup = lib.mkOption {
default = "echo setup";
description = "Command to run before build";
example = "npm i";
type = lib.types.str;
};
# a property 'gh-actions.build'
options.gh-actions.build = lib.mkOption {
default = "echo build";
description = "Command to run as build step";
example = "npm run build";
type = lib.types.str;
};
# a property 'gh-actions.test'
options.gh-actions.test = lib.mkOption {
default = "echo test";
description = "Command to run as test step";
example = "npm test";
type = lib.types.str;
};
# a property 'gh-actions.deploy'
options.gh-actions.deploy = lib.mkOption {
default = "echo deploy";
description = "Command to run as deploy step";
example = "aws s3 sync ./build s3://my-bucket";
type = lib.types.lines;
};
}
Or using lib.types.fluent
# gh-actions-options.nix
{ lib, ...}:
lib.types.fluent {
options.gh-actions.options = {
# defines a property 'gh-actions.setup'
setup.default = "echo setup"; #default is string
setup.mdDoc = "Command to run before build";
setup.example = "npm i";
# defines a property 'gh-actions.build'
build.default = "echo build";
build.mdDoc = "Command to run as build step";
build.example = "npm run build";
# defines a property 'gh-actions.test'
test.default = "echo test";
test.mdDoc = "Command to run as test step";
test.example = "npm test";
# defines a property 'gh-actions.deploy'
deploy.default = "echo deploy";
deploy.mdDoc = "Command to run as deploy step";
deploy.example = "aws s3 sync ./build s3://my-bucket";
deploy.type = lib.types.lines;
};
}
Now, previous config can be used, but it does nothing, it doesn't create yaml.
It knowns what options can be accepted as config
, but not what to do with it.
The following code uses parameter config
that has all evaluated config
values.
# gh-actions.nix
{ config, lib, ... }:
{
imports = [ ./gh-actions-options.nix ];
# use other module that simplify file creation to create config file
files.yaml."/.github/workflows/ci-cd.yaml".jobs.ci-cd.steps = [
{ uses = "actions/checkout@v2.4.0"; }
{ run = config.gh-actions.setup; } #
{ run = config.gh-actions.build; } # Read step scripts from
{ run = config.gh-actions.test; } # config.gh-actions
{ run = config.gh-actions.deploy"; } #
];
files.yaml."/.github/workflows/ci-cd.yaml".on = "push";
files.yaml."/.github/workflows/ci-cd.yaml".jobs.ci-cd.runs-on = "ubuntu-latest";
}
Now it can be imported and set 'setup', 'build', 'test' and 'deploy' configs
# any other module file, maybe project.nix
{
imports = [ ./gh-actions.nix ];
gh-actions.setup = "echo 'paranaue'";
gh-actions.build = "echo 'paranaue parana'";
gh-actions.build = "echo 'paranaue'";
gh-actions.deploy = ''
echo "paranaue
parana"
'';
}
If something that is not a string is set, an error will raise, cheking it against the options schema.
There are other types that can be used (some of them):
- lib.types.bool
- lib.types.path
- lib.types.package
- lib.types.int
- lib.types.ints.unsigned
- lib.types.ints.positive
- lib.types.ints.port
- lib.types.ints.between
- lib.types.str
- lib.types.lines
- lib.types.enum
- lib.types.submodule
- lib.types.nullOr (typed nullable)
- lib.types.listOf (typed array)
- lib.types.attrsOf (typed hash map)
- lib.types.uniq (typed set)
And lib has some modules helpers functions like:
- lib.mkIf : to only set a property if some informaiton is true
- lib.optionals : to return an array or an empty array
- lib.optionalString: to return an string or an empty string