Files
lix/lix/libstore/path-tree.cc
T
Maximilian Bosch da0df360e1 libstore: print dependency in tree boldly
That way it's easier to spot whether a node is the "final" node in the
graph which is especially helpful for larger graphs.

Change-Id: I460a699f07f5455917792599f4247ebf8f430d93
2025-07-07 11:44:17 +00:00

318 lines
8.9 KiB
C++

#include "path-tree.hh"
#include "fs-accessor.hh"
#include "lix/libutil/ansicolor.hh"
#include "lix/libutil/async.hh"
#include "lix/libutil/result.hh"
#include "lix/libutil/strings.hh"
#include "lix/libutil/fmt.hh"
#include <boost/algorithm/string/join.hpp>
#include <queue>
#define ANSI_DIM_ALREADY_VISITED "\e[38;5;244m"
namespace nix {
static std::string hilite(const std::string & s, size_t pos, size_t len,
const std::string & colour = ANSI_RED)
{
return
std::string(s, 0, pos)
+ colour
+ std::string(s, pos, len)
+ ANSI_NORMAL
+ std::string(s, pos + len);
}
static std::string filterPrintable(const std::string & s)
{
std::string res;
for (char c : s)
res += isprint(c) ? c : '.';
return res;
}
static kj::Promise<Result<std::map<std::string, Strings>>> visitPath(
const Path & p,
Store & store,
const Path & pathS,
std::string_view dependencyPathHash,
const std::set<std::string> & hashes,
const StorePath & from,
const StorePath & to
)
try {
/* For each reference, find the files and symlinks that
contain the reference. */
std::map<std::string, Strings> hits;
auto accessor = store.getFSAccessor();
auto st = TRY_AWAIT(accessor->stat(p));
auto p2 = p == pathS ? "/" : std::string(p, pathS.size() + 1);
auto getColour = [&](const std::string & hash) {
return hash == dependencyPathHash ? ANSI_GREEN : ANSI_BLUE;
};
if (st.type == FSAccessor::Type::tDirectory) {
auto names = TRY_AWAIT(accessor->readDirectory(p));
for (auto & name : names) {
auto found = TRY_AWAIT(
visitPath(p + "/" + name, store, pathS, dependencyPathHash, hashes, from, to)
);
hits.merge(found);
}
} else if (st.type == FSAccessor::Type::tRegular) {
auto contents = TRY_AWAIT(accessor->readFile(p));
for (auto & hash : hashes) {
auto pos = contents.find(hash);
if (pos != std::string::npos) {
size_t margin = 32;
auto pos2 = pos >= margin ? pos - margin : 0;
hits[hash].emplace_back(
fmt("%s: …%s…",
p2,
hilite(
filterPrintable(
std::string(contents, pos2, pos - pos2 + hash.size() + margin)
),
pos - pos2,
StorePath::HashLen,
getColour(hash)
))
);
}
}
} else if (st.type == FSAccessor::Type::tSymlink) {
auto target = TRY_AWAIT(accessor->readLink(p));
for (auto & hash : hashes) {
auto pos = target.find(hash);
if (pos != std::string::npos) {
hits[hash].emplace_back(
fmt("%s -> %s", p2, hilite(target, pos, StorePath::HashLen, getColour(hash)))
);
}
}
}
co_return hits;
} catch (...) {
co_return result::current_exception();
}
struct Node
{
StorePath path;
StorePathSet dependencies;
StorePathSet dependents;
std::optional<size_t> dist = std::nullopt;
Node * prev = nullptr;
bool queued = false;
bool visited = false;
};
struct BailOut : BaseException
{};
static kj::Promise<Result<void>> printNode(
Node & node,
const std::string & firstPad,
const std::string & tailPad,
bool all,
bool precise,
Store & store,
const StorePath & packagePath,
const StorePath & dependencyPath,
std::map<StorePath, Node> & graph,
Strings & output
)
try {
auto pathS = store.printStorePath(node.path);
assert(node.dist.has_value());
if (precise) {
output.push_back(
fmt("%s%s%s%s" ANSI_NORMAL,
firstPad,
node.path == dependencyPath ? ANSI_NORMAL
: node.visited ? ANSI_DIM_ALREADY_VISITED
: "",
firstPad != "" ? "→ " : "",
pathS)
);
}
if (node.path == dependencyPath && !all && packagePath != dependencyPath) {
throw BailOut();
}
if (node.visited) {
co_return result::success();
}
if (precise) {
node.visited = true;
}
/* Sort the references by distance to `dependency` to
ensure that the shortest path is printed first. */
std::multimap<size_t, Node *> refs;
std::set<std::string> hashes;
for (auto & ref : node.dependencies) {
if (ref == node.path && packagePath != dependencyPath) {
continue;
}
auto & node2 = graph.at(ref);
if (auto dist = node2.dist) {
refs.emplace(*node2.dist, &node2);
hashes.insert(std::string(node2.path.hashPart()));
}
}
/* For each reference, find the files and symlinks that
contain the reference. */
std::map<std::string, Strings> hits;
// FIXME: should use scanForReferences().
if (precise) {
hits = TRY_AWAIT(visitPath(
pathS, store, pathS, dependencyPath.hashPart(), hashes, packagePath, dependencyPath
));
}
for (auto & ref : refs) {
std::string hash(ref.second->path.hashPart());
bool last = all ? ref == *refs.rbegin() : true;
for (auto & hit : hits[hash]) {
bool first = hit == *hits[hash].begin();
output.push_back(
fmt("%s%s%s",
tailPad,
(first ? (last ? treeLast : treeConn) : (last ? treeNull : treeLine)),
hit)
);
if (!all) {
break;
}
}
if (!precise) {
auto pathS = store.printStorePath(ref.second->path);
output.push_back(
fmt("%s%s%s%s" ANSI_NORMAL,
firstPad,
ref.second->path == dependencyPath ? ANSI_BOLD
: ref.second->visited ? ANSI_DIM_ALREADY_VISITED
: "",
last ? treeLast : treeConn,
pathS)
);
node.visited = true;
}
TRY_AWAIT(printNode(
*ref.second,
tailPad + (last ? treeNull : treeLine),
tailPad + (last ? treeNull : treeLine),
all,
precise,
store,
packagePath,
dependencyPath,
graph,
output
));
}
co_return result::success();
} catch (...) {
co_return result::current_exception();
}
static std::map<StorePath, Node> mkGraph(
const StorePath & packagePath,
const StorePath & dependencyPath,
const std::map<StorePath, StorePathSet> & graph,
Store & store,
bool all,
bool precise
)
{
std::map<StorePath, Node> graph_data;
for (auto & [path, dependencies] : graph) {
graph_data.emplace(
path,
Node{
.path = path,
.dependencies = dependencies,
.dist = path == dependencyPath ? std::optional(0) : std::nullopt,
}
);
}
for (auto & node : graph_data) {
for (auto & ref : node.second.dependencies) {
graph_data.find(ref)->second.dependents.insert(node.first);
}
}
/* Run Dijkstra's shortest path algorithm to get the distance
of every path in the closure to 'dependency'. */
std::priority_queue<Node *> queue;
queue.push(&graph_data.at(dependencyPath));
auto const inf = std::numeric_limits<size_t>::max();
while (!queue.empty()) {
auto & node = *queue.top();
queue.pop();
for (auto & rref : node.dependents) {
auto & node2 = graph_data.at(rref);
auto dist = node.dist.transform([](auto n) { return n + 1; });
if (dist.value_or(inf) < node2.dist.value_or(inf)) {
node2.dist = dist;
node2.prev = &node;
if (!node2.queued) {
node2.queued = true;
queue.push(&node2);
}
}
}
}
return graph_data;
}
kj::Promise<Result<std::string>> genGraphString(
const StorePath & start,
const StorePath & to,
const std::map<StorePath, StorePathSet> & graphData,
Store & store,
bool all,
bool precise
)
try {
auto graph = mkGraph(start, to, graphData, store, all, precise);
Strings output;
if (!precise) {
output.push_back(fmt("%s", store.printStorePath(graph.at(start).path)));
}
try {
TRY_AWAIT(printNode(graph.at(start), "", "", all, precise, store, start, to, graph, output));
} catch (BailOut &) {
}
co_return concatStringsSep("\n", output);
} catch (...) {
co_return result::current_exception();
}
}