1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-11-08 11:36:05 +01:00

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 --specialisation NAME`, which switches to the
  named specialisation. While it was previously possible to accomplish
  this by manually running the specialisation activate script, 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
This commit is contained in:
Robert Helgesson 2024-01-24 22:48:51 +01:00
parent e4bf85da68
commit de448dcb57
No known key found for this signature in database
GPG key ID: 96E745BD17AA17ED
21 changed files with 692 additions and 127 deletions

View file

@ -53,11 +53,6 @@ Home Manager targets [NixOS][] unstable and NixOS version 25.05 (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.

View file

@ -28,6 +28,8 @@
.Cm | packages
.Cm | remove-generations Ar ID \&...
.Cm | switch
.Op Fl -rollback
.Op Bro Fl c | Fl -specialisation Brc Ar NAME
.Cm | uninstall
.Brc
.Op Fl A Ar attrPath
@ -157,9 +159,18 @@ sub-command to find suitable generation numbers.
.RE
.Pp
.It Cm switch
.It Cm switch Oo Fl -rollback Oc Oo Bro Fl c | Fl -specialisation Brc Ar NAME Oc
.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\&.
.sp
If the
.Fl -specialisation
option is given, then the named specialisation is activated\&.
.RE
.Pp

11
docs/manual/internals.md Normal file
View file

@ -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
```

View file

@ -0,0 +1,40 @@
# 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 is 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.

View file

@ -14,6 +14,7 @@ usage.md
nix-flakes.md
writing-modules.md
contributing.md
internals.md
3rd-party.md
faq.md
```

View file

@ -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
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.
You can then run `home-manager switch --rollback` to recover your
previous configuration, which includes the working version of the
package.
To see what happened above we can observe the list of Home Manager
generations before and after the 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
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
```
2. Copy the Nix store path of the generation you chose, e.g.,
/nix/store/mv960kl9chn2lal5q8lnqdp1ygxngcd1-home-manager-generation
for generation 763.
3. Run the `activate` script inside the copied store path:
``` shell
$ /nix/store/mv960kl9chn2lal5q8lnqdp1ygxngcd1-home-manager-generation/activate
$ home-manager switch --rollback
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
```
:::

View file

