Files
home-manager/modules/programs/nh.nix
Austin Horstman 02618d94f8 nh: use systemd escaping for cleanup argument lists
Use the shared helper to preserve argument boundaries in ExecStart, including embedded newlines. Keep legacy strings and Darwin behavior unchanged, and cover the newline case in a Linux regression test.
2026-09-12 22:02:25 -05:00

190 lines
5.5 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.programs.nh;
in
{
meta.maintainers = with lib.maintainers; [
johnrtitor
a-jay98
];
options.programs.nh = {
enable = lib.mkEnableOption "nh, yet another Nix CLI helper";
package = lib.mkPackageOption pkgs "nh" { };
flake = lib.mkOption {
type = with lib.types; nullOr (either singleLineStr path);
default = null;
description = ''
The path that will be used for the {env}`FLAKE` environment variable.
{env}`FLAKE` is used by nh as the default flake for performing actions,
like {command}`nh os switch`.
'';
};
osFlake = lib.mkOption {
type = with lib.types; nullOr (either singleLineStr path);
default = null;
description = ''
The string that will be used for the {env}`NH_OS_FLAKE` environment variable.
{env}`NH_OS_FLAKE` is used by nh as the default flake for performing {command}`nh os`
actions, such as {command}`nh os switch`.
Setting this will take priority over the `flake` option.
'';
};
homeFlake = lib.mkOption {
type = with lib.types; nullOr (either singleLineStr path);
default = null;
description = ''
The string that will be used for the {env}`NH_HOME_FLAKE` environment variable.
{env}`NH_HOME_FLAKE` is used by nh as the default flake for performing {command}`nh home`
actions, such as {command}`nh home switch`.
Setting this will take priority over the `flake` option.
'';
};
darwinFlake = lib.mkOption {
type = with lib.types; nullOr (either singleLineStr path);
default = null;
description = ''
The string that will be used for the {env}`NH_DARWIN_FLAKE` environment variable.
{env}`NH_DARWIN_FLAKE` is used by nh as the default flake for performing
{command}`nh darwin` actions, such as {command}`nh darwin switch`.
Setting this will take priority over the `flake` option.
'';
};
clean = {
enable = lib.mkEnableOption ''
periodic garbage collection for user profile and nix store with nh clean
user'';
dates = lib.mkOption {
type = lib.types.singleLineStr;
default = "weekly";
description = ''
How often cleanup is performed.
On linux the format is described in {manpage}`systemd.time(7)`.
${lib.hm.darwin.intervalDocumentation}
'';
};
extraArgs = lib.mkOption {
type = with lib.types; either singleLineStr (listOf str);
default = "";
example = [
"--keep"
"5"
"--keep-since"
"3d"
];
description = ''
Options given to nh clean when the service is run automatically.
A list is preferred, each element being one argument.
A string keeps its platform-specific meaning: on Linux it is command
line text that systemd splits into arguments, on Darwin it is passed
to nh as a single argument.
See `nh clean all --help` for more information.
'';
};
};
};
config = {
warnings =
lib.optional (cfg.clean.enable && config.nix.gc.automatic)
"programs.nh.clean.enable and nix.gc.automatic (Home-Manager) are both enabled. Please use one or the other to avoid conflict.";
assertions = [
(lib.hm.darwin.assertInterval "programs.nh.clean.dates" cfg.clean.dates pkgs)
];
home = lib.mkIf cfg.enable {
packages = [ cfg.package ];
sessionVariables = lib.mkMerge [
(lib.mkIf (cfg.flake != null) (
let
packageVersion = lib.getVersion cfg.package;
isVersion4OrHigher = lib.versionAtLeast packageVersion "4.0.0";
in
if isVersion4OrHigher then { NH_FLAKE = cfg.flake; } else { FLAKE = cfg.flake; }
))
(lib.mkMerge (
map
(
name:
(lib.mkIf (cfg."${name}Flake" != null) {
"NH_${lib.toUpper name}_FLAKE" = cfg."${name}Flake";
})
)
[
"darwin"
"home"
"os"
]
))
];
};
systemd.user = lib.mkIf cfg.clean.enable {
services.nh-clean = {
Unit.Description = "Nh clean (user)";
Service = {
Type = "oneshot";
ExecStart =
let
extraArgs =
if builtins.isList cfg.clean.extraArgs then
lib.hm.strings.escapeSystemdExecArgs cfg.clean.extraArgs
else
cfg.clean.extraArgs;
in
"${lib.getExe cfg.package} clean user" + lib.optionalString (extraArgs != "") " ${extraArgs}";
};
};
timers.nh-clean = {
Unit.Description = "Run nh clean";
Timer = {
OnCalendar = cfg.clean.dates;
Persistent = true;
};
Install.WantedBy = [ "timers.target" ];
};
};
launchd.agents.nh-clean = lib.mkIf cfg.clean.enable {
enable = true;
config = {
ProgramArguments = [
"${lib.getExe cfg.package}"
"clean"
"user"
]
++ (
if builtins.isList cfg.clean.extraArgs then
cfg.clean.extraArgs
else
lib.optional (cfg.clean.extraArgs != "") cfg.clean.extraArgs
);
StartCalendarInterval = lib.hm.darwin.mkCalendarInterval cfg.clean.dates;
};
};
};
}