#!/bin/bash
# mkchrootb Copyright (C) 2024 kylon
#
# 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 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
cachepath="/home/$(whoami)/.cache/mkchrootb"
repopath="/var/lib/repo/aur"
oldpath="$repopath/old"
rdbpath="$repopath/aur.db.tar.gz"
gitdbpath="$cachepath/gitdb"
declare -A parsedArgs
declare -a chrootIPkgList aurIPkgList pkgDList repoDList
export CHROOT=/opt/chroot
function showHelp() {
echo -e "Usage: mkchrootb [options]\n\noptions:"
echo -e "-c\n clean cache\n"
echo -e "-cr\n clean local repo\n"
echo -e "-co\n clean local repo old pkgs\n"
echo -e "-cc\n clean cache and rebuild chroot\n"
echo -e "-p pkg[,pkg2,pkg3,..]\n install pkgs to chroot, comma separated list (official or local repo)\n"
echo -e "-a pkg[,pkg2,pkg3,..]\n build pkgs from aur, comma separated list\n"
echo -e "-d pkg[,pkg2,pkg3,..]\n uninstall pkgs, comma separated list\n"
echo -e "-da pkg[,pkg2,pkg3,..]\n delete pkgs from local repo database, comma separated list\n"
echo -e "-l\n build PKGBUILD in current path\n"
echo -e "-x\n enable access to X server to build pkgs that run GUIs in build process\n"
echo -e "-r\n add pkgs to local repo after build\n"
echo -e "-i\n install pkgs after build\n"
echo -e "-u\n update pkgs in local repo\n"
echo -e "-w\n ask to view/edit PKGBUILD before build (vifm, vi or nano)\n"
echo -e "-k\n do not remove package repo from cache folder (deleted by default after build)\n"
echo -e "-ss\n create mkchrootb signing key\n"
echo -e "-s\n sign database and pkgs\n"
echo -e "-us\n remove database and pkgs signature\n"
echo -e "-h\n this help\n"
}
function ask() {
local q="$1: "
echo -ne "$q"
read -r inpt
}
function askYN() {
local q="$1 [y/n]: "
while true; do
read -p "$q" -r inpt
if [[ "$inpt" == "y" || "$inpt" == "Y" || "$inpt" == "" ]]; then
return 0
elif [[ "$inpt" == "n" || "$inpt" == "N" ]]; then
return 1
fi
done
}
function print() {
printf "%b\n" "$*" >&2
}
function printHead() {
print " | | | | | | | |"
print " _ __ ___ | | _____| |__ _ __ ___ ___ | |_| |__"
print "| '_ \` _ \| |/ / __| '_ \| '__/ _ \ / _ \| __| '_ \\"
print "| | | | | | < (__| | | | | | (_) | (_) | |_| |_) |"
print "|_| |_| |_|_|\_\___|_| |_|_| \___/ \___/ \__|_.__/"
print "\n"
}
function hasArg() {
if [[ "${parsedArgs[$1]}" == "1" ]]; then
return 0
fi
return 1
}
function parseArguments() {
local args=($@)
local argc=${#args[@]}
local aidx=0
if ((argc<=0)); then
showHelp
exit 0
fi
while ((aidx < argc)); do
local _arg=$(echo "${args[aidx]}" | xargs)
parsedArgs+=([$_arg]="1")
case "$_arg" in
"-p"|"-a"|"-d"|"-da")
((++aidx))
readarray -d "," -t plist < <(echo "${args[aidx]}" | xargs)
;;
*);;
esac
if [[ "$_arg" == "-p" ]]; then
chrootIPkgList=(${plist[@]})
elif [[ "$_arg" == "-a" ]]; then
aurIPkgList=(${plist[@]})
elif [[ "$_arg" == "-d" ]]; then
pkgDList=(${plist[@]})
elif [[ "$_arg" == "-da" ]]; then
repoDList=(${plist[@]})
fi
((++aidx))
done
}
function cleanCache() {
if [[ ! -d "$cachepath" ]]; then
return
fi
local db=""
if [[ -f "$gitdbpath" ]] && askYN "do you want to keep git pkgs update hash cache?"; then
db=$(cat "$gitdbpath")
fi
rm -rf "$cachepath/"*
if [[ "$db" != "" ]]; then
echo "$db" > "$gitdbpath"
fi
}
function cleanCacheChroot() {
if [[ -d "$CHROOT" ]]; then
sudo rm -rf "$CHROOT"
fi
cleanCache
}
function cleanup() {
if hasArg "-x"; then
xhost -local:
sudo sed -i '$d' "$CHROOT/root/etc/environment"
fi
unset CHROOT
unset -v chrootIPkgList aurIPkgList pkgDList repoDList parsedArgs
}
function getGpgHome() {
local gpgfold="${MKCHROOTB_GPGHOME}"
if [[ "$gpgfold" == "" ]]; then
gpgfold="/home/$(whoami)/.mkchrootb-gpg"
fi
echo "$gpgfold"
}
function addToRepo() {
local hasS="${parsedArgs[-s]}"
local rmList=""
local zstList=""
local gpgHome=""
local ret=1
if [[ ! -d "$oldpath" ]]; then
mkdir "$oldpath"
fi
for zst in *.zst; do # check if new pkgs already exist in repo
local _name=$(pacman -Qip "$zst" | grep -i name\ | awk -F":" '{ print $NF }' | xargs)
local _opkg="$repopath/$_name"
zstList+=" $zst"
if pacman -Sl aur|grep -i "$_name" &>/dev/null; then
rmList+=" $_name"
fi
if ls "$_opkg"*.zst &>/dev/null; then
cp -f "$_opkg"*.zst "$oldpath"
rm -f "$_opkg"*.zst
if ls "$_opkg"*.zst.sig &>/dev/null; then
cp -f "$_opkg"*.zst.sig "$oldpath"
rm -f "$_opkg"*.zst.sig
fi
fi
done
cp -f *.zst "$repopath" # cp new pkgs to local repo folder
if [[ "$hasS" == "1" ]]; then
gpgHome=$(getGpgHome)
signPkgs
fi
if [[ "$rmList" != "" ]]; then
if [[ "$hasS" == "1" ]]; then
GNUPGHOME="$gpgHome" repo-remove -v -s "$rdbpath" $rmList
else
repo-remove "$rdbpath" $rmList
fi
fi
if [[ "$hasS" == "1" ]]; then
GNUPGHOME="$gpgHome" repo-add -n -v -s "$rdbpath" $zstList
ret=$?
else
repo-add -n "$rdbpath" $zstList
ret=$?
fi
if [[ "$ret" == "0" ]]; then
arch-nspawn $CHROOT/root pacman -Syy
sudo pacman -Syy
fi
}
function removePkgFromRepo() {
local hasS="$1"
local pkg="$2"
local ret=1
if [[ "$hasS" == "1" ]]; then
repo-remove -v -s "$rdbpath" $pkg
ret=$?
else
repo-remove "$rdbpath" $pkg
ret=$?
fi
if [[ "$ret" == "0" ]]; then
rm -f "$repopath/$pkg"*.zst
rm -f "$repopath/$pkg"*.zst.sig &>/dev/null
fi
}
function uninstallPkgs() {
local ret=1
if askYN "do you want to remove pkg dependancies?"; then
sudo pacman -Rs ${pkgDList[@]}
ret=$?
else
sudo pacman -R ${pkgDList[@]}
ret=$?
fi
if [[ "$ret" != "0" ]]; then
print "\nfailed to uninstall pkgs: ${pkgDList[*]}"
exit 1
else
local hasS="${parsedArgs[-s]}"
for pkg in "${pkgDList[@]}"; do
removePkgFromRepo "$hasS" "$pkg"
done
sudo pacman -Syy
fi
}
function removePkgsFromRepo() {
local hasS="${parsedArgs[-s]}"
for pkg in "${repoDList[@]}"; do
removePkgFromRepo "$hasS" "$pkg"
done
sudo pacman -Syy
}
function getAurPkgListToInstall() {
local listLen=${#aurIPkgList[@]}
local -A flist
local i=0
while ((i < listLen)); do
local jmax=$((i+200)) # query limit
local j=$i
local qargs=""
while ((j < listLen && j < jmax)); do
qargs+="arg[]=${aurIPkgList[$j]}&"
((++j))
done
readarray -t apiRes < <(curl -s "https://aur.archlinux.org/rpc/v5/info?$qargs" | jq '.results | .[] | .Name,.PackageBase' | tr -d '"')
local apiResLen=${#apiRes[@]}
local apiPkgC=$((apiResLen/2)) # res contains both pkg and pkgBase, we only need pkgs count
local c=$((j-i))
local a=0
if ((apiPkgC > c)); then
print "ERR: apiRes and aurIPkg window len mismatch"
cleanup
exit 1
fi
while ((i < j)); do
if [[ "$apiResLen" == "0" || "${aurIPkgList[$i]}" != "${apiRes[$a]}" ]]; then
print "WARN: orphan aur package: ${aurIPkgList[$i]}"
else
local b=$((a+1))
flist["${apiRes[$b]}"]="1"
((a+=2))
fi
((++i))
done
done
echo "${!flist[*]}"
}
function signPkgs() {
local gpgfold=$(getGpgHome)
for file in "$repopath/"*.zst; do
if [[ -f "$file.sig" ]]; then
continue
fi
gpg --homedir "$gpgfold" --output "$file.sig" --detach-sig "$file"
done
}
function unsignPkgs() {
rm -f "$repopath/"*.sig
sudo rm -f /var/lib/pacman/sync/aur.db.sig
sudo rm -f "$CHROOT/root/var/lib/pacman/sync/aur.db.sig"
sudo pacman -Syy
print "\nINFO: pkg and db signatures have been deleted, imported keys must be deleted by the user\n"
}
function setupSKey() {
local gpgfold=$(getGpgHome)
if [[ -f "$gpgfold/trustdb.gpg" ]]; then
return
fi
local who=$(whoami)
rm -R $gpgfold &>/dev/null
mkdir -m 700 $gpgfold
cat >/tmp/mkchbk <"
else
print "\n\nERR: failed to export mkchrootb key!"
fi
}
function editPkgbuild() {
if ! hasArg "-w"; then
return
fi
local editors=""
local _tig=0
local sidx=0
local -a bins
if ! askYN "do you want to view/edit PKGBUILD?"; then
return
fi
if vifm -h &>/dev/null; then
editors+="$sidx] vifm "
bins+=("vifm")
((++sidx))
fi
if nano -h &>/dev/null; then
editors+="$sidx] nano "
bins+=("nano")
((++sidx))
fi
if vi -h &>/dev/null; then
editors+="$sidx] vi"
bins+=("vi")
fi
if [[ "$editors" == "" ]]; then
print "no editor found (vifm, nano, vi), skip.."
return
fi
if tig -h &>/dev/null; then
_tig=1
fi
while true; do
ask "$editors"
if [[ "${bins[$inpt]}" == "vifm" ]]; then
vifm "$1"
elif [[ "${bins[$inpt]}" == "nano" ]]; then
nano "$1/PKGBUILD"
else
vi "$1/PKGBUILD"
fi
if [[ "$_tig" == "1" ]] && askYN "do you want to review the changes in tig?"; then
cd "$1"
tig
cd ..
fi
if askYN "have you done editing?"; then
break
fi
done
}
function getGitHash() {
local srcinfoUrl="https://aur.archlinux.org/cgit/aur.git/plain/.SRCINFO?h=$1"
local source=$(curl -s "$srcinfoUrl" | grep source\ = | head -n 1) # correct? don't see a way to check this
local gitUrl="${source#*git+}"
local hashLen=7
local awkArg="{print substr(\$0, 0, $hashLen)}"
local hash=""
if [[ "$gitUrl" == "" ]]; then
print "INFO: $1 has no source, skipping git hash check.."
hash="x"
elif [[ "$gitUrl" =~ "#" ]]; then
local qr="${gitUrl#*#}"
local gurl="${gitUrl%%#*}"
if [[ "$qr" =~ "branch=" || "$qr" =~ "tag=" ]]; then
local t="${qr#*=}"
if [[ "$qr" =~ "branch=" ]]; then
hash=$(git ls-remote "$gurl" -h "$t" | head -n 1 | awk "$awkArg")
else
hash=$(git ls-remote "$gurl" -t "$t" | head -n 1 | awk "$awkArg")
fi
else
hash=$(git ls-remote "$gitUrl" -t "$qr" | head -n 1 | awk "$awkArg") # try tag
if [[ "$hash" == "" ]]; then # commit hash
hash="${qr:0:$hashLen}"
fi
fi
else
hash=$(git ls-remote "$gitUrl" | head -n 1 | awk "$awkArg")
fi
echo "$hash"
}
function saveGitHash() {
local pkg="$1"
local hash=$(getGitHash "$pkg")
local cur=""
if [[ "$hash" == "" ]]; then
return
elif [[ -f "$gitdbpath" ]]; then
cur=$(cat "$gitdbpath" | grep "$pkg" | xargs)
fi
if [[ "$cur" == "" ]]; then
echo "$pkg=$hash" >> "$gitdbpath"
else
sed -i -E "s/!*$pkg=[a-z0-9A-Z]+/$pkg=$hash/" "$gitdbpath"
fi
}
function isLatestGit() {
local pkg="$1"
local hash=$(getGitHash "$pkg")
if [[ "$hash" == "x" ]]; then # child pkg or orphan, don't try to update
return 0
elif [[ "$hash" == "" || ! -f "$gitdbpath" ]]; then
return 1
else
local cur=$(cat "$gitdbpath" | grep "$pkg" | xargs)
local curhash="${cur#*=}"
if [[ "$curhash" == "" || "${cur:0:1}" == "!" ]]; then
return 1
elif [[ "$curhash" == "$hash" ]]; then
return 0
else
sed -i -E "s/!*$pkg/!$pkg/" "$gitdbpath"
fi
fi
return 1
}
function getUpdatePkgList() {
local -a pkgList
local -a pkgVerList
local -A ghosts
local toUpdList=""
local pkgListLen=0
local i=0
readarray -t repoPkgs < <(pacman -Sl aur)
for rpkg in "${repoPkgs[@]}"; do
readarray -d " " -t pdata < <(printf "%s" "$rpkg")
if [[ "${ghosts[${pdata[1]}]}" == "1" ]]; then
continue
fi
if [[ "${pdata[1]}" =~ "-git" ]]; then
if ! isLatestGit "${pdata[1]}"; then
toUpdList+=" ${pdata[1]}"
else # up to date, ignore children pkgs
local srcinfoUrl="https://aur.archlinux.org/cgit/aur.git/plain/.SRCINFO?h=${pdata[1]}"
local pkgsInPkg=($(curl -s "$srcinfoUrl" | grep pkgname\ = | cut -d "=" -f2 | xargs))
if ((${#pkgsInPkg[@]} > 1)); then
for cpkg in "${pkgsInPkg[@]}"; do
ghosts["$cpkg"]="1"
done
fi
fi
else
pkgList+=("${pdata[1]}")
pkgVerList+=("${pdata[2]}")
fi
done
pkgListLen=${#pkgList[@]}
while ((i < pkgListLen)); do
local jmax=$((i+200)) # query limit
local j=$i
local qargs=""
while ((j < pkgListLen && j < jmax)); do
qargs+="arg[]=${pkgList[$j]}&"
((++j))
done
readarray -t apiRes < <(curl -s "https://aur.archlinux.org/rpc/v5/info?$qargs" | jq '.results | .[] | .Name,.Version' | tr -d '"')
local apiResLen=${#apiRes[@]}
local apiPkgC=$((apiResLen/2)) # res contains both pkg and version, we only need pkgs count
local c=$((j-i))
local a=0
if ((apiPkgC > c)); then
print "ERR: apiRes and update pkgList window len mismatch"
cleanup
exit 1
fi
while ((i < j)); do
if [[ "$apiResLen" == "0" || "${pkgList[$i]}" != "${apiRes[$a]}" ]]; then
print "WARN: orphan aur package: ${pkgList[$i]}"
else
local v=$((a+1))
if [[ "${apiRes[$v]}" < "${pkgVerList[$i]}" ]]; then
print "INFO: package downgrade: ${pkgList[$i]} ${pkgVerList[$i]} -> ${apiRes[$v]}"
toUpdList+=" ${apiRes[$a]}"
elif [[ "${apiRes[$v]}" > "${pkgVerList[$i]}" ]]; then
print "INFO: package upgrade: ${pkgList[$i]} ${pkgVerList[$i]} -> ${apiRes[$v]}"
toUpdList+=" ${apiRes[$a]}"
fi
((a+=2))
fi
((++i))
done
done
echo "$toUpdList"
}
function updateRepoPkgs() {
local pkgUpdList=$(getUpdatePkgList)
local -a selectedPkgs
local i=0
if [[ "$pkgUpdList" == "" ]]; then
print "\nYou are up to date!"
cleanup
exit 0
fi
readarray -d " " -t updList < <(echo "$pkgUpdList" | xargs)
print "\n"
for pkg in "${updList[@]}"; do
printf "%d] %s " $i "$pkg"
((++i))
done
ask "\nselect packages to update [enter=all, n=none]"
if [[ "$inpt" == "n" || "$inpt" == "N" ]]; then
cleanup
exit 0
elif [[ "$inpt" == "" ]]; then
selectedPkgs="${updList[*]}"
else
local -a userSel
readarray -d " " -t selectedPkgs < <(echo "$inpt" | xargs)
for idx in "${selectedPkgs[@]}"; do
if [[ "$idx" =~ "-" ]]; then
readarray -d "-" -t idxs < <(echo "$idx" | xargs)
local jdx="${idxs[0]}"
local edx="${idxs[1]}"
while ((jdx<=edx)); do
userSel[jdx]="${updList[$jdx]}"
((++jdx))
done
else
userSel[idx]="${updList[$idx]}"
fi
done
selectedPkgs="${userSel[*]}"
fi
print "\npackages to update:\n$selectedPkgs\n"
if ! askYN "do you want to proceed?"; then
cleanup
exit 0
fi
buildFromAur "$selectedPkgs" "0" "1"
}
function buildFromAur() {
local list=($1)
local instPkgs="$2"
local toRepo="$3"
local pwdbak=$(pwd)
local failed=""
local ret=1
cd "$cachepath"
if [[ "$instPkgs" == "1" ]]; then
if [[ -d "instdir" ]]; then
rm -rf instdir
fi
mkdir instdir
fi
for pkg in "${list[@]}"; do
if ! hasArg "-k" && [[ -d "$pkg" ]]; then
rm -rf "$pkg"
fi
if [[ -d "$pkg" ]]; then
rm -f "$pkg/"*.zst
git -C "$pkg" pull
ret=$?
else
git clone "https://aur.archlinux.org/$pkg.git"
ret=$?
fi
if [[ "$ret" != "0" || ! -d "$pkg" ]]; then
failed+=" $pkg"
continue
fi
editPkgbuild "$pkg"
cd "$pkg"
if [[ ! -f "PKGBUILD" ]]; then
print "$pkg: no PKGBUILD, skip.."
cd ..
failed+=" $pkg"
continue
fi
makechrootpkg -c -r $CHROOT
if [[ "$?" != "0" ]]; then
cd ..
rm -rf "$pkg"
failed+=" $pkg"
continue
fi
if [[ "$toRepo" == "1" ]]; then
addToRepo
if [[ "$pkg" =~ "-git" ]]; then
saveGitHash "$pkg"
fi
fi
if [[ "$instPkgs" == "1" ]]; then
cp -f *.zst ../instdir
fi
cd ..
if ! hasArg "-k"; then
rm -rf "$pkg"
fi
done
if [[ "$instPkgs" == "1" ]]; then
sudo pacman -U "instdir/"*.zst
rm -rf instdir
fi
cd "$pwdbak"
if [[ "$failed" != "" ]]; then
print "\n\nfailed to build:$failed"
fi
}
function addLocalAurRepo() {
local sync=0
if ! cat /etc/pacman.conf|grep "[aur]" > /dev/null; then
sync=1
print "INFO: adding local [aur] repo to pacman.conf"
sudo tee -a "/etc/pacman.conf" > /dev/null <