tebako 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/src/tebako-main.cpp CHANGED
@@ -35,6 +35,7 @@
35
35
  #include <sys/stat.h>
36
36
  #include <fcntl.h>
37
37
  #include <stdint.h>
38
+ #include <limits.h>
38
39
 
39
40
  #include <string>
40
41
  #include <cstdint>
@@ -45,6 +46,11 @@
45
46
  #ifdef _WIN32
46
47
  #include <winsock2.h>
47
48
  #include <windows.h>
49
+ #include <io.h>
50
+ #endif
51
+
52
+ #ifdef __APPLE__
53
+ #include <mach-o/dyld.h>
48
54
  #endif
49
55
 
50
56
  #include <tebako/tebako-config.h>
@@ -55,9 +61,417 @@
55
61
  #include <tebako/tebako-fs.h>
56
62
  #include <tebako/tebako-cmdline.h>
57
63
 
64
+ /*
65
+ * tpkg manifest trailer reader (Stage 3A).
66
+ * Vendored copy of libtfs include/tebako/tpkg.h @ 37166d3 (PR #155) at
67
+ * include/tebako/tpkg.h in this repo -- keep in sync with upstream.
68
+ * TPKG_IMPLEMENTATION is defined in exactly this translation unit.
69
+ */
70
+ #define TPKG_IMPLEMENTATION
71
+ #include <tebako/tpkg.h>
72
+
73
+ /*
74
+ * Launcher ABI v1 (Stage 3B) -- the bootstrap -> runtime handoff contract.
75
+ * A lean package's bootstrap execs the runtime as
76
+ * <runtime> --tebako-image <file>:<slot>:<mount-point> ...
77
+ * --tebako-entry <argv0> <user args...>
78
+ * and the runtime mounts the referenced image slots directly out of the
79
+ * bootstrap's file (spec 4.4). The Ruby-side constants and the full contract
80
+ * documentation live in lib/tebako/launcher_abi.rb -- keep the two in sync.
81
+ */
82
+ #define TEBAKO_LAUNCHER_ABI_VERSION 1u
83
+ #define TEBAKO_LAUNCHER_IMAGE_ARG "--tebako-image"
84
+ #define TEBAKO_LAUNCHER_ENTRY_ARG "--tebako-entry"
85
+ #define TEBAKO_LAUNCHER_VERSION_ARG "--tebako-launcher-abi"
86
+
58
87
  static int running_miniruby = 0;
59
88
  static tebako::cmdline_args* args = nullptr;
60
89
  static std::vector<char> package;
