diff --git a/DESIGN.md b/DESIGN.md index 9aece720..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_command(CommandInfo const&) -> std::optional = 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,16 +890,25 @@ 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 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..98b38396 100644 --- a/include/pup/graph/dep_scanner.hpp +++ b/include/pup/graph/dep_scanner.hpp @@ -8,7 +8,7 @@ #include "pup/graph/rule_pattern.hpp" #include -#include +#include #include namespace pup::graph { @@ -23,6 +23,56 @@ 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; +}; + +/// 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. @@ -38,18 +88,19 @@ 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 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( - CommandInfo const& cmd - ) const -> std::optional = 0; + virtual auto build_dep_scans( + CommandInfo const& cmd, + CommandTokens const& tokens + ) const -> Vec = 0; /// Get the dependency extraction specification [[nodiscard]] @@ -75,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/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..5262e061 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" @@ -17,12 +16,12 @@ 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_command(CommandInfo const& cmd) const - -> std::optional override; + auto build_dep_scans(CommandInfo const& cmd, CommandTokens const& tokens) 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..1e28e001 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" @@ -17,12 +16,12 @@ 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_command(CommandInfo const& cmd) const - -> std::optional override; + auto build_dep_scans(CommandInfo const& cmd, CommandTokens const& tokens) 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..bf7dfb9f 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,43 @@ 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. 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 - 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..bcc85890 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,37 +146,33 @@ 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); + + // 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; } @@ -186,22 +182,37 @@ 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, tokens)) { + 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 '{}': 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")), + 0, + Diagnostic::Warning, + buf.view() + )); + } } return result; 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 02fbff8e..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,61 +47,60 @@ 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))) { - continue; - } - - auto dep_cmd = scanner->build_dep_command(cmd); - if (!dep_cmd) { + if (scanner->has_dep_flags(tokens)) { 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, tokens)) { + 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..cd1702a8 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,28 @@ 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 {}; - if (scanner.has_dep_flags(global_pool().get(cmd.command))) { - return std::nullopt; + auto result = Vec {}; + auto tokens = tokenize_command(cmd.command); + if (scanner.has_dep_flags(tokens)) { + 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, tokens)) { + 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..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" @@ -133,168 +132,159 @@ 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); } -auto command_words(std::string_view command) -> Vec +/// 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& pool = global_pool(); - auto words = Vec {}; - for (auto id : core::tokenize_shell_command(command)) { - words.push_back(pool.get(id)); + auto length = std::size_t { 0 }; + while (length < invocations.size() && is_recognized_compile(invocations[length])) { + ++length; } - return words; + return invocations.first(length); } } // 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 every_invocation_is_a_compile(std::span { invocations.data(), invocations.size() }); + 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_command(CommandInfo const& cmd) const -> std::optional +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 std::nullopt; - } + 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(tokens.invocations())) { + 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..27dc5135 100644 --- a/src/graph/scanners/gcc.cpp +++ b/src/graph/scanners/gcc.cpp @@ -121,49 +121,44 @@ 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"; }); } -auto command_words(std::string_view command) -> Vec +/// 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& pool = global_pool(); - auto words = Vec {}; - for (auto id : core::tokenize_shell_command(command)) { - words.push_back(pool.get(id)); + auto length = std::size_t { 0 }; + while (length < invocations.size() && is_recognized_compile(invocations[length])) { + ++length; } - return words; + return invocations.first(length); } } // 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 every_invocation_is_a_compile(std::span { invocations.data(), invocations.size() }); + 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])) { @@ -184,106 +179,106 @@ 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*/, CommandTokens const& tokens) const + -> Vec { auto& pool = global_pool(); - auto words = command_words(pool.get(cmd.command)); - if (words.empty()) { - return std::nullopt; - } - - 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]; - } + auto scans = Vec {}; - 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(tokens.invocations())) { + 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..5b87fdcd 100644 --- a/test/unit/test_dep_scanner.cpp +++ b/test/unit/test_dep_scanner.cpp @@ -33,6 +33,43 @@ auto path(std::string_view s) -> pup::PathId static auto paths = pup::PathPool {}; 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, tokens_of(cmd)); + REQUIRE(scans.size() == 1); + return scans[0]; +} } // namespace TEST_CASE("DepScannerRegistry basic operations", "[dep_scanner]") @@ -77,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"); } @@ -94,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); } } @@ -116,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); @@ -135,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()); } @@ -151,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()); } } @@ -182,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") @@ -196,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") @@ -210,30 +247,30 @@ 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_command returns command") + SECTION("build_dep_scans returns the scan") { auto cmd = CommandInfo { .node_id = 4, @@ -245,12 +282,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 +298,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, tokens_of(cmd)); + REQUIRE(scans.empty()); } } @@ -283,10 +319,9 @@ TEST_CASE("GccScanner compiler wrapper handling", "[dep_scanner][gcc]") .working_dir = intern("."), }; - 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")); + REQUIRE(scanner.matches(cmd, tokens_of(cmd))); + auto scan = only_scan(scanner, cmd); + REQUIRE(scan.command == intern("ccache gcc -M foo.c")); } SECTION("handles distcc wrapper") @@ -301,9 +336,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 +357,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 +373,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 +389,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 +405,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 +421,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 +430,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 +442,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 +458,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 +479,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, tokens_of(cmd)); + REQUIRE(scans.empty()); } SECTION("cd-and-compile compound command") @@ -468,8 +495,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, tokens_of(cmd)); + REQUIRE(scans.empty()); } SECTION("env-var assignment before compiler") @@ -484,8 +511,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, tokens_of(cmd)); + REQUIRE(scans.empty()); } } @@ -505,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_command(cmd).has_value()); + REQUIRE(scanner.matches(cmd, tokens_of(cmd))); + REQUIRE(scanner.build_dep_scans(cmd, tokens_of(cmd)).empty()); } } @@ -543,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"))); } } @@ -575,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"))); } } @@ -609,70 +636,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 = scans_of(scanner, 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 = scans_of(scanner, clang_cl_compile(7, "clang-cl @srcs.rsp -c -o foo.obj")); + REQUIRE(scans.empty()); } } @@ -691,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); @@ -700,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()); @@ -708,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()); } } @@ -881,12 +902,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); @@ -930,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"); } @@ -938,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"); } @@ -959,10 +979,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 +994,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 +1025,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 +1056,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 +1072,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 +1138,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 +1146,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 +1176,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 +1219,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 +1245,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 +1307,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 = scans_of(scanner, 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 = scans_of(scanner, 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 = 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 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 = 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"); } } +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(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]") { 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 +1400,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 = 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") ); - 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 +1436,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 +1467,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 +1537,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 +1549,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..8f49393c 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,24 @@ 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); + } + + 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 + ); } } @@ -11159,6 +11172,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 +11285,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); }