]> git.scottworley.com Git - auto-upgrade-with-pinch/commitdiff
25.11: Formatting: nixfmt-classic → nixfmt-rfc-style
authorScott Worley <scottworley@scottworley.com>
Fri, 2 Jan 2026 06:57:59 +0000 (22:57 -0800)
committerScott Worley <scottworley@scottworley.com>
Fri, 2 Jan 2026 06:57:59 +0000 (22:57 -0800)
default.nix
modules/auto-upgrade.nix
overlays/pinch.nix
overlays/polite-merge.nix
pkgs/homeless-gpg.nix
pkgs/keyed-gpg.nix
upgrade-config.nix

index 1413fef74a3415f38f53b3b8035e679dc207ad02..348ba978b601e07cad92a759ce25c281e99b69d7 100644 (file)
@@ -1,10 +1,13 @@
-{ pkgs ? import <nixpkgs> { }, }:
+{
+  pkgs ? import <nixpkgs> { },
+}:
 
-pkgs.lib.makeScope pkgs.newScope (self:
-  with self; {
+pkgs.lib.makeScope pkgs.newScope (
+  self: with self; {
 
     homeless-gpg = callPackage ./pkgs/homeless-gpg.nix { };
 
     keyed-gpg = callPackage ./pkgs/keyed-gpg.nix { };
 
-  })
+  }
+)
index 46e3e7f594e722f6c80ac6ceceb67a0f4a6c9e43..c52f0bce10ab6fa5ce6ba0c47114692290ed84b5 100644 (file)
@@ -4,7 +4,12 @@
 # under the terms of the GNU General Public License as published by the
 # Free Software Foundation, version 3.
 
-{ config, lib, pkgs, ... }:
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
 with lib;
 let
   local-pkgs = import ../. { inherit pkgs; };
@@ -67,39 +72,39 @@ let
   auto-upgrade-script = pkgs.writeShellScript "auto-upgrade" ''
     ${pkgs.coreutils}/bin/nice -n 17 \
     ${pkgs.util-linux}/bin/ionice -c 3 \
-    ${pkgs.util-linux}/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"
+    ${pkgs.util-linux}/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
           "$@"
-          popd
-          ${pkgs.coreutils}/bin/rm -r "$d"
-        }
-
-        ${optionalString (cfg.upgradeConfigOwnershipPolicy != "any") (''
+        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"
@@ -145,68 +150,69 @@ let
                     fi
                   fi
                 '';
-              }."${cfg.upgradeConfigOwnershipPolicy}"
+              }
+              ."${cfg.upgradeConfigOwnershipPolicy}"
             }
           }
         ''
-          + concatMapStringsSep "\n" (f: "verify_ownership ${escapeShellArg f}")
-          cfg.upgradeConfig)}
-
-        config=$(${pkgs.nix}/bin/nix-instantiate --eval --strict --json -A config \
-          --arg upgradeConfig ${
-            escapeShellArg ("["
-              + lib.concatMapStringsSep " " lib.strings.escapeNixString
-              cfg.upgradeConfig + "]")
-          } ${../upgrade-config.nix})
-
-        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
-          pushd /
-          hydrate /run/wrappers/bin/sudo -u "$user" \
-            ${pkgs.nix}/bin/nix-build --no-out-link '<nixpkgs>' -A "$(userenv_query "$user" .package)"
-          popd
-        done < <( config_query '.userEnvironments | keys []' )
-        sync
-
-        # Install
-        hydrate ${config.system.build.nixos-rebuild}/bin/nixos-rebuild switch
+        + concatMapStringsSep "\n" (f: "verify_ownership ${escapeShellArg f}") cfg.upgradeConfig
+      )}
+
+      config=$(${pkgs.nix}/bin/nix-instantiate --eval --strict --json -A config \
+        --arg upgradeConfig ${
+          escapeShellArg (
+            "[" + lib.concatMapStringsSep " " lib.strings.escapeNixString cfg.upgradeConfig + "]"
+          )
+        } ${../upgrade-config.nix})
+
+      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
+        pushd /
+        hydrate /run/wrappers/bin/sudo -u "$user" \
+          ${pkgs.nix}/bin/nix-build --no-out-link '<nixpkgs>' -A "$(userenv_query "$user" .package)"
+        popd
+      done < <( config_query '.userEnvironments | keys []' )
+      sync
+
+      # Install
+      hydrate ${config.system.build.nixos-rebuild}/bin/nixos-rebuild switch
+      sync
+      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)"
         sync
