1 { config, lib, pkgs, ... }:
4 local-pkgs = import ../. { inherit pkgs; };
5 cfg = config.system.autoUpgradeWithPinch;
6 pull-repo-script = pkgs.writeShellScript "pull-repo" ''
13 ${pkgs.jq}/bin/jq -r ".$1" <<< "$config"
16 echo Pulling in "$path" >&2
18 if [[ ! -e "$path" ]];then
20 ${pkgs.git}/bin/git init "$d"
21 ${pkgs.git}/bin/git -C "$d" checkout -b "$(prop localBranch)"
22 ${pkgs.git}/bin/git -C "$d" remote add "$(prop remoteName)" "$(prop url)"
23 ${pkgs.git}/bin/git -C "$d" branch -u "$(prop remoteBranch)"
24 mkdir -p "$(${pkgs.coreutils}/bin/dirname "$path")"
30 if [[ "$(${pkgs.git}/bin/git remote get-url "$(prop remoteName)")" != "$(prop url)" ]]; then
31 echo Expected git remote "$(prop remoteName)" to point at "$(prop url)" \
32 but it points at "$(${pkgs.git}/bin/git remote get-url "$(prop remoteName)")" >&2
33 case "$(prop onRemoteURLMismatch)" in
35 update) echo Updating it >&2
36 ${pkgs.git}/bin/git -C "$d" remote set-url "$(prop remoteName)" "$(prop url)";;
40 ${pkgs.git}/bin/git fetch "$(prop remoteName)" "$(prop remoteBranch)"
42 if [[ "$(${pkgs.git}/bin/git rev-parse --abbrev-ref HEAD)" != "$(prop localBranch)" ]];then
43 echo Could not merge because currently-checked-out \
44 \""$(${pkgs.git}/bin/git rev-parse --abbrev-ref HEAD)"\" is not \
45 \""$(prop localBranch)"\"
46 case "$(prop onBranchMismatch)" in
52 if [[ "$(prop requireSignature)" == true ]]; then
53 ${pkgs.polite-merge}/bin/polite-merge \
54 -c gpg.program=${escapeShellArg (local-pkgs.keyed-gpg cfg.signingKeys)} \
55 merge --ff-only --verify-signatures
57 ${pkgs.polite-merge}/bin/polite-merge merge --ff-only
61 auto-upgrade-script = pkgs.writeShellScript "auto-upgrade" ''
62 ${pkgs.utillinux}/bin/flock /run/auto-upgrade-with-pinch ${
63 pkgs.writeShellScript "auto-upgrade-with-lock-held" ''
68 if [[ "$1" == --dry-run ]];then
70 pinch_args=( --dry-run )
87 d=$(${pkgs.coreutils}/bin/mktemp -d)
91 ${pkgs.coreutils}/bin/rm -r "$d"
94 ${optionalString (cfg.upgradeConfigOwnershipPolicy != "any") (''
96 if [[ "$1" != /* ]];then
97 die "Unexpected relative path: $1"
99 if [[ "$1" != / ]];then
100 verify_ownership "$(${pkgs.coreutils}/bin/dirname "$1")"
102 if [[ ! -e "$1" ]];then
103 die "Could not find upgrade config: $1 does not exist"
105 if [[ -h "$1" ]];then
107 ${pkgs.coreutils}/bin/realpath --no-symlinks \
108 "$(${pkgs.coreutils}/bin/dirname "$1")/$(${pkgs.coreutils}/bin/readlink "$1")"
111 perms="$(${pkgs.findutils}/bin/find "$1" -maxdepth 0 -printf "%M")"
112 if [[ "$perms" == d*t ]];then
113 die "Will not use upgrade config in sticky directory $1"
115 owner=$(${pkgs.findutils}/bin/find "$1" -maxdepth 0 -printf "%u")
116 if [[ "$owner" != root ]];then
117 die "Will not use upgrade config not owned by root in $1"
119 if [[ "$perms" == l* ]];then
120 return 0 # Root-owned symlinks are fine
122 if [[ "$perms" == *w? ]];then
123 die "Will not use world-writable upgrade config in $1"
128 if [[ "$perms" == *w???? ]];then
129 die "Will not use group-writable upgrade config in $1"
133 if [[ "$perms" == *w???? ]];then
134 group=$(${pkgs.findutils}/bin/find "$1" -maxdepth 0 -printf "%g")
135 if [[ "$group" != wheel ]];then
136 die "Will not use non-wheel-group group-writable upgrade config in $1"
140 }."${cfg.upgradeConfigOwnershipPolicy}"
144 + concatMapStringsSep "\n" (f: "verify_ownership ${escapeShellArg f}")
147 config=$(${pkgs.nix}/bin/nix-instantiate --eval --strict --json -A config \
148 --arg upgradeConfig ${
150 + lib.concatMapStringsSep " " lib.strings.escapeNixString
151 cfg.upgradeConfig + "]")
152 } ${../upgrade-config.nix})
155 ${pkgs.jq}/bin/jq -r "$@" <<< "$config"
159 config_query --arg path "$1" ".repos[\$ARGS.named.path]$2"
163 config_query --arg user "$1" ".userEnvironments[\$ARGS.named.user]$2"
168 hydrate /run/wrappers/bin/sudo -u "$(repo_query "$path" .user)" \
169 ${pull-repo-script} "$path" "$(repo_query "$path" "")"
170 done < <( config_query '.repos | keys []' )
173 config_query '.pinchFiles[]' | ${pkgs.findutils}/bin/xargs --no-run-if-empty --delimiter=\\n ${pkgs.pinch}/bin/pinch update "''${pinch_args[@]}"
176 in_tmpdir hydrate ${config.system.build.nixos-rebuild}/bin/nixos-rebuild build
178 hydrate /run/wrappers/bin/sudo -u "$user" \
179 ${pkgs.nix}/bin/nix-build --no-out-link '<nixpkgs>' -A "$(userenv_query "$user" .package)"
180 done < <( config_query '.userEnvironments | keys []' )
183 hydrate ${config.system.build.nixos-rebuild}/bin/nixos-rebuild switch
186 if [[ "$(userenv_query "$user" .otherPackagesAction)" == keep ]];then
189 hydrate /run/wrappers/bin/sudo -u "$user" \
190 ${pkgs.nix}/bin/nix-env -f '<nixpkgs>' $remove_arg -iA "$(userenv_query "$user" .package)"
191 done < <( config_query '.userEnvironments | keys []' )
198 system.autoUpgradeWithPinch = {
204 Whether to periodically upgrade NixOS to the latest version.
205 Presumes that /etc/nixos is a git repo with a remote and
206 contains a pinch file called "channels".
214 Specification (in the format described by
215 <citerefentry><refentrytitle>systemd.time</refentrytitle>
216 <manvolnum>7</manvolnum></citerefentry>) of the time at
217 which the update will occur.
221 signingKeys = mkOption {
222 type = types.listOf types.path;
224 Files containing GPG keys that are authorized to sign updates.
225 Updates are only merged if the commit at the tip of the remote
226 ref is signed with one of these keys.
230 upgradeConfig = mkOption {
231 type = types.listOf types.path;
233 Configuration files that specify what git repo paths to pull, what
234 pinch files to update from, and what user environments to update.
235 These are specified in separate configuration files processed at
236 update time so that changes to this configuration take effect in
237 the same update cycle.
241 upgradeConfigOwnershipPolicy = mkOption {
242 type = types.enum [ "root" "wheel" "any" ];
245 Verify ownership of upgrade config files before using them for
248 root = Config must be writable only by root.
249 wheel = Config must be writable only by root and wheel.
250 any = No checks. Not recommended.
256 config = lib.mkIf cfg.enable {
258 security.sudo.extraRules = lib.mkAfter [{
259 groups = [ "users" ];
261 command = "${auto-upgrade-script}";
262 options = [ "NOPASSWD" "NOSETENV" ];
265 # NOSETENV above still allows through ~17 vars, including PATH. Block those
267 security.sudo.extraConfig = ''
268 Defaults!${auto-upgrade-script} !env_check
269 Defaults!${auto-upgrade-script} !env_keep
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}