diff --git a/lix/libstore/meson.build b/lix/libstore/meson.build index 628ec1e55..5205539b4 100644 --- a/lix/libstore/meson.build +++ b/lix/libstore/meson.build @@ -181,6 +181,7 @@ libstore_sources = files( 'parsed-derivations.cc', 'path-info.cc', 'path-references.cc', + 'path-tree.cc', 'path-with-outputs.cc', 'path.cc', 'pathlocks.cc', @@ -248,6 +249,7 @@ libstore_headers = files( 'path-info.hh', 'path-references.hh', 'path-regex.hh', + 'path-tree.hh', 'path-with-outputs.hh', 'path.hh', 'pathlocks.hh', diff --git a/lix/libstore/path-tree.cc b/lix/libstore/path-tree.cc new file mode 100644 index 000000000..c31e406c3 --- /dev/null +++ b/lix/libstore/path-tree.cc @@ -0,0 +1,311 @@ +#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 +#include + +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>> visitPath( + const Path & p, + Store & store, + const Path & pathS, + std::string_view dependencyPathHash, + const std::set & hashes, + const StorePath & from, + const StorePath & to +) +try { + /* For each reference, find the files and symlinks that + contain the reference. */ + std::map 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 dist = std::nullopt; + Node * prev = nullptr; + bool queued = false; + bool visited = false; +}; + +struct BailOut : BaseException +{}; + +static kj::Promise> 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 & 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.visited ? "\e[38;5;244m" : "", + 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 refs; + std::set 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 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->visited ? "\e[38;5;244m" : "", + 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 mkGraph( + const StorePath & packagePath, + const StorePath & dependencyPath, + const std::map & graph, + Store & store, + bool all, + bool precise +) +{ + std::map 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 queue; + + queue.push(&graph_data.at(dependencyPath)); + auto const inf = std::numeric_limits::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> genGraphString( + const StorePath & start, + const StorePath & to, + const std::map & 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(); +} + +} diff --git a/lix/libstore/path-tree.hh b/lix/libstore/path-tree.hh new file mode 100644 index 000000000..f985eade8 --- /dev/null +++ b/lix/libstore/path-tree.hh @@ -0,0 +1,20 @@ +#pragma once +/// @file + +#include "lix/libutil/result.hh" +#include "path.hh" +#include "store-api.hh" +#include + +#define ANSI_ALREADY_VISITED "\e[38;5;244m" + +namespace nix { +kj::Promise> genGraphString( + const StorePath & start, + const StorePath & to, + const std::map & graphData, + Store & store, + bool all, + bool precise +); +} diff --git a/lix/nix/why-depends.cc b/lix/nix/why-depends.cc index 439471b48..7f99c99c7 100644 --- a/lix/nix/why-depends.cc +++ b/lix/nix/why-depends.cc @@ -1,315 +1,10 @@ #include "lix/libcmd/command.hh" #include "lix/libstore/store-api.hh" -#include "lix/libstore/fs-accessor.hh" #include "lix/libmain/shared.hh" -#include "lix/libutil/error.hh" -#include "lix/libutil/result.hh" -#include "lix/libutil/strings.hh" +#include "lix/libstore/path-tree.hh" #include "why-depends.hh" -#include - 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>> visitPath( - const Path & p, - Store & store, - const Path & pathS, - std::string_view dependencyPathHash, - const std::set & hashes, - const StorePath & from, - const StorePath & to -) -try { - /* For each reference, find the files and symlinks that - contain the reference. */ - std::map 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 dist = std::nullopt; - Node * prev = nullptr; - bool queued = false; - bool visited = false; -}; - -struct BailOut : BaseException -{}; - -static kj::Promise> 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 & 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.visited ? "\e[38;5;244m" : "", - 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 refs; - std::set 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 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->visited ? "\e[38;5;244m" : "", - 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 mkGraph( - const StorePath & packagePath, - const StorePath & dependencyPath, - const std::map & graph, - Store & store, - bool all, - bool precise -) -{ - std::map 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 queue; - - queue.push(&graph_data.at(dependencyPath)); - auto const inf = std::numeric_limits::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; -} - -static kj::Promise> genGraphString( - const StorePath & start, - const StorePath & to, - const std::map & 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(); -} - struct CmdWhyDepends : SourceExprCommand, MixOperateOnOptions { std::string _package, _dependency; diff --git a/tests/unit/libstore/path-tree.cc b/tests/unit/libstore/path-tree.cc new file mode 100644 index 000000000..3ea4c6edc --- /dev/null +++ b/tests/unit/libstore/path-tree.cc @@ -0,0 +1,55 @@ +#include +#include "lix/libstore/path-tree.hh" +#include "lix/libstore/path.hh" +#include "lix/libstore/store-api.hh" +#include "lix/libutil/async.hh" + +namespace nix { +TEST(PathTree, simple) +{ + AsyncIoRoot aio; + + auto store = aio.blockOn(openStore("dummy://")); + + auto parent = StorePath{"hr8lmmjmd1jk6s3p5ymggyk4am7n2lmb-openssh-10.0p2"}; + auto child = StorePath{"q4wq65gl3r8fy746v9bbwgx4gzn0r2kl-glibc-2.40-66"}; + + std::map graph; + graph.insert({child, {}}); + graph.insert({parent, {child}}); + + auto graph_data = + aio.blockOn(genGraphString(parent, child, graph, *store, false, false)); + ASSERT_EQ( + graph_data, + "/nix/store/hr8lmmjmd1jk6s3p5ymggyk4am7n2lmb-openssh-10.0p2\n└───/nix/store/" + "q4wq65gl3r8fy746v9bbwgx4gzn0r2kl-glibc-2.40-66\x1B[0m" + ); +} + +TEST(PathTree, all) +{ + AsyncIoRoot aio; + + auto store = aio.blockOn(openStore("dummy://")); + + auto parent = StorePath{"hr8lmmjmd1jk6s3p5ymggyk4am7n2lmb-openssh-10.0p2"}; + auto child = StorePath{"q4wq65gl3r8fy746v9bbwgx4gzn0r2kl-glibc-2.40-66"}; + auto intermediate = StorePath{"6r4zqb04fq5l5l4zghq76wvcpz7dwd35-linux-pam-1.6.1"}; + + std::map graph; + graph.insert({intermediate, {child}}); + graph.insert({parent, {intermediate, child}}); + graph.insert({child, {}}); + + const std::string expected = + "/nix/store/hr8lmmjmd1jk6s3p5ymggyk4am7n2lmb-openssh-10.0p2\n" + "├───/nix/store/q4wq65gl3r8fy746v9bbwgx4gzn0r2kl-glibc-2.40-66\x1B[0m\n" + "└───/nix/store/6r4zqb04fq5l5l4zghq76wvcpz7dwd35-linux-pam-1.6.1\x1B[0m\n" + " └───/nix/store/q4wq65gl3r8fy746v9bbwgx4gzn0r2kl-glibc-2.40-66\x1B[0m"; + + auto graph_data = + aio.blockOn(genGraphString(parent, child, graph, *store, true, false)); + ASSERT_EQ(graph_data, expected); +} +} diff --git a/tests/unit/meson.build b/tests/unit/meson.build index ceba67704..264a83c6b 100644 --- a/tests/unit/meson.build +++ b/tests/unit/meson.build @@ -141,6 +141,7 @@ libstore_tests_sources = files( 'libstore/nar-info-disk-cache.cc', 'libstore/outputs-spec.cc', 'libstore/path.cc', + 'libstore/path-tree.cc', 'libstore/references.cc', 'libstore/serve-protocol.cc', 'libstore/worker-protocol.cc',