Last active
September 19, 2025 20:14
-
-
Save DrSensor/b765f9cef67dadb3c6cb9eaa53df7833 to your computer and use it in GitHub Desktop.
pacman profile manager
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # pacprofile - Profile-aware wrapper for Arch package managers | |
| # Supports multiple active profiles + pacman hook integration | |
| # Default backend is pacman, override with PACMGR env var | |
| PACMGR="${PACMGR:-pacman}" | |
| BASE_DIR="/etc/pacprofile" | |
| BASE_CONF="$BASE_DIR/pacman.conf" | |
| ACTIVE_DIR="$BASE_DIR/active" | |
| usage() { | |
| cat <<EOF | |
| Usage: | |
| pacprofile upgrade [<profile...>|--system] # Upgrade active or given profiles | |
| pacprofile list # List all profiles | |
| pacprofile show <profile> # Show pkgs in profile | |
| pacprofile set <profile...> # Set active profiles | |
| pacprofile add <profile...> # Add to active profiles | |
| pacprofile del <profile...> # Remove from active profiles | |
| pacprofile current # Show active profiles | |
| pacprofile install <profile> <pkg...> # Install into profile | |
| pacprofile remove <profile> <pkg...> # Remove from profile | |
| pacprofile status <profile> # Show status of profile | |
| EOF | |
| exit 1 | |
| } | |
| profile_dir() { echo "$BASE_DIR/$1"; } | |
| profile_pkgs() { echo "$(profile_dir "$1")/pkgs.txt"; } | |
| profile_conf() { | |
| local dir=$(profile_dir "$1") | |
| [[ -f "$dir/pacman.conf" ]] && echo "$dir/pacman.conf" || echo "$BASE_CONF" | |
| } | |
| require_profile() { | |
| local profile="$1" | |
| [[ -f "$(profile_pkgs "$profile")" ]] || { | |
| echo "Profile '$profile' not found in $BASE_DIR" | |
| exit 1 | |
| } | |
| } | |
| all_profiles_union() { | |
| find "$BASE_DIR" -mindepth 2 -maxdepth 2 -name "pkgs.txt" -exec cat {} + | sort -u | |
| } | |
| ignore_except() { | |
| local allow_file="$1" | |
| local installed=$(pacman -Qq | sort) | |
| local allowed=$(sort "$allow_file") | |
| comm -23 <(echo "$installed") <(echo "$allowed") | |
| } | |
| ignore_all_profiles() { | |
| local installed=$(pacman -Qq | sort) | |
| local all=$(all_profiles_union) | |
| comm -23 <(echo "$installed") <(echo "$all") | |
| } | |
| active_profiles() { | |
| [[ -d "$ACTIVE_DIR" ]] || return 0 | |
| find "$ACTIVE_DIR" -mindepth 1 -maxdepth 1 -type l -printf "%f\n" | sort | |
| } | |
| set_active_profiles() { | |
| sudo rm -rf "$ACTIVE_DIR" | |
| sudo mkdir -p "$ACTIVE_DIR" | |
| for p in "$@"; do | |
| require_profile "$p" | |
| sudo ln -s "../$p" "$ACTIVE_DIR/$p" | |
| done | |
| } | |
| add_active_profiles() { | |
| sudo mkdir -p "$ACTIVE_DIR" | |
| for p in "$@"; do | |
| require_profile "$p" | |
| sudo ln -sfn "../$p" "$ACTIVE_DIR/$p" | |
| done | |
| } | |
| del_active_profiles() { | |
| for p in "$@"; do | |
| sudo rm -f "$ACTIVE_DIR/$p" | |
| done | |
| } | |
| run_upgrade() { | |
| local conf="$1" | |
| local ignore="$2" | |
| if [[ -n "$ignore" ]]; then | |
| sudo $PACMGR --config "$conf" -Syu --ignore "${ignore%,}" | |
| else | |
| sudo $PACMGR --config "$conf" -Syu | |
| fi | |
| } | |
| case "$1" in | |
| list) | |
| find "$BASE_DIR" -mindepth 1 -maxdepth 1 -type d -printf "%f\n" | |
| ;; | |
| show) | |
| [[ -z "$2" ]] && usage | |
| require_profile "$2" | |
| cat "$(profile_pkgs "$2")" | |
| ;; | |
| set) | |
| [[ $# -lt 2 ]] && usage | |
| shift | |
| set_active_profiles "$@" | |
| echo "Active profiles set to: $*" | |
| ;; | |
| add) | |
| [[ $# -lt 2 ]] && usage | |
| shift | |
| add_active_profiles "$@" | |
| echo "Added active profiles: $*" | |
| ;; | |
| del) | |
| [[ $# -lt 2 ]] && usage | |
| shift | |
| del_active_profiles "$@" | |
| echo "Removed active profiles: $*" | |
| ;; | |
| current) | |
| active=$(active_profiles) | |
| if [[ -z "$active" ]]; then | |
| echo "No active profiles." | |
| else | |
| echo "Active profiles: $active" | |
| fi | |
| ;; | |
| install) | |
| [[ $# -lt 3 ]] && usage | |
| profile="$2"; shift 2 | |
| require_profile "$profile" | |
| sudo $PACMGR -S "$@" | |
| for pkg in "$@"; do | |
| grep -qxF "$pkg" "$(profile_pkgs "$profile")" || echo "$pkg" | sudo tee -a "$(profile_pkgs "$profile")" >/dev/null | |
| done | |
| ;; | |
| remove) | |
| [[ $# -lt 3 ]] && usage | |
| profile="$2"; shift 2 | |
| require_profile "$profile" | |
| sudo $PACMGR -Rns "$@" | |
| for pkg in "$@"; do | |
| sudo sed -i "/^$pkg$/d" "$(profile_pkgs "$profile")" | |
| done | |
| ;; | |
| upgrade) | |
| shift | |
| if [[ "$1" == "--system" ]]; then | |
| IGNORE=$(ignore_all_profiles | tr '\n' ',') | |
| run_upgrade "$BASE_CONF" "$IGNORE" | |
| else | |
| profiles=("$@") | |
| if [[ ${#profiles[@]} -eq 0 ]]; then | |
| profiles=($(active_profiles)) | |
| fi | |
| [[ ${#profiles[@]} -eq 0 ]] && { echo "No profiles specified and no active profiles set."; exit 1; } | |
| for profile in "${profiles[@]}"; do | |
| require_profile "$profile" | |
| CONF=$(profile_conf "$profile") | |
| IGNORE=$(ignore_except "$(profile_pkgs "$profile")" | tr '\n' ',') | |
| missing=$(comm -23 <(sort "$(profile_pkgs "$profile")") <(pacman -Qq | sort)) | |
| if [[ -n "$missing" ]]; then | |
| echo "Installing missing packages for '$profile': $missing" | |
| sudo $PACMGR --config "$CONF" -S $missing | |
| fi | |
| echo "Upgrading profile '$profile'..." | |
| run_upgrade "$CONF" "$IGNORE" | |
| done | |
| fi | |
| ;; | |
| status) | |
| [[ -z "$2" ]] && usage | |
| profile="$2" | |
| require_profile "$profile" | |
| installed=$(pacman -Qq | sort) | |
| tracked=$(sort "$(profile_pkgs "$profile")") | |
| missing=$(comm -23 <(echo "$tracked") <(echo "$installed")) | |
| present=$(comm -12 <(echo "$tracked") <(echo "$installed")) | |
| echo "==> Status for profile: $profile" | |
| echo | |
| echo "[Tracked packages]:" | |
| echo "$tracked" | sed 's/^/ /' | |
| echo | |
| echo "[Installed packages in profile]:" | |
| echo "$present" | sed 's/^/ /' | |
| echo | |
| echo "[Missing packages from system]:" | |
| if [[ -n "$missing" ]]; then | |
| echo "$missing" | sed 's/^/ /' | |
| else | |
| echo " (none)" | |
| fi | |
| echo | |
| # Extras: only if user want global check | |
| all_profiles=$(all_profiles_union) | |
| extras=$(comm -23 <(echo "$installed") <(echo "$all_profiles")) | |
| echo "[Extra installed packages (not in any profile)]:" | |
| if [[ -n "$extras" ]]; then | |
| echo "$extras" | sed 's/^/ /' | |
| else | |
| echo " (none)" | |
| fi | |
| ;; | |
| # --- Pacman hooks --- | |
| hook-install) | |
| pkg="$2" | |
| for p in $(active_profiles); do | |
| require_profile "$p" | |
| grep -qxF "$pkg" "$(profile_pkgs "$p")" || echo "$pkg" | sudo tee -a "$(profile_pkgs "$p")" >/dev/null | |
| done | |
| ;; | |
| hook-remove) | |
| pkg="$2" | |
| for dir in $(find "$BASE_DIR" -mindepth 1 -maxdepth 1 -type d); do | |
| sudo sed -i "/^$pkg$/d" "$dir/pkgs.txt" 2>/dev/null || true | |
| done | |
| ;; | |
| *) | |
| usage | |
| ;; | |
| esac |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| pkgname=pacprofile | |
| pkgver=1.0.0 | |
| pkgrel=1 | |
| pkgdesc="pacman profile manager" | |
| arch=('any') | |
| url="https://gist.github.com/DrSensor/b765f9cef67dadb3c6cb9eaa53df7833" | |
| license=('MIT') | |
| depends=('pacman' 'bash') | |
| backup=('etc/pacprofile/pacman.conf') | |
| source=("$pkgname.sh") | |
| sha256sums=('SKIP') | |
| package() { | |
| install -Dm755 "$srcdir/$pkgname.sh" "$pkgdir/usr/local/bin/$pkgname" | |
| # config dir | |
| install -d "$pkgdir/etc/pacprofile" | |
| install -d "$pkgdir/etc/pacprofile/work" | |
| install -d "$pkgdir/etc/pacprofile/gaming" | |
| install -d "$pkgdir/etc/pacprofile/server" | |
| # copy system pacman.conf as base | |
| if [[ -f /etc/pacman.conf ]]; then | |
| install -m644 /etc/pacman.conf "$pkgdir/etc/pacprofile/pacman.conf" | |
| fi | |
| # create empty pkgs.txt for profiles | |
| touch "$pkgdir/etc/pacprofile/work/pkgs.txt" | |
| touch "$pkgdir/etc/pacprofile/gaming/pkgs.txt" | |
| touch "$pkgdir/etc/pacprofile/server/pkgs.txt" | |
| # pacman hooks | |
| install -d "$pkgdir/etc/pacman.d/hooks" | |
| cat >"$pkgdir/etc/pacman.d/hooks/pacprofile-install.hook" <<'EOF' | |
| [Trigger] | |
| Operation = Install | |
| Type = Package | |
| Target = * | |
| [Action] | |
| Description = Update pacprofile pkgs.txt on install | |
| When = PostTransaction | |
| Exec = /usr/local/bin/pacprofile hook-install %n | |
| EOF | |
| cat >"$pkgdir/etc/pacman.d/hooks/pacprofile-remove.hook" <<'EOF' | |
| [Trigger] | |
| Operation = Remove | |
| Type = Package | |
| Target = * | |
| [Action] | |
| Description = Update pacprofile pkgs.txt on remove | |
| When = PostTransaction | |
| Exec = /usr/local/bin/pacprofile hook-remove %n | |
| EOF | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment