From ae3fa667bda75f11881c9209dc67226f98bcfd95 Mon Sep 17 00:00:00 2001 From: Tom van der Lee Date: Thu, 17 Sep 2026 11:05:45 +0200 Subject: Add cross-platform Nix flake bootstrap Adds a flake providing shared modules for NixOS and standalone home-manager, a copyable NixOS host template, and the OpenSpec tooling/change scaffolding used to plan this work. --- .../changes/add-nix-flake-bootstrap/.openspec.yaml | 2 + openspec/changes/add-nix-flake-bootstrap/design.md | 39 ++++++++++++++ .../changes/add-nix-flake-bootstrap/proposal.md | 31 ++++++++++++ .../specs/nix-flake-bootstrap/spec.md | 47 +++++++++++++++++ openspec/changes/add-nix-flake-bootstrap/tasks.md | 59 ++++++++++++++++++++++ openspec/changes/archive/.gitkeep | 0 openspec/config.yaml | 32 ++++++++++++ openspec/specs/.gitkeep | 0 8 files changed, 210 insertions(+) create mode 100644 openspec/changes/add-nix-flake-bootstrap/.openspec.yaml create mode 100644 openspec/changes/add-nix-flake-bootstrap/design.md create mode 100644 openspec/changes/add-nix-flake-bootstrap/proposal.md create mode 100644 openspec/changes/add-nix-flake-bootstrap/specs/nix-flake-bootstrap/spec.md create mode 100644 openspec/changes/add-nix-flake-bootstrap/tasks.md create mode 100644 openspec/changes/archive/.gitkeep create mode 100644 openspec/config.yaml create mode 100644 openspec/specs/.gitkeep (limited to 'openspec') diff --git a/openspec/changes/add-nix-flake-bootstrap/.openspec.yaml b/openspec/changes/add-nix-flake-bootstrap/.openspec.yaml new file mode 100644 index 0000000..f080778 --- /dev/null +++ b/openspec/changes/add-nix-flake-bootstrap/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-16 diff --git a/openspec/changes/add-nix-flake-bootstrap/design.md b/openspec/changes/add-nix-flake-bootstrap/design.md new file mode 100644 index 0000000..133aa15 --- /dev/null +++ b/openspec/changes/add-nix-flake-bootstrap/design.md @@ -0,0 +1,39 @@ +## Context + +See proposal.md - Why. The repo is empty of Nix config; this design establishes the first module layout, so the choices below aren't tweaks to an existing architecture, they're the starting shape everything else builds on. Development happens on macOS (glootie), but glootie has no Nix installed and is not a target host in this change — so nothing here can be locally exercised on real hardware except through `nix flake check` / building `homeConfigurations."tom@linux"`. + +## Goals / Non-Goals + +**Goals:** +- Prove that one module (`users/tom.nix`) drives identical behavior through two different activation mechanisms (NixOS's home-manager module, standalone home-manager). +- Keep the flake's outputs buildable and checkable today, with zero real hardware and zero remote. +- Make the "add a real host" path (copy template → drop in generated hardware config → add flake output) mechanical, not something requiring new code. + +**Non-Goals:** +- Building a real, live host of any kind (NixOS or darwin) — only the template and the standalone `tom@linux` output are real in this change. +- Declarative disk partitioning, secrets management, or multi-host auto-discovery — all explicitly deferred (see proposal.md - Impact). + +## Decisions + +**Plain `flake.nix` + `lib/` helpers, not flake-parts.** flake-parts reduces boilerplate once a flake has many perSystem outputs across many systems, but this flake has exactly one target system per output type initially. A framework input that isn't earning its keep yet is the kind of extra surface this project is trying to avoid. Revisit if the outputs section grows unwieldy. + +**`hosts/template/` is never wired into `nixosConfigurations`.** The alternative — exposing `nixosConfigurations.template` so `nix flake check` exercises it — was rejected because its `hardware-configuration.nix` can only be made to build by inventing plausible-looking `fileSystems` entries for hardware that doesn't exist, which is fabricating success rather than validating anything. Leaving it unwired is honest about what is and isn't tested. + +**nix-darwin is not added as a flake input in this change.** An input with no output exercising it is dead weight in the lockfile, and it can't be evaluated meaningfully — glootie has no Nix installed, so there's no way to build or activate a darwin config here even experimentally. Bringing it in now would not actually strengthen the "shared module" claim, since that claim is already demonstrated by two *real* consumers of `modules/home` (`nixosConfigurations` path via home-manager's NixOS module, and the standalone `homeConfigurations."tom@linux"` output). Add nix-darwin in the follow-up change that configures a real Mac host, when it can be tested for real. Same reasoning applies to `nixos-hardware` (no known hardware yet). + +**Determinate Systems installer recommended for standalone Nix installs (documentation only, not enforced in code).** It enables flakes by default, avoiding a manual `nix.conf` edit — one less manual step that would otherwise need documenting as a "first, do this hack." The official installer remains a valid alternative and is noted in the README as such. + +**`modules/home/*` stays flat, no `linux/`/`darwin/` subdirectories yet.** The only divergence needed today is handled inline with `lib.mkIf pkgs.stdenv.isLinux` / `isDarwin` in individual module files. Splitting into per-OS directories now would structure for divergence that doesn't exist yet (there is no darwin consumer at all in this change). + +**No default `stateVersion` in the `mkNixos`/`mkHome` helpers.** A shared default would silently paper over the fact that this value must reflect the release actually in use at install time for each specific host/user — baking one in is the kind of hidden default that causes real (if usually minor) breakage later. Requiring it explicitly per host/user is the standard, non-hack idiom. + +## Risks / Trade-offs + +- **No remote to clone from during a real NixOS install** → Mitigation: documented as an explicit prerequisite in the README and flagged as follow-up work in the proposal; not something this change can solve since it depends on the user's choice of remote host. +- **No way to locally validate `nixosConfigurations` behavior (no NixOS host exists, and even the template is deliberately unbuilt)** → Mitigation: `nix flake check` still validates `lib`, `homeConfigurations."tom@linux"`, `formatter`, and `devShells`; NixOS-path validation is deferred to when a real host is added, at which point that host's build is the test. +- **Determinate Systems installer is a third-party tool, not the official NixOS project installer** → Mitigation: documented as a recommendation with the official installer named as a fallback, so the choice is visible and reversible, not baked into any script. +- **Flat `modules/home/` with inline `mkIf` branches could get noisy if darwin-specific config grows later** → Mitigation: acceptable now since there are zero darwin branches; the design explicitly allows splitting into per-OS directories in a later change once real divergence exists, rather than pre-building structure for it. + +## Open Questions + +- Whether to install bare Nix (package manager only, no activation) on glootie purely to enable local `nix flake check`/`nix build` validation ahead of a real install elsewhere, versus relying on CI once the repo has a remote. Deferring this doesn't change the specs, the module layout, or the task breakdown for this change — it only affects how validation is performed, and can be decided later. diff --git a/openspec/changes/add-nix-flake-bootstrap/proposal.md b/openspec/changes/add-nix-flake-bootstrap/proposal.md new file mode 100644 index 0000000..50f0db3 --- /dev/null +++ b/openspec/changes/add-nix-flake-bootstrap/proposal.md @@ -0,0 +1,31 @@ +## Why + +This repo currently has no Nix configuration at all — zero `.nix` files, zero commits. The goal is a single flake that can bootstrap a fresh NixOS install after manual disk partitioning, and can also be installed on non-NixOS systems (any Linux now, macOS via nix-darwin later), sharing as much configuration as possible for a uniform experience while staying idiomatic and avoiding hacks. + +## What Changes + +- Add a `flake.nix` with `nixpkgs` and `home-manager` inputs (following the same `nixpkgs`), exposing `nixosConfigurations`, `homeConfigurations`, `formatter`, and `devShells` outputs. +- Add `lib/mkNixos.nix` and `lib/mkHome.nix` — thin wrapper helpers around `nixpkgs.lib.nixosSystem` and `home-manager.lib.homeManagerConfiguration`, both consuming the same per-user module so NixOS and standalone home-manager activation stay in sync. +- Add `modules/nixos/common.nix` — shared NixOS-only system settings (flakes enabled, base user skeleton, locale, gc policy) applied to every NixOS host. +- Add `modules/home/*.nix` — the shared cross-platform home-manager profile (shell/zsh+starship+direnv, git, editor/neovim, CLI tools), consumed identically by NixOS's home-manager module and by standalone `home-manager switch`. +- Add `users/tom.nix` — the identity/profile file both `mkNixos` and `mkHome` import, with platform-computed `home.homeDirectory`. +- Add `hosts/template/` — a non-instantiated, copy-source-only NixOS host skeleton (`configuration.nix` + stub `hardware-configuration.nix`) used to bootstrap a real host after `nixos-generate-config`. +- Add a `homeConfigurations."tom@linux"` output so the shared profile is a real, buildable standalone target in this change. +- Add `README.md` documenting the NixOS install workflow (manual partition → `nixos-generate-config` → copy host template → `nixos-install --flake`) and the standalone workflow (install Nix → `home-manager switch --flake`). +- Add `formatter` (`nixfmt-rfc-style`) and a `devShells.default` (`statix`, `deadnix`) for linting/formatting this repo itself. +- Explicitly out of scope for this change: a concrete nix-darwin/macOS host, disko-based declarative partitioning, and any secrets management (agenix/sops-nix) — all deferred to follow-up changes. + +## Capabilities + +### New Capabilities +- `nix-flake-bootstrap`: A flake that provisions NixOS hosts from a copyable template and provisions a shared, cross-platform home-manager user profile installable standalone on non-NixOS systems, with both consumption paths built from the same per-user module. + +### Modified Capabilities +(none — greenfield repo, no existing specs) + +## Impact + +- Affected code: entirely new — `flake.nix`, `flake.lock`, `lib/`, `modules/`, `hosts/template/`, `users/`, `README.md`. +- Dependencies introduced: `nixpkgs` (nixos-unstable), `home-manager` (following nixpkgs). +- Systems affected: none yet running this config (dev machine `glootie` has no Nix installed; no NixOS host exists yet). This change only needs to be validated via `nix flake check` / building the `homeConfigurations."tom@linux"` output — it does not touch any live system. +- Follow-up work (not part of this change): pushing the repo to a remote (needed before `nixos-install` can `git clone` it during a real install), adding a real NixOS host once hardware is partitioned, adding nix-darwin once a Mac is being configured, and adding secrets management. diff --git a/openspec/changes/add-nix-flake-bootstrap/specs/nix-flake-bootstrap/spec.md b/openspec/changes/add-nix-flake-bootstrap/specs/nix-flake-bootstrap/spec.md new file mode 100644 index 0000000..3fc0055 --- /dev/null +++ b/openspec/changes/add-nix-flake-bootstrap/specs/nix-flake-bootstrap/spec.md @@ -0,0 +1,47 @@ +## Purpose + +Provides a single Nix flake that provisions NixOS hosts from a copyable template and provisions a shared, cross-platform home-manager user profile that installs standalone on non-NixOS systems, keeping both paths built from the same per-user module. + +## ADDED Requirements + +### Requirement: Shared home profile across activation methods +The system SHALL apply the same home-manager user profile (shell, git, editor, CLI tools) whether it is activated as part of a NixOS system build or activated standalone via `home-manager switch` on a non-NixOS system. + +#### Scenario: NixOS-activated profile matches standalone profile +- **WHEN** a NixOS host built with the `mkNixos` helper activates its `home-manager.users.tom` configuration +- **THEN** the resulting user environment includes the same programs and settings as the `homeConfigurations."tom@linux"` output built with the `mkHome` helper, because both consume the same `users/tom.nix` module + +### Requirement: Standalone installation requires no root privileges +The system SHALL allow the shared home profile to be installed on any non-NixOS system without modifying system-level configuration or requiring root/sudo. + +#### Scenario: Standalone home-manager switch +- **WHEN** a user with Nix installed runs `home-manager switch --flake .#tom@linux` on a non-NixOS Linux system +- **THEN** only the user's home directory is modified and no root privileges are required + +### Requirement: NixOS host template is not a live flake output +The system SHALL provide a NixOS host template directory that is not wired into any `nixosConfigurations` output, so it cannot be accidentally built or installed before being customized for real hardware. + +#### Scenario: Flake check does not build the template +- **WHEN** `nix flake check` is run against the repository +- **THEN** no `nixosConfigurations` entry evaluates or builds the `hosts/template` directory directly + +### Requirement: Flakes remain enabled on installed NixOS systems +Every NixOS host built from this flake SHALL have the `nix-command` and `flakes` experimental features enabled by default, so subsequent `nixos-rebuild` invocations do not require extra flags. + +#### Scenario: Rebuilding after install needs no extra flags +- **WHEN** a NixOS system installed from this flake is rebuilt with `nixos-rebuild switch --flake #` +- **THEN** the command succeeds without passing `--extra-experimental-features` + +### Requirement: Per-host and per-user state versions are explicit +Each NixOS host and each home-manager user configuration SHALL declare its own `system.stateVersion` / `home.stateVersion` value; the shared helper functions SHALL NOT supply a default value. + +#### Scenario: Missing stateVersion fails evaluation +- **WHEN** a host or user configuration omits its `stateVersion` setting +- **THEN** evaluating that configuration fails rather than silently falling back to a shared default + +### Requirement: Repository evaluates under pure evaluation +The flake's `homeConfigurations` and check outputs defined in this change SHALL evaluate and build using only pure evaluation, without requiring the `--impure` flag. + +#### Scenario: Pure flake check succeeds +- **WHEN** `nix flake check` is run without `--impure` +- **THEN** it completes successfully for all outputs defined in this change diff --git a/openspec/changes/add-nix-flake-bootstrap/tasks.md b/openspec/changes/add-nix-flake-bootstrap/tasks.md new file mode 100644 index 0000000..4e687ac --- /dev/null +++ b/openspec/changes/add-nix-flake-bootstrap/tasks.md @@ -0,0 +1,59 @@ +## 1. Flake scaffold + +- [x] 1.1 Create `flake.nix` with `nixpkgs` (nixos-unstable) and `home-manager` (following `nixpkgs`) inputs, and an `outputs` function that imports `lib` and wires `nixosConfigurations`, `homeConfigurations`, `formatter`, and `devShells` — verify with `nix flake metadata` succeeding without errors (**nix not installed locally — command-level verification deferred, see note below**) +- [x] 1.2 Add `.gitignore` for `result`/`result-*` symlinks — verify `nix build` output doesn't get tracked by `git status` + +## 2. Shared lib helpers + +- [x] 2.1 Create `lib/mkNixos.nix` wrapping `nixpkgs.lib.nixosSystem`, taking `{ hostname, system ? "x86_64-linux", users ? [ "tom" ], extraModules ? [] }`, including `modules/nixos/common.nix`, `../hosts/${hostname}`, and `home-manager.nixosModules.home-manager` with `home-manager.users. = import ../users/${u}.nix` for each user, and `specialArgs = { inherit inputs; }` — verify by importing it in a `nix repl` and checking it returns a function (**deferred, no local nix**) +- [x] 2.2 Create `lib/mkHome.nix` wrapping `home-manager.lib.homeManagerConfiguration`, taking `{ username, system, extraModules ? [] }`, using `pkgs = nixpkgs.legacyPackages.${system}` and `modules = [ ../users/${username}.nix ] ++ extraModules` — verify the same way (**deferred, no local nix**) +- [x] 2.3 Create `lib/default.nix` aggregating both as `{ mkNixos, mkHome }` given `{ inputs }` — verify `flake.nix` can `import ./lib { inherit inputs; }` without eval errors (**deferred, no local nix**) + +## 3. Shared NixOS module + +- [x] 3.1 Create `modules/nixos/common.nix` with `nix.settings.experimental-features = [ "nix-command" "flakes" ]`, a `tom` user skeleton (`isNormalUser = true`, `extraGroups = [ "wheel" ]`, `shell = pkgs.zsh`), default locale/timezone, and `nix.gc` policy — verify it evaluates standalone via `nix eval --file modules/nixos/common.nix` (or as part of the template host eval in task 5) (**deferred, no local nix**) + +## 4. Shared home-manager profile + +- [x] 4.1 Create `modules/home/git.nix` with `programs.git.enable = true`, `userName`, and `userEmail` sourced from options set in `users/tom.nix` (not hardcoded in the module itself), plus `init.defaultBranch = "main"` and `pull.rebase = true` +- [x] 4.2 Create `modules/home/shell.nix` with `programs.zsh.enable = true` (`autosuggestion.enable`, `syntaxHighlighting.enable`), `programs.starship.enable = true`, and `programs.direnv.enable = true` with `nix-direnv.enable = true` +- [x] 4.3 Create `modules/home/editor.nix` with `programs.neovim.enable = true`, `defaultEditor = true`, `vimAlias = true` +- [x] 4.4 Create `modules/home/cli-tools.nix` with `home.packages` including `ripgrep`, `fd`, `bat`, `eza`, `jq`, `fzf`, `tree` +- [x] 4.5 Create `modules/home/default.nix` importing the four modules above — verify by building `homeConfigurations."tom@linux"` in task 6 and confirming all four programs appear in the activation package (**deferred, no local nix**) + +## 5. User identity module + +- [x] 5.1 Create `users/tom.nix` setting `home.username = "tom"`, `home.homeDirectory = if pkgs.stdenv.isDarwin then "/Users/tom" else "/home/tom"`, `programs.git.userEmail = "t0m.vd.l33@gmail.com"`, `home.stateVersion` (pin to the current home-manager release), and importing `modules/home` — verify it evaluates under both a NixOS host context and standalone `mkHome` without error (**deferred, no local nix; stateVersion pinned to "24.05" — confirm/bump to your actual install-time release before first activation**) + +## 6. Standalone home-manager output + +- [x] 6.1 Add `homeConfigurations."tom@linux" = lib.mkHome { username = "tom"; system = "x86_64-linux"; };` to `flake.nix` — verify with `nix build .#homeConfigurations."tom@linux".activationPackage` succeeding without `--impure` (**deferred, no local nix**) + +## 7. NixOS host template (copy-source only) + +- [x] 7.1 Create `hosts/template/configuration.nix` with placeholders/comments for `networking.hostName`, boot loader choice, and `system.stateVersion`, importing nothing that requires real hardware +- [x] 7.2 Create `hosts/template/hardware-configuration.nix` as a stub importing `(modulesPath + "/installer/scan/not-detected.nix")` with commented example `fileSystems`/`boot.initrd.availableKernelModules` entries, clearly marked as "replace with the output of `nixos-generate-config`" +- [x] 7.3 Confirm `hosts/template` is not referenced by any `nixosConfigurations` entry in `flake.nix` — verify by grepping `flake.nix` for `template` and finding no matches (grep confirmed: only a comment mentions "template", no output reference) + +## 8. Repo tooling + +- [x] 8.1 Add `formatter. = pkgs.nixfmt-rfc-style;` to `flake.nix` — verify with `nix fmt` running successfully across the repo (**deferred, no local nix**) +- [x] 8.2 Add `devShells.default` with `statix` and `deadnix` available — verify `nix develop -c statix check .` and `nix develop -c deadnix .` both run (**deferred, no local nix**) + +## 9. Validation + +- [ ] 9.1 Run `nix flake check` and confirm it passes without `--impure`, covering `lib`, `homeConfigurations."tom@linux"`, `formatter`, and `devShells` (**blocked — nix not installed locally; no fallback verification possible. Run this once Nix is available.**) +- [x] 9.2 Manually review `flake.nix` and confirm `nixosConfigurations` is empty (or absent) in this change, per the design decision not to expose the unbuilt template (confirmed: `nixosConfigurations = { };`) + +## 10. Documentation + +- [x] 10.1 Write `README.md` documenting the repo layout, the NixOS install workflow (manual partition/format → `nixos-generate-config` → copy `hosts/template` → `nixos-install --flake`), and the standalone workflow (install Nix → `home-manager switch --flake`) — verify by following each documented command sequence mentally against the actual files created in this change for consistency +- [x] 10.2 Note in the README, as explicit follow-up items not covered by this change: pushing the repo to a remote (required before `nixos-install` can clone it), adding a real NixOS host, adding nix-darwin for a real Mac host, and adding secrets management + +## 11. Commit + +- [ ] 11.1 `git add` all new files (`flake.nix`, `flake.lock`, `lib/`, `modules/`, `hosts/`, `users/`, `README.md`, `.gitignore`) and create the initial commit — verify with `git status` showing a clean tree and `git log` showing the commit, which also makes the flake's files visible to the Nix evaluator for the first time (**note: no `flake.lock` yet — it can only be generated by `nix flake lock`/`nix flake check`, which require Nix; commit without it for now, it will be created on first `nix` invocation**) (**blocked — sandbox denies write access to `.git/index.lock`; all files are written to disk and staged commands were handed to the user to run directly, e.g. via `!git add ... && git commit ...`**) + +--- + +**Verification note:** Nix is not installed on this machine (confirmed via `which nix`), so none of the `nix flake check` / `nix build` / `nix repl` / `nix fmt` / `nix develop` verification commands above could actually be run. Per explicit user decision, all file-creation tasks are marked complete based on careful manual construction against the Nix/home-manager module system semantics, but **task 9.1 (the full `nix flake check`) remains unexecuted and should be run as soon as Nix is available** — treat that as the real acceptance gate for this change, not the individual checkboxes above. diff --git a/openspec/changes/archive/.gitkeep b/openspec/changes/archive/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/openspec/config.yaml b/openspec/config.yaml new file mode 100644 index 0000000..c4d34ac --- /dev/null +++ b/openspec/config.yaml @@ -0,0 +1,32 @@ +schema: spec-driven + +# Project context (optional) +# This is shown to AI when creating artifacts. +# Add your tech stack, conventions, style guides, domain knowledge, etc. +# Example: +# context: | +# Tech stack: TypeScript, React, Node.js +# We use conventional commits +# Domain: e-commerce platform + +# Per-artifact rules (optional) +# Add custom rules for specific artifacts. +# Example: +# rules: +# proposal: +# - Keep proposals under 500 words +# - Always include a "Non-goals" section +# tasks: +# - Break tasks into chunks of max 2 hours + +# Per-operation guidance (optional) +# Add advisory guidance for how apply and archive work should be conducted. +# This is separate from artifact rules above. +# Example: +# operations: +# apply: +# guidance: +# - Keep test summaries concise +# archive: +# guidance: +# - Summarize the archive outcome before finishing diff --git a/openspec/specs/.gitkeep b/openspec/specs/.gitkeep new file mode 100644 index 0000000..e69de29 -- cgit v1.3