{ config, lib, pkgs, ... }: let cfg = config.services.angrr; direnvCfg = config.programs.direnv.angrr; toml = pkgs.formats.toml { }; exampleSettings = { temporary-root-policies = { direnv = { path-regex = "/\\.direnv/"; period = "14d"; }; result = { path-regex = "/result[^/]*$"; period = "3d"; }; }; profile-policies = { system = { profile-paths = [ "/nix/var/nix/profiles/system" ]; keep-since = "14d"; keep-latest-n = 5; keep-booted-system = true; keep-current-system = true; keep-n-per-bucket = [ { bucket-window = "1 day"; bucket-amount = 7; } { bucket-window = "1 week"; bucket-amount = 4; } ]; }; user = { enable = false; profile-paths = [ "~/.local/state/nix/profiles/profile" "/nix/var/nix/profiles/per-user/root/profile" ]; keep-since = "1d"; keep-latest-n = 1; keep-booted-system = false; keep-current-system = false; }; }; }; settingsOptions = { freeformType = toml.type; options = { owned-only = lib.mkOption { type = with lib.types; enum [ "auto" "true" "false" ]; default = "auto"; description = '' Only monitors owned symbolic link target of GC roots. - "auto": behaves like true for normal users, false for root. - "true": only monitor GC roots owned by the current user. - "false": monitor all GC roots. ''; }; temporary-root-policies = lib.mkOption { type = with lib.types; attrsOf (submodule temporaryRootPolicyOptions); default = { }; description = '' Policies for temporary GC roots(e.g. result and direnv). ''; }; profile-policies = lib.mkOption { type = with lib.types; attrsOf (submodule profilePolicyOptions); default = { }; description = '' Profile GC root policies. ''; }; touch = { project-globs = lib.mkOption { type = with lib.types; listOf str; default = [ "!.git" ]; description = '' List of glob patterns to include or exclude files when touching GC roots. Only applied when `angrr touch` is invoked with the `--project` flag. Patterns use an inverted gitignore-style semantics. See . ''; }; }; }; }; commonPolicyOptions = { options = { enable = lib.mkEnableOption "this angrr policy" // { default = true; example = false; }; }; }; temporaryRootPolicyOptions = { freeformType = toml.type; imports = [ commonPolicyOptions ]; options = { path-regex = lib.mkOption { type = lib.types.str; description = '' Regex pattern to match the GC root path. ''; }; period = lib.mkOption { type = with lib.types; nullOr str; default = null; description = '' Retention period for the GC roots matched by this policy. ''; }; priority = lib.mkOption { type = lib.types.int; default = 100; description = '' Priority of this policy. Lower number means higher priority, if multiple policies monitor the same path, the one with higher priority will be applied. ''; }; filter = lib.mkOption { type = with lib.types; nullOr (submodule filterOptions); default = null; description = '' External filter program to further filter GC roots matched by this policy. ''; }; ignore-prefixes = lib.mkOption { type = with lib.types; nullOr (listOf str); default = null; description = '' List of path prefixes to ignore. If null is specified, angrr builtin settings will be used. ''; }; ignore-prefixes-in-home = lib.mkOption { type = with lib.types; nullOr (listOf str); default = null; description = '' Path prefixes to ignore under home directory. If null is specified, angrr builtin settings will be used. ''; }; }; }; profilePolicyOptions = { freeformType = toml.type; imports = [ commonPolicyOptions ]; options = { profile-paths = lib.mkOption { type = with lib.types; listOf str; description = '' Paths to the Nix profile. When angrr runs in owned-only mode, and the option begins with `~`, it will be expanded to the home directory of the current user. When angrr does not run in owned-only mode, and the option begins with `~`, it will be expanded to the home of all users discovered respectively. ''; }; keep-since = lib.mkOption { type = with lib.types; nullOr str; default = null; description = '' Retention period for the GC roots in this profile. ''; }; keep-latest-n = lib.mkOption { type = with lib.types; nullOr int; default = null; description = '' Keep the latest N GC roots in this profile. ''; }; keep-current-system = lib.mkOption { type = lib.types.bool; default = false; description = '' Whether to keep the current system generation. Only useful for system profiles. ''; }; keep-booted-system = lib.mkOption { type = lib.types.bool; default = false; description = '' Whether to keep the last booted system generation. Only useful for system profiles. ''; }; keep-n-per-bucket = lib.mkOption { type = with lib.types; listOf (submodule keepNPerBucketOptions); default = [ ]; description = '' Specify a list of rules having n, bucket-window, and bucket-amount attributes. ''; }; }; }; filterOptions = { freeformType = toml.type; options = { program = lib.mkOption { type = lib.types.str; description = '' Path to the external filter program. ''; }; arguments = lib.mkOption { type = with lib.types; listOf str; default = [ ]; description = '' Extra command-line arguments pass to the external filter program. ''; }; }; }; keepNPerBucketOptions = { freeformType = toml.type; options = { n = lib.mkOption { type = lib.types.int; default = 1; description = '' Retain n generations every bucket-window duration for bucket-amount buckets. ''; }; bucket-window = lib.mkOption { type = lib.types.str; description = '' The duration of the bucket window. ''; }; bucket-amount = lib.mkOption { type = lib.types.int; default = 1; description = '' The number of buckets to keep. ''; }; }; }; # toml.generate does not support null values, we need to filter them out first filteredSettings = lib.filterAttrsRecursive (name: value: value != null) cfg.settings; originalConfigFile = toml.generate "angrr.toml" filteredSettings; validatedConfigFile = pkgs.runCommand "angrr-config.toml" { } '' ${lib.getExe cfg.package} validate --config "${originalConfigFile}" > $out ''; configFileMigrationMsg = '' This option has been removed since angrr 0.2.0. Please use `services.angrr.settings` to configure retention policies through configuration file. See for a configuration example. ''; in { meta.maintainers = pkgs.angrr.meta.maintainers; imports = [ (lib.mkRemovedOptionModule [ "services" "angrr" "removeRoot" ] configFileMigrationMsg) (lib.mkRemovedOptionModule [ "services" "angrr" "ownedOnly" ] configFileMigrationMsg) ]; options = { services.angrr = { enable = lib.mkEnableOption "angrr"; package = lib.mkPackageOption pkgs "angrr" { }; logLevel = lib.mkOption { type = with lib.types; enum [ "off" "error" "warn" "info" "debug" "trace" ]; default = "info"; description = '' Set the log level of angrr. ''; }; extraArgs = lib.mkOption { type = with lib.types; listOf str; default = [ ]; description = '' Extra command-line arguments pass to angrr. ''; }; period = lib.mkOption { type = with lib.types; nullOr str; default = null; description = '' If set, it configures {option}`services.angrr.settings` to a preset that monitor .direnv, results, system, and user profiles, retaining GC roots that are younger than the specified period. ''; }; settings = lib.mkOption { type = lib.types.submodule settingsOptions; example = exampleSettings; description = '' Global configuration for angrr in TOML format. ''; }; configFile = lib.mkOption { type = with lib.types; nullOr path; default = validatedConfigFile; defaultText = "TOML file generated from {option}`services.angrr.settings`"; description = '' Path to the angrr configuration file in TOML format. If not set, the configuration generated from {option}`services.angrr.settings` will be used. If specified, {option}`services.angrr.settings` will be ignored. ''; }; enableNixGcIntegration = lib.mkOption { type = lib.types.bool; description = '' Whether to enable nix-gc.service integration. ''; }; timer = { enable = lib.mkEnableOption "angrr timer"; dates = lib.mkOption { type = lib.types.str; default = "03:00"; description = '' How often or when the retention policy is performed. ''; }; }; }; programs.direnv.angrr = { enable = lib.mkEnableOption "angrr direnv integration" // { default = true; example = false; }; autoUse = lib.mkOption { type = lib.types.bool; default = true; example = false; description = '' Whether to automatically use angrr before loading .envrc. ''; }; }; }; config = lib.mkIf cfg.enable ( lib.mkMerge [ { assertions = [ { assertion = cfg.enableNixGcIntegration -> config.nix.gc.automatic; message = "angrr nix-gc.service integration requires `nix.gc.automatic = true`"; } ]; services.angrr.enableNixGcIntegration = lib.mkDefault config.nix.gc.automatic; } { environment.etc."angrr/config.toml".source = cfg.configFile; systemd.services.angrr = { description = "Auto Nix GC Roots Retention"; script = '' ${lib.getExe cfg.package} run \ --log-level "${cfg.logLevel}" \ --no-prompt \ ${lib.escapeShellArgs cfg.extraArgs} ''; environment.ANGRR_LOG_STYLE = "systemd"; serviceConfig = { Type = "oneshot"; }; }; environment.systemPackages = [ cfg.package ]; } (lib.mkIf cfg.timer.enable { systemd.timers.angrr = { timerConfig = { OnCalendar = cfg.timer.dates; }; wantedBy = [ "timers.target" ]; }; }) (lib.mkIf cfg.enableNixGcIntegration { systemd.services.angrr = { wantedBy = [ "nix-gc.service" ]; before = [ "nix-gc.service" ]; }; }) (lib.mkIf (config.programs.direnv.enable && direnvCfg.enable) { environment.etc."direnv/lib/angrr.sh".source = "${cfg.package}/share/direnv/lib/angrr.sh"; programs.direnv.direnvrcExtra = lib.mkIf direnvCfg.autoUse '' _angrr_auto_use "$@" ''; }) # When period is set, configure a preset retention policy # Users can still override settings via services.angrr.settings (lib.mkIf (cfg.period != null) { services.angrr.settings = { temporary-root-policies = { direnv = { path-regex = "/\\.direnv/"; period = cfg.period; }; result = { path-regex = "/result[^/]*$"; period = cfg.period; }; }; profile-policies = { system = { profile-paths = [ "/nix/var/nix/profiles/system" ]; keep-since = cfg.period; keep-booted-system = true; keep-current-system = true; }; user = { profile-paths = [ "~/.local/state/nix/profiles/profile" "/nix/var/nix/profiles/per-user/root/profile" ]; keep-since = cfg.period; }; }; }; }) ] ); }