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 is approximate and has a useful granularity of an hour
53 (config.systemd.timers.profile-gc-log-active.timerConfig.OnUnitActiveSec).
54 Do not set less than this.
56 # We admonish the user "Do not set less than this." and check it at runtime rather
57 # than verifying this with an assertion now because parsing these durations at
58 # configuration-time requires import-from-derivation, which we want to avoid. :(
62 keepLatest = lib.mkOption {
64 Keep all profiles younger than this duration (systemd.time format).
65 This control is similar to nix-collect-garbage's --delete-older-than.
70 keepFuture = lib.mkOption {
71 description = "Keep profiles 'ahead' of the current profile (happens after rollback)";
72 type = lib.types.bool;
75 logdir = lib.mkOption {
76 description = "Where to keep liveness logs";
78 default = "/var/log/profile-gc";
82 config = lib.mkIf cfg.enable {
85 assertion = cfg.enable -> config.nix.gc.automatic;
86 message = ''nix.profile-gc.enable requires nix.gc.automatic'';
89 systemd.services.nix-gc.serviceConfig.ExecStartPre = pkgs.writeShellScript "nix-profile-gc" ''
92 if [[ ! -e ${cfg.logdir}/active-system
93 || ! -e ${cfg.logdir}/active-boot
94 || ! -e ${cfg.logdir}/active-profiles ]]
96 echo "Liveness logs not found. Not doing any profile garbage collection." >&2
100 alive_threshold="$(< ${parse-duration cfg.activeThreshold})"
101 alive_loginterval="$(< ${parse-duration config.systemd.timers.profile-gc-log-active.timerConfig.OnUnitActiveSec})"
102 if (( alive_threshold < alive_loginterval ));then
103 echo "Liveness threshold is too low. Not doing any profile garbage collection." >&2
108 ${pkgs.coreutils}/bin/tac "$1" |
109 ${pkgs.gawk}/bin/awk \
112 --assign threshold="$alive_threshold" \
113 --assign loginterval="$alive_loginterval" \
117 if (++count[val] == int(threshold/loginterval)) {
129 echo "Keeping the last $3 $2 entries from $1:" >&2
130 ${pkgs.gawk}/bin/gawk '{ print " " $0 }' >&2 )
133 declare -A active_targets
135 active_targets[$target]=1
137 verbose_topn ${cfg.logdir}/active-system "" ${escapeShellArg cfg.keepLastActiveSystem}
138 verbose_topn ${cfg.logdir}/active-boot "" ${escapeShellArg cfg.keepLastActiveBoot }
141 now=$(${pkgs.coreutils}/bin/date +%s)
142 age_threshold="$(< ${parse-duration cfg.keepLatest})"
143 while read profile;do
144 echo "Contemplating profiles for $profile:" >&2
149 done < <(verbose_topn ${cfg.logdir}/active-profiles "$profile" ${escapeShellArg cfg.keepLastActive})
150 current=$(${pkgs.coreutils}/bin/readlink "$profile")
151 currentgen=''${current%-link}
152 currentgen=''${currentgen##*-}
153 for p in "$profile"-*-link;do
156 if [[ "$p" != "$profile-$pgen-link" ]];then
157 echo "(Disregarding unrelated profile $p)" >&2
160 if [[ "$p" == "$current" ]];then
161 echo "Keeeping current profile $p" >&2
164 if [[ "''${active_targets[$(${pkgs.coreutils}/bin/readlink "$p")]}" ]];then
165 echo "Keeeping active system/boot profile $p" >&2
168 if [[ "''${active[$p]}" ]];then
169 echo "Keeeping active profile $p" >&2
172 if (( (now - "$(${pkgs.findutils}/bin/find "$p" -printf %Ts)") < age_threshold/1000000 ));then
173 echo "Keeeping young profile $p" >&2
176 ${lib.optionalString cfg.keepFuture ''
177 if (( pgen > currentgen ));then
178 echo "Keeeping future profile $p" >&2
182 ${if cfg.dryRun then ''
183 echo "Would remove profile $p" >&2
185 echo "Removing profile $p" >&2
189 done < <(${pkgs.findutils}/bin/find ''${NIX_STATE_DIR:-/nix/var/nix}/profiles/ -type l -not -name '*[0-9]-link')
191 systemd.timers.profile-gc-log-active = {
192 wantedBy = [ "timers.target" ];
193 timerConfig.OnUnitActiveSec = "1 hour";
195 systemd.services.profile-gc-log-active = {
197 "Log the active profiles for gc collection policy evaluation";
198 serviceConfig.Type = "oneshot";
200 ${pkgs.coreutils}/bin/mkdir -p ${cfg.logdir}
201 ${pkgs.coreutils}/bin/readlink /run/current-system >> ${cfg.logdir}/active-system
202 ${pkgs.coreutils}/bin/readlink /run/booted-system >> ${cfg.logdir}/active-boot
203 ${pkgs.findutils}/bin/find ''${NIX_STATE_DIR:-/nix/var/nix}/profiles/ \
204 -type l -not -name '*[0-9]-link' \
205 -exec ${pkgs.stdenv.shell} -c '
208 ${pkgs.coreutils}/bin/readlink "$f"
210 >> ${cfg.logdir}/active-profiles