Nimbin2git.christianimmanuel.de / Linux From Scratch / Packagemanager-LFS-PackageUser-System / packagemanager_install

Packagemanager-LFS-PackageUser-System git · main

git clone https://git.christianimmanuel.de/linux-from-scratch/Packagemanager-LFS-PackageUser-System.gitwget https://git.christianimmanuel.de/linux-from-scratch/Packagemanager-LFS-PackageUser-System/archive/Packagemanager-LFS-PackageUser-System.tar.gz
packagemanager_install 32.7 KB · 858 lines raw
#!/bin/bash
#
# packagemanager_install -- privileged install engine for the pkgusr model.
# Copyright (C) 2025  packagemanager contributors
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.  This program is distributed WITHOUT ANY WARRANTY; see the GNU
# General Public License <https://www.gnu.org/licenses/> for details.
#
#
# the package_short_name is the name for the created user
# packagemanager_install package_short_name path_to_install_file
#

PACKAGEMANAGER_INSTALL_VERSION="1.11.6"

# A fingerprint of this file's own contents, the same as the other four tools.
#
# Without it `lfs-sanity.sh` section 0 -- "host and chroot copies must MATCH" --
# could not read this one at all, and printed the usage error instead:
#     packagemanager_install Please provide an install script with $2
# A tool you cannot ask "which build are you?" is the one that quietly goes
# stale, and a stale copy in the chroot has cost more debugging time in this
# project than anything else.
_pmi_build_id() {
    local f="${BASH_SOURCE[0]:-$0}"
    if [ -r "$f" ] && command -v md5sum >/dev/null 2>&1; then
        md5sum "$f" 2>/dev/null | cut -c1-7
    else
        echo unknown
    fi
}

case "${1:-}" in
    --version|-V)
        echo "packagemanager_install $PACKAGEMANAGER_INSTALL_VERSION (build $(_pmi_build_id))"
        exit 0 ;;
    -h|--help)
        echo "usage: packagemanager_install <package> <install-script>"
        echo "       packagemanager_install --version"
        exit 0 ;;
esac




# Where an account's home is.  Accounts are grouped by kind under /usr/src, so
# a directory listing says what a thing IS:
#     /usr/src/pkgusr/p_gcc            a package
#     /usr/src/cfg/cfg_bootscripts     a config step that installs files
#
# Prefixing is IDEMPOTENT, so this can be handed a package name ("gcc") or an
# account name ("p_gcc") and gives the same answer either way.  Building the
# path by hand instead is what once made the build cd into
# /usr/src/pkgusr/man-pages while the account home was p_man-pages.
PKGUSR_ROOT="${LFS_PKGUSR_ROOT:-/usr/src/pkgusr}"
CFGUSR_ROOT="${LFS_CFGUSR_ROOT:-/usr/src/cfg}"
PKGUSR_PREFIX="${LFS_PKGUSR_PREFIX-p_}"

