nix3-profile: make element names stable

Based off of commit 6268a45b650f563bae2360e0540920a2959bdd40

Upstream-PR: https://github.com/NixOS/nix/pull/9656
Co-authored-by: Eelco Dolstra <edolstra@gmail.com>
Change-Id: I0fcf069a8537c61ad6fc4eee1f3c193a708ea1c4
This commit is contained in:
Qyriad 2024-05-01 19:55:26 -06:00
parent ce70f02aff
commit e0911eef73
6 changed files with 102 additions and 81 deletions

View file

@ -1,7 +1,9 @@
--- ---
synopsis: "`nix profile` now allows referring to elements by human-readable name, and no longer accepts indices" synopsis: "`nix profile` now allows referring to elements by human-readable name, and no longer accepts indices"
prs: 8678 prs: 8678
cls: 978 cls: [978, 980]
--- ---
[`nix profile`](@docroot@/command-ref/new-cli/nix3-profile.md) now uses names to refer to installed packages when running [`list`](@docroot@/command-ref/new-cli/nix3-profile-list.md), [`remove`](@docroot@/command-ref/new-cli/nix3-profile-remove.md) or [`upgrade`](@docroot@/command-ref/new-cli/nix3-profile-upgrade.md) as opposed to indices. Indices have been removed. [`nix profile`](@docroot@/command-ref/new-cli/nix3-profile.md) now uses names to refer to installed packages when running [`list`](@docroot@/command-ref/new-cli/nix3-profile-list.md), [`remove`](@docroot@/command-ref/new-cli/nix3-profile-remove.md) or [`upgrade`](@docroot@/command-ref/new-cli/nix3-profile-upgrade.md) as opposed to indices. Indices have been removed. Profile element names are generated when a package is installed and remain the same until the package is removed.
**Warning**: The `manifest.nix` file used to record the contents of profiles has changed. Nix will automatically upgrade profiles to the new version when you modify the profile. After that, the profile can no longer be used by older versions of Nix.

View file

