2.home-manager/home-manager/home-manager
Graham Christensen 85c513aa86
home-manager: Feature test for flake support (#6824)
Feature testing flakes / nix-command is more robust over configuration
sniffing. Ultimately, the deciding factor should be if flakes work --
not if the config looks like they will / won't work.

This alternative test both asserts that the `nix` command is enabled,
and that flakes are enabled, without depending on whether or not
flakes are emitted as an experimental feature.

This is both repairing support for Determinate Nix 3, and prepares for
a potential future where Nix itself considers Flakes stable.

Closes #6702
2025-04-15 11:48:20 -05:00

1126 lines
34 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 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
}
# Check to see if flakes are functionally available.
function hasFlakeSupport() {
nix eval --expr 'builtins.getFlake' > /dev/null 2>&1
}
# Escape string for use in Nix files.
function escapeForNix() {
printf %s "$1" | sed 's/["$\\]/\\\0/g'
}
# 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="$(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 FQDN, long, and short hostnames; long first to preserve
# pre-existing behaviour in case both happen to be defined.
for n in "$USER@$(hostname -f)" "$USER@$(hostname)" "$USER@$(hostname -s)"; do
if [[ "$(nix eval "$flake#homeConfigurations" --apply "x: x ? \"$(escapeForNix "$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.\"$(printf %s "$name" | jq -sRr @uri)\""
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 <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 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" <<EOF
{ config, pkgs, ... }:
{
# Home Manager needs a bit of information about you and the paths it should
# manage.
home.username = "$(escapeForNix "$USER")";
home.homeDirectory = "$(escapeForNix "$HOME")";
$xdgVars
# This value determines the Home Manager release that your configuration is
# compatible with. This helps avoid breakage when a new Home Manager release
# introduces backwards incompatible changes.
#
# You should not change this value, even if you update Home Manager. If you do
# want to update the value, then make sure to first check the Home Manager
# release notes.
home.stateVersion = "24.11"; # Please read the comment before changing.
# The home.packages option allows you to install Nix packages into your
# environment.
home.packages = [
# # Adds the 'hello' command to your environment. It prints a friendly
# # "Hello, world!" when run.
# pkgs.hello
# # It is sometimes useful to fine-tune packages, for example, by applying
# # overrides. You can do that directly here, just don't forget the
# # parentheses. Maybe you want to install Nerd Fonts with a limited number of
# # fonts?
# (pkgs.nerdfonts.override { fonts = [ "FantasqueSansMono" ]; })
# # You can also create simple shell scripts directly inside your
# # configuration. For example, this adds a command 'my-hello' to your
# # environment:
# (pkgs.writeShellScriptBin "my-hello" ''
# echo "Hello, \${config.home.username}!"
# '')
];
# Home Manager is pretty good at managing dotfiles. The primary way to manage
# plain files is through 'home.file'.
home.file = {
# # Building this configuration will create a copy of 'dotfiles/screenrc' in
# # the Nix store. Activating the configuration will then make '~/.screenrc' a
# # symlink to the Nix store copy.
# ".screenrc".source = dotfiles/screenrc;
# # You can also set the file content immediately.
# ".gradle/gradle.properties".text = ''
# org.gradle.console=verbose
# org.gradle.daemon.idletimeout=3600000
# '';
};
# Home Manager can also manage your environment variables through
# 'home.sessionVariables'. These will be explicitly sourced when using a
# shell provided by Home Manager. If you don't want to manage your shell
# through Home Manager then you have to manually source 'hm-session-vars.sh'
# located at either
#
# ~/.nix-profile/etc/profile.d/hm-session-vars.sh
#
# or
#
# ~/.local/state/nix/profiles/profile/etc/profile.d/hm-session-vars.sh
#
# or
#
# /etc/profiles/per-user/$USER/etc/profile.d/hm-session-vars.sh
#
home.sessionVariables = {
# EDITOR = "emacs";
};
# Let Home Manager install and manage itself.
programs.home-manager.enable = true;
}
EOF
fi
if [[ ! -v withFlake ]]; then
HOME_MANAGER_CONFIG="$confFile"
else
FLAKE_ARG="$confDir"
if [[ -e $flakeFile ]]; then
_i 'The file %s already exists, leaving it unchanged...' "$flakeFile"
else
_i 'Creating %s...' "$flakeFile"
local nixSystem
nixSystem=$(nix eval --expr builtins.currentSystem --raw --impure)
mkdir -p "$confDir"
cat > "$flakeFile" <<EOF
{
description = "Home Manager configuration of $(escapeForNix "$USER")";
inputs = {
# Specify the source of Home Manager and Nixpkgs.
nixpkgs.url = "$nixpkgsUrl";
home-manager = {
url = "$homeManagerUrl";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { nixpkgs, home-manager, ... }:
let
system = "$nixSystem";
pkgs = nixpkgs.legacyPackages.\${system};
in {
homeConfigurations."$(escapeForNix "$USER")" = home-manager.lib.homeManagerConfiguration {
inherit pkgs;
# Specify your home configuration modules here, for example,
# the path to your home.nix.
modules = [ ./home.nix ];
# Optionally use extraSpecialArgs
# to pass through arguments to home.nix
};
};
}
EOF
fi
fi
if [[ -v switch ]]; then
echo
_i "Creating initial Home Manager generation..."
echo
if doSwitch; then
# translators: The "%s" specifier will be replaced by a file path.
_i $'All done! The home-manager tool should now be installed and you can edit\n\n %s\n\nto configure Home Manager. Run \'man home-configuration.nix\' to\nsee all available options.' \
"$confFile"
exit 0
else
# translators: The "%s" specifier will be replaced by a URL.
_i $'Uh oh, the installation failed! Please create an issue at\n\n %s\n\nif the error seems to be the fault of Home Manager.' \
"https://github.com/nix-community/home-manager/issues"
exit 1
fi
fi
}
function doInstantiate() {
setFlakeAttribute
if [[ -v FLAKE_CONFIG_URI ]]; then
# translators: Here "flake" is a noun that refers to the Nix Flakes feature.
_i "Can't instantiate a flake configuration" >&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 \
"<home-manager/home-manager/home-manager.nix>" \
"${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 \
"<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 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 '<home-manager/home-manager/build-news.nix>' \
--arg newsJsonFile "\"$(escapeForNix "$newsJsonFile")\"" \
--arg newsReadIdsFile "\"$(escapeForNix "$readIdsFile")\"" \
"${extraArgs[@]}" \
> "$newsNixFile"
}
function doShowNews() {
setWorkDir
setFlakeAttribute
local newsNixFile="$WORK_DIR/news.nix"
buildNews "$newsNixFile"
local readIdsFile
readIdsFile="$(newsReadIdsFile)"
local newsAttr
case $1 in
--all)
newsAttr="all"
;;
--unread)
newsAttr="unread"
;;
*)
_i 'Unknown argument %s' "$1"
return 1
esac
nix-instantiate --quiet --eval --json --expr "(import ${newsNixFile}).news.$newsAttr" \
| jq -r . \
| ${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" <<EOF
{
uninstall = true;
home.username = "$(escapeForNix "$USER")";
home.homeDirectory = "$(escapeForNix "$HOME")";
home.stateVersion = "24.11";
}
EOF
# shellcheck disable=2064
trap "rm '$HOME_MANAGER_CONFIG'" EXIT
doSwitch --switch
;;
*)
_i "Yay!"
exit 0
;;
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 " Default is '~/.config/home-manager'."
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 " --version Print the Home Manager version"
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 " -L, --print-build-logs"
echo " --log-format FORMAT"
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 " --refresh Consider all previously downloaded files out-of-date"
echo
echo "Commands"
echo
echo " help Print this help"
echo
echo " edit Open the home configuration in \$VISUAL or \$EDITOR"
echo
echo " option OPTION.NAME"
echo " Inspect configuration option named OPTION.NAME."
echo
echo " build Build configuration into result directory"
echo
echo " init [--switch] [DIR]"
echo " Initializes a configuration in the given directory. If the directory"
echo " does not exist, then it will be created. The default directory is"
echo " '~/.config/home-manager'."
echo
echo " --switch Immediately activate the generated configuration."
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"
}
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|init|instantiate|option|edit|expire-generations|generations|help|news|packages|remove-generations|switch|uninstall)
COMMAND="$opt"
;;
-A)
[[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt"
HOME_MANAGER_CONFIG_ATTRIBUTE="$1"
shift
;;
-I)
[[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt"
EXTRA_NIX_PATH+=("$1")
shift
;;
-b)
[[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt"
export HOME_MANAGER_BACKUP_EXT="$1"
shift
;;
-f|--file)
[[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt"
HOME_MANAGER_CONFIG="$1"
shift
;;
--flake)
[[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt"
FLAKE_ARG="$1"
shift
;;
--recreate-lock-file|--no-update-lock-file|--no-write-lock-file|--no-registries|--commit-lock-file|--refresh)
PASSTHROUGH_OPTS+=("$opt")
;;
--update-input)
[[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt"
PASSTHROUGH_OPTS+=("$opt" "$1")
shift
;;
--override-input)
[[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt"
[[ -v 2 && $2 != -* ]] || errMissingOptArg "$opt $1"
PASSTHROUGH_OPTS+=("$opt" "$1" "$2")
shift 2
;;
--experimental-features)
[[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt"
PASSTHROUGH_OPTS+=("$opt" "$1")
shift
;;
--extra-experimental-features)
[[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt"
PASSTHROUGH_OPTS+=("$opt" "$1")
shift
;;
--no-out-link)
NO_OUT_LINK=1
;;
-L|--print-build-logs)
PRINT_BUILD_LOGS=1
;;
-h|--help)
doHelp
exit 0
;;
-n|--dry-run)
export DRY_RUN=1
;;
--option|--arg|--argstr)
[[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt"
[[ -v 2 ]] || errMissingOptArg "$opt $1"
PASSTHROUGH_OPTS+=("$opt" "$1" "$2")
shift 2
;;
-j|--max-jobs|--cores|--builders|--log-format)
[[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt"
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 25.05-pre
exit 0
;;
*)
case $COMMAND in
init|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
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