+let
+ cfg = config.system.autoUpgradeWithPinch;
+ pull-repo-script =
+ pkgs.writeShellScript "pull-repo" ''
+ set -eo pipefail
+
+ path=$1
+ config=$2
+
+ prop() {
+ ${pkgs.jq}/bin/jq -r ".$1" <<< "$config"
+ }
+
+ echo Pulling in "$path" >&2
+
+ if [[ ! -e "$path" ]];then
+ d=$(mktemp -d)
+ ${pkgs.git}/bin/git init "$d"
+ ${pkgs.git}/bin/git -C "$d" checkout -b "$(prop localBranch)"
+ ${pkgs.git}/bin/git -C "$d" remote add "$(prop remoteName)" "$(prop url)"
+ ${pkgs.git}/bin/git -C "$d" branch -u "$(prop remoteBranch)"
+ mkdir -p "$(${pkgs.coreutils}/bin/dirname "$path")"
+ mv "$d" "$path"
+ fi
+
+ cd "$path"
+
+ if [[ "$(${pkgs.git}/bin/git remote get-url "$(prop remoteName)")" != "$(prop url)" ]]; then
+ echo Expected git remote "$(prop remoteName)" to point at "$(prop url)" \
+ but it points at "$(${pkgs.git}/bin/git remote get-url "$(prop remoteName)")" >&2
+ case "$(prop onRemoteURLMismatch)" in
+ abort) exit 1;;
+ update) echo Updating it >&2
+ ${pkgs.git}/bin/git -C "$d" remote set-url "$(prop remoteName)" "$(prop url)";;
+ esac
+ fi
+
+ ${pkgs.git}/bin/git fetch "$(prop remoteName)" "$(prop remoteBranch)"
+
+ if [[ "$(${pkgs.git}/bin/git rev-parse --abbrev-ref HEAD)" != "$(prop localBranch)" ]];then
+ echo Could not merge because currently-checked-out \
+ \""$(${pkgs.git}/bin/git rev-parse --abbrev-ref HEAD)"\" is not \
+ \""$(prop localBranch)"\"
+ case "$(prop onBranchMismatch)" in
+ abort) exit 1;;
+ continue) exit 0;;
+ esac
+ fi
+
+ if [[ "$(prop requireSignature)" == true ]]; then
+ ${pkgs.polite-merge}/bin/polite-merge \
+ -c gpg.program='${pkgs.keyedgpg} '"$(prop 'signingKeys[]' | tr \\n ' ')"' --' \
+ merge --ff-only --verify-signatures
+ else
+ ${pkgs.polite-merge}/bin/polite-merge merge --ff-only
+ fi
+ '';
+
+ auto-upgrade-script = pkgs.writeShellScript "auto-upgrade" ''
+ ${pkgs.utillinux}/bin/flock /run/auto-upgrade-with-pinch ${
+ pkgs.writeShellScript "auto-upgrade-with-lock-held" ''
+ set -eo pipefail
+
+ dry_run=false
+ pinch_args=()
+ if [[ "$1" == --dry-run ]];then
+ dry_run=true
+ pinch_args=( --dry-run )
+ fi
+
+ hydrate() {
+ if "$dry_run";then
+ echo "Would run: $*"
+ else
+ "$@"
+ fi
+ }
+
+ die() {
+ echo "$*" >&2
+ exit 1
+ }
+
+ in_tmpdir() {
+ d=$(${pkgs.coreutils}/bin/mktemp -d)
+ pushd "$d"
+ "$@"
+ popd
+ ${pkgs.coreutils}/bin/rm -r "$d"
+ }
+
+ ${optionalString (cfg.upgradeConfigOwnershipPolicy != "any") (''
+ verify_ownership() {
+ if [[ "$1" != /* ]];then
+ die "Unexpected relative path: $1"
+ fi
+ if [[ "$1" != / ]];then
+ verify_ownership "$(${pkgs.coreutils}/bin/dirname "$1")"
+ fi
+ if [[ ! -e "$1" ]];then
+ die "Could not find upgrade config: $1 does not exist"
+ fi
+ if [[ -h "$1" ]];then
+ verify_ownership "$(
+ ${pkgs.coreutils}/bin/realpath --no-symlinks \
+ "$(${pkgs.coreutils}/bin/dirname "$1")/$(${pkgs.coreutils}/bin/readlink "$1")"
+ )"
+ fi
+ perms="$(${pkgs.findutils}/bin/find "$1" -maxdepth 0 -printf "%M")"
+ if [[ "$perms" == d*t ]];then
+ die "Will not use upgrade config in sticky directory $1"
+ fi
+ owner=$(${pkgs.findutils}/bin/find "$1" -maxdepth 0 -printf "%u")
+ if [[ "$owner" != root ]];then
+ die "Will not use upgrade config not owned by root in $1"
+ fi
+ if [[ "$perms" == l* ]];then
+ return 0 # Root-owned symlinks are fine
+ fi
+ if [[ "$perms" == *w? ]];then
+ die "Will not use world-writable upgrade config in $1"
+ fi
+ ${
+ {
+ root = ''
+ if [[ "$perms" == *w???? ]];then
+ die "Will not use group-writable upgrade config in $1"
+ fi
+ '';
+ wheel = ''
+ if [[ "$perms" == *w???? ]];then
+ group=$(${pkgs.findutils}/bin/find "$1" -maxdepth 0 -printf "%g")
+ if [[ "$group" != wheel ]];then
+ die "Will not use non-wheel-group group-writable upgrade config in $1"
+ fi
+ fi
+ '';
+ }."${cfg.upgradeConfigOwnershipPolicy}"
+ }
+ }
+ ''
+ + concatMapStringsSep "\n" (f: "verify_ownership ${escapeShellArg f}")
+ cfg.upgradeConfig)}
+
+ config=$(${pkgs.nix}/bin/nix eval --json -f ${../upgrade-config.nix} \
+ --arg upgradeConfig ${
+ escapeShellArg ("["
+ + lib.concatMapStringsSep " " lib.strings.escapeNixString
+ cfg.upgradeConfig + "]")
+ } config)
+
+ config_query() {
+ ${pkgs.jq}/bin/jq -r "$@" <<< "$config"
+ }
+
+ repo_query() {
+ config_query --arg path "$1" ".repos[\$ARGS.named.path]$2"
+ }
+
+ userenv_query() {
+ config_query --arg user "$1" ".userEnvironments[\$ARGS.named.user]$2"
+ }
+
+ # Pull updates
+ while read path;do
+ hydrate /run/wrappers/bin/sudo -u "$(repo_query "$path" .user)" \
+ ${pull-repo-script} "$path" "$(repo_query "$path" "")"
+ done < <( config_query '.repos | keys []' )
+
+ # Update channels
+ config_query '.pinchFiles[]' | ${pkgs.findutils}/bin/xargs --no-run-if-empty --delimiter=\\n ${pkgs.pinch}/bin/pinch update "''${pinch_args[@]}"
+
+ # Build
+ in_tmpdir hydrate ${config.system.build.nixos-rebuild}/bin/nixos-rebuild build
+ while read user;do
+ hydrate /run/wrappers/bin/sudo -u "$user" \
+ ${pkgs.nix}/bin/nix-build --no-out-link '<nixpkgs>' -A "$(userenv_query "$user" .package)"
+ done < <( config_query '.userEnvironments | keys []' )
+
+ # Install
+ hydrate ${config.system.build.nixos-rebuild}/bin/nixos-rebuild switch
+ while read user;do
+ remove_arg=-r
+ if [[ "$(userenv_query "$user" .otherPackagesAction)" == keep ]];then
+ remove_arg=
+ fi
+ hydrate /run/wrappers/bin/sudo -u "$user" \
+ ${pkgs.nix}/bin/nix-env -f '<nixpkgs>' $remove_arg -iA "$(userenv_query "$user" .package)"
+ done < <( config_query '.userEnvironments | keys []' )
+ ''
+ }
+ '';
+