Skip to content

Commit

Permalink
nix profile: Make profile element names stable
Browse files Browse the repository at this point in the history
The profile manifest is now an object keyed on the name returned by
getNameFromURL() at installation time, instead of an array. This
ensures that the names of profile elements don't change when other
elements are added/removed.
  • Loading branch information
edolstra committed Dec 22, 2023
1 parent 3187bc9 commit 6268a45
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 77 deletions.
140 changes: 71 additions & 69 deletions src/nix/profile.cc
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ const int defaultPriority = 5;
struct ProfileElement
{
StorePathSet storePaths;
std::string name;
std::optional<ProfileElementSource> source;
bool active = true;
int priority = defaultPriority;
Expand Down Expand Up @@ -82,11 +81,6 @@ struct ProfileElement
return showVersions(versions);
}

bool operator < (const ProfileElement & other) const
{
return std::tuple(identifier(), storePaths) < std::tuple(other.identifier(), other.storePaths);
}

void updateStorePaths(
ref<Store> evalStore,
ref<Store> store,
Expand All @@ -109,7 +103,9 @@ struct ProfileElement

struct ProfileManifest
{
std::vector<ProfileElement> elements;
using ProfileElementName = std::string;

std::map<ProfileElementName, ProfileElement> elements;

ProfileManifest() { }

Expand All @@ -119,8 +115,6 @@ struct ProfileManifest

if (pathExists(manifestPath)) {
auto json = nlohmann::json::parse(readFile(manifestPath));
/* Keep track of already found names to allow preventing duplicates. */
std::set<std::string> foundNames;

auto version = json.value("version", 0);
std::string sUrl;
Expand All @@ -131,14 +125,17 @@ struct ProfileManifest
sOriginalUrl = "originalUri";
break;
case 2:
case 3:
sUrl = "url";
sOriginalUrl = "originalUrl";
break;
default:
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;
for (auto & p : e["storePaths"])
element.storePaths.insert(state.store->parseStorePath((std::string) p));
Expand All @@ -155,25 +152,16 @@ struct ProfileManifest
};
}

std::string nameCandidate = element.identifier();
if (e.contains("name")) {
nameCandidate = e["name"];
}
else if (element.source) {
auto url = parseURL(element.source->to_string());
auto name = getNameFromURL(url);
if (name)
nameCandidate = *name;
}

auto finalName = nameCandidate;
for (int i = 1; foundNames.contains(finalName); ++i) {
finalName = nameCandidate + std::to_string(i);
}
element.name = finalName;
foundNames.insert(element.name);
std::string name =
elems.is_object()
? elem.key()
: e.contains("name")
? (std::string) e["name"]
: element.source
? getNameFromURL(parseURL(element.source->to_string())).value_or(element.identifier())
: element.identifier();

elements.emplace_back(std::move(element));
addElement(name, std::move(element));
}
}

Expand All @@ -187,16 +175,34 @@ struct ProfileManifest
for (auto & drvInfo : drvInfos) {
ProfileElement element;
element.storePaths = {drvInfo.queryOutPath()};
element.name = element.identifier();
elements.emplace_back(std::move(element));
addElement(std::move(element));
}
}
}

void addElement(std::string_view nameCandidate, ProfileElement element)
{
std::string finalName(nameCandidate);
for (int i = 1; elements.contains(finalName); ++i)
finalName = nameCandidate + "-" + std::to_string(i);

elements.insert_or_assign(finalName, std::move(element));
}

void addElement(ProfileElement element)
{
auto name =
element.source
? getNameFromURL(parseURL(element.source->to_string()))
: std::nullopt;
auto name2 = name ? *name : element.identifier();
addElement(name2, std::move(element));
}

nlohmann::json toJSON(Store & store) const
{
auto array = nlohmann::json::array();
for (auto & element : elements) {
auto es = nlohmann::json::object();
for (auto & [name, element] : elements) {
auto paths = nlohmann::json::array();
for (auto & path : element.storePaths)
paths.push_back(store.printStorePath(path));
Expand All @@ -210,11 +216,11 @@ struct ProfileManifest
obj["attrPath"] = element.source->attrPath;
obj["outputs"] = element.source->outputs;
}
array.push_back(obj);
es[name] = obj;
}
nlohmann::json json;
json["version"] = 2;
json["elements"] = array;
json["version"] = 3;
json["elements"] = es;
return json;
}

