diff options
| -rw-r--r-- | .chezmoi.toml.tmpl | 6 | ||||
| -rw-r--r-- | .github/copilot-instructions.md | 12 | ||||
| -rw-r--r-- | README.md | 8 | ||||
| -rw-r--r-- | dot_config/containers/storage.conf.tmpl (renamed from dot_config/containers/storage.conf) | 15 | ||||
| -rw-r--r-- | dot_config/nvim/lua/plugins/ai.lua | 6 | ||||
| -rw-r--r-- | nix/README.md | 73 | ||||
| -rwxr-xr-x | nix/bootstrap.sh | 39 | ||||
| -rw-r--r-- | nix/common.nix | 16 | ||||
| -rw-r--r-- | nix/host.nix | 4 | ||||
| -rw-r--r-- | nix/justfile | 89 | ||||
| -rw-r--r-- | nix/vm.nix | 139 | ||||
| -rw-r--r-- | private_dot_gnupg/gpg.conf | 2 | ||||
| -rwxr-xr-x | run_onchange_after_deploy-etc.sh.tmpl | 8 | ||||
| -rwxr-xr-x | run_onchange_after_deploy-firefox.sh.tmpl | 8 | ||||
| -rw-r--r-- | run_onchange_after_deploy-flatpak-overrides.sh.tmpl | 9 | ||||
| -rw-r--r-- | run_onchange_after_deploy-pteid-pkcs11.sh.tmpl | 73 | ||||
| -rw-r--r-- | run_onchange_after_deploy-tb-eer.sh.tmpl | 1 | ||||
| -rw-r--r--[-rwxr-xr-x] | run_onchange_after_install-copilot-node.sh.tmpl (renamed from run_onchange_after_install-copilot-node.sh) | 8 |
18 files changed, 279 insertions, 237 deletions
diff --git a/.chezmoi.toml.tmpl b/.chezmoi.toml.tmpl index f7b1156..3582b7e 100644 --- a/.chezmoi.toml.tmpl +++ b/.chezmoi.toml.tmpl @@ -1,3 +1,5 @@ +{{- $defaultMachineRole := default "host" (env "CHEZMOI_MACHINE_ROLE") -}} +{{- $machineRole := promptStringOnce . "machineRole" "Machine role (host or vm)" $defaultMachineRole -}} sourceDir = {{ .chezmoi.sourceDir | quote }} [status] @@ -7,7 +9,11 @@ sourceDir = {{ .chezmoi.sourceDir | quote }} exclude = ["scripts"] [data] + # Machine role used by templates and run hooks. Valid values: "host", "vm". + machineRole = {{ $machineRole | quote }} +{{- if eq $machineRole "host" }} # Block device holding the LUKS-encrypted root, without the /dev/ prefix # (e.g. "nvme0n1p2", "sda2"). Resolved to a UUID at apply time via lsblk, # used by etc/kernel/cmdline.tmpl. luksRootPartition = {{ promptStringOnce . "luksRootPartition" "LUKS root partition (e.g. nvme0n1p2)" | quote }} +{{- end }} diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a07e728..df238ad 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -60,13 +60,11 @@ When editing shell config, all zsh configuration goes in `dot_config/zsh/` — d `KEYBINDS.md` at the repository root documents every non-default keybind across neovim, zellij, zsh, ghostty, and sway. Whenever you add, remove, or change a keybind in any of these tools, you must update `KEYBINDS.md` to reflect the change in the same commit. -## Nix VM symlink invariant +## VM chezmoi role -The remote-dev Ubuntu VM does NOT run chezmoi. It re-uses configs via Home-Manager symlinks declared in `nix/vm.nix` (the `xdg.configFile` and `home.file` blocks point at `~/.local/share/dotfiles/<source-path>`). Whenever you **add, remove, or rename** any file or directory under `dot_config/` (or any other chezmoi source path like `dot_claude/`, `private_dot_ssh/`), audit `nix/vm.nix` in the same commit and: +The remote-dev Ubuntu VM also runs chezmoi. Home-Manager installs packages and VM session variables only; it must not deploy normal dotfiles. Machine-specific dotfile behavior belongs in chezmoi templates keyed by `machineRole`: -- If the corresponding binary is provisioned by `nix/common.nix` and is relevant on the headless VM (i.e. not a GUI/wayland tool like sway/mako/waybar/fuzzel/mpv/zathura), **add a matching `xdg.configFile."<dest>".source = link "<source-path>";` entry**. -- If a config is renamed or removed, update / drop the corresponding `link` entry — otherwise `home-manager switch` will fail or leave a dangling symlink. -- Chezmoi attribute prefixes (`executable_`, `private_`, `dot_`) are stripped by chezmoi but NOT by `mkOutOfStoreSymlink`. Map each prefixed source filename to its stripped destination explicitly (see the git-hooks block in `nix/vm.nix` for the reference pattern). -- Paths outside `~/.config/` (e.g. `~/.claude/skills/…`) use `home.file."<dest>".source = link "<source-path>";` instead of `xdg.configFile`. +- `host`: user dotfiles plus host-only `/etc`, Firefox/LibreWolf, and Flatpak integration hooks. +- `vm`: user dotfiles, skipping host-only `/etc` and Firefox/LibreWolf hooks. -Treat the symlink block as part of the chezmoi source tree: a config that lands on the host via chezmoi but not on the VM via nix is the bug class this rule prevents. +When adding, removing, or renaming user config under `dot_config/`, `dot_claude/`, `private_dot_ssh/`, `private_dot_gnupg/`, etc., use chezmoi naming conventions only. Do not add corresponding `xdg.configFile` or `home.file` mappings to `nix/vm.nix`. @@ -19,7 +19,7 @@ My Arch Linux configuration, managed with [chezmoi](https://www.chezmoi.io/). | Category | Choice | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | OS & base | [Arch Linux](https://archlinux.org/) with pacman for official packages, [Nix](https://nixos.org/) / Home Manager for user-leaf tools, [sudo-rs](https://github.com/trifectatechfoundation/sudo-rs) for privilege escalation | -| Dotfile manager | [chezmoi](https://www.chezmoi.io/) (dotfiles and `/etc` both deployed via `chezmoi apply`) | +| Dotfile manager | [chezmoi](https://www.chezmoi.io/) (user dotfiles on host and VM; host-only `/etc` deployment via `chezmoi apply`) | | Task runner | [just](https://just.systems/) — every maintenance action is a recipe (see below) | | Shell | [zsh](https://www.zsh.org/), relocated to `$XDG_CONFIG_HOME/zsh`; plugins via [zinit](https://github.com/zdharma-continuum/zinit) | | Terminal | [ghostty](https://ghostty.org/) | @@ -79,8 +79,8 @@ Everything is driven by [just](https://just.systems/) recipes against four paral | `dot_*`, `private_dot_*` | chezmoi | Dotfiles deployed to `$HOME`. Prefixes: `dot_` → `.`, `private_` → `0600`, `executable_` → `+x`. | | `meta/*.txt` | `just pkg-apply`, `just pkg-status` | Plain-text package lists (one per line, `#` comments). Groups: `base`, `dev`, `wayland`, etc. | | `systemd-units/{system,user}/*.txt` | `just unit-apply`, `just unit-status` | Units to enable, split by scope. `system/` files pair by name with `meta/` groups (`system/base.txt` ↔ `meta/base.txt`); `user/` files are standalone. Recipe group token: `<name>` / `system:<name>` / `user:<name>`. | -| `etc/` | `run_onchange_after_deploy-etc.sh.tmpl` | System-level configs deployed to `/etc/` via a chezmoi onchange hook. | -| `firefox/` | `run_onchange_after_deploy-firefox.sh.tmpl` | LibreWolf hardening: renders nixpkgs' Arkenfox `user.js` plus `firefox/user-overrides.js` into the Flatpak profile, and deploys `userChrome.css` (kept under the familiar `firefox/` name). | +| `etc/` | `run_onchange_after_deploy-etc.sh.tmpl` | Host-only system-level configs deployed to `/etc/` via a chezmoi onchange hook. | +| `firefox/` | `run_onchange_after_deploy-firefox.sh.tmpl` | Host-only LibreWolf hardening: renders nixpkgs' Arkenfox `user.js` plus `firefox/user-overrides.js` into the Flatpak profile, and deploys `userChrome.css` (kept under the familiar `firefox/` name). | | (cartão de cidadão) | `run_onchange_after_deploy-pteid-pkcs11.sh.tmpl` | Bridges the `pt.gov.autenticacao` flatpak's PKCS#11 module into the NSS DB of every flatpak that needs cartão de cidadão (LibreWolf, Thunderbird, Okular, LibreOffice) — `--filesystem` + `--socket=pcsc` override + `modutil -add` per NSS DB (per-profile for Mozilla apps, shared `~/.pki/nssdb` for Okular/LibreOffice). No-op unless `pt.gov.autenticacao` is installed. | | (Thunderbird native editor) | `run_onchange_after_deploy-tb-eer.sh.tmpl` | Bridges `external-editor-revived` (host pacman package) into the Thunderbird flatpak: deploys a `flatpak-spawn --host` wrapper into the sandbox's `~/.mozilla/native-messaging-hosts/` and rewrites the manifest `path` to point at it. No-op unless TB flatpak + EER host package are both installed. | | (flatpak config sharing) | `run_onchange_after_deploy-flatpak-overrides.sh.tmpl` | Read-only `--filesystem=xdg-config/<app>:ro` overrides so the zathura and mpv flatpaks read our chezmoi-managed `~/.config/<app>/` instead of a separate in-sandbox copy. | @@ -93,7 +93,7 @@ Run `just` or `just --list` for the full menu. Recipes follow a `DOMAIN-VERB` sc | ------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | | Setup | `just init` | First-time setup: chezmoi init, git hooks, apply, base packages, curated units | | Day-to-day | `just sync` | `apply` + `pkg-fix` + `unit-apply` (full reconcile) | -| | `just apply` | `chezmoi apply` — atomically deploys dotfiles AND /etc | +| | `just apply` | `chezmoi apply` — deploys dotfiles and host-only `/etc` | | | `just re-add [PATH]` | Pull live changes back into the repo (dotfiles + /etc) | | Add / forget | `just add PATH` | Dispatches to `dotfiles-add` (path) or `etc-add` (`/etc/...`) | | | `just add GROUP NAME…` | Dispatches to `pkg-add` (bare names) or `unit-add` (ends in `.service`/`.timer`/…) | diff --git a/dot_config/containers/storage.conf b/dot_config/containers/storage.conf.tmpl index 3ba957e..62dd35c 100644 --- a/dot_config/containers/storage.conf +++ b/dot_config/containers/storage.conf.tmpl @@ -1,3 +1,17 @@ +{{- $machineRole := default "host" (index . "machineRole") -}} +{{- if eq $machineRole "vm" -}} +# Rootless podman storage configuration. +# +# The VM uses ext4, so use the kernel overlay driver. runroot/graphroot default +# to $XDG_RUNTIME_DIR/containers and $XDG_DATA_HOME/containers/storage. + +[storage] +driver = "overlay" + +[storage.options.overlay] +# Kernel >=5.13 supports rootless overlay natively on the VM, so leave +# mount_program unset and avoid fuse-overlayfs. +{{- else -}} # Rootless podman storage configuration. # # Uses the native kernel btrfs graph driver — much faster than fuse-overlayfs @@ -15,3 +29,4 @@ [storage] driver = "btrfs" +{{- end }} diff --git a/dot_config/nvim/lua/plugins/ai.lua b/dot_config/nvim/lua/plugins/ai.lua index 6ebc6f5..81a8307 100644 --- a/dot_config/nvim/lua/plugins/ai.lua +++ b/dot_config/nvim/lua/plugins/ai.lua @@ -1,8 +1,8 @@ -- Prefer the chezmoi-pinned Node 24 (host has Arch's system node 26, which -- breaks copilot-language-server — see --- ~/.local/share/chezmoi/run_onchange_after_install-copilot-node.sh). Fall --- back to `node` on PATH for hosts that don't run chezmoi (remote-dev VM --- via Nix Home-Manager, where home.nix pins nodejs_24 in the profile). +-- ~/.local/share/chezmoi/run_onchange_after_install-copilot-node.sh.tmpl). +-- Fall back to `node` on PATH on the VM, where Nix pins nodejs_24 in the +-- profile. local pinned_node = vim.fs.joinpath( vim.env.XDG_DATA_HOME or (vim.env.HOME .. "/.local/share"), "copilot-node/bin/node" diff --git a/nix/README.md b/nix/README.md index 26699d0..123bf60 100644 --- a/nix/README.md +++ b/nix/README.md @@ -24,14 +24,12 @@ GitHub on first launch. 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. +3. Runs `home-manager switch --flake .../nix#vm`, which installs the + shared CLI tool subset (see `common.nix` + `vm.nix`). +4. Writes a VM-role chezmoi config and runs `chezmoi apply`, deploying + the same user dotfiles model as the host while skipping host-only + `/etc` and Firefox hooks. +5. Appends the nix-store zsh to `/etc/shells` and `chsh`'s to it. ## Updating after a dotfiles change @@ -44,15 +42,25 @@ 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 pull # fetch the latest checkout only just switch # rebuild home-manager from the current checkout +just apply # apply VM-role chezmoi dotfiles 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`. +> `just update` runs `pull`, `switch`, and `apply`. 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`. + +Existing VMs that predate the chezmoi migration should run once: + +```sh +just migrate-chezmoi +``` + +This removes only safe old Home-Manager-managed symlinks before applying +chezmoi. ## Adding a tool @@ -97,14 +105,16 @@ One-time setup on the VM: ```sh rm -f ~/.ssh/agent.sock ~/.config/git/allowed_signers +just fix-gpg-agent +gpg-connect-agent 'getinfo version' /bye gpg --import /path/to/work-private-key.asc gpg --edit-key 3298945F717C85F8 trust quit gpg --list-secret-keys --with-keygrip 3298945F717C85F8 ``` -The VM profile symlinks the repo-owned `gpg.conf`, `gpg-agent.conf`, -and `sshcontrol` into `~/.gnupg`. The tracked git config already uses -normal OpenPGP signing, so no +Chezmoi deploys the repo-owned `gpg.conf`, `gpg-agent.conf`, and +`sshcontrol` into `~/.gnupg`. The tracked git config already uses normal +OpenPGP signing, so no `~/.config/git/config.local` override is needed for SSH-format signing. If `~/.config/git/config.local` only contains the old SSH-format signing override, remove it too. @@ -112,6 +122,8 @@ signing override, remove it too. Verify on the VM: ```sh +gpg-connect-agent 'getinfo version' /bye +echo 'Hello world!' | gpg -s --armor - ssh-add -L git commit --allow-empty -m test git log --show-signature -1 @@ -121,9 +133,9 @@ git log --show-signature -1 - **GPG / pass**: HM installs `gnupg` and `pass` but does _not_ import any private key. On the VM, import the work key manually; repo-owned - `gpg.conf`, `gpg-agent.conf`, and `sshcontrol` are symlinked by - `vm.nix`. On the host, smartcard access via `pcscd` is configured in `host.nix` - (`~/.gnupg/scdaemon.conf`). + `gpg.conf`, `gpg-agent.conf`, and `sshcontrol` are deployed by + chezmoi. 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 @@ -165,19 +177,22 @@ 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. +`netavark`, `aardvark-dns`, `slirp4netns`, and `passt`. Chezmoi deploys +role-aware `~/.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: +Home-Manager installs packages only. Chezmoi owns dotfiles on both the +host and VM, keyed by `machineRole` in the chezmoi config: + +- `host`: normal user dotfiles plus host-only `/etc`, Firefox, and + Flatpak integration hooks. +- `vm`: normal user dotfiles, skipping host-only `/etc` and Firefox + hooks. -- 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. +`home-manager switch` only needs to re-run when the Nix profile changes. +Config-only edits are picked up by `chezmoi apply`. The zsh plugins (`zsh-syntax-highlighting`, etc.) live in `$HOME/.nix-profile/share/`. The shared `dot_zshrc` prefers the diff --git a/nix/bootstrap.sh b/nix/bootstrap.sh index 86f82ca..58ab190 100755 --- a/nix/bootstrap.sh +++ b/nix/bootstrap.sh @@ -8,7 +8,8 @@ # 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. Add Nix-store zsh to /etc/shells and chsh the user. +# 4. Initialize VM-role chezmoi config and apply dotfiles. +# 5. Add Nix-store zsh to /etc/shells and chsh the user. # # Environment overrides: # DOTFILES_REPO Git URL (default: https://github.com/ruifm/dotfiles) @@ -64,7 +65,41 @@ nix --extra-experimental-features 'nix-command flakes' \ run home-manager/master -- \ switch --impure --flake "$DIR/nix#vm" -b backup -# ── 4. chsh to nix-store zsh ───────────────────────────────────────────────── +# ── 4. Chezmoi dotfiles ────────────────────────────────────────────────────── +log "Writing VM chezmoi config and applying dotfiles…" +CHEZMOI="$HOME/.nix-profile/bin/chezmoi" +if [ ! -x "$CHEZMOI" ]; then + CHEZMOI=$(command -v chezmoi) +fi +CHEZMOI_MACHINE_ROLE=vm "$CHEZMOI" init -S "$DIR" --promptDefaults +CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/chezmoi/chezmoi.toml" +if ! grep -Eq '^[[:space:]]*machineRole[[:space:]]*=[[:space:]]*"vm"[[:space:]]*$' "$CONFIG"; then + err "$CONFIG does not set machineRole = \"vm\"" + exit 1 +fi +"$CHEZMOI" apply -S "$DIR" -v + +log "Restarting GnuPG through the Nix profile…" +GPGCONF="$HOME/.nix-profile/bin/gpgconf" +GPG_CONNECT_AGENT="$HOME/.nix-profile/bin/gpg-connect-agent" +if command -v systemctl >/dev/null 2>&1; then + systemctl --user stop \ + gpg-agent.service \ + gpg-agent.socket \ + gpg-agent-ssh.socket \ + gpg-agent-extra.socket \ + gpg-agent-browser.socket >/dev/null 2>&1 || true + systemctl --user mask \ + gpg-agent.socket \ + gpg-agent-ssh.socket \ + gpg-agent-extra.socket \ + gpg-agent-browser.socket >/dev/null 2>&1 || true +fi +"$GPGCONF" --kill all >/dev/null 2>&1 || true +"$GPGCONF" --launch gpg-agent +"$GPG_CONNECT_AGENT" 'getinfo version' /bye + +# ── 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 diff --git a/nix/common.nix b/nix/common.nix index f9a7042..7290395 100644 --- a/nix/common.nix +++ b/nix/common.nix @@ -1,12 +1,10 @@ { config, pkgs, lib, dotfilesRoot, ... }: -# Shared Home-Manager module: ONLY package installation. Config-file -# deployment is *not* handled here — on the Arch host, chezmoi owns -# every dotfile under $HOME; on the remote-dev VM, `vm.nix` carries -# its own `xdg.configFile`/`home.activation` block since chezmoi isn't -# installed there. Keeping this module deployment-agnostic prevents -# home-manager from conflicting with chezmoi on the host (which would -# otherwise materialize as `.backup` files on every `nix-switch`). +# Shared Home-Manager module: ONLY package installation. Dotfile deployment is +# owned by chezmoi on both the Arch host and the remote-dev VM. Keeping this +# module deployment-agnostic prevents home-manager from conflicting with +# chezmoi-owned files (which would otherwise materialize as `.backup` files on +# every `nix-switch`). # # Policy: this profile carries leaf CLI tools, editor/AI-agent runtimes # (node, uv), and build *orchestrators* (cmake, ninja, ccache, sccache). @@ -47,6 +45,7 @@ choose zoxide just + chezmoi # Viewers bat @@ -160,8 +159,7 @@ # The nix `podman` is wrapped to find these helpers via /nix/store # paths, so we don't need a containers.conf for `helper_binaries_dir`. # Per-user containers config (registries/storage/policy) lives under - # chezmoi at `private_dot_config/containers/` and is symlinked on the - # VM by `vm.nix`'s xdg.configFile block. + # chezmoi at `dot_config/containers/`. podman crun # OCI runtime (lighter than runc; default for rootless) conmon # container monitor process diff --git a/nix/host.nix b/nix/host.nix index de68230..5a3d8a9 100644 --- a/nix/host.nix +++ b/nix/host.nix @@ -185,10 +185,6 @@ in # firefox/user-overrides.js into the Flatpak profile. arkenfox-userjs-profile - # Dotfile manager. bootstrap.sh uses the pacman `just` only long enough - # to run nix-switch; after that, this nix-profile copy is on PATH. - chezmoi - # ── OCR ────────────────────────────────────────────────────────────────── # Override merges eng + por language data into a single derivation, # replacing three pacman packages (tesseract, tesseract-data-eng, diff --git a/nix/justfile b/nix/justfile index 19e4a9b..3368193 100644 --- a/nix/justfile +++ b/nix/justfile @@ -4,10 +4,10 @@ default: @just --list -# Pull latest dotfiles and rebuild Home-Manager profile -update: pull switch +# Pull latest dotfiles, rebuild Home-Manager profile, and apply dotfiles +update: pull switch apply -# Pull latest dotfiles only (config-only changes, no nix rebuild) +# Pull latest dotfiles only pull: git -C {{ justfile_directory() }}/.. pull --ff-only @@ -15,6 +15,89 @@ pull: switch: home-manager switch --impure --flake '{{ justfile_directory() }}#vm' -b backup +# Apply VM dotfiles with chezmoi +apply: _ensure-vm-chezmoi-config + #!/usr/bin/env sh + set -eu + src=$(cd "{{ justfile_directory() }}/.." && pwd -P) + chezmoi apply -S "$src" -v + +_ensure-vm-chezmoi-config: + #!/usr/bin/env sh + set -eu + src=$(cd "{{ justfile_directory() }}/.." && pwd -P) + CHEZMOI_MACHINE_ROLE=vm chezmoi init -S "$src" --promptDefaults + config="${XDG_CONFIG_HOME:-$HOME/.config}/chezmoi/chezmoi.toml" + if ! grep -Eq '^[[:space:]]*machineRole[[:space:]]*=[[:space:]]*"vm"[[:space:]]*$' "$config"; then + echo "error: $config does not set machineRole = \"vm\"" >&2 + exit 1 + fi + +# Restart GnuPG through the Nix profile, avoiding Ubuntu's older user agent +fix-gpg-agent: + #!/usr/bin/env sh + set -eu + gpgconf_bin="$HOME/.nix-profile/bin/gpgconf" + gpg_connect_agent_bin="$HOME/.nix-profile/bin/gpg-connect-agent" + [ -x "$gpgconf_bin" ] || gpgconf_bin=$(command -v gpgconf) + [ -x "$gpg_connect_agent_bin" ] || gpg_connect_agent_bin=$(command -v gpg-connect-agent) + if command -v systemctl >/dev/null 2>&1; then + systemctl --user stop \ + gpg-agent.service \ + gpg-agent.socket \ + gpg-agent-ssh.socket \ + gpg-agent-extra.socket \ + gpg-agent-browser.socket >/dev/null 2>&1 || true + systemctl --user mask \ + gpg-agent.socket \ + gpg-agent-ssh.socket \ + gpg-agent-extra.socket \ + gpg-agent-browser.socket >/dev/null 2>&1 || true + fi + "$gpgconf_bin" --kill all >/dev/null 2>&1 || true + "$gpgconf_bin" --launch gpg-agent + "$gpg_connect_agent_bin" 'getinfo version' /bye + +# One-time migration from the old VM Home-Manager symlink deployment to chezmoi +migrate-chezmoi: pull switch fix-gpg-agent _cleanup-home-manager-dotfiles apply + +_cleanup-home-manager-dotfiles: _ensure-vm-chezmoi-config + #!/usr/bin/env bash + set -euo pipefail + src=$(cd "{{ justfile_directory() }}/.." && pwd -P) + + remove_old_symlink() { + path=$1 + [ -L "$path" ] || return 0 + raw=$(readlink "$path") + resolved=$(readlink -f "$path" 2>/dev/null || true) + case "$raw" in + "$src"/*|/nix/store/*) rm -f "$path"; return 0 ;; + esac + case "$resolved" in + "$src"/*|/nix/store/*) rm -f "$path"; return 0 ;; + esac + printf 'refusing to remove unexpected symlink: %s -> %s\n' "$path" "$raw" >&2 + exit 1 + } + + while IFS= read -r path; do + remove_old_symlink "$path" + done < <(chezmoi managed -S "$src" --include=files,symlinks --path-style=absolute) + + # The old VM profile materialized ~/.ssh/config as a real 0600 file because + # OpenSSH rejects group-writable symlink targets. Chezmoi now owns it; only + # remove the old file when it still exactly matches the repo source. + ssh_config="$HOME/.ssh/config" + if [ -f "$ssh_config" ] && [ ! -L "$ssh_config" ]; then + if cmp -s "$ssh_config" "$src/private_dot_ssh/config"; then + rm -f "$ssh_config" + else + printf 'refusing to overwrite modified %s; merge it before migrating\n' "$ssh_config" >&2 + exit 1 + fi + fi + # Garbage-collect old home-manager generations and nix store gc: home-manager expire-generations '-7 days' @@ -1,14 +1,8 @@ -{ config, pkgs, lib, dotfilesRoot, ... }: +{ ... }: -# VM-only Home-Manager profile (Ubuntu 22.04 remote-dev box). Adds the -# rootless podman stack, the editor/zellij/zsh/git config symlinks -# back into the cloned dotfiles tree, and a minimal ~/.zshenv shim — -# all of which the Arch host gets from chezmoi instead. - -let - dotfiles = "${builtins.getEnv "HOME"}/.local/share/dotfiles"; - link = path: config.lib.file.mkOutOfStoreSymlink "${dotfiles}/${path}"; -in +# VM-only Home-Manager profile (Ubuntu remote-dev box). This installs the +# shared tool profile and VM session variables only; dotfile deployment is +# owned by chezmoi, matching the Arch host. { imports = [ ./common.nix ]; @@ -26,129 +20,4 @@ in # No extra packages — the rootless podman stack now lives in # `common.nix` so the host and VM share the same nix-pinned versions. home.packages = [ ]; - - # ── 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. - # On the Arch host the same files are deployed by chezmoi; this block - # exists because the VM doesn't run chezmoi. - # - # INVARIANT: every program that is both (a) installed by `nix/common.nix` - # and (b) has a config tree under `dot_config/<name>/` MUST appear here. - # Otherwise the VM silently uses the tool's defaults while the host runs - # the tracked config — drift that's hard to spot. See - # `.github/copilot-instructions.md` (§ Nix VM symlink invariant). - xdg.configFile = { - # Editor + multiplexer + terminal - "nvim".source = link "dot_config/nvim"; - "zellij".source = link "dot_config/zellij"; - "ghostty".source = link "dot_config/ghostty"; # for terminfo refs only - - # Shells - "zsh/.zshrc".source = link "dot_config/zsh/dot_zshrc"; - "zsh/.zprofile".source = link "dot_config/zsh/dot_zprofile"; - "direnv/direnvrc".source = link "dot_config/direnv/direnvrc"; - - # Git - "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"; - - # Leaf CLI tools whose binary lives in nix/common.nix - "bat/config".source = link "dot_config/bat/config"; - "lsd/config.yaml".source = link "dot_config/lsd/config.yaml"; - "yazi".source = link "dot_config/yazi"; - "ripgrep/ripgreprc".source = link "dot_config/ripgrep/ripgreprc"; - "fd/ignore".source = link "dot_config/fd/ignore"; - "wget/wgetrc".source = link "dot_config/wget/wgetrc"; - "npm/npmrc".source = link "dot_config/npm/npmrc"; - "ipython/profile_default/ipython_config.py".source = - link "dot_config/ipython/profile_default/ipython_config.py"; - - # Debug / build tooling - "gdb/gdbinit".source = link "dot_config/gdb/gdbinit"; - "gdb/gdbearlyinit".source = link "dot_config/gdb/gdbearlyinit"; - "clangd/config.yaml".source = link "dot_config/clangd/config.yaml"; - "ccache/ccache.conf".source = link "dot_config/ccache/ccache.conf"; - - # Code review (binary from common.nix) - "tuicr/config.toml".source = link "dot_config/tuicr/config.toml"; - - # Rootless podman config — registries.conf + policy.json are - # chezmoi-owned (shared with the host); storage.conf stays inline - # below because the VM needs the overlay driver (ext4 host) while - # the Arch host uses btrfs. - "containers/registries.conf".source = link "dot_config/containers/registries.conf"; - "containers/policy.json".source = link "dot_config/containers/policy.json"; - }; - - # VM-only: overlay driver. (Host's btrfs storage.conf is chezmoi-owned - # at dot_config/containers/storage.conf.) - 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. - ''; - - # Claude-code looks under ~/.claude (NOT XDG). Skills live there. - # Symlink the whole tuicr skill directory so SKILL.md and the wrapper - # script (chezmoi `executable_` prefix preserved → see the dispatch - # comment in SKILL.md) are picked up together. - home.file.".claude/skills/tuicr/SKILL.md".source = - link "dot_claude/skills/tuicr/SKILL.md"; - home.file.".claude/skills/tuicr/tuicr-wrapper.sh".source = - link "dot_claude/skills/tuicr/executable_tuicr-wrapper.sh"; - - # GnuPG config is repo-owned like on the host. Private key material stays in - # ~/.gnupg/private-keys-v1.d and is never tracked. - home.file.".gnupg/gpg.conf".source = link "private_dot_gnupg/gpg.conf"; - home.file.".gnupg/gpg-agent.conf".source = - link "private_dot_gnupg/gpg-agent.conf"; - home.file.".gnupg/sshcontrol".source = - link "private_dot_gnupg/sshcontrol"; - - # ~/.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" - ''; - - # GnuPG cares about the homedir mode; the linked config files themselves - # contain no secrets and are repo-owned. - home.activation.gnupgDirectory = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - run install -d -m 700 "$HOME/.gnupg" - run chmod 700 "$HOME/.gnupg" - ''; - - # 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" - ''; } diff --git a/private_dot_gnupg/gpg.conf b/private_dot_gnupg/gpg.conf index e6672bf..69ceb5a 100644 --- a/private_dot_gnupg/gpg.conf +++ b/private_dot_gnupg/gpg.conf @@ -8,4 +8,4 @@ keyserver-options auto-key-retrieve keyid-format 0xlong with-fingerprint -default-key B79D F5F3 7D7F 9B0F 3902 38D5 3298 945F 717C 85F8 +default-key B79DF5F37D7F9B0F390238D53298945F717C85F8 diff --git a/run_onchange_after_deploy-etc.sh.tmpl b/run_onchange_after_deploy-etc.sh.tmpl index 5f97c70..45e5c26 100755 --- a/run_onchange_after_deploy-etc.sh.tmpl +++ b/run_onchange_after_deploy-etc.sh.tmpl @@ -1,3 +1,5 @@ +{{- $machineRole := default "host" (index . "machineRole") -}} +{{- if eq $machineRole "host" -}} #!/usr/bin/env dash # Deploy system-level configs from etc/ to /etc/. # chezmoi re-runs this script whenever any file under etc/ changes. @@ -64,3 +66,9 @@ target=$(readlink /usr/local/bin/sudo 2>/dev/null || true) if [ "$target" = /usr/bin/sudo-rs ]; then sudo rm -f /usr/local/bin/sudo fi +{{- else -}} +#!/usr/bin/env dash +# /etc deployment is host-only. +set -eu +exit 0 +{{- end }} diff --git a/run_onchange_after_deploy-firefox.sh.tmpl b/run_onchange_after_deploy-firefox.sh.tmpl index c9e49be..a9c3a1d 100755 --- a/run_onchange_after_deploy-firefox.sh.tmpl +++ b/run_onchange_after_deploy-firefox.sh.tmpl @@ -1,3 +1,5 @@ +{{- $machineRole := default "host" (index . "machineRole") -}} +{{- if eq $machineRole "host" -}} #!/usr/bin/env dash # Deploy Firefox/LibreWolf hardening and custom CSS. # chezmoi re-runs this script whenever any file under firefox/ or the nix @@ -36,3 +38,9 @@ find firefox -type f | while IFS= read -r src; do mkdir -p "$(dirname "$dest")" cp --remove-destination "$src" "$dest" done +{{- else -}} +#!/usr/bin/env dash +# Firefox/LibreWolf hardening is host-only. +set -eu +exit 0 +{{- end }} diff --git a/run_onchange_after_deploy-flatpak-overrides.sh.tmpl b/run_onchange_after_deploy-flatpak-overrides.sh.tmpl index 44664cf..441c3ff 100644 --- a/run_onchange_after_deploy-flatpak-overrides.sh.tmpl +++ b/run_onchange_after_deploy-flatpak-overrides.sh.tmpl @@ -5,12 +5,13 @@ # # script hash: {{ output "sh" "-c" (printf "sha256sum %q/run_onchange_after_deploy-flatpak-overrides.sh.tmpl 2>/dev/null || true" .chezmoi.sourceDir) }} set -eu +command -v flatpak >/dev/null 2>&1 || exit 0 apply() { - app=$1 - shift - flatpak info --user "$app" >/dev/null 2>&1 || return 0 - flatpak override --user "$@" "$app" + app=$1 + shift + flatpak info --user "$app" >/dev/null 2>&1 || return 0 + flatpak override --user "$@" "$app" } apply org.pwmt.zathura --filesystem=xdg-config/zathura:ro diff --git a/run_onchange_after_deploy-pteid-pkcs11.sh.tmpl b/run_onchange_after_deploy-pteid-pkcs11.sh.tmpl index 4f57757..504fc4d 100644 --- a/run_onchange_after_deploy-pteid-pkcs11.sh.tmpl +++ b/run_onchange_after_deploy-pteid-pkcs11.sh.tmpl @@ -8,6 +8,7 @@ # # pteid entry hash: {{ output "sh" "-c" (printf "grep '^pt\\.gov\\.autenticacao' %q/meta/flatpak.txt 2>/dev/null || true" .chezmoi.sourceDir) | sha256sum }} set -eu +command -v flatpak >/dev/null 2>&1 || exit 0 PTEID_APP=pt.gov.autenticacao MODULE_NAME=pteid-mw @@ -26,36 +27,36 @@ SO_IN_SANDBOX="/run/host$SO" SO_DIR_IN_SANDBOX="/run/host$SO_DIR" if ! command -v modutil >/dev/null 2>&1 || ! command -v certutil >/dev/null 2>&1; then - echo "pteid-pkcs11: modutil/certutil not found (install nss); skipping NSS registration." >&2 - exit 0 + echo "pteid-pkcs11: modutil/certutil not found (install nss); skipping NSS registration." >&2 + exit 0 fi apply_override() { - flatpak info --user "$1" >/dev/null 2>&1 || return 1 - flatpak override --user \ - --filesystem="$PTEID_LOC/files:ro" \ - --socket=pcsc \ - --env="LD_LIBRARY_PATH=$SO_DIR_IN_SANDBOX" \ - "$1" + flatpak info --user "$1" >/dev/null 2>&1 || return 1 + flatpak override --user \ + --filesystem="$PTEID_LOC/files:ro" \ + --socket=pcsc \ + --env="LD_LIBRARY_PATH=$SO_DIR_IN_SANDBOX" \ + "$1" } register_in_profile() { - prof="$1" - proc_name="$2" - [ -d "$prof" ] || return 0 - if [ ! -f "$prof/cert9.db" ]; then - certutil -N -d "sql:$prof" --empty-password >/dev/null 2>&1 || return 0 - fi - [ -f "$prof/cert9.db" ] || return 0 - if modutil -list -dbdir "sql:$prof" 2>/dev/null | grep -q "^[[:space:]]*Name:[[:space:]]*$MODULE_NAME$"; then - return 0 - fi - if pgrep -u "$(id -u)" -x "$proc_name" >/dev/null 2>&1; then - echo "pteid-pkcs11: $proc_name is running; close it and re-run 'chezmoi apply' to register the PKCS#11 module." >&2 - return 0 - fi - modutil -add "$MODULE_NAME" -libfile "$SO_IN_SANDBOX" -dbdir "sql:$prof" -force >/dev/null - echo "pteid-pkcs11: registered $MODULE_NAME in ${prof#"$HOME/"}" + prof="$1" + proc_name="$2" + [ -d "$prof" ] || return 0 + if [ ! -f "$prof/cert9.db" ]; then + certutil -N -d "sql:$prof" --empty-password >/dev/null 2>&1 || return 0 + fi + [ -f "$prof/cert9.db" ] || return 0 + if modutil -list -dbdir "sql:$prof" 2>/dev/null | grep -q "^[[:space:]]*Name:[[:space:]]*$MODULE_NAME$"; then + return 0 + fi + if pgrep -u "$(id -u)" -x "$proc_name" >/dev/null 2>&1; then + echo "pteid-pkcs11: $proc_name is running; close it and re-run 'chezmoi apply' to register the PKCS#11 module." >&2 + return 0 + fi + modutil -add "$MODULE_NAME" -libfile "$SO_IN_SANDBOX" -dbdir "sql:$prof" -force >/dev/null + echo "pteid-pkcs11: registered $MODULE_NAME in ${prof#"$HOME/"}" } # Mozilla-family flatpaks: per-profile NSS DBs under ~/.var/app/<id>/<profile_subdir>/<profile>/ @@ -65,13 +66,13 @@ io.gitlab.librewolf-community .librewolf librewolf org.mozilla.thunderbird .thunderbird thunderbird" echo "$MOZILLA_APPS" | while IFS=' ' read -r app profile_subdir proc_name; do - [ -n "$app" ] || continue - apply_override "$app" || continue - profiles_dir="$HOME/.var/app/$app/$profile_subdir" - [ -d "$profiles_dir" ] || continue - for prof in "$profiles_dir"/*/; do - register_in_profile "$prof" "$proc_name" - done + [ -n "$app" ] || continue + apply_override "$app" || continue + profiles_dir="$HOME/.var/app/$app/$profile_subdir" + [ -d "$profiles_dir" ] || continue + for prof in "$profiles_dir"/*/; do + register_in_profile "$prof" "$proc_name" + done done # Shared-NSS flatpaks (Poppler/LibreOffice): single ~/.pki/nssdb inside the sandbox. @@ -81,9 +82,9 @@ org.kde.okular okular org.libreoffice.LibreOffice soffice.bin" echo "$SHARED_NSS_APPS" | while IFS=' ' read -r app proc_name; do - [ -n "$app" ] || continue - apply_override "$app" || continue - prof="$HOME/.var/app/$app/.pki/nssdb" - mkdir -p "$prof" - register_in_profile "$prof/" "$proc_name" + [ -n "$app" ] || continue + apply_override "$app" || continue + prof="$HOME/.var/app/$app/.pki/nssdb" + mkdir -p "$prof" + register_in_profile "$prof/" "$proc_name" done diff --git a/run_onchange_after_deploy-tb-eer.sh.tmpl b/run_onchange_after_deploy-tb-eer.sh.tmpl index c4dabff..add406b 100644 --- a/run_onchange_after_deploy-tb-eer.sh.tmpl +++ b/run_onchange_after_deploy-tb-eer.sh.tmpl @@ -10,6 +10,7 @@ # # host manifest hash: {{ output "sh" "-c" "for p in $HOME/.nix-profile/lib/mozilla/native-messaging-hosts/external_editor_revived.json /usr/lib/mozilla/native-messaging-hosts/external_editor_revived.json /usr/lib/thunderbird/native-messaging-hosts/external_editor_revived.json; do [ -f \"$p\" ] && sha256sum \"$p\" && break; done; true" | sha256sum }} set -eu +command -v flatpak >/dev/null 2>&1 || exit 0 TB_APP=org.mozilla.thunderbird MANIFEST_NAME=external_editor_revived.json diff --git a/run_onchange_after_install-copilot-node.sh b/run_onchange_after_install-copilot-node.sh.tmpl index 9f3e72e..03a2010 100755..100644 --- a/run_onchange_after_install-copilot-node.sh +++ b/run_onchange_after_install-copilot-node.sh.tmpl @@ -1,3 +1,5 @@ +{{- $machineRole := default "host" (index . "machineRole") -}} +{{- if eq $machineRole "host" -}} #!/usr/bin/env dash # Install a Node.js 24 (LTS) runtime under ~/.local/share/copilot-node/ for the # exclusive use of copilot.lua / copilot-lsp inside neovim. System-wide nodejs @@ -42,3 +44,9 @@ rm -rf "$DEST" mkdir -p "$(dirname "$DEST")" mv "$tmp/node-v${NODE_VERSION}-linux-x64" "$DEST" touch "$STAMP" +{{- else -}} +#!/usr/bin/env dash +# VM gets Node 24 from the Nix Home-Manager profile. +set -eu +exit 0 +{{- end }} |
