nix-community / nix-direnv

A fast, persistent use_nix/use_flake implementation for direnv [maintainer=@Mic92 / @bbenne10]
MIT License
1.83k stars 103 forks source link

Idiomatic way to GC old unused devshells? #529

Open DrRuhe opened 6 days ago

DrRuhe commented 6 days ago

I am running low on storage space. I investigated nix store usage and with nix-store --gc --print-roots I am noticing many gcroots in .direnv directories. This is expected, since I have been using direnv for a while now, in several projects, each with their own devshells and dependencies.

As many of these devshells are now no longer used I want to delete them. I know that I could just remove all the roots from direnv directories, but this would also shoot down the recently used devshells, which I want to keep.

I wanted to have something similar to nix-collect-garbage --delete-older-than {{PERIOD}} for cleaning up old devshells. For now I have written this nushell script, feel free to link it in the docs or give me pointers for improvement:

#!/usr/bin/env nu

use std log

def nixStoreGetDevshellGcRoots [] {
    return (nix-store --gc --print-roots |
        lines |
        parse "{loc} -> {storepath}" |
        insert dir {|gcRoot|$gcRoot.loc|parse "{path}/.direnv/{x}" | get -i 0.path} |
        group-by --to-table dir |
        rename dir gcRoots |
        insert modified {|devShell|
            $devShell.gcRoots|each {|gcRoot|
                ls -D $gcRoot.loc|get 0.modified
            }|sort --reverse|first
        })
}

def removeGCRootsFromDevshells [] {
    let devshells = $in

    $devshells|each {|devshell|
        log info $"(ansi red_bold)Removing devshell last modified ($devshell.modified|date humanize): ($devshell.dir) (ansi reset)"
        direnv revoke $devshell.dir
        rm -r $"($devshell.dir)/.direnv"
    }

    return
}

def main [--delete-older-than : duration = 31day] {
    nixStoreGetDevshellGcRoots|where modified < ((date now) - $delete_older_than)|removeGCRootsFromDevshells
}

Namely, I know these current limitations:

bbenne10 commented 6 days ago

Does nix-collect-garbage --delete-older-than not work? How did you determine that?

I'm running it now and it seems to work okay?

DrRuhe commented 6 days ago

nix-collect-garbage --delete-older-than works "as designed". Theres just no way to remove old unused devshell gcroots.

Mic92 commented 2 days ago

This should work:

find /nix/var/nix/gcroots/auto -type l -mtime +30 -delete

This will remove all gc roots older than 30 days. You can add this to a systemd timer and combine it with the nix gc option (nix.gc.automatic) in NixOS.

bbenne10 commented 2 days ago

I have the following:

#!/usr/bin/env bash
# N.B Depends on GNU date & stat (from nixpkgs coreutils)

platform=$(uname -s)

date_path=$(command -v date)
if [[ "$platform" = "Darwin" && (! "$date_path" =~ ^/nix/store) ]]; then
    >&2 echo "WARNING: GNU date required (from nixpkgs' coreutils); this will probably fail!"
fi

stat_path=$(command -v stat)
if [[ "$platform" = "Darwin" && (! "$stat_path" =~ ^/nix/store) ]]; then
    >&2 echo "WARNING: GNU stat required (from nixpkgs' coreutils); this will probably fail!"
fi

direnv_cache_pattern=${1:-"^.+\\.direnv"}
shift
older_than_pattern=${*:-"30 days ago"}
early_dt_secs=$(date -d "$older_than_pattern" "+%s")

all_files=$(nix-store --gc --print-roots | grep "${direnv_cache_pattern}" | awk 'FS="->" {print $1}')

while read -r pth; do
    mtime=$(stat -c %Y -- "$pth")
    if [[ $mtime -lt $early_dt_secs ]]; then
        rm "$pth"
    fi
done <<< "$all_files"

This addresses the readability (imo) of the nushell script and the assumption that $DIRENV_LAYOUT_DIR is not customized. I am sure there's a standalone awk invocation that will apply the regex to input, avoiding the "needless" grep. You invoke this with something like: ./clean_old_direnv_roots.sh "cache\/direnv\/direnv\/layouts" "2 weeks ago". Note that this doesn't handle the direnv deny bits from the nushell script above. I couldn't find a way to reverse my direnv layout to the project directory reliably.