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}" + ''; +}