From 3238f97315d701fca5a9c0d3163cf102736a40a1 Mon Sep 17 00:00:00 2001 From: Mura Li <2606021+typeless@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:46:26 +0800 Subject: [PATCH 1/2] Scan a command per invocation and report a rule per object A command whose invocations were all compiles got one scan, taking flags from the first invocation and sources from all of them, and a command with any non-compile invocation got none. Both halves cost real builds. A rule like `gcc -c a.c -o a.o && gcc -Iinc_b -c b.c -o b.o` produced `gcc -M a.c b.c` without -Iinc_b, so the scan could not find b.c's header and the build failed outright. And a rule like `gcc -c a.c -o a.o && cd sub && gcc -c b.c -o b.o` was refused whole, so a.o -- perfectly reproducible from the rule's directory -- went unscanned and was reported as if nothing about it could be recovered. The unit was wrong in both places. Scan per invocation, over the longest leading run of compiles the scanner recognizes: each scan carries that invocation's own flags and its own sources, and scanning stops at the first invocation that could change the directory or the environment, because past it the scan would preprocess in a state the compile never had. An empty prefix still refuses everything, which is the bound that keeps the rule from becoming scan-everything. Reporting follows the same move, from rule to object: an object is covered when some generated scan names it, and every object no scan names is reported. That dissolves "a rule scanned in part" rather than encoding it -- per object the answer is binary again -- and it fixes a case that predates this change, where a rule declaring an object no invocation writes was silent because a scan existed for the rule. Coverage is decided by match_and_generate, never a re-derivation, so the existence-keyed map the check used is gone. Carrying that needed the scanner interface widened. build_dep_scans returns Vec, and DepScan carries the scan command with the object word the invocation writes, so the mapping from scan to object is constructed at the only site able to construct it -- inside the scanner as it reads the -o word -- and travels as one value rather than being re-derived by re-parsing the command text. Empty is refusal: "scanned nothing" and "refused" were never two states, and an optional would have spelled one state twice. GeneratedRule gains covered_object to forward it, because the reporter consumes match_and_generate rather than the scanner. RulePattern::generate widens with them; it is a second singular surface that a name-based grep for the scanner does not reach. The old name is deleted rather than aliased, so every stale call is a compile error instead of a silently adapted one. That widening was designed to land first, as its own behaviour-preserving commit, so this one would show the decision rather than 76 sites of plumbing. It is not separable in the end: each scanner's builder body was re-indented into the per-invocation loop, so the rename and the prefix law occupy one indivisible hunk, and splitting it by line yields a half-rewritten function rather than two commits that build. Landing it whole is the declared divergence, not an oversight. The object comparison is the part to be careful with, and it took two attempts. The scan's output word is relative to the rule's own directory, exactly as the compile resolves it, so joining it there is what puts it in the declared output's space -- %o one directory down expands to ../../build/src/lib/a.o and lands on the declared path. An intermediate version of this work stripped the variant prefix off the declared side instead, which looked right against a rule written `-o a.o` in an out-of-tree build and was backwards: that rule is malformed, writing into the source directory while declaring an object under the variant, and stripping guaranteed a mismatch for every correct rule not at the project root. A cross-model review caught it with the %o repro; the pin added for it uses a subdirectory rule, because a root-level rule has an empty source directory and never exercises the round trip. Both drivers also learn the joined output spelling: -oa.o left the object unrecorded and, worse, rode into the scan command, which would have made the scan write an object. No index change is needed. Two invocations can render byte-identical scan text and share one node, which is correct rather than a collision: identical text requires identical sources and identical flags, so the two scans have identical dependency sets and the nodes are interchangeable by construction. Verified: whole suite green in one process -- 832 cases, 170196 assertions -- plus make check at exit 0, and format, tidy, iwyu and spec-check clean. Each flipped requirement was observed red first; the reporter's red needed a control tree built from 0e7907602, where the same rule names a.o -- the object that is reproducible -- and never names b.o. Left alone deliberately: reports_own_deps still tests the whole command text, so one depfile flag anywhere suppresses reporting for every object of that command. Under a per-object reporter that is an asymmetry, and it belongs to the separate issue #357 rather than here. Closes #355. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01AjcxNmyFfoTJYSZKKgjrCp --- DESIGN.md | 22 +- docs/reference.md | 12 +- include/pup/graph/dep_scanner.hpp | 16 +- include/pup/graph/rule_pattern.hpp | 9 +- include/pup/graph/scanners/clang_cl.hpp | 5 +- include/pup/graph/scanners/gcc.hpp | 5 +- spec/requirements/dep-scan.ears.md | 49 ++-- src/cli/cmd_build.cpp | 6 +- src/cli/strict_checks.cpp | 66 ++--- src/graph/dep_scanner.cpp | 34 ++- src/graph/rule_pattern.cpp | 35 ++- src/graph/scanners/clang_cl.cpp | 194 +++++++------- src/graph/scanners/gcc.cpp | 178 +++++++------ test/unit/test_dep_scanner.cpp | 340 ++++++++++++------------ test/unit/test_e2e.cpp | 77 +++++- 15 files changed, 584 insertions(+), 464 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 9aece720..d7df9e51 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -879,7 +879,7 @@ Scanners detect compiler commands and generate dependency extraction commands: class DepScanner { virtual auto matches(CommandInfo const&) const -> bool = 0; virtual auto has_dep_flags(std::string_view) const -> bool = 0; - virtual auto build_dep_command(CommandInfo const&) -> std::optional = 0; + virtual auto build_dep_scans(CommandInfo const&) -> Vec = 0; }; class DepScannerRegistry { @@ -891,15 +891,17 @@ class DepScannerRegistry { The `GccScanner` implementation handles GCC, Clang, and compatible compilers. **What a scan may cover.** A scan runs from the rule's directory with the rest of the command -stripped, so it may carry a word only from an invocation it can reproduce, and it can reproduce an -invocation only when it and every invocation before it is a compile the scanner recognizes. -Refusal is decidable from the token stream; anything finer requires modeling the shell. A -redirection ends what the scan may read from an invocation but divides no invocation — only a -control operator starts a new one. Two -consequences bind every future change here. Per rule, reporting is binary — scanned, or reported -unscanned — so no encoding may create a scan state the reporter cannot express; widen the reporter -first. And `matches` and `build_dep_command` answer through one criterion, because the unscanned -report consumes the production predicate rather than a re-derivation of it. +stripped, so it may carry a word only from the invocation it reproduces, and it can reproduce an +invocation only when it and every invocation before it is a compile the scanner recognizes — one +scan per invocation of that leading prefix. Refusal is decidable from the token stream; anything +finer requires modeling the shell. A redirection ends what the scan may read from an invocation +but divides no invocation — only a control operator starts a new one. Three consequences bind +every future change here. A scan travels with the object it covers, because only the scanner's +parse of the invocation can say which one it is: anything a consumer must attribute rides inside +`DepScan`, never beside it. Per object, reporting is binary — covered, or reported unscanned — so +no encoding may create a scan state the reporter cannot express; widen the reporter first. And +`matches` and `build_dep_scans` answer through one criterion, because the unscanned report +consumes the production predicate rather than a re-derivation of it. ### Generation Flow diff --git a/docs/reference.md b/docs/reference.md index acccf1b6..8256d118 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -1979,10 +1979,14 @@ the scan itself are left out: substitution a second time, on results the compile never saw. A `-D` whose value is a command substitution is therefore invisible to the scan. -A rule that runs several compiles in one command (`gcc -c a.c … && gcc -c b.c …`) is -scanned once: the scan takes its flags from the first invocation and its sources from -all of them, so every source is covered but a flag that only the later invocation -carries is not. A redirection (`> log`, `2>&1`) ends the invocation the same way. +A rule that runs several compiles in one command (`gcc -c a.c … && gcc -c b.c …`) gets +one scan per compile, each carrying that invocation's own flags and sources. Scanning +stops at the first invocation putup cannot reproduce from the rule's directory — a +directory change, an environment assignment, a link, any other program — because past +it the scan would preprocess in a state the compile never had; that invocation and +every one after it go unscanned, and `parse` names each object they leave uncovered. +A redirection (`> log`, `2>&1`) divides no invocation, though the scan carries no flag +from beyond it. Flags whose argument putup recognizes (GNU driver): `-I`, `-isystem`, `-iquote`, `-include`, `-isysroot`, `--sysroot`, `-D`, `-U` diff --git a/include/pup/graph/dep_scanner.hpp b/include/pup/graph/dep_scanner.hpp index 38575dba..f30d9875 100644 --- a/include/pup/graph/dep_scanner.hpp +++ b/include/pup/graph/dep_scanner.hpp @@ -8,7 +8,6 @@ #include "pup/graph/rule_pattern.hpp" #include -#include #include namespace pup::graph { @@ -23,6 +22,13 @@ struct DepSpec { DepOutputMode output_mode = DepOutputMode::Stdout; }; +/// One scan and the object file whose compile it reproduces. The object travels with the scan +/// because only the scanner's own parse of the invocation can say which one it is. +struct DepScan { + StringId command = StringId::Empty; + StringId object = StringId::Empty; +}; + /// Abstract interface for dependency scanners. /// Implementations detect specific tools (compilers, assemblers, linkers) /// and generate commands to extract their implicit dependencies. @@ -44,12 +50,12 @@ class DepScanner { [[nodiscard]] virtual auto has_dep_flags(std::string_view cmd) const -> bool = 0; - /// Build a command to extract dependencies from the given command. - /// Returns nullopt if deps shouldn't be extracted (e.g., already has flags). + /// Build the scans that extract dependencies from the given command, one per compile + /// invocation it can reproduce. Empty means the command carries no such invocation. [[nodiscard]] - virtual auto build_dep_command( + virtual auto build_dep_scans( CommandInfo const& cmd - ) const -> std::optional = 0; + ) const -> Vec = 0; /// Get the dependency extraction specification [[nodiscard]] diff --git a/include/pup/graph/rule_pattern.hpp b/include/pup/graph/rule_pattern.hpp index 15b176f5..e0047e71 100644 --- a/include/pup/graph/rule_pattern.hpp +++ b/include/pup/graph/rule_pattern.hpp @@ -9,7 +9,6 @@ #include "pup/core/types.hpp" #include "pup/core/vec.hpp" -#include #include namespace pup::graph { @@ -52,16 +51,16 @@ struct GeneratedRule { StringId display = StringId::Empty; Vec outputs; OutputAction action = OutputAction::Normal; - NodeId parent_command = INVALID_NODE_ID; ///< For InjectImplicitDeps + NodeId parent_command = INVALID_NODE_ID; ///< For InjectImplicitDeps + StringId covered_object = StringId::Empty; ///< The parent's object this rule records headers for }; /// Pattern that generates additional rules when matched struct RulePattern { bool (*matches)(std::string_view command); - /// Generate a rule from a matched command - /// Returns nullopt if pattern matches but rule shouldn't be generated - Function(CommandInfo const&)> generate; + /// Generate the rules for a matched command; empty if the pattern matches but generates none + Function(CommandInfo const&)> generate; }; /// Registry for rule patterns diff --git a/include/pup/graph/scanners/clang_cl.hpp b/include/pup/graph/scanners/clang_cl.hpp index 5b031d24..0dc70136 100644 --- a/include/pup/graph/scanners/clang_cl.hpp +++ b/include/pup/graph/scanners/clang_cl.hpp @@ -3,7 +3,6 @@ #pragma once -#include "pup/core/string_id.hpp" #include "pup/graph/dep_scanner.hpp" #include "pup/graph/scanners/dep_words.hpp" @@ -21,8 +20,8 @@ class ClangClScanner final : public DepScanner { [[nodiscard]] auto has_dep_flags(std::string_view cmd) const -> bool override; [[nodiscard]] - auto build_dep_command(CommandInfo const& cmd) const - -> std::optional override; + auto build_dep_scans(CommandInfo const& cmd) const + -> Vec override; [[nodiscard]] auto dep_spec() const -> DepSpec override; [[nodiscard]] diff --git a/include/pup/graph/scanners/gcc.hpp b/include/pup/graph/scanners/gcc.hpp index fd1ec1ee..3c7d4831 100644 --- a/include/pup/graph/scanners/gcc.hpp +++ b/include/pup/graph/scanners/gcc.hpp @@ -3,7 +3,6 @@ #pragma once -#include "pup/core/string_id.hpp" #include "pup/graph/dep_scanner.hpp" #include "pup/graph/scanners/dep_words.hpp" @@ -21,8 +20,8 @@ class GccScanner final : public DepScanner { [[nodiscard]] auto has_dep_flags(std::string_view cmd) const -> bool override; [[nodiscard]] - auto build_dep_command(CommandInfo const& cmd) const - -> std::optional override; + auto build_dep_scans(CommandInfo const& cmd) const + -> Vec override; [[nodiscard]] auto dep_spec() const -> DepSpec override; [[nodiscard]] diff --git a/spec/requirements/dep-scan.ears.md b/spec/requirements/dep-scan.ears.md index 8c9e66ad..77d585c4 100644 --- a/spec/requirements/dep-scan.ears.md +++ b/spec/requirements/dep-scan.ears.md @@ -22,9 +22,9 @@ silence leaves a rule whose headers are never recorded, which is why the last re exists. The unit that narrowness is measured in is the invocation: a command's control operators divide it -into invocations, and a scan may draw a word only from one it can reproduce. A redirection is not -such a divider — it hands its target to the same program — though the scan still carries no flag -from beyond it. +into invocations, each gets its own scan, and a scan may draw a word only from the invocation it +reproduces. A redirection is not such a divider — it hands its target to the same program — though +the scan still carries no flag from beyond it. Two classes escape the report. A compile-and-link command with no `-c` produces an executable rather than an object file, so the output-shaped trigger cannot see it (the `HOSTCC` generator @@ -43,37 +43,42 @@ Which commands a scan is generated for. - conformance: putup-only - discharge: test "GccScanner rejects compound shell commands" - discharge: test "GccScanner compiler wrapper handling" -- discharge: test "GccScanner refuses a command whose later invocation changes directory" -- discharge: test "GccScanner refuses a command whose later invocation is not a compile" -- discharge: test "matches_gcc_compile refuses a link whose later invocation compiles" -- discharge: test "ClangClScanner refuses a later invocation that is not a compile" +- discharge: test "GccScanner scans the prefix before a directory change" +- discharge: test "GccScanner scans the prefix before an invocation that is not a compile" +- discharge: test "matches_gcc_compile refuses a command whose first invocation is not a compile" +- discharge: test "A command whose first invocation is not a compile is scanned nowhere" +- discharge: test "ClangClScanner scans the prefix before an invocation that is not a compile" -Where a command runs any invocation that is not a compile putup recognizes, whether a loop, a +Where a command runs an invocation that is not a compile putup recognizes, whether a loop, a directory change, an environment assignment, a link or any other program, putup shall generate no -dependency scan for that command, because the scan runs from the rule's directory with the rest of -the command stripped and would preprocess in a state the compile never had. +dependency scan for that invocation or for any that follows it, because the scan runs from the +rule's directory with the rest of the command stripped and past such an invocation would +preprocess in a state the compile never had. -### REQ-SCAN-FLAG-SOURCE +### REQ-SCAN-PER-INVOCATION - conformance: putup-only -- discharge: test "GccScanner takes flags from the invocation it scans, sources from all of them" +- discharge: test "GccScanner scans each compile of an all-compile command with its own flags" -Where a command runs more than one invocation and every one of them is a compile putup recognizes, -putup shall build one scan carrying the source-file words of all of them; that scan takes its flags -from the first invocation alone, which issue #355 records as a limitation rather than a behaviour a -later invocation may rely on. +Where a command's leading invocations are compiles putup recognizes, putup shall build one scan per +such invocation, each carrying that invocation's own flags and its own source-file words, because +each one preprocesses a different translation unit and the object it writes is covered only by a +scan derived from it. ## Group: reporting -What putup says about a rule it did not scan. Per rule the reporting is binary — scanned, or -reported unscanned — so no scan decision may create a state this group cannot express, such as a -rule scanned in part; widening what a scan may cover means widening this group first. +What putup says about an object it did not scan. The unit is the object, not the rule: per object +the reporting is binary — covered by a scan derived from the compile that writes it, or reported +unscanned — so a rule scanned in part is not a state this group must express, only what a reader +sees when some of a rule's objects are named. ### REQ-SCAN-REPORT-UNSCANNED - conformance: putup-only - discharge: test "Scenario: A compile-shaped rule with no dependency scan is reported" +- discharge: test "Scenario: An object no scanned invocation writes is reported beside its scanned sibling" -When a rule's declared outputs include an object file, no scan is generated for its command, -and its command carries no depfile flag anywhere in its text, putup shall name that object and -the rule's Tupfile under `parse`, and report how many such rules exist under a build. +When a rule declares an object file that no generated scan covers — every object it declares, +where no scan at all is generated — and the rule's command carries no depfile flag anywhere in its +text, putup shall name that object and the rule's Tupfile under `parse`, and report how many such +objects exist under a build. diff --git a/src/cli/cmd_build.cpp b/src/cli/cmd_build.cpp index 085fecd7..c902154c 100644 --- a/src/cli/cmd_build.cpp +++ b/src/cli/cmd_build.cpp @@ -2202,13 +2202,13 @@ auto build_single_variant( auto num_commands = std::size_t { pup::graph::nodes_of_type(bs.graph, pup::NodeType::Command).size() }; // One line, not one per rule: a warning that fires on every rule of a green build teaches - // everyone to scroll past warnings. The per-rule findings live in `parse`. + // everyone to scroll past warnings. The per-object findings live in `parse`. if (auto unscanned = check_unscanned_compiles(bs.graph, bs.path_cache); !unscanned.empty()) { vprint( variant_name, - "{} rule{} an object file with no dependency scan; run 'putup parse' for the list.\n", + "{} object file{} no dependency scan; run 'putup parse' for the list.\n", unscanned.size(), - unscanned.size() == 1 ? " produces" : "s produce" + unscanned.size() == 1 ? " has" : "s have" ); } diff --git a/src/cli/strict_checks.cpp b/src/cli/strict_checks.cpp index 956e8631..b85f90b4 100644 --- a/src/cli/strict_checks.cpp +++ b/src/cli/strict_checks.cpp @@ -5,7 +5,6 @@ #include "pup/cli/context.hpp" #include "pup/core/buf.hpp" #include "pup/core/global_pool.hpp" -#include "pup/core/node_id_map.hpp" #include "pup/core/path.hpp" #include "pup/core/string_pool.hpp" #include "pup/core/vec.hpp" @@ -13,6 +12,7 @@ #include "pup/parser/ast.hpp" #include "pup/platform/file_io.hpp" +#include #include #include #include @@ -146,33 +146,25 @@ auto check_unscanned_compiles( auto& pool = global_pool(); auto commands = graph::nodes_of_type(graph, NodeType::Command); - // A scan node is proof its parent is covered; absence proves nothing, since `parse` installs no registry. - auto scanned = NodeIdMap32 {}; for (auto id : commands) { - if (auto parent = graph::get_parent_command(graph, id); parent != INVALID_NODE_ID) { - scanned.set(parent, 1); - } - } - - for (auto id : commands) { - if (graph::get_parent_command(graph, id) != INVALID_NODE_ID || scanned.contains(id)) { + if (graph::get_parent_command(graph, id) != INVALID_NODE_ID) { continue; } - auto object = std::string_view {}; + auto objects = Vec {}; for (auto out_id : graph::get_outputs(graph, id)) { - auto sv = pool.get(graph::get_full_path(graph, out_id)); - if (auto ext = pup::path::extension(sv); ext == ".o" || ext == ".obj") { - object = sv; - break; + auto full = graph::get_full_path(graph, out_id); + if (auto ext = pup::path::extension(pool.get(full)); ext == ".o" || ext == ".obj") { + objects.push_back(full); } } - if (object.empty()) { + if (objects.empty()) { continue; } auto text = graph::expand_instruction(graph, id, cache); auto source_dir = graph::get(graph, id); + auto dir = pool.get(source_dir); // The build reads a depfile from beside the object whether or not a scan exists, // so a compile that writes its own is covered without one. @@ -186,22 +178,36 @@ auto check_unscanned_compiles( cmd_info.node_id = id; cmd_info.command = text; cmd_info.working_dir = source_dir; - if (!registry->match_and_generate(cmd_info).empty()) { - continue; + + // The output word is relative to the rule's directory, the same as the compile resolves it, + // so joining it there is what puts it in the declared output's space. + auto covered = Vec {}; + for (auto const& rule : registry->match_and_generate(cmd_info)) { + if (rule.covered_object == StringId::Empty) { + continue; + } + covered.push_back(pup::path::normalize( + pool.get(pup::path::join(dir.empty() ? "." : dir, pool.get(rule.covered_object))) + )); } - auto dir = pool.get(source_dir); - auto buf = Buf {}; - buf.fmt("no dependency scan for '{}': putup recognizes no compile it can reproduce in " - "this command, so any header this rule reads goes unrecorded. Harmless if it " - "preprocesses none — an assembler, a copy, a partial link.", - object); - result.push_back(make_diag( - pool.get(pup::path::join(dir.empty() ? "." : dir, "Tupfile")), - 0, - Diagnostic::Warning, - buf.view() - )); + for (auto object : objects) { + if (std::ranges::find(covered, object) != covered.end()) { + continue; + } + + auto buf = Buf {}; + buf.fmt("no dependency scan for '{}': putup recognizes no compile it can reproduce in " + "this command, so any header this rule reads goes unrecorded. Harmless if it " + "preprocesses none — an assembler, a copy, a partial link.", + pool.get(object)); + result.push_back(make_diag( + pool.get(pup::path::join(dir.empty() ? "." : dir, "Tupfile")), + 0, + Diagnostic::Warning, + buf.view() + )); + } } return result; diff --git a/src/graph/dep_scanner.cpp b/src/graph/dep_scanner.cpp index 02fbff8e..a534aace 100644 --- a/src/graph/dep_scanner.cpp +++ b/src/graph/dep_scanner.cpp @@ -64,27 +64,25 @@ auto DepScannerRegistry::match_and_generate(CommandInfo const& cmd) const continue; } - auto dep_cmd = scanner->build_dep_command(cmd); - if (!dep_cmd) { - continue; - } - auto spec = scanner->dep_spec(); - auto outputs = Vec {}; - if (spec.output_mode == DepOutputMode::Stdout) { - outputs.push_back({ .type = GeneratedOutput::Type::Stdout, .path = StringId::Empty }); + for (auto const& scan : scanner->build_dep_scans(cmd)) { + auto outputs = Vec {}; + if (spec.output_mode == DepOutputMode::Stdout) { + outputs.push_back({ .type = GeneratedOutput::Type::Stdout, .path = StringId::Empty }); + } + + result.push_back(GeneratedRule { + .inputs = cmd.inputs, + .order_only_inputs = cmd.order_only_inputs, + .command = scan.command, + .display = make_dep_display(cmd.inputs), + .outputs = std::move(outputs), + .action = OutputAction::InjectImplicitDeps, + .parent_command = cmd.node_id, + .covered_object = scan.object, + }); } - - result.push_back(GeneratedRule { - .inputs = cmd.inputs, - .order_only_inputs = cmd.order_only_inputs, - .command = *dep_cmd, - .display = make_dep_display(cmd.inputs), - .outputs = std::move(outputs), - .action = OutputAction::InjectImplicitDeps, - .parent_command = cmd.node_id, - }); } return result; diff --git a/src/graph/rule_pattern.cpp b/src/graph/rule_pattern.cpp index c9c6ef7f..fadadc70 100644 --- a/src/graph/rule_pattern.cpp +++ b/src/graph/rule_pattern.cpp @@ -9,7 +9,6 @@ #include "pup/core/vec.hpp" #include "pup/graph/dep_scanner.hpp" #include "pup/graph/scanners/gcc.hpp" -#include #include namespace pup::graph { @@ -30,8 +29,8 @@ auto RulePatternRegistry::match_and_generate(CommandInfo const& cmd) const continue; } - if (auto rule = pattern.generate(cmd)) { - result.push_back(std::move(*rule)); + for (auto& rule : pattern.generate(cmd)) { + result.push_back(std::move(rule)); } } @@ -43,27 +42,27 @@ auto make_gcc_depfile_pattern() -> RulePattern return RulePattern { .matches = scanners::matches_gcc_compile, - .generate = [](CommandInfo const& cmd) -> std::optional { + .generate = [](CommandInfo const& cmd) -> Vec { static auto const scanner = scanners::GccScanner {}; + auto result = Vec {}; if (scanner.has_dep_flags(global_pool().get(cmd.command))) { - return std::nullopt; + return result; } - auto dep_cmd = scanner.build_dep_command(cmd); - if (!dep_cmd) { - return std::nullopt; + for (auto const& scan : scanner.build_dep_scans(cmd)) { + result.push_back(GeneratedRule { + .inputs = cmd.inputs, + .order_only_inputs = cmd.order_only_inputs, + .command = scan.command, + .display = make_dep_display(cmd.inputs), + .outputs = { { .type = GeneratedOutput::Type::Stdout, .path = StringId::Empty } }, + .action = OutputAction::InjectImplicitDeps, + .parent_command = cmd.node_id, + .covered_object = scan.object, + }); } - - return GeneratedRule { - .inputs = cmd.inputs, - .order_only_inputs = cmd.order_only_inputs, - .command = *dep_cmd, - .display = make_dep_display(cmd.inputs), - .outputs = { { .type = GeneratedOutput::Type::Stdout, .path = StringId::Empty } }, - .action = OutputAction::InjectImplicitDeps, - .parent_command = cmd.node_id, - }; + return result; }, }; } diff --git a/src/graph/scanners/clang_cl.cpp b/src/graph/scanners/clang_cl.cpp index e990e5ad..fd6d65fe 100644 --- a/src/graph/scanners/clang_cl.cpp +++ b/src/graph/scanners/clang_cl.cpp @@ -133,17 +133,26 @@ auto driver_index(std::span invocation) -> std::optional return idx; } -/// A scan reproduces one invocation from the rule's directory, so it may carry a word only from a -/// command whose invocations are all compiles it recognizes (#356). -auto every_invocation_is_a_compile(std::span const> invocations) -> bool +auto is_recognized_compile(std::span invocation) -> bool { - return std::ranges::all_of(invocations, [](auto invocation) { - auto idx = driver_index(invocation); - if (!idx) { - return false; - } - return std::ranges::any_of(invocation.subspan(*idx + 1), is_compile_flag); - }); + auto idx = driver_index(invocation); + if (!idx) { + return false; + } + return std::ranges::any_of(invocation.subspan(*idx + 1), is_compile_flag); +} + +/// The leading invocations a scan can reproduce from the rule's directory: one that is not a +/// compile may change the directory or the environment, so it and everything after it are out of +/// reach (#356). +auto compile_prefix(std::span const> invocations) + -> std::span const> +{ + auto length = std::size_t { 0 }; + while (length < invocations.size() && is_recognized_compile(invocations[length])) { + ++length; + } + return invocations.first(length); } auto command_words(std::string_view command) -> Vec @@ -166,7 +175,7 @@ auto matches_clang_cl_compile(std::string_view command) -> bool } auto invocations = split_invocations(std::span { words.data(), words.size() }); - return every_invocation_is_a_compile(std::span { invocations.data(), invocations.size() }); + return !compile_prefix(std::span { invocations.data(), invocations.size() }).empty(); } auto ClangClScanner::matches(CommandInfo const& cmd) const -> bool @@ -188,113 +197,120 @@ auto ClangClScanner::has_dep_flags(std::string_view cmd) const -> bool return false; } -auto ClangClScanner::build_dep_command(CommandInfo const& cmd) const -> std::optional +auto ClangClScanner::build_dep_scans(CommandInfo const& cmd) const -> Vec { auto& pool = global_pool(); auto words = command_words(pool.get(cmd.command)); if (words.empty()) { - return std::nullopt; + return {}; } + auto scans = Vec {}; auto invocations = split_invocations(std::span { words.data(), words.size() }); - if (!every_invocation_is_a_compile(std::span { invocations.data(), invocations.size() })) { - return std::nullopt; - } - - auto const first = invocations[0]; - auto driver_idx = driver_index(first); - if (!driver_idx) { - return std::nullopt; - } - - auto dep_cmd = Buf {}; - for (auto i = std::size_t { 0 }; i <= *driver_idx; ++i) { - if (i > 0) { - dep_cmd += ' '; - } - dep_cmd += first[i]; - } - - dep_cmd += " /clang:-M"; - auto pending = std::optional {}; - auto linker_tail = false; - auto redirected = false; - auto source_files = Vec {}; - for (auto i = *driver_idx + 1; i < first.size(); ++i) { - auto w = first[i]; - - // A redirection hands its target to this same invocation, so the words after it are not - // flags the scan may carry. - if (is_flag_barrier(w)) { - redirected = true; - pending.reset(); + for (auto invocation : compile_prefix(std::span { invocations.data(), invocations.size() })) { + auto driver_idx = driver_index(invocation); + if (!driver_idx) { continue; } - if (redirected) { - if (is_source_file(w)) { - source_files.push_back(w); + auto dep_cmd = Buf {}; + for (auto i = std::size_t { 0 }; i <= *driver_idx; ++i) { + if (i > 0) { + dep_cmd += ' '; } - continue; + dep_cmd += invocation[i]; } - if (pending) { - append_separate_arg_into(dep_cmd, w, *pending); - pending.reset(); - continue; - } + dep_cmd += " /clang:-M"; + + auto pending = std::optional {}; + auto redirected = false; + auto source_files = Vec {}; + auto object = std::string_view {}; + for (auto i = *driver_idx + 1; i < invocation.size(); ++i) { + auto w = invocation[i]; + + // A redirection hands its target to this same invocation, so the words after it are not + // flags the scan may carry. + if (is_flag_barrier(w)) { + redirected = true; + pending.reset(); + continue; + } - if (is_compile_flag(w)) { - continue; - } + if (redirected) { + if (is_source_file(w)) { + source_files.push_back(w); + } + continue; + } - if (w == "-o") { - ++i; - continue; - } + if (pending) { + append_separate_arg_into(dep_cmd, w, *pending); + pending.reset(); + continue; + } - // /link hands everything after it to the linker, source words included -- and the rest of - // the command with it, so no later invocation contributes one either. - if (w == "/link") { - linker_tail = true; - break; - } + if (is_compile_flag(w)) { + continue; + } - if (is_scan_hazard(w)) { - continue; - } + if (w == "-o") { + ++i; + if (i < invocation.size()) { + object = invocation[i]; + } + continue; + } - if (is_source_file(w)) { - source_files.push_back(w); - continue; - } + if (w.starts_with("/Fo") || w.starts_with("-Fo")) { + object = w.substr(3); + continue; + } - dep_cmd += ' '; - auto norm = Buf {}; - normalize_flag_path_into(norm, w); - shell_quote_into(dep_cmd, norm.view()); - pending = separate_arg(w); - } + if (w.starts_with("-o") && w.size() > 2) { + object = w.substr(2); + continue; + } + + // /link hands everything after it to the linker, source words included. + if (w == "/link") { + break; + } + + if (is_scan_hazard(w)) { + continue; + } - for (auto later = std::size_t { 1 }; !linker_tail && later < invocations.size(); ++later) { - for (auto w : invocations[later]) { if (is_source_file(w)) { source_files.push_back(w); + continue; } + + dep_cmd += ' '; + auto norm = Buf {}; + normalize_flag_path_into(norm, w); + shell_quote_into(dep_cmd, norm.view()); + pending = separate_arg(w); } - } - if (source_files.empty()) { - return std::nullopt; - } + if (source_files.empty()) { + continue; + } + + for (auto src : source_files) { + dep_cmd += ' '; + shell_quote_into(dep_cmd, src); + } - for (auto src : source_files) { - dep_cmd += ' '; - shell_quote_into(dep_cmd, src); + scans.push_back(DepScan { + .command = pool.intern(dep_cmd.view()), + .object = pool.intern(object), + }); } - return pool.intern(dep_cmd.view()); + return scans; } auto ClangClScanner::dep_spec() const -> DepSpec diff --git a/src/graph/scanners/gcc.cpp b/src/graph/scanners/gcc.cpp index 3b874b97..a669f176 100644 --- a/src/graph/scanners/gcc.cpp +++ b/src/graph/scanners/gcc.cpp @@ -121,17 +121,26 @@ auto compiler_index(std::span invocation) -> std::option return idx; } -/// A scan reproduces one invocation from the rule's directory, so it may carry a word only from a -/// command whose invocations are all compiles it recognizes (#356). -auto every_invocation_is_a_compile(std::span const> invocations) -> bool +auto is_recognized_compile(std::span invocation) -> bool { - return std::ranges::all_of(invocations, [](auto invocation) { - auto idx = compiler_index(invocation); - if (!idx) { - return false; - } - return std::ranges::any_of(invocation.subspan(*idx + 1), [](auto w) { return w == "-c"; }); - }); + auto idx = compiler_index(invocation); + if (!idx) { + return false; + } + return std::ranges::any_of(invocation.subspan(*idx + 1), [](auto w) { return w == "-c"; }); +} + +/// The leading invocations a scan can reproduce from the rule's directory: one that is not a +/// compile may change the directory or the environment, so it and everything after it are out of +/// reach (#356). +auto compile_prefix(std::span const> invocations) + -> std::span const> +{ + auto length = std::size_t { 0 }; + while (length < invocations.size() && is_recognized_compile(invocations[length])) { + ++length; + } + return invocations.first(length); } auto command_words(std::string_view command) -> Vec @@ -154,7 +163,7 @@ auto matches_gcc_compile(std::string_view command) -> bool } auto invocations = split_invocations(std::span { words.data(), words.size() }); - return every_invocation_is_a_compile(std::span { invocations.data(), invocations.size() }); + return !compile_prefix(std::span { invocations.data(), invocations.size() }).empty(); } auto GccScanner::matches(CommandInfo const& cmd) const -> bool @@ -184,106 +193,111 @@ auto GccScanner::has_dep_flags(std::string_view cmd) const -> bool return false; } -auto GccScanner::build_dep_command(CommandInfo const& cmd) const -> std::optional +auto GccScanner::build_dep_scans(CommandInfo const& cmd) const -> Vec { auto& pool = global_pool(); auto words = command_words(pool.get(cmd.command)); if (words.empty()) { - return std::nullopt; + return {}; } + auto scans = Vec {}; auto invocations = split_invocations(std::span { words.data(), words.size() }); - if (!every_invocation_is_a_compile(std::span { invocations.data(), invocations.size() })) { - return std::nullopt; - } - - auto const first = invocations[0]; - auto compiler_idx = compiler_index(first); - if (!compiler_idx) { - return std::nullopt; - } - auto dep_cmd = Buf {}; - - for (auto i = std::size_t { 0 }; i <= *compiler_idx; ++i) { - if (i > 0) { - dep_cmd += ' '; - } - dep_cmd += first[i]; - } - - dep_cmd += " -M"; - - auto pending = std::optional {}; - auto redirected = false; - auto source_files = Vec {}; - for (auto i = *compiler_idx + 1; i < first.size(); ++i) { - // A redirection hands its target to this same invocation, so the words after it are not - // flags the scan may carry. - if (is_flag_barrier(first[i])) { - redirected = true; - pending.reset(); + for (auto invocation : compile_prefix(std::span { invocations.data(), invocations.size() })) { + auto compiler_idx = compiler_index(invocation); + if (!compiler_idx) { continue; } - if (redirected) { - if (is_source_file(first[i])) { - source_files.push_back(first[i]); + auto dep_cmd = Buf {}; + + for (auto i = std::size_t { 0 }; i <= *compiler_idx; ++i) { + if (i > 0) { + dep_cmd += ' '; } - continue; + dep_cmd += invocation[i]; } - if (pending) { - append_separate_arg_into(dep_cmd, first[i], *pending); - pending.reset(); - continue; - } + dep_cmd += " -M"; + + auto pending = std::optional {}; + auto redirected = false; + auto source_files = Vec {}; + auto object = std::string_view {}; + for (auto i = *compiler_idx + 1; i < invocation.size(); ++i) { + // A redirection hands its target to this same invocation, so the words after it are not + // flags the scan may carry. + if (is_flag_barrier(invocation[i])) { + redirected = true; + pending.reset(); + continue; + } - auto w = first[i]; + if (redirected) { + if (is_source_file(invocation[i])) { + source_files.push_back(invocation[i]); + } + continue; + } - if (w == "-c") { - continue; - } + if (pending) { + append_separate_arg_into(dep_cmd, invocation[i], *pending); + pending.reset(); + continue; + } - if (w == "-o") { - ++i; - continue; - } + auto w = invocation[i]; - if (is_scan_hazard(w)) { - continue; - } + if (w == "-c") { + continue; + } - if (is_source_file(w)) { - source_files.push_back(w); - continue; - } + if (w == "-o") { + ++i; + if (i < invocation.size()) { + object = invocation[i]; + } + continue; + } - dep_cmd += ' '; - auto norm = Buf {}; - normalize_flag_path_into(norm, w); - shell_quote_into(dep_cmd, norm.view()); - pending = separate_arg(w); - } + if (w.starts_with("-o") && w.size() > 2) { + object = w.substr(2); + continue; + } + + if (is_scan_hazard(w)) { + continue; + } - for (auto later = std::size_t { 1 }; later < invocations.size(); ++later) { - for (auto w : invocations[later]) { if (is_source_file(w)) { source_files.push_back(w); + continue; } + + dep_cmd += ' '; + auto norm = Buf {}; + normalize_flag_path_into(norm, w); + shell_quote_into(dep_cmd, norm.view()); + pending = separate_arg(w); } - } - if (source_files.empty()) { - return std::nullopt; - } + if (source_files.empty()) { + continue; + } + + for (auto src : source_files) { + dep_cmd += ' '; + shell_quote_into(dep_cmd, src); + } - for (auto src : source_files) { - dep_cmd += ' '; - shell_quote_into(dep_cmd, src); + scans.push_back(DepScan { + .command = pool.intern(dep_cmd.view()), + .object = pool.intern(object), + }); } - return pool.intern(dep_cmd.view()); + return scans; } auto GccScanner::dep_spec() const -> DepSpec diff --git a/test/unit/test_dep_scanner.cpp b/test/unit/test_dep_scanner.cpp index aa476162..a816d9ac 100644 --- a/test/unit/test_dep_scanner.cpp +++ b/test/unit/test_dep_scanner.cpp @@ -33,6 +33,14 @@ auto path(std::string_view s) -> pup::PathId static auto paths = pup::PathPool {}; return paths.intern_path(s, pup::global_pool(), pup::PathId::BuildRoot); } + +/// The one scan a command is expected to produce, asserting that it produces exactly one. +auto only_scan(DepScanner const& scanner, CommandInfo const& cmd) -> DepScan +{ + auto scans = scanner.build_dep_scans(cmd); + REQUIRE(scans.size() == 1); + return scans[0]; +} } // namespace TEST_CASE("DepScannerRegistry basic operations", "[dep_scanner]") @@ -233,7 +241,7 @@ TEST_CASE("GccScanner interface", "[dep_scanner][gcc]") REQUIRE(!scanner.has_dep_flags("gcc -c foo.c -o foo.o")); } - SECTION("build_dep_command returns command") + SECTION("build_dep_scans returns the scan") { auto cmd = CommandInfo { .node_id = 4, @@ -245,12 +253,11 @@ TEST_CASE("GccScanner interface", "[dep_scanner][gcc]") .working_dir = intern("."), }; - auto dep_cmd = scanner.build_dep_command(cmd); - REQUIRE(dep_cmd.has_value()); - REQUIRE(*dep_cmd == intern("gcc -M foo.c")); + auto scan = only_scan(scanner, cmd); + REQUIRE(scan.command == intern("gcc -M foo.c")); } - SECTION("build_dep_command returns nullopt for empty command") + SECTION("build_dep_scans returns nothing for empty command") { auto cmd = CommandInfo { .node_id = 5, @@ -262,8 +269,8 @@ TEST_CASE("GccScanner interface", "[dep_scanner][gcc]") .working_dir = intern("."), }; - auto dep_cmd = scanner.build_dep_command(cmd); - REQUIRE(!dep_cmd.has_value()); + auto scans = scanner.build_dep_scans(cmd); + REQUIRE(scans.empty()); } } @@ -284,9 +291,8 @@ TEST_CASE("GccScanner compiler wrapper handling", "[dep_scanner][gcc]") }; REQUIRE(scanner.matches(cmd)); - auto dep_cmd = scanner.build_dep_command(cmd); - REQUIRE(dep_cmd.has_value()); - REQUIRE(*dep_cmd == intern("ccache gcc -M foo.c")); + auto scan = only_scan(scanner, cmd); + REQUIRE(scan.command == intern("ccache gcc -M foo.c")); } SECTION("handles distcc wrapper") @@ -301,9 +307,8 @@ TEST_CASE("GccScanner compiler wrapper handling", "[dep_scanner][gcc]") .working_dir = intern("."), }; - auto dep_cmd = scanner.build_dep_command(cmd); - REQUIRE(dep_cmd.has_value()); - REQUIRE(*dep_cmd == intern("distcc g++ -M foo.cpp")); + auto scan = only_scan(scanner, cmd); + REQUIRE(scan.command == intern("distcc g++ -M foo.cpp")); } } @@ -323,9 +328,8 @@ TEST_CASE("GccScanner flag preservation", "[dep_scanner][gcc]") .working_dir = intern("."), }; - auto dep_cmd = scanner.build_dep_command(cmd); - REQUIRE(dep_cmd.has_value()); - REQUIRE(*dep_cmd == intern("gcc -M -I../include -I/usr/local/include foo.c")); + auto scan = only_scan(scanner, cmd); + REQUIRE(scan.command == intern("gcc -M -I../include -I/usr/local/include foo.c")); } SECTION("preserves include paths (separate argument form)") @@ -340,9 +344,8 @@ TEST_CASE("GccScanner flag preservation", "[dep_scanner][gcc]") .working_dir = intern("."), }; - auto dep_cmd = scanner.build_dep_command(cmd); - REQUIRE(dep_cmd.has_value()); - REQUIRE(*dep_cmd == intern("gcc -M -I include -I ../lib foo.c")); + auto scan = only_scan(scanner, cmd); + REQUIRE(scan.command == intern("gcc -M -I include -I ../lib foo.c")); } SECTION("preserves defines") @@ -357,9 +360,8 @@ TEST_CASE("GccScanner flag preservation", "[dep_scanner][gcc]") .working_dir = intern("."), }; - auto dep_cmd = scanner.build_dep_command(cmd); - REQUIRE(dep_cmd.has_value()); - REQUIRE(*dep_cmd == intern("gcc -M -DNDEBUG -DFOO=bar foo.c")); + auto scan = only_scan(scanner, cmd); + REQUIRE(scan.command == intern("gcc -M -DNDEBUG -DFOO=bar foo.c")); } SECTION("preserves -std flag") @@ -374,9 +376,8 @@ TEST_CASE("GccScanner flag preservation", "[dep_scanner][gcc]") .working_dir = intern("."), }; - auto dep_cmd = scanner.build_dep_command(cmd); - REQUIRE(dep_cmd.has_value()); - REQUIRE(*dep_cmd == intern("g++ -M -std=c++20 foo.cpp")); + auto scan = only_scan(scanner, cmd); + REQUIRE(scan.command == intern("g++ -M -std=c++20 foo.cpp")); } SECTION("carries the compile's other flags") @@ -391,9 +392,8 @@ TEST_CASE("GccScanner flag preservation", "[dep_scanner][gcc]") .working_dir = intern("."), }; - auto dep_cmd = scanner.build_dep_command(cmd); - REQUIRE(dep_cmd.has_value()); - REQUIRE(*dep_cmd == intern("gcc -M -Wall -Wextra -O2 -g foo.c")); + auto scan = only_scan(scanner, cmd); + REQUIRE(scan.command == intern("gcc -M -Wall -Wextra -O2 -g foo.c")); } } @@ -401,7 +401,7 @@ TEST_CASE("GccScanner Objective-C support", "[dep_scanner][gcc]") { auto scanner = scanners::GccScanner {}; - SECTION("build_dep_command handles Objective-C files") + SECTION("build_dep_scans handles Objective-C files") { auto cmd = CommandInfo { .node_id = 1, @@ -413,12 +413,11 @@ TEST_CASE("GccScanner Objective-C support", "[dep_scanner][gcc]") .working_dir = intern("."), }; - auto dep_cmd = scanner.build_dep_command(cmd); - REQUIRE(dep_cmd.has_value()); - REQUIRE(*dep_cmd == intern("clang -M foo.m")); + auto scan = only_scan(scanner, cmd); + REQUIRE(scan.command == intern("clang -M foo.m")); } - SECTION("build_dep_command handles Objective-C++ files") + SECTION("build_dep_scans handles Objective-C++ files") { auto cmd = CommandInfo { .node_id = 2, @@ -430,9 +429,8 @@ TEST_CASE("GccScanner Objective-C support", "[dep_scanner][gcc]") .working_dir = intern("."), }; - auto dep_cmd = scanner.build_dep_command(cmd); - REQUIRE(dep_cmd.has_value()); - REQUIRE(*dep_cmd == intern("clang++ -M bar.mm")); + auto scan = only_scan(scanner, cmd); + REQUIRE(scan.command == intern("clang++ -M bar.mm")); } } @@ -452,8 +450,8 @@ TEST_CASE("GccScanner rejects compound shell commands", "[dep_scanner][gcc]") .working_dir = intern("."), }; - auto dep_cmd = scanner.build_dep_command(cmd); - REQUIRE(!dep_cmd.has_value()); + auto scans = scanner.build_dep_scans(cmd); + REQUIRE(scans.empty()); } SECTION("cd-and-compile compound command") @@ -468,8 +466,8 @@ TEST_CASE("GccScanner rejects compound shell commands", "[dep_scanner][gcc]") .working_dir = intern("."), }; - auto dep_cmd = scanner.build_dep_command(cmd); - REQUIRE(!dep_cmd.has_value()); + auto scans = scanner.build_dep_scans(cmd); + REQUIRE(scans.empty()); } SECTION("env-var assignment before compiler") @@ -484,8 +482,8 @@ TEST_CASE("GccScanner rejects compound shell commands", "[dep_scanner][gcc]") .working_dir = intern("."), }; - auto dep_cmd = scanner.build_dep_command(cmd); - REQUIRE(!dep_cmd.has_value()); + auto scans = scanner.build_dep_scans(cmd); + REQUIRE(scans.empty()); } } @@ -506,7 +504,7 @@ TEST_CASE("GccScanner skips commands with no visible source", "[dep_scanner][gcc }; REQUIRE(scanner.matches(cmd)); - REQUIRE(!scanner.build_dep_command(cmd).has_value()); + REQUIRE(scanner.build_dep_scans(cmd).empty()); } } @@ -609,70 +607,64 @@ TEST_CASE("ClangClScanner dep command construction", "[dep_scanner][clang_cl]") SECTION("carries compile flags and drops output paths") { - auto dep_cmd = scanner.build_dep_command( + auto scan = only_scan(scanner, clang_cl_compile(1, "clang-cl /W3 /WX /O2 /GR- /utf-8 /MT -c foo.cpp -o foo.obj") ); - REQUIRE(dep_cmd.has_value()); - REQUIRE(*dep_cmd == intern("clang-cl /clang:-M /W3 /WX /O2 /GR- /utf-8 /MT foo.cpp")); + REQUIRE(scan.command == intern("clang-cl /clang:-M /W3 /WX /O2 /GR- /utf-8 /MT foo.cpp")); } SECTION("drops cl-spelled /c and /Fo") { - auto dep_cmd = scanner.build_dep_command(clang_cl_compile(2, "clang-cl /c foo.cpp /Fofoo.obj")); - REQUIRE(dep_cmd.has_value()); - REQUIRE(*dep_cmd == intern("clang-cl /clang:-M foo.cpp")); + auto scan = only_scan(scanner, clang_cl_compile(2, "clang-cl /c foo.cpp /Fofoo.obj")); + REQUIRE(scan.command == intern("clang-cl /clang:-M foo.cpp")); } SECTION("preserves include, define and standard flags in both spellings") { - auto dep_cmd = scanner.build_dep_command(clang_cl_compile( + auto scan = only_scan(scanner, clang_cl_compile( 3, "clang-cl /std:c++latest -Isrc /I../include -DUNICODE /DFOO=bar -c foo.cpp -o foo.obj" )); - REQUIRE(dep_cmd.has_value()); REQUIRE( - *dep_cmd == intern("clang-cl /clang:-M /std:c++latest -Isrc /I../include -DUNICODE /DFOO=bar foo.cpp") + scan.command == intern("clang-cl /clang:-M /std:c++latest -Isrc /I../include -DUNICODE /DFOO=bar foo.cpp") ); } SECTION("preserves separate-argument system include paths") { - auto dep_cmd = scanner.build_dep_command( + auto scan = only_scan(scanner, clang_cl_compile(4, "clang-cl /imsvc /sdk/crt/include /imsvc /sdk/um -c foo.cpp -o foo.obj") ); - REQUIRE(dep_cmd.has_value()); - REQUIRE(*dep_cmd == intern("clang-cl /clang:-M /imsvc /sdk/crt/include /imsvc /sdk/um foo.cpp")); + REQUIRE(scan.command == intern("clang-cl /clang:-M /imsvc /sdk/crt/include /imsvc /sdk/um foo.cpp")); } SECTION("preserves target triple and force-includes") { - auto dep_cmd = scanner.build_dep_command(clang_cl_compile( + auto scan = only_scan(scanner, clang_cl_compile( 5, "clang-cl --target=x86_64-pc-windows-msvc /FI build/version.h -c foo.cpp -o foo.obj" )); - REQUIRE(dep_cmd.has_value()); REQUIRE( - *dep_cmd == intern("clang-cl /clang:-M --target=x86_64-pc-windows-msvc /FI build/version.h foo.cpp") + scan.command == intern("clang-cl /clang:-M --target=x86_64-pc-windows-msvc /FI build/version.h foo.cpp") ); } SECTION("returns nullopt for a compound shell command") { - auto dep_cmd = scanner.build_dep_command(clang_cl_compile(6, "cd build && clang-cl -c foo.cpp -o foo.obj")); - REQUIRE(!dep_cmd.has_value()); + auto scans = scanner.build_dep_scans(clang_cl_compile(6, "cd build && clang-cl -c foo.cpp -o foo.obj")); + REQUIRE(scans.empty()); } SECTION("drops a smuggled -MD that would redirect the depfile") { - auto dep_cmd = scanner.build_dep_command( + auto scan = only_scan(scanner, clang_cl_compile(8, "clang-cl /clang:-MD -Isrc -c foo.cpp -o foo.obj") ); - REQUIRE(dep_cmd.has_value()); - REQUIRE(*dep_cmd == intern("clang-cl /clang:-M -Isrc foo.cpp")); + REQUIRE(scan.command == intern("clang-cl /clang:-M -Isrc foo.cpp")); } SECTION("returns nullopt when no source word is visible") { - auto dep_cmd = scanner.build_dep_command(clang_cl_compile(7, "clang-cl @srcs.rsp -c -o foo.obj")); - REQUIRE(!dep_cmd.has_value()); + auto scans = scanner.build_dep_scans(clang_cl_compile(7, "clang-cl @srcs.rsp -c -o foo.obj")); + REQUIRE(scans.empty()); } } @@ -881,12 +873,11 @@ TEST_CASE("shell_quote_into round-trips through its target shell", "[dep_scanner TEST_CASE("ClangClScanner keeps a spaced include path in one argument", "[dep_scanner][clang_cl]") { auto scanner = scanners::ClangClScanner {}; - auto dep_cmd = scanner.build_dep_command(clang_cl_compile( + auto scan = only_scan(scanner, clang_cl_compile( 9, "clang-cl /imsvc \"C:/Program Files/LLVM/lib/clang/20/include\" -c foo.cpp -o foo.obj" )); - REQUIRE(dep_cmd.has_value()); - auto command = pup::global_pool().get(*dep_cmd); + auto command = pup::global_pool().get(scan.command); auto words = pup::test::split_for_host(command); INFO("dep command: " << command); @@ -959,10 +950,9 @@ auto gcc_compile(pup::NodeId id, std::string_view command) -> CommandInfo }; } -auto scan_words(std::optional dep_cmd) -> std::vector +auto scan_words(DepScan const& scan) -> std::vector { - REQUIRE(dep_cmd.has_value()); - auto words = pup::test::split_for_host(pup::global_pool().get(*dep_cmd)); + auto words = pup::test::split_for_host(pup::global_pool().get(scan.command)); REQUIRE(words.has_value()); return *words; } @@ -975,23 +965,23 @@ auto check_scanners_normalize(std::string const& p) -> void auto gcc = scanners::GccScanner {}; REQUIRE( - scan_words(gcc.build_dep_command(gcc_compile(30, "gcc -I" + p + " -c foo.c -o foo.o"))) + scan_words(only_scan(gcc, gcc_compile(30, "gcc -I" + p + " -c foo.c -o foo.o"))) == std::vector { "gcc", "-M", "-I" + want, "foo.c" } ); REQUIRE( - scan_words(gcc.build_dep_command(gcc_compile(32, "gcc -I " + p + " -c foo.c -o foo.o"))) + scan_words(only_scan(gcc, gcc_compile(32, "gcc -I " + p + " -c foo.c -o foo.o"))) == std::vector { "gcc", "-M", "-I", want, "foo.c" } ); auto clang_cl = scanners::ClangClScanner {}; REQUIRE( - scan_words(clang_cl.build_dep_command( + scan_words(only_scan(clang_cl, clang_cl_compile(31, "clang-cl /imsvc " + p + " -c foo.cpp -o foo.obj") )) == std::vector { "clang-cl", "/clang:-M", "/imsvc", want, "foo.cpp" } ); REQUIRE( - scan_words(clang_cl.build_dep_command( + scan_words(only_scan(clang_cl, clang_cl_compile(33, "clang-cl -I" + p + " -c foo.cpp -o foo.obj") )) == std::vector { "clang-cl", "/clang:-M", "-I" + want, "foo.cpp" } @@ -1006,23 +996,23 @@ auto check_scanners_normalize_sysroot(std::string const& p) -> void auto gcc = scanners::GccScanner {}; REQUIRE( - scan_words(gcc.build_dep_command(gcc_compile(44, "gcc --sysroot " + p + " -c foo.c -o foo.o"))) + scan_words(only_scan(gcc, gcc_compile(44, "gcc --sysroot " + p + " -c foo.c -o foo.o"))) == std::vector { "gcc", "-M", "--sysroot", want, "foo.c" } ); REQUIRE( - scan_words(gcc.build_dep_command(gcc_compile(45, "gcc --sysroot=" + p + " -c foo.c -o foo.o"))) + scan_words(only_scan(gcc, gcc_compile(45, "gcc --sysroot=" + p + " -c foo.c -o foo.o"))) == std::vector { "gcc", "-M", "--sysroot=" + want, "foo.c" } ); auto clang_cl = scanners::ClangClScanner {}; REQUIRE( - scan_words(clang_cl.build_dep_command( + scan_words(only_scan(clang_cl, clang_cl_compile(46, "clang-cl --sysroot " + p + " -c foo.cpp -o foo.obj") )) == std::vector { "clang-cl", "/clang:-M", "--sysroot", want, "foo.cpp" } ); REQUIRE( - scan_words(clang_cl.build_dep_command( + scan_words(only_scan(clang_cl, clang_cl_compile(47, "clang-cl --sysroot=" + p + " -c foo.cpp -o foo.obj") )) == std::vector { "clang-cl", "/clang:-M", "--sysroot=" + want, "foo.cpp" } @@ -1037,13 +1027,13 @@ auto check_scanners_pass_values_through(std::string const& p) -> void auto gcc = scanners::GccScanner {}; REQUIRE( - scan_words(gcc.build_dep_command(gcc_compile(34, "gcc -D " + want + " -c foo.c -o foo.o"))) + scan_words(only_scan(gcc, gcc_compile(34, "gcc -D " + want + " -c foo.c -o foo.o"))) == std::vector { "gcc", "-M", "-D", want, "foo.c" } ); auto clang_cl = scanners::ClangClScanner {}; REQUIRE( - scan_words(clang_cl.build_dep_command( + scan_words(only_scan(clang_cl, clang_cl_compile(35, "clang-cl /D " + want + " -c foo.cpp -o foo.obj") )) == std::vector { "clang-cl", "/clang:-M", "/D", want, "foo.cpp" } @@ -1053,13 +1043,13 @@ auto check_scanners_pass_values_through(std::string const& p) -> void auto gcc_scan_words(std::string const& command) -> std::vector { auto scanner = scanners::GccScanner {}; - return scan_words(scanner.build_dep_command(gcc_compile(50, command))); + return scan_words(only_scan(scanner, gcc_compile(50, command))); } auto clang_cl_scan_words(std::string const& command) -> std::vector { auto scanner = scanners::ClangClScanner {}; - return scan_words(scanner.build_dep_command(clang_cl_compile(51, command))); + return scan_words(only_scan(scanner, clang_cl_compile(51, command))); } struct WalkTarget { @@ -1119,7 +1109,7 @@ auto check_scan_forwards_the_compile(std::string const& p) -> void auto gcc = scanners::GccScanner {}; REQUIRE( - scan_words(gcc.build_dep_command( + scan_words(only_scan(gcc, gcc_compile(54, "gcc -O2 -MD -I" + p + " -Wall -c foo.c -o foo.o") )) == std::vector { "gcc", "-M", "-O2", "-I" + want, "-Wall", "foo.c" } @@ -1127,7 +1117,7 @@ auto check_scan_forwards_the_compile(std::string const& p) -> void auto clang_cl = scanners::ClangClScanner {}; REQUIRE( - scan_words(clang_cl.build_dep_command( + scan_words(only_scan(clang_cl, clang_cl_compile(55, "clang-cl /O2 /showIncludes /imsvc " + p + " /W4 -c foo.cpp -o foo.obj") )) == std::vector { "clang-cl", "/clang:-M", "/O2", "/imsvc", want, "/W4", "foo.cpp" } @@ -1157,10 +1147,8 @@ auto check_under_root(std::string_view root, void (*check)(std::string const&)) TEST_CASE("GccScanner resolves a root-escaping include path", "[dep_scanner][gcc]") { auto scanner = scanners::GccScanner {}; - auto dep_cmd = scanner.build_dep_command(gcc_compile(20, "gcc -I/a/../.. -c foo.c -o foo.o")); - - REQUIRE(dep_cmd.has_value()); - REQUIRE(pup::global_pool().get(*dep_cmd) == "gcc -M -I/ foo.c"); + auto scan = only_scan(scanner, gcc_compile(20, "gcc -I/a/../.. -c foo.c -o foo.o")); + REQUIRE(pup::global_pool().get(scan.command) == "gcc -M -I/ foo.c"); } TEST_CASE("a scanned path flag says what path::normalize says", "[dep_scanner]") @@ -1202,28 +1190,24 @@ TEST_CASE("a scanned value flag passes its word through", "[dep_scanner]") TEST_CASE("GccScanner passes a separate-word macro definition through", "[dep_scanner][gcc]") { auto scanner = scanners::GccScanner {}; - auto dep_cmd = scanner.build_dep_command(gcc_compile(36, "gcc -D FOO=a/../b -c foo.c -o foo.o")); - - REQUIRE(dep_cmd.has_value()); - REQUIRE(pup::global_pool().get(*dep_cmd) == "gcc -M -D FOO=a/../b foo.c"); + auto scan = only_scan(scanner, gcc_compile(36, "gcc -D FOO=a/../b -c foo.c -o foo.o")); + REQUIRE(pup::global_pool().get(scan.command) == "gcc -M -D FOO=a/../b foo.c"); } TEST_CASE("GccScanner keeps a macro value that is only a slash", "[dep_scanner][gcc]") { auto scanner = scanners::GccScanner {}; - auto dep_cmd = scanner.build_dep_command(gcc_compile(37, "gcc -D ROOT=/ -c foo.c -o foo.o")); - - REQUIRE(dep_cmd.has_value()); - REQUIRE(pup::global_pool().get(*dep_cmd) == "gcc -M -D ROOT=/ foo.c"); + auto scan = only_scan(scanner, gcc_compile(37, "gcc -D ROOT=/ -c foo.c -o foo.o")); + REQUIRE(pup::global_pool().get(scan.command) == "gcc -M -D ROOT=/ foo.c"); } TEST_CASE("a macro value needing quotes survives the scan command", "[dep_scanner][gcc]") { auto scanner = scanners::GccScanner {}; - auto dep_cmd = scanner.build_dep_command(gcc_compile(38, R"(gcc -D "P=a b/../c" -c foo.c -o foo.o)")); + auto scan = only_scan(scanner, gcc_compile(38, R"(gcc -D "P=a b/../c" -c foo.c -o foo.o)")); REQUIRE( - scan_words(dep_cmd) == std::vector { "gcc", "-M", "-D", "P=a b/../c", "foo.c" } + scan_words(scan) == std::vector { "gcc", "-M", "-D", "P=a b/../c", "foo.c" } ); } @@ -1232,19 +1216,19 @@ TEST_CASE("ClangClScanner passes separate-word macro words through", "[dep_scann auto scanner = scanners::ClangClScanner {}; REQUIRE( - scan_words(scanner.build_dep_command(clang_cl_compile(39, "clang-cl /D FOO=a/../b -c foo.cpp -o foo.obj"))) + scan_words(only_scan(scanner, clang_cl_compile(39, "clang-cl /D FOO=a/../b -c foo.cpp -o foo.obj"))) == std::vector { "clang-cl", "/clang:-M", "/D", "FOO=a/../b", "foo.cpp" } ); REQUIRE( - scan_words(scanner.build_dep_command(clang_cl_compile(40, "clang-cl -D FOO=a/../b -c foo.cpp -o foo.obj"))) + scan_words(only_scan(scanner, clang_cl_compile(40, "clang-cl -D FOO=a/../b -c foo.cpp -o foo.obj"))) == std::vector { "clang-cl", "/clang:-M", "-D", "FOO=a/../b", "foo.cpp" } ); REQUIRE( - scan_words(scanner.build_dep_command(clang_cl_compile(41, "clang-cl /U FOO -c foo.cpp -o foo.obj"))) + scan_words(only_scan(scanner, clang_cl_compile(41, "clang-cl /U FOO -c foo.cpp -o foo.obj"))) == std::vector { "clang-cl", "/clang:-M", "/U", "FOO", "foo.cpp" } ); REQUIRE( - scan_words(scanner.build_dep_command(clang_cl_compile(42, "clang-cl -U FOO -c foo.cpp -o foo.obj"))) + scan_words(only_scan(scanner, clang_cl_compile(42, "clang-cl -U FOO -c foo.cpp -o foo.obj"))) == std::vector { "clang-cl", "/clang:-M", "-U", "FOO", "foo.cpp" } ); } @@ -1294,53 +1278,86 @@ TEST_CASE("a scan is the compile without the words that would break it", "[dep_s } } -TEST_CASE("GccScanner takes flags from the invocation it scans, sources from all of them", "[dep_scanner][gcc]") +TEST_CASE("GccScanner scans each compile of an all-compile command with its own flags", "[dep_scanner][gcc]") { auto scanner = scanners::GccScanner {}; - auto dep_cmd = scanner.build_dep_command( + auto scans = scanner.build_dep_scans( gcc_compile(57, "gcc -O2 -c a.c -o a.o && gcc -c b.c -o b.o") ); - REQUIRE(dep_cmd.has_value()); - REQUIRE(pup::global_pool().get(*dep_cmd) == "gcc -M -O2 a.c b.c"); + REQUIRE(scans.size() == 2); + REQUIRE(pup::global_pool().get(scans[0].command) == "gcc -M -O2 a.c"); + REQUIRE(pup::global_pool().get(scans[0].object) == "a.o"); + REQUIRE(pup::global_pool().get(scans[1].command) == "gcc -M b.c"); + REQUIRE(pup::global_pool().get(scans[1].object) == "b.o"); } -TEST_CASE("GccScanner refuses a command whose later invocation changes directory", "[dep_scanner][gcc]") +TEST_CASE("GccScanner scans the prefix before a directory change", "[dep_scanner][gcc]") { auto scanner = scanners::GccScanner {}; - auto dep_cmd = scanner.build_dep_command( + auto scans = scanner.build_dep_scans( gcc_compile(70, "gcc -c a.c -o a.o && cd sub && gcc -c b.c -o b.o") ); - REQUIRE(!dep_cmd.has_value()); + REQUIRE(scans.size() == 1); + REQUIRE(pup::global_pool().get(scans[0].command) == "gcc -M a.c"); + REQUIRE(pup::global_pool().get(scans[0].object) == "a.o"); } -TEST_CASE("GccScanner refuses a command whose later invocation is not a compile", "[dep_scanner][gcc]") +TEST_CASE("GccScanner scans the prefix before an invocation that is not a compile", "[dep_scanner][gcc]") { auto scanner = scanners::GccScanner {}; SECTION("a later invocation that deletes a file contributes no source") { - auto dep_cmd = scanner.build_dep_command(gcc_compile(71, "gcc -c a.c -o a.o && rm junk.c")); - REQUIRE(!dep_cmd.has_value()); + auto scans = scanner.build_dep_scans(gcc_compile(71, "gcc -c a.c -o a.o && rm junk.c")); + REQUIRE(scans.size() == 1); + REQUIRE(pup::global_pool().get(scans[0].command) == "gcc -M a.c"); } SECTION("a later invocation that copies files contributes neither operand") { - auto dep_cmd = scanner.build_dep_command(gcc_compile(72, "gcc -c a.c -o a.o && cp b.c x.c")); - REQUIRE(!dep_cmd.has_value()); + auto scans = scanner.build_dep_scans(gcc_compile(72, "gcc -c a.c -o a.o && cp b.c x.c")); + REQUIRE(scans.size() == 1); + REQUIRE(pup::global_pool().get(scans[0].command) == "gcc -M a.c"); + } +} + +TEST_CASE("A scan reads the object from either spelling of the output flag", "[dep_scanner]") +{ + SECTION("gcc's joined form") + { + auto scanner = scanners::GccScanner {}; + auto scan = only_scan(scanner, gcc_compile(76, "gcc -c a.c -oa.o")); + REQUIRE(pup::global_pool().get(scan.command) == "gcc -M a.c"); + REQUIRE(pup::global_pool().get(scan.object) == "a.o"); + } + + SECTION("clang-cl's joined form") + { + auto scanner = scanners::ClangClScanner {}; + auto scan = only_scan(scanner, clang_cl_compile(77, "clang-cl -c a.cpp -oa.obj")); + REQUIRE(pup::global_pool().get(scan.command) == "clang-cl /clang:-M a.cpp"); + REQUIRE(pup::global_pool().get(scan.object) == "a.obj"); } } +TEST_CASE("A command whose first invocation is not a compile is scanned nowhere", "[dep_scanner][gcc]") +{ + // The bound that keeps the prefix rule from becoming scan-everything: an empty prefix, so no + // later compile is reproducible from the rule's directory either. + auto scanner = scanners::GccScanner {}; + REQUIRE(scanner.build_dep_scans(gcc_compile(75, "cd sub && gcc -c a.c -o a.o")).empty()); +} + TEST_CASE("A separator with nothing after it begins no invocation", "[dep_scanner][gcc]") { auto scanner = scanners::GccScanner {}; SECTION("a trailing terminator") { - auto dep_cmd = scanner.build_dep_command(gcc_compile(74, "gcc -c a.c -o a.o ;")); - REQUIRE(dep_cmd.has_value()); - REQUIRE(pup::global_pool().get(*dep_cmd) == "gcc -M a.c"); + auto scan = only_scan(scanner, gcc_compile(74, "gcc -c a.c -o a.o ;")); + REQUIRE(pup::global_pool().get(scan.command) == "gcc -M a.c"); } SECTION("a trailing backgrounding operator") @@ -1354,31 +1371,35 @@ TEST_CASE("A separator with nothing after it begins no invocation", "[dep_scanne } } -TEST_CASE("matches_gcc_compile refuses a command whose later invocation is not a compile", "[dep_scanner][gcc]") +TEST_CASE("matches_gcc_compile accepts a command whose prefix compiles", "[dep_scanner][gcc]") { // The diagnostic consumes the matcher and the scan consumes the builder; a rule they answer // differently about is a rule reported as covered and scanned wrongly. - REQUIRE(!scanners::matches_gcc_compile("gcc -c a.c -o a.o && rm junk.c")); + REQUIRE(scanners::matches_gcc_compile("gcc -c a.c -o a.o && rm junk.c")); } -TEST_CASE("matches_gcc_compile refuses a link whose later invocation compiles", "[dep_scanner][gcc]") +TEST_CASE("matches_gcc_compile refuses a command whose first invocation is not a compile", "[dep_scanner][gcc]") { + // An empty prefix, so nothing downstream of it is reproducible -- the bound that keeps the + // prefix rule from becoming scan-everything. REQUIRE(!scanners::matches_gcc_compile("gcc -o prog main.c && gcc -c helper.c")); } -TEST_CASE("matches_clang_cl_compile refuses a link whose later invocation compiles", "[dep_scanner][clang_cl]") +TEST_CASE("matches_clang_cl_compile refuses a command whose first invocation is not a compile", "[dep_scanner][clang_cl]") { REQUIRE(!scanners::matches_clang_cl_compile("clang-cl foo.obj -o foo.exe && clang-cl -c bar.cpp")); } -TEST_CASE("ClangClScanner refuses a later invocation that is not a compile", "[dep_scanner][clang_cl]") +TEST_CASE("ClangClScanner scans the prefix before an invocation that is not a compile", "[dep_scanner][clang_cl]") { auto scanner = scanners::ClangClScanner {}; - auto dep_cmd = scanner.build_dep_command( + auto scans = scanner.build_dep_scans( clang_cl_compile(73, "clang-cl -c a.cpp -o a.obj && cd sub && clang-cl -c b.cpp -o b.obj") ); - REQUIRE(!dep_cmd.has_value()); + REQUIRE(scans.size() == 1); + REQUIRE(pup::global_pool().get(scans[0].command) == "clang-cl /clang:-M a.cpp"); + REQUIRE(pup::global_pool().get(scans[0].object) == "a.obj"); } TEST_CASE("a line continuation never reaches the scan command", "[dep_scanner]") @@ -1386,31 +1407,28 @@ TEST_CASE("a line continuation never reaches the scan command", "[dep_scanner]") SECTION("a continuation the command text kept is not a word the scan carries") { auto scanner = scanners::GccScanner {}; - auto dep_cmd = scanner.build_dep_command( + auto scan = only_scan(scanner, gcc_compile(60, "gcc -O2 \n -DFOO=1 -c foo.c -o foo.o \n ") ); - REQUIRE(dep_cmd.has_value()); - REQUIRE(pup::global_pool().get(*dep_cmd) == "gcc -M -O2 -DFOO=1 foo.c"); + REQUIRE(pup::global_pool().get(scan.command) == "gcc -M -O2 -DFOO=1 foo.c"); } SECTION("a newline inside a word survives the shell that runs the scan") { auto scanner = scanners::GccScanner {}; - auto dep_cmd = scanner.build_dep_command(gcc_compile(61, "gcc -DA=a\nb -c foo.c -o foo.o")); - REQUIRE(dep_cmd.has_value()); + auto scan = only_scan(scanner, gcc_compile(61, "gcc -DA=a\nb -c foo.c -o foo.o")); REQUIRE( - scan_words(dep_cmd) == std::vector { "gcc", "-M", "-DA=a\nb", "foo.c" } + scan_words(scan) == std::vector { "gcc", "-M", "-DA=a\nb", "foo.c" } ); } SECTION("a carriage return from a Windows-authored Tupfile") { auto scanner = scanners::ClangClScanner {}; - auto dep_cmd = scanner.build_dep_command( + auto scan = only_scan(scanner, clang_cl_compile(62, "clang-cl /O2 \r\n /I inc -c foo.cpp -o foo.obj") ); - REQUIRE(dep_cmd.has_value()); - REQUIRE(pup::global_pool().get(*dep_cmd) == "clang-cl /clang:-M /O2 /I inc foo.cpp"); + REQUIRE(pup::global_pool().get(scan.command) == "clang-cl /clang:-M /O2 /I inc foo.cpp"); } } @@ -1420,65 +1438,53 @@ TEST_CASE("GccScanner takes no flags from a redirection", "[dep_scanner][gcc]") SECTION("a numbered file descriptor") { - auto dep_cmd = scanner.build_dep_command(gcc_compile(58, "gcc -O2 -c foo.c -o foo.o 1> build.log")); - REQUIRE(dep_cmd.has_value()); - REQUIRE(pup::global_pool().get(*dep_cmd) == "gcc -M -O2 foo.c"); + auto scan = only_scan(scanner, gcc_compile(58, "gcc -O2 -c foo.c -o foo.o 1> build.log")); + REQUIRE(pup::global_pool().get(scan.command) == "gcc -M -O2 foo.c"); } SECTION("a duplicated descriptor") { - auto dep_cmd = scanner.build_dep_command(gcc_compile(59, "gcc -O2 -c foo.c -o foo.o 2>&1")); - REQUIRE(dep_cmd.has_value()); - REQUIRE(pup::global_pool().get(*dep_cmd) == "gcc -M -O2 foo.c"); + auto scan = only_scan(scanner, gcc_compile(59, "gcc -O2 -c foo.c -o foo.o 2>&1")); + REQUIRE(pup::global_pool().get(scan.command) == "gcc -M -O2 foo.c"); } } TEST_CASE("ClangClScanner stops at the linker's share of the command", "[dep_scanner][clang_cl]") { auto scanner = scanners::ClangClScanner {}; - auto dep_cmd = scanner.build_dep_command( + auto scan = only_scan(scanner, clang_cl_compile(56, "clang-cl /I inc -c foo.cpp -o foo.obj /link /LIBPATH:C:/lib") ); - - REQUIRE(dep_cmd.has_value()); - REQUIRE(pup::global_pool().get(*dep_cmd) == "clang-cl /clang:-M /I inc foo.cpp"); + REQUIRE(pup::global_pool().get(scan.command) == "clang-cl /clang:-M /I inc foo.cpp"); } TEST_CASE("GccScanner drops a macro the shell would expand and scans anyway", "[dep_scanner][gcc]") { auto scanner = scanners::GccScanner {}; - auto dep_cmd = scanner.build_dep_command(gcc_compile(52, "gcc -DPATH=$(prefix)/share -c foo.c -o foo.o")); - - REQUIRE(dep_cmd.has_value()); - REQUIRE(pup::global_pool().get(*dep_cmd) == "gcc -M foo.c"); + auto scan = only_scan(scanner, gcc_compile(52, "gcc -DPATH=$(prefix)/share -c foo.c -o foo.o")); + REQUIRE(pup::global_pool().get(scan.command) == "gcc -M foo.c"); } TEST_CASE("GccScanner carries the compile's other flags into the scan", "[dep_scanner][gcc]") { auto scanner = scanners::GccScanner {}; - auto dep_cmd = scanner.build_dep_command(gcc_compile(53, "gcc -O2 -Wall -Iinc -c foo.c -o foo.o")); - - REQUIRE(dep_cmd.has_value()); - REQUIRE(pup::global_pool().get(*dep_cmd) == "gcc -M -O2 -Wall -Iinc foo.c"); + auto scan = only_scan(scanner, gcc_compile(53, "gcc -O2 -Wall -Iinc -c foo.c -o foo.o")); + REQUIRE(pup::global_pool().get(scan.command) == "gcc -M -O2 -Wall -Iinc foo.c"); } TEST_CASE("GccScanner keeps a separate-word sysroot path", "[dep_scanner][gcc]") { auto scanner = scanners::GccScanner {}; - auto dep_cmd = scanner.build_dep_command(gcc_compile(48, "gcc --sysroot /opt/sys/../sys -c foo.c -o foo.o")); - - REQUIRE(dep_cmd.has_value()); - REQUIRE(pup::global_pool().get(*dep_cmd) == "gcc -M --sysroot /opt/sys foo.c"); + auto scan = only_scan(scanner, gcc_compile(48, "gcc --sysroot /opt/sys/../sys -c foo.c -o foo.o")); + REQUIRE(pup::global_pool().get(scan.command) == "gcc -M --sysroot /opt/sys foo.c"); } // gcc takes the word after --sysroot whatever it is, so the scan mirrors that greed. TEST_CASE("GccScanner lets a value-less sysroot take the next word", "[dep_scanner][gcc]") { auto scanner = scanners::GccScanner {}; - auto dep_cmd = scanner.build_dep_command(gcc_compile(49, "gcc --sysroot -c foo.c -o foo.o")); - - REQUIRE(dep_cmd.has_value()); - REQUIRE(pup::global_pool().get(*dep_cmd) == "gcc -M --sysroot -c foo.c"); + auto scan = only_scan(scanner, gcc_compile(49, "gcc --sysroot -c foo.c -o foo.o")); + REQUIRE(pup::global_pool().get(scan.command) == "gcc -M --sysroot -c foo.c"); } TEST_CASE("a scanned sysroot flag says what path::normalize says", "[dep_scanner]") @@ -1502,10 +1508,8 @@ TEST_CASE("a scanned sysroot flag says what path::normalize says", "[dep_scanner TEST_CASE("GccScanner keeps a separate-word undefine", "[dep_scanner][gcc]") { auto scanner = scanners::GccScanner {}; - auto dep_cmd = scanner.build_dep_command(gcc_compile(43, "gcc -U FOO -c foo.c -o foo.o")); - - REQUIRE(dep_cmd.has_value()); - REQUIRE(pup::global_pool().get(*dep_cmd) == "gcc -M -U FOO foo.c"); + auto scan = only_scan(scanner, gcc_compile(43, "gcc -U FOO -c foo.c -o foo.o")); + REQUIRE(pup::global_pool().get(scan.command) == "gcc -M -U FOO foo.c"); } #ifdef _WIN32 @@ -1516,18 +1520,18 @@ TEST_CASE("ClangClScanner keeps the drive root in a scanned flag", "[dep_scanner SECTION("dotdot cannot escape the drive") { - auto dep_cmd = scanner.build_dep_command(clang_cl_compile(21, "clang-cl /imsvc C:/.. -c foo.cpp -o foo.obj")); + auto scan = only_scan(scanner, clang_cl_compile(21, "clang-cl /imsvc C:/.. -c foo.cpp -o foo.obj")); REQUIRE( - scan_words(dep_cmd) + scan_words(scan) == std::vector { "clang-cl", "/clang:-M", "/imsvc", "C:/", "foo.cpp" } ); } SECTION("the drive root stays rooted") { - auto dep_cmd = scanner.build_dep_command(clang_cl_compile(22, "clang-cl /imsvc C:/ -c foo.cpp -o foo.obj")); + auto scan = only_scan(scanner, clang_cl_compile(22, "clang-cl /imsvc C:/ -c foo.cpp -o foo.obj")); REQUIRE( - scan_words(dep_cmd) + scan_words(scan) == std::vector { "clang-cl", "/clang:-M", "/imsvc", "C:/", "foo.cpp" } ); } diff --git a/test/unit/test_e2e.cpp b/test/unit/test_e2e.cpp index 3adf38c5..095851e4 100644 --- a/test/unit/test_e2e.cpp +++ b/test/unit/test_e2e.cpp @@ -11114,7 +11114,7 @@ SCENARIO("Check level controls convention enforcement", "[e2e][strict]") } } -SCENARIO("A rule whose second compile runs elsewhere is reported instead of scanned wrongly", "[e2e][strict][depscan]") +SCENARIO("The object of a compile that runs elsewhere is reported instead of scanned wrongly", "[e2e][strict][depscan]") { // The scan runs from the Tupfile's directory, so a source word taken from an invocation that // ran in sub/ resolves against a same-named file here -- deps recorded for a file the rule @@ -11136,11 +11136,12 @@ SCENARIO("A rule whose second compile runs elsewhere is reported instead of scan { auto result = f.pup({ "parse" }); - THEN("it names the object rather than leaving the rule looking covered") + THEN("it names the unreachable object and leaves the reproducible one scanned") { INFO("stderr: " << result.stderr_output); REQUIRE(result.stderr_output.find("no dependency scan") != std::string::npos); - REQUIRE(result.stderr_output.find("a.o") != std::string::npos); + REQUIRE(result.stderr_output.find("sub/b.o") != std::string::npos); + REQUIRE(result.stderr_output.find("'a.o'") == std::string::npos); } } @@ -11159,6 +11160,74 @@ SCENARIO("A rule whose second compile runs elsewhere is reported instead of scan } } +SCENARIO("An object no scanned invocation writes is reported beside its scanned sibling", "[e2e][strict][depscan]") +{ + GIVEN("a rule whose compile prefix covers one declared object and whose tail covers the other") + { + auto f = E2EFixture { "glob_mixed_space" }; + f.write_file("a.c", "#include \"a.h\"\nint a(void){return 0;}\n"); + f.write_file("a.h", "#define A 1\n"); + f.write_file("Tupfile", ": a.c |> gcc -c a.c -o a.o && cp a.o b.o |> a.o b.o\n"); + + WHEN("parse reports on the rule") + { + auto result = f.pup({ "parse" }); + + THEN("the copied object is named and the compiled one is not") + { + INFO("stderr: " << result.stderr_output); + REQUIRE(result.stderr_output.find("no dependency scan") != std::string::npos); + REQUIRE(result.stderr_output.find("'b.o'") != std::string::npos); + REQUIRE(result.stderr_output.find("'a.o'") == std::string::npos); + } + } + } + + GIVEN("a scanned rule in a subdirectory whose output word points into a variant directory") + { + // %o one directory down expands to '../../build/src/lib/a.o' -- the word resolves against + // the rule's own directory and back into the variant, which a root-level rule never shows. + auto f = E2EFixture { "variant_config_input" }; + f.mkdir("src/lib"); + f.write_file("src/lib/a.c", "#include \"a.h\"\nint a(void){return 0;}\n"); + f.write_file("src/lib/a.h", "#define A 1\n"); + f.write_file("src/lib/Tupfile", ": foreach *.c |> gcc -c %f -o %o |> %B.o\n"); + f.mkdir("build"); + f.write_file("build/tup.config", ""); + + WHEN("parse reports on the rule") + { + auto result = f.pup({ "parse", "-B", "build" }); + + THEN("the scanned object is not named") + { + INFO("stderr: " << result.stderr_output); + REQUIRE(result.success()); + REQUIRE(result.stderr_output.find("no dependency scan") == std::string::npos); + } + } + } + + GIVEN("a single-invocation rule declaring an object its compile never writes") + { + auto f = E2EFixture { "glob_mixed_space" }; + f.write_file("a.c", "int a(void){return 0;}\n"); + f.write_file("Tupfile", ": a.c |> gcc -c a.c -o a.o |> a.o b.o\n"); + + WHEN("parse reports on the rule") + { + auto result = f.pup({ "parse" }); + + THEN("the object attributable to no compile is named even though a scan exists") + { + INFO("stderr: " << result.stderr_output); + REQUIRE(result.stderr_output.find("'b.o'") != std::string::npos); + REQUIRE(result.stderr_output.find("'a.o'") == std::string::npos); + } + } + } +} + SCENARIO("A compile-shaped rule with no dependency scan is reported", "[e2e][strict][depscan]") { // The scan is declined correctly — putup cannot reproduce the prefix's shell state — but @@ -11204,7 +11273,7 @@ SCENARIO("A compile-shaped rule with no dependency scan is reported", "[e2e][str { INFO("stdout: " << result.stdout_output); REQUIRE(result.success()); - REQUIRE(result.stdout_output.find("1 rule produces an object file with no dependency scan") != std::string::npos); + REQUIRE(result.stdout_output.find("1 object file has no dependency scan") != std::string::npos); REQUIRE(result.stdout_output.find("hidden.o") == std::string::npos); } From 6cf813a20258020cdd076a8c50a54640a19cb516 Mon Sep 17 00:00:00 2001 From: Mura Li <2606021+typeless@users.noreply.github.com> Date: Thu, 13 Aug 2026 08:41:20 +0800 Subject: [PATCH 2/2] Tokenize a command once and share it with every scanner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-object reporting made the reporter ask the production predicate about every command carrying an object, where before a single scan node stood as proof its whole rule was covered. The answer got more correct and the dry run got slower: check_unscanned_compiles went from 2.2M instructions to 250.6M on the gcc example — more than the entire measured regression, everything else netting slightly cheaper. The cost was not the predicate running. It was that one run tokenized the same command four times: each scanner's matches, gcc's build_dep_scans, clang-cl's has_dep_flags, with both scanners keeping their own copy of command_words and each re-splitting invocations the other had already split. Deleting the redundant matches() guard inside match_and_generate looks like the fix and is not. It was applied and measured: the work relocates rather than disappearing, because build_dep_scans then runs for every command matches() used to reject, and the tokenizer count does not move. It recovers 0.5%. A command is now tokenized once and the words and the invocation split travel together as CommandTokens, built by the outermost per-command caller and passed to every scanner surface. The split is shared, not only the words: splitting is scanner-independent, so sharing the words alone would leave each scanner re-splitting. Both copies of command_words are gone. The value is derived, so it shares down the stack and never crosses the build boundary. Persisting the covered object in the index would have closed the same number, but parse installs no scan nodes and must recompute regardless — leaving two encodings of coverage obliged to agree forever, for half the paths. Sharing within a frame cannot go stale, because it cannot outlive its input. Four fences keep the shared value from becoming a second source of truth about the command: tokenize_command is the only way to obtain one, and it embeds the text it derived from; the scanner virtuals lose their raw-text forms in this same commit, so no scanner can tokenize independently and drift; copy construction and copy assignment are deleted, because invocations span into the words buffer and a copy would dangle silently where a move is safe; and the type holds views, so persisting it is structurally hostile rather than merely forbidden. Shape A's output side is untouched — build_dep_scans still returns Vec, empty still means refusal, and the object still rides into GeneratedRule. Reporting also stops making a whole-command claim per object. Under a covered prefix the old sentence was false: "gcc -c a.c -o a.o && cd sub && gcc -c b.c -o b.o" named sub/b.o while a.o had a live scan, and the message asserted the command contained no reproducible compile with one standing next to it. A report's sentence carries the report's unit; this one had not moved with it. Measured on the same callgrind pair and workload as the regression. Tokenizer calls per command: 1.53 before #401, 3.45 after, 1.006 now — the absolute tokenizer cost is below the pre-#401 baseline. check_unscanned_compiles 250.6M to 170.7M; the whole dry run 2,037.9M to 1,900.6M, so +13.7% against main becomes +6.0%. The remaining ~108M is the per-object work itself: one build_dep_scans walk and one expand_instruction per object-bearing command. Whole suite green in one process — 832 cases, 170,199 assertions — with make check at exit 0 and format, tidy, iwyu and spec-check clean. The message change was observed red first. The copy deletion is proven by a compile error rather than asserted, and the profile on the final tree is byte-identical to the one measured before it, so every number above describes the code as it stands. A cross-model review found the copy hazard independently, on the same line, while the tree was held frozen. It also priced one accepted cost: the free matches_*_compile entry points now intern the command text per call, on a path that reaches production only through RulePatternRegistry, which the shipped CLI never populates. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01AjcxNmyFfoTJYSZKKgjrCp --- DESIGN.md | 13 ++- include/pup/graph/dep_scanner.hpp | 57 +++++++++-- include/pup/graph/scanners/clang_cl.hpp | 6 +- include/pup/graph/scanners/gcc.hpp | 6 +- spec/requirements/dep-scan.ears.md | 3 +- src/cli/strict_checks.cpp | 13 ++- src/graph/builder.cpp | 3 +- src/graph/dep_scanner.cpp | 34 +++++-- src/graph/rule_pattern.cpp | 5 +- src/graph/scanners/clang_cl.cpp | 48 +++------ src/graph/scanners/gcc.cpp | 37 ++----- test/unit/test_dep_scanner.cpp | 123 +++++++++++++++--------- test/unit/test_e2e.cpp | 12 +++ 13 files changed, 217 insertions(+), 143 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index d7df9e51..c17307b8 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -877,9 +877,9 @@ Scanners detect compiler commands and generate dependency extraction commands: ```cpp class DepScanner { - virtual auto matches(CommandInfo const&) const -> bool = 0; - virtual auto has_dep_flags(std::string_view) const -> bool = 0; - virtual auto build_dep_scans(CommandInfo const&) -> Vec = 0; + virtual auto matches(CommandInfo const&, CommandTokens const&) const -> bool = 0; + virtual auto has_dep_flags(CommandTokens const&) const -> bool = 0; + virtual auto build_dep_scans(CommandInfo const&, CommandTokens const&) -> Vec = 0; }; class DepScannerRegistry { @@ -890,6 +890,13 @@ class DepScannerRegistry { The `GccScanner` implementation handles GCC, Clang, and compatible compilers. +**Derived values share down the stack; only truths cross the build boundary.** `CommandTokens` is +the command's words and invocation split, derived once per command by `tokenize_command` and read by +every scanner. It is a stack value passed `const&`, never a member and never on a node: its views +cannot outlive the frame, so it cannot become a second source of truth about a command. That is the +same class as `PathCache` and the opposite of anything persisted, which needs an owner and a +staleness story. + **What a scan may cover.** A scan runs from the rule's directory with the rest of the command stripped, so it may carry a word only from the invocation it reproduces, and it can reproduce an invocation only when it and every invocation before it is a compile the scanner recognizes — one diff --git a/include/pup/graph/dep_scanner.hpp b/include/pup/graph/dep_scanner.hpp index f30d9875..98b38396 100644 --- a/include/pup/graph/dep_scanner.hpp +++ b/include/pup/graph/dep_scanner.hpp @@ -8,6 +8,7 @@ #include "pup/graph/rule_pattern.hpp" #include +#include #include namespace pup::graph { @@ -29,6 +30,49 @@ struct DepScan { StringId object = StringId::Empty; }; +/// A command's words and the invocations they divide into, derived once and read by every scanner. +/// Obtainable only from `tokenize_command`, so the words can never disagree with the text they came +/// from; its views die with the frame that built it, which is why it is never stored anywhere. +class CommandTokens final { +public: + friend auto tokenize_command(StringId text) -> CommandTokens; + + // Copying would leave the copy's invocation spans pointing into the source's word buffer, so + // the value moves out of its constructor and is passed by reference from there on. + CommandTokens(CommandTokens const&) = delete; + auto operator=(CommandTokens const&) -> CommandTokens& = delete; + CommandTokens(CommandTokens&&) = default; + auto operator=(CommandTokens&&) -> CommandTokens& = default; + ~CommandTokens() = default; + + [[nodiscard]] + auto text() const -> StringId + { + return text_; + } + [[nodiscard]] + auto words() const -> std::span + { + return { words_.data(), words_.size() }; + } + [[nodiscard]] + auto invocations() const -> std::span const> + { + return { invocations_.data(), invocations_.size() }; + } + +private: + CommandTokens() = default; + + StringId text_ = StringId::Empty; + Vec words_; + Vec> invocations_; +}; + +/// The one way to obtain a CommandTokens. +[[nodiscard]] +auto tokenize_command(StringId text) -> CommandTokens; + /// Abstract interface for dependency scanners. /// Implementations detect specific tools (compilers, assemblers, linkers) /// and generate commands to extract their implicit dependencies. @@ -44,17 +88,18 @@ class DepScanner { /// Check if this scanner applies to the given command [[nodiscard]] - virtual auto matches(CommandInfo const& cmd) const -> bool = 0; + virtual auto matches(CommandInfo const& cmd, CommandTokens const& tokens) const -> bool = 0; /// Check if command already has dependency generation enabled [[nodiscard]] - virtual auto has_dep_flags(std::string_view cmd) const -> bool = 0; + virtual auto has_dep_flags(CommandTokens const& tokens) const -> bool = 0; /// Build the scans that extract dependencies from the given command, one per compile /// invocation it can reproduce. Empty means the command carries no such invocation. [[nodiscard]] virtual auto build_dep_scans( - CommandInfo const& cmd + CommandInfo const& cmd, + CommandTokens const& tokens ) const -> Vec = 0; /// Get the dependency extraction specification @@ -81,17 +126,17 @@ class DepScannerRegistry final { /// Find a scanner that matches the command (nullptr if none) [[nodiscard]] - auto find_match(CommandInfo const& cmd) const -> DepScanner const*; + auto find_match(CommandInfo const& cmd, CommandTokens const& tokens) const -> DepScanner const*; /// Generate rules for a command using matching scanners [[nodiscard]] - auto match_and_generate(CommandInfo const& cmd) const + auto match_and_generate(CommandInfo const& cmd, CommandTokens const& tokens) const -> Vec; /// Whether any scanner recognizes the command as writing its own depfile, which the /// build reads back from beside the object whether or not a scan was generated. [[nodiscard]] - auto reports_own_deps(std::string_view cmd) const -> bool; + auto reports_own_deps(CommandTokens const& tokens) const -> bool; [[nodiscard]] auto empty() const -> bool diff --git a/include/pup/graph/scanners/clang_cl.hpp b/include/pup/graph/scanners/clang_cl.hpp index 0dc70136..5262e061 100644 --- a/include/pup/graph/scanners/clang_cl.hpp +++ b/include/pup/graph/scanners/clang_cl.hpp @@ -16,11 +16,11 @@ namespace pup::graph::scanners { class ClangClScanner final : public DepScanner { public: [[nodiscard]] - auto matches(CommandInfo const& cmd) const -> bool override; + auto matches(CommandInfo const& cmd, CommandTokens const& tokens) const -> bool override; [[nodiscard]] - auto has_dep_flags(std::string_view cmd) const -> bool override; + auto has_dep_flags(CommandTokens const& tokens) const -> bool override; [[nodiscard]] - auto build_dep_scans(CommandInfo const& cmd) const + auto build_dep_scans(CommandInfo const& cmd, CommandTokens const& tokens) const -> Vec override; [[nodiscard]] auto dep_spec() const -> DepSpec override; diff --git a/include/pup/graph/scanners/gcc.hpp b/include/pup/graph/scanners/gcc.hpp index 3c7d4831..1e28e001 100644 --- a/include/pup/graph/scanners/gcc.hpp +++ b/include/pup/graph/scanners/gcc.hpp @@ -16,11 +16,11 @@ namespace pup::graph::scanners { class GccScanner final : public DepScanner { public: [[nodiscard]] - auto matches(CommandInfo const& cmd) const -> bool override; + auto matches(CommandInfo const& cmd, CommandTokens const& tokens) const -> bool override; [[nodiscard]] - auto has_dep_flags(std::string_view cmd) const -> bool override; + auto has_dep_flags(CommandTokens const& tokens) const -> bool override; [[nodiscard]] - auto build_dep_scans(CommandInfo const& cmd) const + auto build_dep_scans(CommandInfo const& cmd, CommandTokens const& tokens) const -> Vec override; [[nodiscard]] auto dep_spec() const -> DepSpec override; diff --git a/spec/requirements/dep-scan.ears.md b/spec/requirements/dep-scan.ears.md index 77d585c4..bf7dfb9f 100644 --- a/spec/requirements/dep-scan.ears.md +++ b/spec/requirements/dep-scan.ears.md @@ -70,7 +70,8 @@ scan derived from it. What putup says about an object it did not scan. The unit is the object, not the rule: per object the reporting is binary — covered by a scan derived from the compile that writes it, or reported unscanned — so a rule scanned in part is not a state this group must express, only what a reader -sees when some of a rule's objects are named. +sees when some of a rule's objects are named. The report's sentences carry that unit too: each +speaks about the object it names, not about the command that declares it. ### REQ-SCAN-REPORT-UNSCANNED diff --git a/src/cli/strict_checks.cpp b/src/cli/strict_checks.cpp index b85f90b4..bcc85890 100644 --- a/src/cli/strict_checks.cpp +++ b/src/cli/strict_checks.cpp @@ -166,9 +166,13 @@ auto check_unscanned_compiles( auto source_dir = graph::get(graph, id); auto dir = pool.get(source_dir); + // Tokenized once here and read by both the suppression test and the predicate: a value + // that dies with this frame, never a second source of truth about the command. + auto tokens = graph::tokenize_command(text); + // The build reads a depfile from beside the object whether or not a scan exists, // so a compile that writes its own is covered without one. - if (registry->reports_own_deps(pool.get(text))) { + if (registry->reports_own_deps(tokens)) { continue; } @@ -182,7 +186,7 @@ auto check_unscanned_compiles( // The output word is relative to the rule's directory, the same as the compile resolves it, // so joining it there is what puts it in the declared output's space. auto covered = Vec {}; - for (auto const& rule : registry->match_and_generate(cmd_info)) { + for (auto const& rule : registry->match_and_generate(cmd_info, tokens)) { if (rule.covered_object == StringId::Empty) { continue; } @@ -197,9 +201,10 @@ auto check_unscanned_compiles( } auto buf = Buf {}; - buf.fmt("no dependency scan for '{}': putup recognizes no compile it can reproduce in " - "this command, so any header this rule reads goes unrecorded. Harmless if it " + buf.fmt("no dependency scan for '{}': no compile putup can reproduce writes '{}', so " + "any header that object's compile reads goes unrecorded. Harmless if it " "preprocesses none — an assembler, a copy, a partial link.", + pool.get(object), pool.get(object)); result.push_back(make_diag( pool.get(pup::path::join(dir.empty() ? "." : dir, "Tupfile")), diff --git a/src/graph/builder.cpp b/src/graph/builder.cpp index 116dcaa4..327cc59f 100644 --- a/src/graph/builder.cpp +++ b/src/graph/builder.cpp @@ -2131,7 +2131,8 @@ auto expand_rule( // Use scanner_registry (new modular approach) if available, fall back to pattern_registry auto generated_rules = Vec {}; if (ctx.options.scanner_registry && !ctx.options.scanner_registry->empty()) { - generated_rules = ctx.options.scanner_registry->match_and_generate(cmd_info); + auto tokens = tokenize_command(cmd_text); + generated_rules = ctx.options.scanner_registry->match_and_generate(cmd_info, tokens); } else if (ctx.options.pattern_registry && !ctx.options.pattern_registry->empty()) { generated_rules = ctx.options.pattern_registry->match_and_generate(cmd_info); } diff --git a/src/graph/dep_scanner.cpp b/src/graph/dep_scanner.cpp index a534aace..369670d0 100644 --- a/src/graph/dep_scanner.cpp +++ b/src/graph/dep_scanner.cpp @@ -7,13 +7,30 @@ #include "pup/core/global_pool.hpp" #include "pup/core/string_id.hpp" #include "pup/core/string_pool.hpp" +#include "pup/core/string_utils.hpp" #include "pup/core/vec.hpp" #include "pup/graph/rule_pattern.hpp" +#include "pup/graph/scanners/dep_words.hpp" #include +#include #include namespace pup::graph { +auto tokenize_command(StringId text) -> CommandTokens +{ + auto& pool = global_pool(); + auto tokens = CommandTokens {}; + tokens.text_ = text; + for (auto id : core::tokenize_shell_command(pool.get(text))) { + tokens.words_.push_back(pool.get(id)); + } + tokens.invocations_ = scanners::split_invocations( + std::span { tokens.words_.data(), tokens.words_.size() } + ); + return tokens; +} + auto make_dep_display(Vec const& inputs) -> StringId { if (inputs.empty()) { @@ -30,43 +47,44 @@ auto DepScannerRegistry::register_scanner(std::unique_ptr scanner) - scanners_.push_back(std::move(scanner)); } -auto DepScannerRegistry::find_match(CommandInfo const& cmd) const -> DepScanner const* +auto DepScannerRegistry::find_match(CommandInfo const& cmd, CommandTokens const& tokens) const + -> DepScanner const* { for (auto const& scanner : scanners_) { - if (scanner->matches(cmd)) { + if (scanner->matches(cmd, tokens)) { return scanner.get(); } } return nullptr; } -auto DepScannerRegistry::reports_own_deps(std::string_view cmd) const -> bool +auto DepScannerRegistry::reports_own_deps(CommandTokens const& tokens) const -> bool { for (auto const& scanner : scanners_) { - if (scanner->has_dep_flags(cmd)) { + if (scanner->has_dep_flags(tokens)) { return true; } } return false; } -auto DepScannerRegistry::match_and_generate(CommandInfo const& cmd) const +auto DepScannerRegistry::match_and_generate(CommandInfo const& cmd, CommandTokens const& tokens) const -> Vec { auto result = Vec {}; for (auto const& scanner : scanners_) { - if (!scanner->matches(cmd)) { + if (!scanner->matches(cmd, tokens)) { continue; } - if (scanner->has_dep_flags(global_pool().get(cmd.command))) { + if (scanner->has_dep_flags(tokens)) { continue; } auto spec = scanner->dep_spec(); - for (auto const& scan : scanner->build_dep_scans(cmd)) { + for (auto const& scan : scanner->build_dep_scans(cmd, tokens)) { auto outputs = Vec {}; if (spec.output_mode == DepOutputMode::Stdout) { outputs.push_back({ .type = GeneratedOutput::Type::Stdout, .path = StringId::Empty }); diff --git a/src/graph/rule_pattern.cpp b/src/graph/rule_pattern.cpp index fadadc70..cd1702a8 100644 --- a/src/graph/rule_pattern.cpp +++ b/src/graph/rule_pattern.cpp @@ -46,11 +46,12 @@ auto make_gcc_depfile_pattern() -> RulePattern static auto const scanner = scanners::GccScanner {}; auto result = Vec {}; - if (scanner.has_dep_flags(global_pool().get(cmd.command))) { + auto tokens = tokenize_command(cmd.command); + if (scanner.has_dep_flags(tokens)) { return result; } - for (auto const& scan : scanner.build_dep_scans(cmd)) { + for (auto const& scan : scanner.build_dep_scans(cmd, tokens)) { result.push_back(GeneratedRule { .inputs = cmd.inputs, .order_only_inputs = cmd.order_only_inputs, diff --git a/src/graph/scanners/clang_cl.cpp b/src/graph/scanners/clang_cl.cpp index fd6d65fe..3e0c94ab 100644 --- a/src/graph/scanners/clang_cl.cpp +++ b/src/graph/scanners/clang_cl.cpp @@ -7,7 +7,6 @@ #include "pup/core/global_pool.hpp" #include "pup/core/path.hpp" #include "pup/core/string_pool.hpp" -#include "pup/core/string_utils.hpp" #include "pup/core/vec.hpp" #include "pup/graph/scanners/dep_words.hpp" @@ -155,60 +154,35 @@ auto compile_prefix(std::span const> invocatio return invocations.first(length); } -auto command_words(std::string_view command) -> Vec -{ - auto& pool = global_pool(); - auto words = Vec {}; - for (auto id : core::tokenize_shell_command(command)) { - words.push_back(pool.get(id)); - } - return words; -} - } // namespace auto matches_clang_cl_compile(std::string_view command) -> bool { - auto words = command_words(command); - if (words.empty()) { - return false; - } - - auto invocations = split_invocations(std::span { words.data(), words.size() }); - return !compile_prefix(std::span { invocations.data(), invocations.size() }).empty(); + auto tokens = tokenize_command(global_pool().intern(command)); + return !compile_prefix(tokens.invocations()).empty(); } -auto ClangClScanner::matches(CommandInfo const& cmd) const -> bool +auto ClangClScanner::matches(CommandInfo const& /*cmd*/, CommandTokens const& tokens) const -> bool { - return matches_clang_cl_compile(global_pool().get(cmd.command)); + return !compile_prefix(tokens.invocations()).empty(); } -auto ClangClScanner::has_dep_flags(std::string_view cmd) const -> bool +auto ClangClScanner::has_dep_flags(CommandTokens const& tokens) const -> bool { // Only a pinned -MF counts: /MD and /MT select the CRT here, and a bare // -MD writes the depfile to the cwd instead of beside the object. - auto& pool = global_pool(); - for (auto id : core::tokenize_shell_command(cmd)) { - auto word = pool.get(id); - if (word.starts_with("/clang:-MF") || word.starts_with("-clang:-MF")) { - return true; - } - } - return false; + return std::ranges::any_of(tokens.words(), [](auto word) { + return word.starts_with("/clang:-MF") || word.starts_with("-clang:-MF"); + }); } -auto ClangClScanner::build_dep_scans(CommandInfo const& cmd) const -> Vec +auto ClangClScanner::build_dep_scans(CommandInfo const& /*cmd*/, CommandTokens const& tokens) const + -> Vec { auto& pool = global_pool(); - auto words = command_words(pool.get(cmd.command)); - if (words.empty()) { - return {}; - } - auto scans = Vec {}; - auto invocations = split_invocations(std::span { words.data(), words.size() }); - for (auto invocation : compile_prefix(std::span { invocations.data(), invocations.size() })) { + for (auto invocation : compile_prefix(tokens.invocations())) { auto driver_idx = driver_index(invocation); if (!driver_idx) { continue; diff --git a/src/graph/scanners/gcc.cpp b/src/graph/scanners/gcc.cpp index a669f176..27dc5135 100644 --- a/src/graph/scanners/gcc.cpp +++ b/src/graph/scanners/gcc.cpp @@ -143,36 +143,22 @@ auto compile_prefix(std::span const> invocatio return invocations.first(length); } -auto command_words(std::string_view command) -> Vec -{ - auto& pool = global_pool(); - auto words = Vec {}; - for (auto id : core::tokenize_shell_command(command)) { - words.push_back(pool.get(id)); - } - return words; -} - } // namespace auto matches_gcc_compile(std::string_view command) -> bool { - auto words = command_words(command); - if (words.empty()) { - return false; - } - - auto invocations = split_invocations(std::span { words.data(), words.size() }); - return !compile_prefix(std::span { invocations.data(), invocations.size() }).empty(); + auto tokens = tokenize_command(global_pool().intern(command)); + return !compile_prefix(tokens.invocations()).empty(); } -auto GccScanner::matches(CommandInfo const& cmd) const -> bool +auto GccScanner::matches(CommandInfo const& /*cmd*/, CommandTokens const& tokens) const -> bool { - return matches_gcc_compile(global_pool().get(cmd.command)); + return !compile_prefix(tokens.invocations()).empty(); } -auto GccScanner::has_dep_flags(std::string_view cmd) const -> bool +auto GccScanner::has_dep_flags(CommandTokens const& tokens) const -> bool { + auto cmd = global_pool().get(tokens.text()); auto pos = std::string_view::size_type { 0 }; while ((pos = cmd.find("-M", pos)) != std::string_view::npos) { if (pos > 0 && !pup::core::is_space(cmd[pos - 1])) { @@ -193,18 +179,13 @@ auto GccScanner::has_dep_flags(std::string_view cmd) const -> bool return false; } -auto GccScanner::build_dep_scans(CommandInfo const& cmd) const -> Vec +auto GccScanner::build_dep_scans(CommandInfo const& /*cmd*/, CommandTokens const& tokens) const + -> Vec { auto& pool = global_pool(); - auto words = command_words(pool.get(cmd.command)); - if (words.empty()) { - return {}; - } - auto scans = Vec {}; - auto invocations = split_invocations(std::span { words.data(), words.size() }); - for (auto invocation : compile_prefix(std::span { invocations.data(), invocations.size() })) { + for (auto invocation : compile_prefix(tokens.invocations())) { auto compiler_idx = compiler_index(invocation); if (!compiler_idx) { continue; diff --git a/test/unit/test_dep_scanner.cpp b/test/unit/test_dep_scanner.cpp index a816d9ac..5b87fdcd 100644 --- a/test/unit/test_dep_scanner.cpp +++ b/test/unit/test_dep_scanner.cpp @@ -34,10 +34,39 @@ auto path(std::string_view s) -> pup::PathId return paths.intern_path(s, pup::global_pool(), pup::PathId::BuildRoot); } +/// Tokens for a command, built the one legal way. Tests hold them only for the call they make. +auto tokens_of(CommandInfo const& cmd) -> CommandTokens +{ + return tokenize_command(cmd.command); +} + +auto tokens_of(std::string_view command) -> CommandTokens +{ + return tokenize_command(pup::global_pool().intern(command)); +} + +/// Whether a scanner claims a command, tokenized the one legal way. +auto scanner_matches(DepScanner const& scanner, CommandInfo const& cmd) -> bool +{ + return scanner.matches(cmd, tokens_of(cmd)); +} + +/// The scans a command builds and the rules a registry generates, tokenized the one legal way. +auto scans_of(DepScanner const& scanner, CommandInfo const& cmd) -> pup::Vec +{ + return scanner.build_dep_scans(cmd, tokens_of(cmd)); +} + +auto generated_for(DepScannerRegistry const& registry, CommandInfo const& cmd) + -> pup::Vec +{ + return registry.match_and_generate(cmd, tokens_of(cmd)); +} + /// The one scan a command is expected to produce, asserting that it produces exactly one. auto only_scan(DepScanner const& scanner, CommandInfo const& cmd) -> DepScan { - auto scans = scanner.build_dep_scans(cmd); + auto scans = scanner.build_dep_scans(cmd, tokens_of(cmd)); REQUIRE(scans.size() == 1); return scans[0]; } @@ -85,7 +114,7 @@ TEST_CASE("DepScannerRegistry find_match", "[dep_scanner]") .working_dir = intern("."), }; - auto const* scanner = registry.find_match(cmd); + auto const* scanner = registry.find_match(cmd, tokens_of(cmd)); REQUIRE(scanner != nullptr); REQUIRE(scanner->name() == "gcc"); } @@ -102,7 +131,7 @@ TEST_CASE("DepScannerRegistry find_match", "[dep_scanner]") .working_dir = intern("."), }; - auto const* scanner = registry.find_match(cmd); + auto const* scanner = registry.find_match(cmd, tokens_of(cmd)); REQUIRE(scanner == nullptr); } } @@ -124,7 +153,7 @@ TEST_CASE("DepScannerRegistry match_and_generate", "[dep_scanner]") .working_dir = intern("."), }; - auto rules = registry.match_and_generate(cmd); + auto rules = registry.match_and_generate(cmd, tokens_of(cmd)); REQUIRE(rules.size() == 1); REQUIRE(rules[0].command == intern("gcc -M foo.c")); REQUIRE(rules[0].action == OutputAction::InjectImplicitDeps); @@ -143,7 +172,7 @@ TEST_CASE("DepScannerRegistry match_and_generate", "[dep_scanner]") .working_dir = intern("."), }; - auto rules = registry.match_and_generate(cmd); + auto rules = registry.match_and_generate(cmd, tokens_of(cmd)); REQUIRE(rules.empty()); } @@ -159,7 +188,7 @@ TEST_CASE("DepScannerRegistry match_and_generate", "[dep_scanner]") .working_dir = intern("."), }; - auto rules = registry.match_and_generate(cmd); + auto rules = registry.match_and_generate(cmd, tokens_of(cmd)); REQUIRE(rules.empty()); } } @@ -190,7 +219,7 @@ TEST_CASE("GccScanner interface", "[dep_scanner][gcc]") .outputs = { path("foo.o") }, .working_dir = intern("."), }; - REQUIRE(scanner.matches(cmd)); + REQUIRE(scanner.matches(cmd, tokens_of(cmd))); } SECTION("matches clang compile command") @@ -204,7 +233,7 @@ TEST_CASE("GccScanner interface", "[dep_scanner][gcc]") .outputs = { path("foo.o") }, .working_dir = intern("."), }; - REQUIRE(scanner.matches(cmd)); + REQUIRE(scanner.matches(cmd, tokens_of(cmd))); } SECTION("does not match link command") @@ -218,27 +247,27 @@ TEST_CASE("GccScanner interface", "[dep_scanner][gcc]") .outputs = { path("foo") }, .working_dir = intern("."), }; - REQUIRE(!scanner.matches(cmd)); + REQUIRE(!scanner.matches(cmd, tokens_of(cmd))); } SECTION("has_dep_flags detects -MD") { - REQUIRE(scanner.has_dep_flags("gcc -MD -c foo.c -o foo.o")); + REQUIRE(scanner.has_dep_flags(tokens_of("gcc -MD -c foo.c -o foo.o"))); } SECTION("has_dep_flags detects -MMD") { - REQUIRE(scanner.has_dep_flags("gcc -MMD -c foo.c -o foo.o")); + REQUIRE(scanner.has_dep_flags(tokens_of("gcc -MMD -c foo.c -o foo.o"))); } SECTION("has_dep_flags detects -MF") { - REQUIRE(scanner.has_dep_flags("gcc -MF deps.d -c foo.c -o foo.o")); + REQUIRE(scanner.has_dep_flags(tokens_of("gcc -MF deps.d -c foo.c -o foo.o"))); } SECTION("has_dep_flags returns false for normal compile") { - REQUIRE(!scanner.has_dep_flags("gcc -c foo.c -o foo.o")); + REQUIRE(!scanner.has_dep_flags(tokens_of("gcc -c foo.c -o foo.o"))); } SECTION("build_dep_scans returns the scan") @@ -269,7 +298,7 @@ TEST_CASE("GccScanner interface", "[dep_scanner][gcc]") .working_dir = intern("."), }; - auto scans = scanner.build_dep_scans(cmd); + auto scans = scanner.build_dep_scans(cmd, tokens_of(cmd)); REQUIRE(scans.empty()); } } @@ -290,7 +319,7 @@ TEST_CASE("GccScanner compiler wrapper handling", "[dep_scanner][gcc]") .working_dir = intern("."), }; - REQUIRE(scanner.matches(cmd)); + REQUIRE(scanner.matches(cmd, tokens_of(cmd))); auto scan = only_scan(scanner, cmd); REQUIRE(scan.command == intern("ccache gcc -M foo.c")); } @@ -450,7 +479,7 @@ TEST_CASE("GccScanner rejects compound shell commands", "[dep_scanner][gcc]") .working_dir = intern("."), }; - auto scans = scanner.build_dep_scans(cmd); + auto scans = scanner.build_dep_scans(cmd, tokens_of(cmd)); REQUIRE(scans.empty()); } @@ -466,7 +495,7 @@ TEST_CASE("GccScanner rejects compound shell commands", "[dep_scanner][gcc]") .working_dir = intern("."), }; - auto scans = scanner.build_dep_scans(cmd); + auto scans = scanner.build_dep_scans(cmd, tokens_of(cmd)); REQUIRE(scans.empty()); } @@ -482,7 +511,7 @@ TEST_CASE("GccScanner rejects compound shell commands", "[dep_scanner][gcc]") .working_dir = intern("."), }; - auto scans = scanner.build_dep_scans(cmd); + auto scans = scanner.build_dep_scans(cmd, tokens_of(cmd)); REQUIRE(scans.empty()); } } @@ -503,8 +532,8 @@ TEST_CASE("GccScanner skips commands with no visible source", "[dep_scanner][gcc .working_dir = intern("."), }; - REQUIRE(scanner.matches(cmd)); - REQUIRE(scanner.build_dep_scans(cmd).empty()); + REQUIRE(scanner.matches(cmd, tokens_of(cmd))); + REQUIRE(scanner.build_dep_scans(cmd, tokens_of(cmd)).empty()); } } @@ -541,29 +570,29 @@ TEST_CASE("ClangClScanner matching", "[dep_scanner][clang_cl]") SECTION("matches clang-cl compile with GNU-spelled -c") { - REQUIRE(scanner.matches(clang_cl_compile(1, "clang-cl /std:c++latest -c foo.cpp -o foo.obj"))); + REQUIRE(scanner_matches(scanner, clang_cl_compile(1, "clang-cl /std:c++latest -c foo.cpp -o foo.obj"))); } SECTION("matches clang-cl compile with cl-spelled /c") { - REQUIRE(scanner.matches(clang_cl_compile(2, "clang-cl /std:c++latest /c foo.cpp /Fofoo.obj"))); + REQUIRE(scanner_matches(scanner, clang_cl_compile(2, "clang-cl /std:c++latest /c foo.cpp /Fofoo.obj"))); } SECTION("matches versioned and .exe driver names") { - REQUIRE(scanner.matches(clang_cl_compile(3, "clang-cl-20 -c foo.cpp -o foo.obj"))); - REQUIRE(scanner.matches(clang_cl_compile(4, "/usr/bin/clang-cl.exe -c foo.cpp -o foo.obj"))); + REQUIRE(scanner_matches(scanner, clang_cl_compile(3, "clang-cl-20 -c foo.cpp -o foo.obj"))); + REQUIRE(scanner_matches(scanner, clang_cl_compile(4, "/usr/bin/clang-cl.exe -c foo.cpp -o foo.obj"))); } SECTION("does not match a link command") { - REQUIRE(!scanner.matches(clang_cl_compile(5, "clang-cl foo.obj -o foo.exe"))); + REQUIRE(!scanner_matches(scanner, clang_cl_compile(5, "clang-cl foo.obj -o foo.exe"))); } SECTION("gcc scanner does not claim clang-cl commands") { auto gcc = scanners::GccScanner {}; - REQUIRE(!gcc.matches(clang_cl_compile(6, "clang-cl -c foo.cpp -o foo.obj"))); + REQUIRE(!scanner_matches(gcc, clang_cl_compile(6, "clang-cl -c foo.cpp -o foo.obj"))); } } @@ -573,26 +602,26 @@ TEST_CASE("ClangClScanner dep-flag detection", "[dep_scanner][clang_cl]") SECTION("detects a depfile whose path is pinned with -MF") { - REQUIRE(scanner.has_dep_flags("clang-cl /clang:-MD /clang:-MFfoo.obj.d -c foo.cpp -o foo.obj")); + REQUIRE(scanner.has_dep_flags(tokens_of("clang-cl /clang:-MD /clang:-MFfoo.obj.d -c foo.cpp -o foo.obj"))); } SECTION("bare -MD does not count: the depfile lands in the cwd, not beside the object") { - REQUIRE(!scanner.has_dep_flags("clang-cl /clang:-MD -c foo.cpp -o foo.obj")); - REQUIRE(!scanner.has_dep_flags("clang-cl /clang:-MMD -c foo.cpp -o foo.obj")); + REQUIRE(!scanner.has_dep_flags(tokens_of("clang-cl /clang:-MD -c foo.cpp -o foo.obj"))); + REQUIRE(!scanner.has_dep_flags(tokens_of("clang-cl /clang:-MMD -c foo.cpp -o foo.obj"))); } SECTION("CRT-selection flags are not dep flags") { - REQUIRE(!scanner.has_dep_flags("clang-cl /MT -c foo.cpp -o foo.obj")); - REQUIRE(!scanner.has_dep_flags("clang-cl /MDd -c foo.cpp -o foo.obj")); - REQUIRE(!scanner.has_dep_flags("clang-cl -MD -c foo.cpp -o foo.obj")); - REQUIRE(!scanner.has_dep_flags("clang-cl -MT -c foo.cpp -o foo.obj")); + REQUIRE(!scanner.has_dep_flags(tokens_of("clang-cl /MT -c foo.cpp -o foo.obj"))); + REQUIRE(!scanner.has_dep_flags(tokens_of("clang-cl /MDd -c foo.cpp -o foo.obj"))); + REQUIRE(!scanner.has_dep_flags(tokens_of("clang-cl -MD -c foo.cpp -o foo.obj"))); + REQUIRE(!scanner.has_dep_flags(tokens_of("clang-cl -MT -c foo.cpp -o foo.obj"))); } SECTION("plain compile has no dep flags") { - REQUIRE(!scanner.has_dep_flags("clang-cl /W3 -c foo.cpp -o foo.obj")); + REQUIRE(!scanner.has_dep_flags(tokens_of("clang-cl /W3 -c foo.cpp -o foo.obj"))); } } @@ -649,7 +678,7 @@ TEST_CASE("ClangClScanner dep command construction", "[dep_scanner][clang_cl]") SECTION("returns nullopt for a compound shell command") { - auto scans = scanner.build_dep_scans(clang_cl_compile(6, "cd build && clang-cl -c foo.cpp -o foo.obj")); + auto scans = scans_of(scanner, clang_cl_compile(6, "cd build && clang-cl -c foo.cpp -o foo.obj")); REQUIRE(scans.empty()); } @@ -663,7 +692,7 @@ TEST_CASE("ClangClScanner dep command construction", "[dep_scanner][clang_cl]") SECTION("returns nullopt when no source word is visible") { - auto scans = scanner.build_dep_scans(clang_cl_compile(7, "clang-cl @srcs.rsp -c -o foo.obj")); + auto scans = scans_of(scanner, clang_cl_compile(7, "clang-cl @srcs.rsp -c -o foo.obj")); REQUIRE(scans.empty()); } } @@ -683,7 +712,7 @@ TEST_CASE("Registry generates dep rules for clang-cl", "[dep_scanner][clang_cl]" SECTION("clang-cl compile gets one dep rule") { - auto rules = registry.match_and_generate(clang_cl_compile(1, "clang-cl -Isrc -c foo.cpp -o foo.obj")); + auto rules = generated_for(registry, clang_cl_compile(1, "clang-cl -Isrc -c foo.cpp -o foo.obj")); REQUIRE(rules.size() == 1); REQUIRE(rules[0].command == intern("clang-cl /clang:-M -Isrc foo.cpp")); REQUIRE(rules[0].action == OutputAction::InjectImplicitDeps); @@ -692,7 +721,7 @@ TEST_CASE("Registry generates dep rules for clang-cl", "[dep_scanner][clang_cl]" SECTION("clang-cl compile that already emits a depfile putup can find gets none") { - auto rules = registry.match_and_generate( + auto rules = generated_for(registry, clang_cl_compile(2, "clang-cl /clang:-MD /clang:-MFfoo.obj.d -c foo.cpp -o foo.obj") ); REQUIRE(rules.empty()); @@ -700,7 +729,7 @@ TEST_CASE("Registry generates dep rules for clang-cl", "[dep_scanner][clang_cl]" SECTION("a source-less clang-cl compile gets no rule rather than a failing one") { - auto rules = registry.match_and_generate(clang_cl_compile(3, "clang-cl @srcs.rsp -c -o foo.obj")); + auto rules = generated_for(registry, clang_cl_compile(3, "clang-cl @srcs.rsp -c -o foo.obj")); REQUIRE(rules.empty()); } } @@ -921,7 +950,7 @@ TEST_CASE("Default scanner registry covers both compiler drivers", "[dep_scanner SECTION("a gcc compile finds the gcc scanner") { auto cmd = clang_cl_compile(1, "g++ -c foo.cpp -o foo.o"); - auto const* scanner = registry->find_match(cmd); + auto const* scanner = registry->find_match(cmd, tokens_of(cmd)); REQUIRE(scanner != nullptr); REQUIRE(scanner->name() == "gcc"); } @@ -929,7 +958,7 @@ TEST_CASE("Default scanner registry covers both compiler drivers", "[dep_scanner SECTION("a clang-cl compile finds the clang-cl scanner") { auto cmd = clang_cl_compile(2, "clang-cl -c foo.cpp -o foo.obj"); - auto const* scanner = registry->find_match(cmd); + auto const* scanner = registry->find_match(cmd, tokens_of(cmd)); REQUIRE(scanner != nullptr); REQUIRE(scanner->name() == "clang-cl"); } @@ -1281,7 +1310,7 @@ TEST_CASE("a scan is the compile without the words that would break it", "[dep_s TEST_CASE("GccScanner scans each compile of an all-compile command with its own flags", "[dep_scanner][gcc]") { auto scanner = scanners::GccScanner {}; - auto scans = scanner.build_dep_scans( + auto scans = scans_of(scanner, gcc_compile(57, "gcc -O2 -c a.c -o a.o && gcc -c b.c -o b.o") ); @@ -1295,7 +1324,7 @@ TEST_CASE("GccScanner scans each compile of an all-compile command with its own TEST_CASE("GccScanner scans the prefix before a directory change", "[dep_scanner][gcc]") { auto scanner = scanners::GccScanner {}; - auto scans = scanner.build_dep_scans( + auto scans = scans_of(scanner, gcc_compile(70, "gcc -c a.c -o a.o && cd sub && gcc -c b.c -o b.o") ); @@ -1310,14 +1339,14 @@ TEST_CASE("GccScanner scans the prefix before an invocation that is not a compil SECTION("a later invocation that deletes a file contributes no source") { - auto scans = scanner.build_dep_scans(gcc_compile(71, "gcc -c a.c -o a.o && rm junk.c")); + auto scans = scans_of(scanner, gcc_compile(71, "gcc -c a.c -o a.o && rm junk.c")); REQUIRE(scans.size() == 1); REQUIRE(pup::global_pool().get(scans[0].command) == "gcc -M a.c"); } SECTION("a later invocation that copies files contributes neither operand") { - auto scans = scanner.build_dep_scans(gcc_compile(72, "gcc -c a.c -o a.o && cp b.c x.c")); + auto scans = scans_of(scanner, gcc_compile(72, "gcc -c a.c -o a.o && cp b.c x.c")); REQUIRE(scans.size() == 1); REQUIRE(pup::global_pool().get(scans[0].command) == "gcc -M a.c"); } @@ -1347,7 +1376,7 @@ TEST_CASE("A command whose first invocation is not a compile is scanned nowhere" // The bound that keeps the prefix rule from becoming scan-everything: an empty prefix, so no // later compile is reproducible from the rule's directory either. auto scanner = scanners::GccScanner {}; - REQUIRE(scanner.build_dep_scans(gcc_compile(75, "cd sub && gcc -c a.c -o a.o")).empty()); + REQUIRE(scans_of(scanner, gcc_compile(75, "cd sub && gcc -c a.c -o a.o")).empty()); } TEST_CASE("A separator with nothing after it begins no invocation", "[dep_scanner][gcc]") @@ -1393,7 +1422,7 @@ TEST_CASE("matches_clang_cl_compile refuses a command whose first invocation is TEST_CASE("ClangClScanner scans the prefix before an invocation that is not a compile", "[dep_scanner][clang_cl]") { auto scanner = scanners::ClangClScanner {}; - auto scans = scanner.build_dep_scans( + auto scans = scans_of(scanner, clang_cl_compile(73, "clang-cl -c a.cpp -o a.obj && cd sub && clang-cl -c b.cpp -o b.obj") ); diff --git a/test/unit/test_e2e.cpp b/test/unit/test_e2e.cpp index 095851e4..8f49393c 100644 --- a/test/unit/test_e2e.cpp +++ b/test/unit/test_e2e.cpp @@ -11143,6 +11143,18 @@ SCENARIO("The object of a compile that runs elsewhere is reported instead of sca REQUIRE(result.stderr_output.find("sub/b.o") != std::string::npos); REQUIRE(result.stderr_output.find("'a.o'") == std::string::npos); } + + THEN("it does not claim the command contains no reproducible compile") + { + // One is standing next to it: a.o's compile is the covered prefix. The report's + // unit moved to the object, so its sentence has to speak about the object. + INFO("stderr: " << result.stderr_output); + REQUIRE(result.stderr_output.find("in this command") == std::string::npos); + REQUIRE( + result.stderr_output.find("no compile putup can reproduce writes") + != std::string::npos + ); + } } WHEN("the source that only the Tupfile directory's copy includes is edited")