1 { lib, config, pkgs, ... }:
3 inherit (lib) escapeShellArg;
4 cfg = config.nix.profile-gc;
5 parse-duration = duration: pkgs.runCommand "duration" { buildInputs = with pkgs; [ systemd ]; } ''
7 parsed=$(systemd-analyze timespan ${escapeShellArg duration} | awk '$1 == "μs:" { print $2 }')
8 echo "$parsed" > "$out"
13 enable = lib.mkEnableOption "Automatic profile garbage collection";
14 dryRun = lib.mkOption {
15 description = "Say what would have been deleted rather than actually deleting profiles";
16 type = lib.types.bool;
19 keepLast = lib.mkOption {
21 Number of recent profiles to keep.
22 This control is similar to nix-env --delete-generation's +5 syntax.
24 type = lib.types.ints.unsigned;
27 keepLastActive = lib.mkOption {
28 description = "Number of recent active profiles to keep";
29 type = lib.types.ints.unsigned;
32 keepLastActiveSystem = lib.mkOption {
33 description = "Number of recent active system profiles to keep";
34 type = lib.types.ints.unsigned;
37 keepLastActiveBoot = lib.mkOption {
38 description = "Number of recent active boot profiles to keep";
39 type = lib.types.ints.unsigned;
42 activeThreshold = lib.mkOption {
44 A system profile that is active (or is either /run/current-system or /run/booted-system)
45 for at least this long (of powered-on machine time) is considered 'active' for
46 the purpose of evaluating the keepLastActive number of profiles. This mechanism is
47 intended to preserve profiles that are in some sense stable, that have served us well,
48 so they don't immediately become gc-elligible when a system hasn't been updated in
49 awhile (so keepLatest won't protect them) generates a bunch of broken profiles (so
50 keepLast won't protect them) while trying to get up to date.
52 This threshold is approximate, see activeMeasurementGranularity.
53 Do not set less than activeMeasurementGranularity!
55 # We admonish the user "Do not set less than activeMeasurementGranularity!" and check
56 # it at runtime rather than verifying this with an assertion at evaluation time because
57 # parsing these durations at evaluation-time requires import-from-derivation, which we
62 activeMeasurementGranularity = lib.mkOption {
64 How often to make a note of the currently-active profiles. This is the useful
65 granularity and minimum value of activeThreshold.
69 keepLatest = lib.mkOption {
71 Keep all profiles younger than this duration (systemd.time format).
72 This control is similar to nix-collect-garbage's --delete-older-than.
77 keepFuture = lib.mkOption {
78 description = "Keep profiles 'ahead' of the current profile (happens after rollback)";
79 type = lib.types.bool;
82 logdir = lib.mkOption {
83 description = "Where to keep liveness logs";
85 default = "/var/log/profile-gc";
89 config = lib.mkIf cfg.enable {
92 assertion = cfg.enable -> config.nix.gc.automatic;
93 message = ''nix.profile-gc.enable requires nix.gc.automatic'';
96 systemd.services.nix-gc.serviceConfig.ExecStartPre = pkgs.writeShellScript "nix-profile-gc" ''
99 if [[ ! -e ${cfg.logdir}/active-system
100 || ! -e ${cfg.logdir}/active-boot
101 || ! -e ${cfg.logdir}/active-profiles ]]
103 echo "Liveness logs not found. Not doing any profile garbage collection." >&2
107 alive_threshold="$(< ${parse-duration cfg.activeThreshold})"
108 alive_loginterval="$(< ${parse-duration cfg.activeMeasurementGranularity})"
109 if (( alive_threshold < alive_loginterval ));then
110 echo "Liveness threshold is too low. Not doing any profile garbage collection." >&2
115 ${pkgs.coreutils}/bin/tac "$1" |
116 ${pkgs.gawk}/bin/awk \
119 --assign threshold="$alive_threshold" \
120 --assign loginterval="$alive_loginterval" \
124 if (++count[val] == int(threshold/loginterval)) {
136 echo "Keeping the last $3 $2 entries from $1:" >&2
137 ${pkgs.gawk}/bin/gawk '{ print " " $0 }' >&2 )
140 declare -A active_targets
142 active_targets[$target]=1
144 verbose_topn ${cfg.logdir}/active-system "" ${escapeShellArg cfg.keepLastActiveSystem}
145 verbose_topn ${cfg.logdir}/active-boot "" ${escapeShellArg cfg.keepLastActiveBoot }
148 now=$(${pkgs.coreutils}/bin/date +%s)
149 age_threshold="$(< ${parse-duration cfg.keepLatest})"
150 while read profile;do
151 echo "Contemplating profiles for $profile:" >&2
156 done < <(verbose_topn ${cfg.logdir}/active-profiles "$profile" ${escapeShellArg cfg.keepLastActive})
157 current=$(${pkgs.coreutils}/bin/readlink "$profile")
158 currentgen=''${current%-link}
159 currentgen=''${currentgen##*-}
160 for p in "$profile"-*-link;do
163 if [[ "$p" != "$profile-$pgen-link" ]];then
164 echo "(Disregarding unrelated profile $p)" >&2
167 if [[ "$p" == "$current" ]];then
168 echo "Keeeping current profile $p" >&2
171 if [[ "''${active_targets[$(${pkgs.coreutils}/bin/readlink "$p")]:-}" ]];then
172 echo "Keeeping active system/boot profile $p" >&2
175 if [[ "''${active[$p]:-}" ]];then
176 echo "Keeeping active profile $p" >&2
179 if (( (now - "$(${pkgs.findutils}/bin/find "$p" -printf %Ts)") < age_threshold/1000000 ));then
180 echo "Keeeping young profile $p" >&2
183 ${lib.optionalString cfg.keepFuture ''
184 if (( pgen > currentgen ));then
185 echo "Keeeping future profile $p" >&2
189 ${if cfg.dryRun then ''
190 echo "Would remove profile $p" >&2
192 echo "Removing profile $p" >&2
196 done < <(${pkgs.findutils}/bin/find ''${NIX_STATE_DIR:-/nix/var/nix}/profiles/ -type l -not -name '*[0-9]-link')
198 systemd.timers.profile-gc-log-active = {
199 wantedBy = [ "timers.target" ];
200 timerConfig.OnActiveSec = cfg.activeMeasurementGranularity;
201 timerConfig.OnUnitActiveSec = cfg.activeMeasurementGranularity;
203 systemd.services.profile-gc-log-active = {
205 "Log the active profiles for gc collection policy evaluation";
206 serviceConfig.Type = "oneshot";
208 ${pkgs.coreutils}/bin/mkdir -p ${cfg.logdir}
209 ${pkgs.coreutils}/bin/readlink /run/current-system >> ${cfg.logdir}/active-system
210 ${pkgs.coreutils}/bin/readlink /run/booted-system >> ${cfg.logdir}/active-boot
211 ${pkgs.findutils}/bin/find ''${NIX_STATE_DIR:-/nix/var/nix}/profiles/ \
212 -type l -not -name '*[0-9]-link' \
213 -exec ${pkgs.stdenv.shell} -c '
216 ${pkgs.coreutils}/bin/readlink "$f"
218 >> ${cfg.logdir}/active-profiles