1 { config, lib, pkgs, ... }:
4 cfg = config.system.autoUpgradeWithPinch;
5 pull-repo-script = pkgs.writeShellScript "pull-repo" ''
12 ${pkgs.jq}/bin/jq -r ".$1" <<< "$config"
15 echo Pulling in "$path" >&2
17 if [[ ! -e "$path" ]];then
19 ${pkgs.git}/bin/git init "$d"
20 ${pkgs.git}/bin/git -C "$d" checkout -b "$(prop localBranch)"
21 ${pkgs.git}/bin/git -C "$d" remote add "$(prop remoteName)" "$(prop url)"
22 ${pkgs.git}/bin/git -C "$d" branch -u "$(prop remoteBranch)"
23 mkdir -p "$(${pkgs.coreutils}/bin/dirname "$path")"
29 if [[ "$(${pkgs.git}/bin/git remote get-url "$(prop remoteName)")" != "$(prop url)" ]]; then
30 echo Expected git remote "$(prop remoteName)" to point at "$(prop url)" \
31 but it points at "$(${pkgs.git}/bin/git remote get-url "$(prop remoteName)")" >&2
32 case "$(prop onRemoteURLMismatch)" in
34 update) echo Updating it >&2
35 ${pkgs.git}/bin/git -C "$d" remote set-url "$(prop remoteName)" "$(prop url)";;
39 ${pkgs.git}/bin/git fetch "$(prop remoteName)" "$(prop remoteBranch)"
41 if [[ "$(${pkgs.git}/bin/git rev-parse --abbrev-ref HEAD)" != "$(prop localBranch)" ]];then
42 echo Could not merge because currently-checked-out \
43 \""$(${pkgs.git}/bin/git rev-parse --abbrev-ref HEAD)"\" is not \
44 \""$(prop localBranch)"\"
45 case "$(prop onBranchMismatch)" in
51 if [[ "$(prop requireSignature)" == true ]]; then
52 ${pkgs.polite-merge}/bin/polite-merge \
53 -c gpg.program=${escapeShellArg (pkgs.keyedgpg cfg.signingKeys)} \
54 merge --ff-only --verify-signatures
56 ${pkgs.polite-merge}/bin/polite-merge merge --ff-only
60 auto-upgrade-script = pkgs.writeShellScript "auto-upgrade" ''
61 ${pkgs.utillinux}/bin/flock /run/auto-upgrade-with-pinch ${
62 pkgs.writeShellScript "auto-upgrade-with-lock-held" ''
67 if [[ "$1" == --dry-run ]];then
69 pinch_args=( --dry-run )
86 d=$(${pkgs.coreutils}/bin/mktemp -d)
90 ${pkgs.coreutils}/bin/rm -r "$d"
93 ${optionalString (cfg.upgradeConfigOwnershipPolicy != "any") (''
95 if [[ "$1" != /* ]];then
96 die "Unexpected relative path: $1"
98 if [[ "$1" != / ]];then
99 verify_ownership "$(${pkgs.coreutils}/bin/dirname "$1")"
101 if [[ ! -e "$1" ]];then
102 die "Could not find upgrade config: $1 does not exist"
104 if [[ -h "$1" ]];then
106 ${pkgs.coreutils}/bin/realpath --no-symlinks \
107 "$(${pkgs.coreutils}/bin/dirname "$1")/$(${pkgs.coreutils}/bin/readlink "$1")"
110 perms="$(${pkgs.findutils}/bin/find "$1" -maxdepth 0 -printf "%M")"
111 if [[ "$perms" == d*t ]];then
112 die "Will not use upgrade config in sticky directory $1"
114 owner=$(${pkgs.findutils}/bin/find "$1" -maxdepth 0 -printf "%u")
115 if [[ "$owner" != root ]];then
116 die "Will not use upgrade config not owned by root in $1"
118 if [[ "$perms" == l* ]];then
119 return 0 # Root-owned symlinks are fine
121 if [[ "$perms" == *w? ]];then
122 die "Will not use world-writable upgrade config in $1"
127 if [[ "$perms" == *w???? ]];then
128 die "Will not use group-writable upgrade config in $1"
132 if [[ "$perms" == *w???? ]];then
133 group=$(${pkgs.findutils}/bin/find "$1" -maxdepth 0 -printf "%g")
134 if [[ "$group" != wheel ]];then
135 die "Will not use non-wheel-group group-writable upgrade config in $1"
139 }."${cfg.upgradeConfigOwnershipPolicy}"
143 + concatMapStringsSep "\n" (f: "verify_ownership ${escapeShellArg f}")
146 config=$(${pkgs.nix}/bin/nix eval --json -f ${../upgrade-config.nix} \
147 --arg upgradeConfig ${
149 + lib.concatMapStringsSep " " lib.strings.escapeNixString
150 cfg.upgradeConfig + "]")
154 ${pkgs.jq}/bin/jq -r "$@" <<< "$config"
158 config_query --arg path "$1" ".repos[\$ARGS.named.path]$2"
162 config_query --arg user "$1" ".userEnvironments[\$ARGS.named.user]$2"
167 hydrate /run/wrappers/bin/sudo -u "$(repo_query "$path" .user)" \
168 ${pull-repo-script} "$path" "$(repo_query "$path" "")"
169 done < <( config_query '.repos | keys []' )
172 config_query '.pinchFiles[]' | ${pkgs.findutils}/bin/xargs --no-run-if-empty --delimiter=\\n ${pkgs.pinch}/bin/pinch update "''${pinch_args[@]}"
175 in_tmpdir hydrate ${config.system.build.nixos-rebuild}/bin/nixos-rebuild build
177 hydrate /run/wrappers/bin/sudo -u "$user" \
178 ${pkgs.nix}/bin/nix-build --no-out-link '<nixpkgs>' -A "$(userenv_query "$user" .package)"
179 done < <( config_query '.userEnvironments | keys []' )
182 hydrate ${config.system.build.nixos-rebuild}/bin/nixos-rebuild switch
185 if [[ "$(userenv_query "$user" .otherPackagesAction)" == keep ]];then
188 hydrate /run/wrappers/bin/sudo -u "$user" \
189 ${pkgs.nix}/bin/nix-env -f '<nixpkgs>' $remove_arg -iA "$(userenv_query "$user" .package)"
190 done < <( config_query '.userEnvironments | keys []' )
197 system.autoUpgradeWithPinch = {
203 Whether to periodically upgrade NixOS to the latest version.
204 Presumes that /etc/nixos is a git repo with a remote and
205 contains a pinch file called "channels".
213 Specification (in the format described by
214 <citerefentry><refentrytitle>systemd.time</refentrytitle>
215 <manvolnum>7</manvolnum></citerefentry>) of the time at
216 which the update will occur.
220 signingKeys = mkOption {
221 type = types.listOf types.path;
223 Files containing GPG keys that are authorized to sign updates.
224 Updates are only merged if the commit at the tip of the remote
225 ref is signed with one of these keys.
229 upgradeConfig = mkOption {
230 type = types.listOf types.path;
232 Configuration files that specify what git repo paths to pull, what
233 pinch files to update from, and what user environments to update.
234 These are specified in separate configuration files processed at
235 update time so that changes to this configuration take effect in
236 the same update cycle.
240 upgradeConfigOwnershipPolicy = mkOption {
241 type = types.enum [ "root" "wheel" "any" ];
244 Verify ownership of upgrade config files before using them for
247 root = Config must be writable only by root.
248 wheel = Config must be writable only by root and wheel.
249 any = No checks. Not recommended.
255 config = lib.mkIf cfg.enable {
257 security.sudo.extraRules = lib.mkAfter [{
258 groups = [ "users" ];
260 command = "${auto-upgrade-script}";
261 options = [ "NOPASSWD" "NOSETENV" ];
264 # NOSETENV above still allows through ~17 vars, including PATH. Block those
266 security.sudo.extraConfig = ''
267 Defaults!${auto-upgrade-script} !env_check
268 Defaults!${auto-upgrade-script} !env_keep
272 (import ../overlays/keyedgpg.nix)
273 (import ../overlays/pinch.nix)
274 (import ../overlays/polite-merge.nix)
276 auto-upgrade = super.writeShellScriptBin "auto-upgrade" ''
277 /run/wrappers/bin/sudo ${auto-upgrade-script}
282 environment.systemPackages = [ pkgs.auto-upgrade ];
284 systemd.services.nixos-upgrade = {
285 description = "NixOS Upgrade";
286 restartIfChanged = false;
287 unitConfig.X-StopOnRemoval = false;
288 serviceConfig.Type = "oneshot";
289 environment = config.nix.envVars // {
290 inherit (config.environment.sessionVariables) NIX_PATH;
292 } // config.networking.proxy.envVars;
295 config.nix.package.out
307 # Chill for awhile before applying updates. If applying an update
308 # badly breaks things, we want a window in which an operator can
309 # intervene either to fix the problem or disable automatic updates.
312 ${auto-upgrade-script}