Repository navigation
Expand file tree
/
Copy pathprogram_protocol.cppm
More file actions
219 lines (204 loc) · 12.7 KB
/
Copy pathprogram_protocol.cppm
File metadata and controls
219 lines (204 loc) · 12.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
212
213
214
215
216
217
218
219
// mcpp.build.program_protocol — the CONTRACT between mcpp and a `build.mcpp`.
//
// WHY THIS IS ITS OWN MODULE
//
// `mcpp.build.directives` answers "what is a directive" — one row per wire
// name, driving parse / serialize / apply / declared-output / private-fold.
// That is a big table and it grows every time a directive is added.
//
// The three things in this file are a different kind of fact: they are the
// terms both sides agree on BEFORE any directive is exchanged.
//
// * which wire version is spoken,
// * when previously written cache entries stop meaning what they said,
// * how long the program may run.
//
// They have separate consumers — `mcpp.build.hostprogram` stamps the protocol
// version into the bundled `mcpp` module and needs nothing else from the
// directive table; `mcpp.build.build_program` needs the run bound. Splitting
// them out keeps those two from importing the table, and keeps this file
// small enough that changing a protocol term is visibly a protocol change.
//
// Imports `std` and NOTHING else, deliberately: a protocol term that needed
// the manifest, the toolchain or the filesystem to be stated would not be a
// protocol term.
//
// See .agents/docs/2026-08-30-build-mcpp-extensibility-architecture.md §4 and
// .agents/docs/2026-08-11-source-kind-table-and-build-program-timeout.md §4.
export module mcpp.build.program_protocol;
import std;
export namespace mcpp::build::program_protocol {
// ── Protocol version ───────────────────────────────────────────────────────
//
// The wire version this engine speaks. The bundled `mcpp` module announces the
// version it was built against (`mcpp:protocol=<N>`) before main runs, so a
// program and the engine that compiled it always agree — the announcement only
// ever disagrees when a *cached* helper binary outlives an engine change, which
// is precisely the case worth catching.
//
// Bump when the meaning of an existing directive changes, or when a new
// directive is added that a program may rely on. An engine seeing a HIGHER
// number than this must refuse: it cannot know what it is being asked to do,
// and "warn and ignore" would turn that into a silently different build.
// v2 (#359): adds `rerun-if-changed-glob`.
// v5: adds `warning` — the channel a program uses to succeed and still say
// something. A package that calls `mcpp::warning()` does not fail on an
// older engine with a PROTOCOL error; it fails earlier, at the build.mcpp
// COMPILE, because the bundled module that engine ships has no such function.
// That is the same cost `link-script` carried into v3 and is stated here so
// the next reader does not look for a protocol path that never runs.
// v6: adds `flash`, `monitor`, `debug` and `runner-exclusive` — `runner`'s
// three siblings and the claim that a device admits one user at a time. Same
// cost as v5's: a package calling `mcpp::flash()` fails on an older engine at
// the build.mcpp COMPILE, because that engine's bundled module has no such
// function, not through a protocol refusal.
// v7: adds `fact` and `floor` -- the probe channel. A rule package states a
// fact about the machine and the floor it needs of it; the engine compares
// them before compiling. Same cost as v5's: a package calling `mcpp::fact()`
// fails on an older engine at the build.mcpp COMPILE, not through a refusal.
// v8: adds `link-flag` -- the generic linker-flag outlet. `link-lib`,
// `link-search` and `link-script` cover a library, a search path and a layout;
// a flag a program COMPUTED (a generated version script, `--wrap`,
// `--exclude-libs`) had no way out. Same cost as v5's: a package calling
// `mcpp::link_flag()` fails on an older engine at the build.mcpp COMPILE,
// because that engine's bundled module has no such function.
// v9: adds `pack-format` -- the outlet by which a package says which
// distribution format it provides, so `mcpp pack --format <name>` can dispatch
// to it. Same cost as v5's: a package calling `mcpp::provides_pack_format()`
// fails on an older engine at the build.mcpp COMPILE, because that engine's
// bundled module has no such function.
// v10: adds `windows-subsystem` and `windows-entry` -- a named executable's PE
// subsystem and entry function (#618), the build-program form of the
// `[targets.<name>]` keys of the same names. Same cost as v5's: a package
// calling `mcpp::windows_subsystem()` fails on an older engine at the
// build.mcpp COMPILE, because that engine's bundled module has no such function.
// v11: adds `deploy` -- the build-program form of `[runtime] deploy` (#622
// A4): a file this program produced or selected, placed beside the artifact
// at a path relative to the executable. Same cost as v5's: a package calling
// `mcpp::deploy()` fails on an older engine at the build.mcpp COMPILE, because
// that engine's bundled module has no such function.
// v12 (mcpp#701/#702): adds `runtime-search-dir` -- the build-program form of
// `runtime_search_dirs`: a launch-time search directory for a dependency (a
// vcpkg prefix's `bin/`, a Qt SDK's `bin/`, a directory a `prepare` action
// populates) whose location a build.mcpp learns rather than one an author can
// write into TOML. Same cost as v5's: a package calling
// `mcpp::runtime_search_dir()` fails on an older engine at the build.mcpp
// COMPILE, because that engine's bundled module has no such function.
//
// v12 also adds the `prepare` role: construction whose file names are not
// known when the build program runs. Its constants
// (`mcpp::roles::{source, check, object, artifact, prepare}`) and its
// `mcpp::action::output_dir()` method live in the bundled module for the
// same reason `deploy` and `runtime_search_dir` do -- a program that uses
// them fails to COMPILE on an older engine, naming the missing symbol,
// rather than reaching that engine as a string it misreads.
// v13 (mcpp#708): adds `mcpp::action::env(name, value)` and
// `mcpp::action::cwd(dir)` -- the environment and the directory of an
// action's command, which an argv with no shell cannot express. They reach
// the engine as the `env` and `cwd` fields of the `action` payload and are
// omitted when unset, so an action that uses neither serialises to the bytes
// it did under v12 and no cached entry changes meaning. Same cost as v5's: a
// package calling `env()` fails on an older engine at the build.mcpp COMPILE,
// because that engine's bundled module has no such method.
// v14 (mcpp#734): the interface is also named `mcpp.core` (the module `mcpp`
// stays its permanent equivalent), and it states the resolved toolchain's
// build information -- `tool`, `abi_tool`, `tool_env`, `toolset_identity`,
// `msvc_instance_dir`, `ninja_program`, `cxx_runtime`, `msvc_crt_linkage` --
// together with the batched placement and structured diagnostics of the same
// release. No directive of v13 changes spelling, so a program that uses none of
// these serialises to the bytes it did under v13.
// v15 (mcpp#755): sources. `xpkg_program`, `xpkg_source` and `xpkg_request`
// answer where a declared payload comes from (an `[xlings.overrides]` entry,
// the payload, or "pending" for one declared `provision = "on-request"` that
// is not installed yet); `mcpp:xpkg-request=` asks for such a payload,
// `mcpp:decision=` records which tool a plugin runs, `mcpp:toolchain=` states
// the build toolchain from the root's toolchain phase, and `mcpp::phase()`
// says which phase is running. No directive of v14 changes spelling.
inline constexpr int kProtocolVersion = 15;
// ── Cache-format epoch ─────────────────────────────────────────────────────
//
// Bump ONLY when previously written build.mcpp.cache entries become unusable —
// the record shape changed, or a directive's *interpretation* changed so that
// replaying a cached value would no longer mean what it meant when written.
// Deliberately NOT the mcpp release number: folding the whole version in would
// re-run every build program on every release for nothing. Same discipline as
// mcpp.build.cache_key::kCacheEpoch.
// Epoch 2 (#359): entries gained `glob` records. An engine that does not know
// them would replay a strict subset of the declared inputs and call a stale
// build fresh, which is exactly the silent-wrong-answer this guard exists for.
// Epoch 3 (#703): an `ldflag` value is read into words (SPEC-004 §8), and the
// link directives spell a path as one word. A value an earlier engine cached,
// `-L/opt/my sdk/lib` for instance, would now read as two words.
// Epoch 3 also covers mcpp#702: `decode_action` now refuses an `mcpp:action=` payload
// whose `role` names none of the five the engine knows, where it used to read
// an unrecognised string as `Source` silently. That is an INTERPRETATION
// change on an existing tag (`action`, since v1) rather than a new one: an
// entry cached under epoch 2 could hold such a role and decoded successfully
// then; replaying it through `apply` (which runs no validator) would now
// either refuse or, worse, silently drop the action, neither of which is what
// a fresh run of the SAME build.mcpp would do. Bumping the epoch forces one
// fresh run, which goes through `action_error` and reports exactly what
// changed, instead of a stale entry reinterpreted two ways by two code paths.
inline constexpr int kCacheEpoch = 3;
// ── Run bound ──────────────────────────────────────────────────────────────
//
// How long a build program may RUN before mcpp kills it. The compile is
// deliberately left unbounded — the same asymmetry `mcpp test` settled on
// (run 300s / build 0): a long compile is usually legitimate (a first-run std
// module build is minutes) and killing it produces a baffling failure, while a
// long-running build PROGRAM is usually stuck, and without a bound the whole
// build hangs with no diagnostic at all.
inline constexpr int kDefaultRunTimeoutSecs = 600;
// The environment override, in seconds. `nullopt` means "not set / unusable"
// — a malformed or negative value is ignored rather than fatal, which is the
// behaviour this variable has always had.
//
// Split out from run_timeout() so the PRECEDENCE below is a pure function and
// can be tested without touching the process environment.
std::optional<int> env_timeout_override();
// The effective bound. Precedence, highest first:
//
// 1. `envSecs` — MCPP_BUILD_PROGRAM_TIMEOUT, this invocation only
// 2. `manifestSecs` — `[build] build_program_timeout` in the manifest of
// the package that OWNS this build.mcpp (its author is
// the one who knows how long the generator takes; a
// consumer who needs to override reaches for the env
// var, which is global for exactly that reason)
// 3. kDefaultRunTimeoutSecs
//
// Zero at any level means "no bound" and is returned as a zero duration, which
// capture_exec_deadline treats as unbounded. `nullopt` and `0` are therefore
// NOT the same thing at either level, which is why both are optionals all the
// way down rather than an int with a sentinel.
//
// Mirrors the precedence `macos_deployment_target` documents (env > manifest >
// built-in default); a second shape for the same idea would be one more thing
// to remember.
std::chrono::milliseconds run_timeout(std::optional<int> envSecs,
std::optional<int> manifestSecs);
// Convenience for callers that have a manifest but no reason to read the
// environment themselves.
std::chrono::milliseconds run_timeout_for(std::optional<int> manifestSecs);
} // namespace mcpp::build::program_protocol
namespace mcpp::build::program_protocol {
std::optional<int> env_timeout_override() {
const char* v = std::getenv("MCPP_BUILD_PROGRAM_TIMEOUT");
if (!v) return std::nullopt;
std::string_view sv(v);
int parsed = 0;
auto r = std::from_chars(sv.data(), sv.data() + sv.size(), parsed);
if (r.ec != std::errc{} || r.ptr != sv.data() + sv.size()) return std::nullopt;
if (parsed < 0) return std::nullopt;
return parsed;
}
std::chrono::milliseconds run_timeout(std::optional<int> envSecs,
std::optional<int> manifestSecs) {
int secs = kDefaultRunTimeoutSecs;
if (manifestSecs && *manifestSecs >= 0) secs = *manifestSecs;
if (envSecs && *envSecs >= 0) secs = *envSecs;
return std::chrono::milliseconds(static_cast<long long>(secs) * 1000);
}
std::chrono::milliseconds run_timeout_for(std::optional<int> manifestSecs) {
return run_timeout(env_timeout_override(), manifestSecs);
}
} // namespace mcpp::build::program_protocol