#!/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 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 " 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" <&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/\ //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 the right to install into 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. # "_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 || 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 || 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 || true fi } installPkg $@