diff --git a/agents/skills/nix-dev-init/SKILL.md b/agents/skills/nix-dev-init/SKILL.md index 8ec6c096..8041b6cd 100644 --- a/agents/skills/nix-dev-init/SKILL.md +++ b/agents/skills/nix-dev-init/SKILL.md @@ -15,7 +15,9 @@ The workflow has two phases, and the split is load-bearing: - **Phase 1 — Nix shell** (this file): `github/create_repo.sh --template` (or manual `flake.nix` → `.envrc`) → `direnv allow`. Language-agnostic. - **Phase 2 — Language init** (see `references/lang-.md`): verify toolchain, run language-specific sync/build **inside the direnv-activated shell**. -The whole reason Phase 1 runs before Phase 2 is so the language's commands see the nix-provided toolchain on PATH, not the host's. Do not collapse or reorder the phases. +The whole reason Phase 1 runs before Phase 2 is so the language's commands see the project +devShell toolchain on PATH, not the Nix-managed global fallback. Do not collapse or reorder the +phases. ## Mandatory Order (Phase 1) @@ -37,20 +39,22 @@ The whole reason Phase 1 runs before Phase 2 is so the language's commands see t ### Why this order - **VCS before flake**: the git repo must exist before `nix flake update` creates `flake.lock`. Use `github/create_repo.sh` for template-backed projects and `gh repo create --clone` for one-off non-template projects instead of `git init` — they set up remote and default branch in one shot. Private repos skip `--license` because an accidental visibility flip with MIT attached grants everyone usage rights. -- **Toolchain before init**: running language init commands (e.g. `pnpm install`) on the host shell picks up the host's toolchain, whose version leaks into lockfiles. On another machine (or in CI) this silently breaks reproducibility. +- **Toolchain before init**: running language init commands (e.g. `pnpm install`) on the host shell + picks up the global fallback toolchain, whose version may differ from the project. On another + machine or in CI this silently breaks reproducibility. - **Commit `.envrc` before `direnv allow`**: direnv's trust state is keyed by file hash. Allowing first and editing after immediately invalidates the allow, forcing a re-allow. ## Template Repos All supported languages have a GitHub template repo under `furedea/`. Use `github/create_repo.sh --template` to create new projects — it clones, applies rulesets, and patches the project name in config files automatically. -| Project type | Template repo | create_repo.sh post-processing | Ship `flake.lock`? | -| --- | --- | --- | --- | -| Python (uv) | `furedea/template-python` | `pyproject.toml` name sub + `ruleset_python` | no | -| TypeScript / Node (pnpm) | `furedea/template-typescript` | `package.json` name sub + `ruleset_typescript` | no | -| Rust | `furedea/template-rust` | `Cargo.toml` name sub + `ruleset_rust` | no | -| TeX / LaTeX | `furedea/template-tex` | `ruleset_tex` only (no name sub) | **yes** (in repo) | -| Fallback (unlisted languages) | `furedea/template-minimal` | base ruleset only | no | +| Project type | Template repo | create_repo.sh post-processing | Ship `flake.lock`? | +| ----------------------------- | ----------------------------- | ---------------------------------------------- | ------------------ | +| Python (uv) | `furedea/template-python` | `pyproject.toml` name sub + `ruleset_python` | no | +| TypeScript / Node (pnpm) | `furedea/template-typescript` | `package.json` name sub + `ruleset_typescript` | no | +| Rust | `furedea/template-rust` | `Cargo.toml` name sub + `ruleset_rust` | no | +| TeX / LaTeX | `furedea/template-tex` | `ruleset_tex` only (no name sub) | **yes** (in repo) | +| Fallback (unlisted languages) | `furedea/template-minimal` | base ruleset only | no | Non-TeX templates intentionally omit `flake.lock`: `nix flake update` runs on first `direnv allow` to resolve a fresh `nixpkgs` commit. The TeX template includes `flake.lock` for reasons explained below. @@ -97,13 +101,13 @@ plus language-specific entries. For non-template projects (fallback path), add t After `direnv allow`, hand off to the language-specific reference. Each ref covers verification, sync/build, and language-specific anti-patterns. -| Project type | Reference | Downstream skill | -| --- | --- | --- | -| Python (uv) | `references/lang_python.md` | `python-style` | -| TypeScript / Node (pnpm) | `references/lang_typescript.md` | — | -| Rust | `references/lang_rust.md` | — | -| TeX / LaTeX | `references/lang_tex.md` | — | -| Unlisted languages | `references/lang_fallback.md` | — | +| Project type | Reference | Downstream skill | +| ------------------------ | ------------------------------- | ---------------- | +| Python (uv) | `references/lang_python.md` | `python-style` | +| TypeScript / Node (pnpm) | `references/lang_typescript.md` | — | +| Rust | `references/lang_rust.md` | — | +| TeX / LaTeX | `references/lang_tex.md` | — | +| Unlisted languages | `references/lang_fallback.md` | — | Read only the ref that matches the project's primary language — the files are intentionally standalone so Phase 2 loads one language's context, not all of them. @@ -117,7 +121,9 @@ Read only the ref that matches the project's primary language — the files are - Using `git init` instead of `github/create_repo.sh` or `gh repo create --clone` for new projects → remote URL hand-typing, branch name mismatch (`master` vs `main`), missing license/gitignore. - Running `uv init` / `pnpm install` / `cargo build` on the host shell before `direnv allow` → host toolchain leaks into the project. -- Adding project-only tooling to `~/ghq/github.com/furedea/dotfiles/nix/home/default.nix` → bloats the global user env; keep project tooling in the project's own flake. +- Adding project-only tooling to `~/ghq/github.com/furedea/dotfiles/nix/home/default.nix` → bloats the + global user env; global fallback runtimes belong there, while project-specific tools stay in the + project's own flake. - Running `darwin-rebuild switch` after editing a project's `flake.nix` → unnecessary. `darwin-rebuild` only reads the dotfiles flake + the nix-darwin modules. - Editing files under `.direnv/` by hand → it is a cache; change `flake.nix` instead and let direnv rebuild it on next `cd`. - Manually scaffolding files that the template repo already provides (e.g. running `pnpm init` when `template-typescript` already has `package.json`). diff --git a/agents/skills/nix-dev-init/references/lang_fallback.md b/agents/skills/nix-dev-init/references/lang_fallback.md index 57e63a1d..486575e7 100644 --- a/agents/skills/nix-dev-init/references/lang_fallback.md +++ b/agents/skills/nix-dev-init/references/lang_fallback.md @@ -38,6 +38,8 @@ Do not do this for a single project. The overhead of maintaining a template repo ## What NOT to do -- Do not add the language's toolchain to `~/ghq/github.com/furedea/dotfiles/nix/home/default.nix` "temporarily". Temporary global installs have a way of becoming permanent. +- Do not add project-specific tools to `~/ghq/github.com/furedea/dotfiles/nix/home/default.nix` + temporarily. Deliberate global fallback runtimes are allowed, but a project must still declare + its complete toolchain in its own devShell. - Do not create a new template repo on the first use of a language. Wait for the pattern. - Do not skip Phase 1 and go straight to `go mod init` / `mix new` on the host shell "because it's a quick experiment". Experiments that aren't worth a flake aren't worth committing either — use `nix shell nixpkgs#go` as a throwaway instead. diff --git a/agents/skills/nix-dev-init/references/lang_rust.md b/agents/skills/nix-dev-init/references/lang_rust.md index dbcf4070..7de0ca9d 100644 --- a/agents/skills/nix-dev-init/references/lang_rust.md +++ b/agents/skills/nix-dev-init/references/lang_rust.md @@ -31,6 +31,7 @@ Do **not** add `rust-analyzer` to the devShell. The globally-installed `rust-ana ## What NOT to do - Do not run `cargo init` — the template repo already provides `Cargo.toml` and `src/main.rs`. Running `cargo init` overwrites them. -- Do not add `cargo` / `rustc` to `~/ghq/github.com/furedea/dotfiles/nix/home/default.nix`. Per-project pinning is the whole point. +- Do not rely on the global `cargo` or `rustc` inside a project. The project devShell remains the + source of truth even when Home Manager provides global fallbacks. - Do not commit `target/`. It is a machine-specific build cache. - Do not run `rustup` inside the direnv shell. Nix owns the toolchain; rustup would install a second one into `~/.rustup/` and silently shadow it via `cargo`'s PATH lookup. diff --git a/agents/skills/nix-dev-init/references/lang_typescript.md b/agents/skills/nix-dev-init/references/lang_typescript.md index ee2e79c3..7425dbc0 100644 --- a/agents/skills/nix-dev-init/references/lang_typescript.md +++ b/agents/skills/nix-dev-init/references/lang_typescript.md @@ -28,6 +28,7 @@ They are Rust-based, orders of magnitude faster than the JS-native equivalents, ## What NOT to do - Do not run `pnpm init` — the template repo already provides `package.json`. Running `pnpm init` overwrites it and loses the curated config. -- Do not add `nodejs` or `pnpm` to `~/ghq/github.com/furedea/dotfiles/nix/home/default.nix`. Project-level pinning is the whole point of Phase 1. +- Do not rely on the global `nodejs` or `pnpm` inside a project. The project devShell remains the + source of truth even when Home Manager provides global fallbacks. - Do not commit `node_modules/`. pnpm's lockfile + nix's `nodejs_22` pin is what makes the build reproducible. - Do not mix package managers (npm install + pnpm install in the same repo). pnpm's lockfile format is not interchangeable with npm's. diff --git a/agents/skills/nix-dotfiles/SKILL.md b/agents/skills/nix-dotfiles/SKILL.md index 99a49d75..4a572378 100644 --- a/agents/skills/nix-dotfiles/SKILL.md +++ b/agents/skills/nix-dotfiles/SKILL.md @@ -31,7 +31,7 @@ or rebuild requirements are relevant. ├── home.packages # CLI tools from nixpkgs / unstable / flake inputs ├── programs.* # declarative program configs (git, delta, gh, direnv, atuin, yazi, tmux) ├── home.file.* # dotfile symlinks (mkOutOfStoreSymlink) - └── home.activation # post-activation hooks (rustup, uv, ssh-keygen) + └── home.activation # post-activation hooks for mutable runtime state ``` Platform: `aarch64-darwin` (Apple Silicon). Release channel: `25.11` for nixpkgs, nix-darwin, and home-manager. `nixpkgs-unstable` is available as `unstable` overlay for packages that need bleeding-edge versions. @@ -90,6 +90,7 @@ Changes to files linked via `mkOutOfStoreSymlink` (e.g. `.zshrc`, `starship.toml | CLI tool in nixpkgs stable | `nix/home/default.nix` → `home.packages` | `bat`, `ripgrep` | | CLI tool only in unstable | `nix/home/default.nix` → `home.packages` | `unstable.atuin`, `unstable.oxfmt` | | Tool from a flake input | `nix/home/default.nix` → `home.packages` | `nix-claude-code.packages.${system}.default` | +| Global fallback language runtime | `nix/home/default.nix` → `home.packages` | `python314`, `rustc`, `nodejs_24` | | GUI app (macOS .app) | `nix/darwin/default.nix` → `homebrew.casks` | `"obsidian"`, `"arc"` | | Mac App Store app | `nix/darwin/default.nix` → `homebrew.masApps` | `LINE = 539883307` | | Homebrew formula (not in nixpkgs) | `nix/darwin/default.nix` → `homebrew.brews` | `"winebarrel/kasa/kasa"` | @@ -182,16 +183,11 @@ Use `.text` when the file content depends on Nix store paths (e.g. plugin source ## Activation Hooks -Run commands after home-manager writes files: +Run commands after home-manager writes files when the target is inherently mutable and cannot be +represented by a package or managed file: ```nix home.activation = { - rustupInit = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - ${pkgs.rustup}/bin/rustup toolchain install stable --no-self-update 2>/dev/null || true - ''; - uvPythonInstall = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - ${pkgs.uv}/bin/uv python install 2>/dev/null || true - ''; sshKeyGen = lib.hm.dag.entryAfter [ "writeBoundary" ] '' if [ ! -f ~/.ssh/id_ed25519 ]; then mkdir -p ~/.ssh @@ -201,6 +197,7 @@ home.activation = { }; ``` +- Install language runtimes through `home.packages`; do not download them with activation hooks. - Use `${pkgs.xxx}/bin/xxx` to reference the exact Nix store binary. - End with `|| true` to prevent rebuild failures from non-fatal errors. - `entryAfter [ "writeBoundary" ]` ensures dotfiles are written before the hook runs. diff --git a/docs/adr/0024_manage_language_runtimes_with_nix.md b/docs/adr/0024_manage_language_runtimes_with_nix.md new file mode 100644 index 00000000..3788f389 --- /dev/null +++ b/docs/adr/0024_manage_language_runtimes_with_nix.md @@ -0,0 +1,12 @@ +# ADR-0024: Manage language runtimes with Nix + +- Status: Accepted +- Date: 2026-09-12 + +In the context of needing Python, Node.js, and Rust both for project work and occasional host-shell +commands, facing mutable runtime installations from uv and rustup that cannot be reproduced from a +flake lock, we decided for Nix-managed global fallback runtimes and independently pinned project +devShell toolchains, with uv, pnpm, and Cargo responsible for language dependencies, and against +activation hooks that download runtimes into the home directory, to make each environment +reproducible while keeping convenient host-shell commands, accepting duplicate Nix store closures +when global and project versions differ. diff --git a/nix/home/default.nix b/nix/home/default.nix index c852f84b..e7cee0b6 100644 --- a/nix/home/default.nix +++ b/nix/home/default.nix @@ -231,11 +231,15 @@ in shfmt # Python tooling + python314 uv # Rust tooling + cargo + clippy lspmux - rustup + rustc + rustfmt # TypeScript tooling nodePackages."@antfu/ni" @@ -578,12 +582,6 @@ in "${config.xdg.cacheHome}/zsh/.zcompdump" \ 9>/dev/null ''; - rustupInit = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - ${pkgs.rustup}/bin/rustup toolchain install stable --no-self-update 2>/dev/null || true - ''; - uvPythonInstall = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - ${pkgs.uv}/bin/uv python install 2>/dev/null || true - ''; sshDirectoryPermissions = lib.hm.dag.entryAfter [ "writeBoundary" ] '' if [ -d "$HOME/.ssh" ]; then /bin/chmod 0700 "$HOME/.ssh" diff --git a/tests/nix/language_toolchains.bats b/tests/nix/language_toolchains.bats new file mode 100644 index 00000000..da70c842 --- /dev/null +++ b/tests/nix/language_toolchains.bats @@ -0,0 +1,28 @@ +#!/usr/bin/env bats +# Executable specifications for globally available language toolchains. + +bats_require_minimum_version 1.5.0 + +setup() { + REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" +} + +@test "Home Manager provides global fallback runtimes without mutable installers" { + run --separate-stderr nix eval --no-write-lock-file --json \ + "$REPO_ROOT#homeConfigurations.kaito.config" --apply ' + config: { + packages = map (package: package.pname or package.name) config.home.packages; + activations = builtins.attrNames config.home.activation; + }' + + [ "$status" -eq 0 ] + run jq -e ' + (["python3", "uv", "nodejs", "pnpm", "cargo", "rustc", "clippy", "rustfmt"] + - .packages | length == 0) + and (.packages | index("rustup") == null) + and (.activations | index("rustupInit") == null) + and (.activations | index("uvPythonInstall") == null) + ' <<<"$output" + + [ "$status" -eq 0 ] +}