From 511793cba498f52b0f92904965ea5c9afa8b6ea4 Mon Sep 17 00:00:00 2001 From: sommerfeld Date: Thu, 18 Jun 2026 12:06:20 +0100 Subject: Reduce Arch package surface --- README.md | 19 +- bootstrap.sh | 45 ++--- dot_config/nvim/lua/plugins/treesitter.lua | 10 + dot_config/pacman/makepkg.conf | 9 - dot_config/paru/paru.conf | 25 --- .../systemd/user/pass-secret-service.service | 12 ++ dot_config/waybar/executable_update-status.sh | 2 +- dot_config/zsh/dot_zprofile | 6 + dot_config/zsh/dot_zshrc | 4 +- dot_local/bin/executable_arch-news-check | 216 +++++++++++++++++++++ dot_local/bin/symlink_su | 1 + dot_local/bin/symlink_sudo | 1 + dot_local/bin/symlink_sudoedit | 1 + dot_local/bin/symlink_visudo | 1 + etc/pacman.conf | 4 +- etc/udev/rules.d/51-hid-digitalbitbox.rules | 3 + etc/udev/rules.d/52-hid-digitalbitbox.rules | 3 + etc/udev/rules.d/53-hid-bitbox02.rules | 3 + etc/udev/rules.d/54-hid-bitbox02.rules | 3 + justfile | 25 ++- meta/base.txt | 41 +--- nix/common.nix | 1 + nix/host.nix | 57 +++++- run_onchange_after_deploy-etc.sh.tmpl | 25 ++- run_onchange_after_deploy-firefox.sh.tmpl | 21 +- 25 files changed, 409 insertions(+), 129 deletions(-) delete mode 100644 dot_config/pacman/makepkg.conf delete mode 100644 dot_config/paru/paru.conf create mode 100644 dot_config/systemd/user/pass-secret-service.service create mode 100644 dot_local/bin/executable_arch-news-check create mode 100644 dot_local/bin/symlink_su create mode 100644 dot_local/bin/symlink_sudo create mode 100644 dot_local/bin/symlink_sudoedit create mode 100644 dot_local/bin/symlink_visudo create mode 100644 etc/udev/rules.d/51-hid-digitalbitbox.rules create mode 100644 etc/udev/rules.d/52-hid-digitalbitbox.rules create mode 100644 etc/udev/rules.d/53-hid-bitbox02.rules create mode 100644 etc/udev/rules.d/54-hid-bitbox02.rules diff --git a/README.md b/README.md index d8e603b..2a1e88e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ My Arch Linux configuration, managed with [chezmoi](https://www.chezmoi.io/). - **Wayland only.** No X server, no display manager. Sway starts from `exec sway` at the end of the zsh login shell on TTY1 (autologin via a host-local `getty@tty1` drop-in that's deliberately gitignored). - **XDG everywhere.** Every tool is pushed to `$XDG_CONFIG_HOME` / `$XDG_CACHE_HOME` / `$XDG_DATA_HOME` — `~` stays clean. Zsh itself lives under `$XDG_CONFIG_HOME/zsh`, bootstrapped by a single-line `dot_zshenv`. -- **[sudo-rs](https://github.com/trifectatechfoundation/sudo-rs), not the C sudo.** Memory-safe Rust rewrite, drop-in CLI compatible. Same one that Ubuntu 25.10 ships as default. +- **[sudo-rs](https://github.com/trifectatechfoundation/sudo-rs), not the C sudo.** Memory-safe Rust rewrite, drop-in CLI compatible. Exposed through user-scoped `~/.local/bin` shims instead of system-wide `/usr/local/bin` shadows. - **GPG for everything signable.** Commits and tags are signed; the same GPG agent also serves SSH authentication — one key, one cache, one PIN entry. - **Secrets via [`pass`](https://www.passwordstore.org/).** API keys and tokens are pulled into env vars at shell init; nothing is committed. - **Plain-text over configuration-as-code.** Packages and enabled units are tracked as one-per-line `.txt` files in `meta/` and `systemd-units/`, diffed against `pacman -Qeq` and `systemctl list-unit-files`. No DSL, no state file. @@ -18,7 +18,7 @@ My Arch Linux configuration, managed with [chezmoi](https://www.chezmoi.io/). | Category | Choice | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| OS & base | [Arch Linux](https://archlinux.org/), [paru](https://github.com/Morganamilo/paru) for AUR, [sudo-rs](https://github.com/trifectatechfoundation/sudo-rs) for privilege escalation | +| 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`) | | 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) | @@ -29,7 +29,7 @@ My Arch Linux configuration, managed with [chezmoi](https://www.chezmoi.io/). | Bar / launcher | [waybar](https://github.com/Alexays/Waybar), [fuzzel](https://codeberg.org/dnkl/fuzzel) | | Notifications | [mako](https://github.com/emersion/mako) | | Lock screen | [swaylock](https://github.com/swaywm/swaylock) | -| Browser | [LibreWolf](https://librewolf.net/) (Flathub `io.gitlab.librewolf-community` for bubblewrap host-isolation), hardened via `user-overrides.js` + `userChrome.css` (kept under `firefox/` by name for recognizability) | +| Browser | [LibreWolf](https://librewolf.net/) (Flathub `io.gitlab.librewolf-community` for bubblewrap host-isolation), hardened via nixpkgs' Arkenfox `user.js` + `firefox/user-overrides.js` and `userChrome.css` (kept under `firefox/` by name for recognizability) | | Mail | [Thunderbird](https://www.thunderbird.net/) (Flathub `org.mozilla.thunderbird`) against [ProtonMail Bridge](https://proton.me/mail/bridge) + Radicale (CalDAV/CardDAV); non-private prefs tracked under `thunderbird/` | | Secrets & identity | [GPG](https://gnupg.org/) (commit signing + SSH auth via gpg-agent), [pass](https://www.passwordstore.org/) | | Media & viewers | [mpv](https://mpv.io/) (Flathub `io.mpv.Mpv`; streamlink launches it via `flatpak run`), [zathura](https://pwmt.org/projects/zathura/) (Flathub `org.pwmt.zathura`), [yazi](https://yazi-rs.github.io/) | @@ -56,12 +56,11 @@ curl -fsSL https://raw.githubusercontent.com/sommerfelddev/dotfiles/master/boots ``` The script installs pacman prerequisites, enables `%wheel` in sudoers, -builds `paru-bin` from the AUR, clones this repo to `~/dotfiles`, runs -`just init`, enables recommended systemd units (fstrim, timesyncd, -resolved, reflector, paccache, pkgstats, acpid, cpupower, iwd, plus tlp -on laptops), refreshes the pacman mirrorlist, and creates XDG user -directories. On EFI systems missing an Arch boot entry, it prints the -`efibootmgr` command to register the UKI (run after your first +clones this repo to `~/dotfiles`, runs `just init`, enables recommended +systemd units (fstrim, timesyncd, resolved, reflector, paccache, pkgstats, +acpid, cpupower, iwd, plus tlp on laptops), refreshes the pacman mirrorlist, +and creates XDG user directories. On EFI systems missing an Arch boot entry, +it prints the `efibootmgr` command to register the UKI (run after your first `mkinitcpio -P`). ## Setup on an existing system @@ -81,7 +80,7 @@ Everything is driven by [just](https://just.systems/) recipes against four paral | `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: `` / `system:` / `user:`. | | `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 `user-overrides.js` + `userChrome.css` (kept under the familiar `firefox/` name). | +| `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). | | (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/:ro` overrides so the zathura and mpv flatpaks read our chezmoi-managed `~/.config//` instead of a separate in-sandbox copy. | diff --git a/bootstrap.sh b/bootstrap.sh index 79e6307..6ffbd76 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -5,7 +5,7 @@ # # Prerequisites (from the Arch installation guide): # - A regular user already exists and is a member of the 'wheel' group. -# - You are logged in as that user (paru/makepkg refuse to run as root). +# - You are logged in as that user. # # Usage: # curl -fsSL https://raw.githubusercontent.com/sommerfelddev/dotfiles/master/bootstrap.sh | sh @@ -23,7 +23,7 @@ die() { exit 1 } -# 0. refuse root — paru/makepkg won't run as root +# 0. refuse root — Home Manager and the user-owned dotfiles expect a real user. [ "$(id -u)" -ne 0 ] || die "run this as your regular user, not root" # 1. user must be in wheel (required so the sudoers rule we enable takes effect) @@ -31,14 +31,11 @@ id -nG "$USER" | tr ' ' '\n' | grep -qx wheel || die "user '$USER' must be in the 'wheel' group" # 2. install sudo + pacman prerequisites, enable wheel in sudoers. -# `chezmoi` and `paru` are intentionally NOT in this list — chezmoi -# is run ephemerally via `nix-shell` below for the one-shot deploy, -# and paru lands in ~/.nix-profile/bin after the first nix-switch -# (we install `just init`'s AUR deps using *that* nix-store paru, -# not a manually built paru-bin). `just` and `git` stay on pacman -# so the script + early `just nix-switch` work before the nix +# `chezmoi` is intentionally NOT in this list — it lands in +# ~/.nix-profile/bin after the first nix-switch. `just` and `git` stay +# on pacman so the script + early `just nix-switch` work before the nix # profile is activated. -PREREQS='sudo git base-devel just efibootmgr nix' +PREREQS='sudo git just efibootmgr nix' SUDOERS_SED='s/^# *\(%wheel ALL=(ALL:ALL\(:ALL\)*) ALL\)/\1/' if ! command -v sudo >/dev/null 2>&1; then @@ -86,29 +83,27 @@ else fi cd "$DOTFILES_DIR" -# 6. nix-switch FIRST. This installs paru + chezmoi (plus the wayland -# session tools, qrencode, torsocks, lshw, yt-dlp, streamlink, -# tesseract, whisper-cpp, …) into ~/.nix-profile/bin so the -# subsequent `just init` finds them on PATH. The repo is already a -# valid Nix flake — we don't need chezmoi to have run yet. -log 'running nix-switch (installs paru + user-leaf tools from nix)' +# 6. nix-switch FIRST. This installs chezmoi (plus the wayland session +# tools, qrencode, torsocks, lshw, yt-dlp, streamlink, tesseract, +# whisper-cpp, …) into ~/.nix-profile/bin so the subsequent `just init` +# finds them on PATH. The repo is already a valid Nix flake — we don't +# need chezmoi to have run yet. +log 'running nix-switch (installs chezmoi + user-leaf tools from nix)' just nix-switch # Add nix-profile to PATH for the remaining steps so freshly installed -# tools (paru, chezmoi) are picked up immediately. Login shells will +# tools (chezmoi, etc.) are picked up immediately. Login shells will # resolve it via /etc/profile.d/hm-session-vars.sh after re-login. export PATH="$HOME/.nix-profile/bin:$PATH" # 7. run just init — this deploys chezmoi, installs the 'base' meta list -# (which pulls in sudo-rs via the nix-profile paru), deploys -# /etc/sudoers-rs, /etc/pam.d/sudo, creates -# /usr/local/bin/{sudo,su,visudo,sudoedit} symlinks pointing at -# sudo-rs (PATH precedence shadows /usr/bin/sudo), and installs git -# hooks. The classic 'sudo' package stays installed because -# base-devel hard-depends on it; that's harmless — the binary is -# never invoked once /usr/local/bin/sudo is in place. `just init` -# also re-runs nix-switch as its last step (a no-op since step 6 -# already activated the profile). +# (which pulls in sudo-rs via pacman), deploys +# /etc/sudoers-rs, /etc/pam.d/sudo, creates user-scoped +# ~/.local/bin/{sudo,su,visudo,sudoedit} symlinks pointing at sudo-rs, +# and installs git hooks. The classic 'sudo' package is only a bootstrap +# helper and may be removed if it shows up as undeclared. `just init` +# also re-runs nix-switch as its last step (a no-op since step 6 already +# activated the profile). log 'running just init' just init diff --git a/dot_config/nvim/lua/plugins/treesitter.lua b/dot_config/nvim/lua/plugins/treesitter.lua index 4f3ad31..0cba10f 100644 --- a/dot_config/nvim/lua/plugins/treesitter.lua +++ b/dot_config/nvim/lua/plugins/treesitter.lua @@ -1,5 +1,15 @@ require("treewalker").setup({}) +-- nvim-treesitter's main branch shells out to tree-sitter, whose Rust cc crate +-- respects CC. On the Arch host, Home Manager sets this to a Nix compiler +-- wrapper so parser builds keep working without global base-devel. +if + (vim.env.CC == nil or vim.env.CC == "") + and vim.env.NVIM_TREESITTER_CC ~= nil +then + vim.env.CC = vim.env.NVIM_TREESITTER_CC +end + vim.keymap.set( { "n", "v" }, "", diff --git a/dot_config/pacman/makepkg.conf b/dot_config/pacman/makepkg.conf deleted file mode 100644 index 7233660..0000000 --- a/dot_config/pacman/makepkg.conf +++ /dev/null @@ -1,9 +0,0 @@ -CFLAGS="-march=native -Ofast -pipe -fomit-frame-pointer" -RUSTFLAGS="-C opt-level=2 -C target-cpu=native" -MAKEFLAGS="-j4" -BUILDDIR=/tmp/makepkg -COMPRESSGZ=(pigz -c -f -n) -COMPRESSBZ2=(pbzip2 -c -f) -COMPRESSXZ=(xz -c -z - --threads=0) -COMPRESSZST=(zstd -c -z -q - --threads=0) -PKGEXT='.pkg.tar.zst' diff --git a/dot_config/paru/paru.conf b/dot_config/paru/paru.conf deleted file mode 100644 index 6a3632c..0000000 --- a/dot_config/paru/paru.conf +++ /dev/null @@ -1,25 +0,0 @@ -# -# /etc/paru.conf -# ~/.config/paru/paru.conf -# -# See the paru.conf(5) manpage for options - -# -# GENERAL OPTIONS -# -[options] -PgpFetch -Devel -Provides -DevelSuffixes = -git -cvs -svn -bzr -darcs -always -# BottomUp -RemoveMake -SudoLoop -#UseAsk -CombinedUpgrade -BatchInstall -# UpgradeMenu -NewsOnUpgrade -CleanAfter -SaveChanges -NoWarn = sommerfeld-* diff --git a/dot_config/systemd/user/pass-secret-service.service b/dot_config/systemd/user/pass-secret-service.service new file mode 100644 index 0000000..d7dc564 --- /dev/null +++ b/dot_config/systemd/user/pass-secret-service.service @@ -0,0 +1,12 @@ +[Unit] +Description=Expose the libsecret D-Bus API with pass as backend +Documentation=https://github.com/grimsteel/pass-secret-service + +[Service] +Type=dbus +BusName=org.freedesktop.secrets +ExecStart=%h/.nix-profile/bin/pass-secret-service + +[Install] +WantedBy=default.target +Alias=dbus-org.freedesktop.secrets.service diff --git a/dot_config/waybar/executable_update-status.sh b/dot_config/waybar/executable_update-status.sh index cbd899e..a300245 100755 --- a/dot_config/waybar/executable_update-status.sh +++ b/dot_config/waybar/executable_update-status.sh @@ -3,7 +3,7 @@ # upgraded recently. Source of truth is /var/log/pacman.log — the last # "[PACMAN] starting full system upgrade" entry. No daemon, no -Sy # polling, no opinion about *which* updates are pending; this only -# tracks whether you've run `paru -Syu` (or equivalent) lately. +# tracks whether you've run `pacman -Syu` lately. # # States, by hours since last full upgrade: # < 24h empty (hidden via :empty rule in style.css) diff --git a/dot_config/zsh/dot_zprofile b/dot_config/zsh/dot_zprofile index b95079b..20852db 100644 --- a/dot_config/zsh/dot_zprofile +++ b/dot_config/zsh/dot_zprofile @@ -29,6 +29,12 @@ export XDG_CACHE_HOME="$HOME/.cache" # even if /etc/profile.d snippets try to prepend duplicates. [[ -r /etc/profile ]] && emulate sh -c 'source /etc/profile' +# Home Manager writes host-specific session variables here. Keep this after +# /etc/profile so Nix/HM values can provide user-level tool wiring without +# being clobbered by distro profile snippets. +[[ -r "$HOME/.nix-profile/etc/profile.d/hm-session-vars.sh" ]] && + emulate sh -c ". \"$HOME/.nix-profile/etc/profile.d/hm-session-vars.sh\"" + # ── Locale ──────────────────────────────────────────────────────────────────── export LANG=en_US.UTF-8 diff --git a/dot_config/zsh/dot_zshrc b/dot_config/zsh/dot_zshrc index c697594..d78c9e5 100644 --- a/dot_config/zsh/dot_zshrc +++ b/dot_config/zsh/dot_zshrc @@ -46,7 +46,7 @@ fpath=($XDG_DATA_HOME/zsh/completion $fpath) # Pull in completions from both nix-installed packages AND the system # package manager (pacman on Arch host, apt on Ubuntu VM). nix zsh's # default fpath only includes its own nix-store dirs, so without these -# entries we miss completions for pacman, paru, systemctl, flatpak, +# entries we miss completions for pacman, systemctl, flatpak, # docker, kubectl, etc. on the host, and apt/snap on the VM. for _d in "$HOME/.nix-profile/share/zsh/site-functions" \ "$HOME/.nix-profile/share/zsh/vendor-completions" \ @@ -77,7 +77,7 @@ zstyle ':completion:*' completer _expand_alias _complete _ignored _match _approx zstyle ':completion:*:default' list-colors ${(s.:.)LS_COLORS} # colorize file completions like ls zstyle ':completion:*' use-cache on # cache completions (speeds up pip, dpkg, etc.) zstyle ':completion:*' cache-path "$XDG_CACHE_HOME/zsh" -zstyle ':completion:*' rehash true # rebuild PATH hash on every completion (catches paru, cargo, pip, manual installs) +zstyle ':completion:*' rehash true # rebuild PATH hash on every completion (catches cargo, pip, manual installs) zstyle ':completion:*:match:*' original only # only show original when pattern-matching zstyle ':completion:*:functions' ignored-patterns '_*' # hide internal completion functions zstyle ':completion:*:*:kill:*' menu yes select # interactive menu for kill completion diff --git a/dot_local/bin/executable_arch-news-check b/dot_local/bin/executable_arch-news-check new file mode 100644 index 0000000..1d78a78 --- /dev/null +++ b/dot_local/bin/executable_arch-news-check @@ -0,0 +1,216 @@ +#!/usr/bin/env dash +set -eu + +mark_read=false +for arg in "$@"; do + case "$arg" in + --mark-read) + mark_read=true + ;; + -h | --help) + cat <<'EOF' +usage: arch-news-check [--mark-read] + +Check Arch Linux news before a system upgrade. New feed entries are printed +and followed by a "Proceed with upgrade? [Y/n]" prompt. Choosing yes records +the currently visible feed items as seen. +EOF + exit 0 + ;; + *) + printf 'arch-news-check: unknown option: %s\n' "$arg" >&2 + exit 2 + ;; + esac +done + +prompt_proceed() { + if [ ! -t 0 ]; then + printf 'arch-news-check: non-interactive shell; proceeding by default\n' >&2 + return 0 + fi + + while :; do + printf ':: Proceed with upgrade? [Y/n] ' >/dev/tty + IFS= read -r answer /dev/tty ;; + esac + done +} + +if ! command -v python3 >/dev/null 2>&1; then + if [ "$mark_read" = true ]; then + printf 'arch-news-check: python3 not found; cannot mark Arch news as read\n' >&2 + exit 1 + fi + printf 'arch-news-check: python3 not found; skipping Arch news check\n' >&2 + prompt_proceed + exit $? +fi + +exec python3 - "$@" <<'PY' +from __future__ import annotations + +import email.utils +import html +import json +import os +import re +import sys +import textwrap +import urllib.error +import urllib.request +import xml.etree.ElementTree as ET +from pathlib import Path + +FEED_URL = os.environ.get("ARCH_NEWS_FEED_URL", "https://archlinux.org/feeds/news/") +STATE_FILE = Path( + os.environ.get( + "ARCH_NEWS_STATE_FILE", + Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local/state")) + / "arch-news-check" + / "seen.json", + ) +) + + +def usage_error(message: str) -> int: + print(f"arch-news-check: {message}", file=sys.stderr) + return 2 + + +def prompt_proceed() -> bool: + if not sys.stdin.isatty(): + print("arch-news-check: non-interactive shell; proceeding by default", file=sys.stderr) + return True + + while True: + answer = input(":: Proceed with upgrade? [Y/n] ").strip().lower() + if answer in {"", "y", "yes"}: + return True + if answer in {"n", "no"}: + return False + print("Please answer yes or no.") + + +def fetch_feed() -> bytes: + request = urllib.request.Request( + FEED_URL, + headers={"User-Agent": "arch-news-check/1.0 (+https://archlinux.org/)"}, + ) + with urllib.request.urlopen(request, timeout=10) as response: + return response.read() + + +def clean_text(value: str) -> str: + value = html.unescape(value) + value = re.sub(r"<[^>]+>", " ", value) + value = re.sub(r"\s+", " ", value) + return value.strip() + + +def parse_date(value: str) -> str: + if not value: + return "" + try: + return email.utils.parsedate_to_datetime(value).date().isoformat() + except (TypeError, ValueError): + return value + + +def parse_feed(raw_xml: bytes) -> list[dict[str, str]]: + root = ET.fromstring(raw_xml) + entries: list[dict[str, str]] = [] + for item in root.findall("./channel/item"): + title = clean_text(item.findtext("title", "")) + link = clean_text(item.findtext("link", "")) + guid = clean_text(item.findtext("guid", "")) or link or title + published = parse_date(clean_text(item.findtext("pubDate", ""))) + summary = clean_text(item.findtext("description", "")) + if guid: + entries.append( + { + "id": guid, + "title": title or "(untitled)", + "link": link, + "published": published, + "summary": summary, + } + ) + return entries + + +def load_seen() -> set[str]: + try: + data = json.loads(STATE_FILE.read_text()) + except FileNotFoundError: + return set() + except (OSError, json.JSONDecodeError): + return set() + seen = data.get("seen", []) + if not isinstance(seen, list): + return set() + return {item for item in seen if isinstance(item, str)} + + +def save_seen(ids: set[str]) -> None: + STATE_FILE.parent.mkdir(parents=True, exist_ok=True) + tmp = STATE_FILE.with_suffix(".tmp") + tmp.write_text(json.dumps({"seen": sorted(ids)}, indent=2) + "\n") + tmp.replace(STATE_FILE) + + +def print_news(entries: list[dict[str, str]]) -> None: + print(":: New Arch Linux news") + for entry in entries: + date = f" ({entry['published']})" if entry["published"] else "" + print(f" -> {entry['title']}{date}") + if entry["link"]: + print(f" {entry['link']}") + if entry["summary"]: + summary = textwrap.shorten(entry["summary"], width=500, placeholder=" ...") + print(textwrap.indent(textwrap.fill(summary, width=88), " ")) + print() + + +def main(argv: list[str]) -> int: + mark_read = False + for arg in argv: + if arg == "--mark-read": + mark_read = True + elif arg in {"-h", "--help"}: + return usage_error("help is handled by the shell wrapper") + else: + return usage_error(f"unknown option: {arg}") + + try: + entries = parse_feed(fetch_feed()) + except (ET.ParseError, OSError, urllib.error.URLError) as exc: + print(f"arch-news-check: failed to read Arch news: {exc}", file=sys.stderr) + return 0 if prompt_proceed() else 1 + + current_ids = {entry["id"] for entry in entries} + if mark_read: + save_seen(current_ids) + print("Marked current Arch Linux news as read.") + return 0 + + seen = load_seen() + unread = [entry for entry in entries if entry["id"] not in seen] + if not unread: + return 0 + + print_news(unread) + if not prompt_proceed(): + return 1 + + save_seen(current_ids) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) +PY diff --git a/dot_local/bin/symlink_su b/dot_local/bin/symlink_su new file mode 100644 index 0000000..73045e0 --- /dev/null +++ b/dot_local/bin/symlink_su @@ -0,0 +1 @@ +/usr/bin/su-rs diff --git a/dot_local/bin/symlink_sudo b/dot_local/bin/symlink_sudo new file mode 100644 index 0000000..0830fa1 --- /dev/null +++ b/dot_local/bin/symlink_sudo @@ -0,0 +1 @@ +/usr/bin/sudo-rs diff --git a/dot_local/bin/symlink_sudoedit b/dot_local/bin/symlink_sudoedit new file mode 100644 index 0000000..0830fa1 --- /dev/null +++ b/dot_local/bin/symlink_sudoedit @@ -0,0 +1 @@ +/usr/bin/sudo-rs diff --git a/dot_local/bin/symlink_visudo b/dot_local/bin/symlink_visudo new file mode 100644 index 0000000..535dcf1 --- /dev/null +++ b/dot_local/bin/symlink_visudo @@ -0,0 +1 @@ +/usr/bin/visudo-rs diff --git a/etc/pacman.conf b/etc/pacman.conf index 8f8be59..aaa7fef 100644 --- a/etc/pacman.conf +++ b/etc/pacman.conf @@ -22,8 +22,8 @@ HoldPkg = pacman glibc Architecture = auto # Pacman won't upgrade packages listed in IgnorePkg and members of IgnoreGroup -# llama.cpp-vulkan: AUR rebuilds on every llama.cpp commit (1-2 hour build); -# pin and update manually with `paru -S llama.cpp-vulkan` when intended. +# llama.cpp-vulkan: formerly pinned as an AUR package because every upstream +# commit triggered a 1-2 hour rebuild. Kept as a reminder if it returns. #IgnorePkg = #IgnoreGroup = diff --git a/etc/udev/rules.d/51-hid-digitalbitbox.rules b/etc/udev/rules.d/51-hid-digitalbitbox.rules new file mode 100644 index 0000000..49654ed --- /dev/null +++ b/etc/udev/rules.d/51-hid-digitalbitbox.rules @@ -0,0 +1,3 @@ +# BitBox V1. Sourced from Shift Crypto's Linux post-install rules: +# digitalbitbox/bitbox-wallet-app d045d25ac1a3839bcb7993d95bf2501bc1e86780. +SUBSYSTEM=="usb", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="dbb%n", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2402" diff --git a/etc/udev/rules.d/52-hid-digitalbitbox.rules b/etc/udev/rules.d/52-hid-digitalbitbox.rules new file mode 100644 index 0000000..cded163 --- /dev/null +++ b/etc/udev/rules.d/52-hid-digitalbitbox.rules @@ -0,0 +1,3 @@ +# BitBox V1 hidraw. Sourced from Shift Crypto's Linux post-install rules: +# digitalbitbox/bitbox-wallet-app d045d25ac1a3839bcb7993d95bf2501bc1e86780. +KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2402", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="dbbf%n" diff --git a/etc/udev/rules.d/53-hid-bitbox02.rules b/etc/udev/rules.d/53-hid-bitbox02.rules new file mode 100644 index 0000000..977b1f4 --- /dev/null +++ b/etc/udev/rules.d/53-hid-bitbox02.rules @@ -0,0 +1,3 @@ +# BitBox02. Sourced from Shift Crypto's Linux post-install rules: +# digitalbitbox/bitbox-wallet-app d045d25ac1a3839bcb7993d95bf2501bc1e86780. +SUBSYSTEM=="usb", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02_%n", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403" diff --git a/etc/udev/rules.d/54-hid-bitbox02.rules b/etc/udev/rules.d/54-hid-bitbox02.rules new file mode 100644 index 0000000..e0484ac --- /dev/null +++ b/etc/udev/rules.d/54-hid-bitbox02.rules @@ -0,0 +1,3 @@ +# BitBox02 hidraw. Sourced from Shift Crypto's Linux post-install rules: +# digitalbitbox/bitbox-wallet-app d045d25ac1a3839bcb7993d95bf2501bc1e86780. +KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02-%n" diff --git a/justfile b/justfile index 287955b..6936149 100644 --- a/justfile +++ b/justfile @@ -64,9 +64,18 @@ nix-switch: # Update everything: system packages, flatpaks, nix flake inputs update: pkg-update flatpak-update nix-update -# Upgrade all system + AUR packages +# Upgrade official Arch packages, after showing newly published Arch news. pkg-update: - paru -Syu + @just arch-news-check + @sudo pacman -Syu + +# Show new Arch Linux news and ask whether to proceed, like paru's NewsOnUpgrade. +arch-news-check: + @sh "{{ justfile_directory() }}/dot_local/bin/executable_arch-news-check" + +# Mark the current Arch Linux news feed as seen without running an upgrade. +arch-news-read: + @sh "{{ justfile_directory() }}/dot_local/bin/executable_arch-news-check" --mark-read # Refresh nix flake inputs (nixpkgs, home-manager) then re-activate the profile. @@ -1001,7 +1010,7 @@ pkg-status: fi just undeclared | sed 's/^/ undeclared: /' -# Print undeclared packages one per line, unindented (pipe to 'paru -Rs -' to remove pacman entries) +# Print undeclared packages one per line, unindented. undeclared: #!/usr/bin/env dash active=$(just _active-packages) @@ -1079,7 +1088,7 @@ pkg-list group="": pkg-apply *groups: #!/usr/bin/env dash set -eu - # `paru -S --needed` is a no-op for already-installed packages, which + # `pacman -S --needed` is a no-op for already-installed packages, which # means a package pulled in transitively (and later declared in # meta/*.txt) stays marked "installed as a dependency" forever and # keeps showing up under `pacopt`. After each install pass, force the @@ -1102,7 +1111,7 @@ pkg-apply *groups: fi pkgs=$(sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' "$file") [ -n "$pkgs" ] || continue - printf '%s\n' "$pkgs" | paru -S --needed --noconfirm --ask=4 - + printf '%s\n' "$pkgs" | sudo pacman -S --needed --noconfirm - mark_explicit "$pkgs" done else @@ -1110,7 +1119,7 @@ pkg-apply *groups: | xargs -0 cat \ | sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' \ | sort -u) - printf '%s\n' "$all_pkgs" | paru -S --needed --noconfirm --ask=4 - + printf '%s\n' "$all_pkgs" | sudo pacman -S --needed --noconfirm - mark_explicit "$all_pkgs" [ -f meta/flatpak.txt ] && just _flatpak-install fi @@ -1140,7 +1149,7 @@ pkg-fix: if [ "$group" = "flatpak" ]; then just _flatpak-install else - echo "$pkgs" | paru -S --needed --noconfirm --ask=4 - + echo "$pkgs" | sudo pacman -S --needed --noconfirm - fi fi done @@ -1167,7 +1176,7 @@ pkg-add group +pkgs: https://dl.flathub.org/repo/flathub.flatpakrepo >/dev/null flatpak install --user -y --noninteractive flathub {{ pkgs }} else - paru -S --needed {{ pkgs }} + sudo pacman -S --needed {{ pkgs }} fi # Remove one or more packages from a group list (does NOT uninstall; the package may belong to other groups) diff --git a/meta/base.txt b/meta/base.txt index c0cb1da..d817340 100644 --- a/meta/base.txt +++ b/meta/base.txt @@ -3,14 +3,13 @@ # net / debug+trace / docs / secrets — all provisioned via Home-Manager # from nix/common.nix and live under ~/.nix-profile/bin (first in PATH). # What stays on pacman in this section is the pieces tightly coupled to -# the distro (man-db/man-pages files), the system runtime (sudo-rs, -# base/base-devel), and things needed pre-bootstrap or by other system -# packages transitively. User-leaf CLIs/daemons (chezmoi, paru, qrencode, -# torsocks, lshw, xdg-utils, syncthing) now come from nix/host.nix. +# the distro, the system runtime (sudo-rs, base), and things needed +# pre-bootstrap or by other system packages transitively. User-leaf +# CLIs/daemons/docs (chezmoi, man-db/man-pages, qrencode, torsocks, +# lshw, xdg-utils, syncthing) now come from nix. acpid arch-audit base -base-devel btrfs-progs cpupower dosfstools @@ -23,16 +22,12 @@ linux-hardened-headers linux-lts linux-lts-headers lostfiles -man-db -man-pages nfs-utils nftables ocl-icd # Provides paccache for the repo-owned cache cleanup hook under # etc/pacman.d/hooks/. pacman-contrib -pbzip2 -pigz pkgstats rebuild-detector reflector @@ -53,7 +48,7 @@ ell bolt # --- nix (multi-user daemon mode for hermetic per-project dev shells via -# `nix develop` + direnv `use flake`. Not a replacement for paru/pacman, +# `nix develop` + direnv `use flake`. Not a replacement for pacman, # not home-manager, not NixOS — just a sandboxed second package manager # that gives every project a reproducible toolchain pinned in its own # flake.lock. Pairs with: systemd-units/system.txt (enables @@ -62,14 +57,10 @@ bolt # direnv's source_url with a content hash, so no extra package needed.) --- nix -# --- dev (system-coupled runtimes only — base-devel ships gcc/ld/as/make -# for general-purpose builds; the orchestrators (cmake/ninja/ccache/ -# sccache), debuggers and toolchain-specific compilers/linkers live in -# nix instead. clang/lld/mold/rustup/go are intentionally absent — when -# a project needs them, the project's flake.nix + direnv `.envrc` -# provide them. The podman stack (podman, crun, conmon, netavark, -# aardvark-dns, slirp4netns, passt, podman-compose, podman-docker) now -# comes from nix/common.nix — unified across host and VM.) --- +# --- dev (system-coupled runtime only). No base-devel: AUR/makepkg is not +# part of the normal system, and project toolchains come from direnv + nix +# devShells. Build orchestrators, debuggers, and user CLIs live in +# nix/common.nix. --- perf # links against running kernel ABI; must match kernel pkg # --- sound --- @@ -114,22 +105,12 @@ libnotify # Lock screen (setuid; PAM-coupled) swaylock -# org.freedesktop.secrets D-Bus implementation backed by pass. Required -# by Signal Desktop (flatpak) and other libsecret consumers. Ships both -# a D-Bus activation file and a systemd user unit; we enable the unit -# explicitly so it's visible in `systemctl --user status`. Stores -# secrets under ~/.password-store/secret-service/. -pass-secret-service-bin - # Ships ZSA's upstream udev rules (50-oryx.rules, 50-wally.rules) to # /usr/lib/udev/rules.d/ so VID 3297 hidraw nodes get TAG+=uaccess. # Required for VIA / usevia.app (WebHID) and Wally flashing of the # ErgoDox EZ / Moonlander / Voyager. zsa-udev -# Udev rules for BitBox hardware wallet access from Sparrow. -bitbox-udev - # QR (system lib used by zbarcam; the qrencode CLI is in nix/host.nix) zbar xorg-xwayland # needed for zbarcam's X11 preview @@ -148,10 +129,6 @@ qt5ct qt6ct xdg-user-dirs -# --- browser (LibreWolf flatpak; arkenfox-user.js is the host-side -# hardening overlay deployed by run_onchange_after_deploy-firefox.sh.tmpl) --- -arkenfox-user.js - # --- mail (host-side bits the org.mozilla.thunderbird flatpak depends on) --- # protonmail-bridge now comes from nix/host.nix (the bridge binary + its # repo-owned user unit dot_config/systemd/user/protonmail-bridge.service). diff --git a/nix/common.nix b/nix/common.nix index 2a71de4..c1cf551 100644 --- a/nix/common.nix +++ b/nix/common.nix @@ -117,6 +117,7 @@ doxygen # Docs + less tldr man-db man-pages diff --git a/nix/host.nix b/nix/host.nix index ca8bcc4..de68230 100644 --- a/nix/host.nix +++ b/nix/host.nix @@ -43,6 +43,37 @@ let platforms = platforms.all; }; }; + pass-secret-service-rust = pkgs.rustPlatform.buildRustPackage rec { + pname = "pass-secret-service"; + version = "0.7.0"; + + src = pkgs.fetchFromGitHub { + owner = "grimsteel"; + repo = "pass-secret-service"; + rev = "v${version}"; + hash = "sha256-cBDGxF1ETyszwHZJwN8n+lwKcpOU8Xt1XTOGbUHj9UI="; + }; + + cargoHash = "sha256-Ko8LlgPG6kl+pZ47jrFnKdc+9i7/eh9DMRtG2SWQGjQ="; + nativeBuildInputs = [ pkgs.makeWrapper ]; + + postInstall = '' + wrapProgram "$out/bin/pass-secret-service" \ + --prefix PATH : "${lib.makeBinPath [ pkgs.gnupg ]}" + ''; + + meta = { + description = "Implementation of org.freedesktop.secrets using pass"; + homepage = "https://github.com/grimsteel/pass-secret-service"; + license = lib.licenses.gpl3Only; + platforms = lib.platforms.linux; + mainProgram = "pass-secret-service"; + }; + }; + arkenfox-userjs-profile = pkgs.runCommand "arkenfox-userjs-profile-${pkgs.arkenfox-userjs.version}" { } '' + install -Dm644 ${pkgs.arkenfox-userjs}/user.js $out/share/arkenfox-userjs/user.js + install -Dm644 ${pkgs.arkenfox-userjs}/user.cfg $out/share/arkenfox-userjs/user.cfg + ''; in { imports = [ ./common.nix ]; @@ -50,6 +81,11 @@ in home.username = builtins.getEnv "USER"; home.homeDirectory = builtins.getEnv "HOME"; + # Keep Nix's compiler out of PATH, but make it available to host Neovim for + # nvim-treesitter parser builds. The Nix-provided Neovim loads these parser + # .so files, so using the Nix compiler wrapper is the coherent ABI choice. + home.sessionVariables.NVIM_TREESITTER_CC = "${pkgs.stdenv.cc}/bin/cc"; + home.packages = with pkgs; [ # ── Thunderbird helpers ─────────────────────────────────────────────────── # external-editor-revived is the native-messaging host that lets the @@ -71,6 +107,13 @@ in # `protonmail-bridge-core`. protonmail-bridge + # ── Secrets portal ──────────────────────────────────────────────────────── + # Grimsteel's Rust org.freedesktop.secrets provider backed by pass. This is + # not nixpkgs' Python `pass-secret-service`; the repo-owned user unit at + # dot_config/systemd/user/pass-secret-service.service uses the Rust binary + # name and the PASSWORD_STORE_DIR drop-in. + pass-secret-service-rust + # ── Wayland session: bars, launchers, notifiers, daemons ────────────────── # Pure user-session GUIs/daemons — no system unit, no D-Bus activation # file under /usr/share/dbus-1, no login-manager session entry. The @@ -136,13 +179,15 @@ in # testing. sparrow - # chezmoi & paru — both are pure user CLIs. `paru` wraps pacman+makepkg - # but doesn't link them; it just shells out. bootstrap.sh installs a - # one-shot pacman `chezmoi` for the very first `chezmoi init --apply`, - # then `paru -Rns chezmoi paru` after the first nix-switch drops the - # pacman copies (the nix-profile copies on PATH take over). + # ── Browser hardening ──────────────────────────────────────────────────── + # Upstream Arkenfox user.js from nixpkgs, re-exposed under share/ so the + # chezmoi Firefox/LibreWolf deploy hook can render it with + # 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 - paru # ── OCR ────────────────────────────────────────────────────────────────── # Override merges eng + por language data into a single derivation, diff --git a/run_onchange_after_deploy-etc.sh.tmpl b/run_onchange_after_deploy-etc.sh.tmpl index ba79130..5f97c70 100755 --- a/run_onchange_after_deploy-etc.sh.tmpl +++ b/run_onchange_after_deploy-etc.sh.tmpl @@ -27,6 +27,12 @@ find etc -type f ! -name .ignore | while IFS= read -r src; do esac done +if [ -d etc/udev/rules.d ]; then + sudo udevadm control --reload + sudo udevadm trigger --subsystem-match=usb + sudo udevadm trigger --subsystem-match=hidraw +fi + # sudo-rs: /etc/pam.d/sudo-i is a symlink to /etc/pam.d/sudo sudo ln -sfT sudo /etc/pam.d/sudo-i @@ -46,10 +52,15 @@ sudo systemctl daemon-reload # (e.g. HandlePowerKey overrides) take effect without dropping sessions. sudo systemctl kill -s HUP systemd-logind -# Make sudo-rs the system-wide sudo via /usr/local/bin precedence. -# Targets may not exist yet on first bootstrap (sudo-rs is installed by -# the subsequent pkg-apply step); the symlinks resolve once it lands. -sudo ln -sfT /usr/bin/sudo-rs /usr/local/bin/sudo -sudo ln -sfT /usr/bin/sudo-rs /usr/local/bin/sudoedit -sudo ln -sfT /usr/bin/su-rs /usr/local/bin/su -sudo ln -sfT /usr/bin/visudo-rs /usr/local/bin/visudo +# Old sudo-rs migration used /usr/local/bin to shadow classic sudo globally. +# Current policy keeps those shims user-scoped via ~/.local/bin/symlink_*. +for link in /usr/local/bin/sudoedit /usr/local/bin/su /usr/local/bin/visudo; do + target=$(readlink "$link" 2>/dev/null || true) + case "$target" in + /usr/bin/sudo-rs|/usr/bin/su-rs|/usr/bin/visudo-rs) sudo rm -f "$link" ;; + esac +done +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 diff --git a/run_onchange_after_deploy-firefox.sh.tmpl b/run_onchange_after_deploy-firefox.sh.tmpl index eaecb68..c9e49be 100755 --- a/run_onchange_after_deploy-firefox.sh.tmpl +++ b/run_onchange_after_deploy-firefox.sh.tmpl @@ -1,10 +1,13 @@ #!/usr/bin/env dash -# Deploy Firefox/LibreWolf hardening overrides and custom CSS. -# chezmoi re-runs this script whenever any file under firefox/ changes. +# Deploy Firefox/LibreWolf hardening and custom CSS. +# chezmoi re-runs this script whenever any file under firefox/ or the nix +# inputs that provide arkenfox-userjs change. # firefox/ content hash: {{ output "sh" "-c" (printf "cd %q && find firefox -type f -exec sha256sum {} + | LC_ALL=C sort" .chezmoi.sourceDir) | sha256sum }} +# arkenfox nix hash: {{ output "sh" "-c" (printf "cd %q && sha256sum nix/flake.lock nix/host.nix 2>/dev/null || true" .chezmoi.sourceDir) | sha256sum }} set -eu PROFILES_DIR="$HOME/.var/app/io.gitlab.librewolf-community/.librewolf" +ARKENFOX_USER_JS="$HOME/.nix-profile/share/arkenfox-userjs/user.js" [ -d "$PROFILES_DIR" ] || exit 0 PROFILE=$(find "$PROFILES_DIR" -maxdepth 1 -mindepth 1 -type d -name '*.default-default' | head -1) @@ -14,7 +17,21 @@ fi [ -n "$PROFILE" ] || exit 0 cd "$CHEZMOI_SOURCE_DIR" + +if [ -r "$ARKENFOX_USER_JS" ]; then + tmp=$(mktemp) + cat "$ARKENFOX_USER_JS" >"$tmp" + printf '\n/*** user-overrides.js ***/\n\n' >>"$tmp" + cat firefox/user-overrides.js >>"$tmp" + cp --remove-destination "$tmp" "$PROFILE/user.js" + rm -f "$tmp" + rm -f "$PROFILE/user-overrides.js" +else + echo "deploy-firefox: missing $ARKENFOX_USER_JS; run 'just nix-switch' before regenerating user.js." >&2 +fi + find firefox -type f | while IFS= read -r src; do + [ "$src" = "firefox/user-overrides.js" ] && continue dest="$PROFILE/${src#firefox/}" mkdir -p "$(dirname "$dest")" cp --remove-destination "$src" "$dest" -- cgit v1.3.1