home-manager/home-manager/home-manager

709 lines
19 KiB
Bash

#!@bash@/bin/bash
# Prepare to use tools from Nixpkgs.
PATH=@DEP_PATH@${PATH:+:}$PATH
set -euo pipefail
export TEXTDOMAIN=home-manager
export TEXTDOMAINDIR=@OUT@/share/locale
# shellcheck disable=1091
source @HOME_MANAGER_LIB@
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
_i "No configuration file found at %s" \
"$HOME_MANAGER_CONFIG" >&2
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
_i "No configuration file found. Please create one at %s" \
"$defaultConfFile" >&2
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
_iError "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 <home-manager/modules> {"
modulesExpr+=" configuration = if confAttr == \"\" then confPath else (import confPath).\${confAttr};"
modulesExpr+=" pkgs = import <nixpkgs> {}; check = true; })"
nixos-option \
--options_expr "$modulesExpr.options" \
--config_expr "$modulesExpr.config" \
"${extraArgs[@]}" \
"${PASSTHROUGH_OPTS[@]}"
}
function doInstantiate() {
setFlakeAttribute
if [[ -v FLAKE_CONFIG_URI ]]; then
_i "Can't instantiate a flake configuration" >&2
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 \
"<home-manager/home-manager/home-manager.nix>" \
"${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 \
"<home-manager/home-manager/home-manager.nix>" \
"${extraArgs[@]}" \
"${PASSTHROUGH_OPTS[@]}" \
--argstr confPath "$HOME_MANAGER_CONFIG" \
--argstr confAttr "$HOME_MANAGER_CONFIG_ATTRIBUTE"
}
function doBuildFlake() {
local extraArgs=("$@")
if [[ -v VERBOSE ]]; then
extraArgs=("${extraArgs[@]}" "--verbose")
fi
nix build \
"${extraArgs[@]}" \
"${PASSTHROUGH_OPTS[@]}"
}
# 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 cmd msg
cmd="$(basename "$0")"
msg="$(_ip \
$'There is %d unread and relevant news item.\nRead it by running the command "%s news".' \
$'There are %d unread and relevant news items.\nRead them by running the command "%s news".' \
"$newsNumUnread" "$newsNumUnread" "$cmd")"
# Not actually an error but here stdout is reserved for
# nix-build output.
echo $'\n'"$msg"$'\n' >&2
if [[ -v DISPLAY ]] && type -P notify-send > /dev/null; then
notify-send "Home Manager" "$msg"
fi
elif [[ "$newsDisplay" == "show" ]]; then
doShowNews --unread
else
_i 'Unknown "news.display" setting "%s".' "$newsDisplay" >&2
fi
}
function doEdit() {
if [[ ! -v EDITOR || -z $EDITOR ]]; then
# shellcheck disable=2016
_i 'Please set the $EDITOR environment variable' >&2
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
_i 'Cannot run build in read-only directory' >&2
return 1
fi
setWorkDir
setFlakeAttribute
if [[ -v FLAKE_CONFIG_URI ]]; then
doBuildFlake \
"$FLAKE_CONFIG_URI.activationPackage" \
${DRY_RUN+--dry-run} \
${NO_OUT_LINK+--no-link} \
|| return
else
doBuildAttr \
${NO_OUT_LINK+--no-out-link} \
--attr activationPackage \
|| return
local newsInfo
newsInfo=$(buildNews)
presentNews "$newsInfo"
fi
}
function doSwitch() {
setWorkDir
local generation
# 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"
setFlakeAttribute
if [[ -v FLAKE_CONFIG_URI ]]; then
doBuildFlake \
"$FLAKE_CONFIG_URI.activationPackage" \
--out-link "$generation" \
&& "$generation/activate" || return
else
doBuildAttr \
--out-link "$generation" \
--attr activationPackage \
&& "$generation/activate" || return
local newsInfo
newsInfo=$(buildNews)
presentNews "$newsInfo"
fi
}
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
_i 'No generation with ID %s' "$generationId" >&2
elif [[ $linkName == $(readlink home-manager) ]]; then
_i 'Cannot remove the current generation %s' "$generationId" >&2
else
_i 'Removing generation %s' "$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
_i "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
_i 'No home-manager packages seem to be installed.' >&2
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"
;;
*)
_i 'Unknown argument %s' "$1"
return 1
esac
# shellcheck disable=2154
if [[ -s "$newsUnreadIdsFile" ]]; then
local newsReadIdsFile
newsReadIdsFile="$(newsReadIdsFile)"
cat "$newsUnreadIdsFile" >> "$newsReadIdsFile"
fi
}
function doUninstall() {
setVerboseAndDryRun
_i 'This will remove Home Manager from your system.'
if [[ -v DRY_RUN ]]; then
_i 'This is a dry run, nothing will actually be uninstalled.'
fi
local confirmation
read -r -n 1 -p "$(_i 'Really uninstall Home Manager?') [y/n] " confirmation
echo
case $confirmation in
y|Y)
_i "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"
;;
*)
_i "Yay!"
exit 0
;;
esac
local deleteProfiles
read -r -n 1 \
-p "$(_i 'Remove all Home Manager generations?') [y/n] " \
deleteProfiles
echo
case $deleteProfiles in
y|Y)
doRmAllGenerations
_i 'All generations are now eligible for garbage collection.'
;;
*)
_i 'Leaving generations but they may still be garbage collected.'
;;
esac
_i "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 " --impure"
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 " --no-write-lock-file"
echo " --builders VALUE"
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|--builders)
PASSTHROUGH_OPTS+=("$opt" "$1")
shift
;;
--debug|--keep-failed|--keep-going|--show-trace\
|--substitute|--no-substitute|--impure)
PASSTHROUGH_OPTS+=("$opt")
;;
-v|--verbose)
export VERBOSE=1
;;
--version)
echo 22.05
exit 0
;;
*)
case $COMMAND in
expire-generations|remove-generations|option)
COMMAND_ARGS+=("$opt")
;;
*)
_iError "%s: unknown option '%s'" "$0" "$opt" >&2
_i "Run '%s --help' for usage help" "$0" >&2
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
_i 'expire-generations expects one argument, got %d.' "${#COMMAND_ARGS[@]}" >&2
exit 1
else
doExpireGenerations "${COMMAND_ARGS[@]}"
fi
;;
option)
doInspectOption "${COMMAND_ARGS[@]}"
;;
packages)
doListPackages
;;
news)
doShowNews --all
;;
uninstall)
doUninstall
;;
help)
doHelp
;;
*)
_iError 'Unknown command: %s' "$COMMAND" >&2
doHelp >&2
exit 1
;;
esac
# vim: ft=bash