90
+ /* Owns the self-executable bytes when a tpkg trailer mount replaces the
91
+ incbin image; process-lifetime like the incbin section itself. */
92
+ static std::vector<char> self_image;
93
+
94
+ namespace {
95
+
96
+ /* Path of the running executable, UTF-8 (best effort; empty on failure) */
97
+ std::string self_executable_path()
98
+ {
99
+ #if defined(_WIN32)
100
+ wchar_t wbuf[32768];
101
+ DWORD n = GetModuleFileNameW(nullptr, wbuf, static_cast<DWORD>(sizeof(wbuf) / sizeof(wbuf[0])));
102
+ if (n == 0 || n >= sizeof(wbuf) / sizeof(wbuf[0])) {
103
+ return std::string();
104
+ }
105
+ int len = WideCharToMultiByte(CP_UTF8, 0, wbuf, n, nullptr, 0, nullptr, nullptr);
106
+ if (len <= 0) {
107
+ return std::string();
108
+ }
109
+ std::string out(static_cast<size_t>(len), '\0');
110
+ WideCharToMultiByte(CP_UTF8, 0, wbuf, n, out.data(), len, nullptr, nullptr);
111
+ return out;
112
+ #elif defined(__APPLE__)
113
+ uint32_t bufsize = 0;
114
+ _NSGetExecutablePath(nullptr, &bufsize);
115
+ std::vector<char> buf(bufsize);
116
+ if (_NSGetExecutablePath(buf.data(), &bufsize) != 0) {
117
+ return std::string();
118
+ }
119
+ char resolved[PATH_MAX];
120
+ if (realpath(buf.data(), resolved) != nullptr) {
121
+ return std::string(resolved);
122
+ }
123
+ return std::string(buf.data());
124
+ #else
125
+ char buf[PATH_MAX];
126
+ ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
127
+ if (n <= 0) {
128
+ return std::string();
129
+ }
130
+ buf[n] = '\0';
131
+ return std::string(buf);
132
+ #endif
133
+ }
134
+
135
+ /* Open a file read-only from a UTF-8 path (the running executable or a
136
+ --tebako-image package file); returns fd or -1 */
137
+ int open_self_executable(const std::string& path)
138
+ {
139
+ #if defined(_WIN32)
140
+ int wlen = MultiByteToWideChar(CP_UTF8, 0, path.c_str(), -1, nullptr, 0);
141
+ if (wlen <= 0) {
142
+ return -1;
143
+ }
144
+ std::vector<wchar_t> wpath(static_cast<size_t>(wlen));
145
+ MultiByteToWideChar(CP_UTF8, 0, path.c_str(), -1, wpath.data(), wlen);
146
+ return _wopen(wpath.data(), _O_RDONLY | _O_BINARY);
147
+ #else
148
+ return open(path.c_str(), O_RDONLY);
149
+ #endif
150
+ }
151
+
152
+ /*
153
+ * Probe the own executable for a tpkg manifest trailer.
154
+ *
155
+ * Returns 1 if a valid trailer was read into `manifest`, 0 if no trailer is
156
+ * present (classic incbin bundle -- caller falls back), and -1 if a corrupt
157
+ * trailer was detected (caller must fail startup; spec §6: never a partial
158
+ * mount). On return values other than 0 a message naming the binary is
159
+ * printed for the corrupt case.
160
+ */
161
+ int read_self_manifest(tpkg_manifest* manifest)
162
+ {
163
+ std::string self = self_executable_path();
164
+ if (self.empty()) {
165
+ return 0; // cannot locate self -- treat as classic bundle
166
+ }
167
+
168
+ int fd = open_self_executable(self);
169
+ if (fd < 0) {
170
+ return 0;
171
+ }
172
+
173
+ int rc = tpkg_read_fd(fd, manifest);
174
+ if (rc == 0) {
175
+ // Validate slot geometry against the real file size; a CRC-valid
176
+ // manifest pointing outside the file is still corrupt for us.
177
+ off_t fsize = lseek(fd, 0, SEEK_END);
178
+ close(fd);
179
+ if (fsize < 0) {
180
+ return 0;
181
+ }
182
+ for (uint32_t i = 0; i < manifest->slot_count; ++i) {
183
+ const tpkg_slot& s = manifest->slots[i];
184
+ if (s.offset > static_cast<uint64_t>(fsize) || s.size > static_cast<uint64_t>(fsize) - s.offset) {
185
+ printf(
186
+ "Tebako: package manifest trailer in '%s' is corrupt (slot %u outside file bounds). "
187
+ "Re-stitch the package to repair the manifest.\n",
188
+ self.c_str(), i);
189
+ return -1;
190
+ }
191
+ }
192
+ return 1;
193
+ }
194
+
195
+ close(fd);
196
+ int err = tpkg_errno();
197
+ if (err == TPKG_ERR_NO_TRAILER) {
198
+ return 0; // classic bundle, not an error
199
+ }
200
+
201
+ printf(
202
+ "Tebako: package manifest trailer in '%s' is corrupt (%s). "
203
+ "Re-stitch the package to repair the manifest.\n",
204
+ self.c_str(), tpkg_strerror(err));
205
+ return -1;
206
+ }
207
+
208
+ /* Read the whole file behind `fd` into self_image; 0 on success */
209
+ int read_self_image(int fd)
210
+ {
211
+ off_t fsize = lseek(fd, 0, SEEK_END);
212
+ if (fsize <= 0 || lseek(fd, 0, SEEK_SET) != 0) {
213
+ return -1;
214
+ }
215
+ self_image.resize(static_cast<size_t>(fsize));
216
+ size_t got = 0;
217
+ while (got < static_cast<size_t>(fsize)) {
218
+ ssize_t r = read(fd, self_image.data() + got, static_cast<size_t>(fsize) - got);
219
+ if (r <= 0) {
220
+ self_image.clear();
221
+ return -1;
222
+ }
223
+ got += static_cast<size_t>(r);
224
+ }
225
+ return 0;
226
+ }
227
+
228
+ /*
229
+ * Launcher ABI v1 (Stage 3B) -- runtime side of the bootstrap handoff.
230
+ */
231
+
232
+ /* One --tebako-image reference, resolved against the file's tpkg trailer */
233
+ struct launcher_image {
234
+ std::string file;
235
+ uint32_t slot = 0; /* index into the file's tpkg slot table */
236
+ std::string mount_point;
237
+ uint64_t size = 0; /* image length in bytes, from the trailer */
238
+ const char* window = nullptr; /* slot bytes, owned by launcher_image_store */
239
+ };
240
+
241
+ struct launcher_handoff {
242
+ bool present = false; /* any launcher ABI option seen at all */
243
+ bool version_seen = false; /* --tebako-launcher-abi given */
244
+ uint32_t version = 0;
245
+ std::string entry; /* package argv[0]; empty when --tebako-entry is absent */
246
+ std::vector<launcher_image> images;
247
+ std::vector<std::string> user_args; /* everything after --tebako-entry <argv0> */
248
+ std::string error; /* non-empty: malformed handoff */
249
+ };
250
+
251
+ /* Owns the image slot bytes read out of --tebako-image files;
252
+ process-lifetime like the incbin section itself. Appending moves the
253
+ outer vector but never the inner buffers, so window pointers stay valid. */
254
+ static std::vector<std::vector<char>> launcher_image_store;
255
+
256
+ bool parse_uint32(const std::string& s, uint32_t* out)
257
+ {
258
+ if (s.empty() || s.size() > 10) {
259
+ return false;
260
+ }
261
+ uint64_t v = 0;
262
+ for (char c : s) {
263
+ if (c < '0' || c > '9') {
264
+ return false;
265
+ }
266
+ v = v * 10 + static_cast<uint64_t>(c - '0');
267
+ if (v > UINT32_MAX) {
268
+ return false;
269
+ }
270
+ }
271
+ *out = static_cast<uint32_t>(v);
272
+ return true;
273
+ }
274
+
275
+ /* Split "<file>:<slot>:<mount-point>" on the last two colons; the file
276
+ component may itself contain colons (e.g. Windows drive prefixes). */
277
+ bool split_image_spec(const std::string& spec, std::string* file, uint32_t* slot, std::string* mount_point)
278
+ {
279
+ size_t last = spec.rfind(':');
280
+ if (last == std::string::npos || last == 0) {
281
+ return false;
282
+ }
283
+ size_t prev = spec.rfind(':', last - 1);
284
+ if (prev == std::string::npos) {
285
+ return false;
286
+ }
287
+ *file = spec.substr(0, prev);
288
+ *mount_point = spec.substr(last + 1);
289
+ if (file->empty() || mount_point->empty()) {
290
+ return false;
291
+ }
292
+ return parse_uint32(spec.substr(prev + 1, last - prev - 1), slot);
293
+ }
294
+
295
+ /* Match one launcher ABI option, inline ("--opt=value") or bare ("--opt").
296
+ Returns 1 for --tebako-image, 2 for --tebako-entry, 3 for
297
+ --tebako-launcher-abi, 0 for anything else. For a bare match *had_inline
298
+ is false and *value is untouched; the caller consumes the next argument. */
299
+ int match_launcher_arg(const std::string& arg, bool* had_inline, std::string* value)
300
+ {
301
+ static const std::string keys[] = {TEBAKO_LAUNCHER_IMAGE_ARG, TEBAKO_LAUNCHER_ENTRY_ARG, TEBAKO_LAUNCHER_VERSION_ARG};
302
+ for (size_t k = 0; k < 3; ++k) {
303
+ if (arg == keys[k]) {
304
+ *had_inline = false;
305
+ return static_cast<int>(k) + 1;
306
+ }
307
+ if (arg.size() > keys[k].size() && arg.compare(0, keys[k].size(), keys[k]) == 0 && arg[keys[k].size()] == '=') {
308
+ *had_inline = true;
309
+ *value = arg.substr(keys[k].size() + 1);
310
+ return static_cast<int>(k) + 1;
311
+ }
312
+ }
313
+ return 0;
314
+ }
315
+
316
+ /*
317
+ * Scan argv for the launcher ABI handoff. The three ABI options are
318
+ * recognized until --tebako-entry is consumed; everything after its value is
319
+ * application argv. Non-ABI arguments preceding --tebako-entry are ignored
320
+ * (the bootstrap emits none). When no ABI option is present at all the
321
+ * returned handoff is !present and the caller runs the classic flow
322
+ * unchanged.
323
+ */
324
+ launcher_handoff parse_launcher_handoff(int argc, char** argv)
325
+ {
326
+ launcher_handoff h;
327
+ for (int i = 1; i < argc; ++i) {
328
+ bool had_inline = false;
329
+ std::string value;
330
+ int kind = match_launcher_arg(argv[i], &had_inline, &value);
331
+ if (kind == 0) {
332
+ continue;
333
+ }
334
+ h.present = true;
335
+ if (!had_inline) {
336
+ if (i + 1 >= argc) {
337
+ h.error = std::string("Error: ") + argv[i] +
338
+ (kind == 1 ? " shall be followed by <file>:<slot>:<mount-point>"
339
+ : (kind == 2 ? " shall be followed by the package argv[0]"
340
+ : " shall be followed by an ABI version number"));
341
+ return h;
342
+ }
343
+ value = argv[++i];
344
+ }
345
+ if (kind == 1) {
346
+ launcher_image img;
347
+ if (!split_image_spec(value, &img.file, &img.slot, &img.mount_point)) {
348
+ h.error = "Error: malformed --tebako-image value '" + value + "' -- expected <file>:<slot>:<mount-point>";
349
+ return h;
350
+ }
351
+ h.images.push_back(std::move(img));
352
+ }
353
+ else if (kind == 2) {
354
+ if (value.empty()) {
355
+ h.error = "Error: --tebako-entry shall be followed by the package argv[0]";
356
+ return h;
357
+ }
358
+ h.entry = value;
359
+ for (++i; i < argc; ++i) {
360
+ h.user_args.emplace_back(argv[i]);
361
+ }
362
+ break;
363
+ }
364
+ else {
365
+ if (!parse_uint32(value, &h.version)) {
366
+ h.error = "Error: malformed --tebako-launcher-abi value '" + value + "' -- expected an ABI version number";
367
+ return h;
368
+ }
369
+ h.version_seen = true;
370
+ }
371
+ }
372
+ return h;
373
+ }
374
+
375
+ /* 64-bit file positioning for image slot reads (slot offsets can exceed 2GB) */
376
+ int seek_fd(int fd, uint64_t offset)
377
+ {
378
+ #if defined(_WIN32)
379
+ return _lseeki64(fd, static_cast<__int64>(offset), SEEK_SET) == -1 ? -1 : 0;
380
+ #else
381
+ return lseek(fd, static_cast<off_t>(offset), SEEK_SET) == static_cast<off_t>(-1) ? -1 : 0;
382
+ #endif
383
+ }
384
+
385
+ uint64_t file_size_fd(int fd)
386
+ {
387
+ #if defined(_WIN32)
388
+ __int64 end = _lseeki64(fd, 0, SEEK_END);
389
+ return end < 0 ? 0 : static_cast<uint64_t>(end);
390
+ #else
391
+ off_t end = lseek(fd, 0, SEEK_END);
392
+ return end == static_cast<off_t>(-1) ? 0 : static_cast<uint64_t>(end);
393
+ #endif
394
+ }
395
+
396
+ /* Read exactly n bytes from fd at absolute offset `offset` into dest */
397
+ int read_region(int fd, uint64_t offset, char* dest, size_t n)
398
+ {
399
+ if (seek_fd(fd, offset) != 0) {
400
+ return -1;
401
+ }
402
+ size_t got = 0;
403
+ while (got < n) {
404
+ ssize_t r = read(fd, dest + got, n - got);
405
+ if (r <= 0) {
406
+ return -1;
407
+ }
408
+ got += static_cast<size_t>(r);
409
+ }
410
+ return 0;
411
+ }
412
+
413
+ /*
414
+ * Resolve every --tebako-image reference against its file's tpkg manifest
415
+ * trailer and read the slot's bytes out of the file (no extraction, no temp
416
+ * copies; spec 4.4). On failure prints a named startup error and returns
417
+ * false; the caller must fail startup (spec 6: never a partial mount, one
418
+ * bad slot aborts with the slot index).
419
+ */
420
+ bool resolve_launcher_images(launcher_handoff* h)
421
+ {
422
+ for (auto& img : h->images) {
423
+ int fd = open_self_executable(img.file);
424
+ if (fd < 0) {
425
+ printf("Tebako: cannot open image file '%s': %s\n", img.file.c_str(), strerror(errno));
426
+ return false;
427
+ }
428
+ tpkg_manifest m;
429
+ if (tpkg_read_fd(fd, &m) != 0) {
430
+ int err = tpkg_errno();
431
+ close(fd);
432
+ if (err == TPKG_ERR_NO_TRAILER) {
433
+ printf("Tebako: image file '%s' carries no tpkg manifest trailer --\n"
434
+ " --tebako-image expects a three-part package file (bootstrap + image slots + trailer)\n",
435
+ img.file.c_str());
436
+ }
437
+ else {
438
+ printf("Tebako: package manifest trailer in '%s' is corrupt (%s). "
439
+ "Re-stitch the package to repair the manifest.\n",
440
+ img.file.c_str(), tpkg_strerror(err));
441
+ }
442
+ return false;
443
+ }
444
+ if (img.slot >= m.slot_count) {
445
+ printf("Tebako: --tebako-image slot %u is out of range for '%s' (%u slot(s) in its manifest)\n", img.slot,
446
+ img.file.c_str(), m.slot_count);
447
+ close(fd);
448
+ return false;
449
+ }
450
+ const tpkg_slot& s = m.slots[img.slot];
451
+ uint64_t fsize = file_size_fd(fd);
452
+ if (s.offset > fsize || s.size > fsize - s.offset) {
453
+ printf("Tebako: package manifest trailer in '%s' is corrupt (slot %u outside file bounds). "
454
+ "Re-stitch the package to repair the manifest.\n",
455
+ img.file.c_str(), img.slot);
456
+ close(fd);
457
+ return false;
458
+ }
459
+ launcher_image_store.emplace_back(static_cast<size_t>(s.size));
460
+ std::vector<char>& buf = launcher_image_store.back();
461
+ if (read_region(fd, s.offset, buf.data(), buf.size()) != 0) {
462
+ printf("Tebako: failed to read image slot %u from '%s': %s\n", img.slot, img.file.c_str(), strerror(errno));
463
+ launcher_image_store.pop_back();
464
+ close(fd);
465
+ return false;
466
+ }
467
+ close(fd);
468
+ img.size = s.size;
469
+ img.window = buf.data();
470
+ }
471
+ return true;
472
+ }
473
+
474
+ } // namespace
61
475
 
