#!/usr/bin/env python3 """Generate disjoint, concern-scoped package lists from the current machine.""" from __future__ import annotations import subprocess from pathlib import Path from typing import Final ROOT = Path(__file__).resolve().parents[1] PACKAGES = ROOT / "packages" # A concern is reusable; a machine profile composes concerns elsewhere. # Every installed explicit package must be assigned exactly once on this laptop. CONCERNS: Final = { "common/core": """base base-devel linux linux-firmware linux-headers cryptsetup refind arch-install-scripts git yadm openssh man-db jq ripgrep fd fzf 7zip unzip rsync ntfs-3g os-prober fwupd fish zsh zoxide htop glances starship cowsay ex-vi-compat mtools dmidecode libsmbios catdoc odt2txt hunspell-de""", "common/wayland": """hyprland hypridle hyprlock xdg-desktop-portal-hyprland pipewire-alsa pipewire-pulse wireplumber pavucontrol pamixer mako waybar wofi grim slurp satty wl-clipboard wlr-randr alacritty kitty tmux neovim vim yazi ueberzugpp flameshot playerctl bluez bluez-utils blueman gnome-keyring xorg-xwayland noctalia-git ranger-git""", "common/fonts": """noto-fonts noto-fonts-cjk noto-fonts-emoji ttf-jetbrains-mono-nerd ttf-icomoon-feather ttf-meslo-nerd-font-powerlevel10k""", "common/development": """nodejs npm yarn python python-pip pyenv uv rust cargo go ruby jdk-openjdk docker docker-compose podman vagrant virtualbox virtualbox-host-dkms cloudflared openbsd-netcat pycharm postman-bin mongodb-compass-bin pi engram-bin decman yay gti fcron""", "common/productivity": """brave-bin firefox chromium thunderbird obsidian bitwarden nextcloud-client signal-desktop telegram-desktop libreoffice-fresh evince okular nomacs zotero-bin qalculate-gtk qbittorrent teams teamspeak3 cerebro-bin chatgtk torbrowser-launcher spotify""", "common/media": """vlc mpv smplayer audacity imagemagick steam cudnn""", "common/research": """biber r texlive-basic texlive-bibtexextra texlive-binextra texlive-context texlive-fontsextra texlive-fontsrecommended texlive-fontutils texlive-formatsextra texlive-games texlive-humanities texlive-latex texlive-latexextra texlive-latexrecommended texlive-luatex texlive-mathscience texlive-metapost texlive-music texlive-pictures texlive-plaingeneric texlive-pstricks texlive-publishers texlive-xetex resvg""", "common/network": """networkmanager network-manager-applet avahi nss-mdns dhcpcd gvfs-smb cups cups-pdf mullvad-vpn-beta-bin""", "laptop/hardware": """intel-ucode thermald tlp tlp-pd tlp-rdw powertop psensor lm_sensors ddcutil intel-gpu-tools intel-media-driver mesa-utils vulkan-intel vulkan-tools nvidia-open nvidia-prime nvtop envycontrol light keyd nwg-displays wireless_tools caffeine woeusb""", "common/services": """sddm ly pcmanfm libva-utils""", } def explicit(command: str) -> set[str]: return set( subprocess.run( ["pacman", command], check=True, text=True, stdout=subprocess.PIPE ).stdout.split() ) def write(path: Path, values: set[str]) -> None: resolved = path.resolve() package_root = PACKAGES.resolve() if package_root not in resolved.parents: raise ValueError(f"refusing to write outside packages: {path}") resolved.parent.mkdir(parents=True, exist_ok=True) resolved.write_text("\n".join(sorted(values)) + "\n", encoding="utf-8") def main() -> None: official, aur = explicit("-Qqen"), explicit("-Qqem") assigned: set[str] = set() for name, raw in CONCERNS.items(): selected = set(raw.split()) & (official | aur) overlap = selected & assigned if overlap: raise SystemExit(f"duplicate ownership in {name}: {sorted(overlap)}") assigned |= selected write(PACKAGES / f"{name}-repo.txt", selected & official) write(PACKAGES / f"{name}-aur.txt", selected & aur) remaining = (official | aur) - assigned if remaining: raise SystemExit( "unclassified explicit packages; add them to a named concern: " + ", ".join(sorted(remaining)) ) print(f"assigned={len(assigned)}") if __name__ == "__main__": main()