-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathembed.cppm
More file actions
301 lines (272 loc) · 14.2 KB
/
Copy pathembed.cppm
File metadata and controls
301 lines (272 loc) · 14.2 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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
// mcpp.tools.embed - a data file becomes a header the program compiles in.
//
// WHY THIS IS A TOOL AND NOT A RULE. A rule (`mcpp.rules.<x>`) states how a
// translation unit is compiled by a compiler mcpp does not drive: it submits an
// action, the engine schedules it, and the work happens in the build graph. A
// tool states something a build program needs that no compiler performs. This
// one reads bytes and writes a header, so there is no external program to
// schedule and no action to submit; the file is written while `build.mcpp`
// runs, before the engine plans anything.
//
// THE OUTPUT IS A HEADER, FOR THE REASON `mcpp.rules.spirv` GIVES. A data file
// beside the binary makes the program's correctness depend on its working
// directory. A header compiled into the program does not, and `mcpp pack` of
// that program has nothing further to collect.
//
// IT DOES NOT REWRITE AN UNCHANGED HEADER. Writing the same bytes again would
// still move the file's mtime, and every translation unit that includes it
// would rebuild on a build where nothing changed. The comparison is on content
// and is the reason this tool is safe to call unconditionally from a build
// program that runs on every configure.
//
// WHAT IT DELIBERATELY DOES NOT DO. It does not compress, does not chunk a
// large file across several arrays, and does not emit a `std::span` accessor.
// Each is reachable from what it emits, and a tool in this collection earns a
// feature by being needed by more than one consumer.
module;
#include <cctype>
#include <cstdio>
export module mcpp.tools.embed;
import std;
import mcpp;
// The lib root, which carries `mcpp::plugins::surface` -- the declarations a
// consumer names. `group()` below hands its payloads to it, so a set of files
// embedded by this tool and a set of shaders compiled by `mcpp.rules.spirv`
// reach a consumer through the same shape.
import mcpp.plugins;
import mcpp.plugins.declare;
// WHY NOTHING HERE USES `std::println`, AND WHY THAT IS NOT A STYLE CHOICE.
//
// `std::print` and `std::println` are not header-only. Both of their overloads
// reach into the libc++ DYLIB -- `__is_posix_terminal(FILE*)` for the stdout
// form and `__get_ostream_file(ostream&)` for the stream form -- and those
// symbols were added to that library in a version macOS 14 does not ship. A
// build program's link resolves `-lc++` to the system copy there, so a rule
// that printed with `std::println` compiled and then failed to link:
//
// ld64.lld: error: undefined symbol: std::__1::__is_posix_terminal(__sFILE*)
//
// naming neither the call that needed it nor the reason. Measured on
// macos-14; macos-15 has the symbol, which is why nothing saw this until a
// rule was first compiled on the older of the two supported releases.
//
// `std::format` is header-only and has no such dependency, so every message in
// this file is formatted and then streamed.
export namespace mcpp::tools::embed {
// The element the array is made of. A byte array is the general answer; a
// 32-bit word array is what an API that takes `const uint32_t*` wants -- SPIR-V
// is the case that exists in this repository -- and asking for it here is
// cheaper than a reinterpret_cast at every call site, which is undefined
// behaviour on an under-aligned byte array.
enum class element { byte_, word32 };
struct options {
// Where the header is written. Empty means `<out_dir>/include/mcpp.tools.embed`,
// which is added to the include path; a caller that names a directory owns
// adding it.
std::string out_dir;
// The C++ identifier the array is called. Empty derives it from the input
// file name: every character that is not alphanumeric becomes `_`, and a
// leading digit is prefixed with `_`.
std::string identifier;
// An optional namespace for the two symbols. Nested namespaces are written
// with `::` and emitted as a C++17 nested definition.
std::string name_space;
element elem = element::byte_;
// A trailing zero byte, so the array is usable as a C string. It is counted
// by neither `_size` nor the array's declared bound comment; the array
// simply has one more element than `_size` says.
bool null_terminate = false;
// Bytes per line in the generated file. Only the file's shape depends on
// it; no consumer can observe it.
unsigned width = 16;
};
// ---- internals -------------------------------------------------------------
// The accessor's own name, so a file called `default.bin` must not produce
// `default()`. The lib root owns that decision; this is the one caller that
// needs it here.
inline std::string sanitise(std::string_view stem) {
return mcpp::plugins::surface::identifier(stem, "data");
}
inline std::string default_dir() {
const char* out = mcpp::out_dir();
return (std::filesystem::path(out && *out ? out : ".") / "include" / "mcpp.tools.embed").string();
}
inline std::string identifier_for(const std::filesystem::path& input, const options& opt) {
if (!opt.identifier.empty()) return opt.identifier;
auto stem = input.filename().string();
return sanitise(stem);
}
// The header a given input produces, without producing it. A consumer that
// wants to `#include` it by an explicit path rather than by name asks here.
inline std::string header_path(const std::filesystem::path& input, const options& opt = {}) {
const auto dir = opt.out_dir.empty() ? default_dir() : opt.out_dir;
return (std::filesystem::path(dir) / (identifier_for(input, opt) + ".h")).string();
}
inline bool write_if_different(const std::filesystem::path& path, std::string_view text) {
std::error_code ec;
std::filesystem::create_directories(path.parent_path(), ec);
if (std::ifstream in(path, std::ios::binary); in) {
std::string old((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
if (old == text) return true;
}
std::ofstream out(path, std::ios::binary | std::ios::trunc);
if (!out) return false;
out.write(text.data(), static_cast<std::streamsize>(text.size()));
return static_cast<bool>(out);
}
// ---- the tool ---------------------------------------------------------------
// One file. Returns false and explains on stderr when the input cannot be read
// or the header cannot be written; a build program returns that value.
inline bool file(const std::filesystem::path& input, options opt = {}) {
const std::string root = mcpp::manifest_dir();
const auto absolute = input.is_absolute()
? input : std::filesystem::path(root) / input;
std::ifstream in(absolute, std::ios::binary);
if (!in) {
std::cerr << std::format("mcpp.tools.embed: cannot read {}", absolute.string()) << '\n';
return false;
}
std::string bytes((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
if (opt.elem == element::word32 && bytes.size() % 4 != 0) {
std::cerr << std::format("mcpp.tools.embed: {} is {} bytes, which is not a multiple of 4, and "
"element::word32 was asked for", absolute.string(), bytes.size()) << '\n';
return false;
}
const auto id = identifier_for(absolute, opt);
const auto dir = opt.out_dir.empty() ? default_dir() : opt.out_dir;
const auto out = std::filesystem::path(dir) / (id + ".h");
std::string text;
text += "// Generated by mcpp.tools.embed from ";
text += absolute.filename().string();
text += ". Do not edit.\n#pragma once\n\n#include <cstddef>\n#include <cstdint>\n\n";
std::vector<std::string> opened;
if (!opt.name_space.empty()) {
text += "namespace " + opt.name_space + " {\n\n";
}
const bool word = opt.elem == element::word32;
const auto count = word ? bytes.size() / 4 : bytes.size();
text += word ? "inline constexpr std::uint32_t " : "inline constexpr unsigned char ";
text += id;
text += "[] = {";
const unsigned per_line = opt.width == 0 ? 16 : opt.width;
for (std::size_t i = 0; i < count; ++i) {
if (i % per_line == 0) text += "\n ";
if (word) {
const auto b = reinterpret_cast<const unsigned char*>(bytes.data()) + i * 4;
text += std::format("0x{:08x}u,", static_cast<std::uint32_t>(b[0])
| (static_cast<std::uint32_t>(b[1]) << 8)
| (static_cast<std::uint32_t>(b[2]) << 16)
| (static_cast<std::uint32_t>(b[3]) << 24));
} else {
text += std::format("0x{:02x},", static_cast<unsigned>(
static_cast<unsigned char>(bytes[i])));
}
if (i + 1 < count) text += ' ';
}
if (opt.null_terminate && !word) {
if (count % per_line == 0) text += "\n ";
text += "0x00,";
}
text += "\n};\n\n";
text += std::format("inline constexpr std::size_t {}_size = {};\n", id, count);
if (!opt.name_space.empty()) text += "\n} // namespace " + opt.name_space + "\n";
if (!write_if_different(out, text)) {
std::cerr << std::format("mcpp.tools.embed: cannot write {}", out.string()) << '\n';
return false;
}
// The build program is cached on its inputs, so a file it reads is a file
// it must declare: without this, editing the data leaves the header from
// the previous build in place and the program compiles yesterday's bytes.
mcpp::rerun_if_changed(absolute.string().c_str());
if (opt.out_dir.empty()) mcpp::include_dir(dir.c_str());
return true;
}
// Several files, sharing one set of options. The identifier is derived per
// file, so `options::identifier` is refused here rather than silently applied
// to the first input only.
inline bool files(std::span<const std::string> inputs, options opt = {}) {
if (!opt.identifier.empty()) {
std::cerr << std::format("mcpp.tools.embed: options::identifier names one "
"symbol and files() writes several; call file() per input") << '\n';
return false;
}
for (auto const& one : inputs)
if (!file(one, opt)) return false;
return true;
}
// Several files, reached through ONE declaration a consumer imports.
//
// `files()` writes a header per input and leaves the consumer to include each
// by name. `group()` writes those same headers and then hands them to
// `mcpp::plugins::surface`, so the consumer writes one `import` and names no
// generated file -- the same surface `mcpp.rules.spirv` produces, from the same
// generator, because a payload that was already on disk and one a compiler
// produced are the same thing to whoever consumes it.
//
// The group's own name is required rather than derived. A rule knows what its
// payloads are for and can name the module `<package>.shaders`; a tool called
// on an arbitrary set of files does not, and a derived name would be a guess
// that two calls in one build program could collide on.
inline bool group(std::span<const std::string> inputs,
const std::string& module_name,
mcpp::plugins::surface::kind surface
= mcpp::plugins::surface::default_surface(),
options opt = {}) {
if (inputs.empty()) return true;
if (module_name.empty()) {
std::cerr << "mcpp.tools.embed: group() needs a module name; it is what a "
"consumer imports and the namespace the declarations sit in\n";
return false;
}
if (!files(inputs, opt)) return false;
const auto dir = opt.out_dir.empty() ? default_dir() : opt.out_dir;
std::vector<mcpp::plugins::surface::item> items;
for (auto const& one : inputs) {
const auto absolute = std::filesystem::path(one).is_absolute()
? std::filesystem::path(one)
: std::filesystem::path(mcpp::manifest_dir()) / one;
const auto id = identifier_for(absolute, opt);
// `files()` wrote `<id>.h` beside its siblings, so the include is the
// bare name: this tool's generated tree is flat, unlike a rule's, which
// mirrors the source tree it globbed.
//
// The symbol is QUALIFIED by `options::name_space`, because that is
// where `file()` put the array. Passing the bare name compiles for a
// caller that left the option empty and fails for one that did not,
// which is the shape of a defect that only the second test finds.
const auto sym = opt.name_space.empty() ? id : opt.name_space + "::" + id;
items.push_back({ .identifier = id,
.name_space = {},
.data_header = id + ".h",
.data_symbol = sym,
// `<id>_size` IS A COUNT OF ELEMENTS, AND THE SURFACE
// REPORTS BYTES. For `element::byte_` the two are the
// same number and the distinction is invisible; for
// `word32` it is four times out.
//
// `sizeof` is not the answer either: `null_terminate`
// appends a zero byte that `_size` deliberately does
// not count, so `sizeof` is one too many. Measured on
// a group of null-terminated text payloads, which
// reported one extra byte and failed the fixture's
// comparison on a trailing NUL.
.data_size_expr = opt.elem == element::word32
? sym + "_size * 4"
: sym + "_size" });
}
mcpp::plugins::surface::options so;
so.surface = surface;
so.elem = opt.elem == element::word32
? mcpp::plugins::surface::element::word32
: mcpp::plugins::surface::element::byte_;
so.module_name = module_name;
so.out_dir = dir;
so.produced_by = "mcpp.tools.embed";
// Answered here, not read there: `mcpp.plugins.surface` compiles into a
// plain binary as well as into this build program, so it takes its inputs.
so.target_os = mcpp::target_os();
so.has_gas_assembler = std::string_view(mcpp::compiler()) != "msvc";
const auto out = mcpp::plugins::surface_for(items, so);
return out.ok;
}
} // namespace mcpp::tools::embed