From f69816489d5bcd1329c50fb4a7035a9a9dc19a3b Mon Sep 17 00:00:00 2001 From: Robert Helgesson Date: Sat, 4 Mar 2023 10:39:02 +0100 Subject: [PATCH] home-manager: handle missing per-user profiles directory Specifically, if the global per-user profiles path do not exist and we cannot create it during the activation, then place our profile in the Home Manager data directory. We prefer to use the global location, though, since it makes it visible to `nix-collect-garbage`. This is intended to improve compatibility with Nix version 2.14 and later, which no longer creates the per-user directories. Also, use the Home Manager data directory to manage the gcroot for the current generation. It does not have to sit in the global per-user gcroots directory since it should never be eligible for GC. --- home-manager/home-manager | 72 +++++++++++++++------------- home-manager/po/home-manager.pot | 58 +++++++++------------- modules/files.nix | 5 +- modules/home-environment.nix | 4 +- modules/lib-bash/activation-init.sh | 74 ++++++++++++++++++++++++----- modules/po/hm-modules.pot | 34 +++++++------ 6 files changed, 149 insertions(+), 98 deletions(-) diff --git a/home-manager/home-manager b/home-manager/home-manager index 390ab47d6..d758653d5 100644 --- a/home-manager/home-manager +++ b/home-manager/home-manager @@ -19,7 +19,7 @@ function removeByName() { } function setNixProfileCommands() { - if [[ -e ~/.nix-profile/manifest.json ]] ; then + if [[ -e $HOME/.nix-profile/manifest.json ]] ; then LIST_OUTPATH_CMD="nix profile list" REMOVE_CMD="removeByName" else @@ -93,6 +93,23 @@ function setHomeManagerNixPath() { done } +# Sets some useful Home Manager related paths as global read-only variables. +function setHomeManagerPathVariables() { + declare -r nixStateDir="${NIX_STATE_DIR:-/nix/var/nix}" + declare -r globalProfilesDir="$nixStateDir/profiles/per-user/$USER" + declare -r globalGcrootsDir="$nixStateDir/gcroots/per-user/$USER" + + declare -gr HM_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}/home-manager" + declare -gr HM_STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/home-manager" + declare -gr HM_GCROOT_LEGACY_PATH="$globalGcrootsDir/current-home" + + if [[ -d "$globalProfilesDir" ]]; then + declare -gr HM_PROFILE_DIR="$globalProfilesDir" + else + declare -gr HM_PROFILE_DIR="$HM_STATE_DIR/profiles" + fi +} + function setFlakeAttribute() { local configFlake="${XDG_CONFIG_HOME:-$HOME/.config}/nixpkgs/flake.nix" if [[ -z $FLAKE_ARG && ! -v HOME_MANAGER_CONFIG && -e "$configFlake" ]]; then @@ -339,7 +356,7 @@ function doListGens() { color="always" fi - pushd "$NIX_STATE_DIR/profiles/per-user/$USER" > /dev/null + 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- \ @@ -352,7 +369,7 @@ function doListGens() { function doRmGenerations() { setVerboseAndDryRun - pushd "$NIX_STATE_DIR/profiles/per-user/$USER" > /dev/null + pushd "$HM_PROFILE_DIR" > /dev/null for generationId in "$@"; do local linkName="home-manager-$generationId-link" @@ -370,17 +387,10 @@ function doRmGenerations() { 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" \ + find "$HM_PROFILE_DIR" -name 'home-manager-*-link' -not -newermt "$1" \ | sed 's/^.*-\([0-9]*\)-link$/\1/' \ )" @@ -482,6 +492,7 @@ function doUninstall() { 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..." @@ -493,10 +504,22 @@ function doUninstall() { doSwitch $DRY_RUN_CMD $REMOVE_CMD 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" + + if [[ -e $HM_DATA_HOME ]]; then + $DRY_RUN_CMD rm $VERBOSE_ARG -r "$HM_DATA_HOME" + fi + + if [[ -e $HM_PROFILE_DIR ]]; then + $DRY_RUN_CMD rm $VERBOSE_ARG "$HM_PROFILE_DIR/home-manager"* + fi + + if [[ -e $HM_GCROOT_LEGACY_PATH ]]; then + $DRY_RUN_CMD rm $VERBOSE_ARG "$HM_GCROOT_LEGACY_PATH" + fi + + if [[ -e $HM_STATE_DIR ]]; then + $DRY_RUN_CMD rm $VERBOSE_ARG -r "$HM_STATE_DIR" + fi ;; *) _i "Yay!" @@ -504,22 +527,6 @@ function doUninstall() { ;; 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." } @@ -591,8 +598,6 @@ function doHelp() { echo " uninstall Remove Home Manager" } -readonly NIX_STATE_DIR="${NIX_STATE_DIR:-/nix/var/nix}" - EXTRA_NIX_PATH=() HOME_MANAGER_CONFIG_ATTRIBUTE="" PASSTHROUGH_OPTS=() @@ -601,6 +606,7 @@ COMMAND_ARGS=() FLAKE_ARG="" setHomeManagerNixPath +setHomeManagerPathVariables while [[ $# -gt 0 ]]; do opt="$1" diff --git a/home-manager/po/home-manager.pot b/home-manager/po/home-manager.pot index 003a344b0..a6a77c7f3 100644 --- a/home-manager/po/home-manager.pot +++ b/home-manager/po/home-manager.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Home Manager\n" "Report-Msgid-Bugs-To: https://github.com/nix-community/home-manager/issues\n" -"POT-Creation-Date: 2022-03-26 15:08+0100\n" +"POT-Creation-Date: 2023-03-07 23:36+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -26,15 +26,15 @@ msgstr "" msgid "No configuration file found. Please create one at %s" msgstr "" -#: home-manager/home-manager:122 +#: home-manager/home-manager:146 msgid "Can't inspect options of a flake configuration" msgstr "" -#: home-manager/home-manager:162 +#: home-manager/home-manager:185 msgid "Can't instantiate a flake configuration" msgstr "" -#: home-manager/home-manager:237 +#: home-manager/home-manager:258 msgid "" "There is %d unread and relevant news item.\n" "Read it by running the command \"%s news\"." @@ -44,92 +44,80 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: home-manager/home-manager:251 +#: home-manager/home-manager:272 msgid "Unknown \"news.display\" setting \"%s\"." msgstr "" -#: home-manager/home-manager:258 +#: home-manager/home-manager:279 #, sh-format msgid "Please set the $EDITOR environment variable" msgstr "" -#: home-manager/home-manager:273 +#: home-manager/home-manager:294 msgid "Cannot run build in read-only directory" msgstr "" -#: home-manager/home-manager:355 +#: home-manager/home-manager:378 msgid "No generation with ID %s" msgstr "" -#: home-manager/home-manager:357 +#: home-manager/home-manager:380 msgid "Cannot remove the current generation %s" msgstr "" -#: home-manager/home-manager:359 +#: home-manager/home-manager:382 msgid "Removing generation %s" msgstr "" -#: home-manager/home-manager:385 +#: home-manager/home-manager:401 msgid "No generations to expire" msgstr "" -#: home-manager/home-manager:396 +#: home-manager/home-manager:412 msgid "No home-manager packages seem to be installed." msgstr "" -#: home-manager/home-manager:453 +#: home-manager/home-manager:469 msgid "Unknown argument %s" msgstr "" -#: home-manager/home-manager:469 +#: home-manager/home-manager:485 msgid "This will remove Home Manager from your system." msgstr "" -#: home-manager/home-manager:472 +#: home-manager/home-manager:488 msgid "This is a dry run, nothing will actually be uninstalled." msgstr "" -#: home-manager/home-manager:476 +#: home-manager/home-manager:492 msgid "Really uninstall Home Manager?" msgstr "" -#: home-manager/home-manager:481 +#: home-manager/home-manager:498 msgid "Switching to empty Home Manager configuration..." msgstr "" -#: home-manager/home-manager:493 +#: home-manager/home-manager:525 msgid "Yay!" msgstr "" -#: home-manager/home-manager:500 -msgid "Remove all Home Manager generations?" -msgstr "" - -#: home-manager/home-manager:507 -msgid "All generations are now eligible for garbage collection." -msgstr "" - -#: home-manager/home-manager:510 -msgid "Leaving generations but they may still be garbage collected." -msgstr "" - -#: home-manager/home-manager:514 +#: home-manager/home-manager:530 msgid "Home Manager is uninstalled but your home.nix is left untouched." msgstr "" -#: home-manager/home-manager:673 +#: home-manager/home-manager:695 msgid "%s: unknown option '%s'" msgstr "" -#: home-manager/home-manager:674 +#: home-manager/home-manager:696 msgid "Run '%s --help' for usage help" msgstr "" -#: home-manager/home-manager:708 +#: home-manager/home-manager:730 msgid "expire-generations expects one argument, got %d." msgstr "" -#: home-manager/home-manager:730 +#: home-manager/home-manager:752 msgid "Unknown command: %s" msgstr "" diff --git a/modules/files.nix b/modules/files.nix index 0f4207fe8..4f64a9f86 100644 --- a/modules/files.nix +++ b/modules/files.nix @@ -272,7 +272,10 @@ in $DRY_RUN_CMD nix-env $VERBOSE_ARG --profile "$genProfilePath" --set "$newGenPath" fi - $DRY_RUN_CMD ln -Tsf $VERBOSE_ARG "$newGenPath" "$newGenGcPath" + $DRY_RUN_CMD nix-store --realise "$newGenPath" --add-root "$newGenGcPath" > "$DRY_RUN_NULL" + if [[ -e "$legacyGenGcPath" ]]; then + $DRY_RUN_CMD rm $VERBOSE_ARG "$legacyGenGcPath" + fi else _i "No change so reusing latest profile generation %s" "$oldGenNum" fi diff --git a/modules/home-environment.nix b/modules/home-environment.nix index 098e10b21..aca3723ef 100644 --- a/modules/home-environment.nix +++ b/modules/home-environment.nix @@ -584,7 +584,7 @@ in if config.submoduleSupport.externalPackageInstall then '' - if [[ -e "$nixProfilePath"/manifest.json ]] ; then + if [[ -e $HOME/.nix-profile/manifest.json ]] ; then nix profile list \ | { grep 'home-manager-path$' || test $? = 1; } \ | cut -d ' ' -f 4 \ @@ -608,7 +608,7 @@ in $DRY_RUN_CMD $oldNix profile install $1 } - if [[ -e "$nixProfilePath"/manifest.json ]] ; then + if [[ -e $HOME/.nix-profile/manifest.json ]] ; then INSTALL_CMD="nix profile install" INSTALL_CMD_ACTUAL="nixReplaceProfile" LIST_CMD="nix profile list" diff --git a/modules/lib-bash/activation-init.sh b/modules/lib-bash/activation-init.sh index 3b0f5320a..e719352dc 100644 --- a/modules/lib-bash/activation-init.sh +++ b/modules/lib-bash/activation-init.sh @@ -1,14 +1,60 @@ +# Moves the existing profile from /nix to ~ to match changed behavior in Nix +# 2.14. See https://github.com/NixOS/nix/pull/5226. +# +# Note, this function is intentionally unused for now. There remains a few open +# questions about backwards compatibility and support from +# `nix-collect-garbage`. +function migrateProfile() { + declare -r stateHome="${XDG_STATE_HOME:-$HOME/.local/state}" + declare -r hmStateDir="$stateHome/home-manager" + declare -r nixStateDir="${NIX_STATE_DIR:-/nix/var/nix}" + + declare -r newProfilesDir="$hmStateDir/profiles" + declare -r oldProfilesDir="$nixStateDir/profiles/per-user/$USER" + + if [[ ! -d $newProfilesDir ]]; then + _i 'Migrating profiles from %s to %s' "$oldProfilesDir" "$newProfilesDir" + mkdir -p "$newProfilesDir" + for p in "$oldProfilesDir"/home-manager-*; do + declare -r name="${p##*/}" + nix-store --realise "$p" --add-root "$newProfilesDir/$name" > /dev/null + done + cp -P "$oldProfilesDir/home-manager" "$newProfilesDir" + fi + + rm "$oldProfilesDir"/home-manager-* +} + function setupVars() { - local nixStateDir="${NIX_STATE_DIR:-/nix/var/nix}" - local profilesPath="$nixStateDir/profiles/per-user/$USER" - local gcPath="$nixStateDir/gcroots/per-user/$USER" + declare -r nixStateDir="${NIX_STATE_DIR:-/nix/var/nix}" + declare -r globalProfilesDir="$nixStateDir/profiles/per-user/$USER" + declare -r globalGcrootsDir="$nixStateDir/gcroots/per-user/$USER" - declare -gr nixProfilePath="$profilesPath/profile" - declare -gr genProfilePath="$profilesPath/home-manager" + declare -r stateHome="${XDG_STATE_HOME:-$HOME/.local/state}" + declare -r hmStateDir="$stateHome/home-manager" + declare -r hmGcrootsDir="$hmStateDir/gcroots" + + # If the global profiles path exists or we can create it, then place the HM + # profile there. Otherwise place it in the HM data directory. We prefer to + # use the global location since it makes it visible to + # `nix-collect-garbage`. + # + # In the future we may perform a one-shot migration to the new location. + # + # shellcheck disable=2174 + if [[ -d "$globalProfilesDir" ]] || mkdir -m 0755 -p "$globalProfilesDir"; then + declare -r hmProfilesDir="$globalProfilesDir" + else + declare -r hmProfilesDir="$hmStateDir/profiles" + mkdir -m 0755 -p "$hmProfilesDir" + fi + + declare -gr genProfilePath="$hmProfilesDir/home-manager" declare -gr newGenPath="@GENERATION_DIR@"; - declare -gr newGenGcPath="$gcPath/current-home" + declare -gr newGenGcPath="$hmGcrootsDir/current-home" + declare -gr legacyGenGcPath="$globalGcrootsDir/current-home" - local greatestGenNum + declare greatestGenNum greatestGenNum=$( \ nix-env --list-generations --profile "$genProfilePath" \ | tail -1 \ @@ -21,9 +67,9 @@ function setupVars() { declare -gr newGenNum=1 fi - if [[ -e $profilesPath/home-manager ]] ; then - oldGenPath="$(readlink -e "$profilesPath/home-manager")" - declare -gr oldGenPath + if [[ -e $genProfilePath ]] ; then + declare -g oldGenPath + oldGenPath="$(readlink -e "$genProfilePath")" fi $VERBOSE_RUN _i "Sanity checking oldGenNum and oldGenPath" @@ -31,7 +77,7 @@ function setupVars() { || ! -v oldGenNum && -v oldGenPath ]]; then _i $'The previous generation number and path are in conflict! These\nmust be either both empty or both set but are now set to\n\n \'%s\' and \'%s\'\n\nIf you don\'t mind losing previous profile generations then\nthe easiest solution is probably to run\n\n rm %s/home-manager*\n rm %s/current-home\n\nand trying home-manager switch again. Good luck!' \ "${oldGenNum:-}" "${oldGenPath:-}" \ - "$profilesPath" "$gcPath" + "$hmProfilesDir" "$hmGcrootsDir" exit 1 fi } @@ -58,9 +104,12 @@ setupVars if [[ -v DRY_RUN ]] ; then _i "This is a dry run" export DRY_RUN_CMD=echo + export DRY_RUN_NULL=/dev/stdout else $VERBOSE_RUN _i "This is a live run" export DRY_RUN_CMD="" + export DRY_RUN_NULL=/dev/null + fi if [[ -v VERBOSE ]]; then @@ -77,5 +126,6 @@ else fi $VERBOSE_ECHO " newGenPath=$newGenPath" $VERBOSE_ECHO " newGenNum=$newGenNum" -$VERBOSE_ECHO " newGenGcPath=$newGenGcPath" $VERBOSE_ECHO " genProfilePath=$genProfilePath" +$VERBOSE_ECHO " newGenGcPath=$newGenGcPath" +$VERBOSE_ECHO " legacyGenGcPath=$legacyGenGcPath" diff --git a/modules/po/hm-modules.pot b/modules/po/hm-modules.pot index 7bff64af4..8bbcc221b 100644 --- a/modules/po/hm-modules.pot +++ b/modules/po/hm-modules.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Home Manager Modules\n" "Report-Msgid-Bugs-To: https://github.com/nix-community/home-manager/issues\n" -"POT-Creation-Date: 2022-03-26 15:08+0100\n" +"POT-Creation-Date: 2023-03-07 23:36+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,23 +17,23 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: modules/files.nix:233 +#: modules/files.nix:234 msgid "Creating home file links in %s" msgstr "" -#: modules/files.nix:246 +#: modules/files.nix:247 msgid "Cleaning up orphan links from %s" msgstr "" -#: modules/files.nix:262 +#: modules/files.nix:263 msgid "Creating profile generation %s" msgstr "" -#: modules/files.nix:276 +#: modules/files.nix:280 msgid "No change so reusing latest profile generation %s" msgstr "" -#: modules/home-environment.nix:607 +#: modules/home-environment.nix:625 msgid "" "Oops, Nix failed to install your new Home Manager profile!\n" "\n" @@ -49,15 +49,19 @@ msgid "" "Then try activating your Home Manager configuration again." msgstr "" -#: modules/home-environment.nix:639 +#: modules/home-environment.nix:658 msgid "Activating %s" msgstr "" -#: modules/lib-bash/activation-init.sh:31 +#: modules/lib-bash/activation-init.sh:18 +msgid "Migrating profiles from %s to %s" +msgstr "" + +#: modules/lib-bash/activation-init.sh:77 msgid "Sanity checking oldGenNum and oldGenPath" msgstr "" -#: modules/lib-bash/activation-init.sh:34 +#: modules/lib-bash/activation-init.sh:80 msgid "" "The previous generation number and path are in conflict! These\n" "must be either both empty or both set but are now set to\n" @@ -73,26 +77,26 @@ msgid "" "and trying home-manager switch again. Good luck!" msgstr "" -#: modules/lib-bash/activation-init.sh:51 +#: modules/lib-bash/activation-init.sh:97 msgid "Starting Home Manager activation" msgstr "" -#: modules/lib-bash/activation-init.sh:55 +#: modules/lib-bash/activation-init.sh:101 msgid "Sanity checking Nix" msgstr "" -#: modules/lib-bash/activation-init.sh:61 +#: modules/lib-bash/activation-init.sh:107 msgid "This is a dry run" msgstr "" -#: modules/lib-bash/activation-init.sh:64 +#: modules/lib-bash/activation-init.sh:111 msgid "This is a live run" msgstr "" -#: modules/lib-bash/activation-init.sh:69 +#: modules/lib-bash/activation-init.sh:118 msgid "Using Nix version: %s" msgstr "" -#: modules/lib-bash/activation-init.sh:72 +#: modules/lib-bash/activation-init.sh:121 msgid "Activation variables:" msgstr ""