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.gzpackagemanager_install 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 $@