#!@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 errMissingOptArg() { # translators: For example: "home-manager: missing argument for --cores" _iError "%s: missing argument for %s" "$0" "$1" >&2 exit 1 } function setNixProfileCommands() { if [[ -e $HOME/.nix-profile/manifest.json \ || -e ${XDG_STATE_HOME:-$HOME/.local/state}/nix/profile/manifest.json ]] ; then LIST_OUTPATH_CMD="nix profile list" else LIST_OUTPATH_CMD="nix-env -q --out-path" fi } function setVerboseArg() { if [[ -v VERBOSE ]]; then export VERBOSE_ARG="--verbose" else export VERBOSE_ARG="" 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 } # Checks whether the 'flakes' and 'nix-command' Nix options are enabled. function hasFlakeSupport() { type -p nix > /dev/null \ && nix show-config 2> /dev/null \ | grep experimental-features \ | grep flakes \ | grep -q nix-command } # 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 HOME_MANAGER_CONFIG="$(realpath "$HOME_MANAGER_CONFIG")" else _i 'No configuration file found at %s' \ "$HOME_MANAGER_CONFIG" >&2 exit 1 fi elif [[ ! -v HOME_MANAGER_CONFIG ]]; then local configHome="${XDG_CONFIG_HOME:-$HOME/.config}" local hmConfigHome="$configHome/home-manager" local nixpkgsConfigHome="$configHome/nixpkgs" local defaultConfFile="$hmConfigHome/home.nix" local configFile if [[ -e "$defaultConfFile" ]]; then configFile="$defaultConfFile" elif [[ -e "$nixpkgsConfigHome/home.nix" ]]; then configFile="$nixpkgsConfigHome/home.nix" # translators: The first '%s' specifier will be replaced by either # 'home.nix' or 'flake.nix'. _iWarn $'Keeping your Home Manager %s in %s is deprecated,\nplease move it to %s' \ 'home.nix' "$nixpkgsConfigHome" "$hmConfigHome" >&2 elif [[ -e "$HOME/.nixpkgs/home.nix" ]]; then configFile="$HOME/.nixpkgs/home.nix" _iWarn $'Keeping your Home Manager %s in %s is deprecated,\nplease move it to %s' \ 'home.nix' "$HOME/.nixpkgs" "$hmConfigHome" >&2 fi if [[ -v configFile ]]; then HOME_MANAGER_CONFIG="$(realpath "$configFile")" else _i 'No configuration file found. Please create one at %s' \ "$defaultConfFile" >&2 exit 1 fi fi } function setHomeManagerNixPath() { local path="@HOME_MANAGER_PATH@" if [[ -n "$path" ]] ; then if [[ -e "$path" || "$path" =~ ^https?:// ]] ; then EXTRA_NIX_PATH+=("home-manager=$path") return else _iWarn 'Home Manager not found at %s.' "$path" fi fi for p in "${XDG_CONFIG_HOME:-$HOME/.config}/nixpkgs/home-manager" \ "$HOME/.nixpkgs/home-manager" ; do if [[ -e "$p" ]] ; then # translators: This message will be seen by very few users that likely are familiar with English. So feel free to leave this untranslated. _iWarn $'The fallback Home Manager path %s has been deprecated and a file/directory was found there.' \ "$p" # translators: This message will be seen by very few users that likely are familiar with English. So feel free to leave this untranslated. _i $'To remove this warning, do one of the following. 1. Explicitly tell Home Manager to use the path, for example by adding { programs.home-manager.path = "%s"; } to your configuration. If you import Home Manager directly, you can use the `path` parameter pkgs.callPackage /path/to/home-manager-package { path = "%s"; } when calling the Home Manager package. 2. Remove the deprecated path. $ rm -r "%s"' "$p" "$p" "$p" fi done } # Sets some useful Home Manager related paths as global read-only variables. function setHomeManagerPathVariables() { # If called twice then just exit early. if [[ -v HM_DATA_HOME ]]; then return fi _iVerbose "Sanity checking Nix" nix-build --quiet --expr '{}' --no-out-link > /dev/null 2>&1 || true nix-env -q > /dev/null 2>&1 || true declare -r globalNixStateDir="${NIX_STATE_DIR:-/nix/var/nix}" declare -r globalProfilesDir="$globalNixStateDir/profiles/per-user/$USER" declare -r globalGcrootsDir="$globalNixStateDir/gcroots/per-user/$USER" declare -r stateHome="${XDG_STATE_HOME:-$HOME/.local/state}" declare -r userNixStateDir="$stateHome/nix" declare -gr HM_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}/home-manager" declare -gr HM_STATE_DIR="$stateHome/home-manager" declare -gr HM_GCROOT_LEGACY_PATH="$globalGcrootsDir/current-home" if [[ -d $userNixStateDir/profiles ]]; then declare -gr HM_PROFILE_DIR="$userNixStateDir/profiles" elif [[ -d $globalProfilesDir ]]; then declare -gr HM_PROFILE_DIR="$globalProfilesDir" else _iError 'Could not find suitable profile directory, tried %s and %s' \ "$userNixStateDir/profiles" "$globalProfilesDir" >&2 exit 1 fi } function setFlakeAttribute() { if [[ -z $FLAKE_ARG && ! -v HOME_MANAGER_CONFIG ]]; then local configHome="${XDG_CONFIG_HOME:-$HOME/.config}" local hmConfigHome="$configHome/home-manager" local nixpkgsConfigHome="$configHome/nixpkgs" local configFlake if [[ -e "$hmConfigHome/flake.nix" ]]; then configFlake="$hmConfigHome/flake.nix" elif [[ -e "$nixpkgsConfigHome/flake.nix" ]]; then configFlake="$nixpkgsConfigHome/flake.nix" _iWarn $'Keeping your Home Manager %s in %s is deprecated,\nplease move it to %s' \ 'flake.nix' "$nixpkgsConfigHome" "$hmConfigHome" >&2 fi if [[ -v configFlake ]]; then FLAKE_ARG="path:$(dirname "$(readlink -f "$configFlake")")" fi fi if [[ -n "$FLAKE_ARG" ]]; then local flake="${FLAKE_ARG%#*}" case $FLAKE_ARG in *#*) local name="${FLAKE_ARG#*#}" ;; *) local name="$USER" # Check both long and short hostnames; long first to preserve # pre-existing behaviour in case both happen to be defined. for n in "$USER@$(hostname)" "$USER@$(hostname -s)"; do if [[ "$(nix eval "$flake#homeConfigurations" --apply "x: x ? \"$n\"")" == "true" ]]; then name="$n" if [[ -v VERBOSE ]]; then echo "Using flake homeConfiguration for $name" fi fi done ;; esac export FLAKE_CONFIG_URI="$flake#homeConfigurations.\"$name\"" fi } function doInspectOption() { setFlakeAttribute if [[ -v FLAKE_CONFIG_URI ]]; then # translators: Here "flake" is a noun that refers to the Nix Flakes feature. _iError "Can't inspect options of a flake configuration" exit 1 fi setConfigFile 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 doInit() { # The directory where we should place the initial configuration. local confDir # Whether we should immediate activate the configuration. local switch # Whether we should create a flake file. local withFlake if hasFlakeSupport; then withFlake=1 fi local homeManagerUrl="github:nix-community/home-manager" local nixpkgsUrl="github:nixos/nixpkgs/nixos-unstable" while (( $# > 0 )); do local opt="$1" shift case $opt in --no-flake) unset withFlake ;; --switch) switch=1 ;; --home-manager-url) [[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt" homeManagerUrl="$1" shift ;; --nixpkgs-url) [[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt" nixpkgsUrl="$1" shift ;; -*) _iError "%s: unknown option '%s'" "$0" "$opt" >&2 exit 1 ;; *) if [[ -v confDir ]]; then _i "Run '%s --help' for usage help" "$0" >&2 exit 1 else confDir="$opt" fi ;; esac done if [[ ! -v confDir ]]; then confDir="${XDG_CONFIG_HOME:-$HOME/.config}/home-manager" fi if [[ ! -e $confDir ]]; then mkdir -p "$confDir" fi if [[ ! -d $confDir ]]; then _iError "%s: unknown option '%s'" "$0" "$opt" >&2 exit 1 fi local confFile="$confDir/home.nix" local flakeFile="$confDir/flake.nix" if [[ -e $confFile ]]; then _i 'The file %s already exists, leaving it unchanged...' "$confFile" else _i 'Creating %s...' "$confFile" local nl=$'\n' local xdgVars="" if [[ -v XDG_CACHE_HOME && $XDG_CACHE_HOME != "$HOME/.cache" ]]; then xdgVars="$xdgVars xdg.cacheHome = \"$XDG_CACHE_HOME\";$nl" fi if [[ -v XDG_CONFIG_HOME && $XDG_CONFIG_HOME != "$HOME/.config" ]]; then xdgVars="$xdgVars xdg.configHome = \"$XDG_CONFIG_HOME\";$nl" fi if [[ -v XDG_DATA_HOME && $XDG_DATA_HOME != "$HOME/.local/share" ]]; then xdgVars="$xdgVars xdg.dataHome = \"$XDG_DATA_HOME\";$nl" fi if [[ -v XDG_STATE_HOME && $XDG_STATE_HOME != "$HOME/.local/state" ]]; then xdgVars="$xdgVars xdg.stateHome = \"$XDG_STATE_HOME\";$nl" fi mkdir -p "$confDir" cat > "$confFile" < "$flakeFile" <&2 exit 1 fi setConfigFile 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 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" } function doBuildFlake() { local extraArgs=("$@") if [[ -v VERBOSE ]]; then extraArgs=("${extraArgs[@]}" "--verbose") fi nix build \ "${extraArgs[@]}" \ "${PASSTHROUGH_OPTS[@]}" } # Presents news to the user as specified by the `news.display` option. function presentNews() { local newsNixFile="$WORK_DIR/news.nix" buildNews "$newsNixFile" local newsDisplay newsDisplay="$(nix-instantiate --eval --expr "(import ${newsNixFile}).meta.display" | xargs)" local newsNumUnread newsNumUnread="$(nix-instantiate --eval --expr "(import ${newsNixFile}).meta.numUnread" | xargs)" # 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" > /dev/null 2>&1 || true fi elif [[ "$newsDisplay" == "show" ]]; then doShowNews --unread else _i 'Unknown "news.display" setting "%s".' "$newsDisplay" >&2 fi } function doEdit() { if [[ ! -v VISUAL || -z $VISUAL ]]; then if [[ ! -v EDITOR || -z $EDITOR ]]; then # shellcheck disable=2016 _i 'Please set the $EDITOR or $VISUAL environment variable' >&2 return 1 fi else EDITOR=$VISUAL 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} \ ${PRINT_BUILD_LOGS+--print-build-logs} \ || return else doBuildAttr \ ${NO_OUT_LINK+--no-out-link} \ --attr activationPackage \ || return fi presentNews } 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" \ ${PRINT_BUILD_LOGS+--print-build-logs} \ && "$generation/activate" || return else doBuildAttr \ --out-link "$generation" \ --attr activationPackage \ && "$generation/activate" || return fi presentNews } function doListGens() { setHomeManagerPathVariables # Whether to colorize the generations output. local color="never" if [[ ! -v NO_COLOR && -t 1 ]]; then color="always" fi pushd "$HM_PROFILE_DIR" > /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() { setHomeManagerPathVariables setVerboseArg pushd "$HM_PROFILE_DIR" > /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" run rm $VERBOSE_ARG $linkName fi done popd > /dev/null } function doExpireGenerations() { setHomeManagerPathVariables local generations generations="$( \ find "$HM_PROFILE_DIR" -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() { setNixProfileCommands local outPath outPath="$($LIST_OUTPATH_CMD | grep -o '/.*home-manager-path$')" if [[ -n "$outPath" ]] ; then nix-store -q --references "$outPath" | sed 's/[^-]*-//' | sort --ignore-case 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 # Remove duplicate slashes in case $HOME or $XDG_DATA_HOME have a trailing # slash. Double slashes causes Nix to error out with # # error: syntax error, unexpected PATH_END, expecting DOLLAR_CURLY". echo "$path" | tr -s / } # Builds the Home Manager news data file. # # 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 newsNixFile="$1" local newsJsonFile="$WORK_DIR/news.json" if [[ -v FLAKE_CONFIG_URI ]]; then # TODO: Use check=false to make it more likely that the build succeeds. doBuildFlake \ "$FLAKE_CONFIG_URI.config.news.json.output" \ --quiet \ --out-link "$newsJsonFile" \ || return else doBuildAttr \ --out-link "$newsJsonFile" \ --arg check false \ --attr config.news.json.output \ > /dev/null \ || return fi local extraArgs=() for p in "${EXTRA_NIX_PATH[@]}"; do extraArgs=("${extraArgs[@]}" "-I" "$p") done local readIdsFile readIdsFile="$(newsReadIdsFile)" nix-instantiate \ --no-build-output --strict \ --eval '' \ --arg newsJsonFile "$newsJsonFile" \ --arg newsReadIdsFile "$readIdsFile" \ "${extraArgs[@]}" \ > "$newsNixFile" } function doShowNews() { setWorkDir setFlakeAttribute local newsNixFile="$WORK_DIR/news.nix" buildNews "$newsNixFile" local readIdsFile readIdsFile="$(newsReadIdsFile)" local news # shellcheck disable=2154,2046 case $1 in --all) news="$(nix-instantiate --quiet --eval --expr "(import ${newsNixFile}).news.all")" ;; --unread) news="$(nix-instantiate --quiet --eval --expr "(import ${newsNixFile}).news.unread")" ;; *) _i 'Unknown argument %s' "$1" return 1 esac # Prints the news without surrounding quotes. echo -e "${news:1:-1}" | ${PAGER:-less} local allIds allIds="$(nix-instantiate --quiet --eval --expr "(import ${newsNixFile}).meta.ids")" allIds="${allIds:1:-1}" # Trim surrounding quotes. local readIdsFileNew="$WORK_DIR/news-read-ids.new" { cat "$readIdsFile" echo -e "$allIds" } | sort | uniq > "$readIdsFileNew" mv -f "$readIdsFileNew" "$readIdsFile" } function doUninstall() { setHomeManagerPathVariables setNixProfileCommands _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 # shellcheck disable=2086 case $confirmation in y|Y) _i "Switching to empty Home Manager configuration..." HOME_MANAGER_CONFIG="$(mktemp --tmpdir home-manager.XXXXXXXXXX)" cat > "$HOME_MANAGER_CONFIG" <&2 _i "Run '%s --help' for usage help" "$0" >&2 exit 1 ;; esac ;; esac done setHomeManagerNixPath if [[ -z $COMMAND ]]; then doHelp >&2 exit 1 fi case $COMMAND in edit) doEdit ;; build) doBuild ;; init) doInit "${COMMAND_ARGS[@]}" ;; 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