rbe-tebako 0.15.0 → 0.16.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CMakeLists.txt +19 -3
  3. data/README.adoc +201 -12
  4. data/benchmark/press.rb +130 -0
  5. data/docs/layered-package-format.md +56 -0
  6. data/docs/macos-entitlements.plist +10 -0
  7. data/format/layered_package.json +20 -0
  8. data/format/single_file_bundle.json +12 -0
  9. data/include/tebako/layered-format.h +16 -0
  10. data/include/tebako/single-file-bundle-format.h +17 -0
  11. data/lib/tebako/application_build_state.rb +124 -0
  12. data/lib/tebako/application_builder.rb +96 -0
  13. data/lib/tebako/build_commands.rb +396 -0
  14. data/lib/tebako/build_reporter.rb +110 -0
  15. data/lib/tebako/bundle_builder.rb +57 -0
  16. data/lib/tebako/cache_catalog.rb +195 -0
  17. data/lib/tebako/cli.rb +42 -3
  18. data/lib/tebako/cli_helpers.rb +42 -18
  19. data/lib/tebako/codegen.rb +60 -30
  20. data/lib/tebako/content_manifest.rb +131 -0
  21. data/lib/tebako/deploy_helper.rb +160 -18
  22. data/lib/tebako/deployment_cache.rb +226 -0
  23. data/lib/tebako/error.rb +2 -0
  24. data/lib/tebako/filesystem_cache.rb +139 -0
  25. data/lib/tebako/finalized_runtime_cache.rb +143 -0
  26. data/lib/tebako/generated/layered_format.rb +14 -0
  27. data/lib/tebako/generated/single_file_bundle_format.rb +15 -0
  28. data/lib/tebako/layer_planner.rb +140 -0
  29. data/lib/tebako/layered_package.rb +198 -0
  30. data/lib/tebako/native_gem_cache.rb +195 -0
  31. data/lib/tebako/options_manager.rb +68 -0
  32. data/lib/tebako/packager/pass2_patch.rb +6 -2
  33. data/lib/tebako/packager/patch_buildsystem.rb +34 -3
  34. data/lib/tebako/packager.rb +51 -24
  35. data/lib/tebako/packager_lite.rb +170 -6
  36. data/lib/tebako/release_store.rb +190 -0
  37. data/lib/tebako/ruby_builder.rb +110 -2
  38. data/lib/tebako/runtime_builder.rb +146 -0
  39. data/lib/tebako/runtime_descriptor.rb +142 -0
  40. data/lib/tebako/runtime_sdk.rb +345 -0
  41. data/lib/tebako/scenario_manager.rb +1 -1
  42. data/lib/tebako/single_file_bundle.rb +232 -0
  43. data/lib/tebako/single_file_bundle_builder.rb +113 -0
  44. data/lib/tebako/version.rb +1 -1
  45. data/scripts/generate_layered_format.rb +95 -0
  46. data/src/tebako-main.cpp +277 -6
  47. metadata +31 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fad34d9072105f46cae716b4de5481d747661dca815b24a935fc8f5cad7036e5
4
- data.tar.gz: 9714907bfc2b6e2daf38f691c690e3c522efdbd3625c81594b70494c68d6cd6b
3
+ metadata.gz: 3d051f3caca43f3080ba1793827fe2390b7a9410ed53cd8c5426856c70592cd9
4
+ data.tar.gz: 9466ae396ba0171287dd792fb99cc58995aa76b750772b146f92687f862e28e1
5
5
  SHA512:
6
- metadata.gz: 2ec48c03294d5bc456cf0235f2b06e294d3911c746dadf9fda18aea2c6e857183cf5ca6e9c7c87c992e38a3e75720abdc372f2fd9a95ea9435aa9a19dbd58e12
7
- data.tar.gz: 99413e19ef3c98d3fefbddf3476b94c9f136286b8e5ab427af380afe5ae0e6d51e2a10db919110e201c1077a5609d5ffdd7f315aa0bb3243ce364e4c2ae00be9
6
+ metadata.gz: d3bcdf7fd8770090d25ca313808091f3785c5fbac4015af47a6bd745acaae341efcd9381225eeae1dd1d986cfcb4ad895d92c4215586e338c1127be8a9004c15
7
+ data.tar.gz: 6151c0a0d960ef89d768a3510400a6bd4ba2fa01b4756e224222e026cc776ad2ddf17978bc9b561a230251c6ffa191ec0833c31cea717cd0db499507b1c705c8
data/CMakeLists.txt CHANGED
@@ -436,12 +436,23 @@ if (${SETUP_MODE})
436
436
  # ...................................................................
