Skip to content

Instantly share code, notes, and snippets.

@krisutofu
Last active September 23, 2025 14:08
Show Gist options
  • Select an option

  • Save krisutofu/ca252cc4732acb9ae6b7e0f6a1c11b52 to your computer and use it in GitHub Desktop.

Select an option

Save krisutofu/ca252cc4732acb9ae6b7e0f6a1c11b52 to your computer and use it in GitHub Desktop.
Script that safely updates Garuda with Pacman without crashes when using btrfs and snapper
#!/bin/bash
# This bash script bypasses the crashing problem when using BTRFS and Snapper with Pacman. Useful as long as the Pacman
# space computation bug is not fixed.
# This script expects only a single mountpoint to be updated, and only the root config of snapper to be used.
# `bc` (basic calculator) needs to be installed which did NOT come by default with my Garuda Plasma installation.
if [ $(id -u) != 0 ]; then
exec sudo -s "$0" "$@";
fi
# does not work, it will not show the dialog for unknown reason, same with send-notify
true || {
notificationCommand='
if (( $exitCode )); then
kdialog --warningyesno "System Update: Snapshot cleanup aborted"'\''!'\'' "Still require $(bc -q <<< "$(( requiredSpace - availableSpace )) / 1024^2" ) MiB." --yes-label "pacman -Scc" --no-label "cancel"
case $? in
0 ) pacman -Scc; exec sudo -s "'$0'" "'$@'" ;;
1 ) ;;
* ) ;;
esac
else
kdialog --passivepopup "System Update ready"'\''!'\''
fi
'
trap "$notificationCommand" EXIT # notify user when script finished to take action for pacman
}
# debugCommand=echo; # if you want to test this script without applying changes
updatedMountpoint="/";
snapperconfig="root";
syncronizationPace=$(( 3 )); # each synchronization takes long, this tells the script how many snapshots to delete at once before syncronization of BTRFS space
maxSnapshotPercentToRemove=$(( 50 )); # set here, how many oldest snapshots from the entire `snapper list` may be deleted at most by this script
minSnapshotsPreserved=$(( 8 )); # do not delete more snapshots if the number is less equal to this limit
maxSnapshotPercentToRemove=$(( $maxSnapshotPercentToRemove >= 100 ? 100 : $maxSnapshotPercentToRemove ));
computeSpaceExpression() {
echo "$*" | sed -E -e 's/GiB/*1024^3/g' -e 's/GB?/*1000^3/g' -e 's/MiB/*1024^2/g' -e 's/MB?/*1000^2/g' -e 's/KiB/*1024/g' -e 's/KB?/*1000/g' | bc -q;
}
if [ -n "${debugCommand+used}" ]; then
requiredSpaceThreshold='0';
else
requiredSpaceThreshold="$(computeSpaceExpression "200MiB")"; # safety gap margin in Bytes; minimum additional required space that must be available
fi
# min x, minimum x, at least x, require x, required x, > x, >= x is accepted as argument
if minArgument=$(echo "${*}" | pcre2grep -i -o1 '(?<=^|\s)(?:min(?:imum)|at least|required?|>=?) (\S+)' ) \
&& minArgument=$(computeSpaceExpression "$minArgument") \
&& minArgument=${minArgument%.*} \
&& (( $minArgument > ${requiredSpaceThreshold} ));
then
requiredSpaceThreshold=$minArgument;
fi
computeAvailableSpace() {
if [ -n "${debugCommand+true}" ]; then echo $(( $RANDOM + ${requiredSpaceThreshold} )); return 0; fi
# Using grep and cut on program output is fragile in general but there is no easy usable alternative in shell languages.
computeSpaceExpression "$(btrfs filesystem df "$updatedMountpoint" | grep 'Data, single:' | cut -d' ' -f3- | sed -E 's/total=(.*?),.*? used=(.*?)/\1-\2/')";
}
computeRequiredSpace() {
if [ -n "${debugCommand+true}" ]; then echo $(( $RANDOM + ${requiredSpaceThreshold} )); return 0; fi
# alternative to pacman -Qu: checkupdates (slow!)
computeSpaceExpression "$(pacman -Qu | cut -d' ' -f1 | xargs pacman -Si | grep 'Installed Size\|Download Size' | cut -d':' -f2 | tr '\n' '+' | tr ',' '.') 0";
}
isMoreThanSnapshotLimit() (( $(wc -w <<< "$*") > ${minSnapshotsPreserved} ))
pacman -Sy > /dev/null; # should be automatically called when the system is updated
${debugCommand} btrfs subvolume sync "$updatedMountpoint"; # force update of BTRFS storage info
availableSpace=$(computeAvailableSpace);
availableSpace=${availableSpace%.*}; # availableSpace converted to int
requiredSpace=$(computeRequiredSpace);
requiredSpace=$((${requiredSpace%.*} + $requiredSpaceThreshold));
if (( $availableSpace >= $requiredSpace )); then
echo "enough space available ${availableSpace} = $(bc -q <<<"${availableSpace} / 1024^2") MiB > required space ${requiredSpace} = $(bc -q <<<"${requiredSpace} / 1024^2") MiB";
# all set, go to exit
else
snapshotNumbers=$(snapper list | grep '^ \?[[:digit:]]' | sed -E -e 's;^\s*;;g' | cut -d' ' -f1 | tr '\n' ' ');
toRemove=$(( $(wc -w <<< "$snapshotNumbers") * $maxSnapshotPercentToRemove / 100 ));
toRemove=$(( ($toRemove <= 0 && $maxSnapshotPercentToRemove > 0) ? 1 : $toRemove )); # as long as maxSnapshotPercentToRemove is set, do remove at least one snapshot
# remove groups of contiguous snapshots in steps until sufficient memory is available
while (( $availableSpace < $requiredSpace )) && (( --toRemove >= 0 )) && isMoreThanSnapshotLimit "$snapshotNumbers";
do
echo "removing snapshot ${snapshotNumbers%% *}";
${debugCommand} snapper -c "$snapperconfig" delete ${snapshotNumbers%% *};
snapshotNumbers=${snapshotNumbers#* };
if (( ++removedCount % $syncronizationPace != 0 )) && (( toRemove >= 0 )) && isMoreThanSnapshotLimit "$snapshotNumbers"; then
continue
fi
${debugCommand} btrfs subvolume sync "$updatedMountpoint";
availableSpace=$(computeAvailableSpace);
availableSpace=${availableSpace%.*};
done
if (( $availableSpace < $requiredSpace )); then
echo -e "Not enough space ${availableSpace} for system update ${requiredSpace}"'!!'
if (( $maxSnapshotPercentToRemove > 0 )) && isMoreThanSnapshotLimit "$snapshotNumbers"; then
echo "Run this script again to remove more snapshots";
elif (( isPaccacheCleanupAllowed )); then
echo "Still require $(bc -q <<< "$(( requiredSpace - availableSpace )) / 1024^2" ) MiB." 2>&1
# kdialog --warningyesno "System Update: Snapshot cleanup aborted"'!' "Still require $(bc -q <<< "$(( requiredSpace - availableSpace )) / 1024^2" ) MiB." --yes-label "pacman -Scc" --no-label "cancel" # not working
# case $? in
# 0 ) pacman -Scc; exec sudo -s "'$0'" "'$@'" ;;
# 1 ) ;;
# * ) ;;
# esac
fi
exit ${exitCode:=1};
fi
echo -e "Enough space ${availableSpace} = $(bc -q <<<"${availableSpace} / 1024^2") MiB for system update ${requiredSpace} = $(bc -q <<<"${requiredSpace} / 1024^2") MiB available"'!'
fi
echo -e "Manually check the total size as depicted by Pacman"'!'"\n--------------------";
# kdialog --passivepopup "System Update ready"'!' # does not show anything and stops execution
exit ${exitCode:=0};
@krisutofu
Copy link
Author

Update: to my delight, Garuda finally added an official memory check to garuda-update like my script is doing it. (It doesn't free snapshots however.) In addition to checking memory requirements, my script also removes some snapshots in multiples of n until the requirements are satisfied or until half the snapshots only remain. (In that case, it requires manual action to empty the pacman cache.)

I wondered in the past why 1GiB of extraspace could still lead to Pacman memory crashes during update. The official check provides the answer: I need to add package Download Size to the space requirement besides just buffer space.

Passing "at least 1GiB" as argument to this script before garuda-update will guarantee that garuda-update passes the official memory check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment