From 93bff3aadedb32e416159ad382337d73b6e5351d Mon Sep 17 00:00:00 2001 From: jackjohn7 Date: Sun, 12 Apr 2026 02:46:16 -0500 Subject: [PATCH] init templates --- README.md | 24 ++ modules/templates.nix | 9 + templates/generic/flake.nix | 11 + .../packages/boilerplate/boilerplate.py | 233 ++++++++++++++++++ .../packages/boilerplate/default.nix | 32 +++ templates/generic/nix-modules/parts.nix | 10 + templates/generic/nix-modules/shell.nix | 26 ++ 7 files changed, 345 insertions(+) create mode 100644 modules/templates.nix create mode 100644 templates/generic/flake.nix create mode 100644 templates/generic/nix-modules/packages/boilerplate/boilerplate.py create mode 100644 templates/generic/nix-modules/packages/boilerplate/default.nix create mode 100644 templates/generic/nix-modules/parts.nix create mode 100644 templates/generic/nix-modules/shell.nix diff --git a/README.md b/README.md index df3014b..8d0f263 100644 --- a/README.md +++ b/README.md @@ -33,3 +33,27 @@ boilerplate feature my-feature boilerplate layer my-layer boilerplate package my-package ``` + +## Templates + +I've found myself copying a lot of nix files from one project to the next. I'd like to reduce the +amount of time I spend doing this, so I'm just going to be making reusable nix flake templates. +These are defined in the `templates` directory and exposed in `modules/templates.nix`. They're +separated like that for both cleanliness and to avoid funkiness with `import-tree`. + +You can use my templates like so (using my generic flake for example): + +```sh +nix flake init -t github:jackjohn7/nixconf#generic +``` + +### Generic + +My _generic_ template is just a basic flake following the +[dendritic pattern](https://dendrix.oeiuwq.com/Dendritic.html) based on this repository itself. +I have also included the `boilerplate` package so that boilerplate for basic pieces can just be +generated instead of copied, pasted, and cleaned up. + +If you find any bugs or want to enhance it, feel free to open a PR. I can't promise I'll accept +your change since I use it myself but you'll still have your own fork at the end of the day for +your own consumption. diff --git a/modules/templates.nix b/modules/templates.nix new file mode 100644 index 0000000..14f1263 --- /dev/null +++ b/modules/templates.nix @@ -0,0 +1,9 @@ +{ ... }: +{ + flake.templates = { + generic = { + path = ../templates/generic; + description = "A generic dendritic flake using patterns I enjoy"; + }; + }; +} diff --git a/templates/generic/flake.nix b/templates/generic/flake.nix new file mode 100644 index 0000000..3d702f9 --- /dev/null +++ b/templates/generic/flake.nix @@ -0,0 +1,11 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + flake-parts.url = "github:hercules-ci/flake-parts"; + import-tree.url = "github:vic/import-tree"; + }; + + outputs = + inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } (inputs.import-tree ./nix-modules); +} diff --git a/templates/generic/nix-modules/packages/boilerplate/boilerplate.py b/templates/generic/nix-modules/packages/boilerplate/boilerplate.py new file mode 100644 index 0000000..4b98ad0 --- /dev/null +++ b/templates/generic/nix-modules/packages/boilerplate/boilerplate.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +import textwrap +from pathlib import Path + + +KIND_LAYOUTS = { + "layer": Path("nix-modules/features/layers"), + "feature": Path("nix-modules/features"), + "host": Path("nix-modules/hosts"), + "package": Path("nix-modules/packages"), +} + + +NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*$") + + +def nix_string(value: str) -> str: + return json.dumps(value) + + +def git_root() -> Path | None: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + check=False, + capture_output=True, + text=True, + ) + if result.returncode != 0: + return None + return Path(result.stdout.strip()) + + +def host_templates(name: str) -> dict[Path, str]: + return { + Path("default.nix"): + textwrap.dedent( + f"""\ + {{ self, inputs, ... }}: + {{ + flake.nixosConfigurations.{name} = inputs.nixpkgs.lib.nixosSystem {{ + modules = [ + self.nixosModules.{name}Configuration + ]; + }}; + }} + """ + ), + Path("configuration.nix"): + textwrap.dedent( + f"""\ + {{ self, inputs, ... }}: + {{ + flake.nixosModules.{name}Configuration = + {{ pkgs, lib, ... }}: + {{ + imports = [ + self.nixosModules.{name}Hardware + ]; + }}; + }} + """ + ), + Path("hardware-configuration.nix"): + textwrap.dedent( + f"""\ + {{ self, inputs, ... }}: + {{ + flake.nixosModules.{name}Hardware = + {{ lib, pkgs, ... }}: + {{ + config = {{ + # Copy the generated hardware configuration here. + }}; + }}; + }} + """ + ), + } + + +def template(kind: str, name: str) -> str: + if kind in {"layer", "feature"}: + return textwrap.dedent( + f"""\ + {{ self, inputs, ... }}: + {{ + flake.nixosModules.{name} = {{ pkgs, lib, ... }}: + {{ + config = {{ + }}; + }}; + }} + """ + ) + + if kind == "host": + quoted_name = nix_string(name) + return textwrap.dedent( + f"""\ + {{ self, inputs, ... }}: + {{ + flake.nixosModules.{name} = {{ pkgs, lib, ... }}: + {{ + config = {{ + networking.hostName = {quoted_name}; + # system.stateVersion = "25.11"; + }}; + }}; + + flake.nixosConfigurations.{name} = inputs.nixpkgs.lib.nixosSystem {{ + modules = [ + self.nixosModules.{name} + ]; + }}; + }} + """ + ) + + if kind == "package": + quoted_name = nix_string(name) + return textwrap.dedent( + f"""\ + {{ self, inputs, ... }}: + {{ + perSystem = + {{ pkgs, ... }}: + {{ + packages.{name} = pkgs.writeShellApplication {{ + name = {quoted_name}; + runtimeInputs = [ ]; + text = '' + echo "TODO: implement {name}" + ''; + }}; + }}; + }} + """ + ) + + raise ValueError(f"Unsupported kind: {kind}") + + +def planned_files(kind: str, name: str, multifile: bool) -> dict[Path, str]: + if kind == "host": + return host_templates(name) + + target = Path("default.nix") if multifile else Path(f"{name}.nix") + return {target: template(kind, name)} + + +def write_file(path: Path, content: str) -> None: + if path.exists(): + raise FileExistsError(f"Refusing to overwrite existing path: {path}") + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content.rstrip() + "\n", encoding="utf-8") + + +def stage(paths: list[Path], root: Path) -> None: + subprocess.run( + ["git", "add", "--", *[str(path.relative_to(root)) for path in paths]], + check=True, + cwd=root, + ) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Generate Nix boilerplate modules") + parser.add_argument("kind", choices=sorted(KIND_LAYOUTS)) + parser.add_argument("name") + parser.add_argument("--no-git-stage", action="store_true", help="Do not stage created files") + parser.add_argument("--dry-run", action="store_true", help="Print planned files without creating them") + parser.add_argument( + "--multifile", + action="store_true", + help="Create /default.nix instead of .nix", + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + + if not NAME_RE.fullmatch(args.name): + print( + "name must match ^[A-Za-z0-9][A-Za-z0-9_.-]*$", + file=sys.stderr, + ) + return 2 + + root = git_root() + if root is None: + if args.dry_run or args.no_git_stage: + root = Path.cwd() + else: + print("not inside a git repository; use --no-git-stage", file=sys.stderr) + return 2 + + files = planned_files(args.kind, args.name, args.multifile) + base = root / KIND_LAYOUTS[args.kind] + target_base = base / args.name if args.kind == "host" else base + targets = [target_base / rel for rel in files] + + if args.dry_run: + print("dry run: no files will be created") + for target in targets: + print(target.relative_to(root)) + return 0 + + try: + for rel_path, content in files.items(): + write_file(target_base / rel_path, content) + except FileExistsError as err: + print(err, file=sys.stderr) + return 1 + + if not args.no_git_stage: + stage(targets, root) + + for target in targets: + print(target.relative_to(root)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/templates/generic/nix-modules/packages/boilerplate/default.nix b/templates/generic/nix-modules/packages/boilerplate/default.nix new file mode 100644 index 0000000..9498389 --- /dev/null +++ b/templates/generic/nix-modules/packages/boilerplate/default.nix @@ -0,0 +1,32 @@ +{ self, inputs, ... }: +{ + perSystem = + { pkgs, lib, ... }: + { + packages.boilerplate = pkgs.stdenvNoCC.mkDerivation { + pname = "boilerplate"; + version = "0.1.0"; + + src = ./.; + + dontConfigure = true; + dontBuild = true; + + installPhase = '' + runHook preInstall + + substituteInPlace boilerplate.py \ + --replace-fail '#!/usr/bin/env python3' '#!${pkgs.python3}/bin/python3' + install -Dm755 boilerplate.py "$out/bin/boilerplate" + + runHook postInstall + ''; + + meta = { + description = "Generate boilerplate Nix modules"; + license = lib.licenses.mit; + mainProgram = "boilerplate"; + }; + }; + }; +} diff --git a/templates/generic/nix-modules/parts.nix b/templates/generic/nix-modules/parts.nix new file mode 100644 index 0000000..657e3ca --- /dev/null +++ b/templates/generic/nix-modules/parts.nix @@ -0,0 +1,10 @@ +{ + config = { + systems = [ + "x86_64-linux" + "x86_64-darwin" + "aarch64-linux" + "aarch64-darwin" + ]; + }; +} diff --git a/templates/generic/nix-modules/shell.nix b/templates/generic/nix-modules/shell.nix new file mode 100644 index 0000000..baffcb3 --- /dev/null +++ b/templates/generic/nix-modules/shell.nix @@ -0,0 +1,26 @@ +{ ... }: +{ + perSystem = + { pkgs, self', ... }: + { + # An example devshell with some Rust and Nix tools + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + bacon + cargo + rust-analyzer + rustc + rustfmt + clippy + glibc + sea-orm-cli + nixfmt + nil + alejandra + self'.packages.boilerplate + ]; + nativeBuildInputs = [ pkgs.pkg-config ]; + env.RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; + }; + }; +}