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.
This commit is contained in:
Lin Yinfeng
2025-12-16 16:53:04 +08:00
parent a317f67419
commit 62ea5b9ae7
4 changed files with 427 additions and 45 deletions

View File

@@ -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}

View File

@@ -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 <https://docs.rs/ignore/latest/ignore/overrides/struct.OverrideBuilder.html#method.add>.
'';
};
};
};
};
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 <https://github.com/linyinfeng/angrr/tree/main?tab=readme-ov-file#nixos-module-usage> 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=<true|false>` 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 "$@"
'';
})
]

View File

@@ -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
'';
}

View File

@@ -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 ];