-        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)"
-          sync
-        done < <( config_query '.userEnvironments | keys []' )
-      ''
-    }
+      done < <( config_query '.userEnvironments | keys []' )
+    ''}
   '';
 
-in {
+in
+{
   options = {
     system.autoUpgradeWithPinch = {
 
@@ -252,7 +258,11 @@ in {
       };
 
       upgradeConfigOwnershipPolicy = mkOption {
-        type = types.enum [ "root" "wheel" "any" ];
+        type = types.enum [
+          "root"
+          "wheel"
+          "any"
+        ];
         default = "root";
         description = ''
           Verify ownership of upgrade config files before using them for
@@ -268,13 +278,20 @@ in {
 
   config = lib.mkIf cfg.enable {
 
-    security.sudo.extraRules = lib.mkAfter [{
-      groups = [ "users" ];
-      commands = [{
-        command = "${auto-upgrade-script}";
-        options = [ "NOPASSWD" "NOSETENV" ];
-      }];
-    }];
+    security.sudo.extraRules = lib.mkAfter [
+      {
+        groups = [ "users" ];
+        commands = [
+          {
+            command = "${auto-upgrade-script}";
+            options = [
+              "NOPASSWD"
+              "NOSETENV"
+            ];
+          }
+        ];
+      }
+    ];
     # NOSETENV above still allows through ~17 vars, including PATH.  Block those
     # as well:
     security.sudo.extraConfig = ''
@@ -299,10 +316,13 @@ in {
       restartIfChanged = false;
       unitConfig.X-StopOnRemoval = false;
       serviceConfig.Type = "oneshot";
-      environment = config.nix.envVars // {
-        inherit (config.environment.sessionVariables) NIX_PATH;
-        HOME = "/root";
-      } // config.networking.proxy.envVars;
+      environment =
+        config.nix.envVars
+        // {
+          inherit (config.environment.sessionVariables) NIX_PATH;
+          HOME = "/root";
+        }
+        // config.networking.proxy.envVars;
 
       path = with pkgs; [
         config.nix.package.out
index 9422fe9556c1f69b5dc1978639ee29ea22b8a6b1..d71672068917bbd625fe67347a71e00bc3f1996f 100644 (file)
@@ -1,39 +1,66 @@
 self: super:
 let
-  fallback-git-cache = self.python3Packages.callPackage
-    ({ buildPythonPackage, fetchgit, setuptools, git, backoff, mypy, }:
-      buildPythonPackage rec {
-        pname = "git-cache";
-        version = "1.5.0";
-        src = fetchgit {
-          url = "https://git.scottworley.com/pub/git/git-cache";
-          rev = "v${version}";
-          hash = "sha256-g4TS/zX3e29Q3ThsCAX2wLLlYbi8fdux5uqAc+b/Oww=";
-        };
-        pyproject = true;
-        build-system = [ setuptools ];
-        propagatedBuildInputs = [ backoff ];
-        nativeCheckInputs = [ git mypy ];
-        doCheck = true;
-        checkPhase = "./test.sh";
-      }) { };
+  fallback-git-cache = self.python3Packages.callPackage (
+    {
+      buildPythonPackage,
+      fetchgit,
+      setuptools,
+      git,
+      backoff,
+      mypy,
+    }:
+    buildPythonPackage rec {
+      pname = "git-cache";
+      version = "1.5.0";
+      src = fetchgit {
+        url = "https://git.scottworley.com/pub/git/git-cache";
+        rev = "v${version}";
+        hash = "sha256-g4TS/zX3e29Q3ThsCAX2wLLlYbi8fdux5uqAc+b/Oww=";
+      };
+      pyproject = true;
+      build-system = [ setuptools ];
+      propagatedBuildInputs = [ backoff ];
+      nativeCheckInputs = [
+        git
+        mypy
+      ];
+      doCheck = true;
+      checkPhase = "./test.sh";
+    }
+  ) { };
 
-  fallback-pinch = self.python3Packages.callPackage
-    ({ buildPythonPackage, fetchgit, setuptools, nix, git, mypy, git-cache, }:
-      buildPythonPackage rec {
-        pname = "pinch";
-        version = "3.3.2";
-        src = fetchgit {
-          url = "https://git.scottworley.com/pub/git/pinch";
-          rev = "v${version}";
-          hash = "sha256-UB1hAEX7bD2TfdDv5EOWH1aaLluvzvpW80EjdCBuCCU=";
-        };
-        pyproject = true;
-        build-system = [ setuptools ];
-        propagatedBuildInputs = [ git-cache ];
-        nativeCheckInputs = [ nix git mypy ];
-        doCheck = true;
-        checkPhase = "./test.sh";
-      }) { git-cache = self.python3Packages.git-cache or fallback-git-cache; };
+  fallback-pinch = self.python3Packages.callPackage (
+    {
+      buildPythonPackage,
+      fetchgit,
+      setuptools,
+      nix,
+      git,
+      mypy,
+      git-cache,
+    }:
+    buildPythonPackage rec {
+      pname = "pinch";
+      version = "3.3.2";
+      src = fetchgit {
+        url = "https://git.scottworley.com/pub/git/pinch";
+        rev = "v${version}";
+        hash = "sha256-UB1hAEX7bD2TfdDv5EOWH1aaLluvzvpW80EjdCBuCCU=";
+      };
+      pyproject = true;
+      build-system = [ setuptools ];
+      propagatedBuildInputs = [ git-cache ];
+      nativeCheckInputs = [
+        nix
+        git
+        mypy
+      ];
+      doCheck = true;
+      checkPhase = "./test.sh";
+    }
+  ) { git-cache = self.python3Packages.git-cache or fallback-git-cache; };
 
-in { pinch = super.pinch or fallback-pinch; }
+in
+{
+  pinch = super.pinch or fallback-pinch;
+}
index da4db98cf5f46ffb80d789f9686e6eb4f5509c15..85e80c5a1282c34ef8490d3efd3c973d2053f664 100644 (file)
@@ -1,20 +1,26 @@
 self: super: {
-  polite-merge = if builtins.hasAttr "polite-merge" super then
-    super.polite-merge
-  else
-    self.callPackage ({ fetchgit, git, stdenv, }:
-      stdenv.mkDerivation rec {
-        pname = "polite-merge";
-        version = "2.4.2";
-        src = fetchgit {
-          url = "https://git.scottworley.com/pub/git/polite-merge";
-          rev = "v${version}";
-          hash = "sha256-CUNKLCwIFwwVaA9opw9yql5AGej/ozQv8k1YR/cfV4I=";
-        };
-        postUnpack = "patchShebangs .";
-        nativeCheckInputs = [ git ];
-        doCheck = true;
-        preInstall = "export prefix";
-      }) { };
+  polite-merge =
+    if builtins.hasAttr "polite-merge" super then
+      super.polite-merge
+    else
+      self.callPackage (
+        {
+          fetchgit,
+          git,
+          stdenv,
+        }:
+        stdenv.mkDerivation rec {
+          pname = "polite-merge";
+          version = "2.4.2";
+          src = fetchgit {
+            url = "https://git.scottworley.com/pub/git/polite-merge";
+            rev = "v${version}";
+            hash = "sha256-CUNKLCwIFwwVaA9opw9yql5AGej/ozQv8k1YR/cfV4I=";
+          };
+          postUnpack = "patchShebangs .";
+          nativeCheckInputs = [ git ];
+          doCheck = true;
+          preInstall = "export prefix";
+        }
+      ) { };
 }
-
index 221193f77124e842c0aafd35a4042d73832854ab..a47eee76b6ff80aa3b2ec30a90db450bdd9634cf 100644 (file)
@@ -1,4 +1,8 @@
-{ coreutils, gnupg, writeShellScript }:
+{
+  coreutils,
+  gnupg,
+  writeShellScript,
+}:
 writeShellScript "homeless-gpg" ''
   set -eo pipefail
 
index b6758227f66d5c39966c123adf8f30c1621d6bf8..6fe1ed99a89e33b8370de1d36f08397b21a64eb5 100644 (file)
@@ -1,7 +1,13 @@
 # Following the instructions at https://tribut.de/blog/git-commit-signatures-trusted-keys
 # Use with git with -c gpg.program='keyedgpg /path/to/keyfile.asc'
 
-{ coreutils, gawk, homeless-gpg, lib, writeShellScript, }:
+{
+  coreutils,
+  gawk,
+  homeless-gpg,
+  lib,
+  writeShellScript,
+}:
 keyfiles:
 writeShellScript "keyed-gpg" ''
   set -eo pipefail
@@ -20,4 +26,3 @@ writeShellScript "keyed-gpg" ''
 
   ${homeless-gpg} --keyring="$keyring" "''${trusted_key_args[@]}" "$@"
 ''
-
index 67c4ec1d5a4fc56b13ca03b44063cc2c22cbcd3a..ba2bb5a4f2969648940a0e79d5437a29e49d98d7 100644 (file)
-{ upgradeConfig, lib ? (import <nixpkgs> { }).lib, }:
+{
+  upgradeConfig,
+  lib ? (import <nixpkgs> { }).lib,
+}:
 with lib;
 evalModules {
-  modules = upgradeConfig ++ [{
-    options = {
+  modules = upgradeConfig ++ [
+    {
+      options = {
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to periodically upgrade NixOS to the latest version.
-          Presumes that /etc/nixos is a git repo with a remote and
-          contains a pinch file called "channels".
-        '';
-      };
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Whether to periodically upgrade NixOS to the latest version.
+            Presumes that /etc/nixos is a git repo with a remote and
+            contains a pinch file called "channels".
+          '';
+        };
 
-      dates = mkOption {
-        default = "04:40";
-        type = types.str;
-        description = ''
-          Specification (in the format described by
-          <citerefentry><refentrytitle>systemd.time</refentrytitle>
-          <manvolnum>7</manvolnum></citerefentry>) of the time at
-          which the update will occur.
-        '';
-      };
+        dates = mkOption {
+          default = "04:40";
+          type = types.str;
+          description = ''
+            Specification (in the format described by
+            <citerefentry><refentrytitle>systemd.time</refentrytitle>
+            <manvolnum>7</manvolnum></citerefentry>) of the time at
+            which the update will occur.
+          '';
+        };
 
-      repos = mkOption {
-        description = ''
-          Git repositories to pull before running pinch.  These are maintained
-          as git checkouts at specified places in the filesystem with specified
-          ownership rather than kept read-only in the nix store so that humans
-          can use them both as points of intervention in the automation and to
-          author and push changes back up.
-        '';
-        default = { };
-        type = types.attrsOf (types.submodule {
-          options = {
-            url = mkOption {
-              description = "Remote git repo.";
-              type = types.str;
-            };
-            remoteName = mkOption {
-              description = ''Name of the git remote.  Customarily "origin".'';
-              type = types.str;
-              default = "origin";
-            };
-            onRemoteURLMismatch = mkOption {
-              description = ''
-                What to do if the remote URL in the git repo doesn't match the
-                URL configured here.
-              '';
-              type = types.enum [ "update" "abort" ];
-              default = "update";
-            };
-            onBranchMismatch = mkOption {
-              description = ''
-                What to do if a different branch is currently checked out.
+        repos = mkOption {
+          description = ''
+            Git repositories to pull before running pinch.  These are maintained
+            as git checkouts at specified places in the filesystem with specified
+            ownership rather than kept read-only in the nix store so that humans
+            can use them both as points of intervention in the automation and to
+            author and push changes back up.
+          '';
+          default = { };
+          type = types.attrsOf (
+            types.submodule {
+              options = {
+                url = mkOption {
+                  description = "Remote git repo.";
+                  type = types.str;
+                };
+                remoteName = mkOption {
+                  description = ''Name of the git remote.  Customarily "origin".'';
+                  type = types.str;
+                  default = "origin";
+                };
+                onRemoteURLMismatch = mkOption {
+                  description = ''
+                    What to do if the remote URL in the git repo doesn't match the
+                    URL configured here.
+                  '';
+                  type = types.enum [
+                    "update"
+                    "abort"
+                  ];
+                  default = "update";
+                };
+                onBranchMismatch = mkOption {
+                  description = ''
+                    What to do if a different branch is currently checked out.
 
-                (Changes from <literal>remoteBranch</literal> are only ever
-                merged into <literal>localBranch</literal>, so if a different
-                branch is checked out, no remote changes will be merged.)
-              '';
-              type = types.enum [ "continue" "abort" ];
-              default = "continue";
-            };
-            user = mkOption {
-              description = "User as which to run 'git fetch'";
-              type = types.str;
-            };
-            localBranch = mkOption {
-              description = "";
-              type = types.str;
-              default = "master";
+                    (Changes from <literal>remoteBranch</literal> are only ever
+                    merged into <literal>localBranch</literal>, so if a different
+                    branch is checked out, no remote changes will be merged.)
+                  '';
+                  type = types.enum [
+                    "continue"
+                    "abort"
+                  ];
+                  default = "continue";
+                };
+                user = mkOption {
+                  description = "User as which to run 'git fetch'";
+                  type = types.str;
+                };
+                localBranch = mkOption {
+                  description = "";
+                  type = types.str;
+                  default = "master";
+                };
+                remoteBranch = mkOption {
+                  type = types.str;
+                  default = "master";
+                };
+                requireSignature = mkOption {
+                  type = types.bool;
+                  default = true;
+                  description = ''
+                    Only pull when the tip of the remote ref is signed by a key
+                    specifed in <literal>signingKeys</literal>.
+                  '';
+                };
+              };
+            }
+          );
+          example = {
+            "/etc/nixos" = {
+              url = "https://github.com/chkno/auto-upgrade-demo-nixos";
+              user = "root";
+              signingKeys = [ ./admins.asc ];
             };
-            remoteBranch = mkOption {
-              type = types.str;
-              default = "master";
+            "/home/alice/.config/nixpkgs" = {
+              url = "https://github.com/chkno/auto-upgrade-demo-user-nixpkgs";
+              user = "alice";
+              signingKeys = [
+                ./admins.asc
+                ./alice.asc
+              ];
             };
-            requireSignature = mkOption {
-              type = types.bool;
-              default = true;
-              description = ''
-                Only pull when the tip of the remote ref is signed by a key
-                specifed in <literal>signingKeys</literal>.
-              '';
-            };
-          };
-        });
-        example = {
-          "/etc/nixos" = {
-            url = "https://github.com/chkno/auto-upgrade-demo-nixos";
-            user = "root";
-            signingKeys = [ ./admins.asc ];
-          };
-          "/home/alice/.config/nixpkgs" = {
-            url = "https://github.com/chkno/auto-upgrade-demo-user-nixpkgs";
-            user = "alice";
-            signingKeys = [ ./admins.asc ./alice.asc ];
           };
         };
-      };
 
-      pinchFiles = mkOption {
-        description = ''
-          Pinch files to use for channel updates.  Typically these are inside
-          <literal>repos</literal>' paths.
-        '';
-        type = types.listOf types.path;
-        default = [ ];
-        example = [ "/etc/nixos/channels" ];
-      };
+        pinchFiles = mkOption {
+          description = ''
+            Pinch files to use for channel updates.  Typically these are inside
+            <literal>repos</literal>' paths.
+          '';
+          type = types.listOf types.path;
+          default = [ ];
+          example = [ "/etc/nixos/channels" ];
+        };
 
-      userEnvironments = mkOption {
-        description = ''
-          User environments to update as part of an upgrade run.
-        '';
-        default = { };
-        type = types.attrsOf (types.submodule {
-          options = {
-            package = mkOption {
-              type = types.str;
-              default = "userPackages";
-              description = ''
-                The name of the single package that will be updated.  You'll
-                want to create an 'entire user environment' package as shown in
-                https://nixos.wiki/wiki/FAQ#How_can_I_manage_software_with_nix-env_like_with_configuration.nix.3F
-              '';
-            };
-            otherPackagesAction = mkOption {
-              type = types.enum [ "remove" "keep" "abort" ];
-              default = "remove";
-              description = ''
-                What to do with packages other than <literal>package</literal>.
+        userEnvironments = mkOption {
+          description = ''
+            User environments to update as part of an upgrade run.
+          '';
+          default = { };
+          type = types.attrsOf (
+            types.submodule {
+              options = {
+                package = mkOption {
+                  type = types.str;
+                  default = "userPackages";
+                  description = ''
+                    The name of the single package that will be updated.  You'll
+                    want to create an 'entire user environment' package as shown in
+                    https://nixos.wiki/wiki/FAQ#How_can_I_manage_software_with_nix-env_like_with_configuration.nix.3F
+                  '';
+                };
+                otherPackagesAction = mkOption {
+                  type = types.enum [
+                    "remove"
+                    "keep"
+                    "abort"
+                  ];
+                  default = "remove";
+                  description = ''
+                    What to do with packages other than <literal>package</literal>.
 
-                THIS DEFAULTS TO "remove", WHICH IS POTENTIALLY SOMEWHAT
-                DESTRUCTIVE!  This is the default because it is the recommended
-                setting -- This module recommends managing your environment
-                through your one entire-environment <literal>package</literal>.
-                This keeps your environment declarative and ensures that all
-                packages receive regular updates.
-              '';
-              # It seems like "upgrade" ought to be another choice here, powered
-              # by "nix-env --upgrade".  But when I tried this, it didn't work.
-            };
+                    THIS DEFAULTS TO "remove", WHICH IS POTENTIALLY SOMEWHAT
+                    DESTRUCTIVE!  This is the default because it is the recommended
+                    setting -- This module recommends managing your environment
+                    through your one entire-environment <literal>package</literal>.
+                    This keeps your environment declarative and ensures that all
+                    packages receive regular updates.
+                  '';
+                  # It seems like "upgrade" ought to be another choice here, powered
+                  # by "nix-env --upgrade".  But when I tried this, it didn't work.
+                };
+              };
+            }
+          );
+          example = {
+            alice = { };
           };
-        });
-        example = { alice = { }; };
+        };
       };
-    };
-  }];
+    }
+  ];
 }