{ config, lib, pkgs, ... }: with lib; let cfg = config.system.autoUpgradeWithPinch; auto-upgrade-script = pkgs.writeShellScript "auto-upgrade" '' ${pkgs.utillinux}/bin/flock /run/auto-upgrade-with-pinch ${ pkgs.writeShellScript "auto-upgrade-with-lock-held" '' set -e in_tmpdir() { d=$(mktemp -d) pushd "$d" "$@" popd rm -r "$d" } as_user() { ${ if cfg.userEnvironment.enable then '' /run/wrappers/bin/sudo -u ${escapeShellArg cfg.userEnvironment.user} "$@" '' else '' : '' } } # Update channels ( cd /etc/nixos ${pkgs.git}/bin/git fetch PATH="${pkgs.keyedgit cfg.keys}/bin:$PATH" ${pkgs.polite-merge}/bin/polite-merge --ff-only --verify-signatures ${pkgs.pinch}/bin/pinch update channels ) # Build in_tmpdir ${config.system.build.nixos-rebuild}/bin/nixos-rebuild build as_user nix-build --no-out-link '' -A ${ escapeShellArg cfg.userEnvironment.package } # Install ${config.system.build.nixos-rebuild}/bin/nixos-rebuild switch as_user nix-env -f '' -riA ${ escapeShellArg cfg.userEnvironment.package } '' } ''; in { options = { system.autoUpgradeWithPinch = { 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 systemd.time 7) of the time at which the update will occur. ''; }; keys = mkOption { type = types.path; description = '' File containing GPG keys that sign updates. Updates are only merged if the commit at the tip of the remote branch is signed with one of these keys. ''; }; userEnvironment = { enable = mkOption { type = types.bool; default = false; description = '' Whether to update a user-environment as well. This update is done with nix-env -riA. Note the -r! I.e., ALL OTHER PACKAGES INSTALLED WITH nix-env WILL BE DELETED! This presumes that you have configured 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 To check if you're set up for this, run "nix-env --query". If it only lists one package, you're good to go. ''; }; user = mkOption { type = types.str; description = '' The username of the user whose environment should be updated. ''; }; package = mkOption { type = types.str; example = "nixos.userPackages"; description = '' The name of the single package that is the user's entire environment. ''; }; }; }; }; config = lib.mkIf cfg.enable { 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 = '' Defaults!${auto-upgrade-script} !env_check Defaults!${auto-upgrade-script} !env_keep ''; nixpkgs.overlays = [ (import ../overlays/keyedgit.nix) (import ../overlays/pinch.nix) (import ../overlays/polite-merge.nix) (self: super: { auto-upgrade = super.writeShellScriptBin "auto-upgrade" '' /run/wrappers/bin/sudo ${auto-upgrade-script} ''; }) ]; environment.systemPackages = [ pkgs.auto-upgrade ]; systemd.services.nixos-upgrade = { description = "NixOS Upgrade"; 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; path = with pkgs; [ config.nix.package.out coreutils git gitMinimal gnutar gzip xz.bin ]; script = '' set -e # Chill for awhile before applying updates. If applying an update # badly breaks things, we want a window in which an operator can # intervene either to fix the problem or disable automatic updates. sleep 2h # Wait until outside business hours now=$(date +%s) day_of_week=$(date +%u) business_start=$(date -d 8:00 +%s) business_end=$( date -d 17:00 +%s) if (( day_of_week <= 5 && now > business_start && now < business_end ));then delay=$((business_end - now)) echo "Waiting $delay seconds so we don't upgrade during business hours" >&2 sleep "$delay" fi ${auto-upgrade-script} ''; startAt = cfg.dates; }; assertions = [{ assertion = cfg.userEnvironment.enable -> cfg.enable; message = "User environment upgrades cannot yet be enabled separately from system upgrades."; }]; }; }