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..4940d93f8 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,31 @@ 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 (pathExists(canonProfileDir + "/manifest.nix")) { + + 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 (pathExists(canonProfileDir + "/manifest.json")) { + this->upgradeNewStyleProfile(store, storePath); + } else { + // No I will not use std::unreachable. + // That is undefined behavior if you're wrong. + // This will have a better error message and coredump. + assert( + false && "tried to upgrade unexpected kind of profile, " + "we can only handle `user-environment` and `profile`" + ); } printInfo(ANSI_GREEN "upgrade to version %s done" ANSI_NORMAL, version); @@ -121,23 +150,99 @@ 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") { + throw Error("directory '%s' does not appear to be part of a Nix profile (no /bin dir?)", where); + } - if (!store->isValidPath(store->parseStorePath(userEnv))) + if (!pathExists(userEnv + "/manifest.nix") && !pathExists(userEnv + "/manifest.json")) { + throw Error( + "directory '%s' does not have a compatible profile manifest; was it created by Nix?", + where + ); + } + + 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) {