@@ -47,8 +47,15 @@ std::string_view to_string(GraphShape shape) {
4747
4848// The marker line, without its newline. A ninja comment, so it costs nothing
4949// and older ninja versions do not care.
50- std::string header_line (GraphShape shape) {
51- return std::format (" # mcpp:graph={}" , to_string (shape));
50+ // `scheduleTag` names the SHAPE OF THE MODULE EDGES (see
51+ // mcpp.build.schedule.policy): "none", "two-phase", "detach-codegen". It rides
52+ // the same line for the same reason the shape does — build.ninja is shared
53+ // mutable state and the fast path replays it, so a graph built under one
54+ // schedule must not be replayed under another. Flipping the switch has to
55+ // invalidate the graph, and the only way that cannot be forgotten is if the
56+ // graph says which schedule produced it.
57+ std::string header_line (GraphShape shape, std::string_view scheduleTag) {
58+ return std::format (" # mcpp:graph={};schedule={}" , to_string (shape), scheduleTag);
5259}
5360
5461// Read the shape back. `nullopt` means "this file does not say" — a build.ninja
@@ -68,6 +75,10 @@ std::optional<GraphShape> read_shape(const std::filesystem::path& ninjaPath) {
6875 auto value = std::string_view (line).substr (prefix.size ());
6976 while (!value.empty () && (value.back () == ' \r ' || value.back () == ' ' ))
7077 value.remove_suffix (1 );
78+ // `graph=<shape>[;schedule=<tag>]`. Split before comparing, so adding
79+ // the schedule field does not turn every existing graph into "unknown".
80+ if (const auto semi = value.find (' ;' ); semi != std::string_view::npos)
81+ value = value.substr (0 , semi);
7182 if (value == " normal" ) return GraphShape::Normal;
7283 if (value == " test" ) return GraphShape::WithTests;
7384 // A shape this binary does not know is not `Normal`. An older mcpp
@@ -77,7 +88,40 @@ std::optional<GraphShape> read_shape(const std::filesystem::path& ninjaPath) {
7788 return std::nullopt ;
7889}
7990
91+ // The schedule tag this graph was written with. Empty means the file predates
92+ // the field — which is NOT the same as "none": an unlabelled graph is exactly
93+ // the case that must not be replayed blind, so callers compare and miss.
94+ std::string read_schedule (const std::filesystem::path& ninjaPath) {
95+ std::ifstream input (ninjaPath);
96+ if (!input) return {};
97+ std::string line;
98+ for (int i = 0 ; i < 8 && std::getline (input, line); ++i) {
99+ constexpr std::string_view prefix = " # mcpp:graph=" ;
100+ if (!line.starts_with (prefix)) continue ;
101+ auto value = std::string_view (line).substr (prefix.size ());
102+ while (!value.empty () && (value.back () == ' \r ' || value.back () == ' ' ))
103+ value.remove_suffix (1 );
104+ const auto semi = value.find (' ;' );
105+ if (semi == std::string_view::npos) return {};
106+ auto rest = value.substr (semi + 1 );
107+ constexpr std::string_view schedPrefix = " schedule=" ;
108+ if (!rest.starts_with (schedPrefix)) return {};
109+ return std::string (rest.substr (schedPrefix.size ()));
110+ }
111+ return {};
112+ }
113+
80114// The one question every fast path asks.
115+ //
116+ // It deliberately does NOT compare the schedule tag. The fast paths run BEFORE
117+ // a plan exists, so they have no toolchain to derive the expected schedule
118+ // from — and passing one in would mean deriving the same decision a second
119+ // time, in a place that cannot see the compiler.
120+ //
121+ // Instead the schedule SWITCH is part of the toolchain fingerprint, so flipping
122+ // it lands in a different build directory: a graph written under one schedule
123+ // is structurally unreachable from a build configured with another. The tag on
124+ // the line is then for humans and for `mcpp explain`, not for invalidation.
81125bool is_plain_build_graph (const std::filesystem::path& ninjaPath) {
82126 return read_shape (ninjaPath) == GraphShape::Normal;
83127}
0 commit comments