Expand All @@ -225,7 +231,7 @@ struct ProfileManifest
StorePathSet references;

Packages pkgs;
for (auto & element : elements) {
for (auto & [name, element] : elements) {
for (auto & path : element.storePaths) {
if (element.active)
pkgs.emplace_back(store->printStorePath(path), true, element.priority);
Expand Down Expand Up @@ -267,33 +273,27 @@ struct ProfileManifest

static void printDiff(const ProfileManifest & prev, const ProfileManifest & cur, std::string_view indent)
{
auto prevElems = prev.elements;
std::sort(prevElems.begin(), prevElems.end());

auto curElems = cur.elements;
std::sort(curElems.begin(), curElems.end());

auto i = prevElems.begin();
auto j = curElems.begin();
auto i = prev.elements.begin();
auto j = cur.elements.begin();

bool changes = false;

while (i != prevElems.end() || j != curElems.end()) {
if (j != curElems.end() && (i == prevElems.end() || i->identifier() > j->identifier())) {
logger->cout("%s%s: ∅ -> %s", indent, j->identifier(), j->versions());
while (i != prev.elements.end() || j != cur.elements.end()) {
if (j != cur.elements.end() && (i == prev.elements.end() || i->first > j->first)) {
logger->cout("%s%s: ∅ -> %s", indent, j->second.identifier(), j->second.versions());
changes = true;
++j;
}
else if (i != prevElems.end() && (j == curElems.end() || i->identifier() < j->identifier())) {
logger->cout("%s%s: %s -> ∅", indent, i->identifier(), i->versions());
else if (i != prev.elements.end() && (j == cur.elements.end() || i->first < j->first)) {
logger->cout("%s%s: %s -> ∅", indent, i->second.identifier(), i->second.versions());
changes = true;
++i;
}
else {
auto v1 = i->versions();
auto v2 = j->versions();
auto v1 = i->second.versions();
auto v2 = j->second.versions();
if (v1 != v2) {
logger->cout("%s%s: %s -> %s", indent, i->identifier(), v1, v2);
logger->cout("%s%s: %s -> %s", indent, i->second.identifier(), v1, v2);
changes = true;
}
++i;
Expand Down Expand Up @@ -392,7 +392,7 @@ struct CmdProfileInstall : InstallablesCommand, MixDefaultProfile

element.updateStorePaths(getEvalStore(), store, res);

manifest.elements.push_back(std::move(element));
manifest.addElement(std::move(element));
}

try {
Expand All @@ -402,7 +402,7 @@ struct CmdProfileInstall : InstallablesCommand, MixDefaultProfile
// See https://github.com/NixOS/nix/compare/3efa476c5439f8f6c1968a6ba20a31d1239c2f04..1fe5d172ece51a619e879c4b86f603d9495cc102
auto findRefByFilePath = [&]<typename Iterator>(Iterator begin, Iterator end) {
for (auto it = begin; it != end; it++) {
auto profileElement = *it;
auto & profileElement = it->second;
for (auto & storePath : profileElement.storePaths) {
if (conflictError.fileA.starts_with(store->printStorePath(storePath))) {
return std::pair(conflictError.fileA, profileElement.toInstallables(*store));
Expand Down Expand Up @@ -488,13 +488,17 @@ class MixProfileElementMatchers : virtual Args
return res;
}

bool matches(const Store & store, const ProfileElement & element, const std::vector<Matcher> & matchers)
bool matches(
const Store & store,
const std::string & name,
const ProfileElement & element,
const std::vector<Matcher> & matchers)
{
for (auto & matcher : matchers) {
if (auto path = std::get_if<Path>(&matcher)) {
if (element.storePaths.count(store.parseStorePath(*path))) return true;
} 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;
}
}
Expand Down Expand Up @@ -525,10 +529,9 @@ struct CmdProfileRemove : virtual EvalCommand, MixDefaultProfile, MixProfileElem

ProfileManifest newManifest;

for (size_t i = 0; i < oldManifest.elements.size(); ++i) {
auto & element(oldManifest.elements[i]);
if (!matches(*store, element, matchers)) {
newManifest.elements.push_back(std::move(element));
for (auto & [name, element] : oldManifest.elements) {
if (!matches(*store, name, element, matchers)) {
newManifest.elements.insert_or_assign(name, std::move(element));
} else {
notice("removing '%s'", element.identifier());
}
Expand Down Expand Up @@ -574,14 +577,13 @@ struct CmdProfileUpgrade : virtual SourceExprCommand, MixDefaultProfile, MixProf
auto matchers = getMatchers(store);

Installables installables;
std::vector<size_t> indices;
std::vector<ProfileElement *> elems;

auto matchedCount = 0;
auto upgradedCount = 0;

for (size_t i = 0; i < manifest.elements.size(); ++i) {
auto & element(manifest.elements[i]);
if (!matches(*store, element, matchers)) {
for (auto & [name, element] : manifest.elements) {
if (!matches(*store, name, element, matchers)) {
continue;
}

Expand Down Expand Up @@ -637,7 +639,7 @@ struct CmdProfileUpgrade : virtual SourceExprCommand, MixDefaultProfile, MixProf
};

installables.push_back(installable);
indices.push_back(i);
elems.push_back(&element);
}

if (upgradedCount == 0) {
Expand All @@ -661,7 +663,7 @@ struct CmdProfileUpgrade : virtual SourceExprCommand, MixDefaultProfile, MixProf

for (size_t i = 0; i < installables.size(); ++i) {
auto & installable = installables.at(i);
auto & element = manifest.elements[indices.at(i)];
auto & element = *elems.at(i);
element.updateStorePaths(
getEvalStore(),
store,
Expand Down Expand Up @@ -693,11 +695,11 @@ struct CmdProfileList : virtual EvalCommand, virtual StoreCommand, MixDefaultPro
if (json) {
std::cout << manifest.toJSON(*store).dump() << "\n";
} else {
for (size_t i = 0; i < manifest.elements.size(); ++i) {
auto & element(manifest.elements[i]);
for (const auto & [i, e] : enumerate(manifest.elements)) {
auto & [name, element] = e;
if (i) logger->cout("");
logger->cout("Name: " ANSI_BOLD "%s" ANSI_NORMAL "%s",
element.name,
name,
element.active ? "" : " " ANSI_RED "(inactive)" ANSI_NORMAL);
if (element.source) {
logger->cout("Flake attribute: %s%s", element.source->attrPath, element.source->outputs.to_string());
Expand Down
17 changes: 9 additions & 8 deletions tests/functional/nix-profile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ nix profile diff-closures | grep 'env-manifest.nix: ε → ∅'

# Test XDG Base Directories support
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
[[ $($TEST_HOME/.local/state/nix/profile/bin/hello) = "Hello World" ]]
unset NIX_CONFIG
Expand All @@ -80,24 +80,25 @@ nix profile rollback

# Test uninstall.
[ -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 ])
nix profile history | grep 'foo: 1.0 -> ∅'
nix profile diff-closures | grep 'Version 3 -> 4'

# Test installing a non-flake package.
nix profile install --file ./simple.nix ''
[[ $(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)
[[ $(cat $TEST_HOME/.nix-profile/hello) = "Hello World!" ]]

# Test packages with same name from different sources
mkdir $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 list | grep -A4 'Name:.*simple' | grep 'Name:.*simple1'
nix profile remove simple1
nix profile list | grep -A4 'Name:.*simple' | grep 'Name:.*simple-1'
nix profile remove simple 2>&1 | grep 'removed 1 packages'
nix profile remove simple-1 2>&1 | grep 'removed 1 packages'

# Test wipe-history.
nix profile wipe-history
Expand All @@ -110,7 +111,7 @@ nix profile upgrade flake1
nix profile history | grep "packages.$system.default: 1.0, 1.0-man -> 3.0, 3.0-man"

# 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 Utrecht > $flake1Dir/who
nix profile install $flake1Dir
Expand All @@ -131,14 +132,14 @@ nix profile upgrade flake1
[ -e $TEST_HOME/.nix-profile/share/man ]
[ -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"
(! [ -e $TEST_HOME/.nix-profile/bin/hello ])
[ -e $TEST_HOME/.nix-profile/share/man ]
(! [ -e $TEST_HOME/.nix-profile/include ])

# test priority
nix profile remove flake1
nix profile remove flake1 2>&1 | grep 'removed 1 packages'

# Make another flake.
flake2Dir=$TEST_ROOT/flake2
Expand Down

0 comments on commit 6268a45

Please sign in to comment.