62
476
  static void tebako_clean(void)
63
477
  {
@@ -89,9 +503,46 @@ extern "C" int tebako_main(int* argc, char*** argv)
89
503
  }
90
504
  const void* data = &gfsData[0];
91
505
  size_t size = gfsSize;
506
+ std::string root_image_offset = "auto";
507
+ /* Non-root image slots: (recorded mount point, image bytes, image size).
508
+ window points at the slot's image -- into self_image for the trailer
509
+ path, into launcher_image_store for the launcher ABI path. */
510
+ struct extra_slot {
511
+ std::string mount_point;
512
+ const char* window;
513
+ uint64_t size;
514
+ };
515
+ std::vector<extra_slot> extra_slots;
516
+ bool trailer_corrupt = false;
517
+ bool handoff_failed = false;
92
518
 
93
519
  try {
94
- args = new tebako::cmdline_args(*argc, (const char**)*argv);
520
+ /*
521
+ * Launcher ABI v1 (Stage 3B): a lean package's bootstrap execs the
522
+ * runtime with --tebako-image/--tebako-entry arguments. When present,
523
+ * they are consumed here -- before the classic cmdline/trailer/incbin
524
+ * logic -- and the interpreter is handed a synthetic argv of the
525
+ * package argv[0] and the application arguments.
526
+ */
527
+ launcher_handoff handoff = parse_launcher_handoff(*argc, *argv);
528
+ if (!handoff.error.empty()) {
529
+ printf("Tebako: %s\n", handoff.error.c_str());
530
+ handoff_failed = true;
531
+ }
532
+
533
+ int effective_argc = *argc;
534
+ char** effective_argv = *argv;
535
+ std::vector<const char*> synthetic_argv;
536
+ if (handoff.present && !handoff_failed) {
537
+ synthetic_argv.push_back(handoff.entry.empty() ? (*argv)[0] : handoff.entry.c_str());
538
+ for (const auto& a : handoff.user_args) {
539
+ synthetic_argv.push_back(a.c_str());
540
+ }
541
+ effective_argc = static_cast<int>(synthetic_argv.size());
542
+ effective_argv = const_cast<char**>(synthetic_argv.data());
543
+ }
544
+
545
+ args = new tebako::cmdline_args(effective_argc, (const char**)effective_argv);
95
546
  args->parse_arguments();
96
547
  if (args->with_application()) {
97
548
  args->process_package();
@@ -106,14 +557,131 @@ extern "C" int tebako_main(int* argc, char*** argv)
106
557
  }
107
558
  }
