sini/files
{ "createdAt": "2026-05-21T19:09:47Z", "defaultBranch": "main", "description": "In-repository file generation flake-parts module", "fullName": "sini/files", "homepage": "", "language": "Nix", "name": "files", "pushedAt": "2026-05-29T19:49:46Z", "stargazersCount": 5, "topics": [], "updatedAt": "2026-07-06T00:18:46Z", "url": "https://github.com/sini/files"}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.
Table of Contents
Section titled “Table of Contents”- [Motivation]!(#motivation)
- [What this fork adds]!(#what-this-fork-adds)
- [Templates]!(#templates)
- [Quick start]!(#quick-start)
- [With treefmt]!(#with-treefmt)
- [With derivation sources]!(#with-derivation-sources)
- [In a monorepo]!(#in-a-monorepo)
- [API reference]!(#api-reference)
- [
files.file]!(#filesfile) - [
files.treefmt]!(#filestreefmt) - [
files.formatters]!(#filesformatters) - [Formatter priority]!(#formatter-priority)
- [
files.files(list API)]!(#filesfiles-list-api) - [Other options]!(#other-options)
- [
- [Without flake-parts]!(#without-flake-parts)
Motivation
Section titled “Motivation”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.
What this fork adds
Section titled “What this fork adds”- 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.
Templates
Section titled “Templates”Working examples live in [templates/]!(templates/):
- [
flake-parts]!(templates/flake-parts/) — flake-parts + import-tree with treefmt, global formatters, per-file overrides, onChange hooks, and both APIs - [
bare-flake]!(templates/bare-flake/) — vanilla flake usingevalModules, no flake-parts dependency - [
no-flake]!(templates/no-flake/) — puredefault.nixwithimport, no flake infrastructure - [
dag]!(templates/dag/) — composing a single file from sections across multiple modules using dag for topological ordering
Quick start
Section titled “Quick start”{ 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 disknix run .#diff-files # preview what would changenix run .#diff-files -- -v # preview with full diffsnix flake check # verify they matchWith treefmt
Section titled “With treefmt”Format 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";
}; };}With derivation sources
Section titled “With derivation sources”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;};In a monorepo
Section titled “In a monorepo”For sub-flakes that aren’t at the git root, set relativeRoot so the
writer puts files in the right directory:
perSystem = { config, ... }: { files.relativeRoot = "packages/my-app"; files.file."README.md".text = "# My App"; packages.write-files = config.files.writer.drv;};API reference
Section titled “API reference”All options live under perSystem.files.
files.file
Section titled “files.file”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;};files.treefmt
Section titled “files.treefmt”| Option | Type | Default | Description |
|---|---|---|---|
enable | bool | false | Format entries through nix fmt. |
package | package | config.formatter | Treefmt wrapper to use. |
files.formatters
Section titled “files.formatters”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 '';Formatter priority
Section titled “Formatter priority”- Per-file
format(highest) - Global
formattersby extension treefmt(if enabled)- No formatting (passthrough)
files.files (list API)
Section titled “files.files (list API)”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 ''; }];Other options
Section titled “Other options”| 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). |
Without flake-parts
Section titled “Without flake-parts”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]!(templates/no-flake/default.nix).