From 62ea5b9ae7d4329475089f60232ee1426d9547a9 Mon Sep 17 00:00:00 2001 From: Lin Yinfeng Date: Tue, 16 Dec 2025 16:53:04 +0800 Subject: [PATCH] angrr: 0.1.5 -> 0.2.0 angrr now uses TOML configuration, and also adds the ability to define profile policies. 1. Update the package itself. 2. Update the NixOS module to create, validate, and install config file. 3. Update the NixOS test of angrr to test new functionalities. --- .../manual/release-notes/rl-2605.section.md | 2 + nixos/modules/services/misc/angrr.nix | 283 ++++++++++++++++-- nixos/tests/angrr.nix | 172 ++++++++++- pkgs/by-name/an/angrr/package.nix | 15 +- 4 files changed, 427 insertions(+), 45 deletions(-) diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index cd40062aca34..8069df205252 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -53,6 +53,8 @@ of pulling the upstream container image from Docker Hub. If you want the old beh - The Bash implementation of the `nixos-rebuild` program is removed. All switchable systems now use the Python rewrite. Any prior usage of `system.rebuild.enableNg` must now be removed. If you have any outstanding issues with the new implementation, please open an issue on GitHub. +- `services.angrr` now uses TOML for configuration. Define policies with `services.angrr.settings` (generate TOML file) or point to a file using `services.angrr.configFile`. The legacy options `services.angrr.period`, `services.angrr.ownedOnly`, and `services.angrr.removeRoot` have been removed. See `man 5 angrr` and the description of `services.angrr.settings` options for examples and details. + - `services.pingvin-share` has been removed as the `pingvin-share.backend` package was broken and the project was archived upstream. ## Other Notable Changes {#sec-release-26.05-notable-changes} diff --git a/nixos/modules/services/misc/angrr.nix b/nixos/modules/services/misc/angrr.nix index c2ed4348a02d..92e371efdf2d 100644 --- a/nixos/modules/services/misc/angrr.nix +++ b/nixos/modules/services/misc/angrr.nix @@ -8,36 +8,242 @@ 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; + }; + 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. + ''; + }; + }; + }; + 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. + ''; + }; + }; + }; + + # 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" "period" ] configFileMigrationMsg) + (lib.mkRemovedOptionModule [ "services" "angrr" "removeRoot" ] configFileMigrationMsg) + (lib.mkRemovedOptionModule [ "services" "angrr" "ownedOnly" ] configFileMigrationMsg) + ]; options = { services.angrr = { enable = lib.mkEnableOption "angrr"; package = lib.mkPackageOption pkgs "angrr" { }; - period = lib.mkOption { - type = lib.types.str; - default = "7d"; - example = "2weeks"; - description = '' - The retention period of auto GC roots. - ''; - }; - removeRoot = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Whether to pass the `--remove-root` option to angrr. - ''; - }; - ownedOnly = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Control the `--remove-root=` option of angrr. - ''; - apply = b: if b then "true" else "false"; - }; logLevel = lib.mkOption { type = with lib.types; @@ -61,10 +267,28 @@ in Extra command-line arguments pass to angrr. ''; }; + 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 + Whether to enable nix-gc.service integration. ''; }; timer = { @@ -107,16 +331,17 @@ in } { + 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}" \ - --period "${cfg.period}" \ - ${lib.optionalString cfg.removeRoot "--remove-root"} \ - --owned-only="${cfg.ownedOnly}" \ - --no-prompt ${lib.escapeShellArgs cfg.extraArgs} + --no-prompt \ + ${lib.escapeShellArgs cfg.extraArgs} ''; + environment.ANGRR_LOG_STYLE = "systemd"; serviceConfig = { Type = "oneshot"; }; @@ -144,7 +369,7 @@ in (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 '' - use angrr + _angrr_auto_use "$@" ''; }) ] diff --git a/nixos/tests/angrr.nix b/nixos/tests/angrr.nix index 2c2baefd8e86..300f3643b0ba 100644 --- a/nixos/tests/angrr.nix +++ b/nixos/tests/angrr.nix @@ -1,11 +1,52 @@ -{ ... }: +{ pkgs, ... }: +let + drvForTest = + name: + pkgs.runCommand "angrr-test-${name}" { } '' + mkdir --parents "$out" + echo "${name}" >"$out/${name}" + ''; +in { name = "angrr"; nodes = { machine = { services.angrr = { enable = true; - period = "7d"; + settings = { + temporary-root-policies = { + result = { + path-regex = "/result[^/]*$"; + period = "7d"; + }; + direnv = { + path-regex = "/\\.direnv/"; + period = "14d"; + }; + }; + profile-policies = { + system = { + profile-paths = [ "/nix/var/nix/profiles/system" ]; + keep-since = "7d"; # do not keep based on time + keep-latest-n = 2; # keep latest + keep-current-system = true; + keep-booted-system = true; + }; + user = { + profile-paths = [ + "~/.local/state/nix/profiles/profile" + "/nix/var/nix/profiles/per-user/root/profile" + ]; + # keep-since = "0d"; # do not keep based on time + keep-latest-n = 2; + }; + }; + touch = { + project-globs = [ + "!result-glob-ignored" + ]; + }; + }; }; # `angrr.service` integrates to `nix-gc.service` by default nix.gc.automatic = true; @@ -19,7 +60,23 @@ # Test direnv integration programs.direnv.enable = true; # Verbose logging for angrr in direnv - environment.variables.ANGRR_DIRENV_LOG = "angrr=debug"; + environment.variables.ANGRR_DIRENV_LOG = "debug"; + + # Add some store paths to machine for test + environment.etc."drvs-for-test".text = '' + ${drvForTest "drv1"} + ${drvForTest "drv2"} + ${drvForTest "drv3"} + ${drvForTest "drv4"} + ${drvForTest "drv5"} + ${drvForTest "drv6"} + ${drvForTest "drv7"} + ${drvForTest "drv8"} + ${drvForTest "fake-booted-system"} + ''; + + # Unit start limit workaround + systemd.services.angrr.unitConfig.StartLimitBurst = 10; }; }; @@ -51,7 +108,7 @@ machine.succeed("touch /tmp/result-root-auto-gc-root-2 --no-dereference") machine.succeed("touch /tmp/result-user-auto-gc-root-2 --no-dereference") - machine.systemctl("start nix-gc.service") + machine.systemctl("start angrr.service") # Only GC roots `-1` are removed machine.succeed("test ! -e /tmp/result-root-auto-gc-root-1") machine.succeed("readlink /tmp/result-root-auto-gc-root-2") @@ -60,7 +117,7 @@ # Change time again machine.succeed("date -s '8 days'") - machine.systemctl("start nix-gc.service") + machine.systemctl("start angrr.service") # All auto GC roots are removed machine.succeed("test ! -e /tmp/result-root-auto-gc-root-2") machine.succeed("test ! -e /tmp/result-user-auto-gc-root-2") @@ -69,20 +126,115 @@ machine.succeed("mkdir /tmp/test-direnv") machine.succeed("echo >/tmp/test-direnv/.envrc") # Simply create an empty .envrc machine.succeed("nix build /run/current-system --out-link /tmp/test-direnv/.direnv/gc-root") + machine.succeed("nix build /run/current-system --out-link /tmp/test-direnv/result") machine.succeed("cd /tmp/test-direnv; direnv allow; direnv exec . true") # The root will be removed if we does not use the direnv recently - machine.succeed("date -s '8 days'") - machine.systemctl("start nix-gc.service") + machine.succeed("date -s '15 days'") + machine.systemctl("start angrr.service") machine.succeed("test ! -e /tmp/test-direnv/.direnv/gc-root") + machine.succeed("test ! -e /tmp/test-direnv/result") # Recreate the root machine.succeed("nix build /run/current-system --out-link /tmp/test-direnv/.direnv/gc-root") + machine.succeed("nix build /run/current-system --out-link /tmp/test-direnv/result") + machine.succeed("nix build /run/current-system --out-link /tmp/test-direnv/result-glob-ignored") + machine.succeed("nix build /run/current-system --out-link /tmp/test-outside-direnv/result") # The root will not be remove if we use the direnv recently + machine.succeed("date -s '15 days'") + # test the case that $PWD is different from project root + machine.succeed("cd /tmp; direnv exec /tmp/test-direnv true") + machine.systemctl("start angrr.service") + machine.succeed("readlink /tmp/test-direnv/.direnv/gc-root") + machine.succeed("readlink /tmp/test-direnv/result") + machine.succeed("test ! -e /tmp/test-direnv/result-glob-ignored") + machine.succeed("test ! -e /tmp/test-outside-direnv/result") + + # System profile policy test + # Create a profile for test + machine.succeed("mkdir /tmp/profile-test") + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set ${drvForTest "drv1"}") # generation 1 + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set ${drvForTest "drv2"}") # generation 2 + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set ${drvForTest "drv3"}") # generation 3 + machine.succeed("ln --symbolic --force --no-dereference ${drvForTest "fake-booted-system"} /run/booted-system") + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set /run/booted-system") # generation 4 + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set /run/current-system") # generation 5 machine.succeed("date -s '8 days'") - machine.succeed("cd /tmp/test-direnv; direnv exec . true") - machine.systemctl("start nix-gc.service") - machine.succeed("readlink /tmp/test-direnv/.direnv/gc-root") + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set ${drvForTest "drv4"}") # generation 6 + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set ${drvForTest "drv5"}") # generation 7 + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set ${drvForTest "drv6"}") # generation 8 + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set ${drvForTest "drv7"}") # generation 9 + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set ${drvForTest "drv8"}") # generation 10 + # Rollback to generation 2 to simulate current system + for _ in range(0, 10 - 2): + machine.succeed("nix-env --rollback --profile /nix/var/nix/profiles/system") + + # Run policy + machine.systemctl("start angrr.service") + + # Test + machine.succeed("sh -c 'test $(readlink /nix/var/nix/profiles/system) = system-2-link'") + machine.succeed("test ! -e /nix/var/nix/profiles/system-1-link") + machine.succeed("readlink /nix/var/nix/profiles/system-2-link") # Keep since it is current generation + machine.succeed("test ! -e /nix/var/nix/profiles/system-3-link") + machine.succeed("readlink /nix/var/nix/profiles/system-4-link") # Keep by keep-booted-system + machine.succeed("readlink /nix/var/nix/profiles/system-5-link") # Keep by keep-current-system + machine.succeed("readlink /nix/var/nix/profiles/system-6-link") # Keep by keep-since + machine.succeed("readlink /nix/var/nix/profiles/system-7-link") # Keep by keep-since + machine.succeed("readlink /nix/var/nix/profiles/system-8-link") # Keep by keep-since + machine.succeed("readlink /nix/var/nix/profiles/system-9-link") # Keep by keep-latest-n + machine.succeed("readlink /nix/var/nix/profiles/system-10-link") # Keep by keep-latest-n + + # User profile policy test 1 + # Normal user + machine.succeed("su normal --command 'nix profile add ${drvForTest "drv1"}'") + machine.succeed("su normal --command 'nix profile add ${drvForTest "drv2"}'") + machine.succeed("su normal --command 'nix profile add ${drvForTest "drv3"}'") + # Root user + machine.succeed("nix profile add ${drvForTest "drv1"}") + machine.succeed("nix profile add ${drvForTest "drv2"}") + machine.succeed("nix profile add ${drvForTest "drv3"}") + + # Run policy + machine.systemctl("start angrr.service") + + # Test + machine.succeed("sh -c 'test $(readlink ~normal/.local/state/nix/profiles/profile) = profile-3-link'") + machine.succeed("test ! -e ~normal/.local/state/nix/profiles/profile-1-link") + machine.succeed("readlink ~normal/.local/state/nix/profiles/profile-2-link") # Keep by keep-latest-n + machine.succeed("readlink ~normal/.local/state/nix/profiles/profile-3-link") # Keep since it is current generation + machine.succeed("sh -c 'test $(readlink /nix/var/nix/profiles/per-user/root/profile) = profile-3-link'") + machine.succeed("test ! -e /nix/var/nix/profiles/per-user/root/profile-1-link") + machine.succeed("readlink /nix/var/nix/profiles/per-user/root/profile-2-link") # Keep by keep-latest-n + machine.succeed("readlink /nix/var/nix/profiles/per-user/root/profile-3-link") # Keep since it is current generation + + # User profile policy test 2 + # Create GC roots again + machine.succeed("su normal --command 'nix profile add ${drvForTest "drv1"}'") + machine.succeed("su normal --command 'nix profile add ${drvForTest "drv2"}'") + machine.succeed("su normal --command 'nix profile add ${drvForTest "drv3"}'") + machine.succeed("nix profile add ${drvForTest "drv1"}") + machine.succeed("nix profile add ${drvForTest "drv2"}") + machine.succeed("nix profile add ${drvForTest "drv3"}") + + # Run policy in owned-only mode as normal user + machine.succeed("su normal --command 'angrr run --no-prompt'") + + # Test + machine.succeed("sh -c 'test $(readlink ~normal/.local/state/nix/profiles/profile) = profile-6-link'") + machine.succeed("test ! -e ~normal/.local/state/nix/profiles/profile-1-link") + machine.succeed("test ! -e ~normal/.local/state/nix/profiles/profile-2-link") + machine.succeed("test ! -e ~normal/.local/state/nix/profiles/profile-3-link") + machine.succeed("test ! -e ~normal/.local/state/nix/profiles/profile-4-link") + machine.succeed("readlink ~normal/.local/state/nix/profiles/profile-5-link") # Keep by keep-latest-n + machine.succeed("readlink ~normal/.local/state/nix/profiles/profile-6-link") # Keep since it is current generation + machine.succeed("sh -c 'test $(readlink /nix/var/nix/profiles/per-user/root/profile) = profile-6-link'") + machine.succeed("test ! -e /nix/var/nix/profiles/per-user/root/profile-1-link") + machine.succeed("readlink /nix/var/nix/profiles/per-user/root/profile-2-link") + machine.succeed("readlink /nix/var/nix/profiles/per-user/root/profile-3-link") + machine.succeed("readlink /nix/var/nix/profiles/per-user/root/profile-4-link") # Not monitored + machine.succeed("readlink /nix/var/nix/profiles/per-user/root/profile-5-link") # Not monitored + machine.succeed("readlink /nix/var/nix/profiles/per-user/root/profile-6-link") # Not monitored ''; } diff --git a/pkgs/by-name/an/angrr/package.nix b/pkgs/by-name/an/angrr/package.nix index 36ec20e30d36..8c1d4c2c4367 100644 --- a/pkgs/by-name/an/angrr/package.nix +++ b/pkgs/by-name/an/angrr/package.nix @@ -1,30 +1,33 @@ { lib, - stdenv, rustPlatform, fetchFromGitHub, installShellFiles, nixosTests, testers, nix-update-script, + go-md2man, }: rustPlatform.buildRustPackage (finalAttrs: { pname = "angrr"; - version = "0.1.5"; + version = "0.2.0"; src = fetchFromGitHub { owner = "linyinfeng"; repo = "angrr"; tag = "v${finalAttrs.version}"; - hash = "sha256-PT3oCNPRvEroyVNiICeO0hSHDzKUC6KcP9HnIw1kMQE="; + hash = "sha256-Z+B0MO5ZoPJveO571mlzNVedBEac7P4RE7Cq8e/9bJk="; }; - cargoHash = "sha256-lDOH4Ceap69fX6VWbgQoQfmYWZI+jPE0LJiXmqrTRn8="; + cargoHash = "sha256-j36vyfIP63Qmd55vaVb9buqrCItXwFalelzU8BlKm9s="; buildAndTestSubdir = "angrr"; - nativeBuildInputs = [ installShellFiles ]; + nativeBuildInputs = [ + go-md2man + installShellFiles + ]; postBuild = '' mkdir --parents build/{man-pages,shell-completions} cargo xtask man-pages --out build/man-pages @@ -50,7 +53,7 @@ rustPlatform.buildRustPackage (finalAttrs: { }; meta = { - description = "Temporary GC Roots Cleaner"; + description = "Auto Nix GC Root Retention"; homepage = "https://github.com/linyinfeng/angrr"; license = [ lib.licenses.mit ]; maintainers = with lib.maintainers; [ yinfeng ];