angrr: 0.1.5 -> 0.2.0 (#471312)

This commit is contained in:
Aleksana
2025-12-16 15:02:16 +00:00
committed by GitHub
4 changed files with 427 additions and 45 deletions
@@ -55,6 +55,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}
+254 -29
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 "$@"
'';
})
]
+162 -10
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
'';
}
+9 -6
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 ];