Skip to content

Commit 3bcfdd3

Browse files
committed
lib.modules.systemd: generate timer unit when startAt is set
When the startAt option is used, NixOS generates a corresponding .timer unit. Include it in the outputs alongside the .service file using symlinkJoin.
1 parent 3219740 commit 3bcfdd3

File tree

2 files changed

+129
-24
lines changed

2 files changed

+129
-24
lines changed

checks/systemd.nix

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,31 @@ let
142142
}
143143
);
144144

145+
# Test 7: startAt generates a timer
146+
withTimer = self.lib.wrapModule (
147+
{
148+
config,
149+
lib,
150+
wlib,
151+
...
152+
}:
153+
{
154+
imports = [ wlib.modules.systemd ];
155+
config = {
156+
pkgs = pkgs;
157+
package = pkgs.hello;
158+
systemd = {
159+
serviceConfig.Type = "oneshot";
160+
startAt = "hourly";
161+
};
162+
};
163+
}
164+
);
165+
145166
readUserService = drv: name: builtins.readFile "${drv}/systemd/user/${name}.service";
146167
readSystemService = drv: name: builtins.readFile "${drv}/systemd/system/${name}.service";
168+
readUserTimer = drv: name: builtins.readFile "${drv}/systemd/user/${name}.timer";
169+
readSystemTimer = drv: name: builtins.readFile "${drv}/systemd/system/${name}.timer";
147170
in
148171
pkgs.runCommand "systemd-test" { } ''
149172
echo "Testing systemd module..."
@@ -213,6 +236,17 @@ pkgs.runCommand "systemd-test" { } ''
213236
echo "$hooks" | grep -q 'ExecStopPost=.*hello-post-stop' || { echo "FAIL: postHook not mapped to ExecStopPost"; echo "$hooks"; exit 1; }
214237
echo "PASS: exePath, extraPackages, preHook, postHook"
215238
239+
# Test 7: startAt generates a timer
240+
echo "Test 7: startAt generates a timer"
241+
timerSvc='${readUserService withTimer.outputs.systemd-user "hello"}'
242+
echo "$timerSvc" | grep -q 'ExecStart=.*/bin/hello' || { echo "FAIL: service missing ExecStart"; echo "$timerSvc"; exit 1; }
243+
timer='${readUserTimer withTimer.outputs.systemd-user "hello"}'
244+
echo "$timer" | grep -q 'OnCalendar=hourly' || { echo "FAIL: timer missing OnCalendar"; echo "$timer"; exit 1; }
245+
echo "$timer" | grep -q 'WantedBy=timers.target' || { echo "FAIL: timer missing WantedBy"; echo "$timer"; exit 1; }
246+
systemTimer='${readSystemTimer withTimer.outputs.systemd-system "hello"}'
247+
echo "$systemTimer" | grep -q 'OnCalendar=hourly' || { echo "FAIL: system timer missing OnCalendar"; echo "$systemTimer"; exit 1; }
248+
echo "PASS: startAt generates a timer"
249+
216250
echo "SUCCESS: All systemd tests passed"
217251
touch $out
218252
''

lib/modules/systemd.nix

Lines changed: 95 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ let
88

99
serviceName = builtins.unsafeDiscardStringContext (config.binName or "unknown");
1010

11-
mkService =
11+
mkNixosEval =
1212
type:
1313
let
1414
nixosPath =
@@ -23,29 +23,77 @@ let
2323
"systemd"
2424
"services"
2525
];
26+
in
27+
import (config.pkgs.path + "/nixos/lib/eval-config.nix") {
28+
inherit lib;
29+
system = null;
30+
modules = [
31+
{ config.nixpkgs.hostPlatform = config.pkgs.stdenv.hostPlatform.system; }
32+
{ config.systemd.globalEnvironment = lib.mkForce { }; }
33+
{ config = lib.setAttrByPath nixosPath { ${serviceName} = cfg; }; }
34+
];
35+
};
2636

