Class: Tebako::Stitcher

Inherits:
Object
  • Object
show all
Defined in:
lib/tebako/stitcher.rb

Overview

Stitches tebako images onto a prebuilt runtime binary, producing a single-file package with a tpkg manifest trailer (spec §4.3).

Wire layout (all integers little-endian), appended after the runtime:

[runtime bytes][pad to 8][image 0][pad to 8]...[image n-1]
[slot table: slot_count x 280 bytes][trailer header: 166 bytes at EOF]

This is a pure-Ruby reimplementation of the tpkg.h writer (libtfs include/tebako/tpkg.h); the byte stream is identical to tpkg_write_fd() for the same manifest. CRC32 is the zlib polynomial (0xEDB88320, init/xorout 0xFFFFFFFF) — exactly what Zlib.crc32 computes.

Codesigning: appending bytes invalidates any embedded code signature. On macOS a signed runtime (ad-hoc included, detected via codesign -dv) is re-signed ad-hoc after stitching (codesign --remove-signature, then codesign --sign - --force). Note that codesign(1) refuses to re-sign thin Mach-O binaries carrying trailing payload ("main executable failed strict validation"), so the re-sign is best-effort: on failure a warning is printed and the package is kept — the ad-hoc linker signature is invalidated by construction, but the binary still executes on macOS (verified on macOS 14/arm64). Re-signing with a real identity remains a user post-press step. An unsigned runtime is left alone. On Windows signing is a no-op — re-applying an Authenticode signature (signtool) is left to the user as a post-press step.

Constant Summary collapse

MAGIC =

rubocop:disable Metrics/ClassLength

"TEBAKOTFS\0".b
VERSION =

10 bytes, NUL-terminated

1
MAX_SLOTS =
8
HEADER_SIZE =
166
SLOT_SIZE =
280
MOUNT_POINT_LEN =
256
RUNTIME_REF_LEN =
128
ALIGNMENT =
8
FLAG_LEAN =
0x1
FORMAT_AUTO =
0
FORMAT_DWARFS =
1
FORMAT_SQUASHFS =
2
FORMAT_ZIP =
3
FORMAT_RUNTIME =

Runtime payload slot of a fat package: installed into the shared cache by the bootstrap at first run, never mounted (tpkg.h TPKG_FORMAT_RUNTIME)

4
FORMAT_IDS =
(FORMAT_AUTO..FORMAT_RUNTIME).freeze
OFF_PACKAGE_FLAGS =

Header field offsets

14
OFF_SLOT_COUNT =
18
OFF_TABLE =
22
OFF_RUNTIME_REF =
30
OFF_CRC32 =
162

Class Method Summary collapse

Class Method Details

.adhoc_resign(path) ⇒ Object

Re-apply an ad-hoc signature after the binary was mutated; true on success. codesign(1) refuses to re-sign thin Mach-O binaries with trailing payload (strict validation) — callers treat failure as non-fatal (see the class comment).



130
131
132
133
# File 'lib/tebako/stitcher.rb', line 130

def adhoc_resign(path)
  system("codesign", "--remove-signature", path, out: File::NULL, err: File::NULL) &&
    system("codesign", "--sign", "-", "--force", path, out: File::NULL, err: File::NULL)
end

.macos?Boolean

Returns:

  • (Boolean)


114
115
116
# File 'lib/tebako/stitcher.rb', line 114

def macos?
  RbConfig::CONFIG["host_os"] =~ /darwin/i ? true : false
end

.signed?(path) ⇒ Boolean

True when path carries any code signature (ad-hoc included)

Returns:

  • (Boolean)


119
120
121
122
123
124
# File 'lib/tebako/stitcher.rb', line 119

def signed?(path)
  _out, _err, status = Open3.capture3("codesign", "-dv", path)
  status.success?
rescue Errno::ENOENT
  false
end

.stitch(runtime_path, images:, output:, lean: false, ruby_version: nil, tebako_version: Tebako::VERSION, launcher_abi: 0, runtime_sha256: nil) ⇒ Object

Stitch images (Array of mount_point:, format_id:) onto a copy of the runtime at runtime_path, writing output. lean: true marks the package LEAN and records runtime_ref ("ruby@<ruby_version>;tebako=<tebako_version>"); classic packages carry an empty runtime_ref. runtime_sha256 appends ";sha256=" to the runtime_ref — the checksum the bootstrap verifies a fat package's runtime payload slot against before installing it into the cache.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/tebako/stitcher.rb', line 98

def stitch(runtime_path, images:, output:, lean: false, ruby_version: nil, # rubocop:disable Metrics/ParameterLists
           tebako_version: Tebako::VERSION, launcher_abi: 0, runtime_sha256: nil)
  images = normalize_images(images)
  validate_inputs!(runtime_path, images, lean, ruby_version)
  validate_runtime_sha256!(runtime_sha256)

  FileUtils.mkdir_p(File.dirname(output))
  FileUtils.cp(runtime_path, output)
  FileUtils.chmod(0o755, output)

  append(images, output, package_flags(lean), runtime_ref(lean, ruby_version, tebako_version, runtime_sha256),
         launcher_abi)
  resign_if_needed(output)
  output
end