#!@bash@/bin/bash # Prepare to use tools from Nixpkgs. PATH=@coreutils@/bin:@findutils@/bin:@gnused@/bin:@less@/bin:@nixos-option@/bin${PATH:+:}$PATH set -euo pipefail function errorEcho() { # shellcheck disable=2048,2086 echo $* >&2 } function setVerboseAndDryRun() { if [[ -v VERBOSE ]]; then export VERBOSE_ARG="--verbose" else export VERBOSE_ARG="" fi if [[ -v DRY_RUN ]] ; then export DRY_RUN_CMD=echo else export DRY_RUN_CMD="" fi } function setWorkDir() { if [[ ! -v WORK_DIR ]]; then WORK_DIR="$(mktemp --tmpdir -d home-manager-build.XXXXXXXXXX)" # shellcheck disable=2064 trap "rm -r '$WORK_DIR'" EXIT fi } # Attempts to set the HOME_MANAGER_CONFIG global variable. # # If no configuration file can be found then this function will print # an error message and exit with an error code. function setConfigFile() { if [[ -v HOME_MANAGER_CONFIG ]] ; then if [[ ! -e "$HOME_MANAGER_CONFIG" ]] ; then errorEcho "No configuration file found at $HOME_MANAGER_CONFIG" exit 1 fi HOME_MANAGER_CONFIG="$(realpath "$HOME_MANAGER_CONFIG")" return fi local defaultConfFile="${XDG_CONFIG_HOME:-$HOME/.config}/nixpkgs/home.nix" local confFile for confFile in "$defaultConfFile" \ "$HOME/.nixpkgs/home.nix" ; do if [[ -e "$confFile" ]] ; then HOME_MANAGER_CONFIG="$(realpath "$confFile")" return fi done errorEcho "No configuration file found." \ "Please create one at $defaultConfFile" exit 1 } function setHomeManagerNixPath() { local path for path in "@HOME_MANAGER_PATH@" \ "${XDG_CONFIG_HOME:-$HOME/.config}/nixpkgs/home-manager" \ "$HOME/.nixpkgs/home-manager" ; do if [[ -e "$path" || "$path" =~ ^https?:// ]] ; then export NIX_PATH="home-manager=$path${NIX_PATH:+:}$NIX_PATH" return fi done } function setFlakeAttribute() { local configFlake="${XDG_CONFIG_HOME:-$HOME/.config}/nixpkgs/flake.nix" if [[ -z $FLAKE_ARG && ! -v HOME_MANAGER_CONFIG && -e "$configFlake" ]]; then FLAKE_ARG="$(dirname "$(readlink -f "$configFlake")")" fi if [[ -n "$FLAKE_ARG" ]]; then local flake="${FLAKE_ARG%#*}" case $FLAKE_ARG in *#*) local name="${FLAKE_ARG#*#}" ;; *) local name="$USER@$(hostname)" if [ "$(nix eval "$flake#homeConfigurations" --apply "x: x ? \"$name\"")" = "false" ]; then name="$USER" fi ;; esac export FLAKE_CONFIG_URI="$flake#homeConfigurations.\"$name\"" fi } function doInspectOption() { setFlakeAttribute if [[ -v FLAKE_CONFIG_URI ]]; then errorEcho "Can't inspect options of a flake configuration" exit 1 fi setConfigFile setHomeManagerNixPath local extraArgs=("$@") for p in "${EXTRA_NIX_PATH[@]}"; do extraArgs=("${extraArgs[@]}" "-I" "$p") done if [[ -v VERBOSE ]]; then extraArgs=("${extraArgs[@]}" "--show-trace") fi local HOME_MANAGER_CONFIG_NIX HOME_MANAGER_CONFIG_ATTRIBUTE_NIX HOME_MANAGER_CONFIG_NIX=${HOME_MANAGER_CONFIG//'\'/'\\'} HOME_MANAGER_CONFIG_NIX=${HOME_MANAGER_CONFIG_NIX//'"'/'\"'} HOME_MANAGER_CONFIG_NIX=${HOME_MANAGER_CONFIG_NIX//$'\n'/$'\\n'} HOME_MANAGER_CONFIG_ATTRIBUTE_NIX=${HOME_MANAGER_CONFIG_ATTRIBUTE//'\'/'\\'} HOME_MANAGER_CONFIG_ATTRIBUTE_NIX=${HOME_MANAGER_CONFIG_ATTRIBUTE_NIX//'"'/'\"'} HOME_MANAGER_CONFIG_ATTRIBUTE_NIX=${HOME_MANAGER_CONFIG_ATTRIBUTE_NIX//$'\n'/$'\\n'} local modulesExpr modulesExpr="let confPath = \"${HOME_MANAGER_CONFIG_NIX}\"; " modulesExpr+="confAttr = \"${HOME_MANAGER_CONFIG_ATTRIBUTE_NIX}\"; in " modulesExpr+="(import {" modulesExpr+=" configuration = if confAttr == \"\" then confPath else (import confPath).\${confAttr};" modulesExpr+=" pkgs = import {}; check = true; })" nixos-option \ --options_expr "$modulesExpr.options" \ --config_expr "$modulesExpr.config" \ "${extraArgs[@]}" \ "${PASSTHROUGH_OPTS[@]}" } function doInstantiate() { setFlakeAttribute if [[ -v FLAKE_CONFIG_URI ]]; then errorEcho "Can't instantiate a flake configuration" exit 1 fi setConfigFile setHomeManagerNixPath local extraArgs=() for p in "${EXTRA_NIX_PATH[@]}"; do extraArgs=("${extraArgs[@]}" "-I" "$p") done if [[ -v VERBOSE ]]; then extraArgs=("${extraArgs[@]}" "--show-trace") fi nix-instantiate \ "" \ "${extraArgs[@]}" \ "${PASSTHROUGH_OPTS[@]}" \ --argstr confPath "$HOME_MANAGER_CONFIG" \ --argstr confAttr "$HOME_MANAGER_CONFIG_ATTRIBUTE" } function doBuildAttr() { setConfigFile setHomeManagerNixPath local extraArgs=("$@") for p in "${EXTRA_NIX_PATH[@]}"; do extraArgs=("${extraArgs[@]}" "-I" "$p") done if [[ -v VERBOSE ]]; then extraArgs=("${extraArgs[@]}" "--show-trace") fi nix-build \ "" \ "${extraArgs[@]}" \ "${PASSTHROUGH_OPTS[@]}" \ --argstr confPath "$HOME_MANAGER_CONFIG" \ --argstr confAttr "$HOME_MANAGER_CONFIG_ATTRIBUTE" } # Presents news to the user. Takes as argument the path to a "news # info" file as generated by `buildNews`. function presentNews() { local infoFile="$1" # shellcheck source=/dev/null . "$infoFile" # shellcheck disable=2154 if [[ $newsNumUnread -eq 0 ]]; then return elif [[ "$newsDisplay" == "silent" ]]; then return elif [[ "$newsDisplay" == "notify" ]]; then local msg if [[ $newsNumUnread -eq 1 ]]; then msg="There is an unread and relevant news item.\n" msg+="Read it by running the command '$(basename "$0") news'." else msg="There are $newsNumUnread unread and relevant news items.\n" msg+="Read them by running the command '$(basename "$0") news'." fi # Not actually an error but here stdout is reserved for # nix-build output. errorEcho errorEcho -e "$msg" errorEcho if [[ -v DISPLAY ]] && type -P notify-send > /dev/null; then notify-send "Home Manager" "$msg" fi elif [[ "$newsDisplay" == "show" ]]; then doShowNews --unread else errorEcho "Unknown 'news.display' setting '$newsDisplay'." fi } function doEdit() { if [[ ! -v EDITOR || -z $EDITOR ]]; then errorEcho "Please set the \$EDITOR environment variable" return 1 fi setConfigFile # Don't quote $EDITOR in order to support values including options, e.g., # "code --wait". # # shellcheck disable=2086 exec $EDITOR "$HOME_MANAGER_CONFIG" } function doBuild() { if [[ ! -w . ]]; then errorEcho "Cannot run build in read-only directory"; return 1 fi setFlakeAttribute if [[ -v FLAKE_CONFIG_URI ]]; then local exitCode=0 nix build \ "${PASSTHROUGH_OPTS[@]}" \ ${DRY_RUN+--dry-run} \ ${NO_OUT_LINK+--no-out-link} \ "$FLAKE_CONFIG_URI.activationPackage" \ || exitCode=1 return $exitCode fi setWorkDir local newsInfo newsInfo=$(buildNews) local exitCode doBuildAttr \ ${NO_OUT_LINK+--no-out-link} \ --attr activationPackage \ && exitCode=0 || exitCode=1 presentNews "$newsInfo" return $exitCode } function doSwitch() { setFlakeAttribute if [[ -v FLAKE_CONFIG_URI ]]; then local exitCode=0 nix run \ "${PASSTHROUGH_OPTS[@]}" \ "$FLAKE_CONFIG_URI.activationPackage" \ || exitCode=1 return $exitCode fi setWorkDir local newsInfo newsInfo=$(buildNews) local generation local exitCode=0 # Build the generation and run the activate script. Note, we # specify an output link so that it is treated as a GC root. This # prevents an unfortunately timed GC from removing the generation # before activation completes. generation="$WORK_DIR/generation" doBuildAttr \ --out-link "$generation" \ --attr activationPackage \ && "$generation/activate" || exitCode=1 presentNews "$newsInfo" return $exitCode } function doListGens() { # Whether to colorize the generations output. local color="never" if [[ ! -v NO_COLOR && -t 1 ]]; then color="always" fi pushd "$NIX_STATE_DIR/profiles/per-user/$USER" > /dev/null # shellcheck disable=2012 ls --color=$color -gG --time-style=long-iso --sort time home-manager-*-link \ | cut -d' ' -f 4- \ | sed -E 's/home-manager-([[:digit:]]*)-link/: id \1/' popd > /dev/null } # Removes linked generations. Takes as arguments identifiers of # generations to remove. function doRmGenerations() { setVerboseAndDryRun pushd "$NIX_STATE_DIR/profiles/per-user/$USER" > /dev/null for generationId in "$@"; do local linkName="home-manager-$generationId-link" if [[ ! -e $linkName ]]; then errorEcho "No generation with ID $generationId" elif [[ $linkName == $(readlink home-manager) ]]; then errorEcho "Cannot remove the current generation $generationId" else echo Removing generation $generationId $DRY_RUN_CMD rm $VERBOSE_ARG $linkName fi done popd > /dev/null } function doRmAllGenerations() { $DRY_RUN_CMD rm $VERBOSE_ARG \ "$NIX_STATE_DIR/profiles/per-user/$USER/home-manager"* } function doExpireGenerations() { local profileDir="$NIX_STATE_DIR/profiles/per-user/$USER" local generations generations="$( \ find "$profileDir" -name 'home-manager-*-link' -not -newermt "$1" \ | sed 's/^.*-\([0-9]*\)-link$/\1/' \ )" if [[ -n $generations ]]; then # shellcheck disable=2086 doRmGenerations $generations elif [[ -v VERBOSE ]]; then echo "No generations to expire" fi } function doListPackages() { local outPath outPath="$(nix-env -q --out-path | grep -o '/.*home-manager-path$')" if [[ -n "$outPath" ]] ; then nix-store -q --references "$outPath" | sed 's/[^-]*-//' else errorEcho "No home-manager packages seem to be installed." fi } function newsReadIdsFile() { local dataDir="${XDG_DATA_HOME:-$HOME/.local/share}/home-manager" local path="$dataDir/news-read-ids" # If the path doesn't exist then we should create it, otherwise # Nix will error out when we attempt to use builtins.readFile. if [[ ! -f "$path" ]]; then mkdir -p "$dataDir" touch "$path" fi echo "$path" } # Builds news meta information to be sourced into this script. # # Note, we suppress build output to remove unnecessary verbosity. We # put the output in the work directory to avoid the risk of an # unfortunately timed GC removing it. function buildNews() { local output output="$WORK_DIR/news-info.sh" doBuildAttr \ --out-link "$output" \ --no-build-output \ --quiet \ --arg check false \ --argstr newsReadIdsFile "$(newsReadIdsFile)" \ --attr newsInfo \ > /dev/null echo "$output" } function doShowNews() { setWorkDir local infoFile infoFile=$(buildNews) || return 1 # shellcheck source=/dev/null . "$infoFile" # shellcheck disable=2154 case $1 in --all) ${PAGER:-less} "$newsFileAll" ;; --unread) ${PAGER:-less} "$newsFileUnread" ;; *) errorEcho "Unknown argument $1" return 1 esac # shellcheck disable=2154 if [[ -s "$newsUnreadIdsFile" ]]; then local newsReadIdsFile newsReadIdsFile="$(newsReadIdsFile)" cat "$newsUnreadIdsFile" >> "$newsReadIdsFile" fi } function doUninstall() { setVerboseAndDryRun echo "This will remove Home Manager from your system." if [[ -v DRY_RUN ]]; then echo "This is a dry run, nothing will actually be uninstalled." fi local confirmation read -r -n 1 -p "Really uninstall Home Manager? [y/n] " confirmation echo case $confirmation in y|Y) echo "Switching to empty Home Manager configuration..." HOME_MANAGER_CONFIG="$(mktemp --tmpdir home-manager.XXXXXXXXXX)" echo "{ lib, ... }: { home.file = lib.mkForce {}; }" > "$HOME_MANAGER_CONFIG" doSwitch $DRY_RUN_CMD nix-env -e home-manager-path || true rm "$HOME_MANAGER_CONFIG" $DRY_RUN_CMD rm $VERBOSE_ARG -r \ "${XDG_DATA_HOME:-$HOME/.local/share}/home-manager" $DRY_RUN_CMD rm $VERBOSE_ARG \ "$NIX_STATE_DIR/gcroots/per-user/$USER/current-home" ;; *) echo "Yay!" exit 0 ;; esac local deleteProfiles read -r -n 1 \ -p 'Remove all Home Manager generations? [y/n] ' \ deleteProfiles echo case $deleteProfiles in y|Y) doRmAllGenerations echo "All generations are now eligible for garbage collection." ;; *) echo "Leaving generations but they may still be garbage collected." ;; esac echo "Home Manager is uninstalled but your home.nix is left untouched." } function doHelp() { echo "Usage: $0 [OPTION] COMMAND" echo echo "Options" echo echo " -f FILE The home configuration file." echo " Default is '~/.config/nixpkgs/home.nix'." echo " -A ATTRIBUTE Optional attribute that selects a configuration" echo " expression in the configuration file." echo " -I PATH Add a path to the Nix expression search path." echo " --flake flake-uri Use home-manager configuration at flake-uri" echo " -b EXT Move existing files to new path rather than fail." echo " -v Verbose output" echo " -n Do a dry run, only prints what actions would be taken" echo " -h Print this help" echo echo "Options passed on to nix-build(1)" echo echo " --arg(str) NAME VALUE Override inputs passed to home-manager.nix" echo " --cores NUM" echo " --debug" echo " --keep-failed" echo " --keep-going" echo " -j, --max-jobs NUM" echo " --option NAME VALUE" echo " --show-trace" echo " --(no-)substitute" echo " --no-out-link Do not create a symlink to the output path" echo echo "Commands" echo echo " help Print this help" echo echo " edit Open the home configuration in \$EDITOR" echo echo " option OPTION.NAME" echo " Inspect configuration option named OPTION.NAME." echo echo " build Build configuration into result directory" echo echo " instantiate Instantiate the configuration and print the resulting derivation" echo echo " switch Build and activate configuration" echo echo " generations List all home environment generations" echo echo " remove-generations ID..." echo " Remove indicated generations. Use 'generations' command to" echo " find suitable generation numbers." echo echo " expire-generations TIMESTAMP" echo " Remove generations older than TIMESTAMP where TIMESTAMP is" echo " interpreted as in the -d argument of the date tool. For" echo " example \"-30 days\" or \"2018-01-01\"." echo echo " packages List all packages installed in home-manager-path" echo echo " news Show news entries in a pager" echo echo " uninstall Remove Home Manager" } readonly NIX_STATE_DIR="${NIX_STATE_DIR:-/nix/var/nix}" EXTRA_NIX_PATH=() HOME_MANAGER_CONFIG_ATTRIBUTE="" PASSTHROUGH_OPTS=() COMMAND="" COMMAND_ARGS=() FLAKE_ARG="" while [[ $# -gt 0 ]]; do opt="$1" shift case $opt in build|instantiate|option|edit|expire-generations|generations|help|news|packages|remove-generations|switch|uninstall) COMMAND="$opt" ;; -A) HOME_MANAGER_CONFIG_ATTRIBUTE="$1" shift ;; -I) EXTRA_NIX_PATH+=("$1") shift ;; -b) export HOME_MANAGER_BACKUP_EXT="$1" shift ;; -f|--file) HOME_MANAGER_CONFIG="$1" shift ;; --flake) FLAKE_ARG="$1" shift ;; --recreate-lock-file|--no-update-lock-file|--no-write-lock-file|--no-registries|--commit-lock-file) PASSTHROUGH_OPTS+=("$opt") ;; --update-input) PASSTHROUGH_OPTS+=("$opt" "$1") shift ;; --override-input) PASSTHROUGH_OPTS+=("$opt" "$1" "$2") shift 2 ;; --no-out-link) NO_OUT_LINK=1 ;; -h|--help) doHelp exit 0 ;; -n|--dry-run) export DRY_RUN=1 ;; --option|--arg|--argstr) PASSTHROUGH_OPTS+=("$opt" "$1" "$2") shift 2 ;; -j|--max-jobs|--cores) PASSTHROUGH_OPTS+=("$opt" "$1") shift ;; --debug|--keep-failed|--keep-going|--show-trace\ |--substitute|--no-substitute) PASSTHROUGH_OPTS+=("$opt") ;; -v|--verbose) export VERBOSE=1 ;; --version) echo 21.11 exit 0 ;; *) case $COMMAND in expire-generations|remove-generations|option) COMMAND_ARGS+=("$opt") ;; *) errorEcho "$0: unknown option '$opt'" errorEcho "Run '$0 --help' for usage help" exit 1 ;; esac ;; esac done if [[ -z $COMMAND ]]; then doHelp >&2 exit 1 fi case $COMMAND in edit) doEdit ;; build) doBuild ;; instantiate) doInstantiate ;; switch) doSwitch ;; generations) doListGens ;; remove-generations) doRmGenerations "${COMMAND_ARGS[@]}" ;; expire-generations) if [[ ${#COMMAND_ARGS[@]} != 1 ]]; then errorEcho "expire-generations expects one argument, got ${#COMMAND_ARGS[@]}." exit 1 else doExpireGenerations "${COMMAND_ARGS[@]}" fi ;; option) doInspectOption "${COMMAND_ARGS[@]}" ;; packages) doListPackages ;; news) doShowNews --all ;; uninstall) doUninstall ;; help) doHelp ;; *) errorEcho "Unknown command: $COMMAND" doHelp >&2 exit 1 ;; esac # vim: ft=bash