Last active
February 29, 2024 01:10
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
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.
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
@Bondrake The script breaks if there's no generations under
$profile
with Bash (e.g regular system account), maybe I'm doing something wrong?