@ -7,7 +7,46 @@ section is therefore not final.
This release has the following notable changes:
- No changes.
- Updating the `home-manager` Nix profile inside the activation script
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 remains the default for backwards compatibility
but may emit a deprecation warning in the future and in the longer
term removed all together. If you have developed tooling that
directly call the generated activation script, then you are
encouraged to adapt to the new behavior. See the
[Activation](#sec-internals-activation) section in the manual 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.
- The `home-manager switch` command now offers a
`--specialisation NAME` option. When given, the switch activates the
named specialisation. While it was previously possible to accomplish
this by manually running the specialisation `activate` script it was
quite cumbersome and always created a new profile generation. The
new behavior mirrors the behavior of `nixos-rebuild switch
--specialisation`.
- 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. You can restore the
old behavior by adding
``` nix
home-manager.enableLegacyProfileManagement = true;
```
to your configuration. This option is likely to be deprecated in the
future.
## State Version Changes {#sec-release-25.11-state-version-changes}

View file

@ -17,6 +17,12 @@ function errMissingOptArg() {
exit 1
}
function errTopLevelSubcommandOpt() {
# translators: For example: "home-manager: --rollback can only be used after switch"
_iError '%s: %s can only be used after %s' "$0" "$1" "$2" >&2
exit 1
}
function setNixProfileCommands() {
if [[ -e $HOME/.nix-profile/manifest.json \
|| -e ${XDG_STATE_HOME:-$HOME/.local/state}/nix/profile/manifest.json ]] ; then
@ -481,7 +487,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"
@ -699,10 +705,47 @@ function doRepl() {
}
function doSwitch() {
setHomeManagerPathVariables
setVerboseArg
setWorkDir
local action
local specialisation
while (( $# > 0 )); do
local opt="$1"
shift
case $opt in
--switch)
action='switch'
;;
--test)
action='test'
;;
--rollback)
action='rollback'
;;
--specialisation)
specialisation="$1"
shift
;;
*)
_iError "%s: unknown option '%s'" "home-manager switch" "$opt" >&2
return 1
;;
esac
done
if [[ ! -v action ]]; then
errorEcho "home-manager switch: missing required option" >&2
return 1
fi
local 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
@ -714,16 +757,52 @@ function doSwitch() {
doBuildFlake \
"$FLAKE_CONFIG_URI.activationPackage" \
--out-link "$generation" \
${PRINT_BUILD_LOGS+--print-build-logs} \
&& "$generation/activate" || return
${PRINT_BUILD_LOGS+--print-build-logs}
else
doBuildAttr \
--out-link "$generation" \
--attr activationPackage \
&& "$generation/activate" || return
--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
# Choose the activate script to run.
local activateScript="$generation/activate"
if [[ -v specialisation ]]; then
activateScript="$generation/specialisation/$specialisation/activate"
if [[ ! -x $activateScript ]]; then
_iError 'The configuration did not contain the specialisation "%s"' "$specialisation"
exit 1
fi
fi
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
"$activateScript" --driver-version 1 || return
if [[ $action == 'switch' || $action == 'test' ]]; then
presentNews
fi
}
function doListGens() {
@ -736,10 +815,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
}
@ -994,7 +1077,15 @@ 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 " -c, --specialisation NAME"
echo " Activates the named specialisation; when not specified,"
echo " switching will activate the unspecialised configuration."
echo
echo " generations List all home environment generations"
echo
@ -1028,7 +1119,7 @@ while [[ $# -gt 0 ]]; do
opt="$1"
shift
case $opt in
build|init|instantiate|option|edit|expire-generations|generations|help|news|packages|remove-generations|repl|switch|uninstall)
build|init|instantiate|option|edit|expire-generations|generations|help|news|packages|remove-generations|repl|rollback|switch|test|uninstall)
COMMAND="$opt"
;;
-A)
@ -1093,6 +1184,28 @@ while [[ $# -gt 0 ]]; do
-n|--dry-run)
export DRY_RUN=1
;;
--rollback)
case $COMMAND in
switch)
COMMAND_ARGS+=("$opt")
;;
*)
errTopLevelSubcommandOpt "--rollback" "switch"
;;
esac
;;
-c|--specialisation)
case $COMMAND in
switch)
[[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt"
COMMAND_ARGS+=("--specialisation" "$1")
shift
;;
*)
errTopLevelSubcommandOpt "--specialisation" "switch"
;;
esac
;;
--option|--arg|--argstr)
[[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt"
[[ -v 2 ]] || errMissingOptArg "$opt $1"
@ -1151,14 +1264,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

View file

@ -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: 2025-05-30 15:05+0200\n"
"POT-Creation-Date: 2025-07-22 10:59+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -23,36 +23,41 @@ msgstr ""
msgid "%s: missing argument for %s"
msgstr ""
#: home-manager/home-manager:65
#. translators: For example: "home-manager: --rollback can only be used after switch"
#: home-manager/home-manager:22
msgid "%s: %s can only be used after %s"
msgstr ""
#: home-manager/home-manager:71
msgid "No configuration file found at %s"
msgstr ""
#. translators: The first '%s' specifier will be replaced by either
#. 'home.nix' or 'flake.nix'.
#: home-manager/home-manager:82 home-manager/home-manager:86
#: home-manager/home-manager:185
#: home-manager/home-manager:88 home-manager/home-manager:92
#: home-manager/home-manager:191
msgid ""
"Keeping your Home Manager %s in %s is deprecated,\n"
"please move it to %s"
msgstr ""
#: home-manager/home-manager:93
#: home-manager/home-manager:99
msgid "No configuration file found. Please create one at %s"
msgstr ""
#: home-manager/home-manager:108
#: home-manager/home-manager:114
msgid "Home Manager not found at %s."
msgstr ""
#. translators: This message will be seen by very few users that likely are familiar with English. So feel free to leave this untranslated.
#: home-manager/home-manager:116
#: home-manager/home-manager:122
msgid ""
"The fallback Home Manager path %s has been deprecated and a file/directory "
"was found there."
msgstr ""
#. translators: This message will be seen by very few users that likely are familiar with English. So feel free to leave this untranslated.
#: home-manager/home-manager:119
#: home-manager/home-manager:125
msgid ""
"To remove this warning, do one of the following.\n"
"\n"
@ -73,42 +78,42 @@ msgid ""
" $ rm -r \"%s\""
msgstr ""
#: home-manager/home-manager:147
#: home-manager/home-manager:153
msgid "Sanity checking Nix"
msgstr ""
#: home-manager/home-manager:167
#: home-manager/home-manager:173
msgid "Could not find suitable profile directory, tried %s and %s"
msgstr ""
#. translators: Here "flake" is a noun that refers to the Nix Flakes feature.
#: home-manager/home-manager:222
#: home-manager/home-manager:230
msgid "Can't inspect options of a flake configuration"
msgstr ""
#: home-manager/home-manager:297 home-manager/home-manager:320
#: home-manager/home-manager:1060
#: home-manager/home-manager:305 home-manager/home-manager:328
#: home-manager/home-manager:734 home-manager/home-manager:1237
msgid "%s: unknown option '%s'"
msgstr ""
#: home-manager/home-manager:302 home-manager/home-manager:1061
#: home-manager/home-manager:310 home-manager/home-manager:1238
msgid "Run '%s --help' for usage help"
msgstr ""
#: home-manager/home-manager:328 home-manager/home-manager:433
#: home-manager/home-manager:336 home-manager/home-manager:441
msgid "The file %s already exists, leaving it unchanged..."
msgstr ""
#: home-manager/home-manager:330 home-manager/home-manager:435
#: home-manager/home-manager:338 home-manager/home-manager:443
msgid "Creating %s..."
msgstr ""
#: home-manager/home-manager:479
#: home-manager/home-manager:487
msgid "Creating initial Home Manager generation..."
msgstr ""
#. translators: The "%s" specifier will be replaced by a file path.
#: home-manager/home-manager:484
#: home-manager/home-manager:492
msgid ""
"All done! The home-manager tool should now be installed and you can edit\n"
"\n"
@ -119,7 +124,7 @@ msgid ""
msgstr ""
#. translators: The "%s" specifier will be replaced by a URL.
#: home-manager/home-manager:489
#: home-manager/home-manager:497
msgid ""
"Uh oh, the installation failed! Please create an issue at\n"
"\n"
@ -129,11 +134,11 @@ msgid ""
msgstr ""
#. translators: Here "flake" is a noun that refers to the Nix Flakes feature.
#: home-manager/home-manager:500
#: home-manager/home-manager:508
msgid "Can't instantiate a flake configuration"
msgstr ""
#: home-manager/home-manager:576
#: home-manager/home-manager:584
msgid ""
"There is %d unread and relevant news item.\n"
"Read it by running the command \"%s news\"."
@ -143,72 +148,76 @@ msgid_plural ""
msgstr[0] ""
msgstr[1] ""
#: home-manager/home-manager:590
#: home-manager/home-manager:598
msgid "Unknown \"news.display\" setting \"%s\"."
msgstr ""
#: home-manager/home-manager:598
#: home-manager/home-manager:606
#, sh-format
msgid "Please set the $EDITOR or $VISUAL environment variable"
msgstr ""
#: home-manager/home-manager:616
#: home-manager/home-manager:624
msgid "Cannot run build in read-only directory"
msgstr ""
#: home-manager/home-manager:697
#: home-manager/home-manager:787
msgid "The configuration did not contain the specialisation \"%s\""
msgstr ""
#: home-manager/home-manager:841
msgid "No generation with ID %s"
msgstr ""
#: home-manager/home-manager:699
#: home-manager/home-manager:843
msgid "Cannot remove the current generation %s"
msgstr ""
#: home-manager/home-manager:701
#: home-manager/home-manager:845
msgid "Removing generation %s"
msgstr ""
#: home-manager/home-manager:722
#: home-manager/home-manager:866
msgid "No generations to expire"
msgstr ""
#: home-manager/home-manager:733
#: home-manager/home-manager:877
msgid "No home-manager packages seem to be installed."
msgstr ""
#: home-manager/home-manager:818
#: home-manager/home-manager:962
msgid "Unknown argument %s"
msgstr ""
#: home-manager/home-manager:843
#: home-manager/home-manager:987
msgid "This will remove Home Manager from your system."
msgstr ""
#: home-manager/home-manager:846
#: home-manager/home-manager:990
msgid "This is a dry run, nothing will actually be uninstalled."
msgstr ""
#: home-manager/home-manager:850
#: home-manager/home-manager:994
msgid "Really uninstall Home Manager?"
msgstr ""
#: home-manager/home-manager:856
#: home-manager/home-manager:1000
msgid "Switching to empty Home Manager configuration..."
msgstr ""
#: home-manager/home-manager:871
#: home-manager/home-manager:1015
msgid "Yay!"
msgstr ""
#: home-manager/home-manager:876
#: home-manager/home-manager:1020
msgid "Home Manager is uninstalled but your home.nix is left untouched."
msgstr ""
#: home-manager/home-manager:1100
#: home-manager/home-manager:1285
msgid "expire-generations expects one argument, got %d."
msgstr ""
#: home-manager/home-manager:1122
#: home-manager/home-manager:1310
msgid "Unknown command: %s"
msgstr ""

View file

@ -477,6 +477,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;
@ -627,13 +639,17 @@ in
# 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 = lib.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.
@ -763,6 +779,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
@ -770,13 +818,15 @@ in
checkHomeDirectory ${lib.escapeShellArg config.home.homeDirectory}
fi
${lib.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}
${lib.optionalString (!config.uninstall) ''
${lib.optionalString (config.home.activationGenerateGcRoot && !config.uninstall) ''
# Create the "current generation" GC root.
run --silence nix-store --realise "$newGenPath" --add-root "$currentGenGcPath"
@ -797,6 +847,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

View file

@ -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: 2025-05-30 15:05+0200\n"
"POT-Creation-Date: 2025-07-22 10:59+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -25,15 +25,15 @@ msgstr ""
msgid "Cleaning up orphan links from %s"
msgstr ""
#: modules/home-environment.nix:632
#: modules/home-environment.nix:647
msgid "Creating new profile generation"
msgstr ""
#: modules/home-environment.nix:635
#: modules/home-environment.nix:650
msgid "No change so reusing latest profile generation"
msgstr ""
#: modules/home-environment.nix:683
#: modules/home-environment.nix:699
msgid ""
"Oops, Nix failed to install your new Home Manager profile!\n"
"\n"
@ -49,10 +49,14 @@ msgid ""
"Then try activating your Home Manager configuration again."
msgstr ""
#: modules/home-environment.nix:719
#: modules/home-environment.nix:735
msgid "Activating %s"
msgstr ""
#: modules/home-environment.nix:807
msgid "%s: unknown option '%s'"
msgstr ""
#: modules/lib-bash/activation-init.sh:22
msgid "Migrating profile from %s to %s"
msgstr ""

View file

@ -17,16 +17,22 @@ in
{ home-manager.extraSpecialArgs.darwinConfig = config; }
(lib.mkIf (cfg.users != { }) {
system.activationScripts.postActivation.text = lib.concatStringsSep "\n" (
lib.mapAttrsToList (username: usercfg: ''
lib.mapAttrsToList (
username: usercfg:
let
driverVersion = if cfg.enableLegacyProfileManagement then "0" else "1";
in
''
echo Activating home-manager configuration for ${usercfg.home.username}
launchctl asuser "$(id -u ${usercfg.home.username})" sudo -u ${usercfg.home.username} --set-home ${pkgs.writeShellScript "activation-${usercfg.home.username}" ''
${lib.optionalString (
cfg.backupFileExtension != null
) "export HOME_MANAGER_BACKUP_EXT=${lib.escapeShellArg cfg.backupFileExtension}"}
${lib.optionalString cfg.verbose "export VERBOSE=1"}
exec ${usercfg.home.activationPackage}/activate
exec ${usercfg.home.activationPackage}/activate --driver-version ${driverVersion}
''}
'') cfg.users
''
) cfg.users
);
})
];

View file

@ -108,6 +108,19 @@ in
verbose = mkEnableOption "verbose output on activation";
enableLegacyProfileManagement = mkOption {
type = types.bool;
default = false;
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.
'';
};
users = mkOption {
type = types.attrsOf hmModule;
default = { };

View file

@ -36,6 +36,12 @@ 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;
};
}
];
@ -46,6 +52,7 @@ in
_: usercfg:
let
username = usercfg.home.username;
driverVersion = if cfg.enableLegacyProfileManagement then "0" else "1";
in
lib.nameValuePair "home-manager-${utils.escapeSystemdPath username}" {
description = "Home Manager environment for ${username}";
@ -94,7 +101,7 @@ in
| ${sed} -En '/^(${exportedSystemdVariables})=/s/^/export /p'
)"
exec "$1/activate"
exec "$1/activate" --driver-version ${driverVersion}
'';
in
"${setupEnv} ${usercfg.home.activationPackage}";

View file

@ -19,9 +19,11 @@ let
mu = runTest ./standalone/mu;
nh = runTest ./standalone/nh.nix;
nixos-basics = runTest ./nixos/basics.nix;
nixos-legacy-profile-management = runTest ./nixos/legacy-profile-management.nix;
rclone = runTest ./standalone/rclone;
restic = runTest ./standalone/restic.nix;
standalone-flake-basics = runTest ./standalone/flake-basics.nix;
standalone-specialisation = runTest ./standalone/specialisation.nix;
standalone-standard-basics = runTest ./standalone/standard-basics.nix;
};
in

View file

@ -82,17 +82,16 @@
logout_alice()
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}")
hmProfile = "/home/alice/.local/state/nix/profiles/home-manager"
machine.succeed(f"test ! -e {hmProfile}")
assert gcrootTarget == profile1Target, \
f"expected GC root and profile to point to same, but pointed to {gcrootTarget} and {profile1Target}"
hmGcroot = "/home/alice/.local/state/home-manager/gcroots/current-home"
machine.succeed(f"test ! -e {hmGcroot}")
'';
}

View file

@ -0,0 +1,19 @@
{ ... }:
{
imports = [ ../../../nixos ]; # Import the HM NixOS module.
system.stateVersion = "24.11";
users.users.alice = {
isNormalUser = true;
};
home-manager = {
users.alice =
{ ... }:
{
home.stateVersion = "24.11";
home.file.test.text = "testfile new profile";
};
};
}

View file

@ -0,0 +1,111 @@
{ pkgs, ... }:
{
name = "nixos-legacy-profile-management";
meta.maintainers = [ pkgs.lib.maintainers.rycee ];
nodes.machine =
{ ... }:
{
imports = [
# Make the nixpkgs channel available.
"${pkgs.path}/nixos/modules/installer/cd-dvd/channel.nix"
# Import the HM NixOS module.
../../../nixos
];
system.stateVersion = "24.11";
users.users.alice = {
isNormalUser = true;
};
specialisation = {
legacy.configuration = {
home-manager = {
# Force legacy profile management.
enableLegacyProfileManagement = true;
users.alice =
{ ... }:
{
home.stateVersion = "24.11";
home.file.test.text = "testfile legacy";
};
};
};
modern.configuration = {
home-manager = {
# Assert that we expect the option to default to false.
enableLegacyProfileManagement = pkgs.lib.mkOptionDefault false;
users.alice =
{ ... }:
{
home.stateVersion = "24.11";
home.file.test.text = "testfile modern";
};
};
};
};
};
testScript =
{ nodes, ... }:
let
legacy = "${nodes.machine.system.build.toplevel}/specialisation/legacy";
modern = "${nodes.machine.system.build.toplevel}/specialisation/modern";
in
''
start_all()
machine.wait_for_unit("multi-user.target")
machine.succeed("${legacy}/bin/switch-to-configuration test >&2")
machine.wait_for_console_text("Finished Home Manager environment for alice.")
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 legacy"
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}"
with subtest("Switch to new profile management"):
machine.succeed("${modern}/bin/switch-to-configuration test >&2")
machine.wait_for_console_text("Finished Home Manager environment for alice.")
# 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 modern"
assert actual == expected, f"expected {path} to contain {expected}, but got {actual}"
with subtest("Switch back to old profile management"):
machine.succeed("${legacy}/bin/switch-to-configuration test >&2")
machine.wait_for_console_text("Finished Home Manager environment for alice.")
# 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 legacy"
assert actual == expected, f"expected {path} to contain {expected}, but got {actual}"
'';
}

View file

@ -0,0 +1,16 @@
{ ... }:
{
home.username = "alice";
home.homeDirectory = "/home/alice";
home.stateVersion = "25.05";
home.file.test.text = "test";
home.sessionVariables.EDITOR = "emacs";
programs.bash.enable = true;
programs.home-manager.enable = true;
specialisation.pueue.configuration = {
# Enable a light-weight systemd service.
services.pueue.enable = true;
};
}

View file

@ -0,0 +1,93 @@
{ pkgs, ... }:
{
name = "standalone-specialisation";
meta.maintainers = [ pkgs.lib.maintainers.rycee ];
nodes.machine =
{ ... }:
{
imports = [ "${pkgs.path}/nixos/modules/installer/cd-dvd/channel.nix" ];
virtualisation.memorySize = 2048;
users.users.alice = {
isNormalUser = true;
description = "Alice Foobar";
password = "foobar";
uid = 1000;
};
};
testScript = ''
start_all()
machine.wait_for_unit("network.target")
machine.wait_for_unit("multi-user.target")
home_manager = "${../../..}"
def login_as_alice():
machine.wait_until_tty_matches("1", "login: ")
machine.send_chars("alice\n")
machine.wait_until_tty_matches("1", "Password: ")
machine.send_chars("foobar\n")
machine.wait_until_tty_matches("1", "alice\\@machine")
def logout_alice():
machine.send_chars("exit\n")
def alice_cmd(cmd):
return f"su -l alice --shell /bin/sh -c $'export XDG_RUNTIME_DIR=/run/user/$UID ; {cmd}'"
def succeed_as_alice(cmd):
return machine.succeed(alice_cmd(cmd))
def fail_as_alice(cmd):
return machine.fail(alice_cmd(cmd))
# Create a persistent login so that Alice has a systemd session.
login_as_alice()
# Set up a home-manager channel.
succeed_as_alice(" ; ".join([
"mkdir -p /home/alice/.nix-defexpr/channels",
f"ln -s {home_manager} /home/alice/.nix-defexpr/channels/home-manager"
]))
with subtest("Home Manager installation"):
# Install the configuration with included specialisation.
succeed_as_alice("mkdir -p /home/alice/.config/home-manager")
succeed_as_alice("cp ${./alice-home-specialisation.nix} /home/alice/.config/home-manager/home.nix")
# Install Home Manager with the unspecialised configuration.
succeed_as_alice("nix-shell \"<home-manager>\" -A install")
# Ensure we are activated.
machine.succeed("test -L /home/alice/.cache/.keep")
with subtest("Home Manager switch to missing specialisation"):
actual = fail_as_alice("home-manager switch --specialisation no-such-specialisation")
expected = "The configuration did not contain the specialisation \"no-such-specialisation\""
assert expected in actual, \
f"expected home-manager switch to contain {expected}, but got {actual}"
with subtest("Home Manager switch to specialisation"):
actual = succeed_as_alice("home-manager switch --specialisation pueue")
expected = "Starting units: pueued.service"
assert expected in actual, \
f"expected home-manager switch to contain {expected}, but got {actual}"
actual = succeed_as_alice("pueue status")
expected = "running"
assert expected in actual, \
f"expected pueue status to contain {expected}, but got {actual}"
with subtest("Home Manager switch back to base configuration"):
actual = succeed_as_alice("home-manager switch")
expected = "Stopping units: pueued.service"
assert expected in actual, \
f"expected home-manager switch to contain {expected}, but got {actual}"
fail_as_alice("pueue status")
logout_alice()
'';
}

View file

@ -1,5 +1,5 @@
#! /usr/bin/env nix-shell
#! nix-shell -I https://github.com/NixOS/nixpkgs/archive/62b852f6c6742134ade1abdd2a21685fd617a291.tar.gz -i bash -p gettext
#! nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/62b852f6c6742134ade1abdd2a21685fd617a291.tar.gz -i bash -p gettext
set -euo pipefail
shopt -s globstar