Skip to content

Commit 8bd1504

Browse files
committed
update-flake-inputs: init
A user service which will run `nix flake update` on every flake input in the specified directories.
1 parent ee7f464 commit 8bd1504

File tree

11 files changed

+653
-0
lines changed

11 files changed

+653
-0
lines changed
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
{
2+
config,
3+
lib,
4+
pkgs,
5+
...
6+
}:
7+
let
8+
inherit (lib)
9+
getExe
10+
literalExpression
11+
maintainers
12+
mkEnableOption
13+
mkIf
14+
mkOption
15+
;
16+
inherit (lib.strings) concatStringsSep escapeShellArgs replaceString;
17+
inherit (lib.types)
18+
bool
19+
listOf
20+
package
21+
str
22+
;
23+
24+
cfg = config.services.${unitName};
25+
26+
unitName = "update-flake-inputs";
27+
in
28+
{
29+
meta.maintainers = [
30+
maintainers.l0b0
31+
];
32+
33+
options.services.${unitName} = {
34+
enable = mkEnableOption "Whether to update Nix flake inputs on a schedule";
35+
36+
directories = mkOption {
37+
type = listOf str;
38+
default = [ ];
39+
example = [
40+
"/home/user/foo"
41+
"/home/user/my projects/bar"
42+
];
43+
description = "Absolute paths of directories to perform updates in";
44+
};
45+
46+
afterUpdateCommands = mkOption {
47+
type = listOf str;
48+
default = [ ];
49+
example = [
50+
"NIX_ABORT_ON_WARN=true nix flake check"
51+
"nix develop --ignore-env --command pre-commit run --all-files"
52+
"git commit --message=\"build: Update Nix flake input '$${input}'\" -- flake.lock"
53+
];
54+
description = ''
55+
Commands to run after the update.
56+
The variable {env}`input` can be referenced in this script to refer to the flake input name,
57+
and the variable {env}`PWD` to refer to the flake directory.
58+
'';
59+
};
60+
61+
afterUpdateCommandsDependencies = mkOption {
62+
type = listOf package;
63+
default = [ ];
64+
example = [
65+
literalExpression
66+
"pkgs.firefox"
67+
];
68+
description = ''
69+
Packages required by {option}`afterUpdateCommands`.
70+
'';
71+
};
72+
73+
onCalendar = mkOption {
74+
type = str;
75+
default = "daily";
76+
example = "04:40";
77+
description = ''
78+
How often or when update occurs.
79+
80+
The format is described in
81+
{manpage}`systemd.time(7)`.
82+
'';
83+
};
84+
85+
randomizedDelaySec = mkOption {
86+
default = "0";
87+
type = str;
88+
example = "45 minutes";
89+
description = ''
90+
Add a randomized delay before each run.
91+
The delay will be chosen between zero and this value.
92+
This value must be a time span in the format specified by
93+
{manpage}`systemd.time(7)`
94+
'';
95+
};
96+
97+
fixedRandomDelay = mkOption {
98+
default = false;
99+
type = bool;
100+
example = true;
101+
description = ''
102+
Make the randomized delay consistent between runs.
103+
This reduces the jitter between automatic updates.
104+
See {option}`randomizedDelaySec` for configuring the randomized delay.
105+
'';
106+
};
107+
108+
persistent = mkOption {
109+
default = true;
110+
type = bool;
111+
example = false;
112+
description = ''
113+
Takes a boolean argument. If true, the time when the service
114+
unit was last triggered is stored on disk. When the timer is
115+
activated, the service unit is triggered immediately if it
116+
would have been triggered at least once during the time when
117+
the timer was inactive. Such triggering is nonetheless
118+
subject to the delay imposed by RandomizedDelaySec=. This is
119+
useful to catch up on missed runs of the service when the
120+
system was powered down.
121+
'';
122+
};
123+
};
124+
125+
config = mkIf cfg.enable {
126+
assertions = [
127+
{
128+
assertion = cfg.directories != [ ];
129+
message = "You must specify some directories to act on.";
130+
}
131+
];
132+
133+
systemd.user =
134+
let
135+
Description = "Update Nix flake inputs";
136+
in
137+
{
138+
services.${unitName} =
139+
let
140+
updateFlakeInputs = pkgs.writeShellApplication {
141+
name = unitName;
142+
bashOptions = [ ];
143+
runtimeInputs = [
144+
pkgs.gitMinimal
145+
pkgs.jq
146+
]
147+
++ cfg.afterUpdateCommandsDependencies;
148+
text =
149+
let
150+
script = builtins.readFile ./update.bash;
151+
afterUpdateCommandLine = concatStringsSep " && " (
152+
if cfg.afterUpdateCommands == [ ] then [ "true" ] else cfg.afterUpdateCommands
153+
);
154+
in
155+
replaceString "@afterUpdateCommandLine@" afterUpdateCommandLine script;
156+
};
157+
in
158+
{
159+
Unit = {
160+
inherit Description;
161+
Documentation = [ "man:nix3-flake(1)" ];
162+
163+
After = [ "network-online.target" ];
164+
Wants = [ "network-online.target" ];
165+
166+
X-StopOnRemoval = false;
167+
X-RestartIfChanged = false;
168+
};
169+
170+
Service = {
171+
ExecStart = ''
172+
${getExe updateFlakeInputs} ${escapeShellArgs cfg.directories}
173+
'';
174+
Type = "oneshot";
175+
};
176+
};
177+
178+
timers.${unitName} = {
179+
Install.WantedBy = [ "timers.target" ];
180+
181+
Timer = {
182+
FixedRandomDelay = cfg.fixedRandomDelay;
183+
OnCalendar = cfg.onCalendar;
184+
Persistent = cfg.persistent;
185+
RandomizedDelaySec = cfg.randomizedDelaySec;
186+
Unit = "${unitName}.service";
187+
};
188+
189+
Unit = { inherit Description; };
190+
};
191+
};
192+
};
193+
194+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
set -o errexit -o noclobber -o nounset -o pipefail
2+
shopt -s failglob inherit_errexit
3+
4+
cleanup() {
5+
git restore --staged flake.lock
6+
git checkout flake.lock
7+
}
8+
9+
afterUpdateCommands() {
10+
@afterUpdateCommandLine@
11+
}
12+
13+
update_flake_input() {
14+
local \
15+
dev_shell_name \
16+
input \
17+
machine_type \
18+
nix_build_command \
19+
nixos_configuration \
20+
raw_dev_shell_names \
21+
raw_nixos_configurations
22+
23+
input="$1"
24+
25+
if ! nix flake update "${input}"; then
26+
echo "${0}: Could not update input ${input} in directory ${PWD}!" >&2
27+
cleanup
28+
return 64
29+
fi
30+
31+
if git diff --quiet flake.lock; then
32+
# Already up to date
33+
return 0
34+
fi
35+
36+
nix_build_command=(nix build --no-link --print-out-paths)
37+
38+
if raw_nixos_configurations="$(
39+
nix eval --apply 'attrSet: builtins.toString (builtins.attrNames attrSet)' --raw \
40+
.#.nixosConfigurations
41+
)"; then
42+
readarray -d ' ' -t nixos_configurations <<<"${raw_nixos_configurations}"
43+
for nixos_configuration in "${nixos_configurations[@]}"; do
44+
nix_build_command+=(".#.nixosConfigurations.${nixos_configuration%%$'\n'}.config.system.build.toplevel")
45+
done
46+
fi
47+
48+
machine_type="$(uname --machine)-linux"
49+
if raw_dev_shell_names="$(
50+
nix eval --apply 'attrSet: builtins.toString (builtins.attrNames attrSet)' --raw \
51+
".#.devShells.${machine_type}"
52+
)"; then
53+
readarray -d ' ' -t dev_shell_names <<<"${raw_dev_shell_names}"
54+
for dev_shell_name in "${dev_shell_names[@]}"; do
55+
nix_build_command+=(".#.devShells.${machine_type}.${dev_shell_name%%$'\n'}")
56+
done
57+
fi
58+
59+
if ! "${nix_build_command[@]}"; then
60+
echo "${0}: Could not update input ${input} in directory ${PWD}!"
61+
cleanup
62+
return 65
63+
fi
64+
65+
# shellcheck disable=SC2310
66+
if ! afterUpdateCommands; then
67+
echo "After update commands failed!" >&2
68+
cleanup
69+
return 66
70+
fi
71+
72+
cleanup
73+
}
74+
75+
for directory; do
76+
cd "${directory}"
77+
78+
if ! git diff --cached --quiet; then
79+
echo "${PWD} has staged changes; skipping!" >&2
80+
exit_code=64
81+
continue
82+
fi
83+
84+
if ! git diff --quiet flake.lock; then
85+
echo "${PWD}/flake.lock has changes; skipping!" >&2
86+
exit_code=65
87+
continue
88+
fi
89+
90+
inputs_raw="$(nix flake metadata --json | jq --raw-output '.locks.nodes.root.inputs | keys[]')"
91+
readarray -t inputs <<<"${inputs_raw}"
92+
93+
broken_inputs=()
94+
for input in "${inputs[@]}"; do
95+
# shellcheck disable=SC2310
96+
if ! update_flake_input "${input}"; then
97+
exit_code="$?"
98+
broken_inputs+=("${input}")
99+
fi
100+
done
101+
102+
# Summarize at the end, to avoid mixing with the rest of the output
103+
if ((${#broken_inputs[@]} != 0)); then
104+
echo "Some flake inputs can't be updated automatically:" >&2
105+
fi
106+
for broken_input in "${broken_inputs[@]}"; do
107+
echo "- ${broken_input}" >&2
108+
done
109+
done
110+
111+
exit "${exit_code-0}"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
services.update-flake-inputs = {
3+
enable = true;
4+
directories = [ "/some/path" ];
5+
};
6+
7+
nmt.script = ''
8+
serviceFile=home-files/.config/systemd/user/update-flake-inputs.service
9+
normalizedServiceFile=$(normalizeStorePaths "$serviceFile")
10+
assertFileContent $normalizedServiceFile ${./expected-basic.service}
11+
12+
assertFileContent home-files/.config/systemd/user/update-flake-inputs.timer ${./expected-basic.timer}
13+
14+
scriptFile="$(
15+
grep '^ExecStart=' "$TESTED/$serviceFile" |
16+
cut --delimiter== --fields=2 |
17+
cut --delimiter=' ' --fields=1
18+
)"
19+
normalizedScriptFile=$(normalizeStorePaths "$scriptFile")
20+
assertFileContent "$normalizedScriptFile" ${./expected-basic.bash}
21+
'';
22+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{ lib, pkgs, ... }:
2+
3+
lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux {
4+
update-flake-inputs-basic = ./basic.nix;
5+
update-flake-inputs-full = ./full.nix;
6+
}

0 commit comments

Comments
 (0)