27-
nixos = import (config.pkgs.path + "/nixos/lib/eval-config.nix") {
28-
inherit lib;
29-
system = null;
30-
modules = [
31-
{ config.nixpkgs.hostPlatform = config.pkgs.stdenv.hostPlatform.system; }
32-
{ config.systemd.globalEnvironment = lib.mkForce { }; }
33-
{ config = lib.setAttrByPath nixosPath { ${serviceName} = cfg; }; }
34-
];
35-
};
36-
37-
utils = import (config.pkgs.path + "/nixos/lib/utils.nix") {
38-
inherit lib;
39-
inherit (config) pkgs;
40-
inherit (nixos) config;
41-
};
42-
43-
evaluated = lib.getAttrFromPath nixosPath nixos.config;
44-
unit = utils.systemdUtils.lib.serviceToUnit evaluated.${serviceName};
37+
mkUtils =
38+
nixos:
39+
import (config.pkgs.path + "/nixos/lib/utils.nix") {
40+
inherit lib;
41+
inherit (config) pkgs;
42+
inherit (nixos) config;
43+
};
44+
45+
mkOutputs =
46+
type:
47+
let
48+
nixos = mkNixosEval type;
49+
utils = mkUtils nixos;
50+
51+
servicePath =
52+
if type == "user" then
53+
[
54+
"systemd"
55+
"user"
56+
"services"
57+
]
58+
else
59+
[
60+
"systemd"
61+
"services"
62+
];
63+
64+
timerPath =
65+
if type == "user" then
66+
[
67+
"systemd"
68+
"user"
69+
"timers"
70+
]
71+
else
72+
[
73+
"systemd"
74+
"timers"
75+
];
4576

4677
unitDir = if type == "user" then "systemd/user" else "systemd/system";
78+
79+
evaluatedService = (lib.getAttrFromPath servicePath nixos.config).${serviceName};
80+
serviceUnit = utils.systemdUtils.lib.serviceToUnit evaluatedService;
81+
82+
evaluatedTimers = lib.getAttrFromPath timerPath nixos.config;
83+
hasTimer = evaluatedTimers ? ${serviceName};
84+
timerUnit = utils.systemdUtils.lib.timerToUnit evaluatedTimers.${serviceName};
4785
in
48-
config.pkgs.writeTextDir "${unitDir}/${serviceName}.service" unit.text;
86+
{
87+
service = config.pkgs.writeTextDir "${unitDir}/${serviceName}.service" serviceUnit.text;
88+
timer =
89+
if hasTimer then
90+
config.pkgs.writeTextDir "${unitDir}/${serviceName}.timer" timerUnit.text
91+
else
92+
null;
93+
};
94+
95+
userOutputs = mkOutputs "user";
96+
systemOutputs = mkOutputs "system";
4997
in
5098
{
5199
_file = "lib/modules/systemd.nix";
@@ -61,6 +109,9 @@ in
61109
ExecStart, Environment, PATH, preStart and postStop are set from the
62110
wrapper by default.
63111
112+
If startAt is set, a corresponding .timer unit is generated alongside
113+
the service.
114+
64115
The generated unit files are available at outputs.systemd-user and
65116
outputs.systemd-system.
66117
'';
@@ -80,14 +131,34 @@ in
80131
options.outputs.systemd-user = lib.mkOption {
81132
type = lib.types.package;
82133
readOnly = true;
83-
description = "The generated systemd user service file.";
84-
default = mkService "user";
134+
description = "The generated systemd user service file (and timer if startAt is set).";
135+
default =
136+
if userOutputs.timer != null then
137+
config.pkgs.symlinkJoin {
138+
name = "${serviceName}-user-units";
139+
paths = [
140+
userOutputs.service
141+
userOutputs.timer
142+
];
143+
}
144+
else
145+
userOutputs.service;
85146
};
86147

87148
options.outputs.systemd-system = lib.mkOption {
88149
type = lib.types.package;
89150
readOnly = true;
90-
description = "The generated systemd system service file.";
91-
default = mkService "system";
151+
description = "The generated systemd system service file (and timer if startAt is set).";
152+
default =
153+
if systemOutputs.timer != null then
154+
config.pkgs.symlinkJoin {
155+
name = "${serviceName}-system-units";
156+
paths = [
157+
systemOutputs.service
158+
systemOutputs.timer
159+
];
160+
}
161+
else
162+
systemOutputs.service;
92163
};
93164
}

0 commit comments

Comments
 (0)