437
437
  # Packaged filesystem
438
438
 
439
- add_custom_target(packaged_filesystem
439
+ file(GLOB_RECURSE TEBAKO_PACKAGER_SOURCES CONFIGURE_DEPENDS
440
+ ${CMAKE_CURRENT_SOURCE_DIR}/lib/tebako/*.rb
441
+ )
442
+
443
+ add_custom_command(
444
+ OUTPUT ${DATA_BIN_FILE}
440
445
  COMMAND ruby ${DEPS_BIN_DIR}/deploy.rb ${RUBY_VER}
441
- DEPENDS setup
442
- BYPRODUCTS ${DATA_BIN_FILE}
446
+ DEPENDS ${DEPS_BIN_DIR}/deploy.rb ${PACKAGE_MANIFEST} ${TEBAKO_PACKAGER_SOURCES}
447
+ COMMENT "Building packaged filesystem from changed inputs"
448
+ VERBATIM
443
449
  )
444
450
 
451
+ add_custom_target(packaged_filesystem
452
+ DEPENDS ${DATA_BIN_FILE}
453
+ )
454
+ add_dependencies(packaged_filesystem setup)
455
+
445
456
  set(CMAKE_CXX_FLAGS "${RUBY_C_FLAGS}")
446
457
 
447
458
  add_library(tebako-fs STATIC
@@ -452,6 +463,11 @@ if (${SETUP_MODE})
452
463
  ${DEPS_INCLUDE_DIR}/tebako/tebako-version.h
453
464
  )
454
465
 
466
+ set_source_files_properties(
467
+ ${DEPS_SRC_DIR}/tebako/tebako-fs.cpp
468
+ PROPERTIES OBJECT_DEPENDS ${DATA_BIN_FILE}
469
+ )
470
+
455
471
  if(${RUBY_VER} VERSION_LESS "3.3.0" AND ("${OSTYPE_TXT}" MATCHES "^msys*" OR "${OSTYPE_TXT}" MATCHES "^cygwin*"))
456
472
  target_compile_definitions(tebako-fs PUBLIC RB_W32_PRE_33)
457
473
  endif(${RUBY_VER} VERSION_LESS "3.3.0" AND ("${OSTYPE_TXT}" MATCHES "^msys*" OR "${OSTYPE_TXT}" MATCHES "^cygwin*"))
data/README.adoc CHANGED
@@ -294,7 +294,7 @@ single, performant, executable binary.
294
294
  - Packages all files and dependencies into a single binary
295
295
  - Supports multiple operating systems and architectures
296
296
  - Provides efficient packaging and execution with DwarFS
297
- - Offers security features like signing on macOS
297
+ - Supports macOS signing with the legacy embedded bundle format
298
298
  - Simplifies distribution and deployment
299
299
  - Native running speed
300
300
  |
@@ -403,8 +403,9 @@ the packaging environment and collects the required dependencies. Subsequent
403
403
  invocations are much faster.
404
404
  ====
405
405
 
406
- Upon the next invocation, Tebako will use previously created packaging
407
- environment. The press process itself takes minutes.
406
+ Upon the next invocation, Tebako reuses its packaging environment and
407
+ content-addressed artifacts. An unchanged bundle normally returns after
408
+ verification, and an application-only edit reuses the finalized Ruby runtime.
408
409
 
409
410
  You can manage setup of packaging environment manually; please refer to
410
411
  description of setup and clean commands below.
@@ -416,9 +417,15 @@ tebako press \
416
417
  -r|--root=<project-root-folder> \
417
418
  [-p|--prefix=<tebako-root-folder>] \
418
419
  [-R|--Ruby=<ruby-version>] \
419
- [-o|--output=<packaged-file-name>] \
420
- [-l|--log-level=<error|warn|debug|trace>] \
421
- [-c|--cwd=<package-current-working-directory>] \
420
+ [-o|--output=<packaged-file-name>] \
421
+ [-l|--log-level=<error|warn|debug|trace>] \
422
+ [--compression-level=<0..9>] \
423
+ [--[no-]deployment-cache] \
424
+ [--explain] \
425
+ [--report=json] \
426
+ [--layer-strategy=<coarse|semantic>] \
427
+ [--bundle-format=<layered|legacy>] \
428
+ [-c|--cwd=<package-current-working-directory>] \
422
429
  [-D|--devmode] \
423
430
  [-P|--patchelf] \
424
431
  [-m|--mode=<bundle|both|application|runtime>] \
@@ -470,8 +477,82 @@ and the application package is named according to the `-o` option with the `.teb
470
477
  (optional, defaults to `error`)
471
478
  logging level for the Tebako built-in memory filesystem driver.
472
479
 
473
- `--cwd=<package-current-working-directory>`::
474
- (optional)
480
+ `--compression-level=<0..9>`::
481
+ (optional, defaults to `5`)
482
+ DwarFS compression level for the packaged filesystem. Level `0` prioritizes
483
+ packaging speed, while level `9` prioritizes the smallest package size.
484
+
485
+ When an application has a `Gemfile`, Tebako caches its installed gems under the
486
+ Tebako prefix. Later presses restore the matching dependency set and use
487
+ `bundle check`, running `bundle install` only when gems are missing. The cache
488
+ key includes the lockfile when one exists, plus the Gemfile, gemspec, Ruby ABI,
489
+ platform, Bundler configuration, and native build inputs.
490
+
491
+ `--deployment-cache`::
492
+ (optional, enabled by default)
493
+ Restores a verified, content-addressed deployment tree when the application,
494
+ lockfile, Ruby ABI, platform, and packaging inputs are unchanged. Use
495
+ `--no-deployment-cache` to force deployment work.
496
+
497
+ `--explain`::
498
+ (optional)
499
+ Prints why each fast-build stage was rebuilt or reused.
500
+
501
+ `--report=json`::
502
+ (optional)
503
+ Emits the same build decisions as structured JSON for benchmarks and CI.
504
+
505
+ `--layer-strategy=<coarse|semantic>`::
506
+ (optional, defaults to `coarse`)
507
+ Controls application-layer planning where the selected runtime supports
508
+ multiple secondary filesystems. The current default runtime uses one
509
+ application capsule because its filesystem wrapper supports one secondary
510
+ mount. The planner remains available for runtimes that advertise wider mount
511
+ support.
512
+
513
+ `--bundle-format=<layered|legacy>`::
514
+ (optional, defaults to `layered`)
515
+ Selects the internal representation of a one-file `bundle`.
516
+ +
517
+ `layered` reuses a verified finalized Ruby runtime and combines it with one
518
+ checksummed, compressed application capsule. Linux and Windows store the
519
+ capsule in a versioned trailer. macOS performs a targeted final link that
520
+ places the capsule in the `__TEBAKO,__app` Mach-O section, allowing the complete
521
+ executable to be signed. Application edits never recompile the Ruby runtime.
522
+ The deployed result is still exactly one executable and does not need Ruby on
523
+ the destination machine.
524
+ +
525
+ `legacy` embeds the complete filesystem during the native link. It is slower
526
+ for application edits but remains available for compatibility.
527
+ +
528
+ Layered macOS bundles support normal `codesign` replacement and strict
529
+ verification. Sign with Hardened Runtime and Tebako's supplied entitlements:
530
+ +
531
+ [source,shell]
532
+ ----
533
+ codesign --force --timestamp --options runtime \
534
+ --entitlements docs/macos-entitlements.plist \
535
+ --sign "Developer ID Application: YOUR NAME (TEAMID)" \
536
+ ./my-tebako-app
537
+
538
+ codesign --verify --deep --strict --verbose=4 ./my-tebako-app
539
+ ----
540
+ +
541
+ Ruby's YJIT allocates executable memory without `MAP_JIT`, which requires
542
+ `com.apple.security.cs.allow-unsigned-executable-memory` under Hardened Runtime.
543
+ Packaged native gems are extracted as independently ad-hoc-signed `.bundle`
544
+ files, which requires `com.apple.security.cs.disable-library-validation`.
545
+ The supplied link:docs/macos-entitlements.plist[macOS entitlements file]
546
+ contains those two runtime exceptions.
547
+ +
548
+ Release CI should then submit a ZIP, DMG, or package containing the signed
549
+ executable to `notarytool`, staple the accepted artifact where that container
550
+ supports stapling, and execute the distributed result. Developer ID signing and
551
+ the Apple notary service require project credentials and therefore remain
552
+ release-CI checks.
553
+
554
+ `--cwd=<package-current-working-directory>`::
555
+ (optional)
475
556
  A folder within Tebako memfs where the packaged application will start. This
476
557
  folder should be specified relative to the memfs root.
477
558
  +
@@ -542,10 +623,118 @@ tebako press \
542
623
  --root='~/projects/myproject' \
543
624
  --entry=start.rb \
544
625
  --output=/temp/myproject.tebako
545
- ----
546
- ====
547
-
548
- ==== Setup
626
+ ----
627
+ ====
628
+
629
+ ===== Incremental presses
630
+
631
+ Tebako keeps content-addressed DwarFS images under the Tebako prefix. Cache keys
632
+ include file contents and modes, the DwarFS writer, the compression level, and
633
+ package metadata; timestamps alone do not invalidate an image. Generated files
634
+ are updated only when their contents change, and CMake rebuilds or relinks the
635
+ runtime only when the packaged filesystem or native inputs change.
636
+
637
+ In the default `bundle` path, Tebako reuses an immutable finalized runtime and
638
+ compresses the deployed app, executables, gems, and package metadata into one
639
+ application capsule. The capsule is appended with a versioned, checksummed
640
+ footer, producing exactly one executable. An application-only edit therefore
641
+ does not rebuild the runtime filesystem or relink Ruby.
642
+ +
643
+ `both` mode uses the same capsule representation for its separate application
644
+ package. `application` mode continues to emit a monolithic image so it remains
645
+ compatible with previously built runtime executables.
646
+
647
+ The installed DwarFS writer does not expose a persistent compressed-chunk cache.
648
+ Layer-sized, content-addressed images therefore provide incremental compression
649
+ without changing the DwarFS format itself.
650
+
651
+ To compare warm, unchanged, and application-changed presses on the same machine,
652
+ run:
653
+
654
+ [source,sh]
655
+ ----
656
+ ruby benchmark/press.rb \
657
+ --root tests/test-00 \
658
+ --entry test.rb \
659
+ --prefix PWD \
660
+ --mode bundle
661
+ ----
662
+
663
+ The command emits JSON timings, cache-hit counts, native-link reuse, and a
664
+ runtime validation result. Use `--mode bundle` to measure the primary
665
+ single-executable workflow or `--mode both` to measure split runtime/application
666
+ packaging. Keep the mode, Ruby version, compression level, prefix, machine load,
667
+ and application fixed when comparing revisions.
668
+
669
+ ===== Explicit fast-build commands
670
+
671
+ The runtime and application phases can be invoked independently so application
672
+ edits never enter the runtime build:
673
+
674
+ [source,sh]
675
+ ----
676
+ rbe-tebako runtime build \
677
+ --Ruby=4.0.6 \
678
+ --output=build/tebako-runtime \
679
+ --sdk-output=build/tebako-runtime-sdk.tar.gz
680
+
681
+ SDK_SHA256="$(cut -d' ' -f1 build/tebako-runtime-sdk.tar.gz.sha256)"
682
+ rbe-tebako runtime install build/tebako-runtime-sdk.tar.gz \
683
+ --destination=$HOME/.tebako/runtime-sdk \
684
+ --sha256="$SDK_SHA256"
685
+
686
+ rbe-tebako application build \
687
+ --prefix=$HOME/.tebako/runtime-sdk \
688
+ --runtime=$HOME/.tebako/runtime-sdk/runtime/tebako-runtime \
689
+ --root=. \
690
+ --entry-point=bin/my-app \
691
+ --output=build/my-app \
692
+ --explain
693
+ ----
694
+
695
+ `runtime build --sdk-output` writes the SDK and a conventional adjacent
696
+ `.sha256` checksum file. Distribute or pin that digest through a trusted channel;
697
+ a checksum downloaded from the same untrusted location as the SDK provides
698
+ corruption detection, not publisher authentication.
699
+ +
700
+ SDK installation checks the trusted SHA-256 before extracting anything, verifies
701
+ the archive manifest and individual file checksums, and atomically replaces the
702
+ selected installation. It relocates text-based build configuration from the
703
+ producer's prefix to the installation prefix, registers the packaged runtime in
704
+ the finalized-runtime cache, and includes the prepared Ruby link tree needed for
705
+ fast signable single-file assembly on macOS.
706
+ +
707
+ Set `TEBAKO_PREFIX` to the installation root to make ordinary `press` commands
708
+ reuse the SDK runtime without adding SDK-specific arguments:
709
+
710
+ [source,sh]
711
+ ----
712
+ export TEBAKO_PREFIX=$HOME/.tebako/runtime-sdk
713
+ rbe-tebako press \
714
+ --Ruby=4.0.6 \
715
+ --root=. \
716
+ --entry-point=bin/my-app \
717
+ --output=build/my-app \
718
+ --explain
719
+ ----
720
+
721
+ The runtime is reused only when its descriptor matches the current platform,
722
+ architecture, Ruby version, Tebako source, compression and logging options, and
723
+ native build environment. A mismatch is safe: Tebako builds and caches a
724
+ compatible runtime instead.
725
+ +
726
+ Tebako also exposes cache and package diagnostics:
727
+
728
+ [source,sh]
729
+ ----
730
+ rbe-tebako cache stats
731
+ rbe-tebako cache verify
732
+ rbe-tebako cache prune --max-size=20GB
733
+ rbe-tebako package inspect build/my-app.tebako
734
+ rbe-tebako package verify build/my-app.tebako
735
+ ----
736
+
737
+ ==== Setup
549
738
 
550
739
  Tebako requires post-install setup after gem installation .
551
740
  Post-intall setup is called automatically during the first packaging.
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require "json"
6
+ require "open3"
7
+ require "optparse"
8
+ require "rbconfig"
9
+ require "tmpdir"
10
+
11
+ # Measures warm, unchanged, and application-changed Tebako presses.
12
+ class PressBenchmark
13
+ def initialize(options)
14
+ @options = options
15
+ @repo = File.expand_path("..", __dir__)
16
+ end
17
+
18
+ def run
19
+ Dir.mktmpdir("tebako-press-benchmark") do |directory|
20
+ puts JSON.pretty_generate(results_for(directory))
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def results_for(directory)
27
+ root, output = prepare_workspace(directory)
28
+ results = { mode: @options[:mode], warm: press(root, output), unchanged: press(root, output) }
29
+ add_probe(root)
30
+ results[:application_changed] = press(root, output)
31
+ results[:runtime_validation] = validate(output) unless @options[:skip_run]
32
+ add_comparisons(results)
33
+ results
34
+ end
35
+
36
+ def prepare_workspace(directory)
37
+ root = File.join(directory, "application")
38
+ FileUtils.cp_r(File.join(File.expand_path(@options[:root]), "."), root)
39
+ [root, File.join(directory, "tebako-benchmark")]
40
+ end
41
+
42
+ def add_probe(root)
43
+ timestamp = Process.clock_gettime(Process::CLOCK_REALTIME).to_s
44
+ File.write(File.join(root, ".tebako-benchmark-probe"), timestamp)
45
+ end
46
+
47
+ def add_comparisons(results)
48
+ warm = results[:warm][:seconds]
49
+ results[:comparisons] = {
50
+ unchanged_vs_warm: ratio(results[:unchanged][:seconds], warm),
51
+ application_changed_vs_warm: ratio(results[:application_changed][:seconds], warm)
52
+ }
53
+ end
54
+
55
+ def ratio(value, baseline)
56
+ (value / baseline).round(3)
57
+ end
58
+
59
+ def press(root, output)
60
+ command = press_command(root, output)
61
+ warn "Running #{command.join(" ")}"
62
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
63
+ log, status = Open3.capture2e(*command, chdir: @repo)
64
+ abort log unless status.success?
65
+
66
+ press_result(log, Process.clock_gettime(Process::CLOCK_MONOTONIC) - started)
67
+ end
68
+
69
+ def press_result(log, elapsed)
70
+ report = parse_report(log)
71
+ {
72
+ seconds: elapsed.round(3),
73
+ reused_filesystems: log.scan("reusing packaged filesystem").length,
74
+ reused_native_link: log.include?("reusing Ruby executable"),
75
+ rebuilt_runtime_filesystem: log.include?("Building packaged filesystem from changed inputs"),
76
+ stages: report.fetch("events", []).map do |event|
77
+ event.slice("stage", "status", "duration_seconds", "reason", "details")
78
+ end
79
+ }
80
+ end
81
+
82
+ def parse_report(log)
83
+ line = log.lines.reverse.find { |candidate| candidate.start_with?("{\"schema_version\"") }
84
+ line ? JSON.parse(line) : {}
85
+ rescue JSON::ParserError
86
+ {}
87
+ end
88
+
89
+ def press_command(root, output)
90
+ [
91
+ RbConfig.ruby, "-I#{File.join(@repo, "lib")}", File.join(@repo, "exe", "rbe-tebako"),
92
+ "press", "--mode", @options[:mode], "--prefix", @options[:prefix], "--Ruby", @options[:ruby],
93
+ "--root", root, "--entry-point", @options[:entry], "--output", output,
94
+ "--compression-level", @options[:compression].to_s, "--report", "json"
95
+ ]
96
+ end
97
+
98
+ def validate(output)
99
+ command = @options[:mode] == "bundle" ? [output] : [output, "--tebako-run", "#{output}.tebako"]
100
+ log, status = Open3.capture2e(*command)
101
+ { success: status.success?, output: log }
102
+ end
103
+ end
104
+
105
+ options = {
106
+ root: File.expand_path("../tests/test-00", __dir__),
107
+ entry: "test.rb",
108
+ prefix: "PWD",
109
+ ruby: "4.0.6",
110
+ compression: 5,
111
+ mode: "both",
112
+ skip_run: false
113
+ }
114
+
115
+ OptionParser.new do |parser|
116
+ parser.banner = "Usage: benchmark/press.rb [options]"
117
+ parser.on("--root PATH", "Application root (default: tests/test-00)") { |value| options[:root] = value }
118
+ parser.on("--entry PATH", "Entry point relative to root (default: test.rb)") { |value| options[:entry] = value }
119
+ parser.on("--prefix PATH", "Tebako prefix (default: PWD)") { |value| options[:prefix] = value }
120
+ parser.on("--ruby VERSION", "Packaged Ruby version (default: 4.0.6)") { |value| options[:ruby] = value }
121
+ parser.on("--mode MODE", %w[bundle both], "Package mode: bundle or both (default: both)") do |value|
122
+ options[:mode] = value
123
+ end
124
+ parser.on("--compression N", Integer, "DwarFS compression level (default: 5)") do |value|
125
+ options[:compression] = value
126
+ end
127
+ parser.on("--skip-run", "Do not execute the resulting package") { options[:skip_run] = true }
128
+ end.parse!
129
+
130
+ PressBenchmark.new(options).run
@@ -0,0 +1,56 @@
1
+ # Tebako layered application package format
2
+
3
+ The source of truth for fixed fields is
4
+ [`format/layered_package.json`](../format/layered_package.json). Run
5
+ `ruby scripts/generate_layered_format.rb` after changing it.
6
+
7
+ Version 1.0 uses little-endian integers and ends with the eight-byte magic
8
+ `TEBAKOL1`. The version is encoded in that magic; readers must reject unknown
9
+ magic values rather than guessing a compatible layout.
10
+
11
+ The package layout is:
12
+
13
+ 1. Tebako application descriptor.
14
+ 2. One or more DwarFS layer payloads.
15
+ 3. Layer manifest.
16
+ 4. Footer.
17
+
18
+ The footer contains a `uint64` manifest byte length followed by the magic.
19
+ The manifest starts with a `uint32` layer count. Each record contains a
20
+ `uint16` UTF-8 mount-point length, the mount-point bytes, a `uint64` payload
21
+ offset, and a `uint64` payload length.
22
+
23
+ Mount points are relative, non-empty paths. Empty components and the `.` and
24
+ `..` components are forbidden. Mount points must be unique. Payload ranges
25
+ must be non-empty, must end before the manifest, and must not overlap.
26
+
27
+ Version 1 does not carry embedded payload checksums. `tebako package verify`
28
+ therefore validates its complete structure, bounds, and mount rules and reports
29
+ computed SHA-256 identities for inspection; it cannot authenticate a version 1
30
+ payload against an embedded digest. A checksum-bearing format requires a new
31
+ magic/version and corresponding runtime reader support.
32
+
33
+ ## One-file executable containers
34
+
35
+ A layered package is wrapped in a single-file envelope containing the package
36
+ bytes, a little-endian `uint64` package length, a 32-byte SHA-256 digest, and
37
+ the eight-byte `TEBAKOB1` magic.
38
+
39
+ Linux and Windows place that envelope after the native executable. macOS places
40
+ the same envelope in the `__TEBAKO,__app` section of a dedicated `__TEBAKO`
41
+ Mach-O segment. The section is created during a targeted final link so it is
42
+ part of the signed Mach-O rather than unstructured trailing data. The runtime
43
+ locates the section through the loaded Mach-O header, validates the envelope
44
+ digest, and then parses the layered package.
45
+
46
+ The macOS link reuses the already-built Ruby objects, static extensions, and
47
+ runtime libraries. It does not compile Ruby or regenerate the runtime
48
+ filesystem. The resulting executable supports signature replacement followed
49
+ by `codesign --verify --deep --strict`.
50
+
51
+ Hardened Runtime signing must use `docs/macos-entitlements.plist`. Ruby's YJIT
52
+ requires executable anonymous memory, and native gem `.bundle` files are
53
+ extracted with independent ad-hoc signatures. The signing profile therefore
54
+ allows unsigned executable memory and disables library validation. Both pure
55
+ Ruby and native-gem fixtures are exercised with that profile; credentialed
56
+ Developer ID signing and notarization belong in release CI.
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
6
+ <true/>
7
+ <key>com.apple.security.cs.disable-library-validation</key>
8
+ <true/>
9
+ </dict>
10
+ </plist>
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "Tebako layered application package",
3
+ "major_version": 1,
4
+ "minor_version": 0,
5
+ "magic": "TEBAKOL1",
6
+ "endianness": "little",
7
+ "footer": [
8
+ ["manifest_size", "uint64"],
9
+ ["magic", "bytes[8]"]
10
+ ],
11
+ "manifest_header": [
12
+ ["layer_count", "uint32"]
13
+ ],
14
+ "layer_record": [
15
+ ["mount_point_size", "uint16"],
16
+ ["mount_point", "bytes[mount_point_size]"],
17
+ ["payload_offset", "uint64"],
18
+ ["payload_size", "uint64"]
19
+ ]
20
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "Tebako single-file layered bundle",
3
+ "major_version": 1,
4
+ "minor_version": 0,
5
+ "magic": "TEBAKOB1",
6
+ "endianness": "little",
7
+ "footer": [
8
+ ["application_size", "uint64"],
9
+ ["application_sha256", "bytes[32]"],
10
+ ["magic", "bytes[8]"]
11
+ ]
12
+ }
@@ -0,0 +1,16 @@
1
+ // Generated by scripts/generate_layered_format.rb. Do not edit.
2
+ #ifndef TEBAKO_LAYERED_FORMAT_H
3
+ #define TEBAKO_LAYERED_FORMAT_H
4
+
5
+ #include <cstddef>
6
+ #include <cstdint>
7
+
8
+ namespace tebako::layered_format {
9
+ inline constexpr std::uint16_t major_version = 1;
10
+ inline constexpr std::uint16_t minor_version = 0;
11
+ inline constexpr char magic[] = "TEBAKOL1";
12
+ inline constexpr std::size_t magic_size = sizeof(magic) - 1;
13
+ inline constexpr std::size_t footer_size = sizeof(std::uint64_t) + magic_size;
14
+ } // namespace tebako::layered_format
15
+
16
+ #endif
@@ -0,0 +1,17 @@
1
+ // Generated by scripts/generate_layered_format.rb. Do not edit.
2
+ #ifndef TEBAKO_SINGLE_FILE_BUNDLE_FORMAT_H
3
+ #define TEBAKO_SINGLE_FILE_BUNDLE_FORMAT_H
4
+
5
+ #include <cstddef>
6
+ #include <cstdint>
7
+
8
+ namespace tebako::single_file_bundle_format {
9
+ inline constexpr std::uint16_t major_version = 1;
10
+ inline constexpr std::uint16_t minor_version = 0;
11
+ inline constexpr char magic[] = "TEBAKOB1";
12
+ inline constexpr std::size_t magic_size = sizeof(magic) - 1;
13
+ inline constexpr std::size_t digest_size = 32;
14
+ inline constexpr std::size_t footer_size = sizeof(std::uint64_t) + digest_size + magic_size;
15
+ } // namespace tebako::single_file_bundle_format
16
+
17
+ #endif