diff --git a/src/libcmd/command.cc b/src/libcmd/command.cc index de9f546fc..74dff5dd7 100644 --- a/src/libcmd/command.cc +++ b/src/libcmd/command.cc @@ -269,7 +269,15 @@ void MixProfile::updateProfile(const BuiltPaths & buildables) MixDefaultProfile::MixDefaultProfile() { - profile = getDefaultProfile(); + static bool haveWarned = false; + try { + // `nix profile remove` at the least seems to expect this. + this->profile = ensureDefaultProfile(); + } catch (SysError const & e) { + // However we get called in __dumpCli too, so we should not do fatal IO. + warnOnce(haveWarned, "ignoring error initializing default profile %s", e.what()); + this->profile = getDefaultProfile(); + } } MixEnvironment::MixEnvironment() : ignoreEnvironment(false) diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 7bcbe3298..ec4e90c44 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -216,6 +216,10 @@ LocalStore::LocalStore(const Params & params) } } + if (!readOnly) { + createUser(getUserName(), getuid()); + } + /* Optionally, create directories and set permissions for a multi-user install. */ if (getuid() == 0 && settings.buildUsersGroup != "") { @@ -1788,6 +1792,27 @@ void LocalStore::signRealisation(Realisation & realisation) } } +void LocalStore::createUser(std::string_view userName, uid_t userId) +{ + using namespace std::literals::string_view_literals; + + // FIXME(Qyriad): is /nix/var/nix/gcroots/per-user used anywhere...? + for (auto const & dirPart : {"profiles"sv, "gcroots"sv}) { + + auto const fullPerUserDir = fmt("%s/%s/per-user/%s", stateDir, dirPart, userName); + createDirs(fullPerUserDir); + + if (chmod(fullPerUserDir.c_str(), 0755) == -1) { + throw SysError(errno, "changing permissions of directory '%s'", fullPerUserDir); + } + + if (chown(fullPerUserDir.c_str(), userId, getgid()) == -1) { + throw SysError(errno, "changing owner of directory '%s'", fullPerUserDir); + } + } + +} + void LocalStore::signPathInfo(ValidPathInfo & info) { // FIXME: keep secret keys in memory. diff --git a/src/libstore/local-store.hh b/src/libstore/local-store.hh index 14f024ca9..7496a22a1 100644 --- a/src/libstore/local-store.hh +++ b/src/libstore/local-store.hh @@ -364,6 +364,8 @@ private: void signPathInfo(ValidPathInfo & info); void signRealisation(Realisation &); + void createUser(std::string_view userName, uid_t userId) override; + // XXX: Make a generic `Store` method ContentAddress hashCAPath( const ContentAddressMethod & method, diff --git a/src/libstore/profiles.cc b/src/libstore/profiles.cc index d8717ab8b..7bb264faa 100644 --- a/src/libstore/profiles.cc +++ b/src/libstore/profiles.cc @@ -307,12 +307,7 @@ std::string optimisticLockProfile(const Path & profile) Path profilesDir() { - auto profileRoot = - (getuid() == 0) - ? rootProfilesDir() - : createNixStateDir() + "/profiles"; - createDirs(profileRoot); - return profileRoot; + return fmt("%s/profiles/per-user/%s", settings.nixStateDir, getUserName()); } Path rootProfilesDir() @@ -332,24 +327,40 @@ Path getDefaultProfileLink() return getHome() + "/.nix-profile"; } +Path ensureDefaultProfile() +{ + Path const profileLink = getDefaultProfileLink(); + Path const defaultProfile = profilesDir() + "/profile"; + + if (!pathExists(profileLink)) { + replaceSymlink(defaultProfile, profileLink); + } + + // Backwards compatibiliy measure: Make root's profile available as + // `.../default` as it's what NixOS and most of the init scripts expect + Path const globalProfileLink = settings.nixStateDir + "/profiles/default"; + if (getuid() == 0 && !pathExists(globalProfileLink)) { + replaceSymlink(defaultProfile, globalProfileLink); + } + + return absPath(readLink(profileLink), dirOf(profileLink)); +} + Path getDefaultProfile() { - Path profileLink = settings.useXDGBaseDirectories ? createNixStateDir() + "/profile" : getHome() + "/.nix-profile"; + Path const profileLink = getDefaultProfileLink(); + try { - auto profile = profilesDir() + "/profile"; - if (!pathExists(profileLink)) { - replaceSymlink(profile, profileLink); + if (pathExists(profileLink)) { + return absPath(readLink(profileLink), dirOf(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; + } catch (SysError const & e) { + printTalkative("ignoring error resolving default profile '%s': %s", profileLink, e.what()); + } catch (Error const & e) { + printError("ignoring error resolving default profile '%s': %s", profileLink, e.what()); } + + return profileLink; } Path defaultChannelsDir() diff --git a/src/libstore/profiles.hh b/src/libstore/profiles.hh index bed8bf527..c1e0dfbe4 100644 --- a/src/libstore/profiles.hh +++ b/src/libstore/profiles.hh @@ -235,9 +235,14 @@ Path getDefaultProfileLink(); /** * 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 + * $XDG_STATE_HOME/nix/profile if XDG Base Directory Support is enabled). */ Path getDefaultProfile(); +/** + * Resolve the default profile (see getDefaultProfile()), + * and create it if it doesn't exist. + */ +Path ensureDefaultProfile(); + } diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh index 47e644fed..7df471b60 100644 --- a/src/libstore/store-api.hh +++ b/src/libstore/store-api.hh @@ -845,6 +845,11 @@ public: return toRealPath(printStorePath(storePath)); } + virtual void createUser(std::string_view userName, uid_t userId) + { + warn("base class Store called unimplemented createUser()"); + } + /** * Synchronises the options of the client with those of the daemon * (a no-op when there’s no daemon) diff --git a/src/nix/daemon.cc b/src/nix/daemon.cc index 9d4afb6d9..eaea8085a 100644 --- a/src/nix/daemon.cc +++ b/src/nix/daemon.cc @@ -357,6 +357,13 @@ static void daemonLoop(std::optional forceTrustClientOpt) // Restore normal handling of SIGCHLD. setSigChldAction(false); + auto store = openUncachedStore(); + try { + store->createUser(user, peer.uid); + } catch (SysError const & e) { + printError("ignoring error while creating store per-user state: %s", e.what()); + } + // For debugging, stuff the pid into argv[1]. if (peer.pidKnown && savedArgv[1]) { auto processName = std::to_string(peer.pid); @@ -366,7 +373,7 @@ static void daemonLoop(std::optional forceTrustClientOpt) // Handle the connection. FdSource from(remote.get()); FdSink to(remote.get()); - processConnection(openUncachedStore(), from, to, trusted, NotRecursive); + processConnection(store, from, to, trusted, NotRecursive); exit(0); }, options); diff --git a/tests/functional/common/vars-and-functions.sh.in b/tests/functional/common/vars-and-functions.sh.in index 3d2e44024..ab1e60b30 100644 --- a/tests/functional/common/vars-and-functions.sh.in +++ b/tests/functional/common/vars-and-functions.sh.in @@ -66,7 +66,7 @@ readLink() { } clearProfiles() { - profiles="$HOME"/.local/state/nix/profiles + profiles="$NIX_STATE_DIR/profiles/per-user/$(whoami || echo -n nixbld)" rm -rf "$profiles" } diff --git a/tests/nixos/default.nix b/tests/nixos/default.nix index 987463b07..27c2ad186 100644 --- a/tests/nixos/default.nix +++ b/tests/nixos/default.nix @@ -143,6 +143,8 @@ in nix-upgrade-nix = runNixOSTestFor "x86_64-linux" ./nix-upgrade-nix.nix; + profiles = runNixOSTestFor "x86_64-linux" ./profiles.nix; + nssPreload = runNixOSTestFor "x86_64-linux" ./nss-preload.nix; githubFlakes = runNixOSTestFor "x86_64-linux" ./github-flakes.nix; diff --git a/tests/nixos/profiles.nix b/tests/nixos/profiles.nix new file mode 100644 index 000000000..0606f9ad6 --- /dev/null +++ b/tests/nixos/profiles.nix @@ -0,0 +1,128 @@ +{ lib, config, ... }: + +let + pkgs = config.nodes.machine.nixpkgs.pkgs; + eza = "${lib.getExe pkgs.eza} -laa --group-directories-first --classify --color=always"; + ezaTree = "${lib.getExe pkgs.eza} -la --tree --color=always"; + + oldNix = pkgs.nixVersions.nix_2_18; + + nix23-daemon = lib.getExe' oldNix "nix-daemon"; + nix23-env = lib.getExe' oldNix "nix-env"; + +in { + name = "nix-profiles"; + + nodes = { + machine = { config, lib, pkgs, ... }: { + virtualisation.writableStore = true; + virtualisation.additionalPaths = [ + pkgs.hello + pkgs.cowsay.out + pkgs.cowsay.man + pkgs.cowsay.drvPath + pkgs.eza + (lib.getBin pkgs.nixVersions.nix_2_3) + ]; + users.users.alice.isNormalUser = true; + nix.settings = { + substituters = lib.mkForce [ ]; + experimental-features = [ "nix-command" "flakes" ]; + allowed-users = [ "alice" ]; + use-xdg-base-directories = true; + }; + nix.nixPath = [ "nixpkgs=${pkgs.path}" ]; + #nix.package = pkgs.nixVersions.nix_2_18; + #nix.package = pkgs.nixVersions.nix_2_3; + }; + }; + + testScript = { nodes }: '' + # fmt: off + + start_all() + machine.wait_for_unit("multi-user.target") + + machine.succeed("nix -vv --version >&2") + print(machine.succeed("${eza} ~ >&2")) + print(machine.succeed("realpath ~ >&2")) + + machine.succeed("nix --version -vv >&2") + + # Initial state. + #machine.succeed(""" + # su --login alice -c ' + # set -euxo pipefail + # ${ezaTree} ~ + # ${ezaTree} /nix/var/nix + # :' >&2 + #""") + + # Create some client-side profiles for us to worry about. + machine.succeed(""" + export NIX_DAEMON_SOCKET_PATH=/tmp/nix23-socket + ${nix23-daemon} >&2 & + export _NIX_DAEMON_PID=$! + su --login alice -c ' + export NIX_DAEMON_SOCKET_PATH=/tmp/nix23-socket + ${nix23-env} --version -vv + ${nix23-env} --file "" --install -A hello + ${ezaTree} ~ + ${ezaTree} /nix/var/nix/profiles + :' >&2 + kill "$_NIX_DAEMON_PID" >&2 + """) + + # This one is as root. + #machine.succeed(""" + # nix-env --store local --file "" --install -A hello >&2 + # ${ezaTree} ~ >&2 + # ${ezaTree} /nix/var/nix/profiles >&2 + #""") + + machine.succeed(""" + su --login alice -c ' + set -euxo pipefail + nix-env --file "" --install -A hello + ${ezaTree} ~ + ${ezaTree} /nix/var/nix/profiles + :' >&2 + """) + + machine.succeed(""" + su --login alice -c ' + set -euxo pipefail + nix-env --file "" --install -A cowsay + ${ezaTree} ~ + ${ezaTree} /nix/var/nix/profiles + :' >&2 + """) + + machine.succeed(""" + su --login alice -c ' + set -euxo pipefail + PAGER=cat nix-env --option use-xdg-base-directories false --query '*' + ${ezaTree} ~ + ${ezaTree} /nix/var/nix/profiles + :' >&2 + """) + + machine.succeed(""" + su --login alice -c ' + set -euxo pipefail + nix-env --uninstall cowsay + ${ezaTree} ~ + ${ezaTree} /nix/var/nix/profiles + :' >&2 + """) + + machine.succeed(""" + su --login alice -c ' + set -euxo pipefail + nix profile install -f "" cowsay + ${ezaTree} ~ + ${ezaTree} /nix/var/nix/profiles + :' >&2 + """) + ''; +}