A set of Nix development environment helpers from Determinate Systems.
To use the functions in this flake, add the overlay:
{
pkgs = import inputs.nixpkgs {
overlays = [ inputs.up.overlays.default ];
};
}This provides the lib.mkTaskRunner, lib.mkProcessTree, and other functions that you'll see below.
We also recommend using the exported flake schemas if you create a flake that outputs one of Up's output types.
Process trees are process-compose configurations built with Nix. Here's an example:
{
processTrees = forEachSupportedSystem (
{ pkgs, system }:
{
postgres = pkgs.lib.mkProcessTree {
description = "Run Postgres locally";
packages = with pkgs; [
(postgresql_18.withPackages (p: with p; [ pg_uuidv7 ]))
redis
];
# environment variables that become exports in the command script
environment = rec {
PGDATABASE = "testing";
PGPORT = toString 5432;
PGDATA = "$PWD/.state/postgres";
PGHOST = PGDATA;
};
processes = {
postgres-setup = {
command = ''
mkdir -p $PGDATA
[[ -e "$PGDATA/PG_VERSION" ]] || initdb --no-locale --encoding=UTF8
'';
};
postgres = {
command = "postgres";
depends_on.postgres-setup.condition = "process_completed_successfully";
readiness_probe = {
exec.command = "pg_isready";
initial_delay_seconds = 1;
period_seconds = 2;
failure_threshold = 100;
};
};
postgres-post-startup = {
command = ''
createdb $PGDATABASE || true
psql -c "CREATE EXTENSION IF NOT EXISTS pg_uuidv7;"
'';
depends_on.postgres.condition = "process_healthy";
};
};
};
}
);
}To run:
nix run ".#processTrees.<system>.postgres"You can add process trees to your development environment to run them more easily:
pkgs.mkShell {
packages = [
self.processTrees.${system}.postgres
# other packages
];
}| Attribute | Description | Default |
|---|---|---|
name |
The name of the runnable executable for the process tree | The key in the flake's processTrees attribute set |
processes |
An attribute set of processes to run | |
environment |
Environment variables to pass to all processes (supports variables like $PWD) |
{ } |
package |
The process-compose package | pkgs.process-compose |
packages |
A list of packages to make available to all processes | |
log_level |
info (the default), trace, debug, warn, error, fatal, or panic |
info |
configFileName |
The name of the generated configuration file | process-compose.yaml |
excludeShellChecks |
shellcheck rules to disable in all processes | [ ] |
| Attribute | Description | Default |
|---|---|---|
command |
The runnable command (verified by shellcheck) | |
working_dir |
The working directory for the process | |
watch |
Configuration for a file watcher | |
environment |
Environment variables to pass to the process (supports variables like $PWD) |
{ } |
packages |
A list of packages to make available to the process's command |
[ ] |
description |
The description of the process that shows up in nix flake show output |
|
depends_on |
The processes that the process depends on | { } |
readiness_probe |
A readiness probe for the process | |
liveness_probe |
A liveness probe for the process | |
shutdown |
A shutdown directive for the process | |
excludeShellChecks |
shellcheck rules to disable in the process's command |
[ ] |
Your process tree can have watchexec-driven processes that are restarted when files at specified paths change.
| Attribute | Description | Default |
|---|---|---|
paths |
The filesystem paths to watch | [ ] |
action |
restart (the default), stop, or start |
restart |
ignore |
The filesystem paths to ignore | [ ] |
debounce |
The number of milliseconds to debounce |
Task runners are generated CLI tools that enable you to run tasks. We use the lovely gum to make that generated CLI pretty and lively.
Here's how you can create a task runner in your flake:
{
taskRunners = forEachSupportedSystem (
{ pkgs, system }:
{
work = pkgs.lib.mkTaskRunner {
name = "work";
description = "Run linters and formatters";
packages = with pkgs; [
git
nixfmt
];
tasks = {
check-nix-formatting = {
description = "Check Nix formatting";
command = "git ls-files -z '*.nix' | xargs -0 nixfmt check";
};
format-nix = {
description = "Format Nix files";
command = "git ls-files -z '*.nix' | xargs -0 nixfmt";
after = [ "check-nix-formatting" ];
};
};
};
}
);
}That generates a CLI tool for you called work that provides this help output:
╭───────────────────────────────────╮
│ work — Run linters and formatters │
╰───────────────────────────────────╯
Available tasks:
check-nix-formatting Check Nix formatting
format-nix Format Nix files
all Run all tasks in dependency order
To run it:
nix run ".#taskRunners.<system>.default"You can also task runners to your development environment to run them more easily:
pkgs.mkShell {
packages = [
self.taskRunners.${system}.work
# other packages
];
}The generated all command runs all of your tasks and fails if any of those tasks don't return an exit code of 0.
And it runs those tasks as a directed acyclic graph that's built when you specify before or after tasks (otherwise, it runs them in alphabetical order).
Here's an example:
{
tasks = {
earlier = {
command = "echo 'Earlier'";
before = [ "later" ];
};
later.command = "echo 'Later'";
};
}The function converts each task into a proper Bash script using writeShellApplication, which means that the command is linted by shellcheck.
| Attribute | Description | Default |
|---|---|---|
name |
The name of the runnable executable for the runner | The key in the flake's taskRunners attribute set |
description |
The description of the runner that shows up in nix flake show output |
task runner |
environment |
Environment variables passed to all of the runner's tasks (supports variables like $PWD) |
{ } |
packages |
A list of packages available to all the runner's tasks | |
tasks |
An attribute set of tasks to run |
Here's an example:
pkgs.lib.mkTaskRunner {
name = "rt";
description = "Rust development tasks 🦀";
environment.RUST_LOG = "trace";
packages = with pkgs; [
cargo
rustfmt
];
tasks = {
forrmat-rust = { ... };
};
}| Attribute | Description | Default |
|---|---|---|
name |
The name for the task | The key in the runner's tasks attribute set |
description |
The description of the task that shows up in the CLI and in nix flake show output |
runnable task |
command |
The runnable command (verified by shellcheck) | |
environment |
Environment variables to pass to command (supports variables like $PWD) |
{ } |
packages |
A list of packages to make available to the command |
[ ] |
before |
The tasks before which the task needs to run | [ ] |
after |
The tasks after which the task needs to run | [ ] |
skip |
Whether to skip the task when running all |
false |
requireArgs |
Whether the command requires additional arguments | false |
confirm |
Whether the command requires confirmation to proceed | false |
raw |
Whether you want the command to return raw shell output | false |
aliases |
Aliases for the command | [ ] |
excludeShellChecks |
shellcheck rules to disable in the command | [ ] |
Here's an example:
{
forrmat-rust = {
command = "cargo fmt --all";
packages = with pkgs; [
cargo
rustfmt
];
description = "Format all Rust files in the repo";
before = [ "build" ];
after = [ "lint" ];
aliases = [ "f" ];
};
}Up has a special function called mkBenchmarkTask that generates benchmarking tasks that use hyperfine.
Here's an example task:
{
run-benchmarks =
pkgs.lib.mkBenchmarkTask {
description = "Benchmark the CLI";
after = [ "build" ];
commands = [
{
name = "run";
command = "./target/release/my-cli";
}
];
runs = 10;
};
}These attributes are available:
| Attribute | Description | Default |
|---|---|---|
commands |
The commands to benchmark (a list of sets with the attributes name and command) |
|
packages |
A list of packages available to the commands |
[] |
runs |
The number of times to run the commands |
|
minRuns |
The minimum number of times to run the commands |
|
maxRuns |
The maximum number of times to run the commands |
|
setup |
The command executed before all timing runs | |
prepare |
The command executed before each individual timing run | |
cleanup |
The command executed once after all timing runs for a benchmark have completed | |
exportJson |
The path to which to export results as JSON | |
exportMarkdown |
The path to which to export results as Markdown | |
exportCsv |
The path to which to export results as CSV | |
parameterScan |
Parameter scan parameters of the form { var, min, max, step? } |
|
package |
The Hyperfine package to use | pkgs.hyperfine |
Up has a special function called mkWatch that generates tasks monitored by watchexec.
Here's an example task:
{
rebuild-site = pkgs.lib.mkWatch {
description = "Rebuild site upon change";
packages = [ pkgs.pnpm ];
command = "pnpm run build";
paths = [ "src/content" ];
};
}These attributes are available:
| Attribute | Description | Default |
|---|---|---|
command |
The command to re-run upon change | |
packages |
A list of packages available to the command |
[] |
paths |
A list of paths to watch | [ "." ] |
extensions |
File extensions to watch | [ ] |
ignore |
A list of paths to ignore | [ ] |
debounce |
The number of milliseconds to debounce | |
environment |
Environment variables to pass to command (supports variables like $PWD) |
{ } |
package |
The watchexec package to use | pkgs.watchexec |
excludeShellChecks |
shellcheck rules to disable in the command | [ ] |
There are two types of environment variable sets: static and computed. Static sets are attribute sets of plain strings:
{
staticEnvVars.postgres = {
PGDATA = ".state/postgres";
PGDATABASE = "testing";
PGHOST = "127.0.0.1";
PGPORT = toString 5432;
};
}Computed sets are system specific and may be based on things like packages in Nixpkgs:
computedEnvVars = forEachSupportedSystem (
{ pkgs, ... }:
{
postgres.PGSSLCERT = "${pkgs.postgresql}/share/postgresql/root.crt";
}
);Up has a special function called mkTool that generates Bash scripts wrapping specific tools.
Here's an example:
let
procs = pkgs.lib.mkTool {
name = "procs";
tool = pkgs.bottom;
args = [
"--expanded"
"--default_widget_type=proc"
];
};
in
pkgs.mkShell {
packages = [
procs
];
}It's essentially an intuitive convenience wrapper around writeShellApplication.
These attributes are available:
| Attribute | Description | Default |
|---|---|---|
name |
The name of the runnable executable for the runner | |
tool |
The package available to the script | |
args |
A list of arguments to pass to the package | [] |
packages |
A list of packages to make available at runtime | [] |
environment |
Environment variables to pass to the script (supports variables like $PWD) |
{ } |
We also recommend using the flake schemas for added introspectability into your taskRunners, processTrees, and environment variable outputs:
{
schemas = inputs.up.exportedSchemas // {
# other schemas
};
}