aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorLibravatar sommerfeld <sommerfeld@sommerfeld.dev>2026-05-20 13:56:09 +0100
committerLibravatar sommerfeld <sommerfeld@sommerfeld.dev>2026-05-20 13:56:09 +0100
commitde5146c7976e1fb38e8d1f82c30544462d881100 (patch)
tree2de6f2358d6b83b2f64b68fe105ed11d4ff0feea
parent52e53ad7956f637af3bb87de79934bfda4b74a2e (diff)
downloaddotfiles-de5146c7976e1fb38e8d1f82c30544462d881100.tar.gz
dotfiles-de5146c7976e1fb38e8d1f82c30544462d881100.tar.bz2
dotfiles-de5146c7976e1fb38e8d1f82c30544462d881100.zip
refactor(nix): promote remote-dev/ to nix/ with common/vm/host split
Restructures the Home-Manager profile to support both the Arch host and the Ubuntu remote-dev VM from the same flake. - remote-dev/ → nix/ (hard rename; .chezmoiignore updated) - home.nix split into common.nix (shared), vm.nix (Mason runtime carve-outs + podman stack), host.nix (gpg scdaemon delegation to system pcscd) - flake.nix exposes homeConfigurations.{vm,host} via a mkProfile helper - rj alias in dot_zshrc updated to ~/.local/share/dotfiles/nix - bootstrap.sh / justfile updated to use #vm against the new path The split is behaviour-preserving for the VM: vm.nix + common.nix together carry the same package set as the previous home.nix. host.nix is provisioned but not yet wired into bootstrap (phase p8). Phase 1 of the nix-on-host migration plan.
-rw-r--r--.chezmoiignore2
-rw-r--r--dot_config/zsh/dot_zshrc2
-rw-r--r--nix/README.md (renamed from remote-dev/README.md)111
-rwxr-xr-xnix/bootstrap.sh (renamed from remote-dev/bootstrap.sh)6
-rw-r--r--nix/common.nix160
-rw-r--r--nix/flake.lock (renamed from remote-dev/flake.lock)0
-rw-r--r--nix/flake.nix (renamed from remote-dev/flake.nix)26
-rw-r--r--nix/host.nix21
-rw-r--r--nix/justfile (renamed from remote-dev/justfile)2
-rw-r--r--nix/vm.nix71
-rw-r--r--remote-dev/home.nix223
11 files changed, 325 insertions, 299 deletions
diff --git a/.chezmoiignore b/.chezmoiignore
index acca5f0..34be6e8 100644
--- a/.chezmoiignore
+++ b/.chezmoiignore
@@ -6,7 +6,7 @@ systemd-units/
etc/
firefox/
thunderbird/
-remote-dev/
+nix/
justfile
just-lib.sh
selene.toml
diff --git a/dot_config/zsh/dot_zshrc b/dot_config/zsh/dot_zshrc
index 138982b..b227c8f 100644
--- a/dot_config/zsh/dot_zshrc
+++ b/dot_config/zsh/dot_zshrc
@@ -358,7 +358,7 @@ reload-env() {
# Just
alias j='just'
alias dj='just --justfile ~/dotfiles/justfile --working-directory ~/dotfiles'
-alias rj='just --justfile ~/.local/share/dotfiles/remote-dev/justfile --working-directory ~/.local/share/dotfiles/remote-dev'
+alias rj='just --justfile ~/.local/share/dotfiles/nix/justfile --working-directory ~/.local/share/dotfiles/nix'
# LLVM / Clang tooling
alias ncmake='cmake -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_FLAGS="$DEV_CFLAGS" -DCMAKE_CXX_FLAGS="$DEV_CFLAGS" -DCMAKE_INSTALL_PREFIX=build/install -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=ON -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -B build'
diff --git a/remote-dev/README.md b/nix/README.md
index c6519a5..2bf3383 100644
--- a/remote-dev/README.md
+++ b/nix/README.md
@@ -1,26 +1,31 @@
-# remote-dev
+# nix
-Headless dev environment for an Ubuntu 22.04 VM I SSH into. Deployed with
-Nix + Home-Manager. Shares the host's neovim, zellij, and zsh configs from
-the same repo — no duplication.
+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
-On a fresh VM, as the dev user (must have sudo):
+**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/remote-dev/bootstrap.sh | 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 it does
+## 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 .../remote-dev#vm`, which:
- - Installs the CLI tool subset (see `home.nix`).
+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
@@ -30,7 +35,7 @@ GitHub on first launch.
## Updating after a dotfiles change
-Run from `~/.local/share/dotfiles/remote-dev`:
+Run from `~/.local/share/dotfiles/nix` (host or VM):
```sh
just update # pull + home-manager switch (handles everything)
@@ -46,36 +51,41 @@ 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.
+> otherwise interpret `.#vm` as a glob pattern. On the host, swap
+> `#vm` → `#host`.
## Adding a tool
-Edit `home.nix`, add to `home.packages`, then `just switch` (or `just update`).
+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)
-Nix on this VM carries **leaf CLI tools** plus **editor/AI-agent
+The nix profile carries **leaf CLI tools** plus **editor/AI-agent
runtimes**, and nothing else. Specifically forbidden in `home.packages`
-because they would shadow Ubuntu's 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` (`/usr/bin/python3`) stays the default
+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 Mason/editor/AI agents, never by the
+Explicit carve-outs (used only by editor/AI agents, never by the
project build):
-- `nodejs` — `node`/`npm`/`npx` for npm-based LSPs.
-- `uv` — `uv`/`uvx` for Python LSPs in isolated venvs. `uv` does NOT
- install a `python3` in PATH; it manages its own interpreters under
- `~/.local/share/uv/`. System `python3` is untouched.
+- `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 `home.nix`.
+up). Don't add it to `common.nix`/`host.nix`/`vm.nix`.
## Commit signing on the VM (SSH-format, no GPG secrets)
@@ -127,37 +137,18 @@ git log --show-signature -1
## Caveats
- **GPG / pass**: HM installs `gnupg` and `pass` but does _not_ import
- any private key. Don't try; use SSH-format signing via the forwarded
- agent instead (see above).
-- **Disk usage**: Nix store + nvim plugins consumes ~3-5 GB. Check the
- VM's root partition size first.
+ 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. Mason will also fetch LSP servers using
- `nodejs`/`uv` from this profile.
-- **Mason pip installs need a managed `python3.11`**: a handful of Mason
- packages (autotools-language-server, codespell, mdformat,
- nginx-language-server, systemdlint, yamllint) install themselves into
- per-pkg venvs via pip. Ubuntu 20.04's `/usr/bin/python3` is 3.8 — too
- old. `bootstrap.sh` runs `uv python install 3.11` (uv is in the nix
- profile) and symlinks the resulting binary to
- `~/.local/bin/python3.11`. The versioned name leaves
- `/usr/bin/python3` untouched. On an existing VM run
- `uv python install 3.11 && ln -sf "$(uv python find 3.11)" ~/.local/bin/python3.11`
- once, then `:MasonToolsInstall` (or `:MasonInstallAll`) in nvim.
-- **`basedpyright` is provided by nix, not Mason**: its pypi distro
- pulls `nodejs-wheel-binaries`, which ships only `manylinux_2_28`
- Linux wheels. Neither Nix's python nor uv's standalone
- (`manylinux2014`-tagged) accepts those, so pip falls back to
- compiling Node 24 from source — which fails on Ubuntu 20.04's
- gcc 9.4 (needs gcc ≥10 for `-std=gnu++20`). `home.nix` adds
- `pkgs.basedpyright`; the matching AUR package (`basedpyright-bin`)
- is in `meta/base.txt` for Arch. `mason-tool-installer` no longer tries
- to install it (see `dot_config/nvim/lua/plugins/lsp.lua`).
+ 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)
+## 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:
@@ -185,26 +176,26 @@ 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 home.nix.
+# set in vm.nix.
podman run --rm docker.io/library/alpine echo hi
```
-The home-manager profile already installs `podman`, `crun`, `conmon`,
+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
-`home.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:
+`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.
+- 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` probes Arch system
-paths first, then falls back to the nix-profile path, so the same file
-works on both host and VM unchanged.
+`$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/remote-dev/bootstrap.sh b/nix/bootstrap.sh
index 1b7dafe..9f5e144 100755
--- a/remote-dev/bootstrap.sh
+++ b/nix/bootstrap.sh
@@ -2,12 +2,12 @@
# 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/remote-dev/bootstrap.sh | sh
+# 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 .../remote-dev#vm`.
+# 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.
#
@@ -66,7 +66,7 @@ fi
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/remote-dev#vm" -b backup
+ 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
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/remote-dev/flake.lock b/nix/flake.lock
index 349d5fd..349d5fd 100644
--- a/remote-dev/flake.lock
+++ b/nix/flake.lock
diff --git a/remote-dev/flake.nix b/nix/flake.nix
index 69ddafd..8896f2f 100644
--- a/remote-dev/flake.nix
+++ b/nix/flake.nix
@@ -1,5 +1,5 @@
{
- description = "Headless dev environment for remote Ubuntu VMs.";
+ description = "Home-Manager profiles for the Arch host and the Ubuntu remote-dev VM.";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
@@ -14,25 +14,31 @@
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.
+ # 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"
];
};
- in
- {
- homeConfigurations.vm = home-manager.lib.homeManagerConfiguration {
+
+ mkProfile = module: home-manager.lib.homeManagerConfiguration {
inherit pkgs;
- modules = [ ./home.nix ];
- # Path to the cloned dotfiles checkout — passed in so home.nix can
- # symlink shared configs (nvim, zellij, zsh) from the same repo.
+ 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/remote-dev/justfile b/nix/justfile
index db254df..19e4a9b 100644
--- a/remote-dev/justfile
+++ b/nix/justfile
@@ -1,4 +1,4 @@
-# Recipes for the remote-dev VM. Run from ~/.local/share/dotfiles/remote-dev.
+# Recipes for the remote-dev VM. Run from ~/.local/share/dotfiles/nix.
# Show available recipes (default)
default:
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"; } ];
+ };
+}
diff --git a/remote-dev/home.nix b/remote-dev/home.nix
deleted file mode 100644
index a94278b..0000000
--- a/remote-dev/home.nix
+++ /dev/null
@@ -1,223 +0,0 @@
-{ config, pkgs, lib, dotfilesRoot, ... }:
-
-let
- # The dotfiles checkout is cloned to ~/.local/share/dotfiles by bootstrap.sh.
- # We do NOT use `dotfilesRoot` as a Nix store path because that would copy
- # the entire repo into the store on every rebuild. Instead, we symlink
- # config dirs at runtime via `config.lib.file.mkOutOfStoreSymlink`, which
- # points at the live working tree so edits take effect without a rebuild.
- dotfiles = "${config.home.homeDirectory}/.local/share/dotfiles";
- link = path: config.lib.file.mkOutOfStoreSymlink "${dotfiles}/${path}";
-in
-{
- home.username = builtins.getEnv "USER";
- home.homeDirectory = builtins.getEnv "HOME";
- home.stateVersion = "25.05";
-
- home.sessionVariables = {
- # Ubuntu 20.04 still defaults to cgroups v1; podman 5 warns on every
- # invocation. Flipping to v2 is a host-level reboot (see README) and
- # only matters if we need --memory/--cpus, so silence the warning.
- PODMAN_IGNORE_CGROUPSV1_WARNING = "1";
- };
-
- # ── Packages ────────────────────────────────────────────────────────────────
- # 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 Ubuntu'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 Mason/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.
- 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 (user can bring their key separately)
- 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 # Mason npm LSPs + copilot-language-server (needs Node 24, see ai.lua)
- uv # Mason python LSPs in isolated venvs; brings `uv`/`uvx` only
- jre # for Mason's groovy-language-server (headless Java runtime)
- basedpyright # see lsp.lua: Mason's pypi distro can't install on Ubuntu 20.04
- # (nodejs-wheel-binaries has only manylinux_2_28 wheels which
- # uv's python rejects since it's manylinux2014; source build
- # of Node 24 needs gcc >=10 and host gcc is 9.4)
-
- # NB: python3.11 for Mason is NOT installed here — see bootstrap.sh
- # step 4. Nix's python disables manylinux wheel support by design
- # (its libc is patched and doesn't satisfy any manylinux policy), so
- # pip in a nix-python venv falls back to source builds for packages
- # like `nodejs-wheel-binaries` (pulled in by basedpyright). That
- # source build then fails on Ubuntu 20.04's gcc 9.4 (no C++20).
- # Bootstrap uses `uv python install 3.11` to fetch a portable
- # manylinux-aware CPython and symlinks it to ~/.local/bin/python3.11.
-
- # Rust toolchain for Mason packages whose only install source is
- # `cargo install` (shellharden). The host has these via the Arch
- # package manager; on the VM Mason needs cargo+rustc on PATH or it
- # bails with ENOENT.
- cargo
- rustc
-
- # AI coding agents
- claude-code
- github-copilot-cli # NB: pkgs.copilot-cli is AWS Copilot, NOT this
-
- # Zsh and plugins (sourced from $HOME/.nix-profile/share/... by the shared zshrc)
- zsh
- zsh-syntax-highlighting
- zsh-autosuggestions
- zsh-history-substring-search
-
- # Rootless podman (see README "Podman" section for host prerequisites).
- # 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)
- ];
-
- # ── 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. On remote-dev 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";
- };
-
- # ── 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"; } ];
- };
-
- # ~/.ssh/config from the dotfiles tree (read-only); keys + known_hosts
- # stay machine-local on the VM. We can't symlink via home.file because
- # mkOutOfStoreSymlink exposes the working-tree perms (0664 under Ubuntu's
- # 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 (PODMAN_IGNORE_CGROUPSV1_WARNING, etc.) —
- # 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 (Ubuntu doesn't set these in /etc/profile.d by default) ──
- xdg.enable = true;
-
- # ── Enable HM-managed activation messages ──────────────────────────────────
- programs.home-manager.enable = true;
-}