Files
lix/lix/libcmd/cmd-profiles.cc
T
Raito Bezarius e8015836e3 libcmd/profiles: keep derivations if keep-env-derivations = true
nix3 CLI never honored that setting.

When we update a nix3 profile element with new built store paths for
which we know the deriver (the derivation path which built it), we add
the derivation paths to the profile element to prevent garbage
collection the same way the nix2 CLI logic does.

Fixes #1095.

Change-Id: Icc236d174c5ce5ffe042d1c8e1968f6cb5b50359
Signed-off-by: Raito Bezarius <raito@lix.systems>
2026-03-03 22:13:02 +00:00

316 lines
9.7 KiB
C++

#include <set>
#include "lix/libcmd/cmd-profiles.hh"
#include "lix/libcmd/built-path.hh"
#include "lix/libstore/builtins/buildenv.hh"
#include "lix/libutil/async-io.hh"
#include "lix/libutil/logging.hh"
#include "lix/libstore/names.hh"
#include "lix/libstore/store-api.hh"
#include "lix/libstore/temporary-dir.hh"
#include "lix/libutil/url-name.hh"
namespace nix
{
DrvInfos queryInstalled(EvalState & state, const Path & userEnv)
{
DrvInfos elems;
if (pathExists(userEnv + "/manifest.json"))
throw Error("profile '%s' is incompatible with 'nix-env'; please use 'nix profile' instead", userEnv);
auto manifestFile = userEnv + "/manifest.nix";
if (pathExists(manifestFile)) {
Value v = state.evalFile(CanonPath(manifestFile));
Bindings & bindings(*state.ctx.mem.allocBindings(0));
getDerivations(state, v, "", bindings, elems, false);
}
return elems;
}
std::string showVersions(const std::set<std::string> & versions)
{
if (versions.empty()) return "∅";
std::set<std::string> versions2;
for (auto & version : versions)
versions2.insert(version.empty() ? "ε" : version);
return concatStringsSep(", ", versions2);
}
bool ProfileElementSource::operator<(const ProfileElementSource & other) const
{
return std::tuple(originalRef.to_string(), attrPath, outputs)
< std::tuple(other.originalRef.to_string(), other.attrPath, other.outputs);
}
std::string ProfileElementSource::to_string() const
{
return fmt("%s#%s%s", originalRef, attrPath, outputs.to_string());
}
std::string ProfileElement::identifier() const
{
if (source) {
return source->to_string();
}
StringSet names;
for (auto & path : storePaths) {
names.insert(DrvName(path.name()).name);
}
return concatStringsSep(", ", names);
}
std::set<std::string> ProfileElement::toInstallables(Store & store)
{
if (source) {
return {source->to_string()};
}
StringSet rawPaths;
for (auto & path : storePaths) {
rawPaths.insert(store.printStorePath(path));
}
return rawPaths;
}
std::string ProfileElement::versions() const
{
StringSet versions;
for (auto & path : storePaths) {
versions.insert(DrvName(path.name()).version);
}
return showVersions(versions);
}
bool ProfileElement::operator<(const ProfileElement & other) const
{
return std::tuple(identifier(), storePaths) < std::tuple(other.identifier(), other.storePaths);
}
void ProfileElement::updateStorePaths(
ref<Store> evalStore, ref<Store> store, const BuiltPaths & builtPaths
)
{
storePaths.clear();
for (auto & buildable : builtPaths) {
std::visit(
overloaded{
[&](const BuiltPath::Opaque & bo) { storePaths.insert(bo.path); },
[&](const BuiltPath::Built & bfd) {
for (auto & output : bfd.outputs) {
storePaths.insert(output.second);
}
if (settings.envKeepDerivations) {
storePaths.insert(bfd.drvPath.path);
}
},
},
buildable.raw()
);
}
}
ProfileManifest::ProfileManifest(EvalState & state, const Path & profile)
{
auto manifestPath = profile + "/manifest.json";
if (pathExists(manifestPath)) {
auto json = json::parse(readFile(manifestPath), "a profile manifest");
auto version = json.value("version", 0);
std::string sUrl;
std::string sOriginalUrl;
switch (version) {
case 1:
sUrl = "uri";
sOriginalUrl = "originalUri";
break;
case 2:
[[fallthrough]];
case 3:
sUrl = "url";
sOriginalUrl = "originalUrl";
break;
default:
throw Error("profile manifest '%s' has unsupported version %d", manifestPath, version);
}
auto elems = json["elements"];
for (auto & elem : elems.items()) {
auto & e = elem.value();
ProfileElement element;
for (auto & p : e["storePaths"]) {
element.storePaths.insert(state.ctx.store->parseStorePath((std::string) p));
}
element.active = e["active"];
if (e.contains("priority")) {
element.priority = e["priority"];
}
if (e.value(sUrl, "") != "") {
element.source = ProfileElementSource{
parseFlakeRef(e[sOriginalUrl]),
parseFlakeRef(e[sUrl]),
e["attrPath"],
e["outputs"].get<ExtendedOutputsSpec>()};
}
// TODO(Qyriad): holy crap this chain of ternaries needs cleanup.
std::string name =
elems.is_object()
? elem.key()
: element.source
? getNameFromURL(parseURL(element.source->to_string())).value_or(element.identifier())
: element.identifier();
addElement(name, std::move(element));
}
} else if (pathExists(profile + "/manifest.nix")) {
// FIXME: needed because of pure mode; ugly.
state.ctx.paths.allowPath(state.ctx.store->followLinksToStore(profile));
state.ctx.paths.allowPath(state.ctx.store->followLinksToStore(profile + "/manifest.nix"));
auto drvInfos = queryInstalled(state, state.ctx.store->followLinksToStore(profile));
for (auto & drvInfo : drvInfos) {
ProfileElement element;
element.storePaths = {drvInfo.queryOutPath(state)};
addElement(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));
}
JSON ProfileManifest::toJSON(Store & store) const
{
auto es = JSON::object();
for (auto & [name, element] : elements) {
auto paths = JSON::array();
for (auto & path : element.storePaths) {
paths.push_back(store.printStorePath(path));
}
JSON obj;
obj["storePaths"] = paths;
obj["active"] = element.active;
obj["priority"] = element.priority;
if (element.source) {
obj["originalUrl"] = element.source->originalRef.to_string();
obj["url"] = element.source->lockedRef.to_string();
obj["attrPath"] = element.source->attrPath;
obj["outputs"] = element.source->outputs;
}
es[name] = obj;
}
JSON json;
json["version"] = 3;
json["elements"] = es;
return json;
}
kj::Promise<Result<StorePath>> ProfileManifest::build(ref<Store> store)
try {
auto tempDir = createTempDir();
StorePathSet references;
Packages pkgs;
for (auto & [name, element] : elements) {
for (auto & path : element.storePaths) {
if (element.active) {
pkgs.emplace_back(store->printStorePath(path), true, element.priority);
}
references.insert(path);
}
}
buildProfile(tempDir, std::move(pkgs));
writeFile(tempDir + "/manifest.json", toJSON(*store).dump());
/* Add the symlink tree to the store. */
StringSink sink;
sink << dumpPath(tempDir);
auto narHash = hashString(HashType::SHA256, sink.s);
ValidPathInfo info{
*store,
"profile",
FixedOutputInfo{
.method = FileIngestionMethod::Recursive,
.hash = narHash,
.references =
{
.others = std::move(references),
// profiles never refer to themselves
.self = false,
},
},
narHash,
};
info.narSize = sink.s.size();
AsyncStringInputStream source(sink.s);
TRY_AWAIT(store->addToStore(info, source));
co_return std::move(info.path);
} catch (...) {
co_return result::current_exception();
}
void ProfileManifest::printDiff(
const ProfileManifest & prev, const ProfileManifest & cur, std::string_view indent
)
{
auto prevElemIt = prev.elements.begin();
auto curElemIt = cur.elements.begin();
bool changes = false;
while (prevElemIt != prev.elements.end() || curElemIt != cur.elements.end()) {
if (curElemIt != cur.elements.end() && (prevElemIt == prev.elements.end() || prevElemIt->first > curElemIt->first)) {
logger->cout("%s%s: ∅ -> %s", indent, curElemIt->second.identifier(), curElemIt->second.versions());
changes = true;
++curElemIt;
} else if (prevElemIt != prev.elements.end() && (curElemIt == cur.elements.end() || prevElemIt->first < curElemIt->first)) {
logger->cout("%s%s: %s -> ∅", indent, prevElemIt->second.identifier(), prevElemIt->second.versions());
changes = true;
++prevElemIt;
} else {
auto v1 = prevElemIt->second.versions();
auto v2 = curElemIt->second.versions();
if (v1 != v2) {
logger->cout("%s%s: %s -> %s", indent, prevElemIt->second.identifier(), v1, v2);
changes = true;
}
++prevElemIt;
++curElemIt;
}
}
if (!changes) {
logger->cout("%sNo changes.", indent);
}
}
}