Skip to content

Instantly share code, notes, and snippets.

@krisutofu
Last active March 10, 2025 21:42
Show Gist options
  • Save krisutofu/ca252cc4732acb9ae6b7e0f6a1c11b52 to your computer and use it in GitHub Desktop.
Save krisutofu/ca252cc4732acb9ae6b7e0f6a1c11b52 to your computer and use it in GitHub Desktop.
Script that safely updates 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+used}" ]; 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+used}" ]; 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' | 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

Fetching the snapshotnumbers has not worked properly anymore. Either it's because of reaching snapshot 1000 or bash was updated with breaking changes. Therefore, the initial whitespace of each line in the table output is removed in the extraction of the snapshot numbers.

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