From 2175d007e57eee4a1ef8cad9e2a4e143e63006b4 Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Sat, 9 Aug 2025 11:57:55 +0200 Subject: [PATCH] libstore/path-tree: allow passing a custom accessor If none is given, we fall back to whatever accessor we get from the store. To display which paths actually contain the references leading to e.g. a cycle or triggering a disallowedRequisites error, we'd potentially have to look into the chroot from the previously finished build. This behavior should not be part of the local accessor by default, but part of a "special" accessor. This change allows using such an accessor for `genGraphString()`. Now that we inject the accessor from the outside, we have to mock it anyways in the tests. Hence, this also adds a testcase for the precise=True case. Change-Id: I58465fb944776c2b0262ba054d1f296ed2ae3406 --- lix/libstore/path-tree.cc | 33 +++++++++---- lix/libstore/path-tree.hh | 4 +- tests/unit/libstore/path-tree.cc | 84 +++++++++++++++++++++++++++++++- 3 files changed, 108 insertions(+), 13 deletions(-) diff --git a/lix/libstore/path-tree.cc b/lix/libstore/path-tree.cc index 8a1b8edc9..dca268eae 100644 --- a/lix/libstore/path-tree.cc +++ b/lix/libstore/path-tree.cc @@ -37,13 +37,13 @@ static kj::Promise>> visitPath( std::string_view dependencyPathHash, const std::set & hashes, const StorePath & from, - const StorePath & to + const StorePath & to, + const ref & accessor ) 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); @@ -55,9 +55,9 @@ try { 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) - ); + auto found = TRY_AWAIT(visitPath( + p + "/" + name, store, pathS, dependencyPathHash, hashes, from, to, accessor + )); hits.merge(found); } } else if (st.type == FSAccessor::Type::tRegular) { @@ -124,7 +124,8 @@ static kj::Promise> printNode( const StorePath & packagePath, const StorePath & dependencyPath, std::map & graph, - Strings & output + Strings & output, + ref accessor ) try { auto pathS = store.printStorePath(node.path); @@ -178,7 +179,14 @@ try { if (precise) { hits = TRY_AWAIT(visitPath( - pathS, store, pathS, dependencyPath.hashPart(), hashes, packagePath, dependencyPath + pathS, + store, + pathS, + dependencyPath.hashPart(), + hashes, + packagePath, + dependencyPath, + accessor )); } @@ -224,7 +232,8 @@ try { packagePath, dependencyPath, graph, - output + output, + accessor )); } @@ -294,10 +303,12 @@ kj::Promise> genGraphString( const std::map & graphData, Store & store, bool all, - bool precise + bool precise, + std::optional> maybeAccessor ) try { auto graph = mkGraph(start, to, graphData, store, all, precise); + auto accessor = maybeAccessor ? *maybeAccessor : store.getFSAccessor(); Strings output; if (!precise) { @@ -305,7 +316,9 @@ try { } try { - TRY_AWAIT(printNode(graph.at(start), "", "", all, precise, store, start, to, graph, output)); + TRY_AWAIT(printNode( + graph.at(start), "", "", all, precise, store, start, to, graph, output, accessor + )); } catch (BailOut &) { } diff --git a/lix/libstore/path-tree.hh b/lix/libstore/path-tree.hh index f985eade8..8715ae6d4 100644 --- a/lix/libstore/path-tree.hh +++ b/lix/libstore/path-tree.hh @@ -1,6 +1,7 @@ #pragma once /// @file +#include "fs-accessor.hh" #include "lix/libutil/result.hh" #include "path.hh" #include "store-api.hh" @@ -15,6 +16,7 @@ kj::Promise> genGraphString( const std::map & graphData, Store & store, bool all, - bool precise + bool precise, + std::optional> accessor = std::nullopt ); } diff --git a/tests/unit/libstore/path-tree.cc b/tests/unit/libstore/path-tree.cc index e32ef02e0..fd3f673dc 100644 --- a/tests/unit/libstore/path-tree.cc +++ b/tests/unit/libstore/path-tree.cc @@ -1,10 +1,64 @@ #include #include "lix/libstore/path-tree.hh" +#include "lix/libstore/fs-accessor.hh" +#include "lix/libstore/nar-accessor.hh" #include "lix/libstore/path.hh" #include "lix/libstore/store-api.hh" #include "lix/libutil/async.hh" +#include "lix/libutil/ref.hh" +#include "lix/libutil/json.hh" namespace nix { +static ref makeMockAccessor() +{ + JSON nix = JSON::object(); + nix["type"] = "directory"; + + JSON store = JSON::object(); + nix["entries"] = JSON::object(); + + store["type"] = "directory"; + store["entries"] = JSON::object(); + + const std::string sshContent = + "I do link to /nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-glibc-2.40-66/lib/libc.so.6 in " + "here"; + const auto narOffset = 2342ul; + + auto openssh = JSON::object(); + openssh["type"] = "directory"; + openssh["entries"] = JSON::object(); + openssh["entries"]["bin"] = JSON::object(); + openssh["entries"]["bin"]["type"] = "directory"; + openssh["entries"]["bin"]["entries"] = JSON::object(); + openssh["entries"]["bin"]["entries"]["ssh"] = JSON::object(); + openssh["entries"]["bin"]["entries"]["ssh"]["type"] = "regular"; + openssh["entries"]["bin"]["entries"]["ssh"]["size"] = sshContent.size(); + openssh["entries"]["bin"]["entries"]["ssh"]["narOffset"] = narOffset; + store["entries"]["hr8lmmjmd1jk6s3p5ymggyk4am7n2lmb-openssh-10.0p2"] = openssh; + + nix["entries"]["store"] = store; + + JSON listing = JSON::object(); + listing["type"] = "directory"; + listing["entries"] = JSON::object(); + listing["entries"]["nix"] = nix; + + return makeLazyNarAccessor( + listing.dump(), + [narOffset, sshContent](uint64_t offset, uint64_t length) { + if (offset == narOffset) { + assert(length <= sshContent.size()); + return sshContent.substr(0, length); + } else { + throw Error( + "Invalid offset '%llu' in mock NAR (looking for: %llu)", offset, narOffset + ); + } + } + ); +} + TEST(PathTree, simple) { AsyncIoRoot aio; @@ -19,7 +73,7 @@ TEST(PathTree, simple) graph.insert({parent, {child}}); auto graph_data = - aio.blockOn(genGraphString(parent, child, graph, *store, false, false)); + aio.blockOn(genGraphString(parent, child, graph, *store, false, false, makeMockAccessor())); // clang-format off ASSERT_EQ( @@ -30,6 +84,32 @@ TEST(PathTree, simple) // clang-format on } +TEST(PathTree, precise) +{ + AsyncIoRoot aio; + + auto store = aio.blockOn(openStore("dummy://")); + + auto parent = StorePath{"hr8lmmjmd1jk6s3p5ymggyk4am7n2lmb-openssh-10.0p2"}; + auto child = StorePath{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-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, true, makeMockAccessor())); + + // clang-format off + ASSERT_EQ( + graph_data, + "/nix/store/hr8lmmjmd1jk6s3p5ymggyk4am7n2lmb-openssh-10.0p2\x1B[0m\n" + "└───bin/ssh: …I do link to /nix/store/\x1B[32;1maaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x1B[0m-glibc-2.40-66/lib/libc.so.6 in …\n" + " \x1B[0m→ /nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-glibc-2.40-66\x1B[0m" + ); + // clang-format on +} + TEST(PathTree, all) { AsyncIoRoot aio; @@ -54,7 +134,7 @@ TEST(PathTree, all) // clang-format on auto graph_data = - aio.blockOn(genGraphString(parent, child, graph, *store, true, false)); + aio.blockOn(genGraphString(parent, child, graph, *store, true, false, makeMockAccessor())); ASSERT_EQ(graph_data, expected); } }