From 72fc28f1e032f532453a92998ac909046ae028af Mon Sep 17 00:00:00 2001 From: Robert Helgesson Date: Wed, 24 Jan 2024 22:48:51 +0100 Subject: [PATCH] WIP home-manager: avoid profile management during activation This commit deprecates profile management from the activation script. The profile management is instead the responsibility of the driving software, for example, the `home-manager` tool in the case of standalone installs. The legacy behavior is still available for backwards compatibility but may be removed in the future. The new behavior resolves (or moves us closer to resolving) a number of long standing open issues: - `home-manager switch --rollback`, which performs a rollback to the previous Home Manager generation before activating. While it was previously possible to accomplish this by activating an old generation, it did always create a new profile generation. This option has been implemented as part of this commit. - `home-manager switch --test`, which activates the configuration but does not create a new profile generation. This option has _not_ been implemented here since it relies on the current configuration being activated on login, which we do not currently do. - When using the "Home Manager as a NixOS module" installation method we previously created an odd `home-manager` per-user "shadow profile" for the user. This is no longer necessary. This has been implemented as part of this commit. Fixes #3450 --- README.md | 6 - docs/home-manager.1 | 8 +- docs/manual/internals.md | 11 ++ docs/manual/internals/activation.md | 41 ++++++ docs/manual/manual.md | 1 + docs/manual/usage/rollbacks.md | 59 +++++--- docs/release-notes/rl-2405.md | 24 ++++ home-manager/home-manager | 131 +++++++++++++++--- modules/files.nix | 27 +--- modules/home-environment.nix | 82 ++++++++++- modules/lib-bash/activation-init.sh | 35 +---- nix-darwin/default.nix | 2 +- nixos/default.nix | 31 ++++- tests/integration/default.nix | 2 + tests/integration/nixos/basics.nix | 30 ++-- .../nixos/legacy-profile-management.nix | 46 ++++++ 16 files changed, 408 insertions(+), 128 deletions(-) create mode 100644 docs/manual/internals.md create mode 100644 docs/manual/internals/activation.md create mode 100644 tests/integration/nixos/legacy-profile-management.nix diff --git a/README.md b/README.md index 805d2cfc..7f258a5c 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,6 @@ Home Manager targets [NixOS][] unstable and NixOS version 23.11 (the current stable version), it may or may not work on other Linux distributions and NixOS versions. -Also, the `home-manager` tool does not explicitly support rollbacks at the -moment so if your home directory gets messed up you'll have to fix it yourself. -See the [rollbacks][] section for instructions on how to manually perform a -rollback. - Now when your expectations have been built up and you are eager to try all this out you can go ahead and read the rest of this text. @@ -125,4 +120,3 @@ This project is licensed under the terms of the [MIT license](LICENSE). [manual standalone install]: https://nix-community.github.io/home-manager/#sec-install-standalone [manual]: https://nix-community.github.io/home-manager/ [nix-darwin]: https://github.com/LnL7/nix-darwin -[rollbacks]: https://nix-community.github.io/home-manager/#sec-usage-rollbacks diff --git a/docs/home-manager.1 b/docs/home-manager.1 index 41406aca..c62ceb9d 100644 --- a/docs/home-manager.1 +++ b/docs/home-manager.1 @@ -27,6 +27,7 @@ .Cm | option Ar option.name .Cm | packages .Cm | remove-generations Ar ID \&... +.Cm | switch Op Fl -rollback .Cm | uninstall .Brc .Op Fl A Ar attrPath @@ -155,9 +156,14 @@ sub-command to find suitable generation numbers. .RE .Pp -.It Cm switch +.It Cm switch Op Fl -rollback .RS 4 Build and activate the configuration\&. +.sp +If the +.Fl -rollback +option is given, then the build is not done, instead roll back to and +activate the configuration prior to the current configuration\&. .RE .Pp diff --git a/docs/manual/internals.md b/docs/manual/internals.md new file mode 100644 index 00000000..799d38f1 --- /dev/null +++ b/docs/manual/internals.md @@ -0,0 +1,11 @@ +# Home Manager Internals {#ch-internals} + +This chapter collects some documentation about the internal workings +of Home Manager. The information here is mostly aimed to developers of +Home Manager and those who do non-trivial integration with Home +Manager. + + +```{=include=} sections +internals/activation.md +``` diff --git a/docs/manual/internals/activation.md b/docs/manual/internals/activation.md new file mode 100644 index 00000000..05cdaea0 --- /dev/null +++ b/docs/manual/internals/activation.md @@ -0,0 +1,41 @@ +# Activation {#sec-internals-activation} + +Activating a Home Manager configuration ensures that the built +configuration is introduced into the user's environment. The +activation is performed by a suitably named script +{command}`activate`. This script is generated as part of the +configuration build and will be placed in the root of the build +output. + +The activation script is implemented in the Bash language and consists +of initialization code followed by a number of _activation script +blocks_. These blocks are specified using the +[home.activation](#opt-home.activation) option. The blocks may have +dependencies among themselves and the generated activation script will +contain the blocks serialized such that the dependencies are +satisfied. A dependency cycle causes a failure when the configuration +is built. + +Historically, the activation script has been responsible for creating +a new generation of the `home-manager` Nix profile. The more modern +way, however, is to let the _activation driver_ – that is, the +software calling the activation script – manage the profile. Indeed, +in some cases we may not have a `home-manager` profile at all! This is +the case when Home Manager is used as a NixOS or nix-darwin module, in +these cases the system profile will contain references to the +corresponding Home Manager configurations. + +Note, to maintain backwards compatibility, the old activation script +behavior is still the default. To choose the new mode of operation you +have to call the activation script with the command line option +`--driver-version 1`. The old behavior is available using +`--driver-version 0`, or simply omit it entirely. + +Unfortunately, driver software need to support both modes of operation +for the time being since a user may wish to activate an old generation +that contains an activation script that does not support +`--driver-version`. To determine whether support is available, check +the {file}`gen-version` file in the configuration build output root. +If the file is missing then the activation script does not support +`--driver-version`. If the file exists and contains the integer 1 or +higher, then `--driver-version 1` is supported. diff --git a/docs/manual/manual.md b/docs/manual/manual.md index a1cfb1a0..683eb4f0 100644 --- a/docs/manual/manual.md +++ b/docs/manual/manual.md @@ -13,6 +13,7 @@ usage.md nix-flakes.md writing-modules.md contributing.md +internals.md 3rd-party.md faq.md ``` diff --git a/docs/manual/usage/rollbacks.md b/docs/manual/usage/rollbacks.md index 5b5180f9..d379de9f 100644 --- a/docs/manual/usage/rollbacks.md +++ b/docs/manual/usage/rollbacks.md @@ -1,32 +1,45 @@ # Rollbacks {#sec-usage-rollbacks} -While the `home-manager` tool does not explicitly support rollbacks at -the moment it is relatively easy to perform one manually. The steps to -do so are +When you perform a `home-manager switch` and discover a problem then +it is possible to _roll back_ to the previous version of your +configuration using `home-manager switch --rollback`. This will turn +the previous configuration into the current configuration. -1. Run `home-manager generations` to determine which generation you - wish to rollback to: +::: {.example #ex-rollback-scenario} +### Home Manager Rollback - ``` shell - $ home-manager generations - 2018-01-04 11:56 : id 765 -> /nix/store/kahm1rxk77mnvd2l8pfvd4jkkffk5ijk-home-manager-generation - 2018-01-03 10:29 : id 764 -> /nix/store/2wsmsliqr5yynqkdyjzb1y57pr5q2lsj-home-manager-generation - 2018-01-01 12:21 : id 763 -> /nix/store/mv960kl9chn2lal5q8lnqdp1ygxngcd1-home-manager-generation - 2017-12-29 21:03 : id 762 -> /nix/store/6c0k1r03fxckql4vgqcn9ccb616ynb94-home-manager-generation - 2017-12-25 18:51 : id 761 -> /nix/store/czc5y6vi1rvnkfv83cs3rn84jarcgsgh-home-manager-generation - … - ``` +Imagine you have just updated Nixpkgs and switched to a new Home +Manager configuration. You discover that a package update included in +your new configuration has a bug that was not present in the previous +configuration. -2. Copy the Nix store path of the generation you chose, e.g., +You can then run `home-manager switch --rollback` to recover your +previous configuration, which includes the working version of the +package. - /nix/store/mv960kl9chn2lal5q8lnqdp1ygxngcd1-home-manager-generation +To see what happened above we can observe the list of Home Manager +generations before and after the rollback: - for generation 763. +``` shell +$ home-manager generations +2024-01-04 11:56 : id 765 -> /nix/store/kahm1rxk77mnvd2l8pfvd4jkkffk5ijk-home-manager-generation (current) +2024-01-03 10:29 : id 764 -> /nix/store/2wsmsliqr5yynqkdyjzb1y57pr5q2lsj-home-manager-generation +2024-01-01 12:21 : id 763 -> /nix/store/mv960kl9chn2lal5q8lnqdp1ygxngcd1-home-manager-generation +2023-12-29 21:03 : id 762 -> /nix/store/6c0k1r03fxckql4vgqcn9ccb616ynb94-home-manager-generation +2023-12-25 18:51 : id 761 -> /nix/store/czc5y6vi1rvnkfv83cs3rn84jarcgsgh-home-manager-generation +… -3. Run the `activate` script inside the copied store path: +$ home-manager switch --rollback +Starting home manager activation +… - ``` shell - $ /nix/store/mv960kl9chn2lal5q8lnqdp1ygxngcd1-home-manager-generation/activate - Starting home manager activation - … - ``` +$ home-manager generations +2024-01-04 11:56 : id 765 -> /nix/store/kahm1rxk77mnvd2l8pfvd4jkkffk5ijk-home-manager-generation +2024-01-03 10:29 : id 764 -> /nix/store/2wsmsliqr5yynqkdyjzb1y57pr5q2lsj-home-manager-generation (current) +2024-01-01 12:21 : id 763 -> /nix/store/mv960kl9chn2lal5q8lnqdp1ygxngcd1-home-manager-generation +2023-12-29 21:03 : id 762 -> /nix/store/6c0k1r03fxckql4vgqcn9ccb616ynb94-home-manager-generation +2023-12-25 18:51 : id 761 -> /nix/store/czc5y6vi1rvnkfv83cs3rn84jarcgsgh-home-manager-generation +… +``` + +::: diff --git a/docs/release-notes/rl-2405.md b/docs/release-notes/rl-2405.md index b69b34fe..bfc9aed0 100644 --- a/docs/release-notes/rl-2405.md +++ b/docs/release-notes/rl-2405.md @@ -83,6 +83,30 @@ This release has the following notable changes: use may in the future trigger a warning message and eventually it may be removed entirely. +- The `home-manager` Nix profile update that the Home Manager + activation script has previously performed is now deprecated. The + profile update is instead the responsibility of the software calling + the activation script, such as the `home-manager` tool.. + + The legacy behavior is the default for backwards compatibility but + may be emit a deprecation warning in the future, for eventual + removal. If you have developed tooling that directly call the + generated activation script, then you are encouraged to adapt to the + new behavior. See [Activation](#sec-internals-activation) for + details on how to call the activation script. + +- The `home-manager switch` command now offers a `--rollback` option. + When given, the switch performs a rollback to the Home Manager + generation prior to the current before activating. While it was + previously possible to accomplish this by manually activating an old + generation, it always created a new profile generation. The new + behavior mirrors the behavior of `nixos-rebuild switch --rollback`. + See the [Rollbacks](#sec-usage-rollbacks) section for more. + +- When using Home Manager as a NixOS or nix-darwin module we + previously created an unnecessary `home-manager` per-user "shadow + profile" for the user. This no longer happens. + ## State Version Changes {#sec-release-24.05-state-version-changes} The state version in this release includes the changes below. These diff --git a/home-manager/home-manager b/home-manager/home-manager index 91202c87..14b05f74 100644 --- a/home-manager/home-manager +++ b/home-manager/home-manager @@ -467,7 +467,7 @@ EOF _i "Creating initial Home Manager generation..." echo - if doSwitch; then + if doSwitch --switch; 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" @@ -626,31 +626,89 @@ function doBuild() { } function doSwitch() { + setHomeManagerPathVariables + setVerboseArg setWorkDir + local action + + while (( $# > 0 )); do + local opt="$1" + shift + + case $opt in + --switch) + action='switch' + ;; + --test) + action='test' + ;; + --rollback) + action='rollback' + ;; + *) + errorEcho "home-manager switch: unknown option '%s'" "$opt" >&2 + return 1 + ;; + esac + done + + if [[ ! -v action ]]; then + errorEcho "home-manager switch: missing required option" >&2 + return 1 + fi + 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" + case $action in + switch|test) + # 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 + setFlakeAttribute + if [[ -v FLAKE_CONFIG_URI ]]; then + doBuildFlake \ + "$FLAKE_CONFIG_URI.activationPackage" \ + --out-link "$generation" \ + ${PRINT_BUILD_LOGS+--print-build-logs} + else + doBuildAttr \ + --out-link "$generation" \ + --attr activationPackage + fi + ;; + rollback) + generation="$HM_PROFILE_DIR/home-manager" + ;; + esac + + # If we are doing a switch but built a legacy configuration, where the + # activation script manages the profile, then we instead perform a test + # action. + # + # The migration away from legacy activation scripts happened when + # introducing the gen-version file, hence the existence check. + if [[ $action == 'switch' && ! -e "$generation/gen-version" ]]; then + action='test' fi - presentNews + case $action in + switch) + run nix-env $VERBOSE_ARG --profile "$HM_PROFILE_DIR/home-manager" --set "$generation" + ;; + rollback) + run nix-env $VERBOSE_ARG --profile "$HM_PROFILE_DIR/home-manager" --rollback + ;; + esac + + "$generation"/activate --driver-version 1 || return + + if [[ $action == 'switch' || $action == 'test' ]]; then + presentNews + fi } function doListGens() { @@ -663,10 +721,14 @@ function doListGens() { fi pushd "$HM_PROFILE_DIR" > /dev/null + local curProfile + curProfile=$(readlink home-manager) + # 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/' + | sed -E -e "/$curProfile/ { s/\$/ \(current\)/ }" \ + -e 's/home-manager-([[:digit:]]*)-link/: id \1/' popd > /dev/null } @@ -916,7 +978,11 @@ function doHelp() { echo echo " instantiate Instantiate the configuration and print the resulting derivation" echo - echo " switch Build and activate configuration" + echo " switch [OPTION]" + echo " Build and activate configuration" + echo + echo " --rollback Do not build a new configuration, instead roll back to" + echo " the configuration prior to the current configuration." echo echo " generations List all home environment generations" echo @@ -947,7 +1013,7 @@ 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) + build|init|instantiate|option|edit|expire-generations|generations|help|news|packages|remove-generations|rollback|switch|test|uninstall) COMMAND="$opt" ;; -A) @@ -1002,6 +1068,17 @@ while [[ $# -gt 0 ]]; do -n|--dry-run) export DRY_RUN=1 ;; + --rollback) + case $COMMAND in + switch) + COMMAND_ARGS+=("$opt") + ;; + *) + _iError 'home-manager: "--rollback" can only be used after "switch"' >&2 + exit 1 + ;; + esac + ;; --option|--arg|--argstr) PASSTHROUGH_OPTS+=("$opt" "$1" "$2") shift 2 @@ -1057,14 +1134,22 @@ case $COMMAND in doInstantiate ;; switch) - doSwitch + doSwitch --switch "${COMMAND_ARGS[@]}" ;; + # TODO: The test functionality is not really sensible until we perform + # activation through some form of systemd unit. + # test) + # doSwitch --test + # ;; generations) doListGens ;; remove-generations) doRmGenerations "${COMMAND_ARGS[@]}" ;; + rollback) + doRollback + ;; expire-generations) if [[ ${#COMMAND_ARGS[@]} != 1 ]]; then _i 'expire-generations expects one argument, got %d.' "${#COMMAND_ARGS[@]}" >&2 diff --git a/modules/files.nix b/modules/files.nix index 327cf1bb..63ea9270 100644 --- a/modules/files.nix +++ b/modules/files.nix @@ -150,10 +150,7 @@ in # 1. Remove files from the old generation that are not in the new # generation. # - # 2. Switch over the Home Manager gcroot and current profile - # links. - # - # 3. Symlink files from the new generation into $HOME. + # 2. Symlink files from the new generation into $HOME. # # This order is needed to ensure that we always know which links # belong to which generation. Specifically, if we're moving from @@ -260,28 +257,6 @@ in } cleanOldGen - - if [[ ! -v oldGenPath || "$oldGenPath" != "$newGenPath" ]] ; then - _i "Creating profile generation %s" $newGenNum - if [[ -e "$genProfilePath"/manifest.json ]] ; then - # Remove all packages from "$genProfilePath" - # `nix profile remove '.*' --profile "$genProfilePath"` was not working, so here is a workaround: - nix profile list --profile "$genProfilePath" \ - | cut -d ' ' -f 4 \ - | xargs -rt $DRY_RUN_CMD nix profile remove $VERBOSE_ARG --profile "$genProfilePath" - run nix profile install $VERBOSE_ARG --profile "$genProfilePath" "$newGenPath" - else - run nix-env $VERBOSE_ARG --profile "$genProfilePath" --set "$newGenPath" - fi - - run --silence nix-store --realise "$newGenPath" --add-root "$newGenGcPath" - if [[ -e "$legacyGenGcPath" ]]; then - run rm $VERBOSE_ARG "$legacyGenGcPath" - fi - else - _i "No change so reusing latest profile generation %s" "$oldGenNum" - fi - linkNewGen '' ); diff --git a/modules/home-environment.nix b/modules/home-environment.nix index 27a71e45..f93df13e 100644 --- a/modules/home-environment.nix +++ b/modules/home-environment.nix @@ -425,6 +425,18 @@ in description = "The package containing the complete activation script."; }; + home.activationGenerateGcRoot = mkOption { + internal = true; + type = types.bool; + default = true; + description = '' + Whether the activation script should create a GC root to avoid being + garbage collected. Typically you want this but if you know for certain + that the Home Manager generation is referenced from some other GC root, + then it may be appropriate to not create our own root. + ''; + }; + home.extraActivationPath = mkOption { internal = true; type = types.listOf types.package; @@ -570,9 +582,21 @@ in home.packages = [ config.home.sessionVariablesPackage ]; - # A dummy entry acting as a boundary between the activation - # script's "check" and the "write" phases. - home.activation.writeBoundary = hm.dag.entryAnywhere ""; + # The entry acting as a boundary between the activation script's "check" and + # the "write" phases. This is where we commit to attempting to actually + # activate the configuration. + # + # Note, if we are run by a version 0 driver then we update the profile here. + home.activation.writeBoundary = hm.dag.entryAnywhere '' + if (( $hmDriverVersion < 1 )); then + if [[ ! -v oldGenPath || "$oldGenPath" != "$newGenPath" ]] ; then + _i "Creating new profile generation" + run nix-env $VERBOSE_ARG --profile "$genProfilePath" --set "$newGenPath" + else + _i "No change so reusing latest profile generation" + fi + fi + ''; # Install packages to the user environment. # @@ -698,6 +722,38 @@ in export PATH="${activationBinPaths}" ${config.lib.bash.initHomeManagerLib} + # The driver version indicates the behavior expected by the caller of + # this script. + # + # - 0 : legacy behavior + # - 1 : the script will not attempt to update the Home Manager Nix profile. + hmDriverVersion=0 + + while (( $# > 0 )); do + opt="$1" + shift + + case $opt in + --driver-version) + if (( $# == 0 )); then + errorEcho "$0: no driver version specified" >&2 + exit 1 + elif (( 0 <= $1 && $1 <= 1 )); then + hmDriverVersion=$1 + else + errorEcho "$0: unexpected driver version $1" >&2 + exit 1 + fi + shift + ;; + *) + _iError "%s: unknown option '%s'" "$0" "$opt" >&2 + exit 1 + ;; + esac + done + unset opt + ${builtins.readFile ./lib-bash/activation-init.sh} if [[ ! -v SKIP_SANITY_CHECKS ]]; then @@ -705,7 +761,22 @@ in checkHomeDirectory ${escapeShellArg config.home.homeDirectory} fi + ${optionalString config.home.activationGenerateGcRoot '' + # Create a temporary GC root to prevent collection during activation. + trap 'run rm -f $VERBOSE_ARG "$newGenGcPath"' EXIT + run --silence nix-store --realise "$newGenPath" --add-root "$newGenGcPath" + ''} + ${activationCmds} + + ${optionalString (config.home.activationGenerateGcRoot && !config.uninstall) '' + # Create the "current generation" GC root. + run --silence nix-store --realise "$newGenPath" --add-root "$currentGenGcPath" + + if [[ -e "$legacyGenGcPath" ]]; then + run rm $VERBOSE_ARG "$legacyGenGcPath" + fi + ''} ''; in pkgs.runCommand @@ -718,6 +789,11 @@ in echo "${config.home.version.full}" > $out/hm-version + # The gen-version indicates the format of the generation package + # itself. It allows us to make backwards incompatible changes in the + # package output and have surrounding tooling adapt. + echo 1 > $out/gen-version + cp ${activationScript} $out/activate mkdir $out/bin diff --git a/modules/lib-bash/activation-init.sh b/modules/lib-bash/activation-init.sh index 9a38e7c5..143f0b97 100755 --- a/modules/lib-bash/activation-init.sh +++ b/modules/lib-bash/activation-init.sh @@ -59,34 +59,13 @@ function setupVars() { declare -gr hmDataPath="${XDG_DATA_HOME:-$HOME/.local/share}/home-manager" declare -gr genProfilePath="$profilesDir/home-manager" declare -gr newGenPath="@GENERATION_DIR@"; - declare -gr newGenGcPath="$hmGcrootsDir/current-home" + declare -gr newGenGcPath="$hmGcrootsDir/new-home" + declare -gr currentGenGcPath="$hmGcrootsDir/current-home" declare -gr legacyGenGcPath="$globalGcrootsDir/current-home" - declare greatestGenNum - greatestGenNum=$( \ - nix-env --list-generations --profile "$genProfilePath" \ - | tail -1 \ - | sed -E 's/ *([[:digit:]]+) .*/\1/') - - if [[ -n $greatestGenNum ]] ; then - declare -gr oldGenNum=$greatestGenNum - declare -gr newGenNum=$((oldGenNum + 1)) - else - declare -gr newGenNum=1 - fi - - if [[ -e $genProfilePath ]] ; then + if [[ -e $currentGenGcPath ]] ; then declare -g oldGenPath - oldGenPath="$(readlink -e "$genProfilePath")" - fi - - _iVerbose "Sanity checking oldGenNum and oldGenPath" - if [[ -v oldGenNum && ! -v oldGenPath - || ! -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:-}" \ - "$profilesDir" "$hmGcrootsDir" - exit 1 + oldGenPath="$(readlink -e "$currentGenGcPath")" fi } @@ -181,15 +160,13 @@ if [[ -v VERBOSE ]]; then fi _iVerbose "Activation variables:" -if [[ -v oldGenNum ]] ; then - verboseEcho " oldGenNum=$oldGenNum" +if [[ -v oldGenPath ]] ; then verboseEcho " oldGenPath=$oldGenPath" else - verboseEcho " oldGenNum undefined (first run?)" verboseEcho " oldGenPath undefined (first run?)" fi verboseEcho " newGenPath=$newGenPath" -verboseEcho " newGenNum=$newGenNum" verboseEcho " genProfilePath=$genProfilePath" verboseEcho " newGenGcPath=$newGenGcPath" +verboseEcho " currentGenGcPath=$currentGenGcPath" verboseEcho " legacyGenGcPath=$legacyGenGcPath" diff --git a/nix-darwin/default.nix b/nix-darwin/default.nix index 018e9bab..8c8e1088 100644 --- a/nix-darwin/default.nix +++ b/nix-darwin/default.nix @@ -22,7 +22,7 @@ in { lib.escapeShellArg cfg.backupFileExtension }"} ${lib.optionalString cfg.verbose "export VERBOSE=1"} - exec ${usercfg.home.activationPackage}/activate + exec ${usercfg.home.activationPackage}/activate --driver-version 1 '' } '') cfg.users); diff --git a/nixos/default.nix b/nixos/default.nix index 95d5943f..8ece279a 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -13,6 +13,24 @@ let in { imports = [ ./common.nix ]; + options.home-manager = { + enableLegacyProfileManagement = mkOption { + type = types.bool; + default = versionOlder config.system.stateVersion "24.05"; + defaultText = lib.literalMD '' + - `true` for `system.stateVersion` < 24.05, + - `false` otherwise''; + description = '' + Whether to enable legacy profile (and garbage collection root) + management during activation. When enabled, the Home Manager activation + will produce a per-user `home-manager` Nix profile as well as a garbage + collection root, just like in the standalone installation of Home + Manager. Typically, this is not desired when Home Manager is embedded in + the system configuration. + ''; + }; + }; + config = mkMerge [ { home-manager = { @@ -26,12 +44,21 @@ in { # Inherit glibcLocales setting from NixOS. i18n.glibcLocales = lib.mkDefault config.i18n.glibcLocales; + + # Legacy profile management is when the activation script generates GC + # root and home-manager profile. The modern way simply relies on the + # GC root that the system maintains, which should also protect the + # Home Manager activation package outputs. + home.activationGenerateGcRoot = cfg.enableLegacyProfileManagement; }]; }; } (mkIf (cfg.users != { }) { systemd.services = mapAttrs' (_: usercfg: - let username = usercfg.home.username; + let + username = usercfg.home.username; + driverVersion = + if cfg.enableLegacyProfileManagement then "0" else "1"; in nameValuePair ("home-manager-${utils.escapeSystemdPath username}") { description = "Home Manager environment for ${username}"; wantedBy = [ "multi-user.target" ]; @@ -78,7 +105,7 @@ in { | ${sed} -En '/^(${exportedSystemdVariables})=/s/^/export /p' )" - exec "$1/activate" + exec "$1/activate" --driver-version ${driverVersion} ''; in "${setupEnv} ${usercfg.home.activationPackage}"; }; diff --git a/tests/integration/default.nix b/tests/integration/default.nix index c3103017..3786c342 100644 --- a/tests/integration/default.nix +++ b/tests/integration/default.nix @@ -11,6 +11,8 @@ let tests = { nixos-basics = runTest ./nixos/basics.nix; + nixos-legacy-profile-management = + runTest ./nixos/legacy-profile-management.nix; standalone-flake-basics = runTest ./standalone/flake-basics.nix; standalone-standard-basics = runTest ./standalone/standard-basics.nix; }; diff --git a/tests/integration/nixos/basics.nix b/tests/integration/nixos/basics.nix index 29c2756f..bd20142d 100644 --- a/tests/integration/nixos/basics.nix +++ b/tests/integration/nixos/basics.nix @@ -7,11 +7,17 @@ nodes.machine = { ... }: { imports = [ ../../../nixos ]; # Import the HM NixOS module. + system.stateVersion = "23.11"; + users.users.alice = { isNormalUser = true; }; - home-manager.users.alice = { ... }: { - home.stateVersion = "23.11"; - home.file.test.text = "testfile"; + home-manager = { + enableLegacyProfileManagement = false; + + users.alice = { ... }: { + home.stateVersion = "23.11"; + home.file.test.text = "testfile"; + }; }; }; @@ -28,17 +34,13 @@ expected = "testfile" assert actual == expected, f"expected {path} to contain {expected}, but got {actual}" - with subtest("GC root and profile"): - # There should be a GC root and Home Manager profile and they should point - # to the same path in the Nix store. - gcroot = "/home/alice/.local/state/home-manager/gcroots/current-home" - gcrootTarget = machine.succeed(f"readlink {gcroot}") + with subtest("no GC root and profile"): + # There should be no GC root and Home Manager profile since we are not + # using legacy profile management. + hmState = "/home/alice/.local/state/home-manager" + machine.succeed(f"test ! -e {hmState}") - profile = "/home/alice/.local/state/nix/profiles" - profileTarget = machine.succeed(f"readlink {profile}/home-manager") - profile1Target = machine.succeed(f"readlink {profile}/{profileTarget}") - - assert gcrootTarget == profile1Target, \ - f"expected GC root and profile to point to same, but pointed to {gcrootTarget} and {profile1Target}" + hmProfile = "/home/alice/.local/state/nix/profiles/home-manager" + machine.succeed(f"test ! -e {hmProfile}") ''; } diff --git a/tests/integration/nixos/legacy-profile-management.nix b/tests/integration/nixos/legacy-profile-management.nix new file mode 100644 index 00000000..39b304d7 --- /dev/null +++ b/tests/integration/nixos/legacy-profile-management.nix @@ -0,0 +1,46 @@ +{ pkgs, ... }: + +{ + name = "nixos-legacy-profile-management"; + meta.maintainers = [ pkgs.lib.maintainers.rycee ]; + + nodes.machine = { ... }: { + imports = [ ../../../nixos ]; # Import the HM NixOS module. + + system.stateVersion = "23.11"; + + users.users.alice = { isNormalUser = true; }; + + home-manager.users.alice = { ... }: { + home.stateVersion = "23.11"; + home.file.test.text = "testfile"; + }; + }; + + testScript = '' + start_all() + + machine.wait_for_unit("home-manager-alice.service") + + with subtest("Home Manager file"): + # The file should be linked with the expected content. + path = "/home/alice/test" + machine.succeed(f"test -L {path}") + actual = machine.succeed(f"cat {path}") + expected = "testfile" + assert actual == expected, f"expected {path} to contain {expected}, but got {actual}" + + with subtest("GC root and profile"): + # There should be a GC root and Home Manager profile and they should point + # to the same path in the Nix store. + gcroot = "/home/alice/.local/state/home-manager/gcroots/current-home" + gcrootTarget = machine.succeed(f"readlink {gcroot}") + + profile = "/home/alice/.local/state/nix/profiles" + profileTarget = machine.succeed(f"readlink {profile}/home-manager") + profile1Target = machine.succeed(f"readlink {profile}/{profileTarget}") + + assert gcrootTarget == profile1Target, \ + f"expected GC root and profile to point to same, but pointed to {gcrootTarget} and {profile1Target}" + ''; +}