#!@bash@/bin/bash # Prepare to use tools from Nixpkgs. PATH=@coreutils@/bin:@findutils@/bin:@gnused@/bin:@less@/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 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 # shellcheck disable=2086 if [[ -v USE_NIX2_COMMAND ]]; then nix build \ -f "" \ $extraArgs \ "${PASSTHROUGH_OPTS[@]}" \ --argstr confPath "$HOME_MANAGER_CONFIG" \ --argstr confAttr "$HOME_MANAGER_CONFIG_ATTRIBUTE" else nix-build \ "" \ $extraArgs \ "${PASSTHROUGH_OPTS[@]}" \ --argstr confPath "$HOME_MANAGER_CONFIG" \ --argstr confAttr "$HOME_MANAGER_CONFIG_ATTRIBUTE" fi } # 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 exec "$EDITOR" "$HOME_MANAGER_CONFIG" } function doBuild() { if [[ ! -w . ]]; then errorEcho "Cannot run build in read-only directory"; return 1 fi setWorkDir local newsInfo newsInfo=$(buildNews) local exitCode if [[ -v USE_NIX2_COMMAND ]]; then doBuildAttr activationPackage \ && exitCode=0 || exitCode=1 else doBuildAttr --attr activationPackage \ && exitCode=0 || exitCode=1 fi presentNews "$newsInfo" return $exitCode } function doSwitch() { 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" if [[ -v USE_NIX2_COMMAND ]]; then doBuildAttr \ --out-link "$generation" \ activationPackage \ && "$generation/activate" || exitCode=1 else doBuildAttr \ --out-link "$generation" \ --attr activationPackage \ && "$generation/activate" || exitCode=1 fi presentNews "$newsInfo" return $exitCode } function doListGens() { # Whether to colorize the generations output. local color="never" if [[ -t 1 ]]; then color="always" fi pushd "/nix/var/nix/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/var/nix/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/var/nix/profiles/per-user/$USER/home-manager"* } function doExpireGenerations() { local profileDir="/nix/var/nix/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" if [[ -v USE_NIX2_COMMAND ]]; then doBuildAttr \ --out-link "$output" \ --quiet \ --arg check false \ --argstr newsReadIdsFile "$(newsReadIdsFile)" \ newsInfo else doBuildAttr \ --out-link "$output" \ --no-build-output \ --quiet \ --arg check false \ --argstr newsReadIdsFile "$(newsReadIdsFile)" \ --attr newsInfo \ > /dev/null fi 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 "{}" > "$HOME_MANAGER_CONFIG" doSwitch 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/var/nix/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 " -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 " --cores NUM" echo " --keep-failed" echo " --keep-going" echo " --max-jobs NUM" echo " --option NAME VALUE" echo " --show-trace" echo " --(no-)substitute" echo echo "Commands" echo echo " help Print this help" echo echo " edit Open the home configuration in \$EDITOR" echo echo " build Build configuration into result directory" 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" } EXTRA_NIX_PATH=() HOME_MANAGER_CONFIG_ATTRIBUTE="" PASSTHROUGH_OPTS=() COMMAND="" COMMAND_ARGS=() while [[ $# -gt 0 ]]; do opt="$1" shift case $opt in build|edit|expire-generations|generations|help|news|packages|remove-generations|switch|uninstall) COMMAND="$opt" ;; -2) USE_NIX2_COMMAND=1 ;; -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 ;; -h|--help) doHelp exit 0 ;; -n|--dry-run) export DRY_RUN=1 ;; --option) PASSTHROUGH_OPTS+=("$opt" "$1" "$2") shift 2 ;; --max-jobs|--cores) PASSTHROUGH_OPTS+=("$opt" "$1") shift ;; --keep-failed|--keep-going|--show-trace\ |--substitute|--no-substitute) PASSTHROUGH_OPTS+=("$opt") ;; -v|--verbose) export VERBOSE=1 ;; *) case $COMMAND in expire-generations|remove-generations) 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 ;; 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 ;; packages) doListPackages ;; news) doShowNews --all ;; uninstall) doUninstall ;; help) doHelp ;; *) errorEcho "Unknown command: $COMMAND" doHelp >&2 exit 1 ;; esac