A flake-parts module for managing in-repository
generated files. Declare file contents in Nix, write them with one command,
and verify they stay in sync via nix flake check.
This project is an independent, hard fork of mightyiam/files. It is maintained entirely separately and has no affiliation with, nor is it endorsed by, the original author.
- Motivation
- What this fork adds
- Templates
- Quick start
- With treefmt
- With derivation sources
- In a monorepo
- API reference
- Without flake-parts
The upstream project made several decisions that don't align with how we use it across den's template ecosystem:
- Removed flake export — upstream deliberately dropped
flakeModules.default, requiring consumers to useflake = falseand raw path imports. We need proper flake consumption for composability with other flake-parts modules. - Adopted experimental Nix features — the
|>pipe operator requiresextra-experimental-features = [ "pipe-operators" ]in every consuming flake'snixConfig. This created additional user friction. - Renamed
path_topathwithout alias — a breaking change with no migration path or notification for existing consumers. - No convenience API — the list-of-
{path, drv}interface requires boilerplate for the common case of writing text or copying a file. NixOS has hadenvironment.etc-style attrset APIs for years. - No multi-flake support — the writer hardcoded
git rev-parse --show-toplevelwith no way to target a subdirectory, making it unusable in monorepos. - Eager eval breaks
--no-build—lib.readFileat evaluation time meansnix flake check --no-buildfails when managed files haven't been written yet, which is the normal state for fresh clones. - No formatting pipeline — generated files often need to pass through formatters (nixfmt, prettier, jq) to match project style, but there was no way to post-process file content before writing.
- Works with and without flake-parts —
flakeModulefor flake-parts,modulefor vanilla flakes viaevalModules files.fileconvenience API —environment.etc-style attrset withtext,source,json,toml, andyamloptions- Structured data renderers —
json,toml, andyamloptions serialize Nix values directly, nopkgs.writers.*boilerplate - Diff preview —
nix run .#diff-filesshows what would change without writing;--verbosefor full diffs - treefmt-nix integration —
files.treefmt.enableformats all entries throughnix fmt - Formatters — global
files.formattersby extension and per-fileformatfor custom post-processing - onChange hooks — run shell commands after writing, with optional
runtimeInputsfor extra packages on the writer's PATH - Multi-flake repo support —
relativeRootfor monorepo sub-flakes - Lazy checks —
nix flake check --no-buildworks on fresh clones - Backwards compatibility —
path_andgitToplevelaliases - Stable Nix syntax - The experimental
|>operator is replaced withlib.pipe, so you don't need to enablepipe-operatorsin your nixConfig.
Working examples live in templates/:
flake-parts— flake-parts + import-tree with treefmt, global formatters, per-file overrides, onChange hooks, and both APIsbare-flake— vanilla flake usingevalModules, no flake-parts dependencyno-flake— puredefault.nixwithimport, no flake infrastructuredag— composing a single file from sections across multiple modules using dag for topological ordering
# flake.nix
{
inputs = {
files.url = "github:sini/files";
flake-parts.url = "github:hercules-ci/flake-parts";
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
};
outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } {
systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ];
imports = [ inputs.files.flakeModule ];
perSystem = { ... }: {
files.generateApp = true;
files.file.".gitignore".text = ''
result
.direnv
'';
files.file."README.md".text = ''
# My Project
Generated by Nix.
'';
};
};
}nix run .#write-files # write files to disk
nix run .#diff-files # preview what would change
nix run .#diff-files -- -v # preview with full diffs
nix flake check # verify they matchFormat generated files automatically using your project's treefmt-nix configuration. treefmt's own include/exclude rules apply — files that don't match any formatter pass through unchanged.
{
inputs = {
files.url = "github:sini/files";
flake-parts.url = "github:hercules-ci/flake-parts";
treefmt-nix.url = "github:numtide/treefmt-nix";
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
};
outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } {
systems = [ "x86_64-linux" ];
imports = [
inputs.files.flakeModule
inputs.treefmt-nix.flakeModule
];
perSystem = { config, pkgs, ... }: {
treefmt.programs.nixfmt.enable = true;
treefmt.programs.prettier.enable = true;
files.treefmt.enable = true;
# nixfmt formats this automatically
files.file."config.nix".text = "{foo=1;bar=2;}";
# prettier formats this automatically
files.file."README.md".text = "# Hello World";
# no formatter matches .txt — passes through unchanged
files.file."LICENSE".text = "MIT";
};
};
}Use source for files built from derivations, repo paths, or structured
data:
perSystem = { config, pkgs, ... }: {
# from structured Nix data
files.file.".github/workflows/ci.yaml".source =
pkgs.writers.writeYAML "ci.yaml" {
on.push = { };
jobs.check = {
runs-on = "ubuntu-latest";
steps = [
{ uses = "actions/checkout@v4"; }
{ run = "nix flake check"; }
];
};
};
# from a file in the repo
files.file."LICENSE".source = ./LICENSE.src;
packages.write-files = config.files.writer.drv;
};For sub-flakes that aren't at the git root, set relativeRoot so the
writer puts files in the right directory:
# packages/my-app/flake.nix
perSystem = { config, ... }: {
files.relativeRoot = "packages/my-app";
files.file."README.md".text = "# My App";
packages.write-files = config.files.writer.drv;
};All options live under perSystem.files.
Attrset of files to manage. The attribute name is the file path relative to the project root. Use slashes for subdirectories.
| Option | Type | Default | Description |
|---|---|---|---|
enable |
bool |
true |
Set false to suppress this file. |
text |
nullOr lines |
null |
Inline text content. Sets source automatically. |
json |
nullOr anything |
null |
JSON value to serialize. Sets source automatically. |
toml |
nullOr anything |
null |
TOML value to serialize. Sets source automatically. |
yaml |
nullOr anything |
null |
YAML value to serialize. Sets source automatically. |
source |
path |
— | Path or derivation for the file content. |
executable |
bool |
false |
chmod +x after writing. |
onChange |
str or { runtimeInputs, script } |
"" |
Shell commands run after all files are written. Runs under set -euo pipefail. |
format |
nullOr (name -> drv -> drv) |
null |
Per-file formatter. Overrides all other formatters. |
perSystem = { config, pkgs, ... }: {
files.file."scripts/deploy.sh" = {
text = ''
#!/bin/sh
echo "deploying..."
'';
executable = true;
};
# onChange as a plain string
files.file."flake.nix".text = "{}";
files.file."flake.nix".onChange = "echo 'flake updated'";
# onChange with runtimeInputs — packages are added to the writer's PATH
files.file.".envrc" = {
text = "use flake";
onChange = {
runtimeInputs = [ pkgs.direnv ];
script = "direnv reload";
};
};
# structured data — no pkgs.writers.* boilerplate
files.file."data/config.json".json = { version = 1; debug = false; };
files.file."data/settings.toml".toml = { database.host = "localhost"; };
files.file."data/compose.yaml".yaml = {
version = "3";
services.app.image = "myapp:latest";
};
# disable a file defined by a shared module
files.file."docs/internal.md".enable = false;
packages.write-files = config.files.writer.drv;
};| Option | Type | Default | Description |
|---|---|---|---|
enable |
bool |
false |
Format entries through nix fmt. |
package |
package |
config.formatter |
Treefmt wrapper to use. |
Global formatters keyed by file extension. Signature: name: source: derivation.
Per-file format overrides these. These override treefmt.
files.formatters.json = name: drv:
pkgs.runCommand "jqfmt-${name}" { nativeBuildInputs = [ pkgs.jq ]; } ''
jq --sort-keys . < ${drv} > $out
'';- Per-file
format(highest) - Global
formattersby extension treefmt(if enabled)- No formatting (passthrough)
The original list-based API. files.file entries merge into this list
automatically. Both APIs can be used together. The formatting pipeline
(treefmt, global formatters, per-file format) applies to both APIs.
| Option | Type | Default | Description |
|---|---|---|---|
path |
str |
— | File path relative to project root. |
drv |
package |
— | Derivation whose output is the file content. |
executable |
bool |
false |
chmod +x after writing. |
format |
nullOr (name -> drv -> drv) |
null |
Per-file formatter. Overrides all other formatters. |
onChange |
{ runtimeInputs, script } |
{} |
Shell commands run after writing if content changed. |
files.files = [
{
path = "README.md";
drv = pkgs.writeText "README.md" "# Hello";
}
{
path = "data/config.json";
drv = pkgs.writers.writeJSON "config.json" { version = 1; };
# per-file formatter works in both APIs
format = name: drv:
pkgs.runCommand "jqfmt-${name}" { nativeBuildInputs = [ pkgs.jq ]; } ''
jq --sort-keys . < ${drv} > $out
'';
}
];| Option | Type | Default | Description |
|---|---|---|---|
root |
path |
— | Root for check comparisons. Auto-set to self via flake-parts; set root = self; in vanilla flakes. |
relativeRoot |
str |
"" |
Subdir from git root for the writer. |
generateApp |
bool |
false |
Expose writer and diff as nix run .#write-files / nix run .#diff-files. |
writer.exeFilename |
singleLineStr |
"write-files" |
Writer executable name. |
writer.drv |
package |
(computed) | The writer derivation (read-only). |
diff.exeFilename |
singleLineStr |
"diff-files" |
Diff executable name. |
diff.drv |
package |
(computed) | The diff derivation (read-only). Shows what would change without writing. |
checks |
attrsOf package |
(computed) | Per-file check derivations (read-only). |
Use files.module directly with evalModules — no flake-parts dependency:
{
inputs = {
files.url = "github:sini/files";
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
};
outputs = { self, files, nixpkgs, ... }:
let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
eval = pkgs.lib.evalModules {
modules = [
files.module
{
config._module.args = { inherit pkgs; };
config.files.root = self;
config.files.file."README.md".text = "# Hello";
config.files.file.".gitignore".text = "result";
}
];
};
in {
checks.x86_64-linux = eval.config.files.checks;
packages.x86_64-linux.write-files = eval.config.files.writer.drv;
};
}For usage without any flake infrastructure, see
templates/no-flake.