Skip to content

Instantly share code, notes, and snippets.

@Bondrake
Last active February 29, 2024 01:10
Show Gist options
  • Save Bondrake/27555c9d02c2882fd5e32f8ab3ed620b to your computer and use it in GitHub Desktop.
Save Bondrake/27555c9d02c2882fd5e32f8ab3ed620b to your computer and use it in GitHub Desktop.
NixOS script to keep 10 generations or 7 days, whichever is more (configurable, profile is selectable)
#!/usr/bin/env bash
set -euo pipefail
## Defaults
keepGensDef=10; keepDaysDef=7
keepGens=$keepGensDef; keepDays=$keepDaysDef
## Usage
usage () {
printf "Usage:\n\t trim-generations.sh (defaults are: Keep-Gens=$keepGensDef Keep-Days=$keepDaysDef Profile=user)\n\n"
printf "If you enter any parameters, you must enter all three.\n\n"
printf "Example:\n\t trim-generations.sh 15 10 home-manager\n"
printf "... this will work on the home-manager profile and keep all generations from the last 10 days, and keep at least 15 generations no matter how old.\n"
printf "\nProfile choices available: \t user, home-manager, channels, system (root only)\n"
}
## Handle parameters (and change if root)
if [[ $EUID -ne 0 ]]; then
profile=$(readlink /home/$USER/.nix-profile)
else
profile="/nix/var/nix/profiles/default"
fi
if (( $# < 1 )); then
printf "Keeping default: 10 generations OR 7 days, whichever is more\n"
elif [[ $# -le 2 ]]; then
printf "\nError: Not enough arguments.\n\n" >&2
usage
exit 1
elif (( $# > 4)); then
printf "\nError: Too many arguments.\n\n" >&2
usage
exit 2
else
keepGens=$1; keepDays=$2;
(( keepGens < 1 )) && keepGens=1
(( keepDays < 0 )) && keepDays=0
if [[ $EUID -ne 0 ]]; then
if [[ $3 == "user" ]] || [[ $3 == "default" ]]; then
profile=$(readlink /home/$USER/.nix-profile)
elif [[ $3 == "home-manager" ]]; then
profile="/nix/var/nix/profiles/per-user/$USER/home-manager"
elif [[ $3 == "channels" ]]; then
profile="/nix/var/nix/profiles/per-user/$USER/channels"
else
printf "\nError: Do not understand your third argument. Should be one of: (user / home-manager/ channels)\n\n"
usage
exit 3
fi
else
if [[ $3 == "system" ]]; then
profile="/nix/var/nix/profiles/system"
elif [[ $3 == "user" ]] || [[ $3 == "default" ]]; then
profile="/nix/var/nix/profiles/default"
else
printf "\nError: Do not understand your third argument. Should be one of: (user / system)\n\n"
usage
exit 3
fi
fi
printf "OK! \t Keep Gens = $keepGens \t Keep Days = $keepDays\n\n"
fi
printf "Operating on profile: \t $profile\n\n"
## Runs at the end, to decide whether to delete profiles that match chosen parameters.
choose () {
local default="$1"
local prompt="$2"
local answer
read -p "$prompt" answer
[ -z "$answer" ] && answer="$default"
case "$answer" in
[yY1] ) #printf "answered yes!\n"
nix-env --delete-generations -p $profile ${!gens[@]}
exit 0
;;
[nN0] ) printf "answered no! exiting\n"
exit 6;
;;
* ) printf "%b" "Unexpected answer '$answer'!" >&2
exit 7;
;;
esac
} # end of function choose
## Query nix-env for generations list
IFS=$'\n' nixGens=( $(nix-env --list-generations -p $profile | sed 's:^\s*::; s:\s*$::' | tr '\t' ' ' | tr -s ' ') )
timeNow=$(date +%s)
## Get info on oldest generation
IFS=' ' read -r -a oldestGenArr <<< "${nixGens[0]}"
oldestGen=${oldestGenArr[0]}
oldestDate=${oldestGenArr[1]}
printf "%-30s %s\n" "oldest generation:" $oldestGen
#oldestDate=${nixGens[0]:3:19}
printf "%-30s %s\n" "oldest generation created:" $oldestDate
oldestTime=$(date -d "$oldestDate" +%s)
oldestElapsedSecs=$((timeNow-oldestTime))
oldestElapsedMins=$((oldestElapsedSecs/60))
oldestElapsedHours=$((oldestElapsedMins/60))
oldestElapsedDays=$((oldestElapsedHours/24))
printf "%-30s %s\n" "minutes before now:" $oldestElapsedMins
printf "%-30s %s\n" "hours before now:" $oldestElapsedHours
printf "%-30s %s\n\n" "days before now:" $oldestElapsedDays
## Get info on current generation
for i in "${nixGens[@]}"; do
IFS=' ' read -r -a iGenArr <<< "$i"
genNumber=${iGenArr[0]}
genDate=${iGenArr[1]}
if [[ "$i" =~ current ]]; then
currentGen=$genNumber
printf "%-30s %s\n" "current generation:" $currentGen
currentDate=$genDate
printf "%-30s %s\n" "current generation created:" $currentDate
currentTime=$(date -d "$currentDate" +%s)
currentElapsedSecs=$((timeNow-currentTime))
currentElapsedMins=$((currentElapsedSecs/60))
currentElapsedHours=$((currentElapsedMins/60))
currentElapsedDays=$((currentElapsedHours/24))
printf "%-30s %s\n" "minutes before now:" $currentElapsedMins
printf "%-30s %s\n" "hours before now:" $currentElapsedHours
printf "%-30s %s\n\n" "days before now:" $currentElapsedDays
fi
done
## Compare oldest and current generations
timeBetweenOldestAndCurrent=$((currentTime-oldestTime))
elapsedDays=$((timeBetweenOldestAndCurrent/60/60/24))
generationsDiff=$((currentGen-oldestGen))
## Figure out what we should do, based on generations and options
if [[ elapsedDays -le keepDays ]]; then
printf "All generations are no more than $keepDays days older than current generation. \nOldest gen days difference from current gen: $elapsedDays \n\n\tNothing to do!\n"
exit 4;
elif [[ generationsDiff -lt keepGens ]]; then
printf "Oldest generation ($oldestGen) is only $generationsDiff generations behind current ($currentGen). \n\n\t Nothing to do!\n"
exit 5;
else
printf "\tSomething to do...\n"
declare -a gens
for i in "${nixGens[@]}"; do
IFS=' ' read -r -a iGenArr <<< "$i"
genNumber=${iGenArr[0]}
genDiff=$((currentGen-genNumber))
genDate=${iGenArr[1]}
genTime=$(date -d "$genDate" +%s)
elapsedSecs=$((timeNow-genTime))
genDaysOld=$((elapsedSecs/60/60/24))
if [[ genDaysOld -gt keepDays ]] && [[ genDiff -ge keepGens ]]; then
gens["$genNumber"]="$genDate, $genDaysOld day(s) old"
fi
done
printf "\nFound the following generation(s) to delete:\n"
for K in "${!gens[@]}"; do
printf "generation $K \t ${gens[$K]}\n"
done
printf "\n"
choose "y" "Do you want to delete these? [Y/n]: "
fi
@GrabbenD
Copy link

GrabbenD commented Apr 1, 2023

@Bondrake The script breaks if there's no generations under $profile with Bash (e.g regular system account), maybe I'm doing something wrong?

[me@unix:~]$ sh nixos-trim-generations.sh 1 0 user
OK! 	Keep Gens = 1 	Keep Days = 0

Operating on profile: 	/nix/var/nix/profiles/per-user/me/profile

nixos-trim-generations.sh: line 96: nixGens[0]: unbound variable
[me@unix:~]$ nix-env --list-generations -p me | sed 's:^\s*::; s:\s*$::' | tr '\t' ' ' | tr -s ' '
 

@GrabbenD
Copy link

GrabbenD commented Apr 1, 2023

Also, is there a compact command syntax for deleting all old generations from NIX (every user, home-manager, channels, system) while only keeping the current one? I'm wondering this because nix-collect-garbage -d deletes my compiled nix-shell binaries.

@Jonathan-M-C
Copy link

For users with home-manager and XDG_STATE_HOME defined, here is a patch that may fix the "nixGens[0]: unbound variable" issue.

To apply it, copy the below patch to a file named diff.patch, then execute the following in your shell: patch trim-generations.sh diff.patch

--- trim-generations.sh	2023-06-22 12:36:45.022271393 -0400
+++ trim-generations.sh	2023-06-22 15:30:37.905537535 -0400
@@ -36,9 +36,13 @@
     (( keepDays < 0 )) && keepDays=0
     if [[ $EUID -ne 0 ]]; then
         if [[ $3 == "user" ]] || [[ $3 == "default" ]]; then
             profile=$(readlink /home/$USER/.nix-profile)
         elif [[ $3 == "home-manager" ]]; then
-            profile="/nix/var/nix/profiles/per-user/$USER/home-manager"
+            # home-manager defaults to $XDG_STATE_HOME; otherwise, use
+            # `home-manager generations` and `nix-store --query --roots
+            # /nix/store/...` to figure out what reference is keeping the old
+            # generations alive.
+            profile="${XDG_STATE_HOME:-$HOME/.local/state}/nix/profiles/home-manager"
         elif [[ $3 == "channels" ]]; then
             profile="/nix/var/nix/profiles/per-user/$USER/channels"
         else

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