-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathgraph_shape.cppm
More file actions
211 lines (199 loc) · 10.7 KB
/
Copy pathgraph_shape.cppm
File metadata and controls
211 lines (199 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// mcpp.build.graph_shape — what kind of graph a build.ninja holds, written
// into the file and read back out of it.
//
// `target/<triple>/<fp>/build.ninja` is SHARED MUTABLE STATE. Three modes
// write it — `mcpp build`, `mcpp test`, `mcpp build --configure-only` — and
// they land in the same directory because the fingerprint covers neither
// dev-dependencies nor test targets. But `emit_ninja_string`'s `default` line
// lists every link unit in the plan, and in test mode that is ONLY the test
// binaries: the package's own target is not in the graph at all.
//
// The fast path used to compare build.ninja's mtime against the SOURCES and
// nothing else, so `build → test → build` replayed the test graph, linked the
// tests, never linked the target, and printed `Finished`. Breaking a test file
// then failed a plain `mcpp build` with `src/` untouched.
//
// WHY THE FILE AND NOT THE CACHE. The first repair for this (mcpp#387) was on
// the WRITE side: the mode that rewrites the graph drops the fast-path entry
// afterwards. That works, and it has to be repeated by every future mode that
// rewrites build.ninja — the same decision derived in a new place each time,
// which is how the `mcpp test` half stayed broken after the `--configure-only`
// half was fixed. A READ-side invariant only has to hold once: the fast path
// checks the graph it is about to replay.
//
// Putting the shape in build.ninja rather than in `.build_cache` is the same
// argument one level down. Two files can disagree; a file that describes
// itself cannot. The single producer of build.ninja is also the single writer
// of this line.
export module mcpp.build.graph_shape;
import std;
export namespace mcpp::build {
enum class GraphShape {
// What a plain `mcpp build` generates: the package's own targets.
Normal,
// Dev-dependencies and synthetic test targets are in the plan, so `default`
// names the test binaries. Produced by `mcpp test` and by
// `mcpp build --configure-only`.
WithTests,
};
std::string_view to_string(GraphShape shape) {
return shape == GraphShape::WithTests ? "test" : "normal";
}
// The marker line, without its newline. A ninja comment, so it costs nothing
// and older ninja versions do not care.
// `scheduleTag` names the SHAPE OF THE MODULE EDGES (see
// mcpp.build.schedule.policy): "none", "two-phase", "detach-codegen". It rides
// the same line for the same reason the shape does — build.ninja is shared
// mutable state and the fast path replays it, so a graph built under one
// schedule must not be replayed under another. Flipping the switch has to
// invalidate the graph, and the only way that cannot be forgotten is if the
// graph says which schedule produced it.
// `accelOverridden` records whether `--accel` / `--no-accel` chose the device
// variant. That variant is in the fingerprint, so the two builds land in
// different directories -- which is exactly why the fast path cannot tell
// them apart: it replays the directory built LAST, before any plan exists to
// say which directory a plain build would choose. Measured: `mcpp build`,
// `mcpp build --no-accel`, `mcpp build` -- the third reported "Finished in
// 0.00s" and `mcpp run` executed the CPU variant. A graph an override chose
// says so, and the fast paths, which run only without overrides, decline it.
// `packFormat` records the DISTRIBUTION FORMAT this graph was generated for,
// and empty means "none" -- an ordinary build. It rides this line for exactly
// the reason the other two fields do, and it is the third instance of one
// failure: `mcpp pack --format appimage` makes a build program submit an
// artifact action a plain build must not have, and the two graphs land in the
// same directory because the format is deliberately NOT in the fingerprint
// (putting it there would cost a full recompile to package an already-built
// tree). So `pack --format X` then `build` would replay a graph carrying a dist
// edge, which is the `A then B then A` shape both other fields exist to stop.
std::string header_line(GraphShape shape, std::string_view scheduleTag,
bool accelOverridden = false,
std::string_view packFormat = {}) {
return std::format("# mcpp:graph={};schedule={};accel={};dist={}",
to_string(shape), scheduleTag,
accelOverridden ? "override" : "default",
packFormat.empty() ? std::string_view("none") : packFormat);
}
// Read the shape back. `nullopt` means "this file does not say" — a build.ninja
// written before this line existed, an unreadable file, or something that is
// not a mcpp graph at all. Callers must treat that as a MISS, never as
// `Normal`: the whole point is that an unlabelled graph is exactly the case
// that used to be replayed blind.
std::optional<GraphShape> read_shape(const std::filesystem::path& ninjaPath) {
std::ifstream input(ninjaPath);
if (!input) return std::nullopt;
// The marker is written first, but read a few lines anyway so a future
// banner above it does not silently turn every build into a full prepare.
std::string line;
for (int i = 0; i < 8 && std::getline(input, line); ++i) {
constexpr std::string_view prefix = "# mcpp:graph=";
if (!line.starts_with(prefix)) continue;
auto value = std::string_view(line).substr(prefix.size());
while (!value.empty() && (value.back() == '\r' || value.back() == ' '))
value.remove_suffix(1);
// `graph=<shape>[;schedule=<tag>]`. Split before comparing, so adding
// the schedule field does not turn every existing graph into "unknown".
if (const auto semi = value.find(';'); semi != std::string_view::npos)
value = value.substr(0, semi);
if (value == "normal") return GraphShape::Normal;
if (value == "test") return GraphShape::WithTests;
// A shape this binary does not know is not `Normal`. An older mcpp
// meeting a newer graph must fall back, not guess.
return std::nullopt;
}
return std::nullopt;
}
// The schedule tag this graph was written with. Empty means the file predates
// the field — which is NOT the same as "none": an unlabelled graph is exactly
// the case that must not be replayed blind, so callers compare and miss.
std::string read_schedule(const std::filesystem::path& ninjaPath) {
std::ifstream input(ninjaPath);
if (!input) return {};
std::string line;
for (int i = 0; i < 8 && std::getline(input, line); ++i) {
constexpr std::string_view prefix = "# mcpp:graph=";
if (!line.starts_with(prefix)) continue;
auto value = std::string_view(line).substr(prefix.size());
while (!value.empty() && (value.back() == '\r' || value.back() == ' '))
value.remove_suffix(1);
const auto semi = value.find(';');
if (semi == std::string_view::npos) return {};
auto rest = value.substr(semi + 1);
constexpr std::string_view schedPrefix = "schedule=";
if (!rest.starts_with(schedPrefix)) return {};
rest = rest.substr(schedPrefix.size());
// Cut at the next field: the selection rides the same line.
if (const auto next = rest.find(';'); next != std::string_view::npos)
rest = rest.substr(0, next);
return std::string(rest);
}
return {};
}
// The one question every fast path asks.
//
// It deliberately does NOT compare the schedule tag. The fast paths run BEFORE
// a plan exists, so they have no toolchain to derive the expected schedule
// from — and passing one in would mean deriving the same decision a second
// time, in a place that cannot see the compiler.
//
// Instead the schedule SWITCH is part of the toolchain fingerprint, so flipping
// it lands in a different build directory: a graph written under one schedule
// is structurally unreachable from a build configured with another. The tag on
// the line is then for humans and for `mcpp explain`, not for invalidation.
// The device-variant selection this graph was written under: "default" for
// `[build] accel` as the manifest states it, "override" for a `--accel` or
// `--no-accel` build. Empty when the file predates the field, which callers
// treat as a miss for the reason read_shape gives.
std::string read_accel_selection(const std::filesystem::path& ninjaPath) {
std::ifstream input(ninjaPath);
if (!input) return {};
std::string line;
for (int i = 0; i < 8 && std::getline(input, line); ++i) {
constexpr std::string_view prefix = "# mcpp:graph=";
if (!line.starts_with(prefix)) continue;
auto value = std::string_view(line).substr(prefix.size());
while (!value.empty() && (value.back() == '\r' || value.back() == ' '))
value.remove_suffix(1);
constexpr std::string_view key = ";accel=";
const auto at = value.find(key);
if (at == std::string_view::npos) return {};
auto rest = value.substr(at + key.size());
if (const auto semi = rest.find(';'); semi != std::string_view::npos)
rest = rest.substr(0, semi);
return std::string(rest);
}
return {};
}
// The distribution format this graph was generated for, or "none". Empty when
// the file predates the field, which callers treat as a miss for the reason
// read_shape gives.
std::string read_pack_format(const std::filesystem::path& ninjaPath) {
std::ifstream input(ninjaPath);
if (!input) return {};
std::string line;
for (int i = 0; i < 8 && std::getline(input, line); ++i) {
constexpr std::string_view prefix = "# mcpp:graph=";
if (!line.starts_with(prefix)) continue;
auto value = std::string_view(line).substr(prefix.size());
while (!value.empty() && (value.back() == '\r' || value.back() == ' '))
value.remove_suffix(1);
constexpr std::string_view key = ";dist=";
const auto at = value.find(key);
if (at == std::string_view::npos) return {};
auto rest = value.substr(at + key.size());
if (const auto semi = rest.find(';'); semi != std::string_view::npos)
rest = rest.substr(0, semi);
return std::string(rest);
}
return {};
}
// A graph the fast paths may replay: the package's own targets, the device
// variant the manifest names rather than one a flag chose, and no distribution
// edge. All three fast-path callers run only for a plain build, so a graph any
// of the three axes was pointed at is never the graph a plain build would
// produce.
bool is_plain_build_graph(const std::filesystem::path& ninjaPath) {
return read_shape(ninjaPath) == GraphShape::Normal
&& read_accel_selection(ninjaPath) == "default"
&& read_pack_format(ninjaPath) == "none";
}
} // namespace mcpp::build