X-Git-Url: http://git.scottworley.com/nix-profile-gc/blobdiff_plain/10533fbd6bdf5046f36e41c7848dbc6a324eb4cd..565acdea12c86a788031778b96fc5e111ce957b9:/modules/profile-gc.nix?ds=sidebyside diff --git a/modules/profile-gc.nix b/modules/profile-gc.nix index 8e69301..e2f0e3d 100644 --- a/modules/profile-gc.nix +++ b/modules/profile-gc.nix @@ -1,3 +1,18 @@ +# nix-profile-gc: More gently remove old profiles +# Copyright (C) 2022 Scott Worley +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + { lib, config, pkgs, ... }: let inherit (lib) escapeShellArg; @@ -7,14 +22,27 @@ let parsed=$(systemd-analyze timespan ${escapeShellArg duration} | awk '$1 == "μs:" { print $2 }') echo "$parsed" > "$out" ''; + gather_home_profiles = lib.optionalString cfg.manageHomeProfiles '' + while read -r home_dir;do + home_profile_dir="$home_dir/.local/state/nix/profiles" + if [[ -d "$home_profile_dir" ]];then + home_profile_dirs+=( "$home_profile_dir" ) + fi + done < <(${pkgs.coreutils}/bin/cut -d: -f6 /etc/passwd | ${pkgs.coreutils}/bin/sort -u) + ''; in { options = { nix.profile-gc = { enable = lib.mkEnableOption "Automatic profile garbage collection"; + manageHomeProfiles = lib.mkOption { + description = "Manage profiles in users' $HOME/.local/state/nix/profiles/"; + type = lib.types.bool; + default = false; # Will flip to true later + }; dryRun = lib.mkOption { description = "Say what would have been deleted rather than actually deleting profiles"; type = lib.types.bool; - default = true; + default = false; }; keepLast = lib.mkOption { description = '' @@ -49,16 +77,23 @@ in { awhile (so keepLatest won't protect them) generates a bunch of broken profiles (so keepLast won't protect them) while trying to get up to date. - This is approximate and has a useful granularity of an hour - (config.systemd.timers.profile-gc-log-active.timerConfig.OnUnitActiveSec). - Do not set less than this. + This threshold is approximate, see activeMeasurementGranularity. + Do not set less than activeMeasurementGranularity! ''; - # We admonish the user "Do not set less than this." and check it at runtime rather - # than verifying this with an assertion now because parsing these durations at - # configuration-time requires import-from-derivation, which we want to avoid. :( + # We admonish the user "Do not set less than activeMeasurementGranularity!" and check + # it at runtime rather than verifying this with an assertion at evaluation time because + # parsing these durations at evaluation-time requires import-from-derivation, which we + # want to avoid. :( type = lib.types.str; default = "5 days"; }; + activeMeasurementGranularity = lib.mkOption { + description = '' + How often to make a note of the currently-active profiles. This is the useful + granularity and minimum value of activeThreshold. + ''; + default = "1 hour"; + }; keepLatest = lib.mkOption { description = '' Keep all profiles younger than this duration (systemd.time format). @@ -98,7 +133,7 @@ in { fi alive_threshold="$(< ${parse-duration cfg.activeThreshold})" - alive_loginterval="$(< ${parse-duration config.systemd.timers.profile-gc-log-active.timerConfig.OnUnitActiveSec})" + alive_loginterval="$(< ${parse-duration cfg.activeMeasurementGranularity})" if (( alive_threshold < alive_loginterval ));then echo "Liveness threshold is too low. Not doing any profile garbage collection." >&2 exit 0 @@ -131,21 +166,24 @@ in { } declare -A active_targets - while read target;do + while read -r target;do active_targets[$target]=1 done < <( verbose_topn ${cfg.logdir}/active-system "" ${escapeShellArg cfg.keepLastActiveSystem} verbose_topn ${cfg.logdir}/active-boot "" ${escapeShellArg cfg.keepLastActiveBoot } ) + home_profile_dirs=() + ${gather_home_profiles} + now=$(${pkgs.coreutils}/bin/date +%s) age_threshold="$(< ${parse-duration cfg.keepLatest})" - while read profile;do + while read -r profile;do echo "Contemplating profiles for $profile:" >&2 unset active declare -A active - while read p;do - active[$p]=1 + while read -r pname;do + active[$pname]=1 done < <(verbose_topn ${cfg.logdir}/active-profiles "$profile" ${escapeShellArg cfg.keepLastActive}) current=$(${pkgs.coreutils}/bin/readlink "$profile") currentgen=''${current%-link} @@ -157,15 +195,16 @@ in { echo "(Disregarding unrelated profile $p)" >&2 continue fi - if [[ "$p" == "$current" ]];then + pname=$(${pkgs.coreutils}/bin/basename "$p") + if [[ "$pname" == "$current" ]];then echo "Keeeping current profile $p" >&2 continue fi - if [[ "''${active_targets[$(${pkgs.coreutils}/bin/readlink "$p")]}" ]];then + if [[ "''${active_targets[$(${pkgs.coreutils}/bin/readlink "$p")]:-}" ]];then echo "Keeeping active system/boot profile $p" >&2 continue fi - if [[ "''${active[$p]}" ]];then + if [[ "''${active[$pname]:-}" ]];then echo "Keeeping active profile $p" >&2 continue fi @@ -186,21 +225,25 @@ in { rm "$p" ''} done - done < <(${pkgs.findutils}/bin/find ''${NIX_STATE_DIR:-/nix/var/nix}/profiles/ -type l -not -name '*[0-9]-link') + done < <(${pkgs.findutils}/bin/find "''${NIX_STATE_DIR:-/nix/var/nix}/profiles/" "''${home_profile_dirs[@]}" -type l -not -name '*[0-9]-link') ''; systemd.timers.profile-gc-log-active = { wantedBy = [ "timers.target" ]; - timerConfig.OnUnitActiveSec = "1 hour"; + timerConfig.OnActiveSec = cfg.activeMeasurementGranularity; + timerConfig.OnUnitActiveSec = cfg.activeMeasurementGranularity; }; systemd.services.profile-gc-log-active = { description = "Log the active profiles for gc collection policy evaluation"; serviceConfig.Type = "oneshot"; script = '' + home_profile_dirs=() + ${gather_home_profiles} + ${pkgs.coreutils}/bin/mkdir -p ${cfg.logdir} ${pkgs.coreutils}/bin/readlink /run/current-system >> ${cfg.logdir}/active-system ${pkgs.coreutils}/bin/readlink /run/booted-system >> ${cfg.logdir}/active-boot - ${pkgs.findutils}/bin/find ''${NIX_STATE_DIR:-/nix/var/nix}/profiles/ \ + ${pkgs.findutils}/bin/find ''${NIX_STATE_DIR:-/nix/var/nix}/profiles/ "''${home_profile_dirs[@]}" \ -type l -not -name '*[0-9]-link' \ -exec ${pkgs.stdenv.shell} -c ' for f;do