1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
|
{ 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";
};
# ── 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;
}
|