Skip to content

Instantly share code, notes, and snippets.

@DrSensor
Last active September 19, 2025 20:14
Show Gist options
  • Select an option

  • Save DrSensor/b765f9cef67dadb3c6cb9eaa53df7833 to your computer and use it in GitHub Desktop.

Select an option

Save DrSensor/b765f9cef67dadb3c6cb9eaa53df7833 to your computer and use it in GitHub Desktop.

Revisions

  1. DrSensor revised this gist Sep 19, 2025. 2 changed files with 41 additions and 1 deletion.
    2 changes: 1 addition & 1 deletion PKGBUILD
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,7 @@ pkgver=1.0.0
    pkgrel=1
    pkgdesc="pacman profile manager"
    arch=('any')
    # url="https://gist.github.com/drsensor/pacprofile"
    url="https://gist.github.com/DrSensor/b765f9cef67dadb3c6cb9eaa53df7833"
    license=('MIT')
    depends=('pacman' 'bash')
    backup=('etc/pacprofile/pacman.conf')
    40 changes: 40 additions & 0 deletions pacprofile.sh
    Original file line number Diff line number Diff line change
    @@ -20,6 +20,7 @@ Usage:
    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
    }
    @@ -170,12 +171,51 @@ case "$1" in
    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"
  2. DrSensor created this gist Sep 19, 2025.
    58 changes: 58 additions & 0 deletions PKGBUILD
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,58 @@
    pkgname=pacprofile
    pkgver=1.0.0
    pkgrel=1
    pkgdesc="pacman profile manager"
    arch=('any')
    # url="https://gist.github.com/drsensor/pacprofile"
    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
    }
    198 changes: 198 additions & 0 deletions pacprofile.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,198 @@
    #!/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
    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' ',')
    echo "Upgrading profile '$profile'..."
    run_upgrade "$CONF" "$IGNORE"
    done
    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