aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/nix
diff options
context:
space:
mode:
Diffstat (limited to 'nix')
-rw-r--r--nix/README.md201
-rwxr-xr-xnix/bootstrap.sh111
-rw-r--r--nix/common.nix160
-rw-r--r--nix/flake.lock49
-rw-r--r--nix/flake.nix44
-rw-r--r--nix/host.nix21
-rw-r--r--nix/justfile21
-rw-r--r--nix/vm.nix71
8 files changed, 678 insertions, 0 deletions
diff --git a/nix/README.md b/nix/README.md
new file mode 100644
index 0000000..2bf3383
--- /dev/null
+++ b/nix/README.md
@@ -0,0 +1,201 @@
+# nix
+
+Home-Manager profiles for the Arch host (`host.nix`) and the Ubuntu
+remote-dev VM (`vm.nix`), both layered on top of `common.nix`. Shares
+the same nvim, zellij, and zsh configs from the same repo — no
+duplication across machines.
+
+## Bootstrap
+
+**Host (Arch)**: managed by the top-level `bootstrap.sh` in the repo
+root (installs nix + runs `home-manager switch --flake .../nix#host`
+as part of `just init`).
+
+**VM (Ubuntu)**: as the dev user (must have sudo):
+
+```sh
+curl -fsSL https://raw.githubusercontent.com/sommerfelddev/dotfiles/master/nix/bootstrap.sh | sh
+```
+
+Then log out and back in. Run `nvim` once to let it fetch plugins from
+GitHub on first launch.
+
+## What the VM bootstrap does
+
+1. Installs Nix (Determinate Systems multi-user installer).
+2. Clones this repo to `~/.local/share/dotfiles`.
+3. Runs `home-manager switch --flake .../nix#vm`, which:
+ - Installs the CLI tool subset (see `common.nix` + `vm.nix`).
+ - Symlinks `~/.config/{nvim,zellij,zsh,direnv,ghostty,git}` and
+ `~/.ssh/config` at the cloned working tree via
+ `mkOutOfStoreSymlink`, so `git pull` is enough to pick up config
+ edits — no rebuild needed for config-only changes.
+ - Sets `ZDOTDIR=$HOME/.config/zsh` so the shared zshrc/zprofile load.
+4. Appends the nix-store zsh to `/etc/shells` and `chsh`'s to it.
+
+## Updating after a dotfiles change
+
+Run from `~/.local/share/dotfiles/nix` (host or VM):
+
+```sh
+just update # pull + home-manager switch (handles everything)
+```
+
+Or piece-by-piece if you know which one you need:
+
+```sh
+just pull # config-only changes (nvim/zellij/zsh/git/ssh): no rebuild needed
+just switch # rebuild home-manager from the current checkout
+```
+
+> `just update` runs `pull` then `switch`. The home-manager invocation
+> uses `--impure --flake '.#vm' -b backup`; the single-quotes around the
+> flake ref matter because our zsh enables `extendedglob`, which would
+> otherwise interpret `.#vm` as a glob pattern. On the host, swap
+> `#vm` → `#host`.
+
+## Adding a tool
+
+Edit `common.nix` (shared) or the profile-specific file (`host.nix` /
+`vm.nix`), add to `home.packages`, then `just switch` (or `just
+update`).
+
+## Single-shell policy (leaf tools only)
+
+The nix profile carries **leaf CLI tools** plus **editor/AI-agent
+runtimes**, and nothing else. Specifically forbidden in `home.packages`
+because they would shadow the system toolchain via `PATH` and silently
+break builds against the system sysroot/libc/CI: `cc`, `c++`, `gcc`,
+`g++`, `clang`, `clang++`, `ld`, `make`, `cmake`, `ninja`, `meson`,
+`pkg-config`, `autoconf`, `automake`, `python`, `python3`, `pip`,
+`cargo`, `rustc`, `go`. The system `python3` stays the default
+interpreter for project builds.
+
+Explicit carve-outs (used only by editor/AI agents, never by the
+project build):
+
+- `nodejs` — `node`/`npm`/`npx` for npm-based LSPs and
+ copilot-language-server.
+- `uv` — `uv`/`uvx` for ad-hoc Python tooling in isolated venvs. `uv`
+ does NOT install a `python3` in PATH; it manages its own
+ interpreters under `~/.local/share/uv/`. System `python3` is
+ untouched.
+- `clang-tools` — `clang-format`, `clang-tidy`, `clangd` only (no
+ compiler driver).
+
+If a project needs a newer build toolchain, drop a `flake.nix` +
+`.envrc` in that project tree (direnv + nix-direnv is already wired
+up). Don't add it to `common.nix`/`host.nix`/`vm.nix`.
+
+## Commit signing on the VM (SSH-format, no GPG secrets)
+
+GPG private keys never leave the host. Commits on the VM are signed
+with the **forwarded SSH agent** in SSH-signature format, using the
+authentication subkey gpg-agent already exposes via `ssh-add -L`.
+
+One-time setup on the VM:
+
+```sh
+mkdir -p ~/.config/git
+
+# allowed_signers: maps your committer email to the SSH pubkey of the
+# auth subkey. Adjust the grep if you have multiple keys.
+printf '%s %s\n' \
+ "$(git config user.email)" \
+ "$(ssh-add -L | head -n1)" \
+ > ~/.config/git/allowed_signers
+
+# Machine-local git override (NOT tracked in dotfiles).
+cat > ~/.config/git/config.local <<EOF
+[gpg]
+ format = ssh
+[gpg "ssh"]
+ allowedSignersFile = ~/.config/git/allowed_signers
+[user]
+ signingkey = $(ssh-add -L | head -n1 | awk '{print $1" "$2}')
+EOF
+```
+
+The tracked `dot_config/git/config` ends with `[include] path =
+~/.config/git/config.local`, so the override is picked up
+automatically (and silently ignored on machines that don't have it).
+
+Required on the **host's** `~/.ssh/config` for the VM `Host` block:
+
+```
+ForwardAgent yes
+```
+
+Verify on the VM after SSH-ing in:
+
+```sh
+ssh-add -L # should list your auth pubkey(s)
+git commit --allow-empty -m test
+git log --show-signature -1
+```
+
+## Caveats
+
+- **GPG / pass**: HM installs `gnupg` and `pass` but does _not_ import
+ any private key. On the VM, use SSH-format signing via the forwarded
+ agent instead (see above). On the host, smartcard access via
+ `pcscd` is configured in `host.nix` (`~/.gnupg/scdaemon.conf`).
+- **Disk usage**: Nix store + nvim plugins consumes ~3-5 GB. Check
+ partition size first on the VM.
+- **Network for first nvim launch**: `vim.pack.add` fetches plugins
+ from GitHub on first start.
+- **Ubuntu apt collisions**: Nix-installed binaries appear first in
+ PATH. The leaf-tools policy above exists precisely to keep this
+ shadowing contained to harmless tools.
+
+## Podman (rootless, VM only)
+
+Nix can't manage setuid helpers, `/etc/subuid`/`/etc/subgid`, or kernel
+cmdline. Do this once on the VM as root:
+
+```sh
+sudo apt install -y uidmap
+grep "^$USER:" /etc/subuid /etc/subgid || \
+ sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 "$USER"
+```
+
+Then (optional, **only** if you need rootless CPU/memory limits) enable
+cgroups v2. Ubuntu 20.04 still defaults to v1; flipping this requires a
+reboot and affects every workload on the box, so skip unless you have a
+concrete need:
+
+```sh
+sudo sed -i 's|^GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"|GRUB_CMDLINE_LINUX_DEFAULT="\1 systemd.unified_cgroup_hierarchy=1"|' /etc/default/grub
+sudo update-grub && sudo reboot
+```
+
+Verify:
+
+```sh
+podman info | grep -E 'cgroupVersion|graphDriverName|networkBackend'
+# expected: graphDriverName: overlay, networkBackend: netavark
+# cgroupVersion: v1 is fine — only blocks --memory/--cpus flags. The
+# podman v5 deprecation warning is silenced by PODMAN_IGNORE_CGROUPSV1_WARNING,
+# set in vm.nix.
+podman run --rm docker.io/library/alpine echo hi
+```
+
+The VM home-manager profile installs `podman`, `crun`, `conmon`,
+`netavark`, `aardvark-dns`, `slirp4netns`, and `passt`, and writes
+sensible `~/.config/containers/{registries,storage,policy}.conf` files.
+
+## How it's wired
+
+`common.nix` uses `config.lib.file.mkOutOfStoreSymlink` so the symlinks
+point at the **live working tree** at `~/.local/share/dotfiles/...`,
+not at copies in `/nix/store`. This means:
+
+- Editing `dot_config/nvim/init.lua` in the cloned repo takes effect
+ on the next `nvim` launch with no rebuild.
+- `home-manager switch` only needs to re-run when adding/removing a
+ package or changing what's symlinked.
+
+The zsh plugins (`zsh-syntax-highlighting`, etc.) live in
+`$HOME/.nix-profile/share/`. The shared `dot_zshrc` prefers the
+nix-profile path on both host and VM, falling back to system paths for
+un-bootstrapped states.
diff --git a/nix/bootstrap.sh b/nix/bootstrap.sh
new file mode 100755
index 0000000..9f5e144
--- /dev/null
+++ b/nix/bootstrap.sh
@@ -0,0 +1,111 @@
+#!/usr/bin/env sh
+# Bootstrap a headless dev environment on a fresh Ubuntu 22.04 VM.
+# Idempotent: safe to re-run.
+#
+# curl -fsSL https://raw.githubusercontent.com/<user>/dotfiles/master/nix/bootstrap.sh | sh
+#
+# Steps:
+# 1. Install Nix (Determinate Systems installer, multi-user).
+# 2. Clone (or fast-forward) the dotfiles repo to ~/.local/share/dotfiles.
+# 3. Run `home-manager switch --flake .../nix#vm`.
+# 4. Install python3.11 via `uv` (needed by Mason pip installs).
+# 5. Add Nix-store zsh to /etc/shells and chsh the user.
+#
+# Environment overrides:
+# DOTFILES_REPO Git URL (default: https://github.com/ruifm/dotfiles)
+# DOTFILES_REF Branch/tag/sha (default: master)
+# DOTFILES_DIR Checkout path (default: $HOME/.local/share/dotfiles)
+
+set -eu
+
+REPO="${DOTFILES_REPO:-https://github.com/sommerfelddev/dotfiles}"
+REF="${DOTFILES_REF:-master}"
+DIR="${DOTFILES_DIR:-$HOME/.local/share/dotfiles}"
+
+log() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
+err() { printf '\033[1;31m==>\033[0m %s\n' "$*" >&2; }
+
+# ── 1. Nix ────────────────────────────────────────────────────────────────────
+if ! command -v nix >/dev/null 2>&1; then
+ log "Installing Nix (Determinate Systems installer)…"
+ curl --proto '=https' --tlsv1.2 -sSf -L \
+ https://install.determinate.systems/nix |
+ sh -s -- install linux --no-confirm
+else
+ log "Nix already installed, skipping installer."
+fi
+
+# ── 1b. (moved to step 4 — uv comes from the nix profile, only available
+# after `home-manager switch`) ─────────────────────────────────────────
+
+# Source nix env for the rest of this script (installer writes
+# /etc/profile.d/nix.sh but the current shell hasn't sourced it).
+if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then
+ # shellcheck disable=SC1091
+ . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
+fi
+
+# ── 2. Repo checkout ─────────────────────────────────────────────────────────
+if ! command -v git >/dev/null 2>&1; then
+ log "Bootstrapping git via nix profile…"
+ nix profile install nixpkgs#git
+fi
+
+if [ -d "$DIR/.git" ]; then
+ log "Updating existing checkout at $DIR…"
+ git -C "$DIR" fetch origin "$REF"
+ git -C "$DIR" checkout "$REF"
+ git -C "$DIR" pull --ff-only
+else
+ log "Cloning $REPO ($REF) → $DIR…"
+ mkdir -p "$(dirname "$DIR")"
+ git clone --branch "$REF" "$REPO" "$DIR"
+fi
+
+# ── 3. Home-Manager switch ───────────────────────────────────────────────────
+log "Running home-manager switch (this can take a while on first run)…"
+nix --extra-experimental-features 'nix-command flakes' \
+ run home-manager/master -- \
+ switch --impure --flake "$DIR/nix#vm" -b backup
+
+# ── 4. Mason's python interpreter (via uv from the nix profile) ──────────────
+# Mason installs some LSPs/linters into per-package pip venvs. We need a
+# python3.11 that:
+# (a) meets Mason's >=3.10 version requirement (Ubuntu 20.04 ships
+# /usr/bin/python3 = 3.8 — too old), and
+# (b) accepts manylinux wheels (Nix's python rejects them by design;
+# pip then falls back to compiling `nodejs-wheel-binaries` from
+# source, which fails on the host's gcc 9.4 — needs C++20).
+#
+# `uv python install 3.11` fetches a portable CPython build (python-build-
+# standalone, manylinux-compatible) into ~/.local/share/uv/python/. We
+# symlink its `python3.11` into ~/.local/bin/ (already on PATH from
+# zprofile) so Mason discovers it. Does NOT shadow /usr/bin/python3 —
+# leaf-tools policy intact. Works on any distro/release, no PPA required.
+UV_BIN="$HOME/.nix-profile/bin/uv"
+if [ -x "$UV_BIN" ]; then
+ if [ ! -x "$HOME/.local/bin/python3.11" ]; then
+ log "Installing python3.11 via uv (required for Mason pip installs)…"
+ "$UV_BIN" python install 3.11
+ UV_PY311="$("$UV_BIN" python find 3.11)"
+ mkdir -p "$HOME/.local/bin"
+ ln -sf "$UV_PY311" "$HOME/.local/bin/python3.11"
+ fi
+fi
+
+# ── 5. chsh to nix-store zsh ─────────────────────────────────────────────────
+NIX_ZSH="$HOME/.nix-profile/bin/zsh"
+if [ -x "$NIX_ZSH" ]; then
+ if ! grep -qxF "$NIX_ZSH" /etc/shells 2>/dev/null; then
+ log "Appending $NIX_ZSH to /etc/shells (requires sudo)…"
+ echo "$NIX_ZSH" | sudo tee -a /etc/shells >/dev/null
+ fi
+ current_shell="$(getent passwd "$USER" | cut -d: -f7)"
+ if [ "$current_shell" != "$NIX_ZSH" ]; then
+ log "Changing login shell to $NIX_ZSH (requires sudo)…"
+ sudo chsh -s "$NIX_ZSH" "$USER"
+ fi
+fi
+
+log "Done. Log out and back in for the new shell to take effect."
+log "Then run 'nvim' once to let it fetch plugins on first launch."
diff --git a/nix/common.nix b/nix/common.nix
new file mode 100644
index 0000000..b6a8493
--- /dev/null
+++ b/nix/common.nix
@@ -0,0 +1,160 @@
+{ config, pkgs, lib, dotfilesRoot, ... }:
+
+# Shared Home-Manager module: the leaf-CLI subset, editor/AI-agent
+# runtimes, and the shared dotfiles symlinks used by **both** the Arch
+# host and the Ubuntu remote-dev VM. Profile-specific extras live in
+# `host.nix` and `vm.nix`.
+#
+# Policy: this profile carries leaf CLI tools plus editor/AI-agent
+# runtimes (node, uv). It must NEVER carry anything the project build
+# might invoke. Forbidden on PATH (would shadow the system's and break
+# builds against the system sysroot/libc): cc, c++, gcc, g++, clang,
+# clang++, ld, ld.lld, ar, nm, objcopy, make, cmake, ninja, meson,
+# pkg-config, autoconf, automake, libtool, python, python3, pip,
+# cargo, rustc, go. If a project needs a newer toolchain, put it in a
+# project-local flake.nix + direnv `.envrc`, NOT here.
+#
+# Allowed runtimes (used only by editor/AI agents): node, npm, npx
+# (via `nodejs`), uv, uvx (via `uv` — does NOT install a python3,
+# manages its own interpreters under XDG). `clang-tools` is allowed
+# because it ships only formatters/linters/clangd, no compiler driver.
+
+let
+ dotfiles = "${config.home.homeDirectory}/.local/share/dotfiles";
+ link = path: config.lib.file.mkOutOfStoreSymlink "${dotfiles}/${path}";
+in
+{
+ home.stateVersion = "25.05";
+
+ # ── Packages ────────────────────────────────────────────────────────────────
+ home.packages = with pkgs; [
+ # Editor + multiplexer
+ neovim
+ zellij
+ tree-sitter
+
+ # Search / move
+ ripgrep
+ fd
+ fzf
+ sd
+ choose
+ zoxide
+ just
+
+ # Viewers
+ bat
+ lsd
+ glow
+
+ # Git stack
+ git
+ gh
+ delta
+ mergiraf
+
+ # JSON / YAML
+ jq
+ yq-go
+
+ # System
+ htop
+ fastfetch
+
+ # Net
+ curl
+ curlie
+ wget
+ dog
+ rsync
+ openssh
+
+ # Docs
+ tldr
+ man-db
+ man-pages
+
+ # Secrets
+ gnupg
+ pass
+
+ # C/C++ source tooling (no compiler driver in PATH)
+ clang-tools
+
+ # Editor/AI agent runtimes — NOT for project builds (see policy above)
+ nodejs_24 # copilot-language-server requires Node 24 (see ai.lua)
+ uv # for project tooling that asks for `uv`/`uvx`; brings no python
+
+ # AI coding agents
+ claude-code
+ github-copilot-cli # NB: pkgs.copilot-cli is AWS Copilot, NOT this
+
+ # Zsh and plugins (loaded from $HOME/.nix-profile/share/... by the
+ # shared zshrc; nix-profile path is preferred, system path is the
+ # fallback for un-bootstrapped states).
+ zsh
+ zsh-syntax-highlighting
+ zsh-autosuggestions
+ zsh-history-substring-search
+ ];
+
+ # ── direnv + nix-direnv ─────────────────────────────────────────────────────
+ programs.direnv = {
+ enable = true;
+ nix-direnv.enable = true;
+ enableZshIntegration = false; # zshrc already calls `eval "$(direnv hook zsh)"`
+ };
+
+ # ── Shared config symlinks ──────────────────────────────────────────────────
+ # Live symlinks back into the cloned working tree so `git pull` is enough
+ # to update configs — no `home-manager switch` required after every edit.
+ xdg.configFile = {
+ "nvim".source = link "dot_config/nvim";
+ "zellij".source = link "dot_config/zellij";
+ "zsh/.zshrc".source = link "dot_config/zsh/dot_zshrc";
+ "zsh/.zprofile".source = link "dot_config/zsh/dot_zprofile";
+ "ghostty".source = link "dot_config/ghostty"; # for terminfo refs only
+ "direnv/direnvrc".source = link "dot_config/direnv/direnvrc";
+ "git/config".source = link "dot_config/git/config";
+ "git/attributes".source = link "dot_config/git/attributes";
+ "git/ignore".source = link "dot_config/git/ignore";
+ # Git hooks: source filenames carry the chezmoi `executable_` attribute
+ # prefix which only chezmoi strips. In nix-managed setups we use raw
+ # symlinks, so map each hook to its stripped name explicitly. The
+ # executable bit comes from the working-tree file mode (git resolves
+ # the symlink).
+ "git/hooks/pre-push".source = link "dot_config/git/hooks/executable_pre-push";
+ "git/hooks/pre-commit".source = link "dot_config/git/hooks/executable_pre-commit";
+ "git/hooks/commit-msg".source = link "dot_config/git/hooks/executable_commit-msg";
+ "git/hooks/post-commit".source = link "dot_config/git/hooks/executable_post-commit";
+ "git/hooks/_dispatch.sh".source = link "dot_config/git/hooks/_dispatch.sh";
+ };
+
+ # ~/.ssh/config from the dotfiles tree (read-only); keys + known_hosts
+ # stay machine-local. We can't symlink via home.file because
+ # mkOutOfStoreSymlink exposes the working-tree perms (0664 under a
+ # default umask 002) and OpenSSH refuses any group-writable ssh_config.
+ # Materialize a real 0600 file via activation instead.
+ home.activation.sshConfig = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
+ run install -D -m 600 \
+ "${dotfiles}/private_dot_ssh/config" "$HOME/.ssh/config"
+ '';
+
+ # ZDOTDIR redirect so login shells find ~/.config/zsh/.zprofile etc.
+ # Also source HM's session-vars — HM normally drops these into
+ # ~/.profile, but zsh login shells don't read .profile, and we don't
+ # use programs.zsh.enable.
+ home.file.".zshenv".text = ''
+ if [ -r "$HOME/.nix-profile/etc/profile.d/hm-session-vars.sh" ]; then
+ . "$HOME/.nix-profile/etc/profile.d/hm-session-vars.sh"
+ fi
+ export ZDOTDIR="$HOME/.config/zsh"
+ [[ -r "$ZDOTDIR/.zshenv" ]] && source "$ZDOTDIR/.zshenv"
+ '';
+
+ # ── XDG base dirs ──────────────────────────────────────────────────────────
+ xdg.enable = true;
+
+ # ── Enable HM-managed activation messages ──────────────────────────────────
+ programs.home-manager.enable = true;
+}
diff --git a/nix/flake.lock b/nix/flake.lock
new file mode 100644
index 0000000..349d5fd
--- /dev/null
+++ b/nix/flake.lock
@@ -0,0 +1,49 @@
+{
+ "nodes": {
+ "home-manager": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1778628724,
+ "narHash": "sha256-VNG6hJ146VEenXcDrB3t6MVnrMx+gtyCWTCDkzOp9Qs=",
+ "owner": "nix-community",
+ "repo": "home-manager",
+ "rev": "6a0bbd6b4720da1c9ce7ebf35ff5c41a82db367a",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-community",
+ "ref": "master",
+ "repo": "home-manager",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1778443072,
+ "narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "home-manager": "home-manager",
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/nix/flake.nix b/nix/flake.nix
new file mode 100644
index 0000000..8896f2f
--- /dev/null
+++ b/nix/flake.nix
@@ -0,0 +1,44 @@
+{
+ description = "Home-Manager profiles for the Arch host and the Ubuntu remote-dev VM.";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+ home-manager = {
+ url = "github:nix-community/home-manager/master";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ };
+
+ outputs = { self, nixpkgs, home-manager, ... }:
+ let
+ system = "x86_64-linux";
+ pkgs = import nixpkgs {
+ inherit system;
+ # Whitelist specific unfree packages (claude-code,
+ # github-copilot-cli) instead of globally setting allowUnfree,
+ # so a typo elsewhere can't silently pull in additional unfree
+ # deps.
+ config.allowUnfreePredicate = pkg:
+ builtins.elem (nixpkgs.lib.getName pkg) [
+ "claude-code"
+ "github-copilot-cli"
+ ];
+ };
+
+ mkProfile = module: home-manager.lib.homeManagerConfiguration {
+ inherit pkgs;
+ modules = [ module ];
+ # Path to the cloned dotfiles checkout — passed in so the
+ # modules can symlink shared configs from the same repo.
+ extraSpecialArgs = {
+ dotfilesRoot = ../.;
+ };
+ };
+ in
+ {
+ homeConfigurations = {
+ vm = mkProfile ./vm.nix;
+ host = mkProfile ./host.nix;
+ };
+ };
+}
diff --git a/nix/host.nix b/nix/host.nix
new file mode 100644
index 0000000..7d81ffe
--- /dev/null
+++ b/nix/host.nix
@@ -0,0 +1,21 @@
+{ config, pkgs, lib, dotfilesRoot, ... }:
+
+# Arch host Home-Manager profile. Layered on top of `common.nix`; adds
+# only host-specific concerns that don't make sense on the VM.
+
+{
+ imports = [ ./common.nix ];
+
+ home.username = builtins.getEnv "USER";
+ home.homeDirectory = builtins.getEnv "HOME";
+
+ # ── Smartcard (Yubikey) ────────────────────────────────────────────────────
+ # Nix's gnupg ships its own scdaemon. Delegate to the system pcscd
+ # service instead of letting nix's scdaemon open the USB device
+ # directly (which would race with pcscd). `pcsclite` provides the
+ # shared library at the path below and stays in `meta/base.txt`.
+ home.file.".gnupg/scdaemon.conf".text = ''
+ disable-ccid
+ pcsc-driver /usr/lib/libpcsclite.so.1
+ '';
+}
diff --git a/nix/justfile b/nix/justfile
new file mode 100644
index 0000000..19e4a9b
--- /dev/null
+++ b/nix/justfile
@@ -0,0 +1,21 @@
+# Recipes for the remote-dev VM. Run from ~/.local/share/dotfiles/nix.
+
+# Show available recipes (default)
+default:
+ @just --list
+
+# Pull latest dotfiles and rebuild Home-Manager profile
+update: pull switch
+
+# Pull latest dotfiles only (config-only changes, no nix rebuild)
+pull:
+ git -C {{ justfile_directory() }}/.. pull --ff-only
+
+# Rebuild Home-Manager profile from the current checkout (no pull)
+switch:
+ home-manager switch --impure --flake '{{ justfile_directory() }}#vm' -b backup
+
+# Garbage-collect old home-manager generations and nix store
+gc:
+ home-manager expire-generations '-7 days'
+ nix-collect-garbage -d
diff --git a/nix/vm.nix b/nix/vm.nix
new file mode 100644
index 0000000..d003b6e
--- /dev/null
+++ b/nix/vm.nix
@@ -0,0 +1,71 @@
+{ config, pkgs, lib, dotfilesRoot, ... }:
+
+# VM-only Home-Manager profile (Ubuntu 22.04 remote-dev box). Adds
+# Mason-related runtime carve-outs and the rootless podman stack on
+# top of `common.nix`.
+
+{
+ imports = [ ./common.nix ];
+
+ home.username = builtins.getEnv "USER";
+ home.homeDirectory = builtins.getEnv "HOME";
+
+ home.sessionVariables = {
+ # Ubuntu 20.04-derived hosts still default to cgroups v1; podman 5
+ # warns on every invocation. Flipping to v2 is a host-level reboot
+ # and only matters for --memory/--cpus, so silence the warning.
+ PODMAN_IGNORE_CGROUPSV1_WARNING = "1";
+ };
+
+ home.packages = with pkgs; [
+ # ── Mason-driven LSP carve-outs (removed by phase p6 once Mason is
+ # gone and LSPs come from common.nix directly). Kept here for
+ # now so the VM keeps working between phases. ───────────────────────
+ jre # Mason's groovy-language-server (headless Java)
+ basedpyright # Mason's pypi distro can't install on Ubuntu 20.04
+ # (manylinux_2_28 wheels, uv's python rejects)
+ # Rust toolchain for Mason packages whose only install source is
+ # `cargo install` (shellharden). The Arch host has these via pacman;
+ # on the VM Mason needs cargo+rustc on PATH or it bails with ENOENT.
+ cargo
+ rustc
+
+ # ── Rootless podman ─────────────────────────────────────────────────────
+ # The nix `podman` is wrapped to find these helpers via /nix/store
+ # paths, so we don't need to write a containers.conf for
+ # `helper_binaries_dir`.
+ podman
+ crun # OCI runtime (lighter than runc; default for rootless)
+ conmon # container monitor process
+ netavark # default network stack on podman 4+
+ aardvark-dns # DNS for netavark networks
+ slirp4netns # rootless user-mode networking
+ passt # pasta backend (slirp4netns successor; podman picks it up)
+ ];
+
+ # ── Rootless podman config ──────────────────────────────────────────────────
+ # Kept inline (not in the chezmoi tree) because Arch's system-wide
+ # /etc/containers defaults already work there; these files exist only
+ # to give nix's user-installed podman sane rootless defaults.
+ xdg.configFile."containers/registries.conf".text = ''
+ unqualified-search-registries = ["docker.io", "quay.io", "ghcr.io"]
+ short-name-mode = "permissive"
+ '';
+
+ xdg.configFile."containers/storage.conf".text = ''
+ [storage]
+ # runroot/graphroot default to $XDG_RUNTIME_DIR/containers and
+ # $XDG_DATA_HOME/containers/storage respectively for rootless — leave unset.
+ driver = "overlay"
+
+ [storage.options.overlay]
+ # Kernel >=5.13 supports rootless overlay natively (VM is on 5.15),
+ # so mount_program is left unset → uses the kernel driver directly
+ # instead of fuse-overlayfs.
+ '';
+
+ xdg.configFile."containers/policy.json".text = builtins.toJSON {
+ default = [ { type = "insecureAcceptAnything"; } ];
+ transports.docker-daemon."" = [ { type = "insecureAcceptAnything"; } ];
+ };
+}