From e63572f54035a9bdcd82e076c0be1c3221406d42 Mon Sep 17 00:00:00 2001 From: Qyriad Date: Fri, 3 May 2024 08:21:03 -0600 Subject: [PATCH] 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 | 8 +- src/libstore/local-store.cc | 17 +++ src/libstore/local-store.hh | 2 + src/libstore/profiles.cc | 60 ++++++--- src/libstore/profiles.hh | 2 + src/libstore/store-api.hh | 5 + src/nix-channel/nix-channel.cc | 2 +- .../nix-collect-garbage.cc | 8 +- src/nix-env/nix-env.cc | 30 +++-- src/nix-env/user-env.cc | 8 +- src/nix/daemon.cc | 9 +- .../common/vars-and-functions.sh.in | 2 +- tests/functional/user-envs.sh | 4 +- tests/nixos/default.nix | 2 + tests/nixos/profiles.nix | 117 ++++++++++++++++++ 15 files changed, 241 insertions(+), 35 deletions(-) create mode 100644 tests/nixos/profiles.nix diff --git a/src/libcmd/command.cc b/src/libcmd/command.cc index de9f546fc..4e65a70e1 100644 --- a/src/libcmd/command.cc +++ b/src/libcmd/command.cc @@ -269,7 +269,13 @@ void MixProfile::updateProfile(const BuiltPaths & buildables) MixDefaultProfile::MixDefaultProfile() { - profile = getDefaultProfile(); + // `nix profile remove` at the least expects this. huh. + try { + this->profile = ensureDefaultProfile(); + } catch (SysError const & e) { + // We get called in __dumpCli too, so we should not do fallable IO. + printTalkative("ignoring error initializing default profile %s", e.what()); + } } MixEnvironment::MixEnvironment() : ignoreEnvironment(false) diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 2f59b3591..6d879fac0 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -217,6 +217,8 @@ LocalStore::LocalStore(const Params & params) } } + createUser(getUserName(), getuid()); + /* Optionally, create directories and set permissions for a multi-user install. */ if (getuid() == 0 && settings.buildUsersGroup != "") { @@ -1793,6 +1795,21 @@ void LocalStore::signRealisation(Realisation & realisation) } } +void LocalStore::createUser(std::string_view userName, uid_t userId) +{ + // XXX: previously created gcroots/per-user; should this too? + auto const perUserProfile = fmt("%s/profiles/per-user/%s", stateDir, userName); + createDirs(perUserProfile); + + if (chmod(perUserProfile.c_str(), 0755) == -1) { + throw SysError(errno, "changing permissions of directory '%s'", perUserProfile); + } + + if (chown(perUserProfile.c_str(), userId, getgid()) == -1) { + throw SysError(errno, "changing owner of directory '%s'", perUserProfile); + } +} + 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 b8d1f02ab..b287de22d 100644 --- a/src/libstore/local-store.hh +++ b/src/libstore/local-store.hh @@ -366,6 +366,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 239047dd6..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() @@ -320,25 +315,52 @@ Path rootProfilesDir() return settings.nixStateDir + "/profiles/per-user/root"; } +Path getDefaultProfileLink() +{ + if (getuid() == 0) { + return settings.nixStateDir + "/profiles/default"; + } + + if (settings.useXDGBaseDirectories) { + return createNixStateDir() + "/profile"; + } + 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 193c0bf21..6b0e69463 100644 --- a/src/libstore/profiles.hh +++ b/src/libstore/profiles.hh @@ -234,4 +234,6 @@ Path rootChannelsDir(); */ Path getDefaultProfile(); +Path ensureDefaultProfile(); + } diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh index cb9f8e4a6..2f86c0c5a 100644 --- a/src/libstore/store-api.hh +++ b/src/libstore/store-api.hh @@ -859,6 +859,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-channel/nix-channel.cc b/src/nix-channel/nix-channel.cc index 26003f021..c7d951d0b 100644 --- a/src/nix-channel/nix-channel.cc +++ b/src/nix-channel/nix-channel.cc @@ -169,7 +169,7 @@ static int main_nix_channel(int argc, char ** argv) nixDefExpr = getNixDefExpr(); // Figure out the name of the channels profile. - profile = profilesDir() + "/channels"; + profile = defaultChannelsDir(); createDirs(dirOf(profile)); enum { diff --git a/src/nix-collect-garbage/nix-collect-garbage.cc b/src/nix-collect-garbage/nix-collect-garbage.cc index 1cbba0537..3b7dba127 100644 --- a/src/nix-collect-garbage/nix-collect-garbage.cc +++ b/src/nix-collect-garbage/nix-collect-garbage.cc @@ -81,7 +81,13 @@ static int main_nix_collect_garbage(int argc, char * * argv) if (removeOld) { std::set dirsToClean = { - profilesDir(), settings.nixStateDir + "/profiles", dirOf(getDefaultProfile())}; + // XXX: check + profilesDir(), + // XXX: refactor + settings.nixStateDir + "/profiles", + /// XXX: check + dirOf(getDefaultProfile()), + }; for (auto & dir : dirsToClean) removeOldGenerations(dir); } diff --git a/src/nix-env/nix-env.cc b/src/nix-env/nix-env.cc index ad255a1e1..23e2037fd 100644 --- a/src/nix-env/nix-env.cc +++ b/src/nix-env/nix-env.cc @@ -503,7 +503,7 @@ static bool keep(DrvInfo & drv) static void installDerivations(Globals & globals, const Strings & args, const Path & profile) { - debug("installing derivations"); + debug("installing derivations into profile %s", profile); /* Get the set of user environment elements to be installed. */ DrvInfos newElems, newElemsTmp; @@ -554,8 +554,17 @@ static void installDerivations(Globals & globals, if (globals.dryRun) return; - if (createUserEnv(*globals.state, allElems, - profile, settings.envKeepDerivations, lockToken)) break; + bool success = createUserEnv( + *globals.state, + allElems, + profile, + settings.envKeepDerivations, + lockToken + ); + + if (success) { + break; + } } } @@ -1296,6 +1305,7 @@ static void opSwitchProfile(Globals & globals, Strings opFlags, Strings opArgs) if (opArgs.size() != 1) throw UsageError("exactly one argument expected"); + // XXX: refactor Path profile = absPath(opArgs.front()); Path profileLink = settings.useXDGBaseDirectories ? createNixStateDir() + "/profile" : getHome() + "/.nix-profile"; @@ -1463,8 +1473,10 @@ static int main_nix_env(int argc, char * * argv) op = opQuery; opName = "-query"; } - else if (*arg == "--profile" || *arg == "-p") + else if (*arg == "--profile" || *arg == "-p") { + // XXX: check globals.profile = absPath(getArg(*arg, arg, end)); + } else if (*arg == "--file" || *arg == "-f") file = getArg(*arg, arg, end); else if (*arg == "--switch-profile" || *arg == "-S") { @@ -1528,11 +1540,15 @@ static int main_nix_env(int argc, char * * argv) globals.instSource.autoArgs = myArgs.getAutoArgs(*globals.state); - if (globals.profile == "") + if (globals.profile == "") { + // XXX: refactor? globals.profile = getEnv("NIX_PROFILE").value_or(""); + } - if (globals.profile == "") - globals.profile = getDefaultProfile(); + if (globals.profile == "") { + // XXX: check + globals.profile = ensureDefaultProfile(); + } 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); } 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/functional/user-envs.sh b/tests/functional/user-envs.sh index dcd6b1b97..8d7d1ea38 100644 --- a/tests/functional/user-envs.sh +++ b/tests/functional/user-envs.sh @@ -37,8 +37,10 @@ nix-env -qa '*' --description | grepQuiet silly # Query the system. nix-env -qa '*' --system | grepQuiet $system +readlink $HOME/.nix-profile || true +eza --tree -la $HOME/.local/state/nix || true # Install "foo-1.0". -nix-env -i foo-1.0 +nix-env --debug -i foo-1.0 # Query installed: should contain foo-1.0 now (which should be # executable). 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..0c32c7d69 --- /dev/null +++ b/tests/nixos/profiles.nix @@ -0,0 +1,117 @@ +{ 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"; + +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 + ]; + users.users.alice.isNormalUser = true; + #virtualisation.additionalPaths = [ pkgs.hello.drvPath ]; + #nix.settings.substituters = lib.mkForce [ ]; + #nix.settings.experimental-features = [ "nix-command" "flakes" ]; + 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; + #services.getty.autologinUser = "alice"; + }; + }; + + 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(""" + set -x + #nix config show >&2 + nix-env --version -vv >&2 + """) + + print(machine.succeed("systemctl cat nix-daemon.service")) + + # Initial state. + machine.succeed(""" + su --login alice -c ' + set -euxo pipefail + ${ezaTree} ~ + ${ezaTree} /nix/var/nix + :' >&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 + """) + ''; +}