From 2384d360839e27edb3c928da858ec911415c8b4d Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Wed, 17 Nov 2021 23:35:21 +0300 Subject: [PATCH] A setting to follow XDG Base Directory standard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit XDG Base Directory is a standard for locations for storing various files. Nix has a few files which seem to fit in the standard, but currently use a custom location directly in the user's ~, polluting it: - ~/.nix-profile - ~/.nix-defexpr - ~/.nix-channels This commit adds a config option (use-xdg-base-directories) to follow the XDG spec and instead use the following locations: - $XDG_STATE_HOME/nix/profile - $XDG_STATE_HOME/nix/defexpr - $XDG_STATE_HOME/nix/channels If $XDG_STATE_HOME is not set, it is assumed to be ~/.local/state. Co-authored-by: Théophane Hufschmitt <7226587+thufschmitt@users.noreply.github.com> Co-authored-by: Tim Fenney Co-authored-by: pasqui23 Co-authored-by: Artturin Co-authored-by: John Ericson --- doc/manual/src/command-ref/env-common.md | 13 ++++++++++ scripts/install-multi-user.sh | 2 +- scripts/install-nix-from-closure.sh | 8 +++--- scripts/nix-profile-daemon.sh.in | 31 ++++++++++++++++++++++-- scripts/nix-profile.sh.in | 30 ++++++++++++++++++++--- src/libexpr/eval.cc | 2 +- src/libstore/globals.hh | 21 ++++++++++++++++ src/libstore/profiles.cc | 4 +-- src/libstore/profiles.hh | 5 ++-- src/libutil/util.cc | 13 ++++++++++ src/libutil/util.hh | 6 +++++ src/nix-channel/nix-channel.cc | 4 +-- src/nix-env/nix-env.cc | 7 ++++-- tests/common.sh.in | 6 +++-- tests/nix-channel.sh | 13 ++++++++++ tests/nix-profile.sh | 8 ++++++ 16 files changed, 153 insertions(+), 20 deletions(-) diff --git a/doc/manual/src/command-ref/env-common.md b/doc/manual/src/command-ref/env-common.md index bb85a6b07..c5d38db47 100644 --- a/doc/manual/src/command-ref/env-common.md +++ b/doc/manual/src/command-ref/env-common.md @@ -91,3 +91,16 @@ Most Nix commands interpret the following environment variables: variable sets the initial size of the heap in bytes. It defaults to 384 MiB. Setting it to a low value reduces memory consumption, but will increase runtime due to the overhead of garbage collection. + +## XDG Base Directory + +New Nix commands conform to the [XDG Base Directory Specification], and use the following environment variables to determine locations of various state and configuration files: + +- [`XDG_CONFIG_HOME`]{#env-XDG_CONFIG_HOME} (default `~/.config`) +- [`XDG_STATE_HOME`]{#env-XDG_STATE_HOME} (default `~/.local/state`) +- [`XDG_CACHE_HOME`]{#env-XDG_CACHE_HOME} (default `~/.cache`) + +Classic Nix commands can also be made to follow this standard using the [`use-xdg-base-directories`] configuration option. + +[XDG Base Directory Specification]: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html +[`use-xdg-base-directories`]: ../command-ref/conf-file.md#conf-use-xdg-base-directories \ No newline at end of file diff --git a/scripts/install-multi-user.sh b/scripts/install-multi-user.sh index f149ea0d7..7c66538b0 100644 --- a/scripts/install-multi-user.sh +++ b/scripts/install-multi-user.sh @@ -136,7 +136,7 @@ EOF cat <&2 exit 1 @@ -196,7 +198,7 @@ fi # Install an SSL certificate bundle. if [ -z "$NIX_SSL_CERT_FILE" ] || ! [ -f "$NIX_SSL_CERT_FILE" ]; then "$nix/bin/nix-env" -i "$cacert" - export NIX_SSL_CERT_FILE="$HOME/.nix-profile/etc/ssl/certs/ca-bundle.crt" + export NIX_SSL_CERT_FILE="$NIX_LINK/etc/ssl/certs/ca-bundle.crt" fi # Subscribe the user to the Nixpkgs channel and fetch it. @@ -214,8 +216,8 @@ fi added= p= -p_sh=$HOME/.nix-profile/etc/profile.d/nix.sh -p_fish=$HOME/.nix-profile/etc/profile.d/nix.fish +p_sh=$NIX_LINK/etc/profile.d/nix.sh +p_fish=$NIX_LINK/etc/profile.d/nix.fish if [ -z "$NIX_INSTALLER_NO_MODIFY_PROFILE" ]; then # Make the shell source nix.sh during login. for i in .bash_profile .bash_login .profile; do diff --git a/scripts/nix-profile-daemon.sh.in b/scripts/nix-profile-daemon.sh.in index 0a47571ac..235536c65 100644 --- a/scripts/nix-profile-daemon.sh.in +++ b/scripts/nix-profile-daemon.sh.in @@ -2,7 +2,33 @@ if [ -n "${__ETC_PROFILE_NIX_SOURCED:-}" ]; then return; fi __ETC_PROFILE_NIX_SOURCED=1 -export NIX_PROFILES="@localstatedir@/nix/profiles/default $HOME/.nix-profile" +NIX_LINK=$HOME/.nix-profile +if [ -n "$XDG_STATE_HOME" ]; then + NIX_LINK_NEW="$XDG_STATE_HOME/nix/profile" +else + NIX_LINK_NEW=$HOME/.local/state/nix/profile +fi +if ! [ -e "$NIX_LINK" ]; then + NIX_LINK="$NIX_LINK_NEW" +else + if [ -t 2 ] && [ -e "$NIX_LINK_NEW" ]; then + warning="\033[1;35mwarning:\033[0m" + printf "$warning Both %s and legacy %s exist; using the latter.\n" "$NIX_LINK_NEW" "$NIX_LINK" 1>&2 + if [ "$(realpath "$NIX_LINK")" = "$(realpath "$NIX_LINK_NEW")" ]; then + printf " Since the profiles match, you can safely delete either of them.\n" 1>&2 + else + # This should be an exceptionally rare occasion: the only way to get it would be to + # 1. Update to newer Nix; + # 2. Remove .nix-profile; + # 3. Set the $NIX_LINK_NEW to something other than the default user profile; + # 4. Roll back to older Nix. + # If someone did all that, they can probably figure out how to migrate the profile. + printf "$warning Profiles do not match. You should manually migrate from %s to %s.\n" "$NIX_LINK" "$NIX_LINK_NEW" 1>&2 + fi + fi +fi + +export NIX_PROFILES="@localstatedir@/nix/profiles/default $NIX_LINK" # Set $NIX_SSL_CERT_FILE so that Nixpkgs applications like curl work. if [ -n "${NIX_SSL_CERT_FILE:-}" ]; then @@ -34,4 +60,5 @@ else unset -f check_nix_profiles fi -export PATH="$HOME/.nix-profile/bin:@localstatedir@/nix/profiles/default/bin:$PATH" +export PATH="$NIX_LINK/bin:@localstatedir@/nix/profiles/default/bin:$PATH" +unset NIX_LINK diff --git a/scripts/nix-profile.sh.in b/scripts/nix-profile.sh.in index 5636085d4..264d9a8e2 100644 --- a/scripts/nix-profile.sh.in +++ b/scripts/nix-profile.sh.in @@ -2,11 +2,35 @@ if [ -n "$HOME" ] && [ -n "$USER" ]; then # Set up the per-user profile. - NIX_LINK=$HOME/.nix-profile + NIX_LINK="$HOME/.nix-profile" + if [ -n "$XDG_STATE_HOME" ]; then + NIX_LINK_NEW="$XDG_STATE_HOME/nix/profile" + else + NIX_LINK_NEW="$HOME/.local/state/nix/profile" + fi + if ! [ -e "$NIX_LINK" ]; then + NIX_LINK="$NIX_LINK_NEW" + else + if [ -t 2 ] && [ -e "$NIX_LINK_NEW" ]; then + warning="\033[1;35mwarning:\033[0m" + printf "$warning Both %s and legacy %s exist; using the latter.\n" "$NIX_LINK_NEW" "$NIX_LINK" 1>&2 + if [ "$(realpath "$NIX_LINK")" = "$(realpath "$NIX_LINK_NEW")" ]; then + printf " Since the profiles match, you can safely delete either of them.\n" 1>&2 + else + # This should be an exceptionally rare occasion: the only way to get it would be to + # 1. Update to newer Nix; + # 2. Remove .nix-profile; + # 3. Set the $NIX_LINK_NEW to something other than the default user profile; + # 4. Roll back to older Nix. + # If someone did all that, they can probably figure out how to migrate the profile. + printf "$warning Profiles do not match. You should manually migrate from %s to %s.\n" "$NIX_LINK" "$NIX_LINK_NEW" 1>&2 + fi + fi + fi # Set up environment. # This part should be kept in sync with nixpkgs:nixos/modules/programs/environment.nix - export NIX_PROFILES="@localstatedir@/nix/profiles/default $HOME/.nix-profile" + export NIX_PROFILES="@localstatedir@/nix/profiles/default $NIX_LINK" # Set $NIX_SSL_CERT_FILE so that Nixpkgs applications like curl work. if [ -e /etc/ssl/certs/ca-certificates.crt ]; then # NixOS, Ubuntu, Debian, Gentoo, Arch @@ -31,5 +55,5 @@ if [ -n "$HOME" ] && [ -n "$USER" ]; then fi export PATH="$NIX_LINK/bin:$PATH" - unset NIX_LINK + unset NIX_LINK NIX_LINK_NEW fi diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index a48968656..3e37c7f60 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -2496,7 +2496,7 @@ Strings EvalSettings::getDefaultNixPath() res.push_back(s ? *s + "=" + p : p); }; - add(getHome() + "/.nix-defexpr/channels"); + add(settings.useXDGBaseDirectories ? getStateDir() + "/nix/defexpr/channels" : getHome() + "/.nix-defexpr/channels"); add(settings.nixStateDir + "/profiles/per-user/root/channels/nixpkgs", "nixpkgs"); add(settings.nixStateDir + "/profiles/per-user/root/channels"); diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index c3ccb5e11..40f10fde3 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -975,6 +975,27 @@ public: resolves to a different location from that of the build machine. You can enable this setting if you are sure you're not going to do that. )"}; + + Setting useXDGBaseDirectories{ + this, false, "use-xdg-base-directories", + R"( + If set to `true`, Nix will conform to the [XDG Base Directory Specification] for files in `$HOME`. + The environment variables used to implement this are documented in the [Environment Variables section](@docroot@/installation/env-variables.md). + + [XDG Base Directory Specification]: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + + > **Warning** + > This changes the location of some well-known symlinks that Nix creates, which might break tools that rely on the old, non-XDG-conformant locations. + + In particular, the following locations change: + + | Old | New | + |-------------------|--------------------------------| + | `~/.nix-profile` | `$XDG_STATE_HOME/nix/profile` | + | `~/.nix-defexpr` | `$XDG_STATE_HOME/nix/defexpr` | + | `~/.nix-channels` | `$XDG_STATE_HOME/nix/channels` | + )" + }; }; diff --git a/src/libstore/profiles.cc b/src/libstore/profiles.cc index b202351ce..c551c5f3e 100644 --- a/src/libstore/profiles.cc +++ b/src/libstore/profiles.cc @@ -282,7 +282,7 @@ std::string optimisticLockProfile(const Path & profile) Path profilesDir() { - auto profileRoot = getDataDir() + "/nix/profiles"; + auto profileRoot = createNixStateDir() + "/profiles"; createDirs(profileRoot); return profileRoot; } @@ -290,7 +290,7 @@ Path profilesDir() Path getDefaultProfile() { - Path profileLink = getHome() + "/.nix-profile"; + Path profileLink = settings.useXDGBaseDirectories ? createNixStateDir() + "/profile" : getHome() + "/.nix-profile"; try { auto profile = getuid() == 0 diff --git a/src/libstore/profiles.hh b/src/libstore/profiles.hh index 73667a798..fbf95b850 100644 --- a/src/libstore/profiles.hh +++ b/src/libstore/profiles.hh @@ -72,8 +72,9 @@ std::string optimisticLockProfile(const Path & profile); profiles. */ Path profilesDir(); -/* Resolve ~/.nix-profile. If ~/.nix-profile doesn't exist yet, create - it. */ +/* Resolve the default profile (~/.nix-profile by default, $XDG_STATE_HOME/ + nix/profile if XDG Base Directory Support is enabled), and create if doesn't + exist */ Path getDefaultProfile(); } diff --git a/src/libutil/util.cc b/src/libutil/util.cc index 40faa4bf2..40a54b010 100644 --- a/src/libutil/util.cc +++ b/src/libutil/util.cc @@ -608,6 +608,19 @@ Path getDataDir() return dataDir ? *dataDir : getHome() + "/.local/share"; } +Path getStateDir() +{ + auto stateDir = getEnv("XDG_STATE_HOME"); + return stateDir ? *stateDir : getHome() + "/.local/state"; +} + +Path createNixStateDir() +{ + Path dir = getStateDir() + "/nix"; + createDirs(dir); + return dir; +} + std::optional getSelfExe() { diff --git a/src/libutil/util.hh b/src/libutil/util.hh index 266da0ae3..4fadafaf2 100644 --- a/src/libutil/util.hh +++ b/src/libutil/util.hh @@ -158,6 +158,12 @@ Path getDataDir(); /* Return the path of the current executable. */ std::optional getSelfExe(); +/* Return $XDG_STATE_HOME or $HOME/.local/state. */ +Path getStateDir(); + +/* Create the Nix state directory and return the path to it. */ +Path createNixStateDir(); + /* Create a directory and all its parents, if necessary. Returns the list of created directories, in order of creation. */ Paths createDirs(const Path & path); diff --git a/src/nix-channel/nix-channel.cc b/src/nix-channel/nix-channel.cc index 263d85eea..338a7d18e 100755 --- a/src/nix-channel/nix-channel.cc +++ b/src/nix-channel/nix-channel.cc @@ -164,8 +164,8 @@ static int main_nix_channel(int argc, char ** argv) { // Figure out the name of the `.nix-channels' file to use auto home = getHome(); - channelsList = home + "/.nix-channels"; - nixDefExpr = home + "/.nix-defexpr"; + channelsList = settings.useXDGBaseDirectories ? createNixStateDir() + "/channels" : home + "/.nix-channels"; + nixDefExpr = settings.useXDGBaseDirectories ? createNixStateDir() + "/defexpr" : home + "/.nix-defexpr"; // Figure out the name of the channels profile. profile = profilesDir() + "/channels"; diff --git a/src/nix-env/nix-env.cc b/src/nix-env/nix-env.cc index 406e548c0..0daf374de 100644 --- a/src/nix-env/nix-env.cc +++ b/src/nix-env/nix-env.cc @@ -1289,7 +1289,7 @@ static void opSwitchProfile(Globals & globals, Strings opFlags, Strings opArgs) throw UsageError("exactly one argument expected"); Path profile = absPath(opArgs.front()); - Path profileLink = getHome() + "/.nix-profile"; + Path profileLink = settings.useXDGBaseDirectories ? createNixStateDir() + "/profile" : getHome() + "/.nix-profile"; switchLink(profileLink, profile); } @@ -1393,7 +1393,10 @@ static int main_nix_env(int argc, char * * argv) Globals globals; globals.instSource.type = srcUnknown; - globals.instSource.nixExprPath = getHome() + "/.nix-defexpr"; + { + Path nixExprPath = settings.useXDGBaseDirectories ? createNixStateDir() + "/defexpr" : getHome() + "/.nix-defexpr"; + globals.instSource.nixExprPath = nixExprPath; + } globals.instSource.systemFilter = "*"; if (!pathExists(globals.instSource.nixExprPath)) { diff --git a/tests/common.sh.in b/tests/common.sh.in index 74bbbc8ca..bbe97da68 100644 --- a/tests/common.sh.in +++ b/tests/common.sh.in @@ -27,6 +27,8 @@ export NIX_REMOTE=$NIX_REMOTE_ unset NIX_PATH export TEST_HOME=$TEST_ROOT/test-home export HOME=$TEST_HOME +unset XDG_STATE_HOME +unset XDG_DATA_HOME unset XDG_CONFIG_HOME unset XDG_CONFIG_DIRS unset XDG_CACHE_HOME @@ -62,8 +64,8 @@ readLink() { } clearProfiles() { - profiles="$HOME"/.local/share/nix/profiles - rm -rf $profiles + profiles="$HOME"/.local/state/nix/profiles + rm -rf "$profiles" } clearStore() { diff --git a/tests/nix-channel.sh b/tests/nix-channel.sh index 54b8f5979..b64283f48 100644 --- a/tests/nix-channel.sh +++ b/tests/nix-channel.sh @@ -12,6 +12,19 @@ nix-channel --remove xyzzy [ -e $TEST_HOME/.nix-channels ] [ "$(cat $TEST_HOME/.nix-channels)" = '' ] +# Test the XDG Base Directories support + +export NIX_CONFIG="use-xdg-base-directories = true" + +nix-channel --add http://foo/bar xyzzy +nix-channel --list | grep -q http://foo/bar +nix-channel --remove xyzzy + +unset NIX_CONFIG + +[ -e $TEST_HOME/.local/state/nix/channels ] +[ "$(cat $TEST_HOME/.local/state/nix/channels)" = '' ] + # Create a channel. rm -rf $TEST_ROOT/foo mkdir -p $TEST_ROOT/foo diff --git a/tests/nix-profile.sh b/tests/nix-profile.sh index 7ba3235fa..266dc9e49 100644 --- a/tests/nix-profile.sh +++ b/tests/nix-profile.sh @@ -56,6 +56,14 @@ nix profile history nix profile history | grep "packages.$system.default: ∅ -> 1.0" nix profile diff-closures | grep 'env-manifest.nix: ε → ∅' +# Test XDG Base Directories support + +export NIX_CONFIG="use-xdg-base-directories = true" +nix profile remove 1 +nix profile install $flake1Dir +[[ $($TEST_HOME/.local/state/nix/profile/bin/hello) = "Hello World" ]] +unset NIX_CONFIG + # Test upgrading a package. printf NixOS > $flake1Dir/who printf 2.0 > $flake1Dir/version