#include "command.hh" #include "store-api.hh" #include "progress-bar.hh" #include "fs-accessor.hh" #include "shared.hh" #include using 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; } struct CmdWhyDepends : SourceExprCommand { std::string _package, _dependency; bool all = false; CmdWhyDepends() { expectArg("package", &_package); expectArg("dependency", &_dependency); addFlag({ .longName = "all", .shortName = 'a', .description = "Show all edges in the dependency graph leading from *package* to *dependency*, rather than just a shortest path.", .handler = {&all, true}, }); } std::string description() override { return "show why a package has another package in its closure"; } std::string doc() override { return #include "why-depends.md" ; } Category category() override { return catSecondary; } void run(ref store) override { auto package = parseInstallable(store, _package); auto packagePath = toStorePath(store, Realise::Outputs, operateOn, package); auto dependency = parseInstallable(store, _dependency); auto dependencyPath = toStorePath(store, Realise::Derivation, operateOn, dependency); auto dependencyPathHash = dependencyPath.hashPart(); StorePathSet closure; store->computeFSClosure({packagePath}, closure, false, false); if (!closure.count(dependencyPath)) { printError("'%s' does not depend on '%s'", store->printStorePath(packagePath), store->printStorePath(dependencyPath)); return; } stopProgressBar(); // FIXME auto accessor = store->getFSAccessor(); auto const inf = std::numeric_limits::max(); struct Node { StorePath path; StorePathSet refs; StorePathSet rrefs; size_t dist = inf; Node * prev = nullptr; bool queued = false; bool visited = false; }; std::map graph; for (auto & path : closure) graph.emplace(path, Node { .path = path, .refs = store->queryPathInfo(path)->references, .dist = path == dependencyPath ? 0 : inf }); // Transpose the graph. for (auto & node : graph) for (auto & ref : node.second.refs) graph.find(ref)->second.rrefs.insert(node.first); /* Run Dijkstra's shortest path algorithm to get the distance of every path in the closure to 'dependency'. */ std::priority_queue queue; queue.push(&graph.at(dependencyPath)); while (!queue.empty()) { auto & node = *queue.top(); queue.pop(); for (auto & rref : node.rrefs) { auto & node2 = graph.at(rref); auto dist = node.dist + 1; if (dist < node2.dist) { node2.dist = dist; node2.prev = &node; if (!node2.queued) { node2.queued = true; queue.push(&node2); } } } } /* Print the subgraph of nodes that have 'dependency' in their closure (i.e., that have a non-infinite distance to 'dependency'). Print every edge on a path between `package` and `dependency`. */ std::function printNode; struct BailOut { }; printNode = [&](Node & node, const string & firstPad, const string & tailPad) { auto pathS = store->printStorePath(node.path); assert(node.dist != inf); logger->cout("%s%s%s%s" ANSI_NORMAL, firstPad, node.visited ? "\e[38;5;244m" : "", firstPad != "" ? "→ " : "", pathS); if (node.path == dependencyPath && !all && packagePath != dependencyPath) throw BailOut(); if (node.visited) return; node.visited = true; /* Sort the references by distance to `dependency` to ensure that the shortest path is printed first. */ std::multimap refs; std::set hashes; for (auto & ref : node.refs) { if (ref == node.path && packagePath != dependencyPath) continue; auto & node2 = graph.at(ref); if (node2.dist == inf) continue; 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 hits; std::function visitPath; visitPath = [&](const Path & p) { auto st = 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 = accessor->readDirectory(p); for (auto & name : names) visitPath(p + "/" + name); } else if (st.type == FSAccessor::Type::tRegular) { auto contents = 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…\n", 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 = accessor->readLink(p); for (auto & hash : hashes) { auto pos = target.find(hash); if (pos != std::string::npos) hits[hash].emplace_back(fmt("%s -> %s\n", p2, hilite(target, pos, StorePath::HashLen, getColour(hash)))); } } }; // FIXME: should use scanForReferences(). visitPath(pathS); RunPager pager; 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(); std::cout << tailPad << (first ? (last ? treeLast : treeConn) : (last ? treeNull : treeLine)) << hit; if (!all) break; } printNode(*ref.second, tailPad + (last ? treeNull : treeLine), tailPad + (last ? treeNull : treeLine)); } }; try { printNode(graph.at(packagePath), "", ""); } catch (BailOut & ) { } } }; static auto rCmdWhyDepends = registerCommand("why-depends");