Helps static file/configuration creation with Nix and devshell.
There is a bunch of ways static file/configuration are hard, this will help you generate, validate and distribute JSON, YAML, TOML or TXT.
Your content will be defined in Nix Language, it means you can use variables, functions, imports, read files, etc.
The modular system helps layering configurations, hiding complexity and making it easier for OPS teams.
Your content modules could optionally be well defined and type checked in build proccess with this same tool.
Or you could use Nix as package manager and install any tool to validate your configuration (ie integrating it with existing JSON Schema).
Nix integrates well with git and http, it could be also used to read JSON, YAML, TOML, zip and gz files.
In fact Nix isn't a configuration tool but a package manger, we are only using it as configuration tool because the language is simple and flexible.
You can recreate files of a repository directly to your local machine by running nix develop <flake-uri> --build
, example:
# copy all my dogfood to your current folder
nix develop github:cruel-intentions/devshell-files --build
With help of Nix and devshell you could install any development or deployment tool of its 80 000 packages.
Installing Nix
curl -sSf -L https://install.determinate.systems/nix | sh -s -- install
Configuring new projects:
nix flake new -t github:cruel-intentions/devshell-files my-project
cd my-project
git init
git add *.nix flake.lock
Configuring existing projects:
nix flake new -t github:cruel-intentions/devshell-files ./
git add *.nix
git add flake.lock
nix develop --build
or entering in shell with all commands and alias
nix develop -c $SHELL
# to list commands and alias
# now run: menu
Creating JSON, TEXT, TOML or YAML files
# examples/hello.nix
#
# this is one nix file
{
files.json."/generated/hello.json".hello = "world";
files.toml."/generated/hello.toml".hello = "world";
files.yaml."/generated/hello.yaml".hello = "world";
files.hcl."/generated/hello.hcl".hello = "world";
files.text."/generated/hello.txt" = "world";
}
Your file can be complemented with another module
# examples/world.nix
# almost same as previous example
# but show some language feature
let
name = "hello"; # a variable
in
{
files = {
json."/generated/${name}.json".baz = ["foo" "bar" name];
toml."/generated/${name}.toml".baz = ["foo" "bar" name];
yaml = {
"/generated/${name}.yaml" = {
baz = [
"foo"
"bar"
name
];
};
};
};
}
Content generated by those examples are in generated
# ie ./generated/hello.yaml
baz:
- foo
- bar
- hello
hello: world
This project is configured by module project.nix
# ./project.nix
{
# import other modules
imports = [
./examples/hello.nix
./examples/world.nix
./examples/readme.nix
./examples/gitignore.nix
./examples/license.nix
./examples/interpolation.nix
./examples/docs.nix
./examples/book.nix
./examples/services.nix
./examples/nim.nix
./examples/nushell.nix
./examples/watch.nix
];
# My shell name
devshell.name = "devshell-files";
# install development or deployment tools
packages = [
"convco"
# now we can use 'convco' command https://convco.github.io
# but could be:
# "awscli"
# "azure-cli"
# "cargo"
# "conda"
# "go"
# "nim"
# "nodejs"
# "nodejs-18_x"
# "nushell"
# "pipenv"
# "python39"
# "ruby"
# "rustc"
# "terraform"
# "yarn"
# look at https://search.nixos.org for more packages
];
# create alias
files.alias.feat = ''convco commit --feat $@'';
files.alias.fix = ''convco commit --fix $@'';
files.alias.docs = ''convco commit --docs $@'';
files.alias.alou = ''
#!/usr/bin/env python
print("Alo!") # is hello in portuguese
'';
# now we can use feat, fix, docs and alou commands
# create .envrc for direnv
files.direnv.enable = true;
# disabe file creation when entering in the shell
# call devshell-files instead
# files.on-call = true;
}
This README.md is also a module defined as above
# There is a lot things we could use to write static file
# Basic intro to nix language https://github.com/tazjin/nix-1p
# Some nix functions https://teu5us.github.io/nix-lib.html
{lib, ...}:
{
files.text."/README.md" = builtins.concatStringsSep "\n" [
"# Devshell Files Maker"
(builtins.readFile ./readme/toc.md)
(builtins.readFile ./readme/about.md)
(builtins.readFile ./readme/installation.md)
(builtins.import ./readme/examples.nix)
((builtins.import ./readme/modules.nix) lib)
(builtins.readFile ./readme/todo.md)
(builtins.readFile ./readme/issues.md)
(builtins.readFile ./readme/seeAlso.md)
];
}
Our .gitignore is defined like this
# ./examples/gitignore.nix
{
# create my .gitignore copying ignore patterns from
# github.com/github/gitignore
files.gitignore.enable = true;
files.gitignore.template."Global/Archives" = true;
files.gitignore.template."Global/Backup" = true;
files.gitignore.template."Global/Diff" = true;
files.gitignore.pattern."**/.data" = true;
files.gitignore.pattern."**/.direnv" = true;
files.gitignore.pattern."**/.envrc" = true;
files.gitignore.pattern."**/.gitignore" = true;
files.gitignore.pattern."**/flake.lock" = true;
}
And our LICENSE file is
# ./examples/license.nix
{
# LICENSE file creation
# using templates from https://github.com/spdx/license-list-data
files.license.enable = true;
files.license.spdx.name = "MIT";
files.license.spdx.vars.year = "2023";
files.license.spdx.vars."copyright holders" = "Cruel Intentions";
}
Jump this part if aready know Nix Lang, if don't there is a small concise content of Nix Lang.
If one page is too much to you, the basic is:
:
defines a new function, arg: "Hello ${arg}"
=
instaed of :
, { attr-key = "value"; }
;
instead of ,
and they aren't optional,
ie. [ "some" "value" ]
name | JSON | NIX |
---|---|---|
null | null |
null |
bool | true |
true |
int | 123 |
123 |
float | 12.3 |
12.3 |
string | "string" |
"string" |
array | ["some","array"] |
["some" "array"] |
object | {"some":"value"} |
{ some = "value"; } |
multiline-string | ''... multiline string ... '' |
|
variables | let my-var = 1; other-var = 2; in my-var + other-var |
|
function | my-arg: "Hello ${my-arg}!" |
|
variable-function | let my-function = my-arg: "Hello ${my-arg}!"; in ... |
|
calling-a-function | ... in my-function "World" |
Modules can be defined in two formats:
{ # <|
imports = []; # |
config = {}; # | module info
options = {}; # |
} # <|
All those attributes are optional
Functions has following arguments:
config
with all evaluated configs values, pkgs
with all nixpkgs available.lib
library of useful functions....
to ignore them){ config, pkgs, lib, ... }: # <| function args
{ # <|
imports = []; # |
config = {}; # | module info
options = {}; # |
} # <|
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:
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.
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 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):
And lib has some modules helpers functions like:
Now to not just copy and past it everywhere, we could create a git repository, ie. gh-actions
Then we could let nix manage it for us adding it to flake.nix file like
{
description = "Dev Environment";
inputs.dsf.url = "github:cruel-intentions/devshell-files";
inputs.gha.url = "github:cruel-intentions/gh-actions";
# for private repository use git url
# inputs.gha.url = "git+ssh://git@github.com/cruel-intentions/gh-actions.git";
outputs = inputs: inputs.dsf.lib.mkShell [
"${inputs.gha}/gh-actions.nix"
./project.nix
];
}
Or manage version adding it directly to project.nix (or any other module file)
{
imports =
let gh-actions = builtins.fetchGit {
url = "git+ssh://git@github.com/cruel-intentions/gh-actions.git";
ref = "master";
rev = "46eead778911b5786d299ecf1a95c9ed4c130844";
};
in [
"${gh-actions}/gh-actions.nix"
];
}
To document our modules is simple, we just need to use config.files.docs
as follow
# examples/docs.nix
{lib, pkgs, ...}:
{
files.docs."/gh-pages/src/modules/alias.md".modules = [ ../modules/alias.nix ../modules/alias-complete.nix ];
files.docs."/gh-pages/src/modules/cmds.md".modules = [ ../modules/cmds.nix ];
files.docs."/gh-pages/src/modules/files.md".modules = [ ../modules/files.nix ];
files.docs."/gh-pages/src/modules/git.md".modules = [ ../modules/git.nix ];
files.docs."/gh-pages/src/modules/on-call.md".modules = [ ../modules/startup.nix ];
files.docs."/gh-pages/src/modules/gitignore.md".modules = [ ../modules/gitignore.nix ];
files.docs."/gh-pages/src/modules/hcl.md".modules = [ ../modules/hcl.nix ];
files.docs."/gh-pages/src/modules/json.md".modules = [ ../modules/json.nix ];
files.docs."/gh-pages/src/modules/mdbook.md".modules = [ ../modules/mdbook.nix ];
files.docs."/gh-pages/src/modules/nim.md".modules = [ ../modules/nim.nix ];
files.docs."/gh-pages/src/modules/nushell.md".modules = [ ../modules/nushell.nix ];
files.docs."/gh-pages/src/modules/nush.md".modules = [ ../modules/nush.nix ../modules/nuon.nix ];
files.docs."/gh-pages/src/modules/rc.md".modules = [ ../modules/services/rc-devshell.nix ];
files.docs."/gh-pages/src/modules/services.md".modules = [ ../modules/services.nix ];
files.docs."/gh-pages/src/modules/spdx.md".modules = [ ../modules/spdx.nix ];
files.docs."/gh-pages/src/modules/text.md".modules = [ ../modules/text.nix ];
files.docs."/gh-pages/src/modules/toml.md".modules = [ ../modules/toml.nix ];
files.docs."/gh-pages/src/modules/watch.md".modules = [ ../modules/watch ];
files.docs."/gh-pages/src/modules/yaml.md".modules = [ ../modules/yaml.nix ];
}
And publish this mdbook to github pages with book-as-gh-pages
alias.
Builtin Modules are modules defined with this same package.
They are already included when we use this package.
files.alias
, create bash script aliasfiles.cmds
, install packages from nix repositoryfiles.docs
, convert our modules file into markdown using nmdfiles.git
, configure git with file creationfiles.on-call
, connfigure file to created only when devshell-files command is called, not on shell startfiles.gitignore
, copy .gitignore from templatesfiles.hcl
, create HCL files with nix syntaxfiles.json
, create JSON files with nix syntaxfiles.mdbook
, convert your markdown files to HTML using mdbookfiles.nim
, similar to files.alias
, but compiles Nim codefiles.nus
, similar to files.alias
, but runs in Nushellfiles.nush
, similar to files.nus
, but for subcommandsfiles.services
, process supervisor for development services using s6files.rc
, WIP, process supervisor for development services using s6-rcfiles.spdx
, copy LICENSE from templatesfiles.text
, create free text files with nix syntaxfiles.toml
, create TOML files with nix syntaxfiles.watch
, create an alias and service to run command when file changes using inotify-toolsfiles.yaml
, create YAML files with nix syntaxOur documentation is generated by files.text
, files.docs
and files.mdbook
This project uses git as version control, if your are using other version control system it may not work.
*nix
general definition of Unix/Linux