dc227b579d
This allows to install home-manager on a system without root access. PR #1196
575 lines
15 KiB
Bash
575 lines
15 KiB
Bash
#!@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 doInstantiate() {
|
|
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"
|
|
}
|
|
|
|
# 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
|
|
|
|
doBuildAttr --attr activationPackage \
|
|
&& exitCode=0 || exitCode=1
|
|
|
|
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"
|
|
|
|
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 [[ -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
|
|
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 " -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 " 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=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
opt="$1"
|
|
shift
|
|
case $opt in
|
|
build|instantiate|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
|
|
;;
|
|
-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
|
|
;;
|
|
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
|
|
;;
|
|
packages)
|
|
doListPackages
|
|
;;
|
|
news)
|
|
doShowNews --all
|
|
;;
|
|
uninstall)
|
|
doUninstall
|
|
;;
|
|
help)
|
|
doHelp
|
|
;;
|
|
*)
|
|
errorEcho "Unknown command: $COMMAND"
|
|
doHelp >&2
|
|
exit 1
|
|
;;
|
|
esac
|