108
559
 
109
- fsret = mount_root_memfs(data, size, tebako::fs_log_level, nullptr, nullptr, nullptr, nullptr, "auto");
110
- if (fsret == 0) {
111
- args->process_mountpoints();
112
- args->build_arguments(mount_point.c_str(), entry_point.c_str());
113
- *argc = args->get_argc();
114
- *argv = args->get_argv();
115
- ret = 0;
116
- atexit(tebako_clean);
560
+ if (handoff.present && !handoff_failed) {
561
+ /*
562
+ * Launcher ABI handoff (Stage 3B): mount the images the bootstrap
563
+ * named, directly out of its package file(s) -- no extraction.
564
+ */
565
+ if (handoff.version_seen && handoff.version > TEBAKO_LAUNCHER_ABI_VERSION) {
566
+ printf(
567
+ "Tebako: launcher ABI mismatch -- the bootstrap speaks ABI %u but this runtime supports ABI %u.\n"
568
+ " Refresh the runtime via tebako cache, or re-bundle with a matching tebako-bootstrap.\n",
569
+ handoff.version, TEBAKO_LAUNCHER_ABI_VERSION);
570
+ handoff_failed = true;
571
+ }
572
+ else if (handoff.images.empty()) {
573
+ printf("Tebako: launcher ABI handoff without --tebako-image -- nothing to mount\n");
574
+ handoff_failed = true;
575
+ }
576
+ else if (!resolve_launcher_images(&handoff)) {
577
+ handoff_failed = true; // resolve_launcher_images printed the reason
578
+ }
579
+ else if (data == &gfsData[0]) {
580
+ /* Root image: the one handed over for the package mount point;
581
+ fall back to the first image (mounted at the compiled-in root). */
582
+ size_t root_image = 0;
583
+ for (size_t i = 0; i < handoff.images.size(); ++i) {
584
+ if (mount_point == handoff.images[i].mount_point) {
585
+ root_image = i;
586
+ break;
587
+ }
588
+ }
589
+ data = handoff.images[root_image].window;
590
+ size = static_cast<size_t>(handoff.images[root_image].size);
591
+ root_image_offset = "0";
592
+ for (size_t i = 0; i < handoff.images.size(); ++i) {
593
+ if (i != root_image) {
594
+ extra_slots.push_back({handoff.images[i].mount_point, handoff.images[i].window, handoff.images[i].size});
595
+ }
596
+ }
597
+ }
598
+ else {
599
+ /* --tebako-run owns the root; every handed-over image mounts extra */
600
+ for (const auto& img : handoff.images) {
601
+ extra_slots.push_back({img.mount_point, img.window, img.size});
602
+ }
603
+ }
604
+ }
605
+ else if (data == &gfsData[0]) {
606
+ /*
607
+ * Incbin bundle startup: probe the own executable for a tpkg
608
+ * manifest trailer (Stage 3A, spec §4.3).
609
+ * - trailer present: mount each slot at its recorded mount point via
610
+ * the legacy memfs path with the slot's explicit offset
611
+ * - no trailer: classic incbin mount below, unchanged
612
+ * - corrupt trailer: clean startup error (spec §6), no mount at all
613
+ */
614
+ tpkg_manifest manifest;
615
+ int probe = read_self_manifest(&manifest);
616
+ if (probe < 0) {
617
+ trailer_corrupt = true;
618
+ }
619
+ else if (probe > 0) {
620
+ std::string self = self_executable_path();
621
+ int fd = open_self_executable(self);
622
+ if (fd >= 0 && read_self_image(fd) == 0) {
623
+ close(fd);
624
+ /* Root slot: the one recorded at the package mount point;
625
+ fall back to slot 0 (mounted at the compiled-in root). */
626
+ uint32_t root_slot = 0;
627
+ for (uint32_t i = 0; i < manifest.slot_count; ++i) {
628
+ if (mount_point == manifest.slots[i].mount_point) {
629
+ root_slot = i;
630
+ break;
631
+ }
632
+ }
633
+ /*
634
+ * Mount each slot through a memory window covering exactly the
635
+ * slot's image: the legacy memfs API takes an image offset but
636
+ * no image size, and DwarFS parses up to the end of the view --
637
+ * a whole-file view would let it run into the trailer. The
638
+ * slot's explicit file offset selects the window; the image
639
+ * sits at offset 0 within it.
640
+ */
641
+ data = self_image.data() + manifest.slots[root_slot].offset;
642
+ size = static_cast<size_t>(manifest.slots[root_slot].size);
643
+ root_image_offset = "0";
644
+ for (uint32_t i = 0; i < manifest.slot_count; ++i) {
645
+ if (i != root_slot) {
646
+ extra_slots.push_back({std::string(manifest.slots[i].mount_point),
647
+ self_image.data() + manifest.slots[i].offset, manifest.slots[i].size});
648
+ }
649
+ }
650
+ }
651
+ else {
652
+ if (fd >= 0) {
653
+ close(fd);
654
+ }
655
+ printf("Tebako: failed to read package image from '%s': %s\n", self.c_str(), strerror(errno));
656
+ trailer_corrupt = true;
657
+ }
658
+ }
659
+ }
660
+
661
+ if (!trailer_corrupt && !handoff_failed) {
662
+ fsret = mount_root_memfs(data, size, tebako::fs_log_level, nullptr, nullptr, nullptr, nullptr,
663
+ root_image_offset.c_str());
664
+ if (fsret == 0) {
665
+ for (const auto& slot : extra_slots) {
666
+ // mount_memfs_at_root returns the memfs table index on success,
667
+ // -1 on failure
668
+ if (mount_memfs_at_root(slot.window, static_cast<unsigned int>(slot.size), "0",
669
+ slot.mount_point.c_str()) == -1) {
670
+ printf("Tebako: failed to mount package slot at '%s'\n", slot.mount_point.c_str());
671
+ unmount_root_memfs(); // spec §6: never a partial mount
672
+ fsret = -1;
673
+ break;
674
+ }
675
+ }
676
+ }
677
+ if (fsret == 0) {
678
+ args->process_mountpoints();
679
+ args->build_arguments(mount_point.c_str(), entry_point.c_str());
680
+ *argc = args->get_argc();
681
+ *argv = args->get_argv();
682
+ ret = 0;
683
+ atexit(tebako_clean);
684
+ }
117
685
  }
118
686
  }
