From 2ac0b2557c026e23f7e831e444cf8349b1e553e1 Mon Sep 17 00:00:00 2001 From: Mura Li <2606021+typeless@users.noreply.github.com> Date: Fri, 15 May 2026 14:11:14 +0800 Subject: [PATCH] Add pup show index forensic command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dumps the on-disk .pup/index in a grep-friendly form: file/command counts, per-edge-type totals, and a per-command listing of implicit and sticky deps. Used to diagnose whether a header's implicit-dep edge was ever recorded — the core forensic question for the transitive-dep tracking bug (see pup-header-detection-bug.md). Usage: pup show index - full dump (per-command implicit/sticky edges) pup show index --summary - counts only, including how many commands have any implicit/sticky deps recorded pup show index PATTERN - same as full dump but only commands whose command-string contains PATTERN Loads the index directly via discover_layout + read_index, without parsing Tupfiles. Useful for forensics even when the build graph is in a partially-broken state. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/cli/cmd_show.cpp | 143 ++++++++++++++++++++++++++++++++++++++++- src/cli/options.cpp | 5 +- test/unit/test_e2e.cpp | 56 ++++++++++++++++ 3 files changed, 201 insertions(+), 3 deletions(-) diff --git a/src/cli/cmd_show.cpp b/src/cli/cmd_show.cpp index 29c9178..2df2f86 100644 --- a/src/cli/cmd_show.cpp +++ b/src/cli/cmd_show.cpp @@ -26,6 +26,7 @@ #include "pup/platform/file_io.hpp" #include +#include #include #include #include @@ -626,6 +627,141 @@ auto cmd_export_instructions(Options const& opts, std::string_view variant_name) return EXIT_SUCCESS; } +auto link_type_name(pup::LinkType t) -> char const* +{ + switch (t) { + case pup::LinkType::Normal: + return "Normal"; + case pup::LinkType::Sticky: + return "Sticky"; + case pup::LinkType::Group: + return "Group"; + case pup::LinkType::Implicit: + return "Implicit"; + case pup::LinkType::OrderOnly: + return "OrderOnly"; + } + return "?"; +} + +auto cmd_export_index(Options const& opts, std::string_view variant_name) -> int +{ + auto& pool = global_pool(); + + auto layout_result = pup::discover_layout(make_layout_options(opts)); + if (!layout_result) { + fprintf(stderr, "[%.*s] Error: %s\n", static_cast(variant_name.size()), variant_name.data(), layout_result.error().msg().data()); + return EXIT_FAILURE; + } + auto& layout = *layout_result; + + auto index_path_sv = pool.get(layout.index_path()); + if (!pup::platform::exists(index_path_sv)) { + fprintf(stderr, "[%.*s] Error: No index found at %.*s — run 'putup' first\n", static_cast(variant_name.size()), variant_name.data(), static_cast(index_path_sv.size()), index_path_sv.data()); + return EXIT_FAILURE; + } + + auto index_result = pup::index::read_index(index_path_sv); + if (!index_result) { + fprintf(stderr, "[%.*s] Error reading index: %s\n", static_cast(variant_name.size()), variant_name.data(), index_result.error().msg().data()); + return EXIT_FAILURE; + } + auto& index = *index_result; + + auto edges_by_type = std::array {}; + for (auto const& e : index.edges()) { + auto t = static_cast(e.type); + if (t < edges_by_type.size()) { + ++edges_by_type[t]; + } + } + + printf("[%.*s] Index: %.*s\n", static_cast(variant_name.size()), variant_name.data(), static_cast(index_path_sv.size()), index_path_sv.data()); + printf(" Files: %zu\n", index.files().size()); + printf(" Commands: %zu\n", index.commands().size()); + printf(" Edges: %zu", index.edge_count()); + auto first = true; + for (auto t : { pup::LinkType::Normal, pup::LinkType::Sticky, pup::LinkType::Group, pup::LinkType::Implicit, pup::LinkType::OrderOnly }) { + auto count = edges_by_type[static_cast(t)]; + if (count > 0) { + printf("%s%s=%zu", first ? " (" : ", ", link_type_name(t), count); + first = false; + } + } + if (!first) { + printf(")"); + } + printf("\n"); + + if (opts.summary) { + auto cmds_with_implicit = std::size_t { 0 }; + for (auto const& cmd : index.commands()) { + for (auto const& e : index.edges()) { + if (e.to == cmd.id + && (e.type == pup::LinkType::Implicit || e.type == pup::LinkType::Sticky)) { + ++cmds_with_implicit; + break; + } + } + } + printf(" Commands with implicit/sticky deps: %zu/%zu\n", cmds_with_implicit, index.commands().size()); + return EXIT_SUCCESS; + } + + printf("\nCommands (with implicit/sticky edges):\n"); + auto filter_sv = pool.get(opts.show_var_filter); + for (auto const& cmd : index.commands()) { + auto cmd_str_id = pup::index::get_command_string(index, cmd); + auto cmd_sv = pool.get(cmd_str_id); + + if (!filter_sv.empty() && cmd_sv.find(filter_sv) == std::string_view::npos) { + continue; + } + + auto dir_sv = std::string_view {}; + if (auto const* dir = index.find_file_by_id(cmd.dir_id)) { + dir_sv = pool.get(dir->path); + } + + printf(" c%u [%.*s]\n", static_cast(cmd.id), static_cast(dir_sv.size()), dir_sv.data()); + printf(" cmd: %.*s\n", static_cast(cmd_sv.size()), cmd_sv.data()); + + auto implicit = Vec {}; + auto sticky = Vec {}; + for (auto const& e : index.edges()) { + if (e.to != cmd.id) { + continue; + } + if (e.type != pup::LinkType::Implicit && e.type != pup::LinkType::Sticky) { + continue; + } + auto const* from = index.find_file_by_id(e.from); + if (!from) { + continue; + } + if (e.type == pup::LinkType::Implicit) { + implicit.push_back(from->path); + } else { + sticky.push_back(from->path); + } + } + + if (implicit.empty() && sticky.empty()) { + printf(" (no implicit/sticky deps)\n"); + } + for (auto p : implicit) { + auto p_sv = pool.get(p); + printf(" implicit: %.*s\n", static_cast(p_sv.size()), p_sv.data()); + } + for (auto p : sticky) { + auto p_sv = pool.get(p); + printf(" sticky: %.*s\n", static_cast(p_sv.size()), p_sv.data()); + } + } + + return EXIT_SUCCESS; +} + auto show_single_variant(Options const& opts, std::string_view variant_name) -> int { auto fmt = global_pool().get(opts.show_format); @@ -644,9 +780,12 @@ auto show_single_variant(Options const& opts, std::string_view variant_name) -> if (fmt == "instructions" || fmt == "templates") { return cmd_export_instructions(opts, variant_name); } + if (fmt == "index") { + return cmd_export_index(opts, variant_name); + } fprintf(stderr, "Unknown show format: %.*s\n", static_cast(fmt.size()), fmt.data()); - fprintf(stderr, "Formats: script, compdb, graph, var, instructions\n"); + fprintf(stderr, "Formats: script, compdb, graph, var, instructions, index\n"); return EXIT_FAILURE; } @@ -656,7 +795,7 @@ auto cmd_show(Options const& opts) -> int { if (is_empty(opts.show_format)) { fprintf(stderr, "Usage: putup show \n"); - fprintf(stderr, "Formats: script, compdb, graph, var, instructions\n"); + fprintf(stderr, "Formats: script, compdb, graph, var, instructions, index\n"); return EXIT_FAILURE; } diff --git a/src/cli/options.cpp b/src/cli/options.cpp index 799d01b..efca8d5 100644 --- a/src/cli/options.cpp +++ b/src/cli/options.cpp @@ -134,7 +134,9 @@ auto parse_args(int argc, char** argv) -> Options opts.command = pool.intern(arg); } else if (pool.get(opts.command) == "show" && is_empty(opts.show_format)) { opts.show_format = pool.intern(arg); - } else if (pool.get(opts.command) == "show" && pool.get(opts.show_format) == "var" && is_empty(opts.show_var_filter)) { + } else if (pool.get(opts.command) == "show" + && (pool.get(opts.show_format) == "var" || pool.get(opts.show_format) == "index") + && is_empty(opts.show_var_filter)) { opts.show_var_filter = pool.intern(arg); } else { opts.targets.push_back(pool.intern(arg)); @@ -161,6 +163,7 @@ auto print_usage() -> void " compdb - compile_commands.json\n" " graph - DOT format (--summary for text)\n" " var [NAME] [--json] - Variable tracking\n" + " index - Index dump (--summary for counts only)\n" "\nOptions:\n" " -j, --jobs N Run N jobs in parallel\n" " -k, --keep-going Continue after failures\n" diff --git a/test/unit/test_e2e.cpp b/test/unit/test_e2e.cpp index 071cfab..9ebf118 100644 --- a/test/unit/test_e2e.cpp +++ b/test/unit/test_e2e.cpp @@ -2213,6 +2213,62 @@ SCENARIO("Show graph --summary --all-deps shows implicit edge count", "[e2e][sho } } +SCENARIO("Show index dumps implicit-dep edges from the on-disk index", "[e2e][show]") +{ + GIVEN("a built project with a -MD compile rule (implicit deps recorded)") + { + auto f = E2EFixture { "scoped_implicit_dep" }; + REQUIRE(f.init().success()); + REQUIRE(f.build().success()); + REQUIRE(f.exists("src/main.o")); + + WHEN("show index is run") + { + auto result = f.pup({ "show", "index" }); + + THEN("output reports file/command/edge counts and a header.h implicit edge") + { + INFO("stdout: " << result.stdout_output); + INFO("stderr: " << result.stderr_output); + REQUIRE(result.success()); + REQUIRE(result.stdout_output.find("Files:") != std::string::npos); + REQUIRE(result.stdout_output.find("Commands:") != std::string::npos); + REQUIRE(result.stdout_output.find("Implicit=") != std::string::npos); + REQUIRE(result.stdout_output.find("implicit:") != std::string::npos); + REQUIRE(result.stdout_output.find("header.h") != std::string::npos); + } + } + + WHEN("show index --summary is run") + { + auto result = f.pup({ "show", "index", "--summary" }); + + THEN("output is a single summary block with no per-command listing") + { + INFO("stdout: " << result.stdout_output); + REQUIRE(result.success()); + REQUIRE(result.stdout_output.find("Files:") != std::string::npos); + REQUIRE(result.stdout_output.find("Commands with implicit/sticky deps:") != std::string::npos); + // No per-command section in summary mode + REQUIRE(result.stdout_output.find("Commands (with implicit/sticky edges):") == std::string::npos); + } + } + + WHEN("show index with a positional filter is run") + { + auto result = f.pup({ "show", "index", "nonexistent_xyz" }); + + THEN("the per-command section is empty (filter matched nothing)") + { + INFO("stdout: " << result.stdout_output); + REQUIRE(result.success()); + REQUIRE(result.stdout_output.find("Commands (with implicit/sticky edges):") != std::string::npos); + REQUIRE(result.stdout_output.find("implicit:") == std::string::npos); + } + } + } +} + SCENARIO("Show script generates shell build script", "[e2e][show]") { GIVEN("a simple C project")