pkgusr_home_for() {
    local n="$1" bare
    case "$n" in
        "") printf '%s' "$PKGUSR_ROOT"; return ;;
    esac
    # ASK, if there is anything to ask.  lfs-helper owns this rule and the
    # layout it depends on is configurable; a second copy of it here is right
    # only until someone changes the layout.  The rest of this function is the
    # fallback for a system with no lfs-helper.
    if command -v lfs-helper >/dev/null 2>&1; then
        local _h
        _h="$(lfs-helper pkgusr-home "$n" 2>/dev/null)"
        case "$_h" in /*) printf '%s' "$_h"; return ;; esac
    fi
    if [ -n "$PKGUSR_PREFIX" ]; then
        case "$n" in "$PKGUSR_PREFIX"*) ;; *) n="${PKGUSR_PREFIX}${n}" ;; esac
    fi
    bare="${n#"$PKGUSR_PREFIX"}"
    case "$bare" in
        cfg_*) printf '%s/%s' "$CFGUSR_ROOT" "$n" ;;
        *)     printf '%s/%s' "$PKGUSR_ROOT" "$n" ;;
    esac
}

skel=/etc/pkgusr/skel-package
user_name=""
install_script=""
install_script_basename=""
install_user_dir="$(pkgusr_home_for "$user_name")"
sourced=false
date_log="$(date +%Y-%m-%d_%H-%m-%S)"
home_user="n76310"
home_user_dir="/home/${home_user}"

# --------------------------------------------------------------------------- #
# Command wrappers for the build
# --------------------------------------------------------------------------- #
# Packages routinely try to (re)create and re-mode directories that already
# exist and belong to someone else:
#     install -vdm755 /usr/sbin
#     install: cannot change permissions of '/usr/sbin': Operation not permitted
# The directory is already there with the right mode; the package just insists
# on saying so.  Under the package-user model that is a hard error, and it
# stops installs that would otherwise be completely fine.
#
# So put small wrappers ahead of the real tools for the duration of the build:
#   install -d  on an existing directory  -> succeed, change nothing
#   chmod       on a directory we do not own -> skip, keep going
#   chown/chgrp on anything                  -> skip (a package user may not)
# Anything else is passed straight through.
#
# ONE SET OF WRAPPERS, NOT TWO.  lfs-helper writes these to /usr/lib/pkgusr,
# where they outlive the build and where the package-user profile already
# points; this script wrote its OWN copy to a fresh /tmp directory on every
# run.  Two implementations of "what may a package user do", free to disagree
# about it -- the species of bug that costs this project the most.  Ask for
# them when lfs-helper is on the system, which it is on anything these tools
# built; the copy below is the fallback for a system without it.
_wrapper_dir_from_lfs_helper() {
    command -v lfs-helper >/dev/null 2>&1 || return 1
    local d
    d="$(lfs-helper wrapper-dir 2>/dev/null)" || return 1
    [ -n "$d" ] || return 1
    # Missing or incomplete -- write them.  Idempotent, so this also repairs a
    # tree whose wrappers predate a fix to them.
    if [ ! -x "$d/install" ] || [ ! -x "$d/chown" ]; then
        lfs-helper make-wrappers --run >/dev/null 2>&1 || return 1
    fi
    [ -x "$d/install" ] || return 1
    printf '%s' "$d"
}

# The temporary copy: only for a system with no lfs-helper.
_make_build_wrappers() {
    local d="$1"
    mkdir -p "$d" || return 1

    cat > "$d/install" <<'WRAP'
#!/bin/bash
# only -d (create directories) is special; everything else is the real thing
_dirmode=0
for a in "$@"; do
    case "$a" in
        -d|--directory) _dirmode=1 ;;
        -*d*) case "$a" in -*[!-]*) _dirmode=1 ;; esac ;;
    esac
done
if [ "$_dirmode" = 1 ]; then
    _rc=0
    for a in "$@"; do
        case "$a" in -*) continue ;; esac
        if [ -d "$a" ]; then
            continue                 # already there: nothing to do
        fi
        /usr/bin/install -d "$a" || _rc=$?
    done
    exit $_rc
fi
exec /usr/bin/install "$@"
WRAP

    cat > "$d/chmod" <<'WRAP'
#!/bin/bash
# a package user may only re-mode what it owns
_me="$(id -un)"
_rc=0
for a in "$@"; do
    case "$a" in -*) continue ;; esac
    [ -e "$a" ] || continue
    if [ "$(stat -c %U "$a" 2>/dev/null)" != "$_me" ]; then
        echo "*** chmod $a (skipped: owned by $(stat -c %U "$a" 2>/dev/null))" >&2
        continue
    fi
    /usr/bin/chmod "$@" || _rc=$?
    exit $_rc
done
exit 0
WRAP

    for t in chown chgrp; do
        cat > "$d/$t" <<WRAP
#!/bin/bash
echo "*** $t \$* (skipped: package users cannot change ownership)" >&2
exit 0
WRAP
    done

    chmod 755 "$d"/install "$d"/chmod "$d"/chown "$d"/chgrp
}

echoRed() {
    echo -e "\033[0;31m$1\033[0m"
}

echoGreen() {
    echo -e "\033[0;32m$1\033[0m"
}

echoOrange() {
    echo -e "\033[0;33m$1\033[0m"
}

echoBlue() {
    echo -e "\033[0;34m$1\033[0m"
}

addUser() {
    if [ $# -eq 1 ]; then
        set -- "$1" "$1" "$1"
    fi
    if [ $UID -ne 0 ]; then echo Please run this script as root. ; exit 1; fi
    add_package_user "${1}" $2 10000 20000 $3 10000 20000 || exit  1
    # Keep /etc/passwd and /etc/group in id order.  They are appended to, so
    # without this they drift into install order and someone has to tidy them
    # up later -- which only happens if they remember.  lfs-helper owns the
    # sorting so both halves of the toolchain do it the same way.
    command -v lfs-helper >/dev/null 2>&1 \
        && lfs-helper sort-users --run >/dev/null 2>&1 || true
}

addCollectorGroup() {
    group_name="$1"
    if ! echo "$group_name" | grep -qw "audio"; then
        if ! echo "$group_name" | grep -q "^u_"; then
            echo "$group_name" | grep -q "^nimgnu_" ||
                group_name="nimgnu_$1"
        fi
    fi

    echoBlue "\n\nCreate Collector group $group_name"
    if awk --field-separator=":" '{print $1}' /etc/group | grep $group_name && 
            awk --field-separator=":" '{print $1}' /etc/passwd | grep $group_name; then
        echoGreen "\nGroup and user exist";
        return
    fi
    echo "$group_name" | grep -w "nimgnu_" && 
        echoRed "\ngroup_name missing" &&
        return

    echo "addUser "$group_name""
    echo -n "[y/N]: "
    read -e -r check
    [ "$check" != "y" ] && exit
    addUser "$group_name"
    echo "Created whyle installing $user_name" > info.txt
    echo "Check groups by running group command" >> info.txt
}

addUerToCollectorGroup() {
    group_name="$1"
    if ! echo "$group_name" | grep -qw "audio"; then
        if ! echo "$group_name" | grep -q "^u_"; then
            if ! echo "$group_name" | grep -q "^nimgnu_"; then
                group_name="nimgnu_$1"
            fi
        fi
    fi

    echoBlue "\n\nAdding user $username to group $group_name"
    if ! echo "$group_name" | grep -q "^u_"; then
        echo "$group_name" | grep -qw "nimgnu_" && 
            echoRed "group_name missing" &&
            return
    fi
    [ "$user_name" == "" ] && echoRed "user_name missing" && exit 
    checkIfUserInGroup $user_name $group_name &&
        echoGreen "$user_name is already a part of $group_name group." &&
        return

    usermod -a -G $group_name $user_name

    checkIfUserInGroup $user_name $group_name &&
        echoGreen "$user_name is now a part of $group_name group." &&
        return
}

checkIfUserInGroup() {
    username="$1"
    groupname="$2"
    cat /etc/group | grep  "^$groupname:" | sed "s/.*://g;s/,/\n/g" | grep -w "^$username$"
}

checkGroups() {
    ! grep -q "^install_groups=(.*)" $install_script &&
        return
    ! $sourced && eval "$(grep --color=never "^install_groups=(.*)" $install_script)"
    echoOrange "\n\nThis user may need to be added to collector groups:"

    for group in "${install_groups[@]}"; do
        if [ "$group" == "python-dependencies" ]; then
            group="nimgnu_python-modules"
        fi
        # resolve to the collector-prefixed group if that's the one that exists
        if ! grep -q -E "^$group:" /etc/group && grep -q -E "^nimgnu_$group:" /etc/group; then
            group="nimgnu_$group"
        fi
        # already a member? nothing to do.
        if checkIfUserInGroup "$user_name" "$group" >/dev/null 2>&1; then
            echoGreen "$user_name is already in $group"
            continue
        fi
        grep -q -E "^$group:" /etc/group ||
            addCollectorGroup "$group"
        if [ "${PM_YES:-}" == "1" ]; then
            echoOrange " usermod -a -G $group $user_name  (--yes)"
            addUerToCollectorGroup "$group"
        else
            echoOrange "By adding to a group the user gets access to all files of the group"
            echo -e " usermod -a -G $group $user_name"
            echo -n "[y/N]: "
            read -e -r check
            [ "$check" == "y" ] && [ "$group $user_name" != " " ] &&
                addUerToCollectorGroup "$group"
        fi
    done
}

findAllGroupsOfDir() {
    dir="$1"
    for d in $(find $dir/* -maxdepth 0); do ls -ald "$d" | awk '{print $4}'; done | sort | uniq
}

addUserToGroup() {
    change_name="$1"
    change_group="$2"
    change_dir="$3"
    if [ "$3" == "" ]; then
        echo "ERROR"
        echo "change_name=$1"
        echo "change_group=$2"
        echo "change_dir=$3"
        exit
    fi
    checkIfUserInGroup $change_name $change_group &&
        return
    echo "$change_group to $change_name" | tee "$(pkgusr_home_for "$change_name")/log/install_nimgnu_group-$date_log.log"

    echoBlue "\n\n##########################\n"
    find $change_dir -group $change_name
    echoBlue "\n\n##########################\n"
    echo "$dir"
    echo -e " usermod -a -G $change_group $change_name"
    if [ "${PM_YES:-}" == "1" ]; then
        check="y"
    else
        echo -n "[y/N]: "
        read -e -r check
    fi

    [ "$check" == "y" ] && (
        usermod -a -G $change_group $change_name 
    )

}

installNimgnuGroup() {
    echo -e "\n\nInstall groups for nimgnu_group"
    echo "###${nimgnu_groups_dir[@]}##"
    
    for dir in "${nimgnu_groups_dir[@]}"; do
        old_group="$(ls -ald $dir | awk '{print $4}')"
        dir_groups=($(findAllGroupsOfDir $dir))
        echo "Found groups in $dir:"
        echo -e "${dir_groups[@]}\n"
        for g in ${dir_groups[@]}; do
            if [ "$g" == "$old_group" ]; then
                echo "nothing to change"
            else
                echo "addUserToGroup $g $user_name $dir"
                addUserToGroup $g $user_name $dir
            fi
        done
        for f in $dir/*; do
            echo "############################"
            echo "## Add user rights to group"
            ls -ald $f
            user_perms=$(stat -c "%A" $f | cut -c2-4)
            chmod g=$user_perms $f
            ls -ald $f
        done


        dirs_a=$(echo $dir)
        echo "## $dir" 
        su -c "mkdir -p $dir" $user_name
        if [ -d "$dir" ]; then
            ls -ald $dir
            parameter=""
            if [ "$nimgnu_groups_dir_recrusive" == "true" ]; then
                echo ""
                echo "change permission recrusive for $user_name on;"
                echo "$dir"
                echo -n "[y/N]: "
                read -e -r check

                [ "$check" == "y" ] && 
                    parameter="-R"
            fi
            (chgrp -v $parameter "$user_name" "$dir" &&
                chmod -v 775 $parameter "$dir" && ls -ald "$dir") ||
                echoRed "ERROR: Failed to do\nchgrp -v $user_name $dir &&\nchmod -v 775 $dir"
        else
            echo "$dir: is not a valid directory (installNimgnuGroup)"
        fi

        if ! [ "$old_group" == "" ] && ! [ "$user_name" == "install" ] && ! [ "$user_name" == "root" ] &&  
                ! [ "$old_group" == "root" ] && ! [ "$user_name" == "$old_group" ]; then
            addUserToGroup $user_name $old_group $dir
        fi
    done
}

installNimbinUser() {
    echo -e "\n\nInstall user for local package for the user (u_user)"
    linenumber="$(cat /etc/pam.d/su_u_user | wc -l)"
    if sed "${linenumber}q;d" /etc/pam.d/su_u_user | grep -q "success=1 default=ignore"; then
        t="success=ignore default=1"
    else
        t="success=1 default=ignore"
    fi

    echo "Make HOME 0750"
    chmod -R 0750 "$install_user_dir"

    grep -q "$user_name).*USER.*\<$user_name\>.*\<$home_user\>" /etc/pam.d/su_u_user ||
        sed -i "$((1+$(grep -n 'case "$PAM_USER" in' /etc/pam.d/su_u_user | sed "s/\:.*$//g")))i\ \ $user_name) USERS=\"$user_name $home_user\"; ;;" /etc/pam.d/su_u_user
    echo -e "\n\nModified pam.d/su"
    cat /etc/pam.d/su_u_user 

    if grep -q "^install_groups=(" $install_script; then
        eval "$(grep "^install_groups=(" $install_script)"
        checkGroups
        if echo "${install_groups[@]}" | grep -wq u_xdg_runtime; then
            rm -r $install_user_dir/.bash_profile
            ln -s /etc/pkgusr/skel-u_xdg/.bash_profile $install_user_dir/.bash_profile
            chmod 755 $install_user_dir/.bash_profile
            chown $user_name: $install_user_dir/.bash_profile
        fi 
    fi
    if grep -q "^shared=true" $install_script; then
        s_dir_nam="$(echo "$user_name" | sed "s/^u_//g")"
        mkdir -p "$install_user_dir/${s_dir_nam^}_shared"
        chmod g+sw "$install_user_dir/${s_dir_nam^}_shared"
        chmod 770 "$install_user_dir/${s_dir_nam^}_shared"
        chown -R $user_name: "$install_user_dir/${s_dir_nam^}_shared"
        ln -s "$install_user_dir/${s_dir_nam^}_shared" "$home_user_dir/Shared"
        echo "ln -s $install_user_dir/${s_dir_nam^}_shared $home_user_dir/Shared/"
        chown -R $home_user: $home_user_dir/Shared/${s_dir_nam^}_shared
    fi

    if grep -q "^app=" $install_script; then
        eval "$(grep "^app=" $install_script)"
        if which "$app"; then
            ! [ -f "$home_user_dir/bin/$app" ] &&
            cat > $home_user_dir/bin/$app << EOF
#!/bin/bash

su - u_$app -c "$app $(echo "${@}" | sed "s/\"/\\\"/g")"
EOF
            cat > $home_user_dir/.local/share/applications/$app.desktop << EOF
[Desktop Entry]
Name=${app^}
Exec=script -c "$home_user_dir/bin/$app" /dev/null
Terminal=false
Type=Application
Icon=${app}
StartupWMClass=${app^}
EOF
            chown ${home_user}: $home_user_dir/bin/$app
            chmod +x $home_user_dir/bin/$app
            chown ${home_user}: $home_user_dir/.local/share/applications/$app.desktop
            echoGreen "Creating:"
        else
            echoRed "!!! Error: $app not found !!!"
            echo "Not creating:"
        fi
        echo -e "\t$home_user_dir/bin/$app"
        echo -e "\t$home_user_dir/.local/share/applications/$app.desktop"
    fi

    usermod -a -G $user_name $home_user
    echo -e "\n\n"
}


installPkg() {
    user_name="$1"
    install_user_dir="$(pkgusr_home_for "$user_name")"
    install_dir=/etc/pkgusr/install_scripts
    install_script="$2"
    install_script_basename="$(basename $2)"
    install_log_dir=$install_user_dir/log
    mkdir -p $install_log_dir
    touch $install_log_dir/$install_$user_name-$date_log.diff

    [ -z "$install_script" ] && echo "Please provide an install script with \$2"
    [ -z "$install_script" ] && exit
    [ -f $install_script ] || echo "The install script \"$install_script\" doese not exist"
    [ -f $install_script ] || exit

    echo -e "\033[0;34m"
    echo "–––––––––––––––––––––––––––––––"
    echo -e "Install \033[0;32m$user_name\033[0;34m"
    echo "–––––––––––––––––––––––––––––––"
    echo
    echo -e "\033[0;31mFirst check the install file for:\n\033[0;33mlink, name, wrong commands like permission changes\ncheck if \"$user_name\" is the right username\n\033[0m"
    echo -e "\033[0;36minstall file: $install_script\033[0m"
    if [ "${PM_YES:-}" != "1" ]; then
        echo -e "\033[0;33mPress enter to continue\033[0m"
        read -n1
    fi
    echo


    ####################
    # source the install file
    if grep -q "^install_pkg() {" "$install_script"; then
        sourced=true
        source $install_script
    fi


    ####################
    # install user
    id "$user_name" ||
       addUser $user_name

    id "$user_name" ||
       echo "The user $user_name could not be created."
    

    ####################
    # check for install_groups
    checkGroups

    ####################
    # copy install script
    # special for old install scripts:
    if grep -q "^source installBaseScript" $install_script; then
        if ! [ -f $install_user_dir/installBaseScript ]; then
            ln -s /etc/pkgusr/installBaseScript /$install_user_dir/installBaseScript &&
            chown $user_name: $install_user_dir/installBaseScript
        fi
        grep -q "sudo " $install_script &&
            cat $install_script | sed "s/\<sudo\> //g" > $install_user_dir/install_$user_name
        vim $install_user_dir/install_$user_name
    else

    ####################
    # copy install script
        touch $install_user_dir/install_$user_name
        # record a diff vs the previous install_last (before we overwrite it)
        diff $install_user_dir/install_last $install_script >> $install_user_dir/log/install_$user_name-$date_log.diff 2>/dev/null
        # the canonical, editable script (edit THIS to keep changes across
        # re-installs) and the runtime copy the engine executes -> owned by the
        # package user.  Skip any copy where source and destination are the same
        # file (e.g. when re-installing from the already-in-home script).
        [ "$install_script" -ef "$install_user_dir/$install_script_basename" ] ||
            cp $install_script $install_user_dir/$install_script_basename
        [ "$install_script" -ef "$install_user_dir/install_$user_name" ] ||
            cp $install_script $install_user_dir/install_$user_name
        chown $user_name: "$install_user_dir/$install_script_basename" \
                          "$install_user_dir/install_$user_name" 2>/dev/null || true
        # install_last is only a read-only record of what was last installed.
        if [ "$install_script" -ef "$install_user_dir/install_last" ]; then
            :   # already the record; leave it
        else
            rm -f $install_user_dir/install_last 2>/dev/null
            cp $install_script $install_user_dir/install_last
            chmod 0444 $install_user_dir/install_last 2>/dev/null || true
        fi
    fi




    ####################
    # install package
    #
    # check if install_file installs a nimgnu_group
    parameter="install"
    if echo "$user_name" | grep "^nimgnu_"; then
        installNimgnuGroup
        parameter=""
    fi
    # check if install_file installs a u_user -> groups to run user applications like browser etc.
    if echo "$user_name" | grep "^u_"; then
        installNimbinUser
        parameter=""
    fi

    # New-style scripts (blfs-generated) are PHASED: the dispatcher takes
    # all|unpack|build|install|configure|update.  A full install runs "all";
    # packagemanager sets PM_MODE=update for updates.  nimgnu_/u_ users were
    # intentionally set to "" above; keep that, otherwise use the mode.
    if grep -q "^install_pkg() {" "$install_script"; then
        [ -n "$parameter" ] && parameter="${PM_MODE:-all}"
    fi

    # logs to $install_user_dir/packagemanager_install.log
    chown $user_name: $install_user_dir/install_*
    chown -R $user_name: $install_user_dir/log/
    # pipefail so tee doesn't hide a failing build/install; capture the real code
    _wrapdir="$(_wrapper_dir_from_lfs_helper)" && _wraptmp=0 || {
        _wrapdir="/tmp/pkgusr-wrappers.$$"; _wraptmp=1
        _make_build_wrappers "$_wrapdir" || _wrapdir=""
        chmod 755 "$_wrapdir" 2>/dev/null || true
    }
    su - -c "set -o pipefail; \
             ${_wrapdir:+PATH='$_wrapdir':\$PATH; export PATH; } \
             bash ~/install_$user_name $parameter 2>&1 | tee ~/log/packagemanager_install-$date_log.log" $user_name
    install_rc=${PIPESTATUS[0]}
    [ "${_wraptmp:-0}" = 1 ] && [ -n "${_wrapdir:-}" ] && rm -rf "$_wrapdir"

    if echo "$user_name" | grep "^u_"; then
        gpasswd -d $user_name install # remove install group if nimgnu_group
    fi

# --------------------------------------------------------------------------- #
# Automatic permission repair, the same way lfs-helper does it during the build.
#
# A package that installs into a directory another package owns fails with a
# permission error.  That is not a mistake to report -- it is the normal state
# of affairs under the package-user model, and the answer is always the same:
# put both packages in a collector group that owns the directory.  So do it,
# and retry, instead of stopping and asking the user to type it out.
# --------------------------------------------------------------------------- #

collector_prefix_of() {
    local conf="${PKGUSR_CONFIG:-/etc/pkgusr/packagemanager.conf}" p=""
    [ -f "$conf" ] && p="$(sed -n 's/.*"collector_prefix"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$conf" | head -n1)"
    [ -n "$p" ] || p="sysgroup_"
    case "$p" in *_) ;; *) p="${p}_" ;; esac      # always ends in one underscore
    printf '%s' "$p"
}

# Directories the log says the package could not write into.
#
# Take paths ONLY from lines that actually report a permission error.  A Python
# traceback is full of paths -- meson's own source files, the interpreter's
# stdlib -- and treating those as install targets meant granting a package
# write access to /usr/lib/python3.13.  The failing path is on the error line,
# never in the frames above it.
unwritable_dirs_from_log() {
    local log="$1" user="$2" p d
    [ -f "$log" ] || return 0
    grep -hE "Permission denied|Operation not permitted|Read-only file system" "$log" 2>/dev/null \
    | grep -vE "^[[:space:]]*(File|Traceback|[[:space:]]*[~^]+$)" \
    | { grep -hoE "'[^']+'|\"[^\"]+\"" || true; } | tr -d "'\"" \
    | while IFS= read -r p; do
          case "$p" in /*) ;; *) continue ;; esac
          if [ -d "$p" ]; then echo "$p"; else echo "${p%/*}"; fi
      done | sort -u \
    | while IFS= read -r d; do
          [ -n "$d" ] && [ -d "$d" ] || continue
          su -s /bin/bash "$user" -c "test -w '$d'" 2>/dev/null && continue
          echo "$d"
      done
}

# give <user> the right to install into <dir>
grant_dir_to_user() {
    local dir="$1" user="$2" prefix owner grp cur

    # ONE implementation of the collector-group rules, not two.
    #
    # lfs-helper owns this logic: join the group the directory already carries,
    # ask which group should own it when there is none, never create one for
    # root, never hand a package's tree to the install group.  When it is on
    # the system -- which it is, on anything these tools built -- use it, so a
    # package installed after boot behaves exactly like one built during the
    # chroot stage.  The code below is the fallback for a system without it.
    if command -v lfs-helper >/dev/null 2>&1; then
        lfs-helper grant-dir "$dir" "$user" --run
        return $?
    fi

    prefix="$(collector_prefix_of)"
    owner="$(stat -c %U "$dir" 2>/dev/null)" || return 1
    [ "$owner" = "$user" ] && return 1
    cur="$(stat -c %G "$dir" 2>/dev/null)"

    case "$cur" in
        "${prefix}"*)
            # the directory is ALREADY shared -- just join the existing group,
            # which is the whole point of having one
            grp="$cur"
            ;;
        install)
            grp="install"
            ;;
        *)
            # A ROOT-owned directory is never a package's: it is either shared
            # system infrastructure or something the distribution installed.
            # "<prefix>_root" would be a group meaning "may write anywhere root
            # owns", which is the whole scheme undone -- and there is no user
            # 'root' to add to a collector group in any case.
            if [ "$owner" = "root" ]; then
                echo "    $dir  (owned by root -- NOT shared;" >&2
                echo "      a collector group for root would defeat the scheme." >&2
                echo "      If this really is a shared install directory:" >&2
                echo "        chgrp install '$dir' && chmod g+w '$dir'" >&2
                return 1
            fi
            grp="${prefix}${owner}"
            # Collector groups live in their own id range (90000+), clear of
            # the package users at 10000+ -- otherwise a collector group takes
            # the gid the next package user's group wants and the uid/gid
            # pairing stops lining up.
            if ! getent group "$grp" >/dev/null 2>&1; then
                _cgid=90000
                while getent group "$_cgid" >/dev/null 2>&1; do
                    _cgid=$((_cgid + 1))
                done
                groupadd -g "$_cgid" "$grp" >/dev/null 2>&1 \
                    || groupadd "$grp" >/dev/null 2>&1
            fi
            getent group "$grp" >/dev/null 2>&1 || return 1
            usermod -a -G "$grp" "$owner" 2>/dev/null
            chgrp "$grp" "$dir" 2>/dev/null || return 1
            ;;
    esac

    usermod -a -G "$grp" "$user" 2>/dev/null
    chmod g+w "$dir" 2>/dev/null
    echo "    $dir -> group $grp (owner $owner, + $user)"
    return 0
}

# returns 0 if anything was granted
auto_grant_from_log() {
    local log="$1" user="$2" need d granted=1
    need="$(unwritable_dirs_from_log "$log" "$user")"
    [ -n "$need" ] || return 1
    echo -e "\033[0;33m"
    echo "# '$user' could not write into some directories -- granting access"
    echo "# via collector groups and retrying:"
    while IFS= read -r d; do
        [ -n "$d" ] || continue
        grant_dir_to_user "$d" "$user" && granted=0
    done <<< "$need"
    echo -e "\033[0m"
    return $granted
}

# Say what actually went wrong.
#
# Build systems bury the real error.  Meson reports a permission problem as
# thirty lines of Python traceback ending in
#     PermissionError: [Errno 13] Permission denied: '/usr/share/zsh/site-functions/_p11-kit'
# and the useful part -- one path this package may not write to -- is easy to
# miss entirely.  Pull it out and say what to do about it.
diagnose_install_failure() {
    local user="$1" log="$2"
    [ -f "$log" ] || return 0

    # the paths a permission error named, however the tool phrased it
    # Only the TAIL of the log: a build log runs to tens of megabytes and the
    # error is always at the end.  Scanning all of it, then running stat on
    # every path it mentions, is what made this take minutes of CPU.
    local paths tail_log
    tail_log="$(tail -c 524288 "$log" 2>/dev/null)"
    paths="$(printf '%s\n' "$tail_log" \
             | grep -aE "Permission denied|Operation not permitted|PermissionError" \
             | grep -aoE "/[A-Za-z0-9._/+-]+" | sort -u | head -n 20)"

    if [ -n "$paths" ]; then
        echo -e "\033[0;33m"
        echo "--- what went wrong -------------------------------------------"
        echo "'$user' could not write to:"
        local p d
        while IFS= read -r p; do
            [ -n "$p" ] || continue
            d="$p"; [ -d "$d" ] || d="${p%/*}"
            if [ -d "$d" ]; then
                printf "    %-46s %s %s:%s\n" "$p" \
                    "$(stat -c %A "$d" 2>/dev/null)" \
                    "$(stat -c %U "$d" 2>/dev/null)" "$(stat -c %G "$d" 2>/dev/null)"
            else
                printf "    %-46s (and its parent does not exist)\n" "$p"
            fi
        done <<< "$paths"
        echo
        echo "That directory belongs to another package.  Share it, then retry"
        echo "just the install (no recompile):"
        echo
        local first; first="$(printf '%s\n' "$paths" | head -n1)"
        d="$first"; [ -d "$d" ] || d="${first%/*}"
        echo "    packagemanager add-dir-to-sysgroup $d $user"
        echo "    packagemanager script install $user"
        echo "---------------------------------------------------------------"
        echo -e "\033[0m"
        return 0
    fi

    # otherwise: the last few lines that look like an error, not the whole log
    local tail_err
    tail_err="$(grep -hiE "^(error|fatal|.*: error:|make.*\*\*\*|.*Error [0-9]+)" "$log" 2>/dev/null \
                | grep -viE "error [0-9]+ \(ignored\)" | tail -n 5)"
    if [ -n "$tail_err" ]; then
        echo -e "\033[0;33m"
        echo "--- what went wrong -------------------------------------------"
        printf '%s\n' "$tail_err" | sed 's/^/    /'
        echo "---------------------------------------------------------------"
        echo -e "\033[0m"
    fi
    echo "  full log: $log"
}

    # A permission failure is repairable and common: grant the access and try
    # again, up to a few rounds (each attempt reveals only the FIRST directory
    # it could not write, so one round is rarely enough).
    _log="$(eval echo ~$user_name)/log/packagemanager_install-$date_log.log"
    _round=0
    while [ "$install_rc" != "0" ] && [ "${PM_NO_AUTO_FIX:-0}" != "1" ] \
          && [ "$_round" -lt 20 ]; do
        grep -qiE "permission denied|operation not permitted|cannot (create|remove|touch)" \
             "$_log" 2>/dev/null || break
        auto_grant_from_log "$_log" "$user_name" || break
        _round=$((_round + 1))
        echo -e "\033[0;33m# retrying $user_name ($parameter), round $_round ...\033[0m"
        _wrapdir="$(_wrapper_dir_from_lfs_helper)" && _wraptmp=0 || {
        _wrapdir="/tmp/pkgusr-wrappers.$$"; _wraptmp=1
        _make_build_wrappers "$_wrapdir" || _wrapdir=""
        chmod 755 "$_wrapdir" 2>/dev/null || true
    }
    su - -c "set -o pipefail; \
             ${_wrapdir:+PATH='$_wrapdir':\$PATH; export PATH; } \
             bash ~/install_$user_name $parameter 2>&1 | tee ~/log/packagemanager_install-$date_log.log" $user_name
        install_rc=${PIPESTATUS[0]}
    [ "${_wraptmp:-0}" = 1 ] && [ -n "${_wrapdir:-}" ] && rm -rf "$_wrapdir"
    done

    if [ "$install_rc" != "0" ]; then
        echo -e "\033[0;31m!! FAILED: $user_name ($parameter) exited $install_rc\033[0m"
        diagnose_install_failure "$user_name" "$_log"
        exit $install_rc
    fi
    ###################
    # Refresh the man-page db and the package's pkg.lst manifest as DETACHED
    # background tasks (setsid + disown): they survive this shell exiting and
    # can't be interrupted with Ctrl-C, so the install itself finishes cleanly.
    # When PM_NO_REFRESH=1 (packagemanager's --clean), the pkg.lst refresh is
    # SKIPPED here -- --clean regenerates the manifest itself, after removing old
    # files, so we avoid a redundant/racing background rebuild.
    if [ "${PM_NO_REFRESH:-}" = "1" ]; then
        echo -e "\033[0;33m!! Installed. Refreshing mandb in the background (pkg.lst handled by --clean)...\033[0m"
        setsid mandb >/dev/null 2>&1 </dev/null & disown 2>/dev/null || true
    else
        echo -e "\033[0;33m!! Installed. Refreshing mandb and pkg.lst in the background...\033[0m"
        setsid mandb >/dev/null 2>&1 </dev/null & disown 2>/dev/null || true
        setsid su - "$user_name" -c "list_package '$user_name' > pkg.lst.new 2>/dev/null && mv pkg.lst.new pkg.lst" >/dev/null 2>&1 </dev/null & disown 2>/dev/null || true
    fi

}

installPkg $@