119
687
  catch (std::exception e) {
@@ -48,7 +48,7 @@ jobs:
48
48
  strategy:
49
49
  fail-fast: false
50
50
  matrix:
51
- os: [ macos-13, macos-14 ]
51
+ os: [ macos-14 ]
52
52
  steps:
53
53
  - name: Checkout
54
54
  uses: actions/checkout@v4
@@ -136,7 +136,7 @@ jobs:
136
136
  strategy:
137
137
  fail-fast: false
138
138
  matrix:
139
- os: [ macos-13, macos-14 ]
139
+ os: [ macos-14 ]
140
140
  steps:
141
141
  - name: Checkout
142
142
  uses: actions/checkout@v4
@@ -193,7 +193,7 @@ jobs:
193
193
  strategy:
194
194
  fail-fast: false
195
195
  matrix:
196
- os: [ macos-13, macos-14 ]
196
+ os: [ macos-14 ]
197
197
  steps:
198
198
  - name: Checkout
199
199
  uses: actions/checkout@v4
@@ -245,7 +245,7 @@ jobs:
245
245
  strategy:
246
246
  fail-fast: false
247
247
  matrix:
248
- os: [ macos-13, macos-14 ]
248
+ os: [ macos-14 ]
249
249
  steps:
250
250
  - name: Checkout
251
251
  uses: actions/checkout@v4
@@ -291,7 +291,7 @@ jobs:
291
291
 
292
292
  test-cross-brew-install:
293
293
  name: test arm-brew-setup/install
294
- runs-on: macos-13
294
+ runs-on: macos-14
295
295
  continue-on-error: true
296
296
  steps:
297
297
  - name: Checkout
@@ -315,7 +315,7 @@ jobs:
315
315
  strategy:
316
316
  fail-fast: false
317
317
  matrix:
318
- os: [ macos-13, macos-14 ]
318
+ os: [ macos-14 ]
319
319
 
320
320
  steps:
321
321
  - name: Checkout
@@ -59,9 +59,18 @@ if (CMAKE_HOST_SYSTEM_NAME MATCHES "Darwin")
59
59
  set(OPENSSL_VERSION "3")
60
60
  endif()
61
61
  set(OPENSSL_ROOT_DIR "${BREW_PREFIX}/opt/openssl@${OPENSSL_VERSION}")
62
+
63
+ # IMPORTANT: Set CMAKE_PREFIX_PATH but do NOT add Homebrew include directories
64
+ # globally. vcpkg provides our dependencies and its includes must take precedence.
65
+ # Homebrew's include paths would conflict with vcpkg packages (e.g., dwarfs).
66
+ # Only add specific Homebrew package includes when explicitly needed via target_*
67
+ # functions, not global include_directories().
68
+ #
69
+ # Previously this file had:
70
+ # include_directories("${OPENSSL_ROOT_DIR}/include")
71
+ # include_directories("${BREW_PREFIX}/include")
72
+ # These caused Homebrew's dwarfs headers to shadow vcpkg's newer dwarfs headers.
62
73
  set(CMAKE_PREFIX_PATH "${BREW_PREFIX}")
63
- include_directories("${OPENSSL_ROOT_DIR}/include")
64
- include_directories("${BREW_PREFIX}/include")
65
74
 
66
75
  # https://stackoverflow.com/questions/53877344/cannot-configure-cmake-to-look-for-homebrew-installed-version-of-bison
67
76
  find_and_set_homebrew_prefix("bison" "BISON" "BISON_EXECUTABLE")