@ -6,6 +6,7 @@
#include "logging.hh" #include "logging.hh"
#include "names.hh" #include "names.hh"
#include "store-api.hh" #include "store-api.hh"
#include "url-name.hh"
namespace nix namespace nix
{ {
@ -109,8 +110,6 @@ ProfileManifest::ProfileManifest(EvalState & state, const Path & profile)
if (pathExists(manifestPath)) { if (pathExists(manifestPath)) {
auto json = nlohmann::json::parse(readFile(manifestPath)); auto json = nlohmann::json::parse(readFile(manifestPath));
// Keep track of already found names so we can prevent duplicates.
std::set<std::string> foundNames;
auto version = json.value("version", 0); auto version = json.value("version", 0);
std::string sUrl; std::string sUrl;
@ -121,6 +120,8 @@ ProfileManifest::ProfileManifest(EvalState & state, const Path & profile)
sOriginalUrl = "originalUri"; sOriginalUrl = "originalUri";
break; break;
case 2: case 2:
[[fallthrough]];
case 3:
sUrl = "url"; sUrl = "url";
sOriginalUrl = "originalUrl"; sOriginalUrl = "originalUrl";
break; break;
@ -128,7 +129,10 @@ ProfileManifest::ProfileManifest(EvalState & state, const Path & profile)
throw Error("profile manifest '%s' has unsupported version %d", manifestPath, version); throw Error("profile manifest '%s' has unsupported version %d", manifestPath, version);
} }
for (auto & e : json["elements"]) { auto elems = json["elements"];
for (auto & elem : elems.items()) {
auto & e = elem.value();
ProfileElement element; ProfileElement element;
for (auto & p : e["storePaths"]) { for (auto & p : e["storePaths"]) {
element.storePaths.insert(state.store->parseStorePath((std::string) p)); element.storePaths.insert(state.store->parseStorePath((std::string) p));
@ -145,26 +149,17 @@ ProfileManifest::ProfileManifest(EvalState & state, const Path & profile)
e["outputs"].get<ExtendedOutputsSpec>()}; e["outputs"].get<ExtendedOutputsSpec>()};
} }
std::string nameCandidate(element.identifier()); // TODO(Qyriad): holy crap this chain of ternaries needs cleanup.
std::string name =
elems.is_object()
? elem.key()
: e.contains("name")
? static_cast<std::string>(e["name"])
: element.source
? getNameFromURL(parseURL(element.source->to_string())).value_or(element.identifier())
: element.identifier();
if (e.contains("name")) { addElement(name, std::move(element));
nameCandidate = e["name"];
} else if (element.source) {
auto const url = parseURL(element.source->to_string());
auto const name = getNameFromURL(url);
if (name) {
nameCandidate = *name;
}
}
auto finalName = nameCandidate;
for (unsigned appendedIndex = 1; foundNames.contains(finalName); ++appendedIndex) {
finalName = nameCandidate + std::to_string(appendedIndex);
}
element.name = finalName;
foundNames.insert(element.name);
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. // FIXME: needed because of pure mode; ugly.
@ -176,16 +171,37 @@ ProfileManifest::ProfileManifest(EvalState & state, const Path & profile)
for (auto & drvInfo : drvInfos) { for (auto & drvInfo : drvInfos) {
ProfileElement element; ProfileElement element;
element.storePaths = {drvInfo.queryOutPath()}; element.storePaths = {drvInfo.queryOutPath()};
element.name = element.identifier(); addElement(std::move(element));
elements.emplace_back(std::move(element));
} }
} }
} }
void ProfileManifest::addElement(std::string_view nameCandidate, ProfileElement element)
{
std::string finalName(nameCandidate);
for (unsigned i = 1; elements.contains(finalName); ++i) {
finalName = nameCandidate + "-" + std::to_string(i);
}
elements.insert_or_assign(finalName, std::move(element));
}
void ProfileManifest::addElement(ProfileElement element)
{
auto name =
element.source
? getNameFromURL(parseURL(element.source->to_string()))
: std::nullopt;
auto finalName = name.value_or(element.identifier());
addElement(finalName, std::move(element));
}
nlohmann::json ProfileManifest::toJSON(Store & store) const nlohmann::json ProfileManifest::toJSON(Store & store) const
{ {
auto array = nlohmann::json::array(); auto es = nlohmann::json::object();
for (auto & element : elements) { for (auto & [name, element] : elements) {
auto paths = nlohmann::json::array(); auto paths = nlohmann::json::array();
for (auto & path : element.storePaths) { for (auto & path : element.storePaths) {
paths.push_back(store.printStorePath(path)); paths.push_back(store.printStorePath(path));
@ -200,11 +216,11 @@ nlohmann::json ProfileManifest::toJSON(Store & store) const
obj["attrPath"] = element.source->attrPath; obj["attrPath"] = element.source->attrPath;
obj["outputs"] = element.source->outputs; obj["outputs"] = element.source->outputs;
} }
array.push_back(obj); es[name] = obj;
} }
nlohmann::json json; nlohmann::json json;
json["version"] = 2; json["version"] = 3;
json["elements"] = array; json["elements"] = es;
return json; return json;
} }
@ -215,7 +231,7 @@ StorePath ProfileManifest::build(ref<Store> store)
StorePathSet references; StorePathSet references;
Packages pkgs; Packages pkgs;
for (auto & element : elements) { for (auto & [name, element] : elements) {
for (auto & path : element.storePaths) { for (auto & path : element.storePaths) {
if (element.active) { if (element.active) {
pkgs.emplace_back(store->printStorePath(path), true, element.priority); pkgs.emplace_back(store->printStorePath(path), true, element.priority);
@ -261,35 +277,29 @@ void ProfileManifest::printDiff(
const ProfileManifest & prev, const ProfileManifest & cur, std::string_view indent const ProfileManifest & prev, const ProfileManifest & cur, std::string_view indent
) )
{ {
auto prevElems = prev.elements; auto prevElemIt = prev.elements.begin();
std::sort(prevElems.begin(), prevElems.end()); auto curElemIt = cur.elements.begin();
auto curElems = cur.elements;
std::sort(curElems.begin(), curElems.end());
auto i = prevElems.begin();
auto j = curElems.begin();
bool changes = false; bool changes = false;
while (i != prevElems.end() || j != curElems.end()) { while (prevElemIt != prev.elements.end() || curElemIt != cur.elements.end()) {
if (j != curElems.end() && (i == prevElems.end() || i->identifier() > j->identifier())) { if (curElemIt != cur.elements.end() && (prevElemIt == prev.elements.end() || prevElemIt->first > curElemIt->first)) {
logger->cout("%s%s: ∅ -> %s", indent, j->identifier(), j->versions()); logger->cout("%s%s: ∅ -> %s", indent, curElemIt->second.identifier(), curElemIt->second.versions());
changes = true; changes = true;
++j; ++curElemIt;
} else if (i != prevElems.end() && (j == curElems.end() || i->identifier() < j->identifier())) { } else if (prevElemIt != prev.elements.end() && (curElemIt == cur.elements.end() || prevElemIt->first < curElemIt->first)) {
logger->cout("%s%s: %s -> ∅", indent, i->identifier(), i->versions()); logger->cout("%s%s: %s -> ∅", indent, prevElemIt->second.identifier(), prevElemIt->second.versions());
changes = true; changes = true;
++i; ++prevElemIt;
} else { } else {
auto v1 = i->versions(); auto v1 = prevElemIt->second.versions();
auto v2 = j->versions(); auto v2 = curElemIt->second.versions();
if (v1 != v2) { if (v1 != v2) {
logger->cout("%s%s: %s -> %s", indent, i->identifier(), v1, v2); logger->cout("%s%s: %s -> %s", indent, prevElemIt->second.identifier(), v1, v2);
changes = true; changes = true;
} }
++i; ++prevElemIt;
++j; ++curElemIt;
} }
} }

View file

@ -35,7 +35,6 @@ constexpr int DEFAULT_PRIORITY = 5;
struct ProfileElement struct ProfileElement
{ {
StorePathSet storePaths; StorePathSet storePaths;
std::string name;
std::optional<ProfileElementSource> source; std::optional<ProfileElementSource> source;
bool active = true; bool active = true;
int priority = DEFAULT_PRIORITY; int priority = DEFAULT_PRIORITY;
@ -57,7 +56,7 @@ struct ProfileElement
struct ProfileManifest struct ProfileManifest
{ {
std::vector<ProfileElement> elements; std::map<std::string, ProfileElement> elements;
ProfileManifest() { } ProfileManifest() { }
@ -67,6 +66,9 @@ struct ProfileManifest
StorePath build(ref<Store> store); StorePath build(ref<Store> store);
void addElement(std::string_view nameCandidate, ProfileElement element);
void addElement(ProfileElement element);
static void printDiff(const ProfileManifest & prev, const ProfileManifest & cur, std::string_view indent); static void printDiff(const ProfileManifest & prev, const ProfileManifest & cur, std::string_view indent);
}; };

View file

@ -105,7 +105,7 @@ struct CmdProfileInstall : InstallablesCommand, MixDefaultProfile
element.updateStorePaths(getEvalStore(), store, res); element.updateStorePaths(getEvalStore(), store, res);
manifest.elements.push_back(std::move(element)); manifest.addElement(std::move(element));
} }
try { try {
@ -115,7 +115,7 @@ struct CmdProfileInstall : InstallablesCommand, MixDefaultProfile
// See https://github.com/NixOS/nix/compare/3efa476c5439f8f6c1968a6ba20a31d1239c2f04..1fe5d172ece51a619e879c4b86f603d9495cc102 // See https://github.com/NixOS/nix/compare/3efa476c5439f8f6c1968a6ba20a31d1239c2f04..1fe5d172ece51a619e879c4b86f603d9495cc102
auto findRefByFilePath = [&]<typename Iterator>(Iterator begin, Iterator end) { auto findRefByFilePath = [&]<typename Iterator>(Iterator begin, Iterator end) {
for (auto it = begin; it != end; it++) { for (auto it = begin; it != end; it++) {
auto profileElement = *it; auto & profileElement = it->second;
for (auto & storePath : profileElement.storePaths) { for (auto & storePath : profileElement.storePaths) {
if (conflictError.fileA.starts_with(store->printStorePath(storePath))) { if (conflictError.fileA.starts_with(store->printStorePath(storePath))) {
return std::pair(conflictError.fileA, profileElement.toInstallables(*store)); return std::pair(conflictError.fileA, profileElement.toInstallables(*store));
@ -202,13 +202,19 @@ public:
return res; return res;
} }
bool matches(const Store & store, const ProfileElement & element, const std::vector<Matcher> & matchers) bool matches(
Store const & store,
// regex_match doesn't take a string_view lol
std::string const & name,
ProfileElement const & element,
std::vector<Matcher> const & matchers
)
{ {
for (auto & matcher : matchers) { for (auto & matcher : matchers) {
if (auto path = std::get_if<Path>(&matcher)) { if (auto path = std::get_if<Path>(&matcher)) {
if (element.storePaths.count(store.parseStorePath(*path))) return true; if (element.storePaths.count(store.parseStorePath(*path))) return true;
} else if (auto regex = std::get_if<RegexPattern>(&matcher)) { } else if (auto regex = std::get_if<RegexPattern>(&matcher)) {
if (std::regex_match(element.name, regex->reg)) { if (std::regex_match(name, regex->reg)) {
return true; return true;
} }
} }
@ -240,10 +246,9 @@ struct CmdProfileRemove : virtual EvalCommand, MixDefaultProfile, MixProfileElem
ProfileManifest newManifest; ProfileManifest newManifest;
for (size_t i = 0; i < oldManifest.elements.size(); ++i) { for (auto & [name, element] : oldManifest.elements) {
auto & element(oldManifest.elements[i]); if (!matches(*store, name, element, matchers)) {
if (!matches(*store, element, matchers)) { newManifest.elements.insert_or_assign(name, std::move(element));
newManifest.elements.push_back(std::move(element));
} else { } else {
notice("removing '%s'", element.identifier()); notice("removing '%s'", element.identifier());
} }
@ -289,14 +294,13 @@ struct CmdProfileUpgrade : virtual SourceExprCommand, MixDefaultProfile, MixProf
auto matchers = getMatchers(store); auto matchers = getMatchers(store);
Installables installables; Installables installables;
std::vector<size_t> indices; std::vector<ProfileElement *> elems;
auto matchedCount = 0; auto matchedCount = 0;
auto upgradedCount = 0; auto upgradedCount = 0;
for (size_t i = 0; i < manifest.elements.size(); ++i) { for (auto & [name, element] : manifest.elements) {
auto & element(manifest.elements[i]); if (!matches(*store, name, element, matchers)) {
if (!matches(*store, element, matchers)) {
continue; continue;
} }
@ -368,7 +372,7 @@ struct CmdProfileUpgrade : virtual SourceExprCommand, MixDefaultProfile, MixProf
}; };
installables.push_back(installable); installables.push_back(installable);
indices.push_back(i); elems.push_back(&element);
} }
@ -393,7 +397,7 @@ struct CmdProfileUpgrade : virtual SourceExprCommand, MixDefaultProfile, MixProf
for (size_t i = 0; i < installables.size(); ++i) { for (size_t i = 0; i < installables.size(); ++i) {
auto & installable = installables.at(i); auto & installable = installables.at(i);
auto & element = manifest.elements[indices.at(i)]; auto & element = *elems.at(i);
element.updateStorePaths( element.updateStorePaths(
getEvalStore(), getEvalStore(),
store, store,
@ -425,14 +429,14 @@ struct CmdProfileList : virtual EvalCommand, virtual StoreCommand, MixDefaultPro
if (json) { if (json) {
std::cout << manifest.toJSON(*store).dump() << "\n"; std::cout << manifest.toJSON(*store).dump() << "\n";
} else { } else {
for (size_t i = 0; i < manifest.elements.size(); ++i) { for (auto const & [i, nameElemPair] : enumerate(manifest.elements)) {
auto & element(manifest.elements[i]); auto & [name, element] = nameElemPair;
if (i) { if (i) {
logger->cout(""); logger->cout("");
} }
logger->cout( logger->cout(
"Name: " ANSI_BOLD "%s" ANSI_NORMAL "%s", "Name: " ANSI_BOLD "%s" ANSI_NORMAL "%s",
element.name, name,
element.active ? "" : " " ANSI_RED "(inactive)" ANSI_NORMAL element.active ? "" : " " ANSI_RED "(inactive)" ANSI_NORMAL
); );
if (element.source) { if (element.source) {

View file

@ -208,7 +208,8 @@ struct CmdUpgradeNix : MixDryRun, EvalCommand
// Find which profile element has Nix in it. // Find which profile element has Nix in it.
// It should be impossible to *not* have Nix, since we grabbed this // 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. // store path by looking for things with bin/nix-env in them anyway.
auto findNix = [&](ProfileElement const & elem) -> bool { auto findNix = [&](std::pair<std::string, ProfileElement> const & nameElemPair) -> bool {
auto const & [name, elem] = nameElemPair;
for (auto const & ePath : elem.storePaths) { for (auto const & ePath : elem.storePaths) {
auto const nixEnv = store->printStorePath(ePath) + "/bin/nix-env"; auto const nixEnv = store->printStorePath(ePath) + "/bin/nix-env";
if (pathExists(nixEnv)) { if (pathExists(nixEnv)) {
@ -226,14 +227,15 @@ struct CmdUpgradeNix : MixDryRun, EvalCommand
// *Should* be impossible... // *Should* be impossible...
assert(elemWithNix != std::end(manifest.elements)); assert(elemWithNix != std::end(manifest.elements));
auto const nixElemName = elemWithNix->first;
// Now create a new profile element for the new Nix version... // Now create a new profile element for the new Nix version...
ProfileElement elemForNewNix = { ProfileElement elemForNewNix = {
.storePaths = {newNix}, .storePaths = {newNix},
}; };
// ...and splork it into the manifest where the old profile element was. // ...and splork it into the manifest where the old profile element was.
// (Remember, elemWithNix is an iterator) manifest.elements.at(nixElemName) = elemForNewNix;
*elemWithNix = elemForNewNix;
// Build the new profile, and switch to it. // Build the new profile, and switch to it.
StorePath const newProfile = manifest.build(store); StorePath const newProfile = manifest.build(store);

View file

@ -60,7 +60,7 @@ nix profile diff-closures | grep 'env-manifest.nix: ε → ∅'
# Test XDG Base Directories support # Test XDG Base Directories support
export NIX_CONFIG="use-xdg-base-directories = true" export NIX_CONFIG="use-xdg-base-directories = true"
nix profile remove flake1 nix profile remove flake1 2>&1 | grep 'removed 1 packages'
nix profile install $flake1Dir nix profile install $flake1Dir
[[ $($TEST_HOME/.local/state/nix/profile/bin/hello) = "Hello World" ]] [[ $($TEST_HOME/.local/state/nix/profile/bin/hello) = "Hello World" ]]
unset NIX_CONFIG unset NIX_CONFIG
@ -81,7 +81,7 @@ nix profile rollback
# Test uninstall. # Test uninstall.
[ -e $TEST_HOME/.nix-profile/bin/foo ] [ -e $TEST_HOME/.nix-profile/bin/foo ]
nix profile remove "foo" nix profile remove "foo" 2>&1 | grep 'removed 1 packages'
(! [ -e $TEST_HOME/.nix-profile/bin/foo ]) (! [ -e $TEST_HOME/.nix-profile/bin/foo ])
nix profile history | grep 'foo: 1.0 -> ∅' nix profile history | grep 'foo: 1.0 -> ∅'
nix profile diff-closures | grep 'Version 3 -> 4' nix profile diff-closures | grep 'Version 3 -> 4'
@ -89,7 +89,7 @@ nix profile diff-closures | grep 'Version 3 -> 4'
# Test installing a non-flake package. # Test installing a non-flake package.
nix profile install --file ./simple.nix '' nix profile install --file ./simple.nix ''
[[ $(cat $TEST_HOME/.nix-profile/hello) = "Hello World!" ]] [[ $(cat $TEST_HOME/.nix-profile/hello) = "Hello World!" ]]
nix profile remove simple nix profile remove simple 2>&1 | grep 'removed 1 packages'
nix profile install $(nix-build --no-out-link ./simple.nix) nix profile install $(nix-build --no-out-link ./simple.nix)
[[ $(cat $TEST_HOME/.nix-profile/hello) = "Hello World!" ]] [[ $(cat $TEST_HOME/.nix-profile/hello) = "Hello World!" ]]
@ -97,8 +97,9 @@ nix profile install $(nix-build --no-out-link ./simple.nix)
mkdir $TEST_ROOT/simple-too mkdir $TEST_ROOT/simple-too
cp ./simple.nix ./config.nix simple.builder.sh $TEST_ROOT/simple-too cp ./simple.nix ./config.nix simple.builder.sh $TEST_ROOT/simple-too
nix profile install --file $TEST_ROOT/simple-too/simple.nix '' nix profile install --file $TEST_ROOT/simple-too/simple.nix ''
nix profile list | grep -A4 'Name:.*simple' | grep 'Name:.*simple1' nix profile list | grep -A4 'Name:.*simple' | grep 'Name:.*simple-1'
nix profile remove simple1 nix profile remove simple 2>&1 | grep 'removed 1 packages'
nix profile remove simple-1 2>&1 | grep 'removed 1 packages'
# Test wipe-history. # Test wipe-history.
nix profile wipe-history nix profile wipe-history
@ -111,7 +112,7 @@ nix profile upgrade flake1
nix profile history | grep "packages.$system.default: 1.0, 1.0-man -> 3.0, 3.0-man" nix profile history | grep "packages.$system.default: 1.0, 1.0-man -> 3.0, 3.0-man"
# Test new install of CA package. # Test new install of CA package.
nix profile remove flake1 nix profile remove flake1 2>&1 | grep 'removed 1 packages'
printf 4.0 > $flake1Dir/version printf 4.0 > $flake1Dir/version
printf Utrecht > $flake1Dir/who printf Utrecht > $flake1Dir/who
nix profile install $flake1Dir nix profile install $flake1Dir
@ -132,7 +133,7 @@ nix profile upgrade flake1
[ -e $TEST_HOME/.nix-profile/share/man ] [ -e $TEST_HOME/.nix-profile/share/man ]
[ -e $TEST_HOME/.nix-profile/include ] [ -e $TEST_HOME/.nix-profile/include ]
nix profile remove flake1 nix profile remove flake1 2>&1 | grep 'removed 1 packages'
nix profile install "$flake1Dir^man" nix profile install "$flake1Dir^man"
(! [ -e $TEST_HOME/.nix-profile/bin/hello ]) (! [ -e $TEST_HOME/.nix-profile/bin/hello ])
[ -e $TEST_HOME/.nix-profile/share/man ] [ -e $TEST_HOME/.nix-profile/share/man ]