From 179e1c126ad6d18b69947850a6d760e6091e457f Mon Sep 17 00:00:00 2001 From: Qyriad Date: Sat, 27 Apr 2024 17:28:56 -0600 Subject: [PATCH 1/3] fix `nix upgrade-nix` on new-style profiles nix3-profile automatically migrates any profile its used on to its style of profile -- the ones with manifest.json instead of manifest.nix. On non-NixOS systems, Nix is conventionally installed to the profile at /nix/var/nix/profiles/default, so if a user passed that to `--profile` of `nix profile`, then it would break upgrade-nix from ever working again, without recreating the profile. This commit fixes that, and allows upgrade-nix to work on either kind of profile. Fixes #16. Change-Id: I4c49b1beba93bb50e8f8a107edc451affe08c3f7 --- .../rl-next/upgrade-nix-profile-compat.md | 8 ++ src/libcmd/cmd-profiles.cc | 5 +- src/nix/upgrade-nix.cc | 123 ++++++++++++++++-- 3 files changed, 119 insertions(+), 17 deletions(-) create mode 100644 doc/manual/rl-next/upgrade-nix-profile-compat.md diff --git a/doc/manual/rl-next/upgrade-nix-profile-compat.md b/doc/manual/rl-next/upgrade-nix-profile-compat.md new file mode 100644 index 000000000..df9879c6f --- /dev/null +++ b/doc/manual/rl-next/upgrade-nix-profile-compat.md @@ -0,0 +1,8 @@ +--- +synopsis: using `nix profile` on `/nix/var/nix/profiles/default` no longer breaks `nix upgrade-nix` +cls: 952 +--- + +On non-NixOS, Nix is conventionally installed into a `nix-env` style profile at /nix/var/nix/profiles/default. +Like any `nix-env` profile, using `nix profile` on it automatically migrates it to a `nix profile` style profile, which is incompatible with `nix-env`. +`nix upgrade-nix` previously relied solely on `nix-env` to do the upgrade, but now will work fine with either kind of profile. diff --git a/src/libcmd/cmd-profiles.cc b/src/libcmd/cmd-profiles.cc index b487d2a77..4d8ff7438 100644 --- a/src/libcmd/cmd-profiles.cc +++ b/src/libcmd/cmd-profiles.cc @@ -141,10 +141,7 @@ ProfileManifest::ProfileManifest(EvalState & state, const Path & profile) } elements.emplace_back(std::move(element)); } - } - - else if (pathExists(profile + "/manifest.nix")) - { + } else if (pathExists(profile + "/manifest.nix")) { // FIXME: needed because of pure mode; ugly. state.allowPath(state.store->followLinksToStore(profile)); state.allowPath(state.store->followLinksToStore(profile + "/manifest.nix")); diff --git a/src/nix/upgrade-nix.cc b/src/nix/upgrade-nix.cc index af219c1b9..fd847fa13 100644 --- a/src/nix/upgrade-nix.cc +++ b/src/nix/upgrade-nix.cc @@ -1,5 +1,11 @@ +#include + +#include "cmd-profiles.hh" #include "command.hh" #include "common-args.hh" +#include "local-fs-store.hh" +#include "logging.hh" +#include "profiles.hh" #include "store-api.hh" #include "filetransfer.hh" #include "eval.hh" @@ -10,7 +16,7 @@ using namespace nix; -struct CmdUpgradeNix : MixDryRun, StoreCommand +struct CmdUpgradeNix : MixDryRun, EvalCommand { Path profileDir; std::string storePathsUrl = "https://github.com/NixOS/nixpkgs/raw/master/nixos/modules/installer/tools/nix-fallback-paths.nix"; @@ -59,12 +65,15 @@ struct CmdUpgradeNix : MixDryRun, StoreCommand { evalSettings.pureEval = true; - if (profileDir == "") + if (profileDir == "") { profileDir = getProfileDir(store); + } + + auto canonProfileDir = canonPath(profileDir, true); printInfo("upgrading Nix in profile '%s'", profileDir); - auto storePath = getLatestNix(store); + StorePath storePath = getLatestNix(store); auto version = DrvName(storePath.name()).version; @@ -89,11 +98,28 @@ struct CmdUpgradeNix : MixDryRun, StoreCommand stopProgressBar(); - { - Activity act(*logger, lvlInfo, actUnknown, - fmt("installing '%s' into profile '%s'...", store->printStorePath(storePath), profileDir)); - runProgram(settings.nixBinDir + "/nix-env", false, - {"--profile", profileDir, "-i", store->printStorePath(storePath), "--no-sandbox"}); + auto const fullStorePath = store->printStorePath(storePath); + + if (canonProfileDir.ends_with("user-environment")) { + + std::string nixEnvCmd = settings.nixBinDir + "/nix-env"; + Strings upgradeArgs = { + "--profile", + this->profileDir, + "--install", + fullStorePath, + "--no-sandbox", + }; + + printTalkative("running %s %s", nixEnvCmd, concatStringsSep(" ", upgradeArgs)); + runProgram(nixEnvCmd, false, upgradeArgs); + } else if (canonProfileDir.ends_with("profile")) { + this->upgradeNewStyleProfile(store, storePath); + } else { + // No I will not use std::unreachable. + // That is undefined behavior if you're wrong. + // This will have a half-decent error message and coredump. + assert("unreachable" == nullptr); } printInfo(ANSI_GREEN "upgrade to version %s done" ANSI_NORMAL, version); @@ -121,23 +147,94 @@ struct CmdUpgradeNix : MixDryRun, StoreCommand Path profileDir = dirOf(where); // Resolve profile to /nix/var/nix/profiles/ link. - while (canonPath(profileDir).find("/profiles/") == std::string::npos && isLink(profileDir)) + while (canonPath(profileDir).find("/profiles/") == std::string::npos && isLink(profileDir)) { profileDir = readLink(profileDir); + } printInfo("found profile '%s'", profileDir); Path userEnv = canonPath(profileDir, true); - if (baseNameOf(where) != "bin" || - !userEnv.ends_with("user-environment")) - throw Error("directory '%s' does not appear to be part of a Nix profile", where); + if (baseNameOf(where) != "bin") { + if (!userEnv.ends_with("user-environment") && !userEnv.ends_with("profile")) { + throw Error("directory '%s' does not appear to be part of a Nix profile", where); + } + } - if (!store->isValidPath(store->parseStorePath(userEnv))) + if (!store->isValidPath(store->parseStorePath(userEnv))) { throw Error("directory '%s' is not in the Nix store", userEnv); + } return profileDir; } + // TODO: Is there like, any good naming scheme that distinguishes + // "profiles which nix-env can use" and "profiles which nix profile can use"? + // You can't just say the manifest version since v2 and v3 are both the latter. + void upgradeNewStyleProfile(ref & store, StorePath const & newNix) + { + auto fsStore = store.dynamic_pointer_cast(); + // TODO(Qyriad): this check is here because we need to cast to a LocalFSStore, + // to pass to createGeneration(), ...but like, there's no way a remote store + // would work with the nix-env based upgrade either right? + if (!fsStore) { + throw Error("nix upgrade-nix cannot be used on a remote store"); + } + + // nb: nothing actually gets evaluated here. + // The ProfileManifest constructor only evaluates anything for manifest.nix + // profiles, which this is not. + auto evalState = this->getEvalState(); + + ProfileManifest manifest(*evalState, profileDir); + + // Find which profile element has Nix in it. + // It should be impossible to *not* have Nix, since we grabbed this + // store path by looking for things with bin/nix-env in them anyway. + auto findNix = [&](ProfileElement const & elem) -> bool { + for (auto const & ePath : elem.storePaths) { + auto const nixEnv = store->printStorePath(ePath) + "/bin/nix-env"; + if (pathExists(nixEnv)) { + return true; + } + } + // We checked each store path in this element. No nixes here boss! + return false; + }; + auto elemWithNix = std::find_if( + manifest.elements.begin(), + manifest.elements.end(), + findNix + ); + // *Should* be impossible... + assert(elemWithNix != std::end(manifest.elements)); + + // Now create a new profile element for the new Nix version... + ProfileElement elemForNewNix = { + .storePaths = {newNix}, + }; + + // ...and splork it into the manifest where the old profile element was. + // (Remember, elemWithNix is an iterator) + *elemWithNix = elemForNewNix; + + // Build the new profile, and switch to it. + StorePath const newProfile = manifest.build(store); + printTalkative("built new profile '%s'", store->printStorePath(newProfile)); + auto const newGeneration = createGeneration(*fsStore, this->profileDir, newProfile); + printTalkative( + "switching '%s' to newly created generation '%s'", + this->profileDir, + newGeneration + ); + // TODO(Qyriad): use switchGeneration? + // switchLink's docstring seems to indicate that's preferred, but it's + // not used for any other `nix profile`-style profile code except for + // rollback, and it assumes you already have a generation number, which + // we don't. + switchLink(profileDir, newGeneration); + } + /* Return the store path of the latest stable Nix. */ StorePath getLatestNix(ref store) { From ab8a6d7a835019e9316289513e0c287c261fef65 Mon Sep 17 00:00:00 2001 From: Qyriad Date: Sat, 27 Apr 2024 23:09:02 -0600 Subject: [PATCH 2/3] nix3-upgrade-nix: allow manually specifying new nix This allows manually specifying a store path for the new Nix that gets linked into Nix's profile. Change-Id: Ib71711ffb466febf4a6892e3fdbda644e053770d --- doc/manual/rl-next/upgrade-nix-override.md | 6 ++++++ src/nix/upgrade-nix.cc | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 doc/manual/rl-next/upgrade-nix-override.md diff --git a/doc/manual/rl-next/upgrade-nix-override.md b/doc/manual/rl-next/upgrade-nix-override.md new file mode 100644 index 000000000..d3046ff13 --- /dev/null +++ b/doc/manual/rl-next/upgrade-nix-override.md @@ -0,0 +1,6 @@ +--- +synopsis: add --store-path argument to `nix upgrade-nix`, to manually specify the Nix to upgrade to +cls: 953 +--- + +`nix upgrade-nix` by default downloads a manifest to find the new Nix version to upgrade to, but now you can specify `--store-path` to upgrade Nix to an arbitrary version from the Nix store. diff --git a/src/nix/upgrade-nix.cc b/src/nix/upgrade-nix.cc index fd847fa13..789d98358 100644 --- a/src/nix/upgrade-nix.cc +++ b/src/nix/upgrade-nix.cc @@ -21,6 +21,8 @@ struct CmdUpgradeNix : MixDryRun, EvalCommand Path profileDir; std::string storePathsUrl = "https://github.com/NixOS/nixpkgs/raw/master/nixos/modules/installer/tools/nix-fallback-paths.nix"; + std::optional overrideStorePath; + CmdUpgradeNix() { addFlag({ @@ -31,6 +33,13 @@ struct CmdUpgradeNix : MixDryRun, EvalCommand .handler = {&profileDir} }); + addFlag({ + .longName = "store-path", + .description = "A specific store path to upgrade Nix to", + .labels = {"store-path"}, + .handler = {&overrideStorePath}, + }); + addFlag({ .longName = "nix-store-paths-url", .description = "The URL of the file that contains the store paths of the latest Nix release.", @@ -238,6 +247,14 @@ struct CmdUpgradeNix : MixDryRun, EvalCommand /* Return the store path of the latest stable Nix. */ StorePath getLatestNix(ref store) { + if (this->overrideStorePath) { + printTalkative( + "skipping Nix version query and using '%s' as latest Nix", + *this->overrideStorePath + ); + return store->parseStorePath(*this->overrideStorePath); + } + Activity act(*logger, lvlInfo, actUnknown, "querying latest Nix version"); // FIXME: use nixos.org? From 9cdbff2237ad348752a0ce12ef75601c58cebc2d Mon Sep 17 00:00:00 2001 From: Qyriad Date: Sun, 28 Apr 2024 17:23:31 -0600 Subject: [PATCH 3/3] add VM test for nix upgrade-nix This commit adds a new NixOS VM test, which tests that `nix upgrade-nix` works on both kinds of profiles (manifest.nix and manifest.json). Done as a separate commit from 831d18a13, since it relies on the --store-path argument from 026c90e5f as well. Change-Id: I5fc94b751d252862cb6cffb541a4c072faad9f3b --- tests/nixos/default.nix | 2 + tests/nixos/nix-upgrade-nix.nix | 80 +++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 tests/nixos/nix-upgrade-nix.nix diff --git a/tests/nixos/default.nix b/tests/nixos/default.nix index 3ef1217ac..fc3a757d3 100644 --- a/tests/nixos/default.nix +++ b/tests/nixos/default.nix @@ -141,6 +141,8 @@ in nix-copy = runNixOSTestFor "x86_64-linux" ./nix-copy.nix; + nix-upgrade-nix = runNixOSTestFor "x86_64-linux" ./nix-upgrade-nix.nix; + nssPreload = runNixOSTestFor "x86_64-linux" ./nss-preload.nix; githubFlakes = runNixOSTestFor "x86_64-linux" ./github-flakes.nix; diff --git a/tests/nixos/nix-upgrade-nix.nix b/tests/nixos/nix-upgrade-nix.nix new file mode 100644 index 000000000..039b2d9b3 --- /dev/null +++ b/tests/nixos/nix-upgrade-nix.nix @@ -0,0 +1,80 @@ +{ lib, config, ... }: + +/** + * Test that nix upgrade-nix works regardless of whether /nix/var/nix/profiles/default + * is a nix-env style profile or a nix profile style profile. + */ + +let + pkgs = config.nodes.machine.nixpkgs.pkgs; + + lix = pkgs.nix; + lixVersion = lib.getVersion lix; + + newNix = pkgs.nixVersions.unstable; + newNixVersion = lib.getVersion newNix; + +in { + name = "nix-upgrade-nix"; + + nodes = { + machine = { config, lib, pkgs, ... }: { + virtualisation.writableStore = true; + virtualisation.additionalPaths = [ pkgs.hello.drvPath ]; + nix.settings.substituters = lib.mkForce [ ]; + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + services.getty.autologinUser = "root"; + + }; + }; + + testScript = { nodes }: '' + # fmt: off + + start_all() + + machine.succeed("nix --version >&2") + + # Install Lix into the default profile, overriding /run/current-system/sw/bin/nix, + # and thus making Lix think we're not on NixOS. + machine.succeed("nix-env --install '${lib.getBin lix}' --profile /nix/var/nix/profiles/default >&2") + + # Make sure that correctly got inserted into our PATH. + default_profile_nix_path = machine.succeed("command -v nix") + print(default_profile_nix_path) + assert default_profile_nix_path.strip() == "/nix/var/nix/profiles/default/bin/nix", \ + f"{default_profile_nix_path.strip()=} != /nix/var/nix/profiles/default/bin/nix" + + # And that it's the Nix we specified. + default_profile_version = machine.succeed("nix --version") + assert "${lixVersion}" in default_profile_version, f"${lixVersion} not in {default_profile_version}" + + # Upgrade to a different version of Nix, and make sure that also worked. + + machine.succeed("nix upgrade-nix --store-path ${newNix} >&2") + default_profile_version = machine.succeed("nix --version") + print(default_profile_version) + assert "${newNixVersion}" in default_profile_version, f"${newNixVersion} not in {default_profile_version}" + + # Now 'break' this profile -- use nix profile on it so nix-env will no longer work on it. + machine.succeed( + "nix profile install --profile /nix/var/nix/profiles/default '${pkgs.hello.drvPath}^*' >&2" + ) + + # Confirm that nix-env is broken. + machine.fail( + "nix-env --query --installed --profile /nix/var/nix/profiles/default >&2" + ) + + # And use nix upgrade-nix one more time, on the `nix profile` style profile. + # (Specifying Lix by full path so we can use --store-path.) + machine.succeed( + "${lib.getBin lix}/bin/nix upgrade-nix --store-path '${lix}' >&2" + ) + + default_profile_version = machine.succeed("nix --version") + print(default_profile_version) + assert "${lixVersion}" in default_profile_version, f"${lixVersion} not in {default_profile_version}" + ''; + +}