From ef8ff588027597fa3411dc6e642b0a3b72dc34df Mon Sep 17 00:00:00 2001 From: Qyriad Date: Sat, 11 May 2024 20:13:50 -0600 Subject: [PATCH 1/2] tiny cleanup in user-env.cc Change-Id: I05a619284974342767eb770f8b876bf697b9ae95 --- src/nix-env/nix-env.cc | 2 +- src/nix-env/user-env.cc | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/nix-env/nix-env.cc b/src/nix-env/nix-env.cc index eb33020e0..ed5d53cbc 100644 --- a/src/nix-env/nix-env.cc +++ b/src/nix-env/nix-env.cc @@ -1532,7 +1532,7 @@ static int main_nix_env(int argc, char * * argv) globals.profile = getEnv("NIX_PROFILE").value_or(""); if (globals.profile == "") { - globals.profile = ensureDefaultProfile(); + globals.profile = getDefaultProfile(); } op(globals, std::move(opFlags), std::move(opArgs)); diff --git a/src/nix-env/user-env.cc b/src/nix-env/user-env.cc index f0131a458..70a1fe903 100644 --- a/src/nix-env/user-env.cc +++ b/src/nix-env/user-env.cc @@ -20,6 +20,7 @@ bool createUserEnv(EvalState & state, DrvInfos & elems, const Path & profile, bool keepDerivations, const std::string & lockToken) { + debug("asked to create a user env %s for %u drvs", profile, elems.size()); /* Build the components in the user environment, if they don't exist already. */ std::vector drvsToBuild; @@ -131,10 +132,11 @@ bool createUserEnv(EvalState & state, DrvInfos & elems, state.repair ? bmRepair : bmNormal); /* Switch the current user environment to the output path. */ - auto store2 = state.store.dynamic_pointer_cast(); + auto localStore = state.store.dynamic_pointer_cast(); - if (store2) { + if (localStore) { PathLocks lock; + debug("locking profile %s", profile); lockProfile(lock, profile); Path lockTokenCur = optimisticLockProfile(profile); @@ -144,7 +146,7 @@ bool createUserEnv(EvalState & state, DrvInfos & elems, } debug("switching to new user environment"); - Path generation = createGeneration(*store2, profile, topLevelOut); + Path generation = createGeneration(*localStore, profile, topLevelOut); switchLink(profile, generation); } From 5f7f0e2e2c40bfb6a6ec38904cf4c31fff1af2ec Mon Sep 17 00:00:00 2001 From: Qyriad Date: Sat, 11 May 2024 18:05:14 -0600 Subject: [PATCH 2/2] WIP: restore /nix/var/nix as source of truth for profiles See-also: https://github.com/NixOS/nix/pull/5226 Change-Id: I35082c9a59148f7fb132232659c3a254d514d190 --- src/libcmd/command.cc | 10 +- src/libstore/local-store.cc | 25 ++++ src/libstore/local-store.hh | 2 + src/libstore/profiles.cc | 49 ++++--- src/libstore/profiles.hh | 9 +- src/libstore/store-api.hh | 5 + src/nix-env/nix-env.cc | 2 +- src/nix/daemon.cc | 9 +- .../common/vars-and-functions.sh.in | 2 +- tests/nixos/default.nix | 2 + tests/nixos/profiles.nix | 128 ++++++++++++++++++ 11 files changed, 218 insertions(+), 25 deletions(-) create mode 100644 tests/nixos/profiles.nix 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-env/nix-env.cc b/src/nix-env/nix-env.cc index ed5d53cbc..eb33020e0 100644 --- a/src/nix-env/nix-env.cc +++ b/src/nix-env/nix-env.cc @@ -1532,7 +1532,7 @@ static int main_nix_env(int argc, char * * argv) globals.profile = getEnv("NIX_PROFILE").value_or(""); if (globals.profile == "") { - globals.profile = getDefaultProfile(); + globals.profile = ensureDefaultProfile(); } op(globals, std::move(opFlags), std::move(opArgs)); 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 + """) + ''; +}