Skip to content

Commit

Permalink
Global vs per-user profiles and channels
Browse files Browse the repository at this point in the history
The motivation for this is basically explained in the contribution
guide, as guidance on how to design features like this going forward.

In short, I think it is confusing for operations acting on the default
profile to *always* do something different based on whether the user is
regular or root.

Instead, this makes there be flags which are (from the vantage point of
today) "do the root default" vs "do the regular user" default. This make
the situation teachable: we can point to the flags, and the conditional
default, as *exactly* what varies between the root and non-root cases.
And by manually specifying enough flags, we can ensure those defaults
are overridden and Nix will indeed do the same thing (or fail trying).

This is similar to the logic behind the supplementary group setting
(NixOS#8342), which I also discuss in this new section of the contribution
guide as a second example.

Instead of creating the default dirs in `getDefaultProfile` (which is a
bit odd if the symlink points elsewhere), create the profile dir for the
chosen profile in `createGeneration`. That seems more appropriate and
keeps the test added in e997512 (where
the profiles are deleted but the symlink isn't) working.
  • Loading branch information
Ericson2314 committed Aug 11, 2023
1 parent 584ff40 commit 2c75e65
Show file tree
Hide file tree
Showing 16 changed files with 356 additions and 78 deletions.
22 changes: 17 additions & 5 deletions doc/manual/src/command-ref/files/channels.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
## Channels

A directory containing symlinks to Nix channels, managed by [`nix-channel`]:
A directory containing symlinks to Nix channels, managed by [`nix-channel`].

- `$XDG_STATE_HOME/nix/profiles/channels` for regular users
- `$NIX_STATE_DIR/profiles/per-user/root/channels` for `root`

[`nix-channel`] uses a [profile](@docroot@/command-ref/files/profiles.md) to store channels.
The channels directory is a [profile](@docroot@/command-ref/files/profiles.md), so as to allow easy management of multiple versions and switching between them.
This profile contains symlinks to the contents of those channels.

### User-specific and global channels

Channels are managed either for a specific user, or for all users globally on the system to share.
This matches the [user-specific vs global conventions](@docroot@/command-ref/files/profiles.md#user-specific-and-global-profiles) of profiles themselves.

- [User-specific channels]{#user-channels} are stored in:
```
$XDG_STATE_HOME/nix/profiles/channels
```

- [Global channels]{#global-channels} are stored in
```
$NIX_STATE_DIR/profiles/per-user/root/channels
```

## Subscribed channels

The list of subscribed channels is stored in
Expand Down
4 changes: 2 additions & 2 deletions doc/manual/src/command-ref/files/default-nix-expression.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ A symlink that ensures that [`nix-env`] can find your channels:

This symlink points to:

- `$XDG_STATE_HOME/profiles/channels` for regular users
- `$NIX_STATE_DIR/profiles/per-user/root/channels` for `root`
- The [user-specific channels](@docroot@/command-ref/files/profiles.md#user-channels) (`$XDG_STATE_HOME/profiles/channels`) for regular users
- The [global channels](@docroot@/command-ref/files/profiles.md#global-channels) (`$NIX_STATE_DIR/profiles/per-user/root/channels`) for `root`

In a multi-user installation, you may also have `~/.nix-defexpr/channels_root`, which links to the channels of the root user.[`nix-env`]: ../nix-env.md

Expand Down
33 changes: 27 additions & 6 deletions doc/manual/src/command-ref/files/profiles.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
## Profiles

A directory that contains links to profiles managed by [`nix-env`] and [`nix profile`]:
A directory that contains links to profiles managed by [`nix-env`] and [`nix profile`].

- `$XDG_STATE_HOME/nix/profiles` for regular users
- `$NIX_STATE_DIR/profiles/per-user/root` if the user is `root`
### [User-specific and global profiles]{#user-specific-and-global-profiles}

A profile is a directory of symlinks to files in the Nix store.
Profiles can be placed in any directory, but by default, different locations are used depending on whether the profile is intended to be used by a single user, or shared globally by all users.

- The default directory for user-specific profiles is:
```
$XDG_STATE_HOME/nix/profiles
```

Within it, the [default user profile]{#default-user-profile} is:
```
$XDG_STATE_HOME/nix/profiles/profile
```

- The default directory for global profiles is
```
$NIX_STATE_DIR/profiles
```

Within it, the [default global profile]{#default-global-profile} is
```
$NIX_STATE_DIR/profiles/default
```

Nix command will default to the default user profile for regular users, and the default global profiles for root, but often provide flags to override this behavior.

### Filesystem layout

Expand Down Expand Up @@ -63,8 +84,8 @@ A symbolic link to the user's current profile:

By default, this symlink points to:

- `$XDG_STATE_HOME/nix/profiles/profile` for regular users
- `$NIX_STATE_DIR/profiles/per-user/root/profile` for `root`
- The [default user profile](#default-user-profile) (`$XDG_STATE_HOME/nix/profiles/profile`) for regular users
- The [default global profile](#default-global-profile) (`$NIX_STATE_DIR/profiles/per-user/root/profile`) for `root`

The `PATH` environment variable should include `/bin` subdirectory of the profile link (e.g. `~/.nix-profile/bin`) for the user environment to be visible to the user.
The [installer](@docroot@/installation/installing-binary.md) sets this up by default, unless you enable [`use-xdg-base-directories`].
Expand Down
10 changes: 10 additions & 0 deletions doc/manual/src/command-ref/nix-channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ This command has the following operations:
Revert channels to the state before the last call to `nix-channel --update`.
Optionally, you can specify a specific channel *generation* number to restore.

It also has the following flags, which affect all operations:

- `--global`\
Operate on the [global channels](@docroot@/command-ref/files/channels.md#global-channels), i.e. the channels shared by all users.

- `--user`\
Operate on the [user channels](@docroot@/command-ref/files/channels.md#user-channels), i.e. the channels just for the current user.

The list of subscribed channels is stored in `~/.nix-channels`.

{{#include ./opt-common.md}}

{{#include ./env-common.md}}
Expand Down
2 changes: 1 addition & 1 deletion doc/manual/src/command-ref/nix-env.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
[`--arg` *name* *value*]
[`--argstr` *name* *value*]
[{`--file` | `-f`} *path*]
[{`--profile` | `-p`} *path*]
[{`--profile` | `-p`} *path* | `--global` | `--user`]
[`--system-filter` *system*]
[`--dry-run`]

Expand Down
10 changes: 10 additions & 0 deletions doc/manual/src/command-ref/nix-env/opt-common.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ The following options are allowed for all `nix-env` operations, but may not alwa
sequence of user environments called *generations*, one of which is
the *current generation*.

- `--user`\
Specifies the profile as the [default user profile](@docroot@/command-ref/files/profiles.md#default-user-profile) private to this user.
This is a shorthand for passing `--profile` and the path to that default profile.
This is default if the current user is not root.

- `--global`\
Specifies the profile as the [default global profile](@docroot@/command-ref/files/profiles.md#default-global-profile) shared between all users.
A shorthand instead of passing `--profile` and the path to that default profile.
This is default if the current user is root.

- `--dry-run`\
For the `--install`, `--upgrade`, `--uninstall`,
`--switch-generation`, `--delete-generations` and `--rollback`
Expand Down
47 changes: 47 additions & 0 deletions doc/manual/src/contributing/config-guideline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Configuration guidelines

## Don't just autodetect the environment

Nix can be run in a variety of different ways with different permissions.
Regular users and the super user ("root") can run Nix.
Nix can be be run inside other tool's sandboxes too.

It can be tempting to try to "make Nix work" by changing what it does based on what permissions we have.
E.g. if there is some operation we don't think will work as a regular user, we might skip it as `getuid() != 0`.

The problem with just doing this, however, is that it creates more uncertainty for the user.
Nix operations are supposed to be reproducible, but if we start "bending the rules" based on how Nix is run, it gets increasingly likely that an operation would succeed with different results.

The compromise is as follows:

1. Whenever one wants to condition some operation on an expression like `getuid() != 0`, instead condition it on a boolean setting.

2. Make the setting's default value the condition one would have used.

This still provides the convenience of trying to make things work, but it congregates those suspect impure conditionals in just a select few places, namely where the settings re defined.
This makes it easy to, at glance, see all the ways the current environment influences what is being done.

> In the future we plan on making the default expressions (e.g. not just the values they might happen to evaluate to, like just `true` or `false`) show up in the docs for the settings, so finding all such settings as described above is in fact easy.
> Consulting the source code to get this information should not be necessary.
It also makes it easy to ensure that things like `getuid()` cannot matter, by explicitly forcing all those options with conditional defaults one way or the other.

### Examples

- The default profile

The default profile is a user-specific one for regular users, but the global one for root.
Rather than just having a conditional method when looking up its path, instead be able to (unconditionally) look up either a per-user or global profile.
Expose both options, but if neither is explicitly chosen, only then make the choice of which option based on `getuid() == 0`.

- [`require-drop-supplementary-groups`](@docroot@/command-ref/conf-file.md#conf-require-drop-supplementary-groups)

We always want to drop as many permissions as possible when performing builds, to prevent the derivation being built from doing things we do not expect and do not want it to do.
Part of this is dropping "supplementary groups", which are groups in addition to a user's "primary group".
For non-root users we do not expect this to succeed, because special privilages are required to do this (see the setting for details).
For root users so do expect this to succeed, but inside Linux user namespaces the "fake" root we have may still fail.

Rather than conditionally attempt this operation on whether we are root, we always attempt it, and conditionally abort the build if we get a permission error.
(Other non-permission errors are still abort the build unconditionally.)
Furthermore the condition to ignore the permission failure here is not directly based on `getuid() == 0`, but instead `require-drop-supplementary-groups`.
Rather, that setting is defaulted based upon `getuid() == 0`.
3 changes: 3 additions & 0 deletions doc/manual/src/release-notes/rl-next.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@

- The JSON output for derived paths with are store paths is now a string, not an object with a single `path` field.
This only affects `nix-build --json` when "building" non-derivation things like fetched sources, which is a no-op.

- Commands acting on profiles now have `--global` and `--user` flags as a short hand for these default profiles.
This makes explicit the differences between root and non-root, which could only be described obliquely before.
19 changes: 18 additions & 1 deletion src/libcmd/command.cc
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,24 @@ void MixProfile::updateProfile(const BuiltPaths & buildables)

MixDefaultProfile::MixDefaultProfile()
{
profile = getDefaultProfile();

addFlag({
.longName = "user-profile",
.description = "Act on the profile for this current user",
.handler = {[this]() {
profile = getDefaultProfile(DefaultProfileKind::User);
}}
});

addFlag({
.longName = "global-profile",
.description = "Act on the global profile shared between all default users",
.handler = {[this]() {
profile = getDefaultProfile(DefaultProfileKind::Global);
}}
});

profile = getDefaultProfile(defaultDefaultProfileKind());
}

MixEnvironment::MixEnvironment() : ignoreEnvironment(false)
Expand Down
5 changes: 3 additions & 2 deletions src/libexpr/eval-settings.cc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "globals.hh"
#include "profiles.hh"
#include "profiles/channels.hh"
#include "eval.hh"
#include "eval-settings.hh"

Expand Down Expand Up @@ -64,8 +65,8 @@ Strings EvalSettings::getDefaultNixPath()

if (!evalSettings.restrictEval && !evalSettings.pureEval) {
add(getNixDefExpr() + "/channels");
add(rootChannelsDir() + "/nixpkgs", "nixpkgs");
add(rootChannelsDir());
add(globalChannelsDir() + "/nixpkgs", "nixpkgs");
add(globalChannelsDir());
}

return res;
Expand Down
105 changes: 85 additions & 20 deletions src/libstore/profiles.cc
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ static Path makeName(const Path & profile, GenerationNumber num)

Path createGeneration(LocalFSStore & store, Path profile, StorePath outPath)
{
/* Make sure the directory for this profile exists. */
createDirs(dirOf(profile));

/* The new generation number should be higher than old the
previous ones. */
auto [gens, dummy] = findGenerations(profile);
Expand Down Expand Up @@ -305,50 +308,112 @@ std::string optimisticLockProfile(const Path & profile)
}


Path profilesDir()
Path userProfilesDir()
{
auto profileRoot =
(getuid() == 0)
? rootProfilesDir()
: createNixStateDir() + "/profiles";
auto profileRoot = createNixStateDir() + "/profiles";
createDirs(profileRoot);
return profileRoot;
}

Path rootProfilesDir()
/**
* Return the path to the global profile directory (but don't try creating it)
*
* Just used for the global profile and the global channels, but those
* go in different subdirs, so we do not expose this.
*/
static Path globalProfilesDir()
{
return settings.nixStateDir + "/profiles";
}

/**
* The other directory used for global profiles
*/
static Path perUsersRootDir()
{
return settings.nixStateDir + "/profiles/per-user/root";
return globalProfilesDir() + "/per-user/root/";
}

std::vector<Path> globalProfilesDirs()
{
return {
globalProfilesDir(),
perUsersRootDir(),
};
}

Path getDefaultProfile()
/**
* The default per-user profile.
*/
static Path getDefaultUserProfile()
{
Path profileLink = settings.useXDGBaseDirectories ? createNixStateDir() + "/profile" : getHome() + "/.nix-profile";
return userProfilesDir() + "/profile";
}

/**
* The default global profile.
*/
static Path getDefaultGlobalProfile()
{
return globalProfilesDir() + "/default";
}

DefaultProfileKind defaultDefaultProfileKind()
{
return getuid() == 0
? DefaultProfileKind::Global
: DefaultProfileKind::User;
}

Path defaultProfileLink()
{
return settings.useXDGBaseDirectories ? createNixStateDir() + "/profile" : getHome() + "/.nix-profile";
}

std::optional<Path> tryGetDefaultProfile()
{
Path profileLink = defaultProfileLink();
if (pathExists(profileLink))
return absPath(readLink(profileLink), dirOf(profileLink));
else
return std::nullopt;
}

Path getDefaultProfile(DefaultProfileKind profileKind)
{
Path profileLink = defaultProfileLink();
try {
auto profile = profilesDir() + "/profile";
if (!pathExists(profileLink)) {
Path profile;
switch (profileKind) {
case DefaultProfileKind::Global:
profile = getDefaultGlobalProfile();
break;
case DefaultProfileKind::User:
profile = getDefaultUserProfile();
break;
default:
assert(false);
}
replaceSymlink(profile, profileLink);
}
// Backwards compatibiliy measure: Make root's profile available as
// `.../default` as it's what NixOS and most of the init scripts expect
Path globalProfileLink = settings.nixStateDir + "/profiles/default";
if (getuid() == 0 && !pathExists(globalProfileLink)) {
replaceSymlink(profile, globalProfileLink);
}
return absPath(readLink(profileLink), dirOf(profileLink));
} catch (Error &) {
return profileLink;
}
}

Path defaultChannelsDir()
Path userChannelsDir()
{
return profilesDir() + "/channels";
return userProfilesDir() + "/channels";
}

Path rootChannelsDir()
Path globalChannelsDir()
{
return rootProfilesDir() + "/channels";
/* "per-user" might seem like a weird thing to include in the path
to the "global" channels dir; it is done this way for
back-compat. */
return perUsersRootDir() + "/channels";
}

}
Loading

0 comments on commit 2c75e